sqlite-utils 3.38a0__tar.gz → 3.39__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.38a0/sqlite_utils.egg-info → sqlite_utils-3.39}/PKG-INFO +18 -7
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/changelog.rst +21 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/cli-reference.rst +4 -4
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/cli.rst +18 -2
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/python-api.rst +9 -3
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/setup.py +5 -6
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/cli.py +35 -9
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/db.py +47 -4
- {sqlite_utils-3.38a0 → sqlite_utils-3.39/sqlite_utils.egg-info}/PKG-INFO +18 -7
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/requires.txt +2 -1
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_cli.py +76 -3
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_cli_bulk.py +26 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_cli_memory.py +16 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_create.py +7 -1
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_fts.py +27 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_recipes.py +1 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_transform.py +123 -1
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/LICENSE +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/MANIFEST.in +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/README.md +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/contributing.rst +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/index.rst +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/installation.rst +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/plugins.rst +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/docs/reference.rst +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/setup.cfg +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/__init__.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/__main__.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/hookspecs.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/plugins.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/py.typed +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/recipes.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils/utils.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/SOURCES.txt +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/dependency_links.txt +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/entry_points.txt +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/not-zip-safe +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/sqlite_utils.egg-info/top_level.txt +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/__init__.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/conftest.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_analyze.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_analyze_tables.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_attach.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_cli_convert.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_cli_insert.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_column_affinity.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_constructor.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_conversions.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_convert.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_create_view.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_default_value.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_delete.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_docs.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_duplicate.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_enable_counts.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_extract.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_extracts.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_get.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_gis.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_hypothesis.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_insert_files.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_introspect.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_lookup.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_m2m.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_plugins.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_query.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_recreate.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_register_function.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_rows.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_rows_from_file.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_sniff.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_suggest_column_types.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_tracer.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_update.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_upsert.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_utils.py +0 -0
- {sqlite_utils-3.38a0 → sqlite_utils-3.39}/tests/test_wal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.39
|
|
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
|
|
@@ -15,22 +15,21 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
26
24
|
Description-Content-Type: text/markdown
|
|
27
25
|
License-File: LICENSE
|
|
28
26
|
Requires-Dist: sqlite-fts4
|
|
29
|
-
Requires-Dist: click
|
|
27
|
+
Requires-Dist: click>=8.3.1
|
|
30
28
|
Requires-Dist: click-default-group>=1.2.3
|
|
31
29
|
Requires-Dist: tabulate
|
|
32
30
|
Requires-Dist: python-dateutil
|
|
33
31
|
Requires-Dist: pluggy
|
|
32
|
+
Requires-Dist: pip
|
|
34
33
|
Provides-Extra: test
|
|
35
34
|
Requires-Dist: pytest; extra == "test"
|
|
36
35
|
Requires-Dist: black>=24.1.1; extra == "test"
|
|
@@ -54,6 +53,18 @@ Provides-Extra: flake8
|
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
54
|
Provides-Extra: tui
|
|
56
55
|
Requires-Dist: trogon; extra == "tui"
|
|
56
|
+
Dynamic: author
|
|
57
|
+
Dynamic: classifier
|
|
58
|
+
Dynamic: description
|
|
59
|
+
Dynamic: description-content-type
|
|
60
|
+
Dynamic: home-page
|
|
61
|
+
Dynamic: license
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
Dynamic: project-url
|
|
64
|
+
Dynamic: provides-extra
|
|
65
|
+
Dynamic: requires-dist
|
|
66
|
+
Dynamic: requires-python
|
|
67
|
+
Dynamic: summary
|
|
57
68
|
|
|
58
69
|
# sqlite-utils
|
|
59
70
|
|
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
Changelog
|
|
5
5
|
===========
|
|
6
6
|
|
|
7
|
+
.. _v3_39:
|
|
8
|
+
|
|
9
|
+
3.39 (2025-11-24)
|
|
10
|
+
-----------------
|
|
11
|
+
|
|
12
|
+
- Fixed a bug with ``sqlite-utils install`` when the tool had been installed using ``uv``. (:issue:`687`)
|
|
13
|
+
- The ```--functions``` argument now optionally accepts a path to a Python file as an alternative to a string full of code, and can be specified multiple times - see :ref:`cli_query_functions`. (:issue:`659`)
|
|
14
|
+
- ``sqlite-utils`` now requires on Python 3.10 or higher.
|
|
15
|
+
|
|
16
|
+
`sqlite-utils 4.0a1 <https://sqlite-utils.datasette.io/en/latest/changelog.html#a1-2025-11-23>`__ is now available as an alpha with some `minor breaking changes <https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/>`__.
|
|
17
|
+
|
|
18
|
+
.. _v3_38:
|
|
19
|
+
|
|
20
|
+
3.38 (2024-11-23)
|
|
21
|
+
-----------------
|
|
22
|
+
|
|
23
|
+
- Plugins can now reuse the implementation of the ``sqlite-utils memory`` CLI command with the new ``return_db=True`` parameter. (:issue:`643`)
|
|
24
|
+
- ``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`)
|
|
25
|
+
- ``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>`__)
|
|
26
|
+
- 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`)
|
|
27
|
+
|
|
7
28
|
.. _v3_37:
|
|
8
29
|
|
|
9
30
|
3.37 (2024-07-18)
|
|
@@ -132,7 +132,7 @@ See :ref:`cli_query`.
|
|
|
132
132
|
-r, --raw Raw output, first column of first row
|
|
133
133
|
--raw-lines Raw output, first column of each row
|
|
134
134
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
|
135
|
-
--functions TEXT Python code
|
|
135
|
+
--functions TEXT Python code or file path defining custom SQL
|
|
136
136
|
functions
|
|
137
137
|
--load-extension TEXT Path to SQLite extension, with optional
|
|
138
138
|
:entrypoint
|
|
@@ -175,7 +175,7 @@ See :ref:`cli_memory`.
|
|
|
175
175
|
sqlite-utils memory animals.csv --schema
|
|
176
176
|
|
|
177
177
|
Options:
|
|
178
|
-
--functions TEXT Python code
|
|
178
|
+
--functions TEXT Python code or file path defining custom SQL
|
|
179
179
|
functions
|
|
180
180
|
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
|
181
181
|
filepath
|
|
@@ -375,7 +375,7 @@ See :ref:`cli_bulk`.
|
|
|
375
375
|
|
|
376
376
|
Options:
|
|
377
377
|
--batch-size INTEGER Commit every X records
|
|
378
|
-
--functions TEXT Python code
|
|
378
|
+
--functions TEXT Python code or file path defining custom SQL functions
|
|
379
379
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
|
380
380
|
{"a_b": 1}
|
|
381
381
|
--nl Expect newline-delimited JSON
|
|
@@ -1497,7 +1497,7 @@ See :ref:`cli_spatialite`.
|
|
|
1497
1497
|
paths. To load it from a specific path, use --load-extension.
|
|
1498
1498
|
|
|
1499
1499
|
Options:
|
|
1500
|
-
-t, --type [
|
|
1500
|
+
-t, --type [point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection|geometry]
|
|
1501
1501
|
Specify a geometry type for this column.
|
|
1502
1502
|
[default: GEOMETRY]
|
|
1503
1503
|
--srid INTEGER Spatial Reference ID. See
|
|
@@ -368,6 +368,22 @@ This example defines a function which extracts the domain from a URL:
|
|
|
368
368
|
|
|
369
369
|
Every callable object defined in the block will be registered as a SQL function with the same name, with the exception of functions with names that begin with an underscore.
|
|
370
370
|
|
|
371
|
+
You can also pass the path to a Python file containing function definitions:
|
|
372
|
+
|
|
373
|
+
.. code-block:: bash
|
|
374
|
+
|
|
375
|
+
sqlite-utils query sites.db "select url, domain(url) from urls" --functions functions.py
|
|
376
|
+
|
|
377
|
+
The ``--functions`` option can be used multiple times to load functions from multiple sources:
|
|
378
|
+
|
|
379
|
+
.. code-block:: bash
|
|
380
|
+
|
|
381
|
+
sqlite-utils query sites.db "select url, domain(url), extract_path(url) from urls" \
|
|
382
|
+
--functions domain_funcs.py \
|
|
383
|
+
--functions 'def extract_path(url):
|
|
384
|
+
from urllib.parse import urlparse
|
|
385
|
+
return urlparse(url).path'
|
|
386
|
+
|
|
371
387
|
.. _cli_query_extensions:
|
|
372
388
|
|
|
373
389
|
SQLite extensions
|
|
@@ -1128,7 +1144,7 @@ You can insert binary data into a BLOB column by first encoding it using base64
|
|
|
1128
1144
|
Inserting newline-delimited JSON
|
|
1129
1145
|
--------------------------------
|
|
1130
1146
|
|
|
1131
|
-
You can also import
|
|
1147
|
+
You can also import newline-delimited JSON (see `JSON Lines <https://jsonlines.org/>`__) using the ``--nl`` option:
|
|
1132
1148
|
|
|
1133
1149
|
.. code-block:: bash
|
|
1134
1150
|
|
|
@@ -2796,7 +2812,7 @@ To enable this feature you will need to install the ``trogon`` dependency. You c
|
|
|
2796
2812
|
|
|
2797
2813
|
.. code-block:: bash
|
|
2798
2814
|
|
|
2799
|
-
|
|
2815
|
+
sqlite-utils install trogon
|
|
2800
2816
|
|
|
2801
2817
|
Once installed, running the ``sqlite-utils tui`` command will launch the TUI interface:
|
|
2802
2818
|
|
|
@@ -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
|
|
|
@@ -2703,7 +2711,7 @@ By default, the name of the Python function will be used as the name of the SQL
|
|
|
2703
2711
|
|
|
2704
2712
|
print(db.execute('select rev("hello")').fetchone()[0])
|
|
2705
2713
|
|
|
2706
|
-
|
|
2714
|
+
If a function will return the exact same result for any given inputs you can register it as a `deterministic SQLite function <https://sqlite.org/deterministic.html>`__ allowing SQLite to apply some performance optimizations:
|
|
2707
2715
|
|
|
2708
2716
|
.. code-block:: python
|
|
2709
2717
|
|
|
@@ -2711,8 +2719,6 @@ Python 3.8 added the ability to register `deterministic SQLite functions <https:
|
|
|
2711
2719
|
def reverse_string(s):
|
|
2712
2720
|
return "".join(reversed(list(s)))
|
|
2713
2721
|
|
|
2714
|
-
If you run this on a version of Python prior to 3.8 your code will still work, but the ``deterministic=True`` parameter will be ignored.
|
|
2715
|
-
|
|
2716
2722
|
By default registering a function with the same name and number of arguments will have no effect - the ``Database`` instance keeps track of functions that have already been registered and skips registering them if ``@db.register_function`` is called a second time.
|
|
2717
2723
|
|
|
2718
2724
|
If you want to deliberately replace the registered function with a new implementation, use the ``replace=True`` argument:
|
|
@@ -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.39"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_long_description():
|
|
@@ -25,11 +25,12 @@ setup(
|
|
|
25
25
|
package_data={"sqlite_utils": ["py.typed"]},
|
|
26
26
|
install_requires=[
|
|
27
27
|
"sqlite-fts4",
|
|
28
|
-
"click",
|
|
28
|
+
"click>=8.3.1",
|
|
29
29
|
"click-default-group>=1.2.3",
|
|
30
30
|
"tabulate",
|
|
31
31
|
"python-dateutil",
|
|
32
32
|
"pluggy",
|
|
33
|
+
"pip",
|
|
33
34
|
],
|
|
34
35
|
extras_require={
|
|
35
36
|
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
|
|
@@ -64,20 +65,18 @@ setup(
|
|
|
64
65
|
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
|
65
66
|
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
|
66
67
|
},
|
|
67
|
-
python_requires=">=3.
|
|
68
|
+
python_requires=">=3.10",
|
|
68
69
|
classifiers=[
|
|
69
70
|
"Development Status :: 5 - Production/Stable",
|
|
70
71
|
"Intended Audience :: Developers",
|
|
71
72
|
"Intended Audience :: Science/Research",
|
|
72
73
|
"Intended Audience :: End Users/Desktop",
|
|
73
74
|
"Topic :: Database",
|
|
74
|
-
"License :: OSI Approved :: Apache Software License",
|
|
75
|
-
"Programming Language :: Python :: 3.8",
|
|
76
|
-
"Programming Language :: Python :: 3.9",
|
|
77
75
|
"Programming Language :: Python :: 3.10",
|
|
78
76
|
"Programming Language :: Python :: 3.11",
|
|
79
77
|
"Programming Language :: Python :: 3.12",
|
|
80
78
|
"Programming Language :: Python :: 3.13",
|
|
79
|
+
"Programming Language :: Python :: 3.14",
|
|
81
80
|
],
|
|
82
81
|
# Needed to bundle py.typed so mypy can see it:
|
|
83
82
|
zip_safe=False,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import click
|
|
3
3
|
from click_default_group import DefaultGroup # type: ignore
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
import hashlib
|
|
6
6
|
import pathlib
|
|
7
7
|
from runpy import run_module
|
|
@@ -962,7 +962,7 @@ def insert_upsert_implementation(
|
|
|
962
962
|
db = sqlite_utils.Database(path)
|
|
963
963
|
_load_extensions(db, load_extension)
|
|
964
964
|
if functions:
|
|
965
|
-
|
|
965
|
+
_register_functions_from_multiple(db, functions)
|
|
966
966
|
if (delimiter or quotechar or sniff or no_headers) and not tsv:
|
|
967
967
|
csv = True
|
|
968
968
|
if (nl + csv + tsv) >= 2:
|
|
@@ -1370,7 +1370,9 @@ def upsert(
|
|
|
1370
1370
|
@click.argument("file", type=click.File("rb"), required=True)
|
|
1371
1371
|
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
|
|
1372
1372
|
@click.option(
|
|
1373
|
-
"--functions",
|
|
1373
|
+
"--functions",
|
|
1374
|
+
help="Python code or file path defining custom SQL functions",
|
|
1375
|
+
multiple=True,
|
|
1374
1376
|
)
|
|
1375
1377
|
@import_options
|
|
1376
1378
|
@load_extension_option
|
|
@@ -1759,7 +1761,9 @@ def drop_view(path, view, ignore, load_extension):
|
|
|
1759
1761
|
help="Named :parameters for SQL query",
|
|
1760
1762
|
)
|
|
1761
1763
|
@click.option(
|
|
1762
|
-
"--functions",
|
|
1764
|
+
"--functions",
|
|
1765
|
+
help="Python code or file path defining custom SQL functions",
|
|
1766
|
+
multiple=True,
|
|
1763
1767
|
)
|
|
1764
1768
|
@load_extension_option
|
|
1765
1769
|
def query(
|
|
@@ -1796,7 +1800,7 @@ def query(
|
|
|
1796
1800
|
db.register_fts4_bm25()
|
|
1797
1801
|
|
|
1798
1802
|
if functions:
|
|
1799
|
-
|
|
1803
|
+
_register_functions_from_multiple(db, functions)
|
|
1800
1804
|
|
|
1801
1805
|
_execute_query(
|
|
1802
1806
|
db,
|
|
@@ -1824,7 +1828,9 @@ def query(
|
|
|
1824
1828
|
)
|
|
1825
1829
|
@click.argument("sql")
|
|
1826
1830
|
@click.option(
|
|
1827
|
-
"--functions",
|
|
1831
|
+
"--functions",
|
|
1832
|
+
help="Python code or file path defining custom SQL functions",
|
|
1833
|
+
multiple=True,
|
|
1828
1834
|
)
|
|
1829
1835
|
@click.option(
|
|
1830
1836
|
"--attach",
|
|
@@ -1996,7 +2002,7 @@ def memory(
|
|
|
1996
2002
|
db.register_fts4_bm25()
|
|
1997
2003
|
|
|
1998
2004
|
if functions:
|
|
1999
|
-
|
|
2005
|
+
_register_functions_from_multiple(db, functions)
|
|
2000
2006
|
|
|
2001
2007
|
if return_db:
|
|
2002
2008
|
return db
|
|
@@ -3203,8 +3209,12 @@ FILE_COLUMNS = {
|
|
|
3203
3209
|
"ctime": lambda p: p.stat().st_ctime,
|
|
3204
3210
|
"mtime_int": lambda p: int(p.stat().st_mtime),
|
|
3205
3211
|
"ctime_int": lambda p: int(p.stat().st_ctime),
|
|
3206
|
-
"mtime_iso": lambda p: datetime.
|
|
3207
|
-
|
|
3212
|
+
"mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc)
|
|
3213
|
+
.replace(tzinfo=None)
|
|
3214
|
+
.isoformat(),
|
|
3215
|
+
"ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc)
|
|
3216
|
+
.replace(tzinfo=None)
|
|
3217
|
+
.isoformat(),
|
|
3208
3218
|
"size": lambda p: p.stat().st_size,
|
|
3209
3219
|
"stem": lambda p: p.stem,
|
|
3210
3220
|
"suffix": lambda p: p.suffix,
|
|
@@ -3281,6 +3291,13 @@ def _load_extensions(db, load_extension):
|
|
|
3281
3291
|
|
|
3282
3292
|
def _register_functions(db, functions):
|
|
3283
3293
|
# Register any Python functions as SQL functions:
|
|
3294
|
+
# Check if this is a file path
|
|
3295
|
+
if "\n" not in functions and functions.endswith(".py"):
|
|
3296
|
+
try:
|
|
3297
|
+
functions = pathlib.Path(functions).read_text()
|
|
3298
|
+
except FileNotFoundError:
|
|
3299
|
+
raise click.ClickException("File not found: {}".format(functions))
|
|
3300
|
+
|
|
3284
3301
|
sqlite3.enable_callback_tracebacks(True)
|
|
3285
3302
|
globals = {}
|
|
3286
3303
|
try:
|
|
@@ -3291,3 +3308,12 @@ def _register_functions(db, functions):
|
|
|
3291
3308
|
for name, value in globals.items():
|
|
3292
3309
|
if callable(value) and not name.startswith("_"):
|
|
3293
3310
|
db.register_function(value, name=name)
|
|
3311
|
+
|
|
3312
|
+
|
|
3313
|
+
def _register_functions_from_multiple(db, functions_list):
|
|
3314
|
+
"""Register functions from multiple --functions arguments."""
|
|
3315
|
+
if not functions_list:
|
|
3316
|
+
return
|
|
3317
|
+
for functions in functions_list:
|
|
3318
|
+
if isinstance(functions, str) and functions.strip():
|
|
3319
|
+
_register_functions(db, functions)
|
|
@@ -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,
|
|
@@ -233,36 +237,43 @@ if pd:
|
|
|
233
237
|
|
|
234
238
|
class AlterError(Exception):
|
|
235
239
|
"Error altering table"
|
|
240
|
+
|
|
236
241
|
pass
|
|
237
242
|
|
|
238
243
|
|
|
239
244
|
class NoObviousTable(Exception):
|
|
240
245
|
"Could not tell which table this operation refers to"
|
|
246
|
+
|
|
241
247
|
pass
|
|
242
248
|
|
|
243
249
|
|
|
244
250
|
class NoTable(Exception):
|
|
245
251
|
"Specified table does not exist"
|
|
252
|
+
|
|
246
253
|
pass
|
|
247
254
|
|
|
248
255
|
|
|
249
256
|
class BadPrimaryKey(Exception):
|
|
250
257
|
"Table does not have a single obvious primary key"
|
|
258
|
+
|
|
251
259
|
pass
|
|
252
260
|
|
|
253
261
|
|
|
254
262
|
class NotFoundError(Exception):
|
|
255
263
|
"Record not found"
|
|
264
|
+
|
|
256
265
|
pass
|
|
257
266
|
|
|
258
267
|
|
|
259
268
|
class PrimaryKeyRequired(Exception):
|
|
260
269
|
"Primary key needs to be specified"
|
|
270
|
+
|
|
261
271
|
pass
|
|
262
272
|
|
|
263
273
|
|
|
264
274
|
class InvalidColumns(Exception):
|
|
265
275
|
"Specified columns do not exist"
|
|
276
|
+
|
|
266
277
|
pass
|
|
267
278
|
|
|
268
279
|
|
|
@@ -364,7 +375,7 @@ class Database:
|
|
|
364
375
|
pm.hook.prepare_connection(conn=self.conn)
|
|
365
376
|
self.strict = strict
|
|
366
377
|
|
|
367
|
-
def close(self):
|
|
378
|
+
def close(self) -> None:
|
|
368
379
|
"Close the SQLite connection, and the underlying database file"
|
|
369
380
|
self.conn.close()
|
|
370
381
|
|
|
@@ -934,10 +945,15 @@ class Database:
|
|
|
934
945
|
other_column=foreign_keys_by_column[column_name].other_column,
|
|
935
946
|
)
|
|
936
947
|
)
|
|
948
|
+
column_type_str = COLUMN_TYPE_MAPPING[column_type]
|
|
949
|
+
# Special case for strict tables to map FLOAT to REAL
|
|
950
|
+
# Refs https://github.com/simonw/sqlite-utils/issues/644
|
|
951
|
+
if strict and column_type_str == "FLOAT":
|
|
952
|
+
column_type_str = "REAL"
|
|
937
953
|
column_defs.append(
|
|
938
954
|
" [{column_name}] {column_type}{column_extras}".format(
|
|
939
955
|
column_name=column_name,
|
|
940
|
-
column_type=
|
|
956
|
+
column_type=column_type_str,
|
|
941
957
|
column_extras=(
|
|
942
958
|
(" " + " ".join(column_extras)) if column_extras else ""
|
|
943
959
|
),
|
|
@@ -1972,6 +1988,30 @@ class Table(Queryable):
|
|
|
1972
1988
|
sqls.append(
|
|
1973
1989
|
"ALTER TABLE [{}] RENAME TO [{}];".format(new_table_name, self.name)
|
|
1974
1990
|
)
|
|
1991
|
+
# Re-add existing indexes
|
|
1992
|
+
for index in self.indexes:
|
|
1993
|
+
if index.origin != "pk":
|
|
1994
|
+
index_sql = self.db.execute(
|
|
1995
|
+
"""SELECT sql FROM sqlite_master WHERE type = 'index' AND name = :index_name;""",
|
|
1996
|
+
{"index_name": index.name},
|
|
1997
|
+
).fetchall()[0][0]
|
|
1998
|
+
if index_sql is None:
|
|
1999
|
+
raise TransformError(
|
|
2000
|
+
f"Index '{index.name}' on table '{self.name}' does not have a "
|
|
2001
|
+
"CREATE INDEX statement. You must manually drop this index prior to running this "
|
|
2002
|
+
"transformation and manually recreate the new index after running this transformation."
|
|
2003
|
+
)
|
|
2004
|
+
if keep_table:
|
|
2005
|
+
sqls.append(f"DROP INDEX IF EXISTS [{index.name}];")
|
|
2006
|
+
for col in index.columns:
|
|
2007
|
+
if col in rename.keys() or col in drop:
|
|
2008
|
+
raise TransformError(
|
|
2009
|
+
f"Index '{index.name}' column '{col}' is not in updated table '{self.name}'. "
|
|
2010
|
+
f"You must manually drop this index prior to running this transformation "
|
|
2011
|
+
f"and manually recreate the new index after running this transformation. "
|
|
2012
|
+
f"The original index sql statement is: `{index_sql}`. No changes have been applied to this table."
|
|
2013
|
+
)
|
|
2014
|
+
sqls.append(index_sql)
|
|
1975
2015
|
return sqls
|
|
1976
2016
|
|
|
1977
2017
|
def extract(
|
|
@@ -2650,6 +2690,7 @@ class Table(Queryable):
|
|
|
2650
2690
|
offset: Optional[int] = None,
|
|
2651
2691
|
where: Optional[str] = None,
|
|
2652
2692
|
where_args: Optional[Union[Iterable, dict]] = None,
|
|
2693
|
+
include_rank: bool = False,
|
|
2653
2694
|
quote: bool = False,
|
|
2654
2695
|
) -> Generator[dict, None, None]:
|
|
2655
2696
|
"""
|
|
@@ -2663,6 +2704,7 @@ class Table(Queryable):
|
|
|
2663
2704
|
:param offset: Optional integer SQL offset.
|
|
2664
2705
|
:param where: Extra SQL fragment for the WHERE clause
|
|
2665
2706
|
:param where_args: Arguments to use for :param placeholders in the extra WHERE clause
|
|
2707
|
+
:param include_rank: Select the search rank column in the final query
|
|
2666
2708
|
:param quote: Apply quoting to disable any special characters in the search query
|
|
2667
2709
|
|
|
2668
2710
|
See :ref:`python_api_fts_search`.
|
|
@@ -2682,6 +2724,7 @@ class Table(Queryable):
|
|
|
2682
2724
|
limit=limit,
|
|
2683
2725
|
offset=offset,
|
|
2684
2726
|
where=where,
|
|
2727
|
+
include_rank=include_rank,
|
|
2685
2728
|
),
|
|
2686
2729
|
args,
|
|
2687
2730
|
)
|
|
@@ -3167,7 +3210,7 @@ class Table(Queryable):
|
|
|
3167
3210
|
:param not_null: Set of strings specifying columns that should be ``NOT NULL``.
|
|
3168
3211
|
:param defaults: Dictionary specifying default values for specific columns.
|
|
3169
3212
|
:param hash_id: Name of a column to create and use as a primary key, where the
|
|
3170
|
-
value of
|
|
3213
|
+
value of that primary key will be derived as a SHA1 hash of the other column values
|
|
3171
3214
|
in the record. ``hash_id="id"`` is a common column name used for this.
|
|
3172
3215
|
:param alter: Boolean, should any missing columns be added automatically?
|
|
3173
3216
|
:param ignore: Boolean, if a record already exists with this primary key, ignore this insert.
|
|
@@ -3816,7 +3859,7 @@ def jsonify_if_needed(value):
|
|
|
3816
3859
|
|
|
3817
3860
|
|
|
3818
3861
|
def resolve_extracts(
|
|
3819
|
-
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]]
|
|
3862
|
+
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]],
|
|
3820
3863
|
) -> dict:
|
|
3821
3864
|
if extracts is None:
|
|
3822
3865
|
extracts = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.39
|
|
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
|
|
@@ -15,22 +15,21 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
17
17
|
Classifier: Topic :: Database
|
|
18
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
26
24
|
Description-Content-Type: text/markdown
|
|
27
25
|
License-File: LICENSE
|
|
28
26
|
Requires-Dist: sqlite-fts4
|
|
29
|
-
Requires-Dist: click
|
|
27
|
+
Requires-Dist: click>=8.3.1
|
|
30
28
|
Requires-Dist: click-default-group>=1.2.3
|
|
31
29
|
Requires-Dist: tabulate
|
|
32
30
|
Requires-Dist: python-dateutil
|
|
33
31
|
Requires-Dist: pluggy
|
|
32
|
+
Requires-Dist: pip
|
|
34
33
|
Provides-Extra: test
|
|
35
34
|
Requires-Dist: pytest; extra == "test"
|
|
36
35
|
Requires-Dist: black>=24.1.1; extra == "test"
|
|
@@ -54,6 +53,18 @@ Provides-Extra: flake8
|
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
54
|
Provides-Extra: tui
|
|
56
55
|
Requires-Dist: trogon; extra == "tui"
|
|
56
|
+
Dynamic: author
|
|
57
|
+
Dynamic: classifier
|
|
58
|
+
Dynamic: description
|
|
59
|
+
Dynamic: description-content-type
|
|
60
|
+
Dynamic: home-page
|
|
61
|
+
Dynamic: license
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
Dynamic: project-url
|
|
64
|
+
Dynamic: provides-extra
|
|
65
|
+
Dynamic: requires-dist
|
|
66
|
+
Dynamic: requires-python
|
|
67
|
+
Dynamic: summary
|
|
57
68
|
|
|
58
69
|
# sqlite-utils
|
|
59
70
|
|
|
@@ -806,6 +806,77 @@ def test_hidden_functions_are_hidden(db_path):
|
|
|
806
806
|
assert "_two" not in functions
|
|
807
807
|
|
|
808
808
|
|
|
809
|
+
def test_query_functions_from_file(db_path, tmp_path):
|
|
810
|
+
# Create a temporary file with function definitions
|
|
811
|
+
functions_file = tmp_path / "my_functions.py"
|
|
812
|
+
functions_file.write_text(TEST_FUNCTIONS)
|
|
813
|
+
|
|
814
|
+
result = CliRunner().invoke(
|
|
815
|
+
cli.cli,
|
|
816
|
+
[
|
|
817
|
+
db_path,
|
|
818
|
+
"select zero(), one(1), two(1, 2)",
|
|
819
|
+
"--functions",
|
|
820
|
+
str(functions_file),
|
|
821
|
+
],
|
|
822
|
+
)
|
|
823
|
+
assert result.exit_code == 0
|
|
824
|
+
assert json.loads(result.output.strip()) == [
|
|
825
|
+
{"zero()": 0, "one(1)": 1, "two(1, 2)": 3}
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def test_query_functions_file_not_found(db_path):
|
|
830
|
+
result = CliRunner().invoke(
|
|
831
|
+
cli.cli,
|
|
832
|
+
[
|
|
833
|
+
db_path,
|
|
834
|
+
"select zero()",
|
|
835
|
+
"--functions",
|
|
836
|
+
"nonexistent.py",
|
|
837
|
+
],
|
|
838
|
+
)
|
|
839
|
+
assert result.exit_code == 1
|
|
840
|
+
assert "File not found: nonexistent.py" in result.output
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def test_query_functions_multiple_invocations(db_path):
|
|
844
|
+
# Test using --functions multiple times
|
|
845
|
+
result = CliRunner().invoke(
|
|
846
|
+
cli.cli,
|
|
847
|
+
[
|
|
848
|
+
db_path,
|
|
849
|
+
"select triple(2), quadruple(2)",
|
|
850
|
+
"--functions",
|
|
851
|
+
"def triple(x):\n return x * 3",
|
|
852
|
+
"--functions",
|
|
853
|
+
"def quadruple(x):\n return x * 4",
|
|
854
|
+
],
|
|
855
|
+
)
|
|
856
|
+
assert result.exit_code == 0
|
|
857
|
+
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def test_query_functions_file_and_inline(db_path, tmp_path):
|
|
861
|
+
# Test combining file and inline code
|
|
862
|
+
functions_file = tmp_path / "file_funcs.py"
|
|
863
|
+
functions_file.write_text("def triple(x):\n return x * 3")
|
|
864
|
+
|
|
865
|
+
result = CliRunner().invoke(
|
|
866
|
+
cli.cli,
|
|
867
|
+
[
|
|
868
|
+
db_path,
|
|
869
|
+
"select triple(2), quadruple(2)",
|
|
870
|
+
"--functions",
|
|
871
|
+
str(functions_file),
|
|
872
|
+
"--functions",
|
|
873
|
+
"def quadruple(x):\n return x * 4",
|
|
874
|
+
],
|
|
875
|
+
)
|
|
876
|
+
assert result.exit_code == 0
|
|
877
|
+
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
|
878
|
+
|
|
879
|
+
|
|
809
880
|
LOREM_IPSUM_COMPRESSED = (
|
|
810
881
|
b"x\x9c\xed\xd1\xcdq\x03!\x0c\x05\xe0\xbb\xabP\x01\x1eW\x91\xdc|M\x01\n\xc8\x8e"
|
|
811
882
|
b"f\xf83H\x1e\x97\x1f\x91M\x8e\xe9\xe0\xdd\x96\x05\x84\xf4\xbek\x9fRI\xc7\xf2J"
|
|
@@ -916,7 +987,7 @@ def test_query_json_with_json_cols(db_path):
|
|
|
916
987
|
|
|
917
988
|
@pytest.mark.parametrize(
|
|
918
989
|
"content,is_binary",
|
|
919
|
-
[(b"\x00\
|
|
990
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
920
991
|
)
|
|
921
992
|
def test_query_raw(db_path, content, is_binary):
|
|
922
993
|
Database(db_path)["files"].insert({"content": content})
|
|
@@ -931,7 +1002,7 @@ def test_query_raw(db_path, content, is_binary):
|
|
|
931
1002
|
|
|
932
1003
|
@pytest.mark.parametrize(
|
|
933
1004
|
"content,is_binary",
|
|
934
|
-
[(b"\x00\
|
|
1005
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
935
1006
|
)
|
|
936
1007
|
def test_query_raw_lines(db_path, content, is_binary):
|
|
937
1008
|
Database(db_path)["files"].insert_all({"content": content} for _ in range(3))
|
|
@@ -2416,11 +2487,13 @@ def test_create_table_strict(strict):
|
|
|
2416
2487
|
db = Database("test.db")
|
|
2417
2488
|
result = runner.invoke(
|
|
2418
2489
|
cli.cli,
|
|
2419
|
-
["create-table", "test.db", "items", "id", "integer"]
|
|
2490
|
+
["create-table", "test.db", "items", "id", "integer", "w", "float"]
|
|
2420
2491
|
+ (["--strict"] if strict else []),
|
|
2421
2492
|
)
|
|
2422
2493
|
assert result.exit_code == 0
|
|
2423
2494
|
assert db["items"].strict == strict or not db.supports_strict
|
|
2495
|
+
# Should have a floating point column
|
|
2496
|
+
assert db["items"].columns_dict == {"id": int, "w": float}
|
|
2424
2497
|
|
|
2425
2498
|
|
|
2426
2499
|
@pytest.mark.parametrize("method", ("insert", "upsert"))
|
|
@@ -45,6 +45,32 @@ def test_cli_bulk(test_db_and_path):
|
|
|
45
45
|
] == list(db["example"].rows)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def test_cli_bulk_multiple_functions(test_db_and_path):
|
|
49
|
+
db, db_path = test_db_and_path
|
|
50
|
+
result = CliRunner().invoke(
|
|
51
|
+
cli.cli,
|
|
52
|
+
[
|
|
53
|
+
"bulk",
|
|
54
|
+
db_path,
|
|
55
|
+
"insert into example (id, name) values (:id, myupper(mylower(:name)))",
|
|
56
|
+
"-",
|
|
57
|
+
"--nl",
|
|
58
|
+
"--functions",
|
|
59
|
+
"myupper = lambda s: s.upper()",
|
|
60
|
+
"--functions",
|
|
61
|
+
"mylower = lambda s: s.lower()",
|
|
62
|
+
],
|
|
63
|
+
input='{"id": 3, "name": "ThReE"}\n{"id": 4, "name": "FoUr"}\n',
|
|
64
|
+
)
|
|
65
|
+
assert result.exit_code == 0, result.output
|
|
66
|
+
assert [
|
|
67
|
+
{"id": 1, "name": "One"},
|
|
68
|
+
{"id": 2, "name": "Two"},
|
|
69
|
+
{"id": 3, "name": "THREE"},
|
|
70
|
+
{"id": 4, "name": "FOUR"},
|
|
71
|
+
] == list(db["example"].rows)
|
|
72
|
+
|
|
73
|
+
|
|
48
74
|
def test_cli_bulk_batch_size(test_db_and_path):
|
|
49
75
|
db, db_path = test_db_and_path
|
|
50
76
|
proc = subprocess.Popen(
|
|
@@ -307,6 +307,22 @@ def test_memory_functions():
|
|
|
307
307
|
assert result.output.strip() == '[{"hello()": "Hello"}]'
|
|
308
308
|
|
|
309
309
|
|
|
310
|
+
def test_memory_functions_multiple():
|
|
311
|
+
result = CliRunner().invoke(
|
|
312
|
+
cli.cli,
|
|
313
|
+
[
|
|
314
|
+
"memory",
|
|
315
|
+
"select triple(2), quadruple(2)",
|
|
316
|
+
"--functions",
|
|
317
|
+
"def triple(x):\n return x * 3",
|
|
318
|
+
"--functions",
|
|
319
|
+
"def quadruple(x):\n return x * 4",
|
|
320
|
+
],
|
|
321
|
+
)
|
|
322
|
+
assert result.exit_code == 0
|
|
323
|
+
assert result.output.strip() == '[{"triple(2)": 6, "quadruple(2)": 8}]'
|
|
324
|
+
|
|
325
|
+
|
|
310
326
|
def test_memory_return_db(tmpdir):
|
|
311
327
|
# https://github.com/simonw/sqlite-utils/issues/643
|
|
312
328
|
from sqlite_utils.cli import cli
|
|
@@ -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"})
|
|
@@ -64,6 +64,7 @@ def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
|
|
64
64
|
|
|
65
65
|
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
|
66
66
|
@pytest.mark.parametrize("errors", (None, recipes.SET_NULL, recipes.IGNORE))
|
|
67
|
+
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
|
|
67
68
|
def test_dateparse_errors(fresh_db, fn, errors):
|
|
68
69
|
fresh_db["example"].insert_all(
|
|
69
70
|
[
|
|
@@ -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
|