sqlite2duckdb 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: sqlite2duckdb
3
+ Version: 0.4.0
4
+ Summary: A tool to convert sqlite database to duckdb database
5
+ Project-URL: Homepage, https://github.com/dridk/sqlite2duckdb
6
+ Project-URL: Issues, https://github.com/dridk/sqlite2duckdb/issues
7
+ Author-email: Sacha Schutz <sacha.schutz@pm.me>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: database,duckdb,olap,oltp,sqlite
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: duckdb>=1.1.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # sqlite2duckdb
23
+
24
+ ![CI](https://github.com/dridk/sqlite2duckdb/actions/workflows/ci.yml/badge.svg)
25
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqlite2duckdb)
26
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlite2duckdb)
27
+
28
+ A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckdb](https://duckdb.org/) database
29
+
30
+ ## Description
31
+
32
+ Sqlite is an embedded online database designed for transactional reading and writing.
33
+ Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency.
34
+
35
+ 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)
36
+
37
+ Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on).
38
+
39
+ ## Installation
40
+
41
+ With [uv](https://docs.astral.sh/uv/), no installation is required. `uvx` downloads and runs the tool in one go:
42
+
43
+ ```bash
44
+ uvx sqlite2duckdb source.db target.db
45
+ ```
46
+
47
+ To keep it around:
48
+
49
+ ```bash
50
+ uv tool install sqlite2duckdb
51
+ ```
52
+
53
+ Or with pip:
54
+
55
+ ```bash
56
+ pip install sqlite2duckdb
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### As a command line
62
+
63
+ ```
64
+ usage: sqlite2duckdb [-f] <sqlite_path> <duckdb_path>
65
+
66
+ Convert Sqlite database to Duckdb database
67
+
68
+ positional arguments:
69
+ sqlite_path sqlite file path
70
+ duckdb_path duckdb file path
71
+
72
+ options:
73
+ -h, --help show this help message and exit
74
+ -f, --force overwrite the duckdb file if it already exists
75
+ -q, --quiet only report errors
76
+ --verbose report every step
77
+ -v, --version show program's version number and exit
78
+ ```
79
+
80
+ The tool never overwrites an existing target silently. On a terminal it asks for
81
+ confirmation; anywhere else (a script, a CI job, a pipe) it exits with code 1 and tells you
82
+ to pass `--force`. Progress is written to stderr, so stdout stays free for pipelines.
83
+
84
+ ### Examples
85
+
86
+ ```bash
87
+ uvx sqlite2duckdb source.db target.db
88
+ uvx sqlite2duckdb --force source.db target.db # overwrite target.db without asking
89
+ ```
90
+
91
+ ### From python
92
+
93
+ ```python
94
+ from sqlite2duckdb import sqlite_to_duckdb
95
+
96
+ result = sqlite_to_duckdb("source.sqlite", "target.duckdb")
97
+ print(result.tables, result.elapsed)
98
+ ```
99
+
100
+ `sqlite_to_duckdb(sqlite_db, duck_db, *, overwrite=False)` accepts `str` or `pathlib.Path`
101
+ and returns a `ConversionResult` (`target`, `tables`, `elapsed`). It raises
102
+ `FileNotFoundError` if the source is missing and `FileExistsError` if the target already
103
+ exists and `overwrite` is False. If the conversion fails halfway, the partially written
104
+ target file is removed rather than left behind. Progress is reported through the standard
105
+ `logging` module (logger `sqlite2duckdb.sqlite_to_duckdb`), never printed.
106
+
107
+ ## What is converted
108
+
109
+ | | |
110
+ |---|---|
111
+ | Tables and data | ✅ |
112
+ | Primary keys, NOT NULL constraints, indexes | ✅ |
113
+ | UNIQUE, FOREIGN KEY and CHECK constraints | ❌ |
114
+ | Views | ❌ (silently dropped) |
115
+
116
+ Duckdb's sqlite extension does not expose the last two on the attached database, so they
117
+ cannot be copied. Reading them back from `sqlite_master` would be needed.
118
+
119
+ Tables are recreated from the DDL duckdb derives for the attached database, then filled
120
+ from it, and the indexes are read back from `sqlite_master`. This is what makes sqlite
121
+ files that quote their DDL with `[brackets]` (chinook.db, MS Access exports) convert
122
+ correctly: duckdb's own parser rejects that syntax, so the quoting is translated first.
123
+
124
+ ## Todo
125
+
126
+ - [ ] Custom type mapping
127
+ - [x] Primary keys, NOT NULL constraints and indexes
128
+ - [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints
129
+
130
+ ## Contributing
131
+
132
+ The project uses [uv](https://docs.astral.sh/uv/) for everything:
133
+
134
+ ```bash
135
+ make dev # uv sync — installs duckdb plus the test deps (pytest, faker, ruff)
136
+ make test # uv run pytest
137
+ make lint # uv run ruff check . && uv run ruff format --check .
138
+ make build # uv build
139
+ make publish # uv publish (PyPI trusted publishing, also run on tags by CI)
140
+ ```
141
+
142
+ ### See also
143
+
144
+ - [Harlequin](https://github.com/tconbeer/harlequin): A nice duckdb IDE for your terminal
@@ -0,0 +1,123 @@
1
+ # sqlite2duckdb
2
+
3
+ ![CI](https://github.com/dridk/sqlite2duckdb/actions/workflows/ci.yml/badge.svg)
4
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sqlite2duckdb)
5
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/sqlite2duckdb)
6
+
7
+ A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckdb](https://duckdb.org/) database
8
+
9
+ ## Description
10
+
11
+ Sqlite is an embedded online database designed for transactional reading and writing.
12
+ Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency.
13
+
14
+ 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)
15
+
16
+ Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on).
17
+
18
+ ## Installation
19
+
20
+ With [uv](https://docs.astral.sh/uv/), no installation is required. `uvx` downloads and runs the tool in one go:
21
+
22
+ ```bash
23
+ uvx sqlite2duckdb source.db target.db
24
+ ```
25
+
26
+ To keep it around:
27
+
28
+ ```bash
29
+ uv tool install sqlite2duckdb
30
+ ```
31
+
32
+ Or with pip:
33
+
34
+ ```bash
35
+ pip install sqlite2duckdb
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### As a command line
41
+
42
+ ```
43
+ usage: sqlite2duckdb [-f] <sqlite_path> <duckdb_path>
44
+
45
+ Convert Sqlite database to Duckdb database
46
+
47
+ positional arguments:
48
+ sqlite_path sqlite file path
49
+ duckdb_path duckdb file path
50
+
51
+ options:
52
+ -h, --help show this help message and exit
53
+ -f, --force overwrite the duckdb file if it already exists
54
+ -q, --quiet only report errors
55
+ --verbose report every step
56
+ -v, --version show program's version number and exit
57
+ ```
58
+
59
+ The tool never overwrites an existing target silently. On a terminal it asks for
60
+ confirmation; anywhere else (a script, a CI job, a pipe) it exits with code 1 and tells you
61
+ to pass `--force`. Progress is written to stderr, so stdout stays free for pipelines.
62
+
63
+ ### Examples
64
+
65
+ ```bash
66
+ uvx sqlite2duckdb source.db target.db
67
+ uvx sqlite2duckdb --force source.db target.db # overwrite target.db without asking
68
+ ```
69
+
70
+ ### From python
71
+
72
+ ```python
73
+ from sqlite2duckdb import sqlite_to_duckdb
74
+
75
+ result = sqlite_to_duckdb("source.sqlite", "target.duckdb")
76
+ print(result.tables, result.elapsed)
77
+ ```
78
+
79
+ `sqlite_to_duckdb(sqlite_db, duck_db, *, overwrite=False)` accepts `str` or `pathlib.Path`
80
+ and returns a `ConversionResult` (`target`, `tables`, `elapsed`). It raises
81
+ `FileNotFoundError` if the source is missing and `FileExistsError` if the target already
82
+ exists and `overwrite` is False. If the conversion fails halfway, the partially written
83
+ target file is removed rather than left behind. Progress is reported through the standard
84
+ `logging` module (logger `sqlite2duckdb.sqlite_to_duckdb`), never printed.
85
+
86
+ ## What is converted
87
+
88
+ | | |
89
+ |---|---|
90
+ | Tables and data | ✅ |
91
+ | Primary keys, NOT NULL constraints, indexes | ✅ |
92
+ | UNIQUE, FOREIGN KEY and CHECK constraints | ❌ |
93
+ | Views | ❌ (silently dropped) |
94
+
95
+ Duckdb's sqlite extension does not expose the last two on the attached database, so they
96
+ cannot be copied. Reading them back from `sqlite_master` would be needed.
97
+
98
+ Tables are recreated from the DDL duckdb derives for the attached database, then filled
99
+ from it, and the indexes are read back from `sqlite_master`. This is what makes sqlite
100
+ files that quote their DDL with `[brackets]` (chinook.db, MS Access exports) convert
101
+ correctly: duckdb's own parser rejects that syntax, so the quoting is translated first.
102
+
103
+ ## Todo
104
+
105
+ - [ ] Custom type mapping
106
+ - [x] Primary keys, NOT NULL constraints and indexes
107
+ - [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints
108
+
109
+ ## Contributing
110
+
111
+ The project uses [uv](https://docs.astral.sh/uv/) for everything:
112
+
113
+ ```bash
114
+ make dev # uv sync — installs duckdb plus the test deps (pytest, faker, ruff)
115
+ make test # uv run pytest
116
+ make lint # uv run ruff check . && uv run ruff format --check .
117
+ make build # uv build
118
+ make publish # uv publish (PyPI trusted publishing, also run on tags by CI)
119
+ ```
120
+
121
+ ### See also
122
+
123
+ - [Harlequin](https://github.com/tconbeer/harlequin): A nice duckdb IDE for your terminal
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "sqlite2duckdb"
8
+ version = "0.4.0"
9
+ authors = [{name="Sacha Schutz", email="sacha.schutz@pm.me"}]
10
+ description = "A tool to convert sqlite database to duckdb database"
11
+ readme = "README.md"
12
+ requires-python = ">=3.9"
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ keywords = ["sqlite", "duckdb", "database", "olap", "oltp"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+ dependencies = [
26
+ 'duckdb >= 1.1.0'
27
+ ]
28
+
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/dridk/sqlite2duckdb"
32
+ Issues = "https://github.com/dridk/sqlite2duckdb/issues"
33
+
34
+ [project.scripts]
35
+ sqlite2duckdb = "sqlite2duckdb.__main__:main_cli"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest >= 7.0",
40
+ "faker",
41
+ "ruff",
42
+ ]
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["sqlite2duckdb"]
46
+
47
+ # Allow list rather than a deny list, so that anything new landing in the repo
48
+ # stays out of the distribution unless it is explicitly wanted.
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["sqlite2duckdb", "tests", "README.md"]
51
+
52
+ [tool.pytest.ini_options]
53
+ pythonpath = ["."]
54
+ testpaths = ["tests"]
55
+
56
+ [tool.ruff]
57
+ target-version = "py39"
58
+
59
+ [tool.ruff.lint]
60
+ extend-select = ["I", "UP"]
@@ -0,0 +1,14 @@
1
+ import importlib.metadata
2
+
3
+ from sqlite2duckdb.sqlite_to_duckdb import ConversionResult, sqlite_to_duckdb
4
+
5
+ try:
6
+ __version__ = importlib.metadata.version("sqlite2duckdb")
7
+ except importlib.metadata.PackageNotFoundError:
8
+ # Running from a source checkout that was never installed.
9
+ __version__ = "0.0.0.dev0"
10
+
11
+ # Deprecated alias, kept so that existing imports keep working.
12
+ __VERSION__ = __version__
13
+
14
+ __all__ = ["ConversionResult", "__version__", "sqlite_to_duckdb"]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ import sys
7
+
8
+ from sqlite2duckdb import __version__, sqlite_to_duckdb
9
+
10
+
11
+ def main_cli() -> int:
12
+ parser = argparse.ArgumentParser(
13
+ prog="sqlite2duckdb",
14
+ description="Convert Sqlite database to Duckdb database",
15
+ usage="sqlite2duckdb [-f] <sqlite_path> <duckdb_path>",
16
+ )
17
+
18
+ parser.add_argument("sqlite_path", type=str, help="sqlite file path")
19
+ parser.add_argument("duckdb_path", type=str, help="duckdb file path")
20
+ parser.add_argument(
21
+ "-f",
22
+ "--force",
23
+ action="store_true",
24
+ help="overwrite the duckdb file if it already exists",
25
+ )
26
+ parser.add_argument("-q", "--quiet", action="store_true", help="only report errors")
27
+ parser.add_argument("--verbose", action="store_true", help="report every step")
28
+ parser.add_argument(
29
+ "-v", "--version", action="version", version=f"sqlite2duckdb {__version__}"
30
+ )
31
+
32
+ args = parser.parse_args()
33
+
34
+ if args.quiet:
35
+ level = logging.WARNING
36
+ elif args.verbose:
37
+ level = logging.DEBUG
38
+ else:
39
+ level = logging.INFO
40
+ # Progress goes to stderr so that stdout stays free for pipelines.
41
+ logging.basicConfig(level=level, format="%(message)s", stream=sys.stderr)
42
+
43
+ overwrite = args.force
44
+ if not overwrite and os.path.exists(args.duckdb_path):
45
+ if not sys.stdin.isatty():
46
+ print(
47
+ f"{args.duckdb_path} already exists. Use --force to overwrite it.",
48
+ file=sys.stderr,
49
+ )
50
+ return 1
51
+ try:
52
+ answer = (
53
+ input(
54
+ f"{args.duckdb_path} already exists. do you want to delete this file ? (yes/no): "
55
+ )
56
+ .strip()
57
+ .lower()
58
+ )
59
+ except EOFError:
60
+ print(f"{args.duckdb_path} already exists.", file=sys.stderr)
61
+ return 1
62
+ if answer not in ("yes", "y"):
63
+ return 1
64
+ overwrite = True
65
+
66
+ try:
67
+ sqlite_to_duckdb(args.sqlite_path, args.duckdb_path, overwrite=overwrite)
68
+ except (FileNotFoundError, FileExistsError) as error:
69
+ print(f"error: {error}", file=sys.stderr)
70
+ return 1
71
+
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main_cli())
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import logging
5
+ import os
6
+ import sqlite3
7
+ import time
8
+ from dataclasses import dataclass
9
+
10
+ import duckdb
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class ConversionResult:
17
+ """What a successful conversion produced."""
18
+
19
+ target: str
20
+ tables: int
21
+ elapsed: float
22
+ """Wall clock duration of the conversion, in seconds."""
23
+
24
+
25
+ def _quote_identifier(name: str) -> str:
26
+ return '"' + name.replace('"', '""') + '"'
27
+
28
+
29
+ def _format_duration(seconds: float) -> str:
30
+ if seconds < 1:
31
+ return f"{seconds * 1000:.2f} ms"
32
+ return f"{seconds:.2f} s"
33
+
34
+
35
+ def _remove_quietly(path: str) -> None:
36
+ for candidate in (path, path + ".wal"):
37
+ try:
38
+ os.remove(candidate)
39
+ except OSError:
40
+ pass
41
+
42
+
43
+ def _brackets_to_quotes(sql: str) -> str:
44
+ """Rewrite sqlite's [bracket] identifiers into standard "double quoted" ones.
45
+
46
+ Quoted runs are copied verbatim so that a `[` inside a string literal survives.
47
+ """
48
+
49
+ out = []
50
+ index = 0
51
+ length = len(sql)
52
+
53
+ while index < length:
54
+ char = sql[index]
55
+
56
+ if char in "'\"`":
57
+ end = index + 1
58
+ while end < length:
59
+ if sql[end] == char:
60
+ if end + 1 < length and sql[end + 1] == char:
61
+ end += 2
62
+ continue
63
+ break
64
+ end += 1
65
+ out.append(sql[index : end + 1])
66
+ index = end + 1
67
+
68
+ elif char == "[":
69
+ end = sql.find("]", index)
70
+ if end == -1:
71
+ out.append(char)
72
+ index += 1
73
+ else:
74
+ out.append(_quote_identifier(sql[index + 1 : end]))
75
+ index = end + 1
76
+
77
+ else:
78
+ out.append(char)
79
+ index += 1
80
+
81
+ return "".join(out)
82
+
83
+
84
+ def _copy_indexes(conn: duckdb.DuckDBPyConnection, sqlite_path: str) -> None:
85
+ """Recreate the source indexes, translating sqlite quoting on the way."""
86
+
87
+ with contextlib.closing(sqlite3.connect(sqlite_path)) as source:
88
+ indexes = source.execute(
89
+ "SELECT name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL"
90
+ ).fetchall()
91
+
92
+ for name, sql in indexes:
93
+ try:
94
+ conn.sql(_brackets_to_quotes(sql))
95
+ except duckdb.Error as error:
96
+ logger.warning("Could not recreate index %s: %s", name, error)
97
+
98
+
99
+ def _copy_tables(
100
+ conn: duckdb.DuckDBPyConnection, tables: list[tuple[str, str]]
101
+ ) -> None:
102
+ """Recreate each table then fill it from the attached database."""
103
+
104
+ for table_name, ddl in tables:
105
+ # This DDL is regenerated by duckdb from the attached catalog, so unlike
106
+ # sqlite's own it always parses, and it still carries the primary keys
107
+ # and the NOT NULL constraints.
108
+ conn.sql(ddl)
109
+ quoted = _quote_identifier(table_name)
110
+ conn.sql(f"INSERT INTO {quoted} SELECT * FROM __other.{quoted}")
111
+
112
+
113
+ def sqlite_to_duckdb(
114
+ sqlite_db: str | os.PathLike[str],
115
+ duck_db: str | os.PathLike[str],
116
+ *,
117
+ overwrite: bool = False,
118
+ ) -> ConversionResult:
119
+ """Copy a sqlite database into a new duckdb database.
120
+
121
+ Tables, data, primary keys, NOT NULL constraints and indexes are copied.
122
+ Views and UNIQUE / FOREIGN KEY / CHECK constraints are not: duckdb's sqlite
123
+ extension does not expose them on the attached database.
124
+
125
+ Raises FileNotFoundError if `sqlite_db` does not exist, and FileExistsError
126
+ if `duck_db` already exists and `overwrite` is False.
127
+ """
128
+
129
+ sqlite_path = os.fspath(sqlite_db)
130
+ duck_path = os.fspath(duck_db)
131
+
132
+ if not os.path.exists(sqlite_path):
133
+ raise FileNotFoundError(f"File {sqlite_path} doesn't exist")
134
+
135
+ if os.path.exists(duck_path):
136
+ if not overwrite:
137
+ raise FileExistsError(f"Database {duck_path} already exists")
138
+ _remove_quietly(duck_path)
139
+
140
+ logger.info("Create %s database", duck_path)
141
+ start_time = time.perf_counter()
142
+
143
+ conn = duckdb.connect(duck_path)
144
+ try:
145
+ ## Install sqlite
146
+ conn.sql("INSTALL sqlite; LOAD sqlite;")
147
+
148
+ # Bound parameters are not allowed in ATTACH, so escape the quotes ourselves
149
+ source_path = sqlite_path.replace("'", "''")
150
+ conn.sql(f"ATTACH '{source_path}' AS __other (TYPE SQLITE, READ_ONLY)")
151
+
152
+ ## Get sqlite Names, along with the CREATE statement duckdb derives for them
153
+ tables = conn.sql(
154
+ "SELECT table_name, sql FROM duckdb_tables WHERE database_name = '__other'"
155
+ ).fetchall()
156
+ logger.info("%d table(s) found", len(tables))
157
+
158
+ _copy_tables(conn, tables)
159
+ _copy_indexes(conn, sqlite_path)
160
+
161
+ conn.sql("DETACH __other")
162
+ except BaseException:
163
+ # Never leave a half-written database behind: it would trip the
164
+ # "already exists" guard on the next run.
165
+ conn.close()
166
+ _remove_quietly(duck_path)
167
+ raise
168
+
169
+ conn.close()
170
+
171
+ elapsed = time.perf_counter() - start_time
172
+ logger.info("Done in %s !", _format_duration(elapsed))
173
+
174
+ return ConversionResult(target=duck_path, tables=len(tables), elapsed=elapsed)
@@ -0,0 +1,51 @@
1
+ import pytest
2
+
3
+ from tests import utils
4
+
5
+
6
+ def _module_db(tmp_path_factory, name, builder):
7
+ path = tmp_path_factory.mktemp(name) / f"{name}.sqlite"
8
+ builder(path)
9
+ return path
10
+
11
+
12
+ @pytest.fixture(scope="module")
13
+ def fake_sqlite(tmp_path_factory):
14
+ return _module_db(tmp_path_factory, "fake", utils.build_fake_sqlite)
15
+
16
+
17
+ @pytest.fixture(scope="module")
18
+ def edge_case_sqlite(tmp_path_factory):
19
+ return _module_db(tmp_path_factory, "edge", utils.build_edge_case_sqlite)
20
+
21
+
22
+ @pytest.fixture(scope="module")
23
+ def bracket_sqlite(tmp_path_factory):
24
+ return _module_db(tmp_path_factory, "bracket", utils.build_bracket_sqlite)
25
+
26
+
27
+ @pytest.fixture(scope="module")
28
+ def dotted_column_sqlite(tmp_path_factory):
29
+ return _module_db(tmp_path_factory, "dotted", utils.build_dotted_column_sqlite)
30
+
31
+
32
+ @pytest.fixture(scope="module")
33
+ def fidelity_sqlite(tmp_path_factory):
34
+ return _module_db(tmp_path_factory, "fidelity", utils.build_fidelity_sqlite)
35
+
36
+
37
+ @pytest.fixture(scope="module")
38
+ def empty_sqlite(tmp_path_factory):
39
+ return _module_db(tmp_path_factory, "empty", utils.build_empty_sqlite)
40
+
41
+
42
+ @pytest.fixture
43
+ def duckdb_path(tmp_path):
44
+ """A path where the target database does not exist yet."""
45
+
46
+ return tmp_path / "target.duckdb"
47
+
48
+
49
+ @pytest.fixture(scope="module")
50
+ def bracket_index_sqlite(tmp_path_factory):
51
+ return _module_db(tmp_path_factory, "bindex", utils.build_bracket_index_sqlite)
@@ -0,0 +1,29 @@
1
+ from sqlite2duckdb.sqlite_to_duckdb import _brackets_to_quotes
2
+
3
+
4
+ def test_translates_bracket_identifiers():
5
+ assert (
6
+ _brackets_to_quotes("CREATE INDEX [i] ON [my table] ([a], [b])")
7
+ == 'CREATE INDEX "i" ON "my table" ("a", "b")'
8
+ )
9
+
10
+
11
+ def test_leaves_string_literals_alone():
12
+ assert (
13
+ _brackets_to_quotes("CREATE INDEX [i] ON t (x) WHERE y = 'a [b] c'")
14
+ == "CREATE INDEX \"i\" ON t (x) WHERE y = 'a [b] c'"
15
+ )
16
+
17
+
18
+ def test_leaves_already_quoted_identifiers_alone():
19
+ assert _brackets_to_quotes('CREATE INDEX i ON "a [b]" (c)') == (
20
+ 'CREATE INDEX i ON "a [b]" (c)'
21
+ )
22
+
23
+
24
+ def test_escapes_double_quotes_inside_brackets():
25
+ assert _brackets_to_quotes('SELECT [a"b]') == 'SELECT "a""b"'
26
+
27
+
28
+ def test_leaves_unterminated_bracket_alone():
29
+ assert _brackets_to_quotes("SELECT [oops") == "SELECT [oops"