sqlite-utils 3.37__tar.gz → 3.38__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.
- {sqlite_utils-3.37/sqlite_utils.egg-info → sqlite_utils-3.38}/PKG-INFO +1 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/changelog.rst +10 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/cli.rst +2 -2
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/plugins.rst +20 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/python-api.rst +8 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/setup.py +1 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/cli.py +7 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/db.py +42 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38/sqlite_utils.egg-info}/PKG-INFO +1 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_cli.py +3 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_cli_memory.py +14 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_create.py +7 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_fts.py +27 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_transform.py +123 -1
- {sqlite_utils-3.37 → sqlite_utils-3.38}/LICENSE +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/MANIFEST.in +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/README.md +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/cli-reference.rst +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/contributing.rst +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/index.rst +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/installation.rst +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/docs/reference.rst +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/setup.cfg +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/__init__.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/__main__.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/hookspecs.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/plugins.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/py.typed +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/recipes.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils/utils.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/SOURCES.txt +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/dependency_links.txt +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/entry_points.txt +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/not-zip-safe +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/requires.txt +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/sqlite_utils.egg-info/top_level.txt +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/__init__.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/conftest.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_analyze.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_analyze_tables.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_attach.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_cli_bulk.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_cli_convert.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_cli_insert.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_column_affinity.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_constructor.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_conversions.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_convert.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_create_view.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_default_value.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_delete.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_docs.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_duplicate.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_enable_counts.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_extract.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_extracts.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_get.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_gis.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_hypothesis.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_insert_files.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_introspect.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_lookup.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_m2m.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_plugins.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_query.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_recipes.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_recreate.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_register_function.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_rows.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_rows_from_file.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_sniff.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_suggest_column_types.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_tracer.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_update.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_upsert.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_utils.py +0 -0
- {sqlite_utils-3.37 → sqlite_utils-3.38}/tests/test_wal.py +0 -0
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
Changelog
|
|
5
5
|
===========
|
|
6
6
|
|
|
7
|
+
.. _v3_38:
|
|
8
|
+
|
|
9
|
+
3.38 (2024-11-23)
|
|
10
|
+
-----------------
|
|
11
|
+
|
|
12
|
+
- Plugins can now reuse the implementation of the ``sqlite-utils memory`` CLI command with the new ``return_db=True`` parameter. (:issue:`643`)
|
|
13
|
+
- ``table.transform()`` now recreates indexes after transforming a table. A new ``sqlite_utils.db.TransformError`` exception is raised if these indexes cannot be recreated due to conflicting changes to the table such as a column rename. Thanks, `Mat Miller <https://github.com/matdmiller>`__. (:issue:`633`)
|
|
14
|
+
- ``table.search()`` now accepts a ``include_rank=True`` parameter, causing the resulting rows to have a ``rank`` column showing the calculated relevance score. Thanks, `liunux4odoo <https://github.com/liunux4odoo>`__. (`#628 <https://github.com/simonw/sqlite-utils/pull/628>`__)
|
|
15
|
+
- Fixed an error that occurred when creating a strict table with at least one floating point column. These ``FLOAT`` columns are now correctly created as ``REAL`` as well, but only for strict tables. (:issue:`644`)
|
|
16
|
+
|
|
7
17
|
.. _v3_37:
|
|
8
18
|
|
|
9
19
|
3.37 (2024-07-18)
|
|
@@ -2084,7 +2084,7 @@ Every option for this table (with the exception of ``--pk-none``) can be specifi
|
|
|
2084
2084
|
``--drop-foreign-key column``
|
|
2085
2085
|
Drop the specified foreign key.
|
|
2086
2086
|
|
|
2087
|
-
``--add-
|
|
2087
|
+
``--add-foreign-key column other_table other_column``
|
|
2088
2088
|
Add a foreign key constraint to ``column`` pointing to ``other_table.other_column``.
|
|
2089
2089
|
|
|
2090
2090
|
If you want to see the SQL that will be executed to make the change without actually executing it, add the ``--sql`` flag. For example:
|
|
@@ -2796,7 +2796,7 @@ To enable this feature you will need to install the ``trogon`` dependency. You c
|
|
|
2796
2796
|
|
|
2797
2797
|
.. code-block:: bash
|
|
2798
2798
|
|
|
2799
|
-
|
|
2799
|
+
sqlite-utils install trogon
|
|
2800
2800
|
|
|
2801
2801
|
Once installed, running the ``sqlite-utils tui`` command will launch the TUI interface:
|
|
2802
2802
|
|
|
@@ -115,6 +115,26 @@ Example implementation:
|
|
|
115
115
|
"Say hello world"
|
|
116
116
|
click.echo("Hello world!")
|
|
117
117
|
|
|
118
|
+
New commands implemented by plugins can invoke existing commands using the `context.invoke <https://click.palletsprojects.com/en/stable/api/#click.Context.invoke>`__ mechanism.
|
|
119
|
+
|
|
120
|
+
As a special niche feature, if your plugin needs to import some files and then act against an in-memory database containing those files you can forward to the :ref:`sqlite-utils memory command <cli_memory>` and pass it ``return_db=True``:
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
@cli.command()
|
|
125
|
+
@click.pass_context
|
|
126
|
+
@click.argument(
|
|
127
|
+
"paths",
|
|
128
|
+
type=click.Path(file_okay=True, dir_okay=False, allow_dash=True),
|
|
129
|
+
required=False,
|
|
130
|
+
nargs=-1,
|
|
131
|
+
)
|
|
132
|
+
def show_schema_for_files(ctx, paths):
|
|
133
|
+
from sqlite_utils.cli import memory
|
|
134
|
+
db = ctx.invoke(memory, paths=paths, return_db=True)
|
|
135
|
+
# Now do something with that database
|
|
136
|
+
click.echo(db.schema)
|
|
137
|
+
|
|
118
138
|
.. _plugins_hooks_prepare_connection:
|
|
119
139
|
|
|
120
140
|
prepare_connection(conn)
|
|
@@ -1249,6 +1249,9 @@ If you pass a Python type, it will be mapped to SQLite types as shown here::
|
|
|
1249
1249
|
np.float32: "FLOAT"
|
|
1250
1250
|
np.float64: "FLOAT"
|
|
1251
1251
|
|
|
1252
|
+
.. note::
|
|
1253
|
+
In sqlite-utils 3.x ``FLOAT`` is used for floating point columns when the correct column type is actually ``REAL``. If you specify ``strict=True`` tables created in strict mode will use the correct column type of ``REAL`` instead. We plan to change this behavior in ``sqlite-utils`` 4.x to always use ``REAL``, but this will represent a minor breaking change and so is being held for the next major release, see issue :issue:`645`.
|
|
1254
|
+
|
|
1252
1255
|
You can also add a column that is a foreign key reference to another table using the ``fk`` parameter:
|
|
1253
1256
|
|
|
1254
1257
|
.. code-block:: python
|
|
@@ -1402,6 +1405,8 @@ To keep the original table around instead of dropping it, pass the ``keep_table=
|
|
|
1402
1405
|
|
|
1403
1406
|
table.transform(types={"age": int}, keep_table="original_table")
|
|
1404
1407
|
|
|
1408
|
+
This method raises a ``sqlite_utils.db.TransformError`` exception if the table cannot be transformed, usually because there are existing constraints or indexes that are incompatible with modifications to the columns.
|
|
1409
|
+
|
|
1405
1410
|
.. _python_api_transform_alter_column_types:
|
|
1406
1411
|
|
|
1407
1412
|
Altering column types
|
|
@@ -2309,6 +2314,9 @@ The ``.search()`` method also accepts the following optional parameters:
|
|
|
2309
2314
|
``where_args`` dictionary
|
|
2310
2315
|
Arguments to use for ``:param`` placeholders in the extra WHERE clause
|
|
2311
2316
|
|
|
2317
|
+
``include_rank`` bool
|
|
2318
|
+
If set a ``rank`` column will be included with the BM25 ranking score - for FTS5 tables only.
|
|
2319
|
+
|
|
2312
2320
|
``quote`` bool
|
|
2313
2321
|
Apply :ref:`FTS quoting rules <python_api_quote_fts>` to the search query, disabling advanced query syntax in a way that avoids surprising errors.
|
|
2314
2322
|
|
|
@@ -1894,6 +1894,7 @@ def memory(
|
|
|
1894
1894
|
save,
|
|
1895
1895
|
analyze,
|
|
1896
1896
|
load_extension,
|
|
1897
|
+
return_db=False,
|
|
1897
1898
|
):
|
|
1898
1899
|
"""Execute SQL query against an in-memory database, optionally populated by imported data
|
|
1899
1900
|
|
|
@@ -1922,6 +1923,7 @@ def memory(
|
|
|
1922
1923
|
sqlite-utils memory animals.csv --schema
|
|
1923
1924
|
"""
|
|
1924
1925
|
db = sqlite_utils.Database(memory=True)
|
|
1926
|
+
|
|
1925
1927
|
# If --dump or --save or --analyze used but no paths detected, assume SQL query is a path:
|
|
1926
1928
|
if (dump or save or schema or analyze) and not paths:
|
|
1927
1929
|
paths = [sql]
|
|
@@ -1954,6 +1956,7 @@ def memory(
|
|
|
1954
1956
|
rows = tracker.wrap(rows)
|
|
1955
1957
|
if flatten:
|
|
1956
1958
|
rows = (_flatten(row) for row in rows)
|
|
1959
|
+
|
|
1957
1960
|
db[file_table].insert_all(rows, alter=True)
|
|
1958
1961
|
if tracker is not None:
|
|
1959
1962
|
db[file_table].transform(types=tracker.types)
|
|
@@ -1964,6 +1967,7 @@ def memory(
|
|
|
1964
1967
|
for view_name in view_names:
|
|
1965
1968
|
if not db[view_name].exists():
|
|
1966
1969
|
db.create_view(view_name, "select * from [{}]".format(file_table))
|
|
1970
|
+
|
|
1967
1971
|
if fp:
|
|
1968
1972
|
fp.close()
|
|
1969
1973
|
|
|
@@ -1994,6 +1998,9 @@ def memory(
|
|
|
1994
1998
|
if functions:
|
|
1995
1999
|
_register_functions(db, functions)
|
|
1996
2000
|
|
|
2001
|
+
if return_db:
|
|
2002
|
+
return db
|
|
2003
|
+
|
|
1997
2004
|
_execute_query(
|
|
1998
2005
|
db,
|
|
1999
2006
|
sql,
|
|
@@ -156,6 +156,10 @@ XIndexColumn = namedtuple(
|
|
|
156
156
|
Trigger = namedtuple("Trigger", ("name", "table", "sql"))
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
class TransformError(Exception):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
159
163
|
ForeignKeyIndicator = Union[
|
|
160
164
|
str,
|
|
161
165
|
ForeignKey,
|
|
@@ -325,6 +329,8 @@ class Database:
|
|
|
325
329
|
execute_plugins: bool = True,
|
|
326
330
|
strict: bool = False,
|
|
327
331
|
):
|
|
332
|
+
self.memory_name = None
|
|
333
|
+
self.memory = False
|
|
328
334
|
assert (filename_or_conn is not None and (not memory and not memory_name)) or (
|
|
329
335
|
filename_or_conn is None and (memory or memory_name)
|
|
330
336
|
), "Either specify a filename_or_conn or pass memory=True"
|
|
@@ -335,8 +341,11 @@ class Database:
|
|
|
335
341
|
uri=True,
|
|
336
342
|
check_same_thread=False,
|
|
337
343
|
)
|
|
344
|
+
self.memory = True
|
|
345
|
+
self.memory_name = memory_name
|
|
338
346
|
elif memory or filename_or_conn == ":memory:":
|
|
339
347
|
self.conn = sqlite3.connect(":memory:")
|
|
348
|
+
self.memory = True
|
|
340
349
|
elif isinstance(filename_or_conn, (str, pathlib.Path)):
|
|
341
350
|
if recreate and os.path.exists(filename_or_conn):
|
|
342
351
|
try:
|
|
@@ -929,10 +938,15 @@ class Database:
|
|
|
929
938
|
other_column=foreign_keys_by_column[column_name].other_column,
|
|
930
939
|
)
|
|
931
940
|
)
|
|
941
|
+
column_type_str = COLUMN_TYPE_MAPPING[column_type]
|
|
942
|
+
# Special case for strict tables to map FLOAT to REAL
|
|
943
|
+
# Refs https://github.com/simonw/sqlite-utils/issues/644
|
|
944
|
+
if strict and column_type_str == "FLOAT":
|
|
945
|
+
column_type_str = "REAL"
|
|
932
946
|
column_defs.append(
|
|
933
947
|
" [{column_name}] {column_type}{column_extras}".format(
|
|
934
948
|
column_name=column_name,
|
|
935
|
-
column_type=
|
|
949
|
+
column_type=column_type_str,
|
|
936
950
|
column_extras=(
|
|
937
951
|
(" " + " ".join(column_extras)) if column_extras else ""
|
|
938
952
|
),
|
|
@@ -1967,6 +1981,30 @@ class Table(Queryable):
|
|
|
1967
1981
|
sqls.append(
|
|
1968
1982
|
"ALTER TABLE [{}] RENAME TO [{}];".format(new_table_name, self.name)
|
|
1969
1983
|
)
|
|
1984
|
+
# Re-add existing indexes
|
|
1985
|
+
for index in self.indexes:
|
|
1986
|
+
if index.origin != "pk":
|
|
1987
|
+
index_sql = self.db.execute(
|
|
1988
|
+
"""SELECT sql FROM sqlite_master WHERE type = 'index' AND name = :index_name;""",
|
|
1989
|
+
{"index_name": index.name},
|
|
1990
|
+
).fetchall()[0][0]
|
|
1991
|
+
if index_sql is None:
|
|
1992
|
+
raise TransformError(
|
|
1993
|
+
f"Index '{index.name}' on table '{self.name}' does not have a "
|
|
1994
|
+
"CREATE INDEX statement. You must manually drop this index prior to running this "
|
|
1995
|
+
"transformation and manually recreate the new index after running this transformation."
|
|
1996
|
+
)
|
|
1997
|
+
if keep_table:
|
|
1998
|
+
sqls.append(f"DROP INDEX IF EXISTS [{index.name}];")
|
|
1999
|
+
for col in index.columns:
|
|
2000
|
+
if col in rename.keys() or col in drop:
|
|
2001
|
+
raise TransformError(
|
|
2002
|
+
f"Index '{index.name}' column '{col}' is not in updated table '{self.name}'. "
|
|
2003
|
+
f"You must manually drop this index prior to running this transformation "
|
|
2004
|
+
f"and manually recreate the new index after running this transformation. "
|
|
2005
|
+
f"The original index sql statement is: `{index_sql}`. No changes have been applied to this table."
|
|
2006
|
+
)
|
|
2007
|
+
sqls.append(index_sql)
|
|
1970
2008
|
return sqls
|
|
1971
2009
|
|
|
1972
2010
|
def extract(
|
|
@@ -2645,6 +2683,7 @@ class Table(Queryable):
|
|
|
2645
2683
|
offset: Optional[int] = None,
|
|
2646
2684
|
where: Optional[str] = None,
|
|
2647
2685
|
where_args: Optional[Union[Iterable, dict]] = None,
|
|
2686
|
+
include_rank: bool = False,
|
|
2648
2687
|
quote: bool = False,
|
|
2649
2688
|
) -> Generator[dict, None, None]:
|
|
2650
2689
|
"""
|
|
@@ -2658,6 +2697,7 @@ class Table(Queryable):
|
|
|
2658
2697
|
:param offset: Optional integer SQL offset.
|
|
2659
2698
|
:param where: Extra SQL fragment for the WHERE clause
|
|
2660
2699
|
:param where_args: Arguments to use for :param placeholders in the extra WHERE clause
|
|
2700
|
+
:param include_rank: Select the search rank column in the final query
|
|
2661
2701
|
:param quote: Apply quoting to disable any special characters in the search query
|
|
2662
2702
|
|
|
2663
2703
|
See :ref:`python_api_fts_search`.
|
|
@@ -2677,6 +2717,7 @@ class Table(Queryable):
|
|
|
2677
2717
|
limit=limit,
|
|
2678
2718
|
offset=offset,
|
|
2679
2719
|
where=where,
|
|
2720
|
+
include_rank=include_rank,
|
|
2680
2721
|
),
|
|
2681
2722
|
args,
|
|
2682
2723
|
)
|
|
@@ -2416,11 +2416,13 @@ def test_create_table_strict(strict):
|
|
|
2416
2416
|
db = Database("test.db")
|
|
2417
2417
|
result = runner.invoke(
|
|
2418
2418
|
cli.cli,
|
|
2419
|
-
["create-table", "test.db", "items", "id", "integer"]
|
|
2419
|
+
["create-table", "test.db", "items", "id", "integer", "w", "float"]
|
|
2420
2420
|
+ (["--strict"] if strict else []),
|
|
2421
2421
|
)
|
|
2422
2422
|
assert result.exit_code == 0
|
|
2423
2423
|
assert db["items"].strict == strict or not db.supports_strict
|
|
2424
|
+
# Should have a floating point column
|
|
2425
|
+
assert db["items"].columns_dict == {"id": int, "w": float}
|
|
2424
2426
|
|
|
2425
2427
|
|
|
2426
2428
|
@pytest.mark.parametrize("method", ("insert", "upsert"))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import click
|
|
1
2
|
import json
|
|
2
|
-
|
|
3
3
|
import pytest
|
|
4
4
|
from click.testing import CliRunner
|
|
5
5
|
|
|
@@ -305,3 +305,16 @@ def test_memory_functions():
|
|
|
305
305
|
)
|
|
306
306
|
assert result.exit_code == 0
|
|
307
307
|
assert result.output.strip() == '[{"hello()": "Hello"}]'
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_memory_return_db(tmpdir):
|
|
311
|
+
# https://github.com/simonw/sqlite-utils/issues/643
|
|
312
|
+
from sqlite_utils.cli import cli
|
|
313
|
+
|
|
314
|
+
path = str(tmpdir / "dogs.csv")
|
|
315
|
+
open(path, "w").write("id,name\n1,Cleo")
|
|
316
|
+
|
|
317
|
+
with click.Context(cli) as ctx:
|
|
318
|
+
db = ctx.invoke(cli.commands["memory"], paths=(path,), return_db=True)
|
|
319
|
+
|
|
320
|
+
assert db.table_names() == ["dogs"]
|
|
@@ -1350,8 +1350,14 @@ def test_insert_upsert_strict(fresh_db, method_name, strict):
|
|
|
1350
1350
|
|
|
1351
1351
|
@pytest.mark.parametrize("strict", (False, True))
|
|
1352
1352
|
def test_create_table_strict(fresh_db, strict):
|
|
1353
|
-
table = fresh_db.create_table("t", {"id": int}, strict=strict)
|
|
1353
|
+
table = fresh_db.create_table("t", {"id": int, "f": float}, strict=strict)
|
|
1354
1354
|
assert table.strict == strict or not fresh_db.supports_strict
|
|
1355
|
+
expected_schema = "CREATE TABLE [t] (\n" " [id] INTEGER,\n" " [f] FLOAT\n" ")"
|
|
1356
|
+
if strict and not fresh_db.supports_strict:
|
|
1357
|
+
return
|
|
1358
|
+
if strict:
|
|
1359
|
+
expected_schema = "CREATE TABLE [t] (\n [id] INTEGER,\n [f] REAL\n) STRICT"
|
|
1360
|
+
assert table.schema == expected_schema
|
|
1355
1361
|
|
|
1356
1362
|
|
|
1357
1363
|
@pytest.mark.parametrize("strict", (False, True))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
from sqlite_utils import Database
|
|
3
3
|
from sqlite_utils.utils import sqlite3
|
|
4
|
+
from unittest.mock import ANY
|
|
4
5
|
|
|
5
6
|
search_records = [
|
|
6
7
|
{
|
|
@@ -126,6 +127,32 @@ def test_search_where_args_disallows_query(fresh_db):
|
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
def test_search_include_rank(fresh_db):
|
|
131
|
+
table = fresh_db["t"]
|
|
132
|
+
table.insert_all(search_records)
|
|
133
|
+
table.enable_fts(["text", "country"], fts_version="FTS5")
|
|
134
|
+
results = list(table.search("are", include_rank=True))
|
|
135
|
+
assert results == [
|
|
136
|
+
{
|
|
137
|
+
"rowid": 1,
|
|
138
|
+
"text": "tanuki are running tricksters",
|
|
139
|
+
"country": "Japan",
|
|
140
|
+
"not_searchable": "foo",
|
|
141
|
+
"rank": ANY,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"rowid": 2,
|
|
145
|
+
"text": "racoons are biting trash pandas",
|
|
146
|
+
"country": "USA",
|
|
147
|
+
"not_searchable": "bar",
|
|
148
|
+
"rank": ANY,
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
assert isinstance(results[0]["rank"], float)
|
|
152
|
+
assert isinstance(results[1]["rank"], float)
|
|
153
|
+
assert results[0]["rank"] < results[1]["rank"]
|
|
154
|
+
|
|
155
|
+
|
|
129
156
|
def test_enable_fts_table_names_containing_spaces(fresh_db):
|
|
130
157
|
table = fresh_db["test"]
|
|
131
158
|
table.insert({"column with spaces": "in its name"})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from sqlite_utils.db import ForeignKey
|
|
1
|
+
from sqlite_utils.db import ForeignKey, TransformError
|
|
2
2
|
from sqlite_utils.utils import OperationalError
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
@@ -539,3 +539,125 @@ def test_transform_strict(fresh_db, strict):
|
|
|
539
539
|
assert dogs.strict == strict or not fresh_db.supports_strict
|
|
540
540
|
dogs.transform(not_null={"name"})
|
|
541
541
|
assert dogs.strict == strict or not fresh_db.supports_strict
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@pytest.mark.parametrize(
|
|
545
|
+
"indexes, transform_params",
|
|
546
|
+
[
|
|
547
|
+
([["name"]], {"types": {"age": str}}),
|
|
548
|
+
([["name"], ["age", "breed"]], {"types": {"age": str}}),
|
|
549
|
+
([], {"types": {"age": str}}),
|
|
550
|
+
([["name"]], {"types": {"age": str}, "keep_table": "old_dogs"}),
|
|
551
|
+
],
|
|
552
|
+
)
|
|
553
|
+
def test_transform_indexes(fresh_db, indexes, transform_params):
|
|
554
|
+
# https://github.com/simonw/sqlite-utils/issues/633
|
|
555
|
+
# New table should have same indexes as old table after transformation
|
|
556
|
+
dogs = fresh_db["dogs"]
|
|
557
|
+
dogs.insert({"id": 1, "name": "Cleo", "age": 5, "breed": "Labrador"}, pk="id")
|
|
558
|
+
|
|
559
|
+
for index in indexes:
|
|
560
|
+
dogs.create_index(index)
|
|
561
|
+
|
|
562
|
+
indexes_before_transform = dogs.indexes
|
|
563
|
+
|
|
564
|
+
dogs.transform(**transform_params)
|
|
565
|
+
|
|
566
|
+
assert sorted(
|
|
567
|
+
[
|
|
568
|
+
{k: v for k, v in idx._asdict().items() if k != "seq"}
|
|
569
|
+
for idx in dogs.indexes
|
|
570
|
+
],
|
|
571
|
+
key=lambda x: x["name"],
|
|
572
|
+
) == sorted(
|
|
573
|
+
[
|
|
574
|
+
{k: v for k, v in idx._asdict().items() if k != "seq"}
|
|
575
|
+
for idx in indexes_before_transform
|
|
576
|
+
],
|
|
577
|
+
key=lambda x: x["name"],
|
|
578
|
+
), f"Indexes before transform: {indexes_before_transform}\nIndexes after transform: {dogs.indexes}"
|
|
579
|
+
if "keep_table" in transform_params:
|
|
580
|
+
assert all(
|
|
581
|
+
index.origin == "pk"
|
|
582
|
+
for index in fresh_db[transform_params["keep_table"]].indexes
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_transform_retains_indexes_with_foreign_keys(fresh_db):
|
|
587
|
+
dogs = fresh_db["dogs"]
|
|
588
|
+
owners = fresh_db["owners"]
|
|
589
|
+
|
|
590
|
+
dogs.insert({"id": 1, "name": "Cleo", "owner_id": 1}, pk="id")
|
|
591
|
+
owners.insert({"id": 1, "name": "Alice"}, pk="id")
|
|
592
|
+
|
|
593
|
+
dogs.create_index(["name"])
|
|
594
|
+
|
|
595
|
+
indexes_before_transform = dogs.indexes
|
|
596
|
+
|
|
597
|
+
fresh_db.add_foreign_keys([("dogs", "owner_id", "owners", "id")]) # calls transform
|
|
598
|
+
|
|
599
|
+
assert sorted(
|
|
600
|
+
[
|
|
601
|
+
{k: v for k, v in idx._asdict().items() if k != "seq"}
|
|
602
|
+
for idx in dogs.indexes
|
|
603
|
+
],
|
|
604
|
+
key=lambda x: x["name"],
|
|
605
|
+
) == sorted(
|
|
606
|
+
[
|
|
607
|
+
{k: v for k, v in idx._asdict().items() if k != "seq"}
|
|
608
|
+
for idx in indexes_before_transform
|
|
609
|
+
],
|
|
610
|
+
key=lambda x: x["name"],
|
|
611
|
+
), f"Indexes before transform: {indexes_before_transform}\nIndexes after transform: {dogs.indexes}"
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@pytest.mark.parametrize(
|
|
615
|
+
"transform_params",
|
|
616
|
+
[
|
|
617
|
+
{"rename": {"age": "dog_age"}},
|
|
618
|
+
{"drop": ["age"]},
|
|
619
|
+
],
|
|
620
|
+
)
|
|
621
|
+
def test_transform_with_indexes_errors(fresh_db, transform_params):
|
|
622
|
+
# Should error with a compound (name, age) index if age is renamed or dropped
|
|
623
|
+
dogs = fresh_db["dogs"]
|
|
624
|
+
dogs.insert({"id": 1, "name": "Cleo", "age": 5}, pk="id")
|
|
625
|
+
|
|
626
|
+
dogs.create_index(["name", "age"])
|
|
627
|
+
|
|
628
|
+
with pytest.raises(TransformError) as excinfo:
|
|
629
|
+
dogs.transform(**transform_params)
|
|
630
|
+
|
|
631
|
+
assert (
|
|
632
|
+
"Index 'idx_dogs_name_age' column 'age' is not in updated table 'dogs'. "
|
|
633
|
+
"You must manually drop this index prior to running this transformation"
|
|
634
|
+
in str(excinfo.value)
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_transform_with_unique_constraint_implicit_index(fresh_db):
|
|
639
|
+
dogs = fresh_db["dogs"]
|
|
640
|
+
# Create a table with a UNIQUE constraint on 'name', which creates an implicit index
|
|
641
|
+
fresh_db.execute(
|
|
642
|
+
"""
|
|
643
|
+
CREATE TABLE dogs (
|
|
644
|
+
id INTEGER PRIMARY KEY,
|
|
645
|
+
name TEXT UNIQUE,
|
|
646
|
+
age INTEGER
|
|
647
|
+
);
|
|
648
|
+
"""
|
|
649
|
+
)
|
|
650
|
+
dogs.insert({"id": 1, "name": "Cleo", "age": 5})
|
|
651
|
+
|
|
652
|
+
# Attempt to transform the table without modifying 'name'
|
|
653
|
+
with pytest.raises(TransformError) as excinfo:
|
|
654
|
+
dogs.transform(types={"age": str})
|
|
655
|
+
|
|
656
|
+
assert (
|
|
657
|
+
"Index 'sqlite_autoindex_dogs_1' on table 'dogs' does not have a CREATE INDEX statement."
|
|
658
|
+
in str(excinfo.value)
|
|
659
|
+
)
|
|
660
|
+
assert (
|
|
661
|
+
"You must manually drop this index prior to running this transformation and manually recreate the new index after running this transformation."
|
|
662
|
+
in str(excinfo.value)
|
|
663
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|