feat: add URI filename support

This commit is contained in:
fry69 2025-10-10 15:14:38 +02:00
commit 37ef98352a
5 changed files with 308 additions and 48 deletions

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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
View 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")