sqlite2duckdb 0.4.0__tar.gz → 0.5.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.
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/.gitignore +3 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/PKG-INFO +13 -29
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/README.md +12 -28
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/pyproject.toml +1 -1
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/sqlite2duckdb/sqlite_to_duckdb.py +97 -4
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/conftest.py +10 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/test_convert.py +5 -0
- sqlite2duckdb-0.5.0/tests/test_views_and_unique.py +87 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/utils.py +50 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/LICENSE +0 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/sqlite2duckdb/__init__.py +0 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/sqlite2duckdb/__main__.py +0 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/test_brackets.py +0 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/test_cli.py +0 -0
- {sqlite2duckdb-0.4.0 → sqlite2duckdb-0.5.0}/tests/test_examples.py +0 -0
|
@@ -158,3 +158,6 @@ cython_debug/
|
|
|
158
158
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
159
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
160
|
#.idea/
|
|
161
|
+
|
|
162
|
+
# Test databases generated at the repo root
|
|
163
|
+
/*.db
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlite2duckdb
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A tool to convert sqlite database to duckdb database
|
|
5
5
|
Project-URL: Homepage, https://github.com/dridk/sqlite2duckdb
|
|
6
6
|
Project-URL: Issues, https://github.com/dridk/sqlite2duckdb/issues
|
|
@@ -32,9 +32,8 @@ A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckd
|
|
|
32
32
|
Sqlite is an embedded online database designed for transactional reading and writing.
|
|
33
33
|
Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency.
|
|
34
34
|
|
|
35
|
-
For more details [
|
|
35
|
+
For more details [Medium post](https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777)
|
|
36
36
|
|
|
37
|
-
Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on).
|
|
38
37
|
|
|
39
38
|
## Installation
|
|
40
39
|
|
|
@@ -77,10 +76,6 @@ options:
|
|
|
77
76
|
-v, --version show program's version number and exit
|
|
78
77
|
```
|
|
79
78
|
|
|
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
79
|
### Examples
|
|
85
80
|
|
|
86
81
|
```bash
|
|
@@ -94,38 +89,27 @@ uvx sqlite2duckdb --force source.db target.db # overwrite target.db without as
|
|
|
94
89
|
from sqlite2duckdb import sqlite_to_duckdb
|
|
95
90
|
|
|
96
91
|
result = sqlite_to_duckdb("source.sqlite", "target.duckdb")
|
|
97
|
-
print(result.tables, result.elapsed)
|
|
92
|
+
print(result.tables, result.views, result.elapsed)
|
|
98
93
|
```
|
|
99
94
|
|
|
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
95
|
## What is converted
|
|
108
96
|
|
|
109
97
|
| | |
|
|
110
98
|
|---|---|
|
|
111
99
|
| Tables and data | ✅ |
|
|
112
|
-
| Primary keys, NOT NULL constraints
|
|
113
|
-
|
|
|
114
|
-
| Views |
|
|
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.
|
|
100
|
+
| Primary keys, NOT NULL and UNIQUE constraints | ✅ |
|
|
101
|
+
| Indexes | ✅ |
|
|
102
|
+
| Views | ✅ best effort |
|
|
103
|
+
| FOREIGN KEY and CHECK constraints | ❌ |
|
|
118
104
|
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
105
|
+
A view whose SQL uses something duckdb has no equivalent for (`MATCH`, `julianday()`) is
|
|
106
|
+
skipped with a warning instead of failing the conversion.
|
|
123
107
|
|
|
124
|
-
|
|
108
|
+
FOREIGN KEY and CHECK are not copied, though not for lack of support: duckdb accepts and
|
|
109
|
+
enforces both in `CREATE TABLE`. It checks foreign keys row by row, so a self-referencing
|
|
110
|
+
table cannot be bulk loaded, and there is no `ALTER TABLE ADD CONSTRAINT` to add them once
|
|
111
|
+
the data is in.
|
|
125
112
|
|
|
126
|
-
- [ ] Custom type mapping
|
|
127
|
-
- [x] Primary keys, NOT NULL constraints and indexes
|
|
128
|
-
- [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints
|
|
129
113
|
|
|
130
114
|
## Contributing
|
|
131
115
|
|
|
@@ -11,9 +11,8 @@ A tool for converting a [sqlite](https://www.sqlite.org/) database into a [duckd
|
|
|
11
11
|
Sqlite is an embedded online database designed for transactional reading and writing.
|
|
12
12
|
Duckdb is also an embedded database, but column-oriented, designed for analytical process with a very high reading efficiency.
|
|
13
13
|
|
|
14
|
-
For more details [
|
|
14
|
+
For more details [Medium post](https://towardsdatascience.com/forget-about-sqlite-use-duckdb-instead-and-thank-me-later-df76ee9bb777)
|
|
15
15
|
|
|
16
|
-
Requires Python >= 3.9 and duckdb >= 1.1.0 (indexes are only copied from that version on).
|
|
17
16
|
|
|
18
17
|
## Installation
|
|
19
18
|
|
|
@@ -56,10 +55,6 @@ options:
|
|
|
56
55
|
-v, --version show program's version number and exit
|
|
57
56
|
```
|
|
58
57
|
|
|
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
58
|
### Examples
|
|
64
59
|
|
|
65
60
|
```bash
|
|
@@ -73,38 +68,27 @@ uvx sqlite2duckdb --force source.db target.db # overwrite target.db without as
|
|
|
73
68
|
from sqlite2duckdb import sqlite_to_duckdb
|
|
74
69
|
|
|
75
70
|
result = sqlite_to_duckdb("source.sqlite", "target.duckdb")
|
|
76
|
-
print(result.tables, result.elapsed)
|
|
71
|
+
print(result.tables, result.views, result.elapsed)
|
|
77
72
|
```
|
|
78
73
|
|
|
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
74
|
## What is converted
|
|
87
75
|
|
|
88
76
|
| | |
|
|
89
77
|
|---|---|
|
|
90
78
|
| Tables and data | ✅ |
|
|
91
|
-
| Primary keys, NOT NULL constraints
|
|
92
|
-
|
|
|
93
|
-
| Views |
|
|
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.
|
|
79
|
+
| Primary keys, NOT NULL and UNIQUE constraints | ✅ |
|
|
80
|
+
| Indexes | ✅ |
|
|
81
|
+
| Views | ✅ best effort |
|
|
82
|
+
| FOREIGN KEY and CHECK constraints | ❌ |
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
84
|
+
A view whose SQL uses something duckdb has no equivalent for (`MATCH`, `julianday()`) is
|
|
85
|
+
skipped with a warning instead of failing the conversion.
|
|
102
86
|
|
|
103
|
-
|
|
87
|
+
FOREIGN KEY and CHECK are not copied, though not for lack of support: duckdb accepts and
|
|
88
|
+
enforces both in `CREATE TABLE`. It checks foreign keys row by row, so a self-referencing
|
|
89
|
+
table cannot be bulk loaded, and there is no `ALTER TABLE ADD CONSTRAINT` to add them once
|
|
90
|
+
the data is in.
|
|
104
91
|
|
|
105
|
-
- [ ] Custom type mapping
|
|
106
|
-
- [x] Primary keys, NOT NULL constraints and indexes
|
|
107
|
-
- [ ] Views, and UNIQUE / FOREIGN KEY / CHECK constraints
|
|
108
92
|
|
|
109
93
|
## Contributing
|
|
110
94
|
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "sqlite2duckdb"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.5.0"
|
|
9
9
|
authors = [{name="Sacha Schutz", email="sacha.schutz@pm.me"}]
|
|
10
10
|
description = "A tool to convert sqlite database to duckdb database"
|
|
11
11
|
readme = "README.md"
|
|
@@ -20,6 +20,7 @@ class ConversionResult:
|
|
|
20
20
|
tables: int
|
|
21
21
|
elapsed: float
|
|
22
22
|
"""Wall clock duration of the conversion, in seconds."""
|
|
23
|
+
views: int = 0
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _quote_identifier(name: str) -> str:
|
|
@@ -96,6 +97,88 @@ def _copy_indexes(conn: duckdb.DuckDBPyConnection, sqlite_path: str) -> None:
|
|
|
96
97
|
logger.warning("Could not recreate index %s: %s", name, error)
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
def _copy_unique_constraints(
|
|
101
|
+
conn: duckdb.DuckDBPyConnection, sqlite_path: str, table_names: list[str]
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Replay the UNIQUE constraints that sqlite records without any SQL.
|
|
104
|
+
|
|
105
|
+
A column or table level UNIQUE becomes an autoindex whose sqlite_master row
|
|
106
|
+
has a NULL sql, so _copy_indexes cannot see it. Duckdb has no ALTER TABLE ADD
|
|
107
|
+
CONSTRAINT either, so a unique index is how the guarantee is carried over.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
with contextlib.closing(sqlite3.connect(sqlite_path)) as source:
|
|
111
|
+
for table in table_names:
|
|
112
|
+
indexes = source.execute(
|
|
113
|
+
f"PRAGMA index_list({_quote_identifier(table)})"
|
|
114
|
+
).fetchall()
|
|
115
|
+
|
|
116
|
+
for _, index_name, unique, origin, _partial in indexes:
|
|
117
|
+
# 'c' indexes carry their own SQL and are handled by _copy_indexes,
|
|
118
|
+
# and 'pk' is already part of the table DDL.
|
|
119
|
+
if not unique or origin != "u":
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
columns = [
|
|
123
|
+
row[2]
|
|
124
|
+
for row in source.execute(
|
|
125
|
+
f"PRAGMA index_info({_quote_identifier(index_name)})"
|
|
126
|
+
).fetchall()
|
|
127
|
+
]
|
|
128
|
+
if any(column is None for column in columns):
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Skipping unique index %s: it is built on an expression",
|
|
131
|
+
index_name,
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
targets = ", ".join(_quote_identifier(column) for column in columns)
|
|
136
|
+
try:
|
|
137
|
+
conn.sql(
|
|
138
|
+
f"CREATE UNIQUE INDEX {_quote_identifier(index_name)} "
|
|
139
|
+
f"ON {_quote_identifier(table)} ({targets})"
|
|
140
|
+
)
|
|
141
|
+
except duckdb.Error as error:
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Could not recreate unique index %s: %s", index_name, error
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _copy_views(conn: duckdb.DuckDBPyConnection, sqlite_path: str) -> int:
|
|
148
|
+
"""Recreate the source views, and return how many made it across."""
|
|
149
|
+
|
|
150
|
+
with contextlib.closing(sqlite3.connect(sqlite_path)) as source:
|
|
151
|
+
views = source.execute(
|
|
152
|
+
"SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql IS NOT NULL"
|
|
153
|
+
).fetchall()
|
|
154
|
+
|
|
155
|
+
pending = [(name, _brackets_to_quotes(sql)) for name, sql in views]
|
|
156
|
+
errors: dict[str, duckdb.Error] = {}
|
|
157
|
+
created = 0
|
|
158
|
+
|
|
159
|
+
# A view can sit on top of another one and sqlite_master does not guarantee
|
|
160
|
+
# dependency order, so keep retrying while a pass still makes progress.
|
|
161
|
+
while pending:
|
|
162
|
+
failed = []
|
|
163
|
+
for name, statement in pending:
|
|
164
|
+
try:
|
|
165
|
+
conn.sql(statement)
|
|
166
|
+
except duckdb.Error as error:
|
|
167
|
+
errors[name] = error
|
|
168
|
+
failed.append((name, statement))
|
|
169
|
+
else:
|
|
170
|
+
created += 1
|
|
171
|
+
|
|
172
|
+
if len(failed) == len(pending):
|
|
173
|
+
break
|
|
174
|
+
pending = failed
|
|
175
|
+
|
|
176
|
+
for name, _ in pending:
|
|
177
|
+
logger.warning("Could not recreate view %s: %s", name, errors[name])
|
|
178
|
+
|
|
179
|
+
return created
|
|
180
|
+
|
|
181
|
+
|
|
99
182
|
def _copy_tables(
|
|
100
183
|
conn: duckdb.DuckDBPyConnection, tables: list[tuple[str, str]]
|
|
101
184
|
) -> None:
|
|
@@ -118,9 +201,13 @@ def sqlite_to_duckdb(
|
|
|
118
201
|
) -> ConversionResult:
|
|
119
202
|
"""Copy a sqlite database into a new duckdb database.
|
|
120
203
|
|
|
121
|
-
Tables, data, primary keys, NOT NULL
|
|
122
|
-
|
|
123
|
-
|
|
204
|
+
Tables, data, views, primary keys, NOT NULL and UNIQUE constraints and
|
|
205
|
+
indexes are copied. FOREIGN KEY and CHECK constraints are not: duckdb checks
|
|
206
|
+
foreign keys row by row, so a self-referencing table cannot be bulk loaded,
|
|
207
|
+
and there is no ALTER TABLE ADD CONSTRAINT to add them once the data is in.
|
|
208
|
+
|
|
209
|
+
A view duckdb cannot bind is skipped with a warning rather than failing the
|
|
210
|
+
whole conversion.
|
|
124
211
|
|
|
125
212
|
Raises FileNotFoundError if `sqlite_db` does not exist, and FileExistsError
|
|
126
213
|
if `duck_db` already exists and `overwrite` is False.
|
|
@@ -157,6 +244,10 @@ def sqlite_to_duckdb(
|
|
|
157
244
|
|
|
158
245
|
_copy_tables(conn, tables)
|
|
159
246
|
_copy_indexes(conn, sqlite_path)
|
|
247
|
+
_copy_unique_constraints(conn, sqlite_path, [name for name, _ in tables])
|
|
248
|
+
views = _copy_views(conn, sqlite_path)
|
|
249
|
+
if views:
|
|
250
|
+
logger.info("%d view(s) copied", views)
|
|
160
251
|
|
|
161
252
|
conn.sql("DETACH __other")
|
|
162
253
|
except BaseException:
|
|
@@ -171,4 +262,6 @@ def sqlite_to_duckdb(
|
|
|
171
262
|
elapsed = time.perf_counter() - start_time
|
|
172
263
|
logger.info("Done in %s !", _format_duration(elapsed))
|
|
173
264
|
|
|
174
|
-
return ConversionResult(
|
|
265
|
+
return ConversionResult(
|
|
266
|
+
target=duck_path, tables=len(tables), elapsed=elapsed, views=views
|
|
267
|
+
)
|
|
@@ -49,3 +49,13 @@ def duckdb_path(tmp_path):
|
|
|
49
49
|
@pytest.fixture(scope="module")
|
|
50
50
|
def bracket_index_sqlite(tmp_path_factory):
|
|
51
51
|
return _module_db(tmp_path_factory, "bindex", utils.build_bracket_index_sqlite)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture(scope="module")
|
|
55
|
+
def views_sqlite(tmp_path_factory):
|
|
56
|
+
return _module_db(tmp_path_factory, "views", utils.build_views_sqlite)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(scope="module")
|
|
60
|
+
def unique_sqlite(tmp_path_factory):
|
|
61
|
+
return _module_db(tmp_path_factory, "unique", utils.build_unique_sqlite)
|
|
@@ -95,6 +95,11 @@ def test_bracket_quoted_view(bracket_sqlite, duckdb_path):
|
|
|
95
95
|
(2, 7),
|
|
96
96
|
]
|
|
97
97
|
|
|
98
|
+
# The bracket quoted view built on it comes across too.
|
|
99
|
+
assert d_conn.sql(
|
|
100
|
+
'SELECT * FROM "Order Subtotals" ORDER BY OrderID'
|
|
101
|
+
).fetchall() == [(1, 5), (2, 7)]
|
|
102
|
+
|
|
98
103
|
|
|
99
104
|
def test_dotted_column_names(dotted_column_sqlite, duckdb_path):
|
|
100
105
|
"""Regression test for issue #4: a STRICT table with numeric-looking column
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Views and UNIQUE constraints, which duckdb's sqlite extension does not expose
|
|
2
|
+
on an attached database and which have to be read back from sqlite_master."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import duckdb
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from sqlite2duckdb import sqlite_to_duckdb
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_views_are_copied(views_sqlite, duckdb_path):
|
|
13
|
+
result = sqlite_to_duckdb(views_sqlite, duckdb_path)
|
|
14
|
+
|
|
15
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
16
|
+
|
|
17
|
+
assert d_conn.sql("SELECT * FROM by_region ORDER BY region").fetchall() == [
|
|
18
|
+
("north", 15),
|
|
19
|
+
("south", 7),
|
|
20
|
+
]
|
|
21
|
+
assert result.views == 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_view_built_on_another_view(views_sqlite, duckdb_path):
|
|
25
|
+
sqlite_to_duckdb(views_sqlite, duckdb_path)
|
|
26
|
+
|
|
27
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
28
|
+
|
|
29
|
+
assert d_conn.sql("SELECT * FROM big_regions").fetchall() == [("north",)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_bracket_quoted_view_is_translated(views_sqlite, duckdb_path):
|
|
33
|
+
sqlite_to_duckdb(views_sqlite, duckdb_path)
|
|
34
|
+
|
|
35
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
36
|
+
|
|
37
|
+
assert d_conn.sql('SELECT COUNT(*) FROM "north sales"').fetchone() == (2,)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_view_duckdb_cannot_parse_is_skipped_with_a_warning(
|
|
41
|
+
views_sqlite, duckdb_path, caplog
|
|
42
|
+
):
|
|
43
|
+
with caplog.at_level(logging.WARNING, logger="sqlite2duckdb.sqlite_to_duckdb"):
|
|
44
|
+
sqlite_to_duckdb(views_sqlite, duckdb_path)
|
|
45
|
+
|
|
46
|
+
assert "matched" in caplog.text
|
|
47
|
+
|
|
48
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
49
|
+
views = {
|
|
50
|
+
row[0]
|
|
51
|
+
for row in d_conn.sql(
|
|
52
|
+
"SELECT view_name FROM duckdb_views() WHERE NOT internal"
|
|
53
|
+
).fetchall()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
assert "matched" not in views
|
|
57
|
+
# The rest of the database must still be intact.
|
|
58
|
+
assert d_conn.sql("SELECT COUNT(*) FROM sales").fetchone() == (3,)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_unique_constraints_are_enforced(unique_sqlite, duckdb_path):
|
|
62
|
+
sqlite_to_duckdb(unique_sqlite, duckdb_path)
|
|
63
|
+
|
|
64
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
65
|
+
|
|
66
|
+
# Column level UNIQUE, recorded by sqlite as an autoindex with no SQL.
|
|
67
|
+
with pytest.raises(duckdb.ConstraintException):
|
|
68
|
+
d_conn.sql("INSERT INTO members VALUES (2, 'ada@example.com', 'x', 'y')")
|
|
69
|
+
|
|
70
|
+
# Table level UNIQUE over two columns.
|
|
71
|
+
with pytest.raises(duckdb.ConstraintException):
|
|
72
|
+
d_conn.sql(
|
|
73
|
+
"INSERT INTO members VALUES (3, 'other@example.com', 'ada', 'lovelace')"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# An explicit CREATE UNIQUE INDEX, which does carry its SQL.
|
|
77
|
+
with pytest.raises(duckdb.ConstraintException):
|
|
78
|
+
d_conn.sql("INSERT INTO members VALUES (4, 'x@example.com', 'x', 'lovelace')")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_non_unique_rows_still_insert(unique_sqlite, duckdb_path):
|
|
82
|
+
sqlite_to_duckdb(unique_sqlite, duckdb_path)
|
|
83
|
+
|
|
84
|
+
d_conn = duckdb.connect(str(duckdb_path))
|
|
85
|
+
d_conn.sql("INSERT INTO members VALUES (5, 'grace@example.com', 'grace', 'hopper')")
|
|
86
|
+
|
|
87
|
+
assert d_conn.sql("SELECT COUNT(*) FROM members").fetchone() == (2,)
|
|
@@ -200,3 +200,53 @@ def build_bracket_index_sqlite(path):
|
|
|
200
200
|
conn.close()
|
|
201
201
|
|
|
202
202
|
return path
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_views_sqlite(path):
|
|
206
|
+
"""Views, including one chained on another and one duckdb cannot bind."""
|
|
207
|
+
|
|
208
|
+
conn = sqlite3.connect(path)
|
|
209
|
+
conn.executescript(
|
|
210
|
+
"""
|
|
211
|
+
CREATE TABLE sales (id INTEGER, region TEXT, amount INTEGER);
|
|
212
|
+
INSERT INTO sales VALUES (1, 'north', 10), (2, 'north', 5), (3, 'south', 7);
|
|
213
|
+
|
|
214
|
+
CREATE VIEW by_region AS
|
|
215
|
+
SELECT region, SUM(amount) AS total FROM sales GROUP BY region;
|
|
216
|
+
|
|
217
|
+
CREATE VIEW big_regions AS
|
|
218
|
+
SELECT region FROM by_region WHERE total > 8;
|
|
219
|
+
|
|
220
|
+
CREATE VIEW [north sales] AS SELECT * FROM sales WHERE [region] = 'north';
|
|
221
|
+
|
|
222
|
+
-- MATCH is sqlite only, and duckdb's parser rejects it outright.
|
|
223
|
+
CREATE VIEW matched AS SELECT * FROM sales WHERE region MATCH 'north';
|
|
224
|
+
"""
|
|
225
|
+
)
|
|
226
|
+
conn.commit()
|
|
227
|
+
conn.close()
|
|
228
|
+
|
|
229
|
+
return path
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_unique_sqlite(path):
|
|
233
|
+
"""The three ways sqlite records uniqueness."""
|
|
234
|
+
|
|
235
|
+
conn = sqlite3.connect(path)
|
|
236
|
+
conn.executescript(
|
|
237
|
+
"""
|
|
238
|
+
CREATE TABLE members (
|
|
239
|
+
id INTEGER PRIMARY KEY,
|
|
240
|
+
email TEXT UNIQUE,
|
|
241
|
+
first TEXT,
|
|
242
|
+
last TEXT,
|
|
243
|
+
UNIQUE (first, last)
|
|
244
|
+
);
|
|
245
|
+
INSERT INTO members VALUES (1, 'ada@example.com', 'ada', 'lovelace');
|
|
246
|
+
CREATE UNIQUE INDEX idx_members_last ON members (last);
|
|
247
|
+
"""
|
|
248
|
+
)
|
|
249
|
+
conn.commit()
|
|
250
|
+
conn.close()
|
|
251
|
+
|
|
252
|
+
return path
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|