diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c87c8c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: uv sync --locked + - run: uv run ruff check . + - run: uv run ruff format --check . + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + env: + UV_PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + # --locked fails the build if uv.lock drifted from pyproject.toml + - run: uv sync --locked + - run: uv run pytest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fedfab6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,17 @@ +name: Publish + +on: + push: + tags: ["v*"] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + # Required for PyPI trusted publishing: no API token to store as a secret. + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: uv build + - run: uv publish --trusted-publishing always diff --git a/Makefile b/Makefile index f922491..83c4fa8 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ -run: - python -m sqlite2duckdb - -build: - rm -Rf dist/ ; python -m build +dev: + uv sync + +lint: + uv run ruff check . + uv run ruff format --check . test: - python -m pytest + uv run pytest + +build: + rm -rf dist/ && uv build publish: - python -m twine upload dist/* + uv publish diff --git a/README.md b/README.md index b0862d7..aec6fbc 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,46 @@ # sqlite2duckdb +![CI](https://github.com/dridk/sqlite2duckdb/actions/workflows/ci.yml/badge.svg) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqlite2duckdb) ![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlite2duckdb) A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckdb](https://duckdb.org/) database - -## Description +## Description Sqlite is an embedded online database designed for transactional reading and writing. Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency. For more details [https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777](https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777) +Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on). + +## Installation + +With [uv](https://docs.astral.sh/uv/), no installation is required. `uvx` downloads and runs the tool in one go: + +```bash +uvx sqlite2duckdb source.db target.db +``` -## Installation +To keep it around: +```bash +uv tool install sqlite2duckdb ``` + +Or with pip: + +```bash pip install sqlite2duckdb ``` -## Usage +## Usage -### As a command line +### As a command line ``` - -usage: sqlite2duckdb +usage: sqlite2duckdb [-f] Convert Sqlite database to Duckdb database @@ -36,35 +50,74 @@ positional arguments: options: -h, --help show this help message and exit + -f, --force overwrite the duckdb file if it already exists + -q, --quiet only report errors + --verbose report every step -v, --version show program's version number and exit - - ``` -### Examples +The tool never overwrites an existing target silently. On a terminal it asks for +confirmation; anywhere else (a script, a CI job, a pipe) it exits with code 1 and tells you +to pass `--force`. Progress is written to stderr, so stdout stays free for pipelines. + +### Examples ```bash -sqlite2duckdb source.db target.db +uvx sqlite2duckdb source.db target.db +uvx sqlite2duckdb --force source.db target.db # overwrite target.db without asking ``` -### From python +### From python ```python +from sqlite2duckdb import sqlite_to_duckdb -from sqlite2duckdb import sqlite_to_duckdb -sqlite_to_duckdb("source.sqlite", "target.duckdb") - +result = sqlite_to_duckdb("source.sqlite", "target.duckdb") +print(result.tables, result.elapsed) ``` -## Todo +`sqlite_to_duckdb(sqlite_db, duck_db, *, overwrite=False)` accepts `str` or `pathlib.Path` +and returns a `ConversionResult` (`target`, `tables`, `elapsed`). It raises +`FileNotFoundError` if the source is missing and `FileExistsError` if the target already +exists and `overwrite` is False. If the conversion fails halfway, the partially written +target file is removed rather than left behind. Progress is reported through the standard +`logging` module (logger `sqlite2duckdb.sqlite_to_duckdb`), never printed. -- [ ] Custom mapping -- [ ] Relation and constraint +## What is converted +| | | +|---|---| +| Tables and data | ✅ | +| Primary keys, NOT NULL constraints, indexes | ✅ | +| UNIQUE, FOREIGN KEY and CHECK constraints | ❌ | +| Views | ❌ (silently dropped) | -### See also +Duckdb's sqlite extension does not expose the last two on the attached database, so they +cannot be copied. Reading them back from `sqlite_master` would be needed. -- [Harlequin](https://github.com/tconbeer/harlequin): A nice duckdb IDE for your terminal +Tables are recreated from the DDL duckdb derives for the attached database, then filled +from it, and the indexes are read back from `sqlite_master`. This is what makes sqlite +files that quote their DDL with `[brackets]` (chinook.db, MS Access exports) convert +correctly: duckdb's own parser rejects that syntax, so the quoting is translated first. + +## Todo + +- [ ] Custom type mapping +- [x] Primary keys, NOT NULL constraints and indexes +- [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints +## Contributing +The project uses [uv](https://docs.astral.sh/uv/) for everything: +```bash +make dev # uv sync — installs duckdb plus the test deps (pytest, faker, ruff) +make test # uv run pytest +make lint # uv run ruff check . && uv run ruff format --check . +make build # uv build +make publish # uv publish (PyPI trusted publishing, also run on tags by CI) +``` + +### See also + +- [Harlequin](https://github.com/tconbeer/harlequin): A nice duckdb IDE for your terminal diff --git a/examples/chinook.sqlite.db b/examples/chinook.sqlite.db new file mode 100644 index 0000000..38a98b3 Binary files /dev/null and b/examples/chinook.sqlite.db differ diff --git a/pyproject.toml b/pyproject.toml index 05b3844..05a973f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,29 @@ [build-system] -requires = ["hatchling", "build", "twine"] +requires = ["hatchling>=1.27"] build-backend = "hatchling.build" [project] name = "sqlite2duckdb" -version = "0.3.0" +version = "0.4.0" authors = [{name="Sacha Schutz", email="sacha.schutz@pm.me"}] description = "A tool to convert sqlite database to duckdb database" readme = "README.md" -requres-python = ">=3.8" +requires-python = ">=3.9" +license = "MIT" +license-files = ["LICENSE"] keywords = ["sqlite", "duckdb", "database", "olap", "oltp"] classifiers = [ "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ] dependencies = [ - 'duckdb >= 0.10.0' + 'duckdb >= 1.1.0' ] @@ -27,3 +33,28 @@ Issues = "https://github.com/dridk/sqlite2duckdb/issues" [project.scripts] sqlite2duckdb = "sqlite2duckdb.__main__:main_cli" + +[dependency-groups] +dev = [ + "pytest >= 7.0", + "faker", + "ruff", +] + +[tool.hatch.build.targets.wheel] +packages = ["sqlite2duckdb"] + +# Allow list rather than a deny list, so that anything new landing in the repo +# stays out of the distribution unless it is explicitly wanted. +[tool.hatch.build.targets.sdist] +include = ["sqlite2duckdb", "tests", "README.md"] + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] + +[tool.ruff] +target-version = "py39" + +[tool.ruff.lint] +extend-select = ["I", "UP"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8a6ba6a..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -duckdb diff --git a/sqlite2duckdb/__init__.py b/sqlite2duckdb/__init__.py index 3ce2595..e0fe3c3 100644 --- a/sqlite2duckdb/__init__.py +++ b/sqlite2duckdb/__init__.py @@ -1,4 +1,14 @@ import importlib.metadata -from sqlite2duckdb.sqlite_to_duckdb import sqlite_to_duckdb -__VERSION__ = importlib.metadata.version("sqlite2duckdb") +from sqlite2duckdb.sqlite_to_duckdb import ConversionResult, sqlite_to_duckdb + +try: + __version__ = importlib.metadata.version("sqlite2duckdb") +except importlib.metadata.PackageNotFoundError: + # Running from a source checkout that was never installed. + __version__ = "0.0.0.dev0" + +# Deprecated alias, kept so that existing imports keep working. +__VERSION__ = __version__ + +__all__ = ["ConversionResult", "__version__", "sqlite_to_duckdb"] diff --git a/sqlite2duckdb/__main__.py b/sqlite2duckdb/__main__.py index a49bbff..45b736c 100644 --- a/sqlite2duckdb/__main__.py +++ b/sqlite2duckdb/__main__.py @@ -1,38 +1,76 @@ -import duckdb +from __future__ import annotations + import argparse +import logging import os -from sqlite2duckdb import sqlite_to_duckdb, __VERSION__ +import sys + +from sqlite2duckdb import __version__, sqlite_to_duckdb -def main_cli(): +def main_cli() -> int: parser = argparse.ArgumentParser( + prog="sqlite2duckdb", description="Convert Sqlite database to Duckdb database", - usage="sqlite2duckdb ", + usage="sqlite2duckdb [-f] ", ) parser.add_argument("sqlite_path", type=str, help="sqlite file path") parser.add_argument("duckdb_path", type=str, help="duckdb file path") parser.add_argument( - "-v", "--version", action="version", version=f"sqlite2duckdb {__VERSION__}" + "-f", + "--force", + action="store_true", + help="overwrite the duckdb file if it already exists", + ) + parser.add_argument("-q", "--quiet", action="store_true", help="only report errors") + parser.add_argument("--verbose", action="store_true", help="report every step") + parser.add_argument( + "-v", "--version", action="version", version=f"sqlite2duckdb {__version__}" ) - # Analyser les arguments args = parser.parse_args() - if os.path.exists(args.duckdb_path): - delete_input = ( - input( - f"{args.duckdb_path} already exists. do you want to delete this file ? (yes/no): " + if args.quiet: + level = logging.WARNING + elif args.verbose: + level = logging.DEBUG + else: + level = logging.INFO + # Progress goes to stderr so that stdout stays free for pipelines. + logging.basicConfig(level=level, format="%(message)s", stream=sys.stderr) + + overwrite = args.force + if not overwrite and os.path.exists(args.duckdb_path): + if not sys.stdin.isatty(): + print( + f"{args.duckdb_path} already exists. Use --force to overwrite it.", + file=sys.stderr, + ) + return 1 + try: + answer = ( + input( + f"{args.duckdb_path} already exists. do you want to delete this file ? (yes/no): " + ) + .strip() + .lower() ) - .strip() - .lower() - ) - if delete_input in ("yes", "y"): - os.remove(args.duckdb_path) - else: - exit(1) - sqlite_to_duckdb(args.sqlite_path, args.duckdb_path) + except EOFError: + print(f"{args.duckdb_path} already exists.", file=sys.stderr) + return 1 + if answer not in ("yes", "y"): + return 1 + overwrite = True + + try: + sqlite_to_duckdb(args.sqlite_path, args.duckdb_path, overwrite=overwrite) + except (FileNotFoundError, FileExistsError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + return 0 if __name__ == "__main__": - main_cli() + sys.exit(main_cli()) diff --git a/sqlite2duckdb/sqlite_to_duckdb.py b/sqlite2duckdb/sqlite_to_duckdb.py index cff6ee0..b1bcc82 100644 --- a/sqlite2duckdb/sqlite_to_duckdb.py +++ b/sqlite2duckdb/sqlite_to_duckdb.py @@ -1,47 +1,174 @@ -import duckdb +from __future__ import annotations + +import contextlib +import logging import os import sqlite3 import time +from dataclasses import dataclass +import duckdb -def sqlite_to_duckdb(sqlite_db: str, duck_db: str): - print(f"Create {duck_db} databases") +logger = logging.getLogger(__name__) - if not os.path.exists(sqlite_db): - raise Exception(f"File {sqlite_db} doesn't exists") - # Remove target db if exists - if os.path.exists(duck_db): - raise Exception(f"Database {duck_db} already exists") +@dataclass +class ConversionResult: + """What a successful conversion produced.""" - # Create databases + target: str + tables: int + elapsed: float + """Wall clock duration of the conversion, in seconds.""" - start_time = time.perf_counter() - conn = duckdb.connect(duck_db) - db_name = conn.sql("SELECT database_name FROM duckdb_databases").fetchone()[0] - - ## Install sqlite - conn.sql( - f""" - INSTALL sqlite; - LOAD sqlite; - ATTACH '{sqlite_db}' as __other; + +def _quote_identifier(name: str) -> str: + return '"' + name.replace('"', '""') + '"' + + +def _format_duration(seconds: float) -> str: + if seconds < 1: + return f"{seconds * 1000:.2f} ms" + return f"{seconds:.2f} s" + + +def _remove_quietly(path: str) -> None: + for candidate in (path, path + ".wal"): + try: + os.remove(candidate) + except OSError: + pass + + +def _brackets_to_quotes(sql: str) -> str: + """Rewrite sqlite's [bracket] identifiers into standard "double quoted" ones. + + Quoted runs are copied verbatim so that a `[` inside a string literal survives. + """ + + out = [] + index = 0 + length = len(sql) + + while index < length: + char = sql[index] + + if char in "'\"`": + end = index + 1 + while end < length: + if sql[end] == char: + if end + 1 < length and sql[end + 1] == char: + end += 2 + continue + break + end += 1 + out.append(sql[index : end + 1]) + index = end + 1 + + elif char == "[": + end = sql.find("]", index) + if end == -1: + out.append(char) + index += 1 + else: + out.append(_quote_identifier(sql[index + 1 : end])) + index = end + 1 + + else: + out.append(char) + index += 1 + + return "".join(out) + + +def _copy_indexes(conn: duckdb.DuckDBPyConnection, sqlite_path: str) -> None: + """Recreate the source indexes, translating sqlite quoting on the way.""" + + with contextlib.closing(sqlite3.connect(sqlite_path)) as source: + indexes = source.execute( + "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL" + ).fetchall() + + for name, sql in indexes: + try: + conn.sql(_brackets_to_quotes(sql)) + except duckdb.Error as error: + logger.warning("Could not recreate index %s: %s", name, error) + + +def _copy_tables( + conn: duckdb.DuckDBPyConnection, tables: list[tuple[str, str]] +) -> None: + """Recreate each table then fill it from the attached database.""" + + for table_name, ddl in tables: + # This DDL is regenerated by duckdb from the attached catalog, so unlike + # sqlite's own it always parses, and it still carries the primary keys + # and the NOT NULL constraints. + conn.sql(ddl) + quoted = _quote_identifier(table_name) + conn.sql(f"INSERT INTO {quoted} SELECT * FROM __other.{quoted}") + + +def sqlite_to_duckdb( + sqlite_db: str | os.PathLike[str], + duck_db: str | os.PathLike[str], + *, + overwrite: bool = False, +) -> ConversionResult: + """Copy a sqlite database into a new duckdb database. + + Tables, data, primary keys, NOT NULL constraints and indexes are copied. + Views and UNIQUE / FOREIGN KEY / CHECK constraints are not: duckdb's sqlite + extension does not expose them on the attached database. + + Raises FileNotFoundError if `sqlite_db` does not exist, and FileExistsError + if `duck_db` already exists and `overwrite` is False. """ - ) - ## Get sqlite Names - conn.sql("USE __other") - tables = [i[0] for i in conn.sql("SHOW tables").fetchall()] - print(f"{len(tables)} tables found(s)") - conn.sql(f"USE {db_name}") + sqlite_path = os.fspath(sqlite_db) + duck_path = os.fspath(duck_db) - # Create tables - for table in tables: - print(f"Create duckdb table {table}") - conn.sql(f"CREATE TABLE {table} AS select * FROM __other.{table}") + if not os.path.exists(sqlite_path): + raise FileNotFoundError(f"File {sqlite_path} doesn't exist") + + if os.path.exists(duck_path): + if not overwrite: + raise FileExistsError(f"Database {duck_path} already exists") + _remove_quietly(duck_path) + + logger.info("Create %s database", duck_path) + start_time = time.perf_counter() + + conn = duckdb.connect(duck_path) + try: + ## Install sqlite + conn.sql("INSTALL sqlite; LOAD sqlite;") + + # Bound parameters are not allowed in ATTACH, so escape the quotes ourselves + source_path = sqlite_path.replace("'", "''") + conn.sql(f"ATTACH '{source_path}' AS __other (TYPE SQLITE, READ_ONLY)") + + ## Get sqlite Names, along with the CREATE statement duckdb derives for them + tables = conn.sql( + "SELECT table_name, sql FROM duckdb_tables WHERE database_name = '__other'" + ).fetchall() + logger.info("%d table(s) found", len(tables)) + + _copy_tables(conn, tables) + _copy_indexes(conn, sqlite_path) + + conn.sql("DETACH __other") + except BaseException: + # Never leave a half-written database behind: it would trip the + # "already exists" guard on the next run. + conn.close() + _remove_quietly(duck_path) + raise - conn.sql(f"DETACH __other") conn.close() - end_time = time.perf_counter() - execution_time = (end_time - start_time) * 1000 - print(f"Done in {execution_time:.2f} ms !") + + elapsed = time.perf_counter() - start_time + logger.info("Done in %s !", _format_duration(elapsed)) + + return ConversionResult(target=duck_path, tables=len(tables), elapsed=elapsed) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d833073 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +import pytest + +from tests import utils + + +def _module_db(tmp_path_factory, name, builder): + path = tmp_path_factory.mktemp(name) / f"{name}.sqlite" + builder(path) + return path + + +@pytest.fixture(scope="module") +def fake_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "fake", utils.build_fake_sqlite) + + +@pytest.fixture(scope="module") +def edge_case_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "edge", utils.build_edge_case_sqlite) + + +@pytest.fixture(scope="module") +def bracket_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "bracket", utils.build_bracket_sqlite) + + +@pytest.fixture(scope="module") +def dotted_column_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "dotted", utils.build_dotted_column_sqlite) + + +@pytest.fixture(scope="module") +def fidelity_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "fidelity", utils.build_fidelity_sqlite) + + +@pytest.fixture(scope="module") +def empty_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "empty", utils.build_empty_sqlite) + + +@pytest.fixture +def duckdb_path(tmp_path): + """A path where the target database does not exist yet.""" + + return tmp_path / "target.duckdb" + + +@pytest.fixture(scope="module") +def bracket_index_sqlite(tmp_path_factory): + return _module_db(tmp_path_factory, "bindex", utils.build_bracket_index_sqlite) diff --git a/tests/test_brackets.py b/tests/test_brackets.py new file mode 100644 index 0000000..e5971a2 --- /dev/null +++ b/tests/test_brackets.py @@ -0,0 +1,29 @@ +from sqlite2duckdb.sqlite_to_duckdb import _brackets_to_quotes + + +def test_translates_bracket_identifiers(): + assert ( + _brackets_to_quotes("CREATE INDEX [i] ON [my table] ([a], [b])") + == 'CREATE INDEX "i" ON "my table" ("a", "b")' + ) + + +def test_leaves_string_literals_alone(): + assert ( + _brackets_to_quotes("CREATE INDEX [i] ON t (x) WHERE y = 'a [b] c'") + == "CREATE INDEX \"i\" ON t (x) WHERE y = 'a [b] c'" + ) + + +def test_leaves_already_quoted_identifiers_alone(): + assert _brackets_to_quotes('CREATE INDEX i ON "a [b]" (c)') == ( + 'CREATE INDEX i ON "a [b]" (c)' + ) + + +def test_escapes_double_quotes_inside_brackets(): + assert _brackets_to_quotes('SELECT [a"b]') == 'SELECT "a""b"' + + +def test_leaves_unterminated_bracket_alone(): + assert _brackets_to_quotes("SELECT [oops") == "SELECT [oops" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..8b514d7 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,79 @@ +"""End to end tests of the command line entry point. + +They run the CLI in a subprocess with stdin closed, which is exactly the +situation (CI, pipes, `uvx ... < /dev/null`) the interactive prompt used to hang in. +""" + +import subprocess +import sys + +import pytest + +REPO_ROOT = str(__import__("pathlib").Path(__file__).resolve().parent.parent) + + +def run_cli(*args, stdin=subprocess.DEVNULL, timeout=120): + return subprocess.run( + [sys.executable, "-m", "sqlite2duckdb", *map(str, args)], + capture_output=True, + check=False, + text=True, + stdin=stdin, + cwd=REPO_ROOT, + timeout=timeout, + ) + + +def test_version(): + result = run_cli("--version") + + assert result.returncode == 0 + assert result.stdout.startswith("sqlite2duckdb ") + + +def test_converts(edge_case_sqlite, duckdb_path): + result = run_cli(edge_case_sqlite, duckdb_path) + + assert result.returncode == 0, result.stderr + assert duckdb_path.exists() + # Progress must not pollute stdout. + assert result.stdout == "" + assert "3 table(s) found" in result.stderr + + +def test_quiet_says_nothing(edge_case_sqlite, duckdb_path): + result = run_cli("--quiet", edge_case_sqlite, duckdb_path) + + assert result.returncode == 0 + assert result.stdout == "" + assert result.stderr == "" + + +def test_missing_source_reports_error(tmp_path, duckdb_path): + result = run_cli(tmp_path / "nope.sqlite", duckdb_path) + + assert result.returncode == 1 + assert "doesn't exist" in result.stderr + assert "Traceback" not in result.stderr + + +def test_existing_target_without_tty_fails_instead_of_hanging( + edge_case_sqlite, duckdb_path +): + duckdb_path.write_bytes(b"keep me") + + result = run_cli(edge_case_sqlite, duckdb_path, timeout=30) + + assert result.returncode == 1 + assert "--force" in result.stderr + assert duckdb_path.read_bytes() == b"keep me" + + +@pytest.mark.parametrize("flag", ["-f", "--force"]) +def test_force_overwrites(flag, edge_case_sqlite, duckdb_path): + duckdb_path.write_bytes(b"stale") + + result = run_cli(flag, edge_case_sqlite, duckdb_path) + + assert result.returncode == 0, result.stderr + assert duckdb_path.read_bytes() != b"stale" diff --git a/tests/test_convert.py b/tests/test_convert.py index a41e1bb..9dba590 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,39 +1,210 @@ -import sqlite3 -import duckdb +import sqlite3 + +import duckdb +import pytest + +from sqlite2duckdb import ConversionResult, sqlite_to_duckdb from tests import utils -import os -from sqlite2duckdb.__main__ import sqlite_to_duckdb -def test_conversion(): +def test_conversion(fake_sqlite, duckdb_path): + result = sqlite_to_duckdb(fake_sqlite, duckdb_path) + + assert isinstance(result, ConversionResult) + assert result.target == str(duckdb_path) + assert result.tables == 1 + assert result.elapsed > 0 + assert duckdb_path.exists() + + +def test_count(fake_sqlite, duckdb_path): + sqlite_to_duckdb(fake_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + s_conn = sqlite3.connect(fake_sqlite) + + tables = d_conn.sql("SHOW TABLES").fetchall() + assert tables + + for (table_name,) in tables: + query = f'SELECT COUNT(*) FROM "{table_name}"' + + assert d_conn.sql(query).fetchone() == s_conn.execute(query).fetchone() + + +def test_value_fidelity(fidelity_sqlite, duckdb_path): + """Values must come back identical, not merely in the right number.""" + + sqlite_to_duckdb(fidelity_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + s_conn = sqlite3.connect(fidelity_sqlite) + + query = "SELECT * FROM roundtrip ORDER BY id" + duck_rows = d_conn.sql(query).fetchall() + + assert duck_rows == s_conn.execute(query).fetchall() + assert duck_rows == utils.FIDELITY_ROWS + + +def test_quoted_table_names(edge_case_sqlite, duckdb_path): + sqlite_to_duckdb(edge_case_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + tables = {table[0] for table in d_conn.sql("SHOW TABLES").fetchall()} + + assert {"users", "my table", "order"} <= tables + + assert d_conn.sql('SELECT * FROM "my table"').fetchall() == [(1, "x")] + assert d_conn.sql('SELECT * FROM "order"').fetchall() == [(7,)] + + +def test_constraints_preserved(edge_case_sqlite, duckdb_path): + sqlite_to_duckdb(edge_case_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + + constraints = { + row[0] + for row in d_conn.sql( + "SELECT constraint_type FROM duckdb_constraints() WHERE table_name = 'users'" + ).fetchall() + } + + assert "PRIMARY KEY" in constraints + assert "NOT NULL" in constraints + + indexes = { + row[0] + for row in d_conn.sql("SELECT index_name FROM duckdb_indexes()").fetchall() + } + + assert "idx_users_age" in indexes + + +def test_bracket_quoted_view(bracket_sqlite, duckdb_path): + """Regression test for issue #3: a view whose SQL uses [bracket] identifiers + cannot be parsed by duckdb, which used to break table discovery.""" + + sqlite_to_duckdb(bracket_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + + assert d_conn.sql('SELECT * FROM "Order Details" ORDER BY OrderID').fetchall() == [ + (1, 5), + (2, 7), + ] + + +def test_dotted_column_names(dotted_column_sqlite, duckdb_path): + """Regression test for issue #4: a STRICT table with numeric-looking column + names used to fail with `Parser Error: syntax error at or near ".1"`.""" + + sqlite_to_duckdb(dotted_column_sqlite, duckdb_path) + + d_conn = duckdb.connect(str(duckdb_path)) + + assert d_conn.sql('SELECT "2.3.4", "1e5" FROM measures').fetchall() == [ + (42, "hello") + ] + + +def test_empty_database(empty_sqlite, duckdb_path): + result = sqlite_to_duckdb(empty_sqlite, duckdb_path) + + assert result.tables == 0 + assert duckdb_path.exists() + + +def test_missing_source_raises(tmp_path, duckdb_path): + with pytest.raises(FileNotFoundError): + sqlite_to_duckdb(tmp_path / "nope.sqlite", duckdb_path) + + assert not duckdb_path.exists() + + +def test_existing_target_raises(fake_sqlite, duckdb_path): + duckdb_path.write_bytes(b"not a database") + + with pytest.raises(FileExistsError): + sqlite_to_duckdb(fake_sqlite, duckdb_path) + + # The untouched file must still be there. + assert duckdb_path.read_bytes() == b"not a database" + + +def test_overwrite(edge_case_sqlite, duckdb_path): + sqlite_to_duckdb(edge_case_sqlite, duckdb_path) + + result = sqlite_to_duckdb(edge_case_sqlite, duckdb_path, overwrite=True) + + assert result.tables == 3 + d_conn = duckdb.connect(str(duckdb_path)) + assert d_conn.sql("SELECT COUNT(*) FROM users").fetchone() == (2,) + + +def test_corrupt_source_leaves_no_partial_database(tmp_path, duckdb_path): + """A failed conversion must not leave a half-written file behind, which would + otherwise trip the `already exists` guard on the next run.""" + + broken = tmp_path / "broken.sqlite" + broken.write_bytes(b"definitely not a sqlite file" * 10) + + with pytest.raises(duckdb.Error): + sqlite_to_duckdb(broken, duckdb_path) + + assert not duckdb_path.exists() + + +def test_source_path_with_quote_and_space(tmp_path, duckdb_path): + weird = tmp_path / "it's a db.sqlite" + utils.build_edge_case_sqlite(weird) + + result = sqlite_to_duckdb(weird, duckdb_path) + + assert result.tables == 3 - duckdb_path = utils.generate_fake_duckdb() - sqlite_path = utils.generate_fake_sqlite() - sqlite_to_duckdb(sqlite_path, duckdb_path) +def test_accepts_str_paths(edge_case_sqlite, duckdb_path): + result = sqlite_to_duckdb(str(edge_case_sqlite), str(duckdb_path)) + assert result.tables == 3 -def test_count(): - - duckdb_path = utils.generate_fake_duckdb() - sqlite_path = utils.generate_fake_sqlite() - sqlite_to_duckdb(sqlite_path, duckdb_path) +def test_bracket_quoted_index(bracket_index_sqlite, duckdb_path): + """Regression test: chinook.db writes its index DDL with [bracket] identifiers, + which duckdb's own parser rejects.""" + result = sqlite_to_duckdb(bracket_index_sqlite, duckdb_path) - d_conn = duckdb.connect(duckdb_path) - s_conn = sqlite3.connect(sqlite_path) + # albums, "track list", plus the sqlite_sequence table AUTOINCREMENT creates. + assert result.tables == 3 - for table in d_conn.sql("SHOW TABLES").fetchall(): - table_name = table[0] + d_conn = duckdb.connect(str(duckdb_path)) - query = f"SELECT COUNT(*) FROM {table_name}" + assert d_conn.sql('SELECT * FROM "albums" ORDER BY AlbumId').fetchall() == [ + (1, "For Those About To Rock", 1), + (2, "Balls to the Wall", 2), + ] - duckdb_count = d_conn.sql(query).fetchone() - sqlite_count = s_conn.execute(query).fetchone() + # The index and the constraints must survive too, as they do on the fast path. + indexes = { + row[0] + for row in d_conn.sql("SELECT index_name FROM duckdb_indexes()").fetchall() + } + assert {"IFK_AlbumArtistId", "idx track"} <= indexes - assert duckdb_count == sqlite_count + # The fallback quotes identifiers itself, so odd table names must survive. + assert d_conn.sql('SELECT * FROM "track list" ORDER BY "Track Id"').fetchall() == [ + (1, "a [b] c"), + (2, None), + ] - - - + constraints = { + row[0] + for row in d_conn.sql( + "SELECT constraint_type FROM duckdb_constraints() WHERE table_name = 'albums'" + ).fetchall() + } + assert "PRIMARY KEY" in constraints + assert "NOT NULL" in constraints diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..581513b --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,94 @@ +"""Integration test against the real chinook database shipped in examples/. + +The synthetic fixtures cover the mechanisms one at a time; this one checks the +whole conversion on a database nobody wrote for the tests. Chinook quotes its +DDL with [brackets], which duckdb's parser rejects. +""" + +import sqlite3 +from pathlib import Path + +import duckdb +import pytest + +from sqlite2duckdb import sqlite_to_duckdb + +CHINOOK = Path(__file__).resolve().parent.parent / "examples" / "chinook.sqlite.db" + +pytestmark = pytest.mark.skipif( + not CHINOOK.exists(), reason="examples/chinook.sqlite.db is not available" +) + + +@pytest.fixture(scope="module") +def chinook(tmp_path_factory): + target = tmp_path_factory.mktemp("chinook") / "chinook.duckdb" + result = sqlite_to_duckdb(CHINOOK, target) + + return result, duckdb.connect(str(target)), sqlite3.connect(CHINOOK) + + +def test_every_table_is_copied(chinook): + result, d_conn, s_conn = chinook + + expected = { + row[0] + for row in s_conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'") + } + converted = {row[0] for row in d_conn.sql("SHOW TABLES").fetchall()} + + assert converted == expected + assert result.tables == len(expected) + + +def test_every_row_is_copied(chinook): + _, d_conn, s_conn = chinook + + for (table,) in s_conn.execute( + "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name" + ).fetchall(): + query = f'SELECT COUNT(*) FROM "{table}"' + + assert d_conn.sql(query).fetchone() == s_conn.execute(query).fetchone(), table + + +def test_values_are_unchanged(chinook): + _, d_conn, s_conn = chinook + + query = "SELECT AlbumId, Title, ArtistId FROM albums ORDER BY AlbumId" + duck_rows = d_conn.sql(query).fetchall() + + assert duck_rows == s_conn.execute(query).fetchall() + assert duck_rows[0] == (1, "For Those About To Rock We Salute You", 1) + + +def test_bracket_quoted_indexes_are_recreated(chinook): + _, d_conn, s_conn = chinook + + expected = { + row[0] + for row in s_conn.execute( + "SELECT name FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL" + ) + } + converted = { + row[0] + for row in d_conn.sql("SELECT index_name FROM duckdb_indexes()").fetchall() + } + + assert expected <= converted + assert "IFK_AlbumArtistId" in converted + + +def test_constraints_are_preserved(chinook): + _, d_conn, _ = chinook + + constraints = { + (row[0], row[1]) + for row in d_conn.sql( + "SELECT table_name, constraint_type FROM duckdb_constraints()" + ).fetchall() + } + + assert ("albums", "PRIMARY KEY") in constraints + assert ("albums", "NOT NULL") in constraints diff --git a/tests/utils.py b/tests/utils.py index abcdae9..33a5639 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,68 +1,202 @@ +"""Builders for the sqlite databases used as test fixtures.""" -import sqlite3 import random -import datetime -import tempfile +import sqlite3 + from faker import Faker -import os - -def generate_fake_duckdb(): - filename = tempfile.mkstemp(prefix="sqlite2duckdb_duck", suffix=".duckdb")[1] - if os.path.exists(filename): - os.remove(filename) - - return filename - -def generate_fake_sqlite(): - - temp_file_path = tempfile.mkstemp(prefix="sqlite2duckdb", suffix=".sqlite")[1] - print(f"Create temp sqlite db at {temp_file_path}") - fake = Faker() - - conn = sqlite3.connect(temp_file_path) - cursor = conn.cursor() - - # Create sqlite database - cursor.execute(''' - CREATE TABLE IF NOT EXISTS data ( - id INTEGER PRIMARY KEY, - integer_col INTEGER, - real_col REAL, - text_col TEXT, - boolean_col BOOLEAN, - date_col DATE, - time_col TIME, - datetime_col DATETIME, - blob_col BLOB, - numeric_col NUMERIC, - null_col NULL - ) - ''') - - # Generate fake data - def generate_fake_data(): - integer_col = random.randint(1, 1000) - real_col = random.uniform(1.0, 1000.0) - text_col = fake.text(max_nb_chars=20) - boolean_col = random.choice([True, False]) - date_col = fake.date() - time_col = fake.date_time() - datetime_col = fake.date_time() - blob_col = fake.binary(length=10) - numeric_col = random.choice([integer_col, real_col]) - null_col = None - return (integer_col, real_col, text_col, boolean_col, date_col, time_col, datetime_col, blob_col, numeric_col, null_col) - - - for _ in range(1000): - - cursor.execute(''' - INSERT INTO data (integer_col, real_col, text_col, boolean_col, date_col, time_col, datetime_col, blob_col, numeric_col, null_col) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', generate_fake_data()) - - # Sauvegarde (commit) des changements et fermeture de la connexion - conn.commit() - conn.close() - - return temp_file_path + +FAKE_ROWS = 1000 + + +def build_fake_sqlite(path, rows=FAKE_ROWS): + """A database covering every sqlite type affinity, with reproducible data.""" + + fake = Faker() + Faker.seed(1234) + rng = random.Random(1234) + + conn = sqlite3.connect(path) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY, + integer_col INTEGER, + real_col REAL, + text_col TEXT, + boolean_col BOOLEAN, + date_col DATE, + time_col TIME, + datetime_col DATETIME, + blob_col BLOB, + numeric_col NUMERIC, + null_col NULL + ) + """ + ) + + def generate_fake_data(): + integer_col = rng.randint(1, 1000) + real_col = rng.uniform(1.0, 1000.0) + return ( + integer_col, + real_col, + fake.text(max_nb_chars=20), + rng.choice([True, False]), + fake.date(), + # Adapt datetimes ourselves: sqlite3's implicit adapter is deprecated. + fake.date_time().isoformat(sep=" "), + fake.date_time().isoformat(sep=" "), + fake.binary(length=10), + rng.choice([integer_col, real_col]), + None, + ) + + conn.executemany( + """ + INSERT INTO data (integer_col, real_col, text_col, boolean_col, date_col, + time_col, datetime_col, blob_col, numeric_col, null_col) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + [generate_fake_data() for _ in range(rows)], + ) + conn.commit() + conn.close() + + return path + + +def build_edge_case_sqlite(path): + """Quoted and reserved table names, plus constraints and an index.""" + + conn = sqlite3.connect(path) + conn.executescript( + """ + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + age INTEGER + ); + INSERT INTO users (name, age) VALUES ('alice', 30), ('bob', 40); + CREATE INDEX idx_users_age ON users(age); + + CREATE TABLE "my table" (a INTEGER, b TEXT); + INSERT INTO "my table" VALUES (1, 'x'); + + CREATE TABLE "order" (x INTEGER); + INSERT INTO "order" VALUES (7); + """ + ) + conn.commit() + conn.close() + + return path + + +def build_bracket_sqlite(path): + """Sqlite db using [bracket] quoting, as produced by MS Access exports (issue #3).""" + + conn = sqlite3.connect(path) + conn.executescript( + """ + CREATE TABLE [Order Details] ([OrderID] INTEGER, [Qty] INTEGER); + INSERT INTO [Order Details] VALUES (1, 5), (2, 7); + + CREATE VIEW [Order Subtotals] AS + SELECT [Order Details].OrderID, Sum([Order Details].Qty) AS Total + FROM [Order Details] + GROUP BY [Order Details].OrderID; + """ + ) + conn.commit() + conn.close() + + return path + + +def build_dotted_column_sqlite(path): + """STRICT table whose column names look like numbers (issue #4).""" + + conn = sqlite3.connect(path) + conn.executescript( + """ + CREATE TABLE measures ("2.3.4" INTEGER, "1e5" TEXT) STRICT; + INSERT INTO measures VALUES (42, 'hello'); + """ + ) + conn.commit() + conn.close() + + return path + + +#: Values chosen to survive a sqlite -> duckdb round trip byte for byte. +FIDELITY_ROWS = [ + (1, 0, 0.0, "", b"", None), + (2, -1, -1.5, "héllo ünicode ✓", b"\x00\x01\xff", 7), + (3, 9223372036854775807, 1e308, "with 'quotes' and \"double\"", b"\n\r\t", None), + (4, None, None, None, None, None), +] + + +def build_fidelity_sqlite(path): + """A table whose exact values must come out unchanged on the duckdb side.""" + + conn = sqlite3.connect(path) + conn.execute( + """ + CREATE TABLE roundtrip ( + id INTEGER PRIMARY KEY, + int_col INTEGER, + real_col REAL, + text_col TEXT, + blob_col BLOB, + nullable_col INTEGER + ) + """ + ) + conn.executemany("INSERT INTO roundtrip VALUES (?, ?, ?, ?, ?, ?)", FIDELITY_ROWS) + conn.commit() + conn.close() + + return path + + +def build_empty_sqlite(path): + """A valid sqlite file holding no table at all.""" + + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE placeholder (x INTEGER)") + conn.execute("DROP TABLE placeholder") + conn.commit() + conn.close() + + return path + + +def build_bracket_index_sqlite(path): + """A chinook-shaped database: index DDL written with [bracket] identifiers. + + Duckdb's parser rejects `[`, so that quoting has to be translated on the way in. + """ + + conn = sqlite3.connect(path) + conn.executescript( + """ + CREATE TABLE "albums" + ( + [AlbumId] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + [Title] NVARCHAR(160) NOT NULL, + [ArtistId] INTEGER NOT NULL + ); + INSERT INTO "albums" VALUES (1, 'For Those About To Rock', 1), (2, 'Balls to the Wall', 2); + CREATE INDEX [IFK_AlbumArtistId] ON "albums" ([ArtistId]); + + CREATE TABLE [track list] ([Track Id] INTEGER, [Label] TEXT); + INSERT INTO [track list] VALUES (1, 'a [b] c'), (2, NULL); + CREATE INDEX [idx track] ON [track list] ([Track Id]); + """ + ) + conn.commit() + conn.close() + + return path diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..68b580b --- /dev/null +++ b/uv.lock @@ -0,0 +1,372 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "duckdb" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/05/9e32eb606684bbfd739a757acfa887705930b84e5a598da6bb85c48eb35f/duckdb-1.4.5.tar.gz", hash = "sha256:783779bde612172b06c250b5f34f7fc29471833545f2894aadedbffbbcc49013", size = 18424446, upload-time = "2026-06-17T10:46:36.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/64/d080742e4f57f2e458fa43643c4d8b0f0ee07c302202189f27985d8fc179/duckdb-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d432aa456d6ef3b87795f6ec725732f1f2746589e308878ee7f16287bdc3ca", size = 28919587, upload-time = "2026-06-17T10:44:32.797Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/f916cd736873ef22fe12c847b177a834a7b99985a87015eab6b89d7cd209/duckdb-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c412f665f8e2e65b3851bea8d63effd01113e3743a27e7718403cd1b16e52f59", size = 15364028, upload-time = "2026-06-17T10:44:36.484Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/0f97d8c4387d3e2054ba5c48f60f6f2873c9895404c96857027d3d72224f/duckdb-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70755e3b7c22267e566fbc611370ca6c3ab143198bbdccdd500f29fb0ebf05e8", size = 13678963, upload-time = "2026-06-17T10:44:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/56/0e/0faf134b35489582c4f5a5698a85b851a9f0706417041216fea5bc59c573/duckdb-1.4.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b1849e4647a744d0f184f3ff53e180fd245198312cf445a0af735cce6dc55ca", size = 18447270, upload-time = "2026-06-17T10:44:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/7a/66/9032647dbbc1bb17d715ad50d8fbf874593e646425ecb0709d57c149f8ec/duckdb-1.4.5-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11f2b26b8b0f0fa6ab44cabc77c30b1ddb44f8e81bc5669c0809a647f62e27ef", size = 20448081, upload-time = "2026-06-17T10:44:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/63062f0a56bb16f7a62260e2b5424aef93536d54e46a8154f99d921e29ca/duckdb-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:62cb03e4c7dc938daa3d4f29b8aed99b329d1633fe0f60bf4991402a21ea3dbc", size = 12266992, upload-time = "2026-06-17T10:44:47.977Z" }, + { url = "https://files.pythonhosted.org/packages/64/c5/0364355e4a25a1f2cb70a5a04d8caad7ee7e9b6b67b4a524b3fa53b3bfdc/duckdb-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:46eb53cd9ecec2972044a988be4a2e60d58cd185349d4a27f4944b8824d137af", size = 28924380, upload-time = "2026-06-17T10:44:51.456Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7d74d0e3ee5a4396495c22551f9422543bb7ee324d24394adeae73b9ccf5/duckdb-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14ee4000e879ce1f9a1a6dc08936cca5bfe0990b81e1b5a0466a746070bf1033", size = 15364409, upload-time = "2026-06-17T10:44:54.4Z" }, + { url = "https://files.pythonhosted.org/packages/81/ff/dfe91b05ac76b63f54e72a3b336f7c6800bb3f973fedf9466209053104c7/duckdb-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58df29096a43c1ad29f0a323babe0de1c2e15b0921f7642a35b0e9b2e05a766a", size = 13681675, upload-time = "2026-06-17T10:44:57.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5a/710056b19860f43bcdb6c4ad574fa012ac8488880d42cbf76c1b0690f0ba/duckdb-1.4.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:326429624e488faecafcee8c1d02668bf424b144f1ac6ef8706028c439c3f5ab", size = 18448570, upload-time = "2026-06-17T10:45:00.186Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b1/b9acfa09c7ed5e793f528886f9b7e207698d5cf1988b6e6a68a5bbcaffb4/duckdb-1.4.5-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45b6ac74a17a80d19e9da4b224115aac1ed691dcb56e271a88ee665c9e05c57a", size = 20448938, upload-time = "2026-06-17T10:45:03.33Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/05cb1adf33606877865bccebcb517e26a2090e4d89e5b0fe804d31222256/duckdb-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:00690b6aabd731144697a08bba16e35c748a3f06cefcc166ee8597159fc6bf6c", size = 12267440, upload-time = "2026-06-17T10:45:06.238Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ec/e9d71c5213ede2a6c47e7c9f37044301e3e9b4be3a44c9f9d5b2ac2d15e8/duckdb-1.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:00f0c430da0eff57d46a1c0fbc0d605ce66508fac0bc5c485067a19d8d4f0a2b", size = 13029594, upload-time = "2026-06-17T10:45:09.649Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ac/b30b1ddf2a4948e520c99eeb868de3d5299c2ffdfb94ca8cac2203f092c9/duckdb-1.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:09823cdf26dd0aa99a4c23a47f2b0a29c285a68db7e075f8603b678d8a3ddeb6", size = 28967421, upload-time = "2026-06-17T10:45:13.277Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/06fcf75bb9b22221b6f2fbb0c5327670e36974d05d84c8e5a73a87676477/duckdb-1.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c08999ed92ac66caecfc3945dd7184fdc145570e56ec5af6ec4dd84f1e1bab8c", size = 15388216, upload-time = "2026-06-17T10:45:16.374Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f7/cb0c5e2ed724de27fdb945ff5101c48216afe1aacc1294462658bfa7676e/duckdb-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07328a3e3a52221bd13c7dfc2f072be4fae84d42a5ef272d6fd497cda43e375f", size = 13705300, upload-time = "2026-06-17T10:45:19.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a2/dbc65b784ee731e246fe5b3066b61aa0afe01dbf4927d3f2db97ced45d6f/duckdb-1.4.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c72b1dcf27a71ef5f3dc14b92b9ed9274c5584bb0e88590b78907cbb8e254f3", size = 18476603, upload-time = "2026-06-17T10:45:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/f6fbb91cab7209acaffa1d861f54d67d55254d5c20d73191867a2f91d613/duckdb-1.4.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa294d028c149ca21110e366eaffcb4fc9ab11d7d203d50f7bc49a07ab34b960", size = 20483899, upload-time = "2026-06-17T10:45:26.431Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c0/cf35aeb21f9c94ec1fc409d21f746109959272356ee6a8b0479113f9eadc/duckdb-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b8d992d957c89e83d697756f6c5b5aea910d6bf16e2666da4c508f891932ae2", size = 12279480, upload-time = "2026-06-17T10:45:29.201Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c5/aef86244585028c344703d0bb7d23c0b7cc4d8f606e1e58fa8d43c61de6b/duckdb-1.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:47d2a6cbf7ccb8723d716150a3aa6c22647177876278aa781bf843d649011e72", size = 13036352, upload-time = "2026-06-17T10:45:31.894Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6e/6a4eb99ccbc7e0025a9d07899402a4cb2235943f5c17596c889654744c1a/duckdb-1.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d01a209288c3f96ffa230b6d09db2ab4c25dc936c379ca76a0a03f5d9f626877", size = 28967514, upload-time = "2026-06-17T10:45:35.084Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/0d5d0f200ec6f1c6bdd08d3568aa6b33b7b05fd7cb0b69aa234b37484251/duckdb-1.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e8345293e882459bc628eb8279f86f88e2eaf3e5512aaba3c86ae68530c1ca22", size = 15388459, upload-time = "2026-06-17T10:45:38.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2e/5ec931079f5ac0cd06d5b07cf5f0fdcd2b2b8fff26a7fc5d59c1767c1036/duckdb-1.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b7d36ffe6f2f318d2596b3fc8890d33feafda82058768d1be36434842ee1a458", size = 13705203, upload-time = "2026-06-17T10:45:41.137Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/8070360dde385797350c3b129381c4439e144b3d6a04271d505bf28e80b2/duckdb-1.4.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:414d50b59864582cf00e503c316d7ca5a8577ee628c62fc203993eba2ad51a69", size = 18477206, upload-time = "2026-06-17T10:45:44.044Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ef/408b94919c4b3674aed78bcc3d82bfccf32a2c6b1436f633ebb098d1542e/duckdb-1.4.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3569583e12d61f9b8446ca8a0e4ee25c2fe9b04c2b010c2e3bad26fc3d65882", size = 20482742, upload-time = "2026-06-17T10:45:47.126Z" }, + { url = "https://files.pythonhosted.org/packages/cd/eb/5921b7d628749629838549b0e6d0b24cdc1516cfad279d50267743f9bb31/duckdb-1.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:095084610af93d4b5c88f80e1691b380ea82c0d338452bcd4c77e8a3fa54047d", size = 12279386, upload-time = "2026-06-17T10:45:50.162Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b6/6be43fcdac3d3fd6f726e1fdc032d6ee1a17b9c019dadbc265cbaf8650ae/duckdb-1.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6f2ddc1267024a45bbcf011955353a4627199ef0d0b59815c9187edf03aaa45d", size = 13035812, upload-time = "2026-06-17T10:45:52.84Z" }, + { url = "https://files.pythonhosted.org/packages/a1/da/9b264e0590c7eba5201324109b92288b352aa976fe2767b4fc3888e04678/duckdb-1.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d840ec4e17674287adf8a6aa55ca923d8f437ef1ab8ac94d45295bcf4013f9dd", size = 28982957, upload-time = "2026-06-17T10:45:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d3/cc3461b6b933895025bdc129d22e6484cc0a0ce3cd4b6f7fa3c01ff97533/duckdb-1.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b80258133bafe9647e81e4e301987d0885cd977e0eee7b03949f23c0c8a548c1", size = 15392703, upload-time = "2026-06-17T10:45:59.142Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/77824a1fe0c73fe8190d940085950d8fd1afb0df789342182234964e0383/duckdb-1.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:81a95990020595a02aa157dc4c00a1d3eff25dc3c131e891d11ffee55ba6213c", size = 13708317, upload-time = "2026-06-17T10:46:01.795Z" }, + { url = "https://files.pythonhosted.org/packages/8e/82/b71c51548a675d383b5f32fcc13386d2c4e364b86a89c8374037691de18e/duckdb-1.4.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52f429653701676df74ccfbfb05baf9ee8cf46d830353574872d053142d6b018", size = 18480349, upload-time = "2026-06-17T10:46:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/38/d6/3d7a50c956fb9b7fccc5ca936daf55b8d52ffcfdd47bbebc401138da824c/duckdb-1.4.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64fe5e7ec74696788ce1e4157d1b70e45806756234c22c1a59bfcd28de1cae7b", size = 20488575, upload-time = "2026-06-17T10:46:07.688Z" }, + { url = "https://files.pythonhosted.org/packages/38/0a/9c8a286cdc0c2930b239aa849f647fed18e22582463110af160ff02dee36/duckdb-1.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:d95061ccce933d43e6d9d20bb527ec30bf9acfdf6950e7f6fb61f86b2ab93621", size = 12782419, upload-time = "2026-06-17T10:46:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6d/0dbbb910abb04e2e1df8f923c552c6f99869af1614cd6ef646f5ec00b63e/duckdb-1.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:9250c9315dcc5519da85fc9f7a26432f87d2b95b57513e5438a682118667b92b", size = 13494758, upload-time = "2026-06-17T10:46:13.68Z" }, + { url = "https://files.pythonhosted.org/packages/fb/18/f88a3caca49484fdc264fe3eac9cd341788cd36fcf6b63686b3a0950a238/duckdb-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dc2b8ca30e77f15ffad1db83363d8913ff646df003a6a9cd6e344a17a15f9fbf", size = 28919562, upload-time = "2026-06-17T10:46:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/62/32/2f0bcc423c248bc7181879c83ecb759a86095040b3b5cfe364f7cda16acd/duckdb-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f3c764e4cf66b56491f500439cac0a34a5e25952c91c4ce97cc09cefb708941", size = 15363340, upload-time = "2026-06-17T10:46:20.57Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/889aaae1385263fd4da997d531fcd9f91c82739381ec284727dd7678af7d/duckdb-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f14d34c3512a7a1533951e5b3e351adf2196ba4a9bb5f35b412fb9a82be0469c", size = 13678691, upload-time = "2026-06-17T10:46:23.47Z" }, + { url = "https://files.pythonhosted.org/packages/3f/1f/721b56fa27e5c0e7105a1a954c39da0cc0cc4a8d7455f37159dd3ccb439b/duckdb-1.4.5-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d53d64fda21c2a5830487499849e66532ba5c5b34161ca2b4542e58d3327ef", size = 18435452, upload-time = "2026-06-17T10:46:26.399Z" }, + { url = "https://files.pythonhosted.org/packages/cc/33/17c34961554c190d66d78340028e47aaba57fcff8a97ce78960d80f446e1/duckdb-1.4.5-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a10292e7981a5a3472c7ceddf233ae88adf4daa47e97e3e09ea1aa6d9d300b2", size = 20429376, upload-time = "2026-06-17T10:46:29.975Z" }, + { url = "https://files.pythonhosted.org/packages/8b/70/f32b8b77b3dc4ad7060aff36a679b47827a2dccd3aa68ffad92efdcb481f/duckdb-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:b10af1702c1dbf55099c777f27f21ce6ec0f3f1e2c54774b360278df3c8caaa7", size = 12265075, upload-time = "2026-06-17T10:46:32.961Z" }, +] + +[[package]] +name = "duckdb" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/19/e57151753576373c6696a12022648546cca6038e8833fda2908ee2342d9b/duckdb-1.5.5.tar.gz", hash = "sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238", size = 18066741, upload-time = "2026-07-22T10:55:17.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d4/298acf9331a80b3ce6ac64dd940e7e13f4058fb69d18914445f02e3c7bfe/duckdb-1.5.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f", size = 32702934, upload-time = "2026-07-22T10:53:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/c489fb63d64b2e7ee109ce8460bdede003a0f256e5b41a03a2a1c4764058/duckdb-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6", size = 17343604, upload-time = "2026-07-22T10:53:22.767Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/effa80a15b1f0c61c235622f797868485359e8c9ad6a8e358e7a0c479151/duckdb-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079", size = 15488179, upload-time = "2026-07-22T10:53:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/5d/07/21212345c8d24ba62dceaa20be3b21f5c46f1510b1b42ce93bb058afe0c4/duckdb-1.5.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c", size = 19367323, upload-time = "2026-07-22T10:53:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/10371ae875fb4b5ef61bb892743b4b2e90c512b371fdf29317deb744857d/duckdb-1.5.5-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf", size = 21476568, upload-time = "2026-07-22T10:53:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/09568ce617dd7bc0757b3d7b6a981660b9e4f0b7594de8ed776755eae740/duckdb-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb", size = 13156129, upload-time = "2026-07-22T10:53:37.55Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c2/b62ec24d57bb8df4e24b0b58f7f8facb32f5fdb9f1895aed9e9fcdded168/duckdb-1.5.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528", size = 32708371, upload-time = "2026-07-22T10:53:41.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ce/769171ba45f0b73632dc3bc3108d891e81dd6c6bbfba630a34a75b4dcc0f/duckdb-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a", size = 17343979, upload-time = "2026-07-22T10:53:44.951Z" }, + { url = "https://files.pythonhosted.org/packages/46/59/a8e3384ee916e00d5dcf985194c1511d61978540778a1e96fa47f9fb3e0d/duckdb-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4", size = 15493704, upload-time = "2026-07-22T10:53:47.912Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1d/9840179c2607b90523a2884a129c4d4e6dbdc1178ba62a976c1043beba88/duckdb-1.5.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f", size = 19366574, upload-time = "2026-07-22T10:53:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/b5/55/f9641a4eebcc2f4df631287d6c3b9ed2eea3b92644f93acbad825e3972b6/duckdb-1.5.5-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe", size = 21477952, upload-time = "2026-07-22T10:53:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3a/07c3556e37a5c97b95917b029c8fdde4a25fbd76a660bacdac195cf20dcb/duckdb-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc", size = 13156986, upload-time = "2026-07-22T10:53:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ff/07b48eef2078ca033847e9caa46cc7633b714c5f91ad1ce091c8ca89d792/duckdb-1.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c", size = 14001317, upload-time = "2026-07-22T10:54:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/40/2e05d324400fdaa5656c9f48d6298da421cb034d85e509fa0e6e325cf04b/duckdb-1.5.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e", size = 32753858, upload-time = "2026-07-22T10:54:05.514Z" }, + { url = "https://files.pythonhosted.org/packages/79/15/5ceb58ffb5bb8a62b3fd7abb39c41467cdf94850ece02e6d88664dfc75ce/duckdb-1.5.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7", size = 17368293, upload-time = "2026-07-22T10:54:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5c/bf02da0b354fe83cca4f95a4fbf762181af466f7d551ab2a093f7698882a/duckdb-1.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae", size = 15509131, upload-time = "2026-07-22T10:54:12.228Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a9/5f1f09da421d8e930e0b063d11c1b3f90363f40ede74438cd188afdd13a2/duckdb-1.5.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6", size = 19391959, upload-time = "2026-07-22T10:54:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/6549769f158126fa64fd6c1ac2eb59a18282146c939867a3eb31b7c1db07/duckdb-1.5.5-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1", size = 21510909, upload-time = "2026-07-22T10:54:19.681Z" }, + { url = "https://files.pythonhosted.org/packages/af/b7/5753b41d3124838f868f9f523362812d9fc45409e9e4dd70dcbb0a25826e/duckdb-1.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f", size = 13168544, upload-time = "2026-07-22T10:54:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/44b679c7d46245f8398feae7edac959d1b83d4eb143e25b3fce0630b78bd/duckdb-1.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282", size = 13988684, upload-time = "2026-07-22T10:54:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/47/37/4a38116e7700720fd152c666292214fd3abdf916496991296d8d1f66efbf/duckdb-1.5.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0", size = 32754294, upload-time = "2026-07-22T10:54:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/7d392f1ba1eee0eaf4ab4c8c7a604bfe3536cd63f979cf5c98798664f807/duckdb-1.5.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366", size = 17368211, upload-time = "2026-07-22T10:54:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a5/0a6f4fa60562faa615e55e15bd1953a2f2b17a8edd8105e5cda215e43457/duckdb-1.5.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c", size = 15509136, upload-time = "2026-07-22T10:54:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/023c89f51978545b9fab318581bba0c457a58e7530d2d933e54ae7d8647c/duckdb-1.5.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a", size = 19392147, upload-time = "2026-07-22T10:54:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/41bef391fb8b23dbc133c9f2ba016e7a7a8124513d2cc1b430f1897d87e4/duckdb-1.5.5-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053", size = 21511060, upload-time = "2026-07-22T10:54:42.788Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/c44dfc1f924ac29b3252dc1b91393c01d009dbfe9f8ed33f10b986151bd1/duckdb-1.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28", size = 13168028, upload-time = "2026-07-22T10:54:46.127Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/591384b2cd59abddd6f5dc175e60374f9abae6064429f0c4402854c10f44/duckdb-1.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de", size = 13989955, upload-time = "2026-07-22T10:54:49.262Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/12c65bfa2d2605b81981b264788891bcf11ec72227889554cead5d8d13b9/duckdb-1.5.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2", size = 32761946, upload-time = "2026-07-22T10:54:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/b9/46/682ce155f17e0d2822d4f13ee3db9ca4b5b7c2da61b841b2629035e1f4bc/duckdb-1.5.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8", size = 17375069, upload-time = "2026-07-22T10:54:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/39/ce/a24bcbd3289c8f305a430759c5fc12242740b4af3e17f7593f3a34e333d2/duckdb-1.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151", size = 15519791, upload-time = "2026-07-22T10:55:00.681Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/3a01afbc615c1d418c0de58a6b68ac5ce2a8563232c0464bfbc2ce552398/duckdb-1.5.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece", size = 19398251, upload-time = "2026-07-22T10:55:04.168Z" }, + { url = "https://files.pythonhosted.org/packages/a1/43/3a5e81d1728f4d234c79bfe385808ee7c04834f7c37a4b5c257459c25614/duckdb-1.5.5-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe", size = 21513851, upload-time = "2026-07-22T10:55:07.864Z" }, + { url = "https://files.pythonhosted.org/packages/91/41/fc7c829172c60ca22485251eab285f4f1a0d87b486a024c726f21471d86e/duckdb-1.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353", size = 13691858, upload-time = "2026-07-22T10:55:11.181Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/95d9216b79e9273689d7ebce125a54503ed0c9bd7da931f0265888e99779/duckdb-1.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433", size = 14470180, upload-time = "2026-07-22T10:55:14.445Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "faker" +version = "37.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, +] + +[[package]] +name = "faker" +version = "40.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/fb/35acc76128d5ee8983d940da871cc8eba876e7b0e9e6bd402ecd7104dcdc/faker-40.37.0.tar.gz", hash = "sha256:a92dff7f310e61fb544c61720e15edb2e7448bc33d15a321a99e9ab7b94abf54", size = 2025920, upload-time = "2026-08-21T16:33:16.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/22/bf589b6b2c7527047f55450358fbe5961aeaadf168d6b51a1a1c67da497f/faker-40.37.0-py3-none-any.whl", hash = "sha256:ddbafa55c94d5b69c08ced3a7f202614204a02e07ba6548c729b8d18acc0b490", size = 2062853, upload-time = "2026-08-21T16:33:14.516Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, + { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, + { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, + { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, +] + +[[package]] +name = "sqlite2duckdb" +version = "0.4.0" +source = { editable = "." } +dependencies = [ + { name = "duckdb", version = "1.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "duckdb", version = "1.5.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "faker", version = "37.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "faker", version = "40.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "duckdb", specifier = ">=1.1.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "faker" }, + { name = "pytest", specifier = ">=7.0" }, + { name = "ruff" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +]