sqlite-utils 3.38__tar.gz → 4.0a0__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.38/sqlite_utils.egg-info → sqlite_utils-4.0a0}/PKG-INFO +15 -6
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/changelog.rst +12 -1
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/cli-reference.rst +0 -18
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/cli.rst +2 -28
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/python-api.rst +8 -3
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/setup.py +2 -4
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/cli.py +3 -10
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/db.py +187 -106
- {sqlite_utils-3.38 → sqlite_utils-4.0a0/sqlite_utils.egg-info}/PKG-INFO +15 -6
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/requires.txt +0 -3
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_cli.py +5 -8
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_create.py +5 -3
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_plugins.py +15 -1
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_tracer.py +1 -1
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_upsert.py +5 -2
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/LICENSE +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/MANIFEST.in +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/README.md +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/contributing.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/index.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/installation.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/plugins.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/docs/reference.rst +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/setup.cfg +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/__init__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/__main__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/hookspecs.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/plugins.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/py.typed +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/recipes.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils/utils.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/SOURCES.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/dependency_links.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/entry_points.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/not-zip-safe +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/sqlite_utils.egg-info/top_level.txt +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/__init__.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/conftest.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_analyze.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_analyze_tables.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_attach.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_cli_bulk.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_cli_convert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_cli_insert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_cli_memory.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_column_affinity.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_constructor.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_conversions.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_convert.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_create_view.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_default_value.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_delete.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_docs.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_duplicate.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_enable_counts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_extract.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_extracts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_fts.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_get.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_gis.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_hypothesis.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_insert_files.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_introspect.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_lookup.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_m2m.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_query.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_recipes.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_recreate.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_register_function.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_rows.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_rows_from_file.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_sniff.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_suggest_column_types.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_transform.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_update.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/tests/test_utils.py +0 -0
- {sqlite_utils-3.38 → sqlite_utils-4.0a0}/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
|
+
Version: 4.0a0
|
|
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,12 @@ 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.8
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: sqlite-fts4
|
|
@@ -52,8 +51,18 @@ Requires-Dist: types-pluggy; extra == "mypy"
|
|
|
52
51
|
Requires-Dist: data-science-types; extra == "mypy"
|
|
53
52
|
Provides-Extra: flake8
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Dynamic: author
|
|
55
|
+
Dynamic: classifier
|
|
56
|
+
Dynamic: description
|
|
57
|
+
Dynamic: description-content-type
|
|
58
|
+
Dynamic: home-page
|
|
59
|
+
Dynamic: license
|
|
60
|
+
Dynamic: license-file
|
|
61
|
+
Dynamic: project-url
|
|
62
|
+
Dynamic: provides-extra
|
|
63
|
+
Dynamic: requires-dist
|
|
64
|
+
Dynamic: requires-python
|
|
65
|
+
Dynamic: summary
|
|
57
66
|
|
|
58
67
|
# sqlite-utils
|
|
59
68
|
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
Changelog
|
|
5
5
|
===========
|
|
6
6
|
|
|
7
|
+
.. _v4_0a0:
|
|
8
|
+
|
|
9
|
+
4.0a0 (2025-05-08)
|
|
10
|
+
------------------
|
|
11
|
+
|
|
12
|
+
- Upsert operations now use SQLite's ``INSERT ... ON CONFLICT SET`` syntax on all SQLite versions later than 3.23.1. This is a very slight breaking change for apps that depend on the previous ``INSERT OR IGNORE`` followed by ``UPDATE`` behavior. (:issue:`652`)
|
|
13
|
+
- Python library users can opt-in to the previous implementation by passing ``use_old_upsert=True`` to the ``Database()`` constructor, see :ref:`python_api_old_upsert`.
|
|
14
|
+
- Dropped support for Python 3.8, added support for Python 3.13. (:issue:`646`)
|
|
15
|
+
- ``sqlite-utils tui`` is now provided by the `sqlite-utils-tui <https://github.com/simonw/sqlite-utils-tui>`__ plugin. (:issue:`648`)
|
|
16
|
+
- Test suite now also runs against SQLite 3.23.1, the last version (from 2018-04-10) before the new ``INSERT ... ON CONFLICT SET`` syntax was added. (:issue:`654`)
|
|
17
|
+
|
|
7
18
|
.. _v3_38:
|
|
8
19
|
|
|
9
20
|
3.38 (2024-11-23)
|
|
@@ -111,7 +122,7 @@ This release introduces a new :ref:`plugin system <plugins>`. Read more about th
|
|
|
111
122
|
3.32 (2023-05-21)
|
|
112
123
|
-----------------
|
|
113
124
|
|
|
114
|
-
- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon <https://github.com/Textualize/trogon>`__. This requires an optional dependency, installed using ``sqlite-utils install trogon``.
|
|
125
|
+
- New experimental ``sqlite-utils tui`` interface for interactively building command-line invocations, powered by `Trogon <https://github.com/Textualize/trogon>`__. This requires an optional dependency, installed using ``sqlite-utils install trogon``. (:issue:`545`)
|
|
115
126
|
- ``sqlite-utils analyze-tables`` command (:ref:`documentation <cli_analyze_tables>`) now has a ``--common-limit 20`` option for changing the number of common/least-common values shown for each column. (:issue:`544`)
|
|
116
127
|
- ``sqlite-utils analyze-tables --no-most`` and ``--no-least`` options for disabling calculation of most-common and least-common values.
|
|
117
128
|
- If a column contains only ``null`` values, ``analyze-tables`` will no longer attempt to calculate the most common and least common values for that column. (:issue:`547`)
|
|
@@ -65,7 +65,6 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.
|
|
|
65
65
|
"create-spatial-index": "cli_spatialite_indexes",
|
|
66
66
|
"install": "cli_install",
|
|
67
67
|
"uninstall": "cli_uninstall",
|
|
68
|
-
"tui": "cli_tui",
|
|
69
68
|
}
|
|
70
69
|
commands.sort(key = lambda command: go_first.index(command) if command in go_first else 999)
|
|
71
70
|
cog.out("\n")
|
|
@@ -1045,23 +1044,6 @@ disable-fts
|
|
|
1045
1044
|
-h, --help Show this message and exit.
|
|
1046
1045
|
|
|
1047
1046
|
|
|
1048
|
-
.. _cli_ref_tui:
|
|
1049
|
-
|
|
1050
|
-
tui
|
|
1051
|
-
===
|
|
1052
|
-
|
|
1053
|
-
See :ref:`cli_tui`.
|
|
1054
|
-
|
|
1055
|
-
::
|
|
1056
|
-
|
|
1057
|
-
Usage: sqlite-utils tui [OPTIONS]
|
|
1058
|
-
|
|
1059
|
-
Open Textual TUI.
|
|
1060
|
-
|
|
1061
|
-
Options:
|
|
1062
|
-
-h, --help Show this message and exit.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
1047
|
.. _cli_ref_optimize:
|
|
1066
1048
|
|
|
1067
1049
|
optimize
|
|
@@ -1128,7 +1128,7 @@ You can insert binary data into a BLOB column by first encoding it using base64
|
|
|
1128
1128
|
Inserting newline-delimited JSON
|
|
1129
1129
|
--------------------------------
|
|
1130
1130
|
|
|
1131
|
-
You can also import
|
|
1131
|
+
You can also import newline-delimited JSON (see `JSON Lines <https://jsonlines.org/>`__) using the ``--nl`` option:
|
|
1132
1132
|
|
|
1133
1133
|
.. code-block:: bash
|
|
1134
1134
|
|
|
@@ -1285,7 +1285,7 @@ You can set the ``SQLITE_UTILS_DETECT_TYPES`` environment variable if you want `
|
|
|
1285
1285
|
|
|
1286
1286
|
If a CSV or TSV file includes empty cells, like this one:
|
|
1287
1287
|
|
|
1288
|
-
|
|
1288
|
+
::
|
|
1289
1289
|
|
|
1290
1290
|
name,age,weight
|
|
1291
1291
|
Cleo,6,
|
|
@@ -2782,29 +2782,3 @@ You can uninstall packages that were installed using ``sqlite-utils install`` wi
|
|
|
2782
2782
|
sqlite-utils uninstall beautifulsoup4
|
|
2783
2783
|
|
|
2784
2784
|
Use ``-y`` to skip the request for confirmation.
|
|
2785
|
-
|
|
2786
|
-
.. _cli_tui:
|
|
2787
|
-
|
|
2788
|
-
Experimental TUI
|
|
2789
|
-
================
|
|
2790
|
-
|
|
2791
|
-
A TUI is a "text user interface" (or "terminal user interface") - a keyboard and mouse driven graphical interface running in your terminal.
|
|
2792
|
-
|
|
2793
|
-
``sqlite-utils`` has experimental support for a TUI for building command-line invocations, built on top of the `Trogon <https://github.com/Textualize/trogon/>`__ TUI library.
|
|
2794
|
-
|
|
2795
|
-
To enable this feature you will need to install the ``trogon`` dependency. You can do that like so:
|
|
2796
|
-
|
|
2797
|
-
.. code-block:: bash
|
|
2798
|
-
|
|
2799
|
-
sqlite-utils install trogon
|
|
2800
|
-
|
|
2801
|
-
Once installed, running the ``sqlite-utils tui`` command will launch the TUI interface:
|
|
2802
|
-
|
|
2803
|
-
.. code-block:: bash
|
|
2804
|
-
|
|
2805
|
-
sqlite-utils tui
|
|
2806
|
-
|
|
2807
|
-
You can then construct a command by selecting options from the menus, and execute it using ``Ctrl+R``.
|
|
2808
|
-
|
|
2809
|
-
.. image:: _static/img/tui.png
|
|
2810
|
-
:alt: A TUI interface for sqlite-utils - the left column shows a list of commands, while the right panel has a form for constructing arguments to the add-column command.
|
|
@@ -927,6 +927,13 @@ An ``upsert_all()`` method is also available, which behaves like ``insert_all()`
|
|
|
927
927
|
.. note::
|
|
928
928
|
``.upsert()`` and ``.upsert_all()`` in sqlite-utils 1.x worked like ``.insert(..., replace=True)`` and ``.insert_all(..., replace=True)`` do in 2.x. See `issue #66 <https://github.com/simonw/sqlite-utils/issues/66>`__ for details of this change.
|
|
929
929
|
|
|
930
|
+
.. _python_api_old_upsert:
|
|
931
|
+
|
|
932
|
+
Alternative upserts using INSERT OR IGNORE
|
|
933
|
+
------------------------------------------
|
|
934
|
+
|
|
935
|
+
Upserts use ``INSERT INTO ... ON CONFLICT SET``. Prior to ``sqlite-utils 4.0`` these used a sequence of ``INSERT OR IGNORE`` followed by an ``UPDATE``. This older method is still used for SQLite 3.23.1 and earlier. You can force the older implementation by passing ``use_old_upsert=True`` to the ``Database()`` constructor.
|
|
936
|
+
|
|
930
937
|
.. _python_api_convert:
|
|
931
938
|
|
|
932
939
|
Converting data in columns
|
|
@@ -2711,7 +2718,7 @@ By default, the name of the Python function will be used as the name of the SQL
|
|
|
2711
2718
|
|
|
2712
2719
|
print(db.execute('select rev("hello")').fetchone()[0])
|
|
2713
2720
|
|
|
2714
|
-
|
|
2721
|
+
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:
|
|
2715
2722
|
|
|
2716
2723
|
.. code-block:: python
|
|
2717
2724
|
|
|
@@ -2719,8 +2726,6 @@ Python 3.8 added the ability to register `deterministic SQLite functions <https:
|
|
|
2719
2726
|
def reverse_string(s):
|
|
2720
2727
|
return "".join(reversed(list(s)))
|
|
2721
2728
|
|
|
2722
|
-
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.
|
|
2723
|
-
|
|
2724
2729
|
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.
|
|
2725
2730
|
|
|
2726
2731
|
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 = "
|
|
5
|
+
VERSION = "4.0a0"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_long_description():
|
|
@@ -50,7 +50,6 @@ setup(
|
|
|
50
50
|
"data-science-types",
|
|
51
51
|
],
|
|
52
52
|
"flake8": ["flake8"],
|
|
53
|
-
"tui": ["trogon"],
|
|
54
53
|
},
|
|
55
54
|
entry_points="""
|
|
56
55
|
[console_scripts]
|
|
@@ -64,7 +63,7 @@ setup(
|
|
|
64
63
|
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
|
65
64
|
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
|
66
65
|
},
|
|
67
|
-
python_requires=">=3.
|
|
66
|
+
python_requires=">=3.9",
|
|
68
67
|
classifiers=[
|
|
69
68
|
"Development Status :: 5 - Production/Stable",
|
|
70
69
|
"Intended Audience :: Developers",
|
|
@@ -72,7 +71,6 @@ setup(
|
|
|
72
71
|
"Intended Audience :: End Users/Desktop",
|
|
73
72
|
"Topic :: Database",
|
|
74
73
|
"License :: OSI Approved :: Apache Software License",
|
|
75
|
-
"Programming Language :: Python :: 3.8",
|
|
76
74
|
"Programming Language :: Python :: 3.9",
|
|
77
75
|
"Programming Language :: Python :: 3.10",
|
|
78
76
|
"Programming Language :: Python :: 3.11",
|
|
@@ -35,11 +35,6 @@ from .utils import (
|
|
|
35
35
|
TypeTracker,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
try:
|
|
39
|
-
import trogon # type: ignore
|
|
40
|
-
except ImportError:
|
|
41
|
-
trogon = None
|
|
42
|
-
|
|
43
38
|
|
|
44
39
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
45
40
|
|
|
@@ -127,10 +122,6 @@ def cli():
|
|
|
127
122
|
pass
|
|
128
123
|
|
|
129
124
|
|
|
130
|
-
if trogon is not None:
|
|
131
|
-
cli = trogon.tui()(cli)
|
|
132
|
-
|
|
133
|
-
|
|
134
125
|
@cli.command()
|
|
135
126
|
@click.argument(
|
|
136
127
|
"path",
|
|
@@ -1107,7 +1098,9 @@ def insert_upsert_implementation(
|
|
|
1107
1098
|
if (
|
|
1108
1099
|
isinstance(e, OperationalError)
|
|
1109
1100
|
and e.args
|
|
1110
|
-
and
|
|
1101
|
+
and (
|
|
1102
|
+
"has no column named" in e.args[0] or "no such column" in e.args[0]
|
|
1103
|
+
)
|
|
1111
1104
|
):
|
|
1112
1105
|
raise click.ClickException(
|
|
1113
1106
|
"{}\n\nTry using --alter to add additional columns".format(
|
|
@@ -237,37 +237,30 @@ if pd:
|
|
|
237
237
|
|
|
238
238
|
class AlterError(Exception):
|
|
239
239
|
"Error altering table"
|
|
240
|
-
pass
|
|
241
240
|
|
|
242
241
|
|
|
243
242
|
class NoObviousTable(Exception):
|
|
244
243
|
"Could not tell which table this operation refers to"
|
|
245
|
-
pass
|
|
246
244
|
|
|
247
245
|
|
|
248
246
|
class NoTable(Exception):
|
|
249
247
|
"Specified table does not exist"
|
|
250
|
-
pass
|
|
251
248
|
|
|
252
249
|
|
|
253
250
|
class BadPrimaryKey(Exception):
|
|
254
251
|
"Table does not have a single obvious primary key"
|
|
255
|
-
pass
|
|
256
252
|
|
|
257
253
|
|
|
258
254
|
class NotFoundError(Exception):
|
|
259
255
|
"Record not found"
|
|
260
|
-
pass
|
|
261
256
|
|
|
262
257
|
|
|
263
258
|
class PrimaryKeyRequired(Exception):
|
|
264
259
|
"Primary key needs to be specified"
|
|
265
|
-
pass
|
|
266
260
|
|
|
267
261
|
|
|
268
262
|
class InvalidColumns(Exception):
|
|
269
263
|
"Specified columns do not exist"
|
|
270
|
-
pass
|
|
271
264
|
|
|
272
265
|
|
|
273
266
|
class DescIndex(str):
|
|
@@ -311,6 +304,8 @@ class Database:
|
|
|
311
304
|
``sql, parameters`` every time a SQL query is executed
|
|
312
305
|
:param use_counts_table: set to ``True`` to use a cached counts table, if available. See
|
|
313
306
|
:ref:`python_api_cached_table_counts`
|
|
307
|
+
:param use_old_upsert: set to ``True`` to force the older upsert implementation. See
|
|
308
|
+
:ref:`python_api_old_upsert`
|
|
314
309
|
:param strict: Apply STRICT mode to all created tables (unless overridden)
|
|
315
310
|
"""
|
|
316
311
|
|
|
@@ -327,10 +322,12 @@ class Database:
|
|
|
327
322
|
tracer: Optional[Callable] = None,
|
|
328
323
|
use_counts_table: bool = False,
|
|
329
324
|
execute_plugins: bool = True,
|
|
325
|
+
use_old_upsert: bool = False,
|
|
330
326
|
strict: bool = False,
|
|
331
327
|
):
|
|
332
328
|
self.memory_name = None
|
|
333
329
|
self.memory = False
|
|
330
|
+
self.use_old_upsert = use_old_upsert
|
|
334
331
|
assert (filename_or_conn is not None and (not memory and not memory_name)) or (
|
|
335
332
|
filename_or_conn is None and (memory or memory_name)
|
|
336
333
|
), "Either specify a filename_or_conn or pass memory=True"
|
|
@@ -678,16 +675,46 @@ class Database:
|
|
|
678
675
|
@property
|
|
679
676
|
def supports_strict(self) -> bool:
|
|
680
677
|
"Does this database support STRICT mode?"
|
|
681
|
-
|
|
678
|
+
if not hasattr(self, "_supports_strict"):
|
|
679
|
+
try:
|
|
680
|
+
table_name = "t{}".format(secrets.token_hex(16))
|
|
681
|
+
with self.conn:
|
|
682
|
+
self.conn.execute(
|
|
683
|
+
"create table {} (name text) strict".format(table_name)
|
|
684
|
+
)
|
|
685
|
+
self.conn.execute("drop table {}".format(table_name))
|
|
686
|
+
self._supports_strict = True
|
|
687
|
+
except Exception:
|
|
688
|
+
self._supports_strict = False
|
|
689
|
+
return self._supports_strict
|
|
690
|
+
|
|
691
|
+
@property
|
|
692
|
+
def supports_on_conflict(self) -> bool:
|
|
693
|
+
# SQLite's upsert is implemented as INSERT INTO ... ON CONFLICT DO ...
|
|
694
|
+
if not hasattr(self, "_supports_on_conflict"):
|
|
682
695
|
table_name = "t{}".format(secrets.token_hex(16))
|
|
683
|
-
|
|
684
|
-
self.conn
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
696
|
+
try:
|
|
697
|
+
with self.conn:
|
|
698
|
+
self.conn.execute(
|
|
699
|
+
"create table {} (id integer primary key, name text)".format(
|
|
700
|
+
table_name
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
self.conn.execute(
|
|
704
|
+
"insert into {} (id, name) values (1, 'one')".format(table_name)
|
|
705
|
+
)
|
|
706
|
+
self.conn.execute(
|
|
707
|
+
(
|
|
708
|
+
"insert into {} (id, name) values (1, 'two') "
|
|
709
|
+
"on conflict do update set name = 'two'"
|
|
710
|
+
).format(table_name)
|
|
711
|
+
)
|
|
712
|
+
self._supports_on_conflict = True
|
|
713
|
+
except Exception:
|
|
714
|
+
self._supports_on_conflict = False
|
|
715
|
+
finally:
|
|
716
|
+
self.conn.execute("drop table if exists {}".format(table_name))
|
|
717
|
+
return self._supports_on_conflict
|
|
691
718
|
|
|
692
719
|
@property
|
|
693
720
|
def sqlite_version(self) -> Tuple[int, ...]:
|
|
@@ -2973,13 +3000,19 @@ class Table(Queryable):
|
|
|
2973
3000
|
replace,
|
|
2974
3001
|
ignore,
|
|
2975
3002
|
):
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3003
|
+
"""
|
|
3004
|
+
Given a list ``chunk`` of records that should be written to *this* table,
|
|
3005
|
+
return a list of ``(sql, parameters)`` 2-tuples which, when executed in
|
|
3006
|
+
order, perform the desired INSERT / UPSERT / REPLACE operation.
|
|
3007
|
+
"""
|
|
2980
3008
|
if hash_id_columns and hash_id is None:
|
|
2981
3009
|
hash_id = "id"
|
|
3010
|
+
|
|
2982
3011
|
extracts = resolve_extracts(extracts)
|
|
3012
|
+
|
|
3013
|
+
# Build a row-list ready for executemany-style flattening
|
|
3014
|
+
values = []
|
|
3015
|
+
|
|
2983
3016
|
for record in chunk:
|
|
2984
3017
|
record_values = []
|
|
2985
3018
|
for key in all_columns:
|
|
@@ -2999,76 +3032,103 @@ class Table(Queryable):
|
|
|
2999
3032
|
record_values.append(value)
|
|
3000
3033
|
values.append(record_values)
|
|
3001
3034
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
[
|
|
3039
|
-
+ [record[pk] for pk in pks],
|
|
3035
|
+
columns_sql = ", ".join(f"[{c}]" for c in all_columns)
|
|
3036
|
+
placeholder_expr = ", ".join(conversions.get(c, "?") for c in all_columns)
|
|
3037
|
+
row_placeholders_sql = ", ".join(f"({placeholder_expr})" for _ in values)
|
|
3038
|
+
flat_params = list(itertools.chain.from_iterable(values))
|
|
3039
|
+
|
|
3040
|
+
# replace=True mean INSERT OR REPLACE INTO
|
|
3041
|
+
if replace:
|
|
3042
|
+
sql = (
|
|
3043
|
+
f"INSERT OR REPLACE INTO [{self.name}] "
|
|
3044
|
+
f"({columns_sql}) VALUES {row_placeholders_sql}"
|
|
3045
|
+
)
|
|
3046
|
+
return [(sql, flat_params)]
|
|
3047
|
+
|
|
3048
|
+
# If not an upsert it's an INSERT, maybe with OR IGNORE
|
|
3049
|
+
if not upsert:
|
|
3050
|
+
or_ignore = ""
|
|
3051
|
+
if ignore:
|
|
3052
|
+
or_ignore = " OR IGNORE"
|
|
3053
|
+
sql = (
|
|
3054
|
+
f"INSERT{or_ignore} INTO [{self.name}] "
|
|
3055
|
+
f"({columns_sql}) VALUES {row_placeholders_sql}"
|
|
3056
|
+
)
|
|
3057
|
+
return [(sql, flat_params)]
|
|
3058
|
+
|
|
3059
|
+
# Everything from here on is for upsert=True
|
|
3060
|
+
pk_cols = [pk] if isinstance(pk, str) else list(pk)
|
|
3061
|
+
non_pk_cols = [c for c in all_columns if c not in pk_cols]
|
|
3062
|
+
conflict_sql = ", ".join(f"[{c}]" for c in pk_cols)
|
|
3063
|
+
|
|
3064
|
+
if self.db.supports_on_conflict and not self.db.use_old_upsert:
|
|
3065
|
+
if non_pk_cols:
|
|
3066
|
+
# DO UPDATE
|
|
3067
|
+
assignments = []
|
|
3068
|
+
for c in non_pk_cols:
|
|
3069
|
+
if c in conversions:
|
|
3070
|
+
assignments.append(
|
|
3071
|
+
f"[{c}] = {conversions[c].replace('?', f'excluded.[{c}]')}"
|
|
3040
3072
|
)
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3073
|
+
else:
|
|
3074
|
+
assignments.append(f"[{c}] = excluded.[{c}]")
|
|
3075
|
+
do_clause = "DO UPDATE SET " + ", ".join(assignments)
|
|
3076
|
+
else:
|
|
3077
|
+
# All columns are in the PK – nothing to update.
|
|
3078
|
+
do_clause = "DO NOTHING"
|
|
3047
3079
|
|
|
3080
|
+
sql = (
|
|
3081
|
+
f"INSERT INTO [{self.name}] ({columns_sql}) "
|
|
3082
|
+
f"VALUES {row_placeholders_sql} "
|
|
3083
|
+
f"ON CONFLICT({conflict_sql}) {do_clause}"
|
|
3084
|
+
)
|
|
3085
|
+
return [(sql, flat_params)]
|
|
3086
|
+
|
|
3087
|
+
# At this point we need compatibility UPSERT for SQLite < 3.24.0
|
|
3088
|
+
# (INSERT OR IGNORE + second UPDATE stage)
|
|
3089
|
+
queries_and_params = []
|
|
3090
|
+
if isinstance(pk, str):
|
|
3091
|
+
pks = [pk]
|
|
3048
3092
|
else:
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3093
|
+
pks = pk
|
|
3094
|
+
self.last_pk = None
|
|
3095
|
+
for record_values in values:
|
|
3096
|
+
record = dict(zip(all_columns, record_values))
|
|
3097
|
+
placeholders = list(pks)
|
|
3098
|
+
# Need to populate not-null columns too, or INSERT OR IGNORE ignores
|
|
3099
|
+
# them since it ignores the resulting integrity errors
|
|
3100
|
+
if not_null:
|
|
3101
|
+
placeholders.extend(not_null)
|
|
3102
|
+
sql = "INSERT OR IGNORE INTO [{table}]({cols}) VALUES({placeholders});".format(
|
|
3058
3103
|
table=self.name,
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
"({placeholders})".format(
|
|
3062
|
-
placeholders=", ".join(
|
|
3063
|
-
[conversions.get(col, "?") for col in all_columns]
|
|
3064
|
-
)
|
|
3065
|
-
)
|
|
3066
|
-
for record in chunk
|
|
3067
|
-
),
|
|
3104
|
+
cols=", ".join(["[{}]".format(p) for p in placeholders]),
|
|
3105
|
+
placeholders=", ".join(["?" for p in placeholders]),
|
|
3068
3106
|
)
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3107
|
+
queries_and_params.append(
|
|
3108
|
+
(sql, [record[col] for col in pks] + ["" for _ in (not_null or [])])
|
|
3109
|
+
)
|
|
3110
|
+
# UPDATE [book] SET [name] = 'Programming' WHERE [id] = 1001;
|
|
3111
|
+
set_cols = [col for col in all_columns if col not in pks]
|
|
3112
|
+
if set_cols:
|
|
3113
|
+
sql2 = "UPDATE [{table}] SET {pairs} WHERE {wheres}".format(
|
|
3114
|
+
table=self.name,
|
|
3115
|
+
pairs=", ".join(
|
|
3116
|
+
"[{}] = {}".format(col, conversions.get(col, "?"))
|
|
3117
|
+
for col in set_cols
|
|
3118
|
+
),
|
|
3119
|
+
wheres=" AND ".join("[{}] = ?".format(pk) for pk in pks),
|
|
3120
|
+
)
|
|
3121
|
+
queries_and_params.append(
|
|
3122
|
+
(
|
|
3123
|
+
sql2,
|
|
3124
|
+
[record[col] for col in set_cols] + [record[pk] for pk in pks],
|
|
3125
|
+
)
|
|
3126
|
+
)
|
|
3127
|
+
# We can populate .last_pk right here
|
|
3128
|
+
if num_records_processed == 1:
|
|
3129
|
+
self.last_pk = tuple(record[pk] for pk in pks)
|
|
3130
|
+
if len(self.last_pk) == 1:
|
|
3131
|
+
self.last_pk = self.last_pk[0]
|
|
3072
3132
|
return queries_and_params
|
|
3073
3133
|
|
|
3074
3134
|
def insert_chunk(
|
|
@@ -3086,7 +3146,7 @@ class Table(Queryable):
|
|
|
3086
3146
|
num_records_processed,
|
|
3087
3147
|
replace,
|
|
3088
3148
|
ignore,
|
|
3089
|
-
):
|
|
3149
|
+
) -> Optional[sqlite3.Cursor]:
|
|
3090
3150
|
queries_and_params = self.build_insert_queries_and_params(
|
|
3091
3151
|
extracts,
|
|
3092
3152
|
chunk,
|
|
@@ -3101,9 +3161,8 @@ class Table(Queryable):
|
|
|
3101
3161
|
replace,
|
|
3102
3162
|
ignore,
|
|
3103
3163
|
)
|
|
3104
|
-
|
|
3164
|
+
result = None
|
|
3105
3165
|
with self.db.conn:
|
|
3106
|
-
result = None
|
|
3107
3166
|
for query, params in queries_and_params:
|
|
3108
3167
|
try:
|
|
3109
3168
|
result = self.db.execute(query, params)
|
|
@@ -3132,7 +3191,7 @@ class Table(Queryable):
|
|
|
3132
3191
|
ignore,
|
|
3133
3192
|
)
|
|
3134
3193
|
|
|
3135
|
-
self.insert_chunk(
|
|
3194
|
+
result = self.insert_chunk(
|
|
3136
3195
|
alter,
|
|
3137
3196
|
extracts,
|
|
3138
3197
|
second_half,
|
|
@@ -3150,20 +3209,7 @@ class Table(Queryable):
|
|
|
3150
3209
|
|
|
3151
3210
|
else:
|
|
3152
3211
|
raise
|
|
3153
|
-
|
|
3154
|
-
self.last_rowid = result.lastrowid
|
|
3155
|
-
self.last_pk = self.last_rowid
|
|
3156
|
-
# self.last_rowid will be 0 if a "INSERT OR IGNORE" happened
|
|
3157
|
-
if (hash_id or pk) and self.last_rowid:
|
|
3158
|
-
row = list(self.rows_where("rowid = ?", [self.last_rowid]))[0]
|
|
3159
|
-
if hash_id:
|
|
3160
|
-
self.last_pk = row[hash_id]
|
|
3161
|
-
elif isinstance(pk, str):
|
|
3162
|
-
self.last_pk = row[pk]
|
|
3163
|
-
else:
|
|
3164
|
-
self.last_pk = tuple(row[p] for p in pk)
|
|
3165
|
-
|
|
3166
|
-
return
|
|
3212
|
+
return result
|
|
3167
3213
|
|
|
3168
3214
|
def insert(
|
|
3169
3215
|
self,
|
|
@@ -3203,7 +3249,7 @@ class Table(Queryable):
|
|
|
3203
3249
|
:param not_null: Set of strings specifying columns that should be ``NOT NULL``.
|
|
3204
3250
|
:param defaults: Dictionary specifying default values for specific columns.
|
|
3205
3251
|
:param hash_id: Name of a column to create and use as a primary key, where the
|
|
3206
|
-
value of
|
|
3252
|
+
value of that primary key will be derived as a SHA1 hash of the other column values
|
|
3207
3253
|
in the record. ``hash_id="id"`` is a common column name used for this.
|
|
3208
3254
|
:param alter: Boolean, should any missing columns be added automatically?
|
|
3209
3255
|
:param ignore: Boolean, if a record already exists with this primary key, ignore this insert.
|
|
@@ -3283,6 +3329,7 @@ class Table(Queryable):
|
|
|
3283
3329
|
|
|
3284
3330
|
if upsert and (not pk and not hash_id):
|
|
3285
3331
|
raise PrimaryKeyRequired("upsert() requires a pk")
|
|
3332
|
+
|
|
3286
3333
|
assert not (hash_id and pk), "Use either pk= or hash_id="
|
|
3287
3334
|
if hash_id_columns and (hash_id is None):
|
|
3288
3335
|
hash_id = "id"
|
|
@@ -3314,6 +3361,7 @@ class Table(Queryable):
|
|
|
3314
3361
|
self.last_pk = None
|
|
3315
3362
|
if truncate and self.exists():
|
|
3316
3363
|
self.db.execute("DELETE FROM [{}];".format(self.name))
|
|
3364
|
+
result = None
|
|
3317
3365
|
for chunk in chunks(itertools.chain([first_record], records), batch_size):
|
|
3318
3366
|
chunk = list(chunk)
|
|
3319
3367
|
num_records_processed += len(chunk)
|
|
@@ -3321,6 +3369,12 @@ class Table(Queryable):
|
|
|
3321
3369
|
if not self.exists():
|
|
3322
3370
|
# Use the first batch to derive the table names
|
|
3323
3371
|
column_types = suggest_column_types(chunk)
|
|
3372
|
+
if extracts:
|
|
3373
|
+
for col in extracts:
|
|
3374
|
+
if col in column_types:
|
|
3375
|
+
column_types[col] = (
|
|
3376
|
+
int # This will be an integer foreign key
|
|
3377
|
+
)
|
|
3324
3378
|
column_types.update(columns or {})
|
|
3325
3379
|
self.create(
|
|
3326
3380
|
column_types,
|
|
@@ -3348,7 +3402,7 @@ class Table(Queryable):
|
|
|
3348
3402
|
|
|
3349
3403
|
first = False
|
|
3350
3404
|
|
|
3351
|
-
self.insert_chunk(
|
|
3405
|
+
result = self.insert_chunk(
|
|
3352
3406
|
alter,
|
|
3353
3407
|
extracts,
|
|
3354
3408
|
chunk,
|
|
@@ -3364,6 +3418,33 @@ class Table(Queryable):
|
|
|
3364
3418
|
ignore,
|
|
3365
3419
|
)
|
|
3366
3420
|
|
|
3421
|
+
# If we only handled a single row populate self.last_pk
|
|
3422
|
+
if num_records_processed == 1:
|
|
3423
|
+
# For an insert we need to use result.lastrowid
|
|
3424
|
+
if not upsert and result is not None:
|
|
3425
|
+
self.last_rowid = result.lastrowid
|
|
3426
|
+
if (hash_id or pk) and self.last_rowid:
|
|
3427
|
+
# Set self.last_pk to the pk(s) for that rowid
|
|
3428
|
+
row = list(self.rows_where("rowid = ?", [self.last_rowid]))[0]
|
|
3429
|
+
if hash_id:
|
|
3430
|
+
self.last_pk = row[hash_id]
|
|
3431
|
+
elif isinstance(pk, str):
|
|
3432
|
+
self.last_pk = row[pk]
|
|
3433
|
+
else:
|
|
3434
|
+
self.last_pk = tuple(row[p] for p in pk)
|
|
3435
|
+
else:
|
|
3436
|
+
self.last_pk = self.last_rowid
|
|
3437
|
+
else:
|
|
3438
|
+
# For an upsert use first_record from earlier
|
|
3439
|
+
if hash_id:
|
|
3440
|
+
self.last_pk = hash_record(first_record, hash_id_columns)
|
|
3441
|
+
else:
|
|
3442
|
+
self.last_pk = (
|
|
3443
|
+
first_record[pk]
|
|
3444
|
+
if isinstance(pk, str)
|
|
3445
|
+
else tuple(first_record[p] for p in pk)
|
|
3446
|
+
)
|
|
3447
|
+
|
|
3367
3448
|
if analyze:
|
|
3368
3449
|
self.analyze()
|
|
3369
3450
|
|
|
@@ -3852,7 +3933,7 @@ def jsonify_if_needed(value):
|
|
|
3852
3933
|
|
|
3853
3934
|
|
|
3854
3935
|
def resolve_extracts(
|
|
3855
|
-
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]]
|
|
3936
|
+
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]],
|
|
3856
3937
|
) -> dict:
|
|
3857
3938
|
if extracts is None:
|
|
3858
3939
|
extracts = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite-utils
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0a0
|
|
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,12 @@ 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.8
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: sqlite-fts4
|
|
@@ -52,8 +51,18 @@ Requires-Dist: types-pluggy; extra == "mypy"
|
|
|
52
51
|
Requires-Dist: data-science-types; extra == "mypy"
|
|
53
52
|
Provides-Extra: flake8
|
|
54
53
|
Requires-Dist: flake8; extra == "flake8"
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Dynamic: author
|
|
55
|
+
Dynamic: classifier
|
|
56
|
+
Dynamic: description
|
|
57
|
+
Dynamic: description-content-type
|
|
58
|
+
Dynamic: home-page
|
|
59
|
+
Dynamic: license
|
|
60
|
+
Dynamic: license-file
|
|
61
|
+
Dynamic: project-url
|
|
62
|
+
Dynamic: provides-extra
|
|
63
|
+
Dynamic: requires-dist
|
|
64
|
+
Dynamic: requires-python
|
|
65
|
+
Dynamic: summary
|
|
57
66
|
|
|
58
67
|
# sqlite-utils
|
|
59
68
|
|
|
@@ -916,7 +916,7 @@ def test_query_json_with_json_cols(db_path):
|
|
|
916
916
|
|
|
917
917
|
@pytest.mark.parametrize(
|
|
918
918
|
"content,is_binary",
|
|
919
|
-
[(b"\x00\
|
|
919
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
920
920
|
)
|
|
921
921
|
def test_query_raw(db_path, content, is_binary):
|
|
922
922
|
Database(db_path)["files"].insert({"content": content})
|
|
@@ -931,7 +931,7 @@ def test_query_raw(db_path, content, is_binary):
|
|
|
931
931
|
|
|
932
932
|
@pytest.mark.parametrize(
|
|
933
933
|
"content,is_binary",
|
|
934
|
-
[(b"\x00\
|
|
934
|
+
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
|
935
935
|
)
|
|
936
936
|
def test_query_raw_lines(db_path, content, is_binary):
|
|
937
937
|
Database(db_path)["files"].insert_all({"content": content} for _ in range(3))
|
|
@@ -1116,11 +1116,8 @@ def test_upsert_alter(db_path, tmpdir):
|
|
|
1116
1116
|
cli.cli, ["upsert", db_path, "dogs", json_path, "--pk", "id"]
|
|
1117
1117
|
)
|
|
1118
1118
|
assert result.exit_code == 1
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
"sql = UPDATE [dogs] SET [age] = ? WHERE [id] = ?\n"
|
|
1122
|
-
"parameters = [5, 1]"
|
|
1123
|
-
) == result.output.strip()
|
|
1119
|
+
# Could be one of two errors depending on SQLite version
|
|
1120
|
+
assert ("Try using --alter to add additional columns") in result.output.strip()
|
|
1124
1121
|
# Should succeed with --alter
|
|
1125
1122
|
result = CliRunner().invoke(
|
|
1126
1123
|
cli.cli, ["upsert", db_path, "dogs", json_path, "--pk", "id", "--alter"]
|
|
@@ -2248,7 +2245,7 @@ def test_integer_overflow_error(tmpdir):
|
|
|
2248
2245
|
assert result.exit_code == 1
|
|
2249
2246
|
assert result.output == (
|
|
2250
2247
|
"Error: Python int too large to convert to SQLite INTEGER\n\n"
|
|
2251
|
-
"sql = INSERT INTO [items] ([bignumber]) VALUES (?)
|
|
2248
|
+
"sql = INSERT INTO [items] ([bignumber]) VALUES (?)\n"
|
|
2252
2249
|
"parameters = [34223049823094832094802398430298048240]\n"
|
|
2253
2250
|
)
|
|
2254
2251
|
|
|
@@ -173,14 +173,16 @@ def test_create_table_from_example_with_compound_primary_keys(fresh_db):
|
|
|
173
173
|
@pytest.mark.parametrize(
|
|
174
174
|
"method_name", ("insert", "upsert", "insert_all", "upsert_all")
|
|
175
175
|
)
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
|
177
|
+
def test_create_table_with_custom_columns(method_name, use_old_upsert):
|
|
178
|
+
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
|
179
|
+
table = db["dogs"]
|
|
178
180
|
method = getattr(table, method_name)
|
|
179
181
|
record = {"id": 1, "name": "Cleo", "age": "5"}
|
|
180
182
|
if method_name.endswith("_all"):
|
|
181
183
|
record = [record]
|
|
182
184
|
method(record, pk="id", columns={"age": int, "weight": float})
|
|
183
|
-
assert ["dogs"] ==
|
|
185
|
+
assert ["dogs"] == db.table_names()
|
|
184
186
|
expected_columns = [
|
|
185
187
|
{"name": "id", "type": "INTEGER"},
|
|
186
188
|
{"name": "name", "type": "TEXT"},
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
from click.testing import CliRunner
|
|
2
2
|
import click
|
|
3
3
|
import importlib
|
|
4
|
+
import pytest
|
|
4
5
|
from sqlite_utils import cli, Database, hookimpl, plugins
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
def _supports_pragma_function_list():
|
|
9
|
+
db = Database(memory=True)
|
|
10
|
+
try:
|
|
11
|
+
db.execute("select * from pragma_function_list()")
|
|
12
|
+
except Exception:
|
|
13
|
+
return False
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
|
|
7
17
|
def test_register_commands():
|
|
8
18
|
importlib.reload(cli)
|
|
9
19
|
assert plugins.get_plugins() == []
|
|
@@ -37,6 +47,10 @@ def test_register_commands():
|
|
|
37
47
|
assert plugins.get_plugins() == []
|
|
38
48
|
|
|
39
49
|
|
|
50
|
+
@pytest.mark.skipif(
|
|
51
|
+
not _supports_pragma_function_list(),
|
|
52
|
+
reason="Needs SQLite version that supports pragma_function_list()",
|
|
53
|
+
)
|
|
40
54
|
def test_prepare_connection():
|
|
41
55
|
importlib.reload(cli)
|
|
42
56
|
assert plugins.get_plugins() == []
|
|
@@ -54,7 +68,7 @@ def test_prepare_connection():
|
|
|
54
68
|
return [
|
|
55
69
|
row[0]
|
|
56
70
|
for row in db.execute(
|
|
57
|
-
"select distinct name from pragma_function_list order by 1"
|
|
71
|
+
"select distinct name from pragma_function_list() order by 1"
|
|
58
72
|
).fetchall()
|
|
59
73
|
]
|
|
60
74
|
|
|
@@ -18,7 +18,7 @@ def test_tracer():
|
|
|
18
18
|
("select name from sqlite_master where type = 'view'", None),
|
|
19
19
|
("CREATE TABLE [dogs] (\n [name] TEXT\n);\n ", None),
|
|
20
20
|
("select name from sqlite_master where type = 'view'", None),
|
|
21
|
-
("INSERT INTO [dogs] ([name]) VALUES (?)
|
|
21
|
+
("INSERT INTO [dogs] ([name]) VALUES (?)", ["Cleopaws"]),
|
|
22
22
|
("select name from sqlite_master where type = 'view'", None),
|
|
23
23
|
(
|
|
24
24
|
"CREATE VIRTUAL TABLE [dogs_fts] USING FTS5 (\n [name],\n content=[dogs]\n)",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from sqlite_utils.db import PrimaryKeyRequired
|
|
2
|
+
from sqlite_utils import Database
|
|
2
3
|
import pytest
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
|
7
|
+
def test_upsert(use_old_upsert):
|
|
8
|
+
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
|
9
|
+
table = db["table"]
|
|
7
10
|
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
|
8
11
|
table.upsert({"id": 1, "age": 5}, pk="id", alter=True)
|
|
9
12
|
assert list(table.rows) == [{"id": 1, "name": "Cleo", "age": 5}]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|