mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-09-12 03:24:23 +02:00
feat: add URI filename support
This commit is contained in:
parent
c479ca0f44
commit
37ef98352a
5 changed files with 308 additions and 48 deletions
41
docs/cli.rst
41
docs/cli.rst
|
|
@ -11,6 +11,41 @@ Once :ref:`installed <installation>` the tool should be available as ``sqlite-ut
|
|||
.. contents:: :local:
|
||||
:class: this-will-duplicate-information-and-it-is-still-useful-here
|
||||
|
||||
.. _cli_uri_filenames:
|
||||
|
||||
URI filenames
|
||||
=============
|
||||
|
||||
``sqlite-utils`` supports `SQLite URI filenames <https://www.sqlite.org/uri.html>`__, which allow you to open databases with special parameters.
|
||||
|
||||
A URI filename starts with ``file:`` and can include query parameters to control database behavior:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sqlite-utils tables 'file:data.db?mode=ro'
|
||||
|
||||
Common URI parameters include:
|
||||
|
||||
- ``mode=ro`` - Open database read-only
|
||||
- ``mode=rw`` - Open read-write (default)
|
||||
- ``mode=rwc`` - Open read-write and create if it doesn't exist
|
||||
- ``mode=memory`` - Open as an in-memory database
|
||||
- ``immutable=1`` - Open as immutable (SQLite will not try to modify the file)
|
||||
|
||||
Example opening a database as read-only and immutable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sqlite-utils query 'file:data.db?mode=ro&immutable=1' "select * from items"
|
||||
|
||||
This is particularly useful for:
|
||||
|
||||
- Safely querying databases without risk of modification
|
||||
- Opening databases on read-only filesystems
|
||||
- Sharing databases between multiple processes with ``immutable=1``
|
||||
|
||||
URI filenames work with all ``sqlite-utils`` commands that accept database paths.
|
||||
|
||||
.. _cli_query:
|
||||
|
||||
Running SQL queries
|
||||
|
|
@ -504,7 +539,7 @@ By default, ``sqlite-utils memory`` will attempt to detect the incoming data for
|
|||
You can instead specify an explicit format by adding a ``:csv``, ``:tsv``, ``:json`` or ``:nl`` (for newline-delimited JSON) suffix to the filename. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
sqlite-utils memory one.dat:csv two.dat:nl \
|
||||
"select * from one union select * from two"
|
||||
|
||||
|
|
@ -1153,7 +1188,7 @@ You can also pipe ``sqlite-utils`` together to create a new SQLite database file
|
|||
sqlite-utils sf-trees.db \
|
||||
"select TreeID, qAddress, Latitude, Longitude from Street_Tree_List" --nl \
|
||||
| sqlite-utils insert saved.db trees - --nl
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sqlite-utils saved.db "select * from trees limit 5" --csv
|
||||
|
|
@ -2729,7 +2764,7 @@ You can convert an existing table to a geographic table by adding a geometry col
|
|||
|
||||
The table (``locations`` in the example above) must already exist before adding a geometry column. Use ``sqlite-utils create-table`` first, then ``add-geometry-column``.
|
||||
|
||||
Use the ``--type`` option to specify a geometry type. By default, ``add-geometry-column`` uses a generic ``GEOMETRY``, which will work with any type, though it may not be supported by some desktop GIS applications.
|
||||
Use the ``--type`` option to specify a geometry type. By default, ``add-geometry-column`` uses a generic ``GEOMETRY``, which will work with any type, though it may not be supported by some desktop GIS applications.
|
||||
|
||||
Eight (case-insensitive) types are allowed:
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,16 @@ Instead of a file path you can pass in an existing SQLite connection:
|
|||
|
||||
db = Database(sqlite3.connect("my_database.db"))
|
||||
|
||||
You can also use `SQLite URI filenames <https://www.sqlite.org/uri.html>`__ to open databases with special parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Open database in read-only mode
|
||||
db = Database("file:data.db?mode=ro")
|
||||
|
||||
# Open as read-only and immutable
|
||||
db = Database("file:data.db?mode=ro&immutable=1")
|
||||
|
||||
If you want to create an in-memory database, you can do so like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
|||
|
|
@ -55,6 +55,37 @@ It's often worth trying: --encoding=latin-1
|
|||
maximize_csv_field_size_limit()
|
||||
|
||||
|
||||
class DatabasePath(click.Path):
|
||||
"""
|
||||
Custom Click parameter type for database paths that supports SQLite URI filenames.
|
||||
|
||||
URIs (starting with 'file:') skip file existence validation and are passed
|
||||
directly to sqlite3.connect() with uri=True.
|
||||
|
||||
See: https://www.sqlite.org/uri.html
|
||||
"""
|
||||
def __init__(self, exists=False, **kwargs):
|
||||
# Store original exists parameter for URI detection
|
||||
self._check_exists = exists
|
||||
# Always pass exists=False to parent to skip validation
|
||||
# We'll do our own validation for non-URI paths
|
||||
super().__init__(exists=False, **kwargs)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
# If it's a URI (starts with "file:"), skip existence check
|
||||
if isinstance(value, str) and value.startswith("file:"):
|
||||
return value
|
||||
|
||||
# For non-URI paths, do normal path validation
|
||||
if self._check_exists:
|
||||
# Create a temporary Path validator with exists=True
|
||||
validator = click.Path(exists=True, file_okay=self.file_okay,
|
||||
dir_okay=self.dir_okay, allow_dash=self.allow_dash)
|
||||
return validator.convert(value, param, ctx)
|
||||
|
||||
return super().convert(value, param, ctx)
|
||||
|
||||
|
||||
class CaseInsensitiveChoice(click.Choice):
|
||||
def __init__(self, choices):
|
||||
super().__init__([choice.lower() for choice in choices])
|
||||
|
|
@ -125,7 +156,7 @@ def cli():
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
|
|
@ -223,7 +254,7 @@ def tables(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
|
|
@ -288,7 +319,7 @@ def views(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1)
|
||||
|
|
@ -316,7 +347,7 @@ def optimize(path, tables, no_vacuum, load_extension):
|
|||
@cli.command(name="rebuild-fts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1)
|
||||
|
|
@ -341,7 +372,7 @@ def rebuild_fts(path, tables, load_extension):
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("names", nargs=-1)
|
||||
|
|
@ -367,7 +398,7 @@ def analyze(path, names):
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
def vacuum(path):
|
||||
|
|
@ -384,7 +415,7 @@ def vacuum(path):
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@load_extension_option
|
||||
|
|
@ -405,7 +436,7 @@ def dump(path, load_extension):
|
|||
@cli.command(name="add-column")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -471,7 +502,7 @@ def add_column(
|
|||
@cli.command(name="add-foreign-key")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -505,7 +536,7 @@ def add_foreign_key(
|
|||
@cli.command(name="add-foreign-keys")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("foreign_key", nargs=-1)
|
||||
|
|
@ -539,7 +570,7 @@ def add_foreign_keys(path, foreign_key, load_extension):
|
|||
@cli.command(name="index-foreign-keys")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@load_extension_option
|
||||
|
|
@ -560,7 +591,7 @@ def index_foreign_keys(path, load_extension):
|
|||
@cli.command(name="create-index")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -616,7 +647,7 @@ def create_index(
|
|||
@cli.command(name="enable-fts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -670,7 +701,7 @@ def enable_fts(
|
|||
@cli.command(name="populate-fts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -692,7 +723,7 @@ def populate_fts(path, table, column, load_extension):
|
|||
@cli.command(name="disable-fts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -714,7 +745,7 @@ def disable_fts(path, table, load_extension):
|
|||
@click.argument(
|
||||
"path",
|
||||
nargs=-1,
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@load_extension_option
|
||||
|
|
@ -736,7 +767,7 @@ def enable_wal(path, load_extension):
|
|||
@click.argument(
|
||||
"path",
|
||||
nargs=-1,
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@load_extension_option
|
||||
|
|
@ -757,7 +788,7 @@ def disable_wal(path, load_extension):
|
|||
@cli.command(name="enable-counts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1)
|
||||
|
|
@ -786,7 +817,7 @@ def enable_counts(path, tables, load_extension):
|
|||
@cli.command(name="reset-counts")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@load_extension_option
|
||||
|
|
@ -854,7 +885,7 @@ def insert_upsert_options(*, require_pk=False):
|
|||
(
|
||||
click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
),
|
||||
click.argument("table"),
|
||||
|
|
@ -1362,7 +1393,7 @@ def upsert(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("sql")
|
||||
|
|
@ -1447,7 +1478,7 @@ def bulk(
|
|||
@cli.command(name="create-database")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
|
|
@ -1483,7 +1514,7 @@ def create_database(path, enable_wal, init_spatialite, load_extension):
|
|||
@cli.command(name="create-table")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -1594,7 +1625,7 @@ def create_table(
|
|||
@cli.command(name="duplicate")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -1617,7 +1648,7 @@ def duplicate(path, table, new_table, ignore, load_extension):
|
|||
@cli.command(name="rename-table")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -1642,7 +1673,7 @@ def rename_table(path, table, new_name, ignore, load_extension):
|
|||
@cli.command(name="drop-table")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -1667,7 +1698,7 @@ def drop_table(path, table, ignore, load_extension):
|
|||
@cli.command(name="create-view")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("view")
|
||||
|
|
@ -1712,7 +1743,7 @@ def create_view(path, view, select, ignore, replace, load_extension):
|
|||
@cli.command(name="drop-view")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("view")
|
||||
|
|
@ -1737,13 +1768,13 @@ def drop_view(path, view, ignore, load_extension):
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("sql")
|
||||
@click.option(
|
||||
"--attach",
|
||||
type=(str, click.Path(file_okay=True, dir_okay=False, allow_dash=False)),
|
||||
type=(str, DatabasePath(file_okay=True, dir_okay=False, allow_dash=False)),
|
||||
multiple=True,
|
||||
help="Additional databases to attach - specify alias and filepath",
|
||||
)
|
||||
|
|
@ -1826,7 +1857,7 @@ def query(
|
|||
)
|
||||
@click.option(
|
||||
"--attach",
|
||||
type=(str, click.Path(file_okay=True, dir_okay=False, allow_dash=False)),
|
||||
type=(str, DatabasePath(file_okay=True, dir_okay=False, allow_dash=False)),
|
||||
multiple=True,
|
||||
help="Additional databases to attach - specify alias and filepath",
|
||||
)
|
||||
|
|
@ -1859,7 +1890,7 @@ def query(
|
|||
@click.option("--dump", is_flag=True, help="Dump SQL for in-memory database")
|
||||
@click.option(
|
||||
"--save",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
help="Save in-memory database to this file",
|
||||
)
|
||||
@click.option(
|
||||
|
|
@ -2077,7 +2108,7 @@ def _execute_query(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("dbtable")
|
||||
|
|
@ -2174,7 +2205,7 @@ def search(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("dbtable")
|
||||
|
|
@ -2260,7 +2291,7 @@ def rows(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1)
|
||||
|
|
@ -2313,7 +2344,7 @@ def triggers(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1)
|
||||
|
|
@ -2380,7 +2411,7 @@ def indexes(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("tables", nargs=-1, required=False)
|
||||
|
|
@ -2409,7 +2440,7 @@ def schema(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -2538,7 +2569,7 @@ def transform(
|
|||
@cli.command()
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -2584,7 +2615,7 @@ def extract(
|
|||
@cli.command(name="insert-files")
|
||||
@click.argument(
|
||||
"path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table")
|
||||
|
|
@ -2921,7 +2952,7 @@ def _generate_convert_help():
|
|||
@cli.command(help=_generate_convert_help())
|
||||
@click.argument(
|
||||
"db_path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table", type=str)
|
||||
|
|
@ -3060,7 +3091,7 @@ def convert(
|
|||
@cli.command("add-geometry-column")
|
||||
@click.argument(
|
||||
"db_path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table", type=str)
|
||||
|
|
@ -3136,7 +3167,7 @@ def add_geometry_column(
|
|||
@cli.command("create-spatial-index")
|
||||
@click.argument(
|
||||
"db_path",
|
||||
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
type=DatabasePath(file_okay=True, dir_okay=False, allow_dash=False),
|
||||
required=True,
|
||||
)
|
||||
@click.argument("table", type=str)
|
||||
|
|
|
|||
|
|
@ -348,7 +348,10 @@ class Database:
|
|||
self.conn = sqlite3.connect(":memory:")
|
||||
self.memory = True
|
||||
elif isinstance(filename_or_conn, (str, pathlib.Path)):
|
||||
if recreate and os.path.exists(filename_or_conn):
|
||||
filename_str = str(filename_or_conn)
|
||||
# Check if this is a URI filename (starts with "file:")
|
||||
is_uri = filename_str.startswith("file:")
|
||||
if recreate and not is_uri and os.path.exists(filename_or_conn):
|
||||
try:
|
||||
os.remove(filename_or_conn)
|
||||
except OSError:
|
||||
|
|
@ -356,7 +359,11 @@ class Database:
|
|||
# https://github.com/simonw/sqlite-utils/issues/503
|
||||
self.conn = sqlite3.connect(":memory:")
|
||||
raise
|
||||
self.conn = sqlite3.connect(str(filename_or_conn))
|
||||
if is_uri:
|
||||
# URI filenames need uri=True parameter
|
||||
self.conn = sqlite3.connect(filename_str, uri=True)
|
||||
else:
|
||||
self.conn = sqlite3.connect(filename_str)
|
||||
else:
|
||||
assert not recreate, "recreate cannot be used with connections, only paths"
|
||||
self.conn = filename_or_conn
|
||||
|
|
|
|||
177
tests/test_uri.py
Normal file
177
tests/test_uri.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"""
|
||||
Tests for SQLite URI filename support
|
||||
https://github.com/simonw/sqlite-utils/issues/650
|
||||
"""
|
||||
import pytest
|
||||
import sqlite_utils
|
||||
from sqlite_utils import Database
|
||||
from click.testing import CliRunner
|
||||
from sqlite_utils import cli
|
||||
import pathlib
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_db_file(tmp_path):
|
||||
"""Create a test database file with some data"""
|
||||
db_path = tmp_path / "test.db"
|
||||
db = Database(str(db_path))
|
||||
db["test_table"].insert_all([
|
||||
{"id": 1, "name": "Alice"},
|
||||
{"id": 2, "name": "Bob"},
|
||||
{"id": 3, "name": "Charlie"},
|
||||
], pk="id")
|
||||
db.close()
|
||||
return db_path
|
||||
|
||||
|
||||
def test_database_class_with_uri(test_db_file):
|
||||
"""Test that Database class can open a URI"""
|
||||
# Test read-only mode
|
||||
uri = f"file:{test_db_file}?mode=ro"
|
||||
db = Database(uri)
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 3
|
||||
assert rows[0]["name"] == "Alice"
|
||||
db.close()
|
||||
|
||||
|
||||
def test_database_class_with_uri_immutable(test_db_file):
|
||||
"""Test that Database class can open a URI with immutable flag"""
|
||||
uri = f"file:{test_db_file}?immutable=1"
|
||||
db = Database(uri)
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 3
|
||||
db.close()
|
||||
|
||||
|
||||
def test_database_class_with_uri_multiple_params(test_db_file):
|
||||
"""Test URI with multiple query parameters"""
|
||||
uri = f"file:{test_db_file}?mode=ro&immutable=1"
|
||||
db = Database(uri)
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 3
|
||||
db.close()
|
||||
|
||||
|
||||
def test_cli_tables_with_uri(test_db_file):
|
||||
"""Test that tables command works with URI"""
|
||||
runner = CliRunner()
|
||||
uri = f"file:{test_db_file}?mode=ro"
|
||||
result = runner.invoke(cli.cli, ["tables", uri, "--csv"])
|
||||
assert result.exit_code == 0
|
||||
assert "test_table" in result.output
|
||||
|
||||
|
||||
def test_cli_tables_with_uri_immutable(test_db_file):
|
||||
"""Test that tables command works with URI and immutable flag"""
|
||||
runner = CliRunner()
|
||||
uri = f"file:{test_db_file}?mode=ro&immutable=1"
|
||||
result = runner.invoke(cli.cli, ["tables", uri])
|
||||
assert result.exit_code == 0
|
||||
assert "test_table" in result.output
|
||||
|
||||
|
||||
def test_cli_query_with_uri(test_db_file):
|
||||
"""Test that query command works with URI"""
|
||||
runner = CliRunner()
|
||||
uri = f"file:{test_db_file}?mode=ro"
|
||||
result = runner.invoke(cli.cli, ["query", uri, "SELECT * FROM test_table"])
|
||||
assert result.exit_code == 0
|
||||
assert "Alice" in result.output
|
||||
assert "Bob" in result.output
|
||||
|
||||
|
||||
def test_cli_query_with_uri_multiple_params(test_db_file):
|
||||
"""Test that query command works with URI with multiple parameters"""
|
||||
runner = CliRunner()
|
||||
uri = f"file:{test_db_file}?mode=ro&immutable=1"
|
||||
result = runner.invoke(cli.cli, ["query", uri, "SELECT name FROM test_table WHERE id = 1"])
|
||||
assert result.exit_code == 0
|
||||
assert "Alice" in result.output
|
||||
|
||||
|
||||
def test_cli_rows_with_uri(test_db_file):
|
||||
"""Test that rows command works with URI"""
|
||||
runner = CliRunner()
|
||||
uri = f"file:{test_db_file}?mode=ro"
|
||||
result = runner.invoke(cli.cli, ["rows", uri, "test_table"])
|
||||
assert result.exit_code == 0
|
||||
assert "Alice" in result.output
|
||||
|
||||
|
||||
def test_uri_with_relative_path(tmp_path):
|
||||
"""Test URI with relative path"""
|
||||
# Create a database in a subdirectory
|
||||
db_path = tmp_path / "test.db"
|
||||
db = Database(str(db_path))
|
||||
db["test_table"].insert({"id": 1, "name": "Test"})
|
||||
db.close()
|
||||
|
||||
# Test with relative path in URI
|
||||
old_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
uri = "file:test.db?mode=ro"
|
||||
db = Database(uri)
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 1
|
||||
db.close()
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
def test_uri_with_absolute_path(test_db_file):
|
||||
"""Test URI with absolute path"""
|
||||
# Use triple slash for absolute path
|
||||
uri = f"file:///{test_db_file}?mode=ro"
|
||||
db = Database(uri)
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 3
|
||||
db.close()
|
||||
|
||||
|
||||
def test_regular_path_still_works(test_db_file):
|
||||
"""Ensure regular file paths still work after URI changes"""
|
||||
# Test Database class
|
||||
db = Database(str(test_db_file))
|
||||
rows = list(db["test_table"].rows)
|
||||
assert len(rows) == 3
|
||||
db.close()
|
||||
|
||||
# Test CLI
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli.cli, ["tables", str(test_db_file)])
|
||||
assert result.exit_code == 0
|
||||
assert "test_table" in result.output
|
||||
|
||||
|
||||
def test_cli_insert_with_uri_fails_readonly():
|
||||
"""Test that insert fails with read-only URI"""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem():
|
||||
# Create a database first
|
||||
db = Database("test.db")
|
||||
db["test_table"].insert({"id": 1, "name": "Test"})
|
||||
db.close()
|
||||
|
||||
# Try to insert with read-only URI
|
||||
uri = "file:test.db?mode=ro"
|
||||
result = runner.invoke(cli.cli, ["insert", uri, "test_table", "-"],
|
||||
input='{"id": 2, "name": "Another"}')
|
||||
assert result.exit_code != 0
|
||||
assert "readonly" in result.output.lower() or "read-only" in result.output.lower() or "attempt to write" in result.output.lower()
|
||||
|
||||
|
||||
def test_nonexistent_file_with_uri_mode_rwc():
|
||||
"""Test that URI with mode=rwc can create new database"""
|
||||
runner = CliRunner()
|
||||
with runner.isolated_filesystem():
|
||||
uri = "file:newdb.db?mode=rwc"
|
||||
# This should create the database and table
|
||||
result = runner.invoke(cli.cli, ["insert", uri, "test_table", "-"],
|
||||
input='{"id": 1, "name": "Test"}')
|
||||
assert result.exit_code == 0
|
||||
# Verify the database was created
|
||||
assert os.path.exists("newdb.db")
|
||||
Loading…
Add table
Add a link
Reference in a new issue