sqlite-utils 3.36__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.36/sqlite_utils.egg-info → sqlite_utils-3.38}/PKG-INFO +4 -4
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/changelog.rst +19 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/cli-reference.rst +19 -18
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/cli.rst +7 -3
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/plugins.rst +20 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/python-api.rst +11 -2
- {sqlite-utils-3.36 → sqlite_utils-3.38}/setup.py +4 -4
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/cli.py +25 -14
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/db.py +75 -27
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/utils.py +1 -4
- {sqlite-utils-3.36 → sqlite_utils-3.38/sqlite_utils.egg-info}/PKG-INFO +4 -4
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/requires.txt +1 -1
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_cli.py +19 -11
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_cli_insert.py +10 -5
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_cli_memory.py +14 -1
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_create.py +7 -1
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_fts.py +27 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_insert_files.py +10 -2
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_transform.py +123 -1
- {sqlite-utils-3.36 → sqlite_utils-3.38}/LICENSE +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/MANIFEST.in +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/README.md +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/contributing.rst +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/index.rst +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/installation.rst +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/docs/reference.rst +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/setup.cfg +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/__init__.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/__main__.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/hookspecs.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/plugins.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/py.typed +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils/recipes.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/SOURCES.txt +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/dependency_links.txt +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/entry_points.txt +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/not-zip-safe +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/sqlite_utils.egg-info/top_level.txt +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/__init__.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/conftest.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_analyze.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_analyze_tables.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_attach.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_cli_bulk.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_cli_convert.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_column_affinity.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_constructor.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_conversions.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_convert.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_create_view.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_default_value.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_delete.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_docs.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_duplicate.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_enable_counts.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_extract.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_extracts.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_get.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_gis.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_hypothesis.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_introspect.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_lookup.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_m2m.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_plugins.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_query.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_recipes.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_recreate.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_register_function.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_rows.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_rows_from_file.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_sniff.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_suggest_column_types.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_tracer.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_update.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_upsert.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_utils.py +0 -0
- {sqlite-utils-3.36 → sqlite_utils-3.38}/tests/test_wal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.38
|
|
4
4
|
Summary: CLI tool and Python library for manipulating SQLite databases
|
|
5
5
|
Home-page: https://github.com/simonw/sqlite-utils
|
|
6
6
|
Author: Simon Willison
|
|
@@ -16,13 +16,13 @@ Classifier: Intended Audience :: Science/Research
|
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
18
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
-
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: sqlite-fts4
|
|
@@ -33,7 +33,7 @@ Requires-Dist: python-dateutil
|
|
|
33
33
|
Requires-Dist: pluggy
|
|
34
34
|
Provides-Extra: test
|
|
35
35
|
Requires-Dist: pytest; extra == "test"
|
|
36
|
-
Requires-Dist: black; extra == "test"
|
|
36
|
+
Requires-Dist: black>=24.1.1; extra == "test"
|
|
37
37
|
Requires-Dist: hypothesis; extra == "test"
|
|
38
38
|
Requires-Dist: cogapp; extra == "test"
|
|
39
39
|
Provides-Extra: docs
|
|
@@ -4,6 +4,25 @@
|
|
|
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
|
+
|
|
17
|
+
.. _v3_37:
|
|
18
|
+
|
|
19
|
+
3.37 (2024-07-18)
|
|
20
|
+
-----------------
|
|
21
|
+
|
|
22
|
+
- The ``create-table`` and ``insert-files`` commands all now accept multiple ``--pk`` options for compound primary keys. (:issue:`620`)
|
|
23
|
+
- Now tested against Python 3.13 pre-release. (`#619 <https://github.com/simonw/sqlite-utils/pull/619>`__)
|
|
24
|
+
- Fixed a crash that can occur in environments with a broken ``numpy`` installation, producing a ``module 'numpy' has no attribute 'int8'``. (:issue:`632`)
|
|
25
|
+
|
|
7
26
|
.. _v3_36:
|
|
8
27
|
|
|
9
28
|
3.36 (2023-12-07)
|
|
@@ -85,6 +85,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.
|
|
|
85
85
|
cog.out("::\n\n")
|
|
86
86
|
result = CliRunner().invoke(cli.cli, [command, "--help"])
|
|
87
87
|
output = result.output.replace("Usage: cli ", "Usage: sqlite-utils ")
|
|
88
|
+
output = output.replace('\b', '')
|
|
88
89
|
cog.out(textwrap.indent(output, ' '))
|
|
89
90
|
cog.out("\n\n")
|
|
90
91
|
.. ]]]
|
|
@@ -603,9 +604,9 @@ See :ref:`cli_convert`.
|
|
|
603
604
|
|
|
604
605
|
Convert columns using Python code you supply. For example:
|
|
605
606
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
607
|
+
sqlite-utils convert my.db mytable mycolumn \
|
|
608
|
+
'"\n".join(textwrap.wrap(value, 10))' \
|
|
609
|
+
--import=textwrap
|
|
609
610
|
|
|
610
611
|
"value" is a variable with the column value to be converted.
|
|
611
612
|
|
|
@@ -615,30 +616,30 @@ See :ref:`cli_convert`.
|
|
|
615
616
|
|
|
616
617
|
r.jsonsplit(value, delimiter=',', type=<class 'str'>)
|
|
617
618
|
|
|
618
|
-
|
|
619
|
+
Convert a string like a,b,c into a JSON array ["a", "b", "c"]
|
|
619
620
|
|
|
620
621
|
r.parsedate(value, dayfirst=False, yearfirst=False, errors=None)
|
|
621
622
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
623
|
+
Parse a date and convert it to ISO date format: yyyy-mm-dd
|
|
624
|
+
|
|
625
|
+
- dayfirst=True: treat xx as the day in xx/yy/zz
|
|
626
|
+
- yearfirst=True: treat xx as the year in xx/yy/zz
|
|
627
|
+
- errors=r.IGNORE to ignore values that cannot be parsed
|
|
628
|
+
- errors=r.SET_NULL to set values that cannot be parsed to null
|
|
628
629
|
|
|
629
630
|
r.parsedatetime(value, dayfirst=False, yearfirst=False, errors=None)
|
|
630
631
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
632
|
+
Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
|
|
633
|
+
|
|
634
|
+
- dayfirst=True: treat xx as the day in xx/yy/zz
|
|
635
|
+
- yearfirst=True: treat xx as the year in xx/yy/zz
|
|
636
|
+
- errors=r.IGNORE to ignore values that cannot be parsed
|
|
637
|
+
- errors=r.SET_NULL to set values that cannot be parsed to null
|
|
637
638
|
|
|
638
639
|
You can use these recipes like so:
|
|
639
640
|
|
|
640
|
-
|
|
641
|
-
|
|
641
|
+
sqlite-utils convert my.db mytable mycolumn \
|
|
642
|
+
'r.jsonsplit(value, delimiter=":")'
|
|
642
643
|
|
|
643
644
|
Options:
|
|
644
645
|
--import TEXT Python modules to import
|
|
@@ -1088,11 +1088,13 @@ You can import all three records into an automatically created ``dogs`` table an
|
|
|
1088
1088
|
|
|
1089
1089
|
sqlite-utils insert dogs.db dogs dogs.json --pk=id
|
|
1090
1090
|
|
|
1091
|
+
Pass ``--pk`` multiple times to define a compound primary key.
|
|
1092
|
+
|
|
1091
1093
|
You can skip inserting any records that have a primary key that already exists using ``--ignore``:
|
|
1092
1094
|
|
|
1093
1095
|
.. code-block:: bash
|
|
1094
1096
|
|
|
1095
|
-
sqlite-utils insert dogs.db dogs dogs.json --ignore
|
|
1097
|
+
sqlite-utils insert dogs.db dogs dogs.json --pk=id --ignore
|
|
1096
1098
|
|
|
1097
1099
|
You can delete all the existing rows in the table before inserting the new records using ``--truncate``:
|
|
1098
1100
|
|
|
@@ -1909,6 +1911,8 @@ This will create a table called ``mytable`` with two columns - an integer ``id``
|
|
|
1909
1911
|
|
|
1910
1912
|
You can pass as many column-name column-type pairs as you like. Valid types are ``integer``, ``text``, ``float`` and ``blob``.
|
|
1911
1913
|
|
|
1914
|
+
Pass ``--pk`` more than once for a compound primary key that covers multiple columns.
|
|
1915
|
+
|
|
1912
1916
|
You can specify columns that should be NOT NULL using ``--not-null colname``. You can specify default values for columns using ``--default colname defaultvalue``.
|
|
1913
1917
|
|
|
1914
1918
|
.. code-block:: bash
|
|
@@ -2080,7 +2084,7 @@ Every option for this table (with the exception of ``--pk-none``) can be specifi
|
|
|
2080
2084
|
``--drop-foreign-key column``
|
|
2081
2085
|
Drop the specified foreign key.
|
|
2082
2086
|
|
|
2083
|
-
``--add-
|
|
2087
|
+
``--add-foreign-key column other_table other_column``
|
|
2084
2088
|
Add a foreign key constraint to ``column`` pointing to ``other_table.other_column``.
|
|
2085
2089
|
|
|
2086
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:
|
|
@@ -2792,7 +2796,7 @@ To enable this feature you will need to install the ``trogon`` dependency. You c
|
|
|
2792
2796
|
|
|
2793
2797
|
.. code-block:: bash
|
|
2794
2798
|
|
|
2795
|
-
|
|
2799
|
+
sqlite-utils install trogon
|
|
2796
2800
|
|
|
2797
2801
|
Once installed, running the ``sqlite-utils tui`` command will launch the TUI interface:
|
|
2798
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)
|
|
@@ -890,7 +890,8 @@ You can delete all records in a table that match a specific WHERE statement usin
|
|
|
890
890
|
|
|
891
891
|
>>> db = sqlite_utils.Database("dogs.db")
|
|
892
892
|
>>> # Delete every dog with age less than 3
|
|
893
|
-
>>> db
|
|
893
|
+
>>> with db.conn:
|
|
894
|
+
>>> db["dogs"].delete_where("age < ?", [3])
|
|
894
895
|
|
|
895
896
|
Calling ``table.delete_where()`` with no other arguments will delete every row in the table.
|
|
896
897
|
|
|
@@ -1248,6 +1249,9 @@ If you pass a Python type, it will be mapped to SQLite types as shown here::
|
|
|
1248
1249
|
np.float32: "FLOAT"
|
|
1249
1250
|
np.float64: "FLOAT"
|
|
1250
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
|
+
|
|
1251
1255
|
You can also add a column that is a foreign key reference to another table using the ``fk`` parameter:
|
|
1252
1256
|
|
|
1253
1257
|
.. code-block:: python
|
|
@@ -1401,6 +1405,8 @@ To keep the original table around instead of dropping it, pass the ``keep_table=
|
|
|
1401
1405
|
|
|
1402
1406
|
table.transform(types={"age": int}, keep_table="original_table")
|
|
1403
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
|
+
|
|
1404
1410
|
.. _python_api_transform_alter_column_types:
|
|
1405
1411
|
|
|
1406
1412
|
Altering column types
|
|
@@ -2177,7 +2183,7 @@ The ``.has_counts_triggers`` property shows if a table has been configured with
|
|
|
2177
2183
|
>>> db["authors"].has_counts_triggers
|
|
2178
2184
|
True
|
|
2179
2185
|
|
|
2180
|
-
.. _python_api_introspection_supports_strict
|
|
2186
|
+
.. _python_api_introspection_supports_strict:
|
|
2181
2187
|
|
|
2182
2188
|
db.supports_strict
|
|
2183
2189
|
------------------
|
|
@@ -2308,6 +2314,9 @@ The ``.search()`` method also accepts the following optional parameters:
|
|
|
2308
2314
|
``where_args`` dictionary
|
|
2309
2315
|
Arguments to use for ``:param`` placeholders in the extra WHERE clause
|
|
2310
2316
|
|
|
2317
|
+
``include_rank`` bool
|
|
2318
|
+
If set a ``rank`` column will be included with the BM25 ranking score - for FTS5 tables only.
|
|
2319
|
+
|
|
2311
2320
|
``quote`` bool
|
|
2312
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.
|
|
2313
2322
|
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
import io
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
|
-
VERSION = "3.
|
|
5
|
+
VERSION = "3.38"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_long_description():
|
|
@@ -32,7 +32,7 @@ setup(
|
|
|
32
32
|
"pluggy",
|
|
33
33
|
],
|
|
34
34
|
extras_require={
|
|
35
|
-
"test": ["pytest", "black", "hypothesis", "cogapp"],
|
|
35
|
+
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
|
|
36
36
|
"docs": [
|
|
37
37
|
"furo",
|
|
38
38
|
"sphinx-autobuild",
|
|
@@ -64,7 +64,7 @@ setup(
|
|
|
64
64
|
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
|
65
65
|
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
|
66
66
|
},
|
|
67
|
-
python_requires=">=3.
|
|
67
|
+
python_requires=">=3.8",
|
|
68
68
|
classifiers=[
|
|
69
69
|
"Development Status :: 5 - Production/Stable",
|
|
70
70
|
"Intended Audience :: Developers",
|
|
@@ -72,12 +72,12 @@ setup(
|
|
|
72
72
|
"Intended Audience :: End Users/Desktop",
|
|
73
73
|
"Topic :: Database",
|
|
74
74
|
"License :: OSI Approved :: Apache Software License",
|
|
75
|
-
"Programming Language :: Python :: 3.7",
|
|
76
75
|
"Programming Language :: Python :: 3.8",
|
|
77
76
|
"Programming Language :: Python :: 3.9",
|
|
78
77
|
"Programming Language :: Python :: 3.10",
|
|
79
78
|
"Programming Language :: Python :: 3.11",
|
|
80
79
|
"Programming Language :: Python :: 3.12",
|
|
80
|
+
"Programming Language :: Python :: 3.13",
|
|
81
81
|
],
|
|
82
82
|
# Needed to bundle py.typed so mypy can see it:
|
|
83
83
|
zip_safe=False,
|
|
@@ -1489,7 +1489,7 @@ def create_database(path, enable_wal, init_spatialite, load_extension):
|
|
|
1489
1489
|
)
|
|
1490
1490
|
@click.argument("table")
|
|
1491
1491
|
@click.argument("columns", nargs=-1, required=True)
|
|
1492
|
-
@click.option("--pk", help="Column to use as primary key")
|
|
1492
|
+
@click.option("pks", "--pk", help="Column to use as primary key", multiple=True)
|
|
1493
1493
|
@click.option(
|
|
1494
1494
|
"--not-null",
|
|
1495
1495
|
multiple=True,
|
|
@@ -1532,7 +1532,7 @@ def create_table(
|
|
|
1532
1532
|
path,
|
|
1533
1533
|
table,
|
|
1534
1534
|
columns,
|
|
1535
|
-
|
|
1535
|
+
pks,
|
|
1536
1536
|
not_null,
|
|
1537
1537
|
default,
|
|
1538
1538
|
fk,
|
|
@@ -1581,7 +1581,7 @@ def create_table(
|
|
|
1581
1581
|
)
|
|
1582
1582
|
db[table].create(
|
|
1583
1583
|
coltypes,
|
|
1584
|
-
pk=
|
|
1584
|
+
pk=pks[0] if len(pks) == 1 else pks,
|
|
1585
1585
|
not_null=not_null,
|
|
1586
1586
|
defaults=dict(default),
|
|
1587
1587
|
foreign_keys=fk,
|
|
@@ -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,
|
|
@@ -2594,7 +2601,7 @@ def extract(
|
|
|
2594
2601
|
multiple=True,
|
|
2595
2602
|
help="Column definitions for the table",
|
|
2596
2603
|
)
|
|
2597
|
-
@click.option("--pk",
|
|
2604
|
+
@click.option("pks", "--pk", help="Column to use as primary key", multiple=True)
|
|
2598
2605
|
@click.option("--alter", is_flag=True, help="Alter table to add missing columns")
|
|
2599
2606
|
@click.option("--replace", is_flag=True, help="Replace files with matching primary key")
|
|
2600
2607
|
@click.option("--upsert", is_flag=True, help="Upsert files with matching primary key")
|
|
@@ -2611,7 +2618,7 @@ def insert_files(
|
|
|
2611
2618
|
table,
|
|
2612
2619
|
file_or_dir,
|
|
2613
2620
|
column,
|
|
2614
|
-
|
|
2621
|
+
pks,
|
|
2615
2622
|
alter,
|
|
2616
2623
|
replace,
|
|
2617
2624
|
upsert,
|
|
@@ -2641,8 +2648,8 @@ def insert_files(
|
|
|
2641
2648
|
column = ["path:path", "content_text:content_text", "size:size"]
|
|
2642
2649
|
else:
|
|
2643
2650
|
column = ["path:path", "content:content", "size:size"]
|
|
2644
|
-
if not
|
|
2645
|
-
|
|
2651
|
+
if not pks:
|
|
2652
|
+
pks = ["path"]
|
|
2646
2653
|
|
|
2647
2654
|
def yield_paths_and_relative_paths():
|
|
2648
2655
|
for f_or_d in file_or_dir:
|
|
@@ -2712,7 +2719,11 @@ def insert_files(
|
|
|
2712
2719
|
try:
|
|
2713
2720
|
with db.conn:
|
|
2714
2721
|
db[table].insert_all(
|
|
2715
|
-
to_insert(),
|
|
2722
|
+
to_insert(),
|
|
2723
|
+
pk=pks[0] if len(pks) == 1 else pks,
|
|
2724
|
+
alter=alter,
|
|
2725
|
+
replace=replace,
|
|
2726
|
+
upsert=upsert,
|
|
2716
2727
|
)
|
|
2717
2728
|
except UnicodeDecodeErrorForPath as e:
|
|
2718
2729
|
raise click.ClickException(
|
|
@@ -2871,9 +2882,9 @@ def _generate_convert_help():
|
|
|
2871
2882
|
Convert columns using Python code you supply. For example:
|
|
2872
2883
|
|
|
2873
2884
|
\b
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2885
|
+
sqlite-utils convert my.db mytable mycolumn \\
|
|
2886
|
+
'"\\n".join(textwrap.wrap(value, 10))' \\
|
|
2887
|
+
--import=textwrap
|
|
2877
2888
|
|
|
2878
2889
|
"value" is a variable with the column value to be converted.
|
|
2879
2890
|
|
|
@@ -2892,7 +2903,7 @@ def _generate_convert_help():
|
|
|
2892
2903
|
for name in recipe_names:
|
|
2893
2904
|
fn = getattr(recipes, name)
|
|
2894
2905
|
help += "\n\nr.{}{}\n\n\b{}".format(
|
|
2895
|
-
name, str(inspect.signature(fn)), fn.__doc__.rstrip()
|
|
2906
|
+
name, str(inspect.signature(fn)), textwrap.dedent(fn.__doc__.rstrip())
|
|
2896
2907
|
)
|
|
2897
2908
|
help += "\n\n"
|
|
2898
2909
|
help += textwrap.dedent(
|
|
@@ -2900,8 +2911,8 @@ def _generate_convert_help():
|
|
|
2900
2911
|
You can use these recipes like so:
|
|
2901
2912
|
|
|
2902
2913
|
\b
|
|
2903
|
-
|
|
2904
|
-
|
|
2914
|
+
sqlite-utils convert my.db mytable mycolumn \\
|
|
2915
|
+
'r.jsonsplit(value, delimiter=":")'
|
|
2905
2916
|
"""
|
|
2906
2917
|
).strip()
|
|
2907
2918
|
return help
|
|
@@ -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,
|
|
@@ -206,21 +210,25 @@ COLUMN_TYPE_MAPPING = {
|
|
|
206
210
|
}
|
|
207
211
|
# If numpy is available, add more types
|
|
208
212
|
if np:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
213
|
+
try:
|
|
214
|
+
COLUMN_TYPE_MAPPING.update(
|
|
215
|
+
{
|
|
216
|
+
np.int8: "INTEGER",
|
|
217
|
+
np.int16: "INTEGER",
|
|
218
|
+
np.int32: "INTEGER",
|
|
219
|
+
np.int64: "INTEGER",
|
|
220
|
+
np.uint8: "INTEGER",
|
|
221
|
+
np.uint16: "INTEGER",
|
|
222
|
+
np.uint32: "INTEGER",
|
|
223
|
+
np.uint64: "INTEGER",
|
|
224
|
+
np.float16: "FLOAT",
|
|
225
|
+
np.float32: "FLOAT",
|
|
226
|
+
np.float64: "FLOAT",
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
except AttributeError:
|
|
230
|
+
# https://github.com/simonw/sqlite-utils/issues/632
|
|
231
|
+
pass
|
|
224
232
|
|
|
225
233
|
# If pandas is available, add more types
|
|
226
234
|
if pd:
|
|
@@ -321,6 +329,8 @@ class Database:
|
|
|
321
329
|
execute_plugins: bool = True,
|
|
322
330
|
strict: bool = False,
|
|
323
331
|
):
|
|
332
|
+
self.memory_name = None
|
|
333
|
+
self.memory = False
|
|
324
334
|
assert (filename_or_conn is not None and (not memory and not memory_name)) or (
|
|
325
335
|
filename_or_conn is None and (memory or memory_name)
|
|
326
336
|
), "Either specify a filename_or_conn or pass memory=True"
|
|
@@ -331,8 +341,11 @@ class Database:
|
|
|
331
341
|
uri=True,
|
|
332
342
|
check_same_thread=False,
|
|
333
343
|
)
|
|
344
|
+
self.memory = True
|
|
345
|
+
self.memory_name = memory_name
|
|
334
346
|
elif memory or filename_or_conn == ":memory:":
|
|
335
347
|
self.conn = sqlite3.connect(":memory:")
|
|
348
|
+
self.memory = True
|
|
336
349
|
elif isinstance(filename_or_conn, (str, pathlib.Path)):
|
|
337
350
|
if recreate and os.path.exists(filename_or_conn):
|
|
338
351
|
try:
|
|
@@ -457,8 +470,7 @@ class Database:
|
|
|
457
470
|
fn_name, arity, fn, **dict(kwargs, deterministic=True)
|
|
458
471
|
)
|
|
459
472
|
registered = True
|
|
460
|
-
except
|
|
461
|
-
# TypeError is Python 3.7 "function takes at most 3 arguments"
|
|
473
|
+
except sqlite3.NotSupportedError:
|
|
462
474
|
pass
|
|
463
475
|
if not registered:
|
|
464
476
|
self.conn.create_function(fn_name, arity, fn, **kwargs)
|
|
@@ -926,13 +938,18 @@ class Database:
|
|
|
926
938
|
other_column=foreign_keys_by_column[column_name].other_column,
|
|
927
939
|
)
|
|
928
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"
|
|
929
946
|
column_defs.append(
|
|
930
947
|
" [{column_name}] {column_type}{column_extras}".format(
|
|
931
948
|
column_name=column_name,
|
|
932
|
-
column_type=
|
|
933
|
-
column_extras=(
|
|
934
|
-
|
|
935
|
-
|
|
949
|
+
column_type=column_type_str,
|
|
950
|
+
column_extras=(
|
|
951
|
+
(" " + " ".join(column_extras)) if column_extras else ""
|
|
952
|
+
),
|
|
936
953
|
)
|
|
937
954
|
)
|
|
938
955
|
extra_pk = ""
|
|
@@ -1481,9 +1498,11 @@ class Table(Queryable):
|
|
|
1481
1498
|
def __repr__(self) -> str:
|
|
1482
1499
|
return "<Table {}{}>".format(
|
|
1483
1500
|
self.name,
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1501
|
+
(
|
|
1502
|
+
" (does not exist yet)"
|
|
1503
|
+
if not self.exists()
|
|
1504
|
+
else " ({})".format(", ".join(c.name for c in self.columns))
|
|
1505
|
+
),
|
|
1487
1506
|
)
|
|
1488
1507
|
|
|
1489
1508
|
@property
|
|
@@ -1962,6 +1981,30 @@ class Table(Queryable):
|
|
|
1962
1981
|
sqls.append(
|
|
1963
1982
|
"ALTER TABLE [{}] RENAME TO [{}];".format(new_table_name, self.name)
|
|
1964
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)
|
|
1965
2008
|
return sqls
|
|
1966
2009
|
|
|
1967
2010
|
def extract(
|
|
@@ -2640,6 +2683,7 @@ class Table(Queryable):
|
|
|
2640
2683
|
offset: Optional[int] = None,
|
|
2641
2684
|
where: Optional[str] = None,
|
|
2642
2685
|
where_args: Optional[Union[Iterable, dict]] = None,
|
|
2686
|
+
include_rank: bool = False,
|
|
2643
2687
|
quote: bool = False,
|
|
2644
2688
|
) -> Generator[dict, None, None]:
|
|
2645
2689
|
"""
|
|
@@ -2653,6 +2697,7 @@ class Table(Queryable):
|
|
|
2653
2697
|
:param offset: Optional integer SQL offset.
|
|
2654
2698
|
:param where: Extra SQL fragment for the WHERE clause
|
|
2655
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
|
|
2656
2701
|
:param quote: Apply quoting to disable any special characters in the search query
|
|
2657
2702
|
|
|
2658
2703
|
See :ref:`python_api_fts_search`.
|
|
@@ -2672,6 +2717,7 @@ class Table(Queryable):
|
|
|
2672
2717
|
limit=limit,
|
|
2673
2718
|
offset=offset,
|
|
2674
2719
|
where=where,
|
|
2720
|
+
include_rank=include_rank,
|
|
2675
2721
|
),
|
|
2676
2722
|
args,
|
|
2677
2723
|
)
|
|
@@ -2940,9 +2986,11 @@ class Table(Queryable):
|
|
|
2940
2986
|
value = jsonify_if_needed(
|
|
2941
2987
|
record.get(
|
|
2942
2988
|
key,
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2989
|
+
(
|
|
2990
|
+
None
|
|
2991
|
+
if key != hash_id
|
|
2992
|
+
else hash_record(record, hash_id_columns)
|
|
2993
|
+
),
|
|
2946
2994
|
)
|
|
2947
2995
|
)
|
|
2948
2996
|
if key in extracts:
|
|
@@ -304,10 +304,7 @@ def rows_from_file(
|
|
|
304
304
|
rows = rows_from_file(
|
|
305
305
|
fp, format=Format.CSV, dialect=csv.excel_tab, encoding=encoding
|
|
306
306
|
)[0]
|
|
307
|
-
return (
|
|
308
|
-
_extra_key_strategy(rows, ignore_extras, extras_key),
|
|
309
|
-
Format.TSV,
|
|
310
|
-
)
|
|
307
|
+
return _extra_key_strategy(rows, ignore_extras, extras_key), Format.TSV
|
|
311
308
|
elif format is None:
|
|
312
309
|
# Detect the format, then call this recursively
|
|
313
310
|
buffered = io.BufferedReader(cast(io.RawIOBase, fp), buffer_size=4096)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.38
|
|
4
4
|
Summary: CLI tool and Python library for manipulating SQLite databases
|
|
5
5
|
Home-page: https://github.com/simonw/sqlite-utils
|
|
6
6
|
Author: Simon Willison
|
|
@@ -16,13 +16,13 @@ Classifier: Intended Audience :: Science/Research
|
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
18
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
-
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: sqlite-fts4
|
|
@@ -33,7 +33,7 @@ Requires-Dist: python-dateutil
|
|
|
33
33
|
Requires-Dist: pluggy
|
|
34
34
|
Provides-Extra: test
|
|
35
35
|
Requires-Dist: pytest; extra == "test"
|
|
36
|
-
Requires-Dist: black; extra == "test"
|
|
36
|
+
Requires-Dist: black>=24.1.1; extra == "test"
|
|
37
37
|
Requires-Dist: hypothesis; extra == "test"
|
|
38
38
|
Requires-Dist: cogapp; extra == "test"
|
|
39
39
|
Provides-Extra: docs
|
|
@@ -1180,6 +1180,14 @@ def test_upsert_alter(db_path, tmpdir):
|
|
|
1180
1180
|
["age", "integer", "--default", "age", "3"],
|
|
1181
1181
|
("CREATE TABLE [t] (\n" " [age] INTEGER DEFAULT '3'\n" ")"),
|
|
1182
1182
|
),
|
|
1183
|
+
# Compound primary key
|
|
1184
|
+
(
|
|
1185
|
+
["category", "text", "name", "text", "--pk", "category", "--pk", "name"],
|
|
1186
|
+
(
|
|
1187
|
+
"CREATE TABLE [t] (\n [category] TEXT,\n [name] TEXT,\n"
|
|
1188
|
+
" PRIMARY KEY ([category], [name])\n)"
|
|
1189
|
+
),
|
|
1190
|
+
),
|
|
1183
1191
|
],
|
|
1184
1192
|
)
|
|
1185
1193
|
def test_create_table(args, schema):
|
|
@@ -2082,11 +2090,10 @@ def test_schema(tmpdir, options, expected):
|
|
|
2082
2090
|
def test_long_csv_column_value(tmpdir):
|
|
2083
2091
|
db_path = str(tmpdir / "test.db")
|
|
2084
2092
|
csv_path = str(tmpdir / "test.csv")
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
csv_file.close()
|
|
2093
|
+
with open(csv_path, "w") as csv_file:
|
|
2094
|
+
long_string = "a" * 131073
|
|
2095
|
+
csv_file.write("id,text\n")
|
|
2096
|
+
csv_file.write("1,{}\n".format(long_string))
|
|
2090
2097
|
result = CliRunner().invoke(
|
|
2091
2098
|
cli.cli,
|
|
2092
2099
|
["insert", db_path, "bigtable", csv_path, "--csv"],
|
|
@@ -2110,11 +2117,10 @@ def test_long_csv_column_value(tmpdir):
|
|
|
2110
2117
|
def test_import_no_headers(tmpdir, args, tsv):
|
|
2111
2118
|
db_path = str(tmpdir / "test.db")
|
|
2112
2119
|
csv_path = str(tmpdir / "test.csv")
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
csv_file.close()
|
|
2120
|
+
with open(csv_path, "w") as csv_file:
|
|
2121
|
+
sep = "\t" if tsv else ","
|
|
2122
|
+
csv_file.write("Cleo{sep}Dog{sep}5\n".format(sep=sep))
|
|
2123
|
+
csv_file.write("Tracy{sep}Spider{sep}7\n".format(sep=sep))
|
|
2118
2124
|
result = CliRunner().invoke(
|
|
2119
2125
|
cli.cli,
|
|
2120
2126
|
["insert", db_path, "creatures", csv_path] + args,
|
|
@@ -2410,11 +2416,13 @@ def test_create_table_strict(strict):
|
|
|
2410
2416
|
db = Database("test.db")
|
|
2411
2417
|
result = runner.invoke(
|
|
2412
2418
|
cli.cli,
|
|
2413
|
-
["create-table", "test.db", "items", "id", "integer"]
|
|
2419
|
+
["create-table", "test.db", "items", "id", "integer", "w", "float"]
|
|
2414
2420
|
+ (["--strict"] if strict else []),
|
|
2415
2421
|
)
|
|
2416
2422
|
assert result.exit_code == 0
|
|
2417
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}
|
|
2418
2426
|
|
|
2419
2427
|
|
|
2420
2428
|
@pytest.mark.parametrize("method", ("insert", "upsert"))
|
|
@@ -77,19 +77,24 @@ def test_insert_json_flatten_nl(tmpdir):
|
|
|
77
77
|
]
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
@pytest.mark.parametrize(
|
|
81
|
+
"args,expected_pks",
|
|
82
|
+
(
|
|
83
|
+
(["--pk", "id"], ["id"]),
|
|
84
|
+
(["--pk", "id", "--pk", "name"], ["id", "name"]),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
def test_insert_with_primary_keys(db_path, tmpdir, args, expected_pks):
|
|
81
88
|
json_path = str(tmpdir / "dog.json")
|
|
82
89
|
with open(json_path, "w") as fp:
|
|
83
90
|
fp.write(json.dumps({"id": 1, "name": "Cleo", "age": 4}))
|
|
84
|
-
result = CliRunner().invoke(
|
|
85
|
-
cli.cli, ["insert", db_path, "dogs", json_path, "--pk", "id"]
|
|
86
|
-
)
|
|
91
|
+
result = CliRunner().invoke(cli.cli, ["insert", db_path, "dogs", json_path] + args)
|
|
87
92
|
assert result.exit_code == 0
|
|
88
93
|
assert [{"id": 1, "age": 4, "name": "Cleo"}] == list(
|
|
89
94
|
Database(db_path).query("select * from dogs")
|
|
90
95
|
)
|
|
91
96
|
db = Database(db_path)
|
|
92
|
-
assert
|
|
97
|
+
assert db["dogs"].pks == expected_pks
|
|
93
98
|
|
|
94
99
|
|
|
95
100
|
def test_insert_multiple_with_primary_key(db_path, tmpdir):
|
|
@@ -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"})
|
|
@@ -7,7 +7,14 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@pytest.mark.parametrize("silent", (False, True))
|
|
10
|
-
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
"pk_args,expected_pks",
|
|
12
|
+
(
|
|
13
|
+
(["--pk", "path"], ["path"]),
|
|
14
|
+
(["--pk", "path", "--pk", "name"], ["path", "name"]),
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
def test_insert_files(silent, pk_args, expected_pks):
|
|
11
18
|
runner = CliRunner()
|
|
12
19
|
with runner.isolated_filesystem():
|
|
13
20
|
tmpdir = pathlib.Path(".")
|
|
@@ -42,7 +49,7 @@ def test_insert_files(silent):
|
|
|
42
49
|
cli.cli,
|
|
43
50
|
["insert-files", db_path, "files", str(tmpdir)]
|
|
44
51
|
+ cols
|
|
45
|
-
+
|
|
52
|
+
+ pk_args
|
|
46
53
|
+ (["--silent"] if silent else []),
|
|
47
54
|
catch_exceptions=False,
|
|
48
55
|
)
|
|
@@ -105,6 +112,7 @@ def test_insert_files(silent):
|
|
|
105
112
|
for colname, expected_type in expected_types.items():
|
|
106
113
|
for row in (one, two, three):
|
|
107
114
|
assert isinstance(row[colname], expected_type)
|
|
115
|
+
assert set(db["files"].pks) == set(expected_pks)
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
@pytest.mark.parametrize(
|
|
@@ -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
|