mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-26 19:04:32 +02:00
Compare commits
31 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a456830ca |
||
|
|
a7b734946f | ||
|
|
c621499ed1 | ||
|
|
69a1c0d960 |
||
|
|
a947dc6739 | ||
|
|
458b3ab5b1 |
||
|
|
f66ddcb215 |
||
|
|
d714200659 |
||
|
|
3f0471701b |
||
|
|
5e8822efd2 |
||
|
|
57c1617391 | ||
|
|
b74b727035 |
||
|
|
dc61f75a0b | ||
|
|
6531a57863 | ||
|
|
0f2d525d06 | ||
|
|
7a52214624 | ||
|
|
d302835d57 | ||
|
|
d2ac3765ed | ||
|
|
092f0919c3 | ||
|
|
569608e40f | ||
|
|
23a21c1d6b | ||
|
|
ebafb84c93 | ||
|
|
aa300942bf | ||
|
|
cf3373e7b7 | ||
|
|
8ee0b7c65c | ||
|
|
fa5d66bf53 | ||
|
|
619770bf42 | ||
|
|
353baf280d | ||
|
|
8bc9213a8e | ||
|
|
60811e7305 |
||
|
|
d314d04215 |
65 changed files with 2544 additions and 1178 deletions
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
|
|
@ -12,9 +12,9 @@ jobs:
|
||||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
@ -29,9 +29,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [test]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.14'
|
python-version: '3.14'
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
|
||||||
4
.github/workflows/test-coverage.yml
vendored
4
.github/workflows/test-coverage.yml
vendored
|
|
@ -12,9 +12,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
|
||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -14,9 +14,9 @@ jobs:
|
||||||
numpy: [0, 1]
|
numpy: [0, 1]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
|
os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
allow-prereleases: true
|
allow-prereleases: true
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,6 +15,7 @@ venv
|
||||||
.schema
|
.schema
|
||||||
.vscode
|
.vscode
|
||||||
.hypothesis
|
.hypothesis
|
||||||
|
.claude/
|
||||||
Pipfile
|
Pipfile
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
|
||||||
1
Justfile
1
Justfile
|
|
@ -16,6 +16,7 @@
|
||||||
uv run ty check sqlite_utils
|
uv run ty check sqlite_utils
|
||||||
uv run cog --check README.md docs/*.rst
|
uv run cog --check README.md docs/*.rst
|
||||||
uv run --group docs codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt
|
uv run --group docs codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt
|
||||||
|
uv run --group docs codespell sqlite_utils --ignore-words docs/codespell-ignore-words.txt
|
||||||
|
|
||||||
# Rebuild docs with cog
|
# Rebuild docs with cog
|
||||||
@cog:
|
@cog:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,64 @@
|
||||||
Changelog
|
Changelog
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
.. _v3_39_1:
|
||||||
|
|
||||||
|
3.39.1 (2026-07-25)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Fixed a bug where ``table.delete_where()`` left the connection in an open transaction, causing deleted rows to be silently restored when the connection was closed. (:issue:`815`)
|
||||||
|
|
||||||
|
.. _v4_1_1:
|
||||||
|
|
||||||
|
4.1.1 (2026-07-12)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- ``table.transform()`` now raises a ``TransactionError`` if called while a transaction is open with ``PRAGMA foreign_keys`` enabled and the table is referenced by foreign keys with destructive ``ON DELETE`` actions - ``CASCADE``, ``SET NULL`` or ``SET DEFAULT``. The pragma cannot be changed inside a transaction, so previously dropping the old table as part of the transform could fire those actions and silently delete or modify referencing rows. See :ref:`python_api_transform_foreign_keys_transactions` for details and workarounds. (:issue:`794`)
|
||||||
|
- The :ref:`CLI <cli>` and :ref:`Python API <python_api>` documentation now cross-reference each other: CLI sections link to the equivalent Python API functionality and Python API sections link back to the corresponding CLI command. (:issue:`791`)
|
||||||
|
.. _v4_1:
|
||||||
|
|
||||||
|
4.1 (2026-07-11)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- ``sqlite-utils insert`` and ``sqlite-utils upsert`` now accept a ``--code`` option for :ref:`providing a block of Python code <cli_insert_code>` (or a path to a ``.py`` file) that defines a ``rows()`` function or ``rows`` iterable of rows to insert, as an alternative to importing from a file. (:issue:`684`)
|
||||||
|
- ``sqlite-utils insert`` and ``sqlite-utils upsert`` now accept ``--type column-name type`` to :ref:`override the type automatically chosen when the table is created <cli_insert_csv_tsv_column_types>`. This is useful for CSV or TSV columns such as ZIP codes that look like integers but should be stored as ``TEXT`` to preserve leading zeros. (:issue:`131`)
|
||||||
|
- New ``table.drop_index(name)`` method and ``sqlite-utils drop-index`` command for dropping an index by name. Both accept ``ignore=True``/``--ignore`` to ignore a missing index. (:issue:`626`)
|
||||||
|
- ``sqlite-utils query`` can now read the SQL query from standard input by passing ``-`` in place of the query, for example ``echo "select * from dogs" | sqlite-utils query dogs.db -``. (:issue:`765`)
|
||||||
|
- ``sqlite-utils upsert`` can now infer the primary key of an existing table, so ``--pk`` can be omitted when upserting into a table that already has a primary key.
|
||||||
|
- ``table.transform()`` and ``table.transform_sql()`` now accept ``strict=True`` or ``strict=False`` to change a table's `SQLite strict mode <https://www.sqlite.org/stricttables.html>`__. Omitting the option preserves the existing mode. (:issue:`787`)
|
||||||
|
- The ``sqlite-utils transform`` command now accepts ``--strict`` and ``--no-strict`` to change a table's strict mode. (:issue:`787`)
|
||||||
|
|
||||||
|
.. _v4_0:
|
||||||
|
|
||||||
|
4.0 (2026-07-07)
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The 4.0 release includes some minor backwards-incompatible fixes (hence the major version number bump) and introduces three major new features:
|
||||||
|
|
||||||
|
- :ref:`Database migrations <migrations>`, providing a structured mechanism for evolving a project's schema over time. (:issue:`752`)
|
||||||
|
- :ref:`Nested transaction support <python_api_atomic>` via ``db.atomic()``, plus numerous improvements to how transactions work across the library. (:issue:`755`)
|
||||||
|
- Support for :ref:`compound foreign keys <python_api_compound_foreign_keys>`, including creation, transformation and introspection through :ref:`table.foreign_keys <python_api_introspection_foreign_keys>`. (:issue:`594`)
|
||||||
|
|
||||||
|
Other notable changes include:
|
||||||
|
|
||||||
|
- Upserts now use SQLite's ``INSERT ... ON CONFLICT ... DO UPDATE SET`` syntax, detect existing table primary keys automatically and reject records that are missing required primary key values. (:issue:`652`)
|
||||||
|
- ``db.query()`` now executes immediately and rejects statements that do not return rows; use ``db.execute()`` for writes and DDL.
|
||||||
|
- CSV and TSV imports now detect column types by default, while inserts into existing tables preserve those tables' column types. (:issue:`679`)
|
||||||
|
- Foreign key handling now preserves ``ON DELETE``/``ON UPDATE`` actions during transforms and resolves referenced primary keys more accurately. (:issue:`530`)
|
||||||
|
- Column names passed to Python API methods are now matched case-insensitively, mirroring SQLite's own identifier behavior. (:issue:`760`)
|
||||||
|
- The command-line tool now emits UTF-8 JSON output by default, with ``--ascii`` available to restore escaped output. (:issue:`625`)
|
||||||
|
- ``table.extract()`` and ``extracts=`` no longer create lookup table records for all-``null`` values. (:issue:`186`)
|
||||||
|
|
||||||
|
See :ref:`upgrading_3_to_4` for details on backwards-incompatible changes.
|
||||||
|
|
||||||
|
The detailed release notes for the features and fixes shipped during the 4.0 pre-release cycle are available in :ref:`4.0a0 <v4_0a0>`, :ref:`4.0a1 <v4_0a1>`, :ref:`4.0rc1 <v4_0rc1>`, :ref:`4.0rc2 <v4_0rc2>`, :ref:`4.0rc3 <v4_0rc3>` and :ref:`4.0rc4 <v4_0rc4>`.
|
||||||
|
|
||||||
|
Bug fixes since 4.0rc4
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
- Fixed 4.0 regressions in ``insert``/``upsert`` against tables that use SQLite's implicit ``rowid`` primary key. Passing ``pk="rowid"``, ``pk="_rowid_"`` or ``pk="oid"`` now works again for rowid tables, and ``last_pk`` is set correctly. (:issue:`781`)
|
||||||
|
- Fixed ``insert(..., ignore=True)`` and ``insert_all(..., ignore=True)`` so an ignored insert that conflicts with an existing primary key row now reports that existing row in ``last_rowid`` and ``last_pk`` where possible. This also works for compound primary keys and list-mode inserts. (:issue:`783`)
|
||||||
|
|
||||||
.. _v4_0rc4:
|
.. _v4_0rc4:
|
||||||
|
|
||||||
4.0rc4 (2026-07-06)
|
4.0rc4 (2026-07-06)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.
|
||||||
go_first = [
|
go_first = [
|
||||||
"query", "memory", "insert", "upsert", "bulk", "search", "transform", "extract",
|
"query", "memory", "insert", "upsert", "bulk", "search", "transform", "extract",
|
||||||
"schema", "insert-files", "analyze-tables", "convert", "tables", "views", "rows",
|
"schema", "insert-files", "analyze-tables", "convert", "tables", "views", "rows",
|
||||||
"triggers", "indexes", "create-database", "create-table", "create-index",
|
"triggers", "indexes", "create-database", "create-table", "create-index", "drop-index",
|
||||||
"migrate", "enable-fts", "populate-fts", "rebuild-fts", "disable-fts"
|
"migrate", "enable-fts", "populate-fts", "rebuild-fts", "disable-fts"
|
||||||
]
|
]
|
||||||
refs = {
|
refs = {
|
||||||
|
|
@ -46,6 +46,7 @@ This page lists the ``--help`` for every ``sqlite-utils`` CLI sub-command.
|
||||||
"add-foreign-keys": "cli_add_foreign_keys",
|
"add-foreign-keys": "cli_add_foreign_keys",
|
||||||
"index-foreign-keys": "cli_index_foreign_keys",
|
"index-foreign-keys": "cli_index_foreign_keys",
|
||||||
"create-index": "cli_create_index",
|
"create-index": "cli_create_index",
|
||||||
|
"drop-index": "cli_drop_index",
|
||||||
"enable-wal": "cli_wal",
|
"enable-wal": "cli_wal",
|
||||||
"enable-counts": "cli_enable_counts",
|
"enable-counts": "cli_enable_counts",
|
||||||
"bulk": "cli_bulk",
|
"bulk": "cli_bulk",
|
||||||
|
|
@ -109,6 +110,10 @@ See :ref:`cli_query`.
|
||||||
"select * from chickens where age > :age" \
|
"select * from chickens where age > :age" \
|
||||||
-p age 1
|
-p age 1
|
||||||
|
|
||||||
|
Pass "-" as the SQL to read the query from standard input:
|
||||||
|
|
||||||
|
echo "select * from chickens" | sqlite-utils data.db -
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
||||||
filepath
|
filepath
|
||||||
|
|
@ -134,8 +139,8 @@ See :ref:`cli_query`.
|
||||||
-r, --raw Raw output, first column of first row
|
-r, --raw Raw output, first column of first row
|
||||||
--raw-lines Raw output, first column of each row
|
--raw-lines Raw output, first column of each row
|
||||||
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
||||||
--functions TEXT Python code or file path defining custom SQL
|
--functions TEXT Python code or a file path defining custom SQL
|
||||||
functions
|
functions; can be used multiple times
|
||||||
--load-extension TEXT Path to SQLite extension, with optional
|
--load-extension TEXT Path to SQLite extension, with optional
|
||||||
:entrypoint
|
:entrypoint
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
@ -177,8 +182,8 @@ See :ref:`cli_memory`.
|
||||||
sqlite-utils memory animals.csv --schema
|
sqlite-utils memory animals.csv --schema
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--functions TEXT Python code or file path defining custom SQL
|
--functions TEXT Python code or a file path defining custom SQL
|
||||||
functions
|
functions; can be used multiple times
|
||||||
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
||||||
filepath
|
filepath
|
||||||
--flatten Flatten nested JSON objects, so {"foo": {"bar":
|
--flatten Flatten nested JSON objects, so {"foo": {"bar":
|
||||||
|
|
@ -226,7 +231,7 @@ See :ref:`cli_inserting_data`, :ref:`cli_insert_csv_tsv`, :ref:`cli_insert_unstr
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Usage: sqlite-utils insert [OPTIONS] PATH TABLE FILE
|
Usage: sqlite-utils insert [OPTIONS] PATH TABLE [FILE]
|
||||||
|
|
||||||
Insert records from FILE into a table, creating the table if it does not
|
Insert records from FILE into a table, creating the table if it does not
|
||||||
already exist.
|
already exist.
|
||||||
|
|
@ -242,6 +247,9 @@ See :ref:`cli_inserting_data`, :ref:`cli_insert_csv_tsv`, :ref:`cli_insert_unstr
|
||||||
- Use --lines to write each incoming line to a column called "line"
|
- Use --lines to write each incoming line to a column called "line"
|
||||||
- Use --text to write the entire input to a column called "text"
|
- Use --text to write the entire input to a column called "text"
|
||||||
|
|
||||||
|
Use --type column-name type to override the type automatically chosen when the
|
||||||
|
table is created.
|
||||||
|
|
||||||
You can also use --convert to pass a fragment of Python code that will be used
|
You can also use --convert to pass a fragment of Python code that will be used
|
||||||
to convert each input.
|
to convert each input.
|
||||||
|
|
||||||
|
|
@ -268,8 +276,20 @@ See :ref:`cli_inserting_data`, :ref:`cli_insert_csv_tsv`, :ref:`cli_insert_unstr
|
||||||
echo 'A bunch of words' | sqlite-utils insert words.db words - \
|
echo 'A bunch of words' | sqlite-utils insert words.db words - \
|
||||||
--text --convert '({"word": w} for w in text.split())'
|
--text --convert '({"word": w} for w in text.split())'
|
||||||
|
|
||||||
|
Instead of a FILE you can use --code to provide a block of Python code that
|
||||||
|
defines the rows to insert, as either a rows() function that yields
|
||||||
|
dictionaries or a "rows" iterable. --code can also be a path to a .py file:
|
||||||
|
|
||||||
|
sqlite-utils insert data.db creatures --code '
|
||||||
|
def rows():
|
||||||
|
yield {"id": 1, "name": "Cleo"}
|
||||||
|
yield {"id": 2, "name": "Suna"}
|
||||||
|
' --pk id
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--pk TEXT Columns to use as the primary key, e.g. id
|
--pk TEXT Columns to use as the primary key, e.g. id
|
||||||
|
--code TEXT Python code defining a rows() function or iterable
|
||||||
|
of rows to insert
|
||||||
--flatten Flatten nested JSON objects, so {"a": {"b": 1}}
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}}
|
||||||
becomes {"a_b": 1}
|
becomes {"a_b": 1}
|
||||||
--nl Expect newline-delimited JSON
|
--nl Expect newline-delimited JSON
|
||||||
|
|
@ -290,6 +310,7 @@ See :ref:`cli_inserting_data`, :ref:`cli_insert_csv_tsv`, :ref:`cli_insert_unstr
|
||||||
--alter Alter existing table to add any missing columns
|
--alter Alter existing table to add any missing columns
|
||||||
--not-null TEXT Columns that should be created as NOT NULL
|
--not-null TEXT Columns that should be created as NOT NULL
|
||||||
--default <TEXT TEXT>... Default value that should be set for a column
|
--default <TEXT TEXT>... Default value that should be set for a column
|
||||||
|
--type <TEXT CHOICE>... Column types to use when creating the table
|
||||||
--no-detect-types Treat all CSV/TSV columns as TEXT
|
--no-detect-types Treat all CSV/TSV columns as TEXT
|
||||||
--analyze Run ANALYZE at the end of this operation
|
--analyze Run ANALYZE at the end of this operation
|
||||||
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
||||||
|
|
@ -311,12 +332,17 @@ See :ref:`cli_upsert`.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Usage: sqlite-utils upsert [OPTIONS] PATH TABLE FILE
|
Usage: sqlite-utils upsert [OPTIONS] PATH TABLE [FILE]
|
||||||
|
|
||||||
Upsert records based on their primary key. Works like 'insert' but if an
|
Upsert records based on their primary key. Works like 'insert' but if an
|
||||||
incoming record has a primary key that matches an existing record the existing
|
incoming record has a primary key that matches an existing record the existing
|
||||||
record will be updated.
|
record will be updated.
|
||||||
|
|
||||||
|
If the table already exists and has a primary key, --pk can be omitted.
|
||||||
|
|
||||||
|
Use --type column-name type to override the type automatically chosen when the
|
||||||
|
table is created.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
echo '[
|
echo '[
|
||||||
|
|
@ -326,7 +352,8 @@ See :ref:`cli_upsert`.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--pk TEXT Columns to use as the primary key, e.g. id
|
--pk TEXT Columns to use as the primary key, e.g. id
|
||||||
[required]
|
--code TEXT Python code defining a rows() function or iterable
|
||||||
|
of rows to insert
|
||||||
--flatten Flatten nested JSON objects, so {"a": {"b": 1}}
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}}
|
||||||
becomes {"a_b": 1}
|
becomes {"a_b": 1}
|
||||||
--nl Expect newline-delimited JSON
|
--nl Expect newline-delimited JSON
|
||||||
|
|
@ -347,6 +374,7 @@ See :ref:`cli_upsert`.
|
||||||
--alter Alter existing table to add any missing columns
|
--alter Alter existing table to add any missing columns
|
||||||
--not-null TEXT Columns that should be created as NOT NULL
|
--not-null TEXT Columns that should be created as NOT NULL
|
||||||
--default <TEXT TEXT>... Default value that should be set for a column
|
--default <TEXT TEXT>... Default value that should be set for a column
|
||||||
|
--type <TEXT CHOICE>... Column types to use when creating the table
|
||||||
--no-detect-types Treat all CSV/TSV columns as TEXT
|
--no-detect-types Treat all CSV/TSV columns as TEXT
|
||||||
--analyze Run ANALYZE at the end of this operation
|
--analyze Run ANALYZE at the end of this operation
|
||||||
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
||||||
|
|
@ -379,7 +407,8 @@ See :ref:`cli_bulk`.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--batch-size INTEGER Commit every X records
|
--batch-size INTEGER Commit every X records
|
||||||
--functions TEXT Python code or file path defining custom SQL functions
|
--functions TEXT Python code or a file path defining custom SQL
|
||||||
|
functions; can be used multiple times
|
||||||
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
||||||
{"a_b": 1}
|
{"a_b": 1}
|
||||||
--nl Expect newline-delimited JSON
|
--nl Expect newline-delimited JSON
|
||||||
|
|
@ -479,6 +508,8 @@ See :ref:`cli_transform_table`.
|
||||||
Add a foreign key constraint from a column to
|
Add a foreign key constraint from a column to
|
||||||
another table with another column
|
another table with another column
|
||||||
--drop-foreign-key TEXT Drop foreign key constraint for this column
|
--drop-foreign-key TEXT Drop foreign key constraint for this column
|
||||||
|
--strict / --no-strict Enable or disable STRICT mode (default:
|
||||||
|
preserve current mode)
|
||||||
--sql Output SQL without executing it
|
--sql Output SQL without executing it
|
||||||
--load-extension TEXT Path to SQLite extension, with optional
|
--load-extension TEXT Path to SQLite extension, with optional
|
||||||
:entrypoint
|
:entrypoint
|
||||||
|
|
@ -616,6 +647,11 @@ See :ref:`cli_convert`.
|
||||||
|
|
||||||
"value" is a variable with the column value to be converted.
|
"value" is a variable with the column value to be converted.
|
||||||
|
|
||||||
|
CODE can also be a reference to a callable that takes the value, for example:
|
||||||
|
|
||||||
|
sqlite-utils convert my.db mytable date r.parsedate
|
||||||
|
sqlite-utils convert my.db mytable data json.loads --import json
|
||||||
|
|
||||||
Use "-" for CODE to read Python code from standard input.
|
Use "-" for CODE to read Python code from standard input.
|
||||||
|
|
||||||
The following common operations are available as recipe functions:
|
The following common operations are available as recipe functions:
|
||||||
|
|
@ -626,20 +662,18 @@ See :ref:`cli_convert`.
|
||||||
Convert a string like a,b,c into a JSON array ["a", "b", "c"]
|
Convert a string like a,b,c into a JSON array ["a", "b", "c"]
|
||||||
|
|
||||||
r.parsedate(value: 'str', dayfirst: 'bool' = False, yearfirst: 'bool' = False,
|
r.parsedate(value: 'str', dayfirst: 'bool' = False, yearfirst: 'bool' = False,
|
||||||
errors: 'Optional[object]' = None) -> 'Optional[str]'
|
errors: 'object | None' = None) -> 'str | None'
|
||||||
|
|
||||||
Parse a date and convert it to ISO date format: yyyy-mm-dd
|
Parse a date and convert it to ISO date format: yyyy-mm-dd
|
||||||
|
|
||||||
- dayfirst=True: treat xx as the day in xx/yy/zz
|
- dayfirst=True: treat xx as the day in xx/yy/zz
|
||||||
- yearfirst=True: treat xx as the year in xx/yy/zz
|
- yearfirst=True: treat xx as the year in xx/yy/zz
|
||||||
- errors=r.IGNORE to ignore values that cannot be parsed
|
- errors=r.IGNORE to ignore values that cannot be parsed
|
||||||
- errors=r.SET_NULL to set values that cannot be parsed to null
|
- errors=r.SET_NULL to set values that cannot be parsed to null
|
||||||
|
|
||||||
r.parsedatetime(value: 'str', dayfirst: 'bool' = False, yearfirst: 'bool' =
|
r.parsedatetime(value: 'str', dayfirst: 'bool' = False, yearfirst: 'bool' =
|
||||||
False, errors: 'Optional[object]' = None) -> 'Optional[str]'
|
False, errors: 'object | None' = None) -> 'str | None'
|
||||||
|
|
||||||
Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
|
Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
|
||||||
|
|
||||||
- dayfirst=True: treat xx as the day in xx/yy/zz
|
- dayfirst=True: treat xx as the day in xx/yy/zz
|
||||||
- yearfirst=True: treat xx as the year in xx/yy/zz
|
- yearfirst=True: treat xx as the year in xx/yy/zz
|
||||||
- errors=r.IGNORE to ignore values that cannot be parsed
|
- errors=r.IGNORE to ignore values that cannot be parsed
|
||||||
|
|
@ -926,10 +960,10 @@ See :ref:`cli_create_table`.
|
||||||
sqlite-utils create-table my.db people \
|
sqlite-utils create-table my.db people \
|
||||||
id integer \
|
id integer \
|
||||||
name text \
|
name text \
|
||||||
height float \
|
height real \
|
||||||
photo blob --pk id
|
photo blob --pk id
|
||||||
|
|
||||||
Valid column types are text, integer, float and blob.
|
Valid column types are text, integer, real, float and blob.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--pk TEXT Column to use as primary key
|
--pk TEXT Column to use as primary key
|
||||||
|
|
@ -975,6 +1009,29 @@ See :ref:`cli_create_index`.
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
.. _cli_ref_drop_index:
|
||||||
|
|
||||||
|
drop-index
|
||||||
|
==========
|
||||||
|
|
||||||
|
See :ref:`cli_drop_index`.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Usage: sqlite-utils drop-index [OPTIONS] PATH TABLE INDEX
|
||||||
|
|
||||||
|
Drop an index by index name from the specified table
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
sqlite-utils drop-index chickens.db chickens idx_chickens_name
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--ignore Ignore if index does not exist
|
||||||
|
--load-extension TEXT Path to SQLite extension, with optional :entrypoint
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
|
||||||
.. _cli_ref_migrate:
|
.. _cli_ref_migrate:
|
||||||
|
|
||||||
migrate
|
migrate
|
||||||
|
|
@ -1024,7 +1081,7 @@ See :ref:`cli_fts`.
|
||||||
|
|
||||||
Usage: sqlite-utils enable-fts [OPTIONS] PATH TABLE COLUMN...
|
Usage: sqlite-utils enable-fts [OPTIONS] PATH TABLE COLUMN...
|
||||||
|
|
||||||
Enable full-text search for specific table and columns"
|
Enable full-text search for specific table and columns
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
|
||||||
171
docs/cli.rst
171
docs/cli.rst
|
|
@ -29,6 +29,16 @@ The ``sqlite-utils query`` command lets you run queries directly against a SQLit
|
||||||
.. note::
|
.. note::
|
||||||
In Python: :ref:`db.query() <python_api_query>` CLI reference: :ref:`sqlite-utils query <cli_ref_query>`
|
In Python: :ref:`db.query() <python_api_query>` CLI reference: :ref:`sqlite-utils query <cli_ref_query>`
|
||||||
|
|
||||||
|
Pass ``-`` as the SQL query to read the query from standard input. This is useful for longer queries that would otherwise require careful shell escaping, or for piping in SQL generated by another tool:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
echo "select * from dogs" | sqlite-utils query dogs.db -
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils query dogs.db - < query.sql
|
||||||
|
|
||||||
.. _cli_query_json:
|
.. _cli_query_json:
|
||||||
|
|
||||||
Returning JSON
|
Returning JSON
|
||||||
|
|
@ -351,7 +361,7 @@ To return the first column of each result as raw data, separated by newlines, us
|
||||||
Using named parameters
|
Using named parameters
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
You can pass named parameters to the query using ``-p``:
|
You can pass named parameters to the query using ``-p name value``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|
@ -414,6 +424,9 @@ The ``--functions`` option can be used multiple times to load functions from mul
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
return urlparse(url).path'
|
return urlparse(url).path'
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.register_function() <python_api_register_function>`
|
||||||
|
|
||||||
.. _cli_query_extensions:
|
.. _cli_query_extensions:
|
||||||
|
|
||||||
SQLite extensions
|
SQLite extensions
|
||||||
|
|
@ -1014,6 +1027,9 @@ To show more than 10 common values, use ``--common-limit 20``. To skip the most
|
||||||
|
|
||||||
sqlite-utils analyze-tables github.db tags --common-limit 20 --no-least
|
sqlite-utils analyze-tables github.db tags --common-limit 20 --no-least
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.analyze_column() <python_api_analyze_column>` CLI reference: :ref:`sqlite-utils analyze-tables <cli_ref_analyze_tables>`
|
||||||
|
|
||||||
.. _cli_analyze_tables_save:
|
.. _cli_analyze_tables_save:
|
||||||
|
|
||||||
Saving the analyzed table details
|
Saving the analyzed table details
|
||||||
|
|
@ -1181,6 +1197,9 @@ You can delete all the existing rows in the table before inserting the new recor
|
||||||
|
|
||||||
You can add the ``--analyze`` option to run ``ANALYZE`` against the table after the rows have been inserted.
|
You can add the ``--analyze`` option to run ``ANALYZE`` against the table after the rows have been inserted.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.insert_all() <python_api_bulk_inserts>` CLI reference: :ref:`sqlite-utils insert <cli_ref_insert>`
|
||||||
|
|
||||||
.. _cli_inserting_data_binary:
|
.. _cli_inserting_data_binary:
|
||||||
|
|
||||||
Inserting binary data
|
Inserting binary data
|
||||||
|
|
@ -1356,6 +1375,25 @@ Will produce this schema with automatically detected types:
|
||||||
"weight" REAL
|
"weight" REAL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
.. _cli_insert_csv_tsv_column_types:
|
||||||
|
|
||||||
|
Overriding column types
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Use ``--type column-name type`` to override the type automatically chosen when the table is created. This option can be used more than once, and works with both ``insert`` and ``upsert``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils insert places.db places places.csv --csv \
|
||||||
|
--type zipcode text \
|
||||||
|
--type score real
|
||||||
|
|
||||||
|
This is useful for values such as ZIP codes, which may look like integers but should be stored as ``TEXT`` to preserve leading zeros.
|
||||||
|
|
||||||
|
The column type should be one of ``TEXT``, ``INTEGER``, ``FLOAT``, ``REAL`` or ``BLOB``. Column types are matched case-insensitively.
|
||||||
|
|
||||||
|
As with detected column types, ``--type`` only affects tables created by the command. If the table already exists, its existing column types are left unchanged.
|
||||||
|
|
||||||
To disable type detection and treat all columns as TEXT, use ``--no-detect-types``:
|
To disable type detection and treat all columns as TEXT, use ``--no-detect-types``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
@ -1551,6 +1589,27 @@ The result looks like this:
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
|
|
||||||
|
.. _cli_insert_code:
|
||||||
|
|
||||||
|
Inserting rows generated by Python code
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Instead of providing a ``FILE`` to import, you can use the ``--code`` option to pass a block of Python code that generates the rows to insert. This is the command-line equivalent of calling ``db["creatures"].insert_all(rows())`` from the :ref:`Python API <python_api>`.
|
||||||
|
|
||||||
|
Your code should define either a ``rows()`` function that returns or yields dictionaries, or a ``rows`` iterable such as a list of dictionaries:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils insert data.db creatures --code '
|
||||||
|
def rows():
|
||||||
|
yield {"id": 1, "name": "Cleo"}
|
||||||
|
yield {"id": 2, "name": "Suna"}
|
||||||
|
' --pk id
|
||||||
|
|
||||||
|
``--code`` can also be given a path to a Python ``.py`` file.
|
||||||
|
|
||||||
|
The ``--code`` option works with both ``sqlite-utils insert`` and ``sqlite-utils upsert``, and composes with table options such as ``--pk``, ``--replace``, ``--alter``, ``--not-null`` and ``--default``. It cannot be combined with a ``FILE`` argument or with input format options such as ``--csv`` or ``--convert``.
|
||||||
|
|
||||||
.. _cli_insert_replace:
|
.. _cli_insert_replace:
|
||||||
|
|
||||||
Insert-replacing data
|
Insert-replacing data
|
||||||
|
|
@ -1565,6 +1624,9 @@ To replace a dog with in ID of 2 with a new record, run the following:
|
||||||
echo '{"id": 2, "name": "Pancakes", "age": 3}' | \
|
echo '{"id": 2, "name": "Pancakes", "age": 3}' | \
|
||||||
sqlite-utils insert dogs.db dogs - --pk=id --replace
|
sqlite-utils insert dogs.db dogs - --pk=id --replace
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.insert(..., replace=True) <python_api_insert_replace>` CLI reference: :ref:`sqlite-utils insert <cli_ref_insert>`
|
||||||
|
|
||||||
.. _cli_upsert:
|
.. _cli_upsert:
|
||||||
|
|
||||||
Upserting data
|
Upserting data
|
||||||
|
|
@ -1583,12 +1645,17 @@ For example:
|
||||||
|
|
||||||
This will update the dog with an ID of 2 to have an age of 4, creating a new record (with a null name) if one does not exist. If a row DOES exist the name will be left as-is.
|
This will update the dog with an ID of 2 to have an age of 4, creating a new record (with a null name) if one does not exist. If a row DOES exist the name will be left as-is.
|
||||||
|
|
||||||
|
If the table already exists and has a primary key, you can omit the ``--pk`` option and ``sqlite-utils`` will use that existing primary key.
|
||||||
|
|
||||||
The command will fail if you reference columns that do not exist on the table. To automatically create missing columns, use the ``--alter`` option.
|
The command will fail if you reference columns that do not exist on the table. To automatically create missing columns, use the ``--alter`` option.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
``upsert`` in sqlite-utils 1.x worked like ``insert ... --replace`` does in 2.x. See `issue #66 <https://github.com/simonw/sqlite-utils/issues/66>`__ for details of this change.
|
``upsert`` in sqlite-utils 1.x worked like ``insert ... --replace`` does in 2.x. See `issue #66 <https://github.com/simonw/sqlite-utils/issues/66>`__ for details of this change.
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.upsert() <python_api_upsert>` CLI reference: :ref:`sqlite-utils upsert <cli_ref_upsert>`
|
||||||
|
|
||||||
.. _cli_bulk:
|
.. _cli_bulk:
|
||||||
|
|
||||||
Executing SQL in bulk
|
Executing SQL in bulk
|
||||||
|
|
@ -1790,6 +1857,9 @@ You can include named parameters in your where clause and populate them using on
|
||||||
|
|
||||||
The ``--dry-run`` option will output a preview of the conversion against the first ten rows, without modifying the database.
|
The ``--dry-run`` option will output a preview of the conversion against the first ten rows, without modifying the database.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.convert() <python_api_convert>` CLI reference: :ref:`sqlite-utils convert <cli_ref_convert>`
|
||||||
|
|
||||||
.. _cli_convert_import:
|
.. _cli_convert_import:
|
||||||
|
|
||||||
Importing additional modules
|
Importing additional modules
|
||||||
|
|
@ -2088,6 +2158,9 @@ If a table with the same name already exists, you will get an error. You can cho
|
||||||
|
|
||||||
You can also pass ``--transform`` to transform the existing table to match the new schema. See :ref:`python_api_explicit_create` in the Python library documentation for details of how this option works.
|
You can also pass ``--transform`` to transform the existing table to match the new schema. See :ref:`python_api_explicit_create` in the Python library documentation for details of how this option works.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.create() <python_api_explicit_create>` CLI reference: :ref:`sqlite-utils create-table <cli_ref_create_table>`
|
||||||
|
|
||||||
.. _cli_renaming_tables:
|
.. _cli_renaming_tables:
|
||||||
|
|
||||||
Renaming a table
|
Renaming a table
|
||||||
|
|
@ -2101,6 +2174,9 @@ Yo ucan rename a table using the ``rename-table`` command:
|
||||||
|
|
||||||
Pass ``--ignore`` to ignore any errors caused by the table not existing, or the new name already being in use.
|
Pass ``--ignore`` to ignore any errors caused by the table not existing, or the new name already being in use.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.rename_table() <python_api_rename_table>` CLI reference: :ref:`sqlite-utils rename-table <cli_ref_rename_table>`
|
||||||
|
|
||||||
.. _cli_duplicate_table:
|
.. _cli_duplicate_table:
|
||||||
|
|
||||||
Duplicating tables
|
Duplicating tables
|
||||||
|
|
@ -2112,6 +2188,9 @@ The ``duplicate`` command duplicates a table - creating a new table with the sam
|
||||||
|
|
||||||
sqlite-utils duplicate books.db authors authors_copy
|
sqlite-utils duplicate books.db authors authors_copy
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.duplicate() <python_api_duplicate>` CLI reference: :ref:`sqlite-utils duplicate <cli_ref_duplicate>`
|
||||||
|
|
||||||
.. _cli_drop_table:
|
.. _cli_drop_table:
|
||||||
|
|
||||||
Dropping tables
|
Dropping tables
|
||||||
|
|
@ -2125,12 +2204,15 @@ You can drop a table using the ``drop-table`` command:
|
||||||
|
|
||||||
Use ``--ignore`` to ignore the error if the table does not exist.
|
Use ``--ignore`` to ignore the error if the table does not exist.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.drop() <python_api_drop>` CLI reference: :ref:`sqlite-utils drop-table <cli_ref_drop_table>`
|
||||||
|
|
||||||
.. _cli_transform_table:
|
.. _cli_transform_table:
|
||||||
|
|
||||||
Transforming tables
|
Transforming tables
|
||||||
===================
|
===================
|
||||||
|
|
||||||
The ``transform`` command allows you to apply complex transformations to a table that cannot be implemented using a regular SQLite ``ALTER TABLE`` command. See :ref:`python_api_transform` for details of how this works. The ``transform`` command preserves a table's ``STRICT`` mode.
|
The ``transform`` command allows you to apply complex transformations to a table that cannot be implemented using a regular SQLite ``ALTER TABLE`` command. See :ref:`python_api_transform` for details of how this works. By default, the ``transform`` command preserves a table's ``STRICT`` mode.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|
@ -2176,6 +2258,12 @@ Every option for this table (with the exception of ``--pk-none``) can be specifi
|
||||||
``--add-foreign-key column other_table other_column``
|
``--add-foreign-key column other_table other_column``
|
||||||
Add a foreign key constraint to ``column`` pointing to ``other_table.other_column``.
|
Add a foreign key constraint to ``column`` pointing to ``other_table.other_column``.
|
||||||
|
|
||||||
|
``--strict``
|
||||||
|
Convert the table to a `SQLite STRICT table <https://www.sqlite.org/stricttables.html>`__. The command fails if the available SQLite version does not support strict tables. If existing rows contain values that are incompatible with their declared column types the transformation fails and the original table is left unchanged.
|
||||||
|
|
||||||
|
``--no-strict``
|
||||||
|
Convert a strict table back to a regular non-strict table.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
@ -2202,6 +2290,9 @@ If you want to see the SQL that will be executed to make the change without actu
|
||||||
DROP TABLE "roadside_attractions";
|
DROP TABLE "roadside_attractions";
|
||||||
ALTER TABLE "roadside_attractions_new_4033a60276b9" RENAME TO "roadside_attractions";
|
ALTER TABLE "roadside_attractions_new_4033a60276b9" RENAME TO "roadside_attractions";
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.transform() <python_api_transform>` CLI reference: :ref:`sqlite-utils transform <cli_ref_transform>`
|
||||||
|
|
||||||
.. _cli_transform_table_add_primary_key_to_rowid:
|
.. _cli_transform_table_add_primary_key_to_rowid:
|
||||||
|
|
||||||
Adding a primary key to a rowid table
|
Adding a primary key to a rowid table
|
||||||
|
|
@ -2379,6 +2470,9 @@ After running the above, the command ``sqlite-utils schema global.db`` reveals t
|
||||||
CREATE UNIQUE INDEX "idx_countries_country_name"
|
CREATE UNIQUE INDEX "idx_countries_country_name"
|
||||||
ON "countries" ("country", "name");
|
ON "countries" ("country", "name");
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.extract() <python_api_extract>` CLI reference: :ref:`sqlite-utils extract <cli_ref_extract>`
|
||||||
|
|
||||||
.. _cli_create_view:
|
.. _cli_create_view:
|
||||||
|
|
||||||
Creating views
|
Creating views
|
||||||
|
|
@ -2400,6 +2494,9 @@ You can create a view using the ``create-view`` command:
|
||||||
|
|
||||||
Use ``--replace`` to replace an existing view of the same name, and ``--ignore`` to do nothing if a view already exists.
|
Use ``--replace`` to replace an existing view of the same name, and ``--ignore`` to do nothing if a view already exists.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.create_view() <python_api_create_view>` CLI reference: :ref:`sqlite-utils create-view <cli_ref_create_view>`
|
||||||
|
|
||||||
.. _cli_drop_view:
|
.. _cli_drop_view:
|
||||||
|
|
||||||
Dropping views
|
Dropping views
|
||||||
|
|
@ -2413,6 +2510,9 @@ You can drop a view using the ``drop-view`` command:
|
||||||
|
|
||||||
Use ``--ignore`` to ignore the error if the view does not exist.
|
Use ``--ignore`` to ignore the error if the view does not exist.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`view.drop() <python_api_drop>` CLI reference: :ref:`sqlite-utils drop-view <cli_ref_drop_view>`
|
||||||
|
|
||||||
.. _cli_add_column:
|
.. _cli_add_column:
|
||||||
|
|
||||||
Adding columns
|
Adding columns
|
||||||
|
|
@ -2453,6 +2553,9 @@ You can set a ``NOT NULL DEFAULT 'x'`` constraint on the new column using ``--no
|
||||||
|
|
||||||
sqlite-utils add-column mydb.db dogs friends_count integer --not-null-default 0
|
sqlite-utils add-column mydb.db dogs friends_count integer --not-null-default 0
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.add_column() <python_api_add_column>` CLI reference: :ref:`sqlite-utils add-column <cli_ref_add_column>`
|
||||||
|
|
||||||
.. _cli_add_column_alter:
|
.. _cli_add_column_alter:
|
||||||
|
|
||||||
Adding columns automatically on insert/update
|
Adding columns automatically on insert/update
|
||||||
|
|
@ -2464,6 +2567,9 @@ You can use the ``--alter`` option to automatically add new columns if the data
|
||||||
|
|
||||||
sqlite-utils insert dogs.db dogs new-dogs.json --pk=id --alter
|
sqlite-utils insert dogs.db dogs new-dogs.json --pk=id --alter
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.insert(..., alter=True) <python_api_add_column_alter>`
|
||||||
|
|
||||||
.. _cli_add_foreign_key:
|
.. _cli_add_foreign_key:
|
||||||
|
|
||||||
Adding foreign key constraints
|
Adding foreign key constraints
|
||||||
|
|
@ -2491,6 +2597,9 @@ Add ``--ignore`` to ignore an existing foreign key (as opposed to returning an e
|
||||||
|
|
||||||
See :ref:`python_api_add_foreign_key` in the Python API documentation for further details, including how the automatic table guessing mechanism works.
|
See :ref:`python_api_add_foreign_key` in the Python API documentation for further details, including how the automatic table guessing mechanism works.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.add_foreign_key() <python_api_add_foreign_key>` CLI reference: :ref:`sqlite-utils add-foreign-key <cli_ref_add_foreign_key>`
|
||||||
|
|
||||||
.. _cli_add_foreign_keys:
|
.. _cli_add_foreign_keys:
|
||||||
|
|
||||||
Adding multiple foreign keys at once
|
Adding multiple foreign keys at once
|
||||||
|
|
@ -2506,6 +2615,9 @@ Adding a foreign key requires a ``VACUUM``. On large databases this can be an ex
|
||||||
|
|
||||||
When you are using this command each foreign key needs to be defined in full, as four arguments - the table, column, other table and other column.
|
When you are using this command each foreign key needs to be defined in full, as four arguments - the table, column, other table and other column.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.add_foreign_keys() <python_api_add_foreign_keys>` CLI reference: :ref:`sqlite-utils add-foreign-keys <cli_ref_add_foreign_keys>`
|
||||||
|
|
||||||
.. _cli_index_foreign_keys:
|
.. _cli_index_foreign_keys:
|
||||||
|
|
||||||
Adding indexes for all foreign keys
|
Adding indexes for all foreign keys
|
||||||
|
|
@ -2517,6 +2629,9 @@ If you want to ensure that every foreign key column in your database has a corre
|
||||||
|
|
||||||
sqlite-utils index-foreign-keys books.db
|
sqlite-utils index-foreign-keys books.db
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.index_foreign_keys() <python_api_index_foreign_keys>` CLI reference: :ref:`sqlite-utils index-foreign-keys <cli_ref_index_foreign_keys>`
|
||||||
|
|
||||||
.. _cli_defaults_not_null:
|
.. _cli_defaults_not_null:
|
||||||
|
|
||||||
Setting defaults and not null constraints
|
Setting defaults and not null constraints
|
||||||
|
|
@ -2532,6 +2647,9 @@ You can use the ``--not-null`` and ``--default`` options (to both ``insert`` and
|
||||||
--default age 2 \
|
--default age 2 \
|
||||||
--default score 5
|
--default score 5
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`not_null= and defaults= arguments <python_api_defaults_not_null>`
|
||||||
|
|
||||||
.. _cli_create_index:
|
.. _cli_create_index:
|
||||||
|
|
||||||
Creating indexes
|
Creating indexes
|
||||||
|
|
@ -2563,6 +2681,25 @@ If your column names are already prefixed with a hyphen you'll need to manually
|
||||||
|
|
||||||
Add the ``--analyze`` option to run ``ANALYZE`` against the index after it has been created.
|
Add the ``--analyze`` option to run ``ANALYZE`` against the index after it has been created.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.create_index() <python_api_create_index>` CLI reference: :ref:`sqlite-utils create-index <cli_ref_create_index>`
|
||||||
|
|
||||||
|
.. _cli_drop_index:
|
||||||
|
|
||||||
|
Dropping indexes
|
||||||
|
================
|
||||||
|
|
||||||
|
You can drop an index from an existing table using the ``drop-index`` command:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils drop-index mydb.db mytable idx_mytable_col1
|
||||||
|
|
||||||
|
Use ``--ignore`` to ignore the error if the index does not exist on that table.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.drop_index() <python_api_create_index>` CLI reference: :ref:`sqlite-utils drop-index <cli_ref_drop_index>`
|
||||||
|
|
||||||
.. _cli_fts:
|
.. _cli_fts:
|
||||||
|
|
||||||
Configuring full-text search
|
Configuring full-text search
|
||||||
|
|
@ -2616,6 +2753,9 @@ You can rebuild every FTS table by running ``rebuild-fts`` without passing any t
|
||||||
|
|
||||||
sqlite-utils rebuild-fts mydb.db
|
sqlite-utils rebuild-fts mydb.db
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.enable_fts() <python_api_fts_enable>` CLI reference: :ref:`sqlite-utils enable-fts <cli_ref_enable_fts>`
|
||||||
|
|
||||||
.. _cli_search:
|
.. _cli_search:
|
||||||
|
|
||||||
Executing searches
|
Executing searches
|
||||||
|
|
@ -2672,6 +2812,9 @@ Use the ``--sql`` option to output the SQL that would be executed, rather than r
|
||||||
order by
|
order by
|
||||||
"documents_fts".rank
|
"documents_fts".rank
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.search() <python_api_fts_search>` CLI reference: :ref:`sqlite-utils search <cli_ref_search>`
|
||||||
|
|
||||||
.. _cli_enable_counts:
|
.. _cli_enable_counts:
|
||||||
|
|
||||||
Enabling cached counts
|
Enabling cached counts
|
||||||
|
|
@ -2695,6 +2838,9 @@ If the ``_counts`` table ever becomes out-of-sync with the actual table counts y
|
||||||
|
|
||||||
sqlite-utils reset-counts mydb.db
|
sqlite-utils reset-counts mydb.db
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.enable_counts() <python_api_cached_table_counts>` CLI reference: :ref:`sqlite-utils enable-counts <cli_ref_enable_counts>`
|
||||||
|
|
||||||
.. _cli_analyze:
|
.. _cli_analyze:
|
||||||
|
|
||||||
Optimizing index usage with ANALYZE
|
Optimizing index usage with ANALYZE
|
||||||
|
|
@ -2718,6 +2864,9 @@ You can run it against specific tables, or against specific named indexes, by pa
|
||||||
|
|
||||||
You can also run ``ANALYZE`` as part of another command using the ``--analyze`` option. This is supported by the ``create-index``, ``insert`` and ``upsert`` commands.
|
You can also run ``ANALYZE`` as part of another command using the ``--analyze`` option. This is supported by the ``create-index``, ``insert`` and ``upsert`` commands.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.analyze() <python_api_analyze>` CLI reference: :ref:`sqlite-utils analyze <cli_ref_analyze>`
|
||||||
|
|
||||||
.. _cli_vacuum:
|
.. _cli_vacuum:
|
||||||
|
|
||||||
Vacuum
|
Vacuum
|
||||||
|
|
@ -2729,6 +2878,9 @@ You can run VACUUM to optimize your database like so:
|
||||||
|
|
||||||
sqlite-utils vacuum mydb.db
|
sqlite-utils vacuum mydb.db
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.vacuum() <python_api_vacuum>` CLI reference: :ref:`sqlite-utils vacuum <cli_ref_vacuum>`
|
||||||
|
|
||||||
.. _cli_optimize:
|
.. _cli_optimize:
|
||||||
|
|
||||||
Optimize
|
Optimize
|
||||||
|
|
@ -2752,6 +2904,9 @@ To optimize specific tables rather than every FTS table, pass those tables as ex
|
||||||
|
|
||||||
sqlite-utils optimize mydb.db table_1 table_2
|
sqlite-utils optimize mydb.db table_1 table_2
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.optimize() <python_api_fts_optimize>` CLI reference: :ref:`sqlite-utils optimize <cli_ref_optimize>`
|
||||||
|
|
||||||
.. _cli_wal:
|
.. _cli_wal:
|
||||||
|
|
||||||
WAL mode
|
WAL mode
|
||||||
|
|
@ -2771,6 +2926,9 @@ You can disable WAL mode using ``disable-wal``:
|
||||||
|
|
||||||
Both of these commands accept one or more database files as arguments.
|
Both of these commands accept one or more database files as arguments.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.enable_wal() and db.disable_wal() <python_api_wal>` CLI reference: :ref:`sqlite-utils enable-wal <cli_ref_enable_wal>`
|
||||||
|
|
||||||
.. _cli_dump:
|
.. _cli_dump:
|
||||||
|
|
||||||
Dumping the database to SQL
|
Dumping the database to SQL
|
||||||
|
|
@ -2786,6 +2944,9 @@ The ``dump`` command outputs a SQL dump of the schema and full contents of the s
|
||||||
...
|
...
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`db.iterdump() <python_api_itedump>` CLI reference: :ref:`sqlite-utils dump <cli_ref_dump>`
|
||||||
|
|
||||||
.. _cli_load_extension:
|
.. _cli_load_extension:
|
||||||
|
|
||||||
Loading SQLite extensions
|
Loading SQLite extensions
|
||||||
|
|
@ -2833,6 +2994,9 @@ Eight (case-insensitive) types are allowed:
|
||||||
* GEOMETRYCOLLECTION
|
* GEOMETRYCOLLECTION
|
||||||
* GEOMETRY
|
* GEOMETRY
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.add_geometry_column() <python_api_gis_add_geometry_column>` CLI reference: :ref:`sqlite-utils add-geometry-column <cli_ref_add_geometry_column>`
|
||||||
|
|
||||||
.. _cli_spatialite_indexes:
|
.. _cli_spatialite_indexes:
|
||||||
|
|
||||||
Adding spatial indexes
|
Adding spatial indexes
|
||||||
|
|
@ -2846,6 +3010,9 @@ Once you have a geometry column, you can speed up bounding box queries by adding
|
||||||
|
|
||||||
See this `SpatiaLite Cookbook recipe <http://www.gaia-gis.it/gaia-sins/spatialite-cookbook-5/cookbook_topics.03.html#topic_Wonderful_RTree_Spatial_Index>`__ for examples of how to use a spatial index.
|
See this `SpatiaLite Cookbook recipe <http://www.gaia-gis.it/gaia-sins/spatialite-cookbook-5/cookbook_topics.03.html#topic_Wonderful_RTree_Spatial_Index>`__ for examples of how to use a spatial index.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In Python: :ref:`table.create_spatial_index() <python_api_gis_create_spatial_index>` CLI reference: :ref:`sqlite-utils create-spatial-index <cli_ref_create_spatial_index>`
|
||||||
|
|
||||||
.. _cli_install:
|
.. _cli_install:
|
||||||
|
|
||||||
Installing packages
|
Installing packages
|
||||||
|
|
|
||||||
11
docs/conf.py
11
docs/conf.py
|
|
@ -1,10 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from pathlib import Path
|
|
||||||
from subprocess import Popen, PIPE, check_output
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import PIPE, CalledProcessError, Popen, check_output
|
||||||
|
|
||||||
# This file is execfile()d with the current directory set to its
|
# This file is execfile()d with the current directory set to its
|
||||||
# containing dir.
|
# containing dir.
|
||||||
|
|
@ -50,7 +47,7 @@ extlinks = {
|
||||||
def _linkcode_git_ref():
|
def _linkcode_git_ref():
|
||||||
try:
|
try:
|
||||||
return check_output(["git", "rev-parse", "HEAD"]).decode("utf8").strip()
|
return check_output(["git", "rev-parse", "HEAD"]).decode("utf8").strip()
|
||||||
except Exception:
|
except (CalledProcessError, OSError):
|
||||||
return "main"
|
return "main"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -79,7 +76,7 @@ def linkcode_resolve(domain, info):
|
||||||
obj = inspect.unwrap(obj)
|
obj = inspect.unwrap(obj)
|
||||||
source_file = inspect.getsourcefile(obj)
|
source_file = inspect.getsourcefile(obj)
|
||||||
_, line_number = inspect.getsourcelines(obj)
|
_, line_number = inspect.getsourcelines(obj)
|
||||||
except Exception:
|
except (OSError, TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if source_file is None:
|
if source_file is None:
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ Here is a simple example of a ``migrations.py`` file which creates a table, then
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from sqlite_utils import Database, Migrations
|
from sqlite_utils import Migrations
|
||||||
|
|
||||||
migrations = Migrations("creatures")
|
migrations = Migrations("creatures")
|
||||||
|
|
||||||
|
|
@ -51,6 +51,8 @@ Once you have a ``Migrations(name)`` collection with one or more migrations regi
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sqlite_utils import Database
|
||||||
|
|
||||||
db = Database("creatures.db")
|
db = Database("creatures.db")
|
||||||
migrations.apply(db)
|
migrations.apply(db)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,14 @@ You can also create a named in-memory database. Unlike regular memory databases
|
||||||
|
|
||||||
db = Database(memory_name="my_shared_database")
|
db = Database(memory_name="my_shared_database")
|
||||||
|
|
||||||
|
After creating a ``Database`` you can use ``db.memory`` and ``db.memory_name`` to tell whether it is backed by an in-memory database and to read the shared cache name. ``db.memory`` is ``True`` for any in-memory database and ``db.memory_name`` holds the name passed to ``memory_name=``, or ``None`` otherwise.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
db = Database(memory_name="shared")
|
||||||
|
db.memory # True
|
||||||
|
db.memory_name # "shared"
|
||||||
|
|
||||||
Connections use ``PRAGMA recursive_triggers=on`` by default. If you don't want to use `recursive triggers <https://www.sqlite.org/pragma.html#pragma_recursive_triggers>`__ you can turn them off using:
|
Connections use ``PRAGMA recursive_triggers=on`` by default. If you don't want to use `recursive triggers <https://www.sqlite.org/pragma.html#pragma_recursive_triggers>`__ you can turn them off using:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
@ -176,6 +184,9 @@ You can attach an additional database using the ``.attach()`` method, providing
|
||||||
|
|
||||||
You can reference tables in the attached database using the alias value you passed to ``db.attach(alias, filepath)`` as a prefix, for example the ``second.table_in_second`` reference in the SQL query above.
|
You can reference tables in the attached database using the alias value you passed to ``db.attach(alias, filepath)`` as a prefix, for example the ``second.table_in_second`` reference in the SQL query above.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils --attach <cli_query_attach>`
|
||||||
|
|
||||||
.. _python_api_tracing:
|
.. _python_api_tracing:
|
||||||
|
|
||||||
Tracing queries
|
Tracing queries
|
||||||
|
|
@ -246,6 +257,9 @@ If a query returns more than one column with the same name - a join between two
|
||||||
|
|
||||||
A suffix that would collide with another column in the query is skipped - ``select 1 as id, 2 as id, 3 as id_2`` returns ``{'id': 1, 'id_3': 2, 'id_2': 3}``. The same renaming is applied by ``table.rows_where()`` and ``table.search()``.
|
A suffix that would collide with another column in the query is skipped - ``select 1 as id, 2 as id, 3 as id_2`` returns ``{'id': 1, 'id_3': 2, 'id_2': 3}``. The same renaming is applied by ``table.rows_where()`` and ``table.search()``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils query <cli_query>`
|
||||||
|
|
||||||
.. _python_api_execute:
|
.. _python_api_execute:
|
||||||
|
|
||||||
db.execute(sql, params)
|
db.execute(sql, params)
|
||||||
|
|
@ -312,11 +326,15 @@ Every method in this library that writes to the database - ``insert()``, ``upser
|
||||||
|
|
||||||
The same applies to raw SQL executed with :ref:`db.execute() <python_api_transactions_execute>` - a write statement is committed as soon as it has run.
|
The same applies to raw SQL executed with :ref:`db.execute() <python_api_transactions_execute>` - a write statement is committed as soon as it has run.
|
||||||
|
|
||||||
|
Another way to think about this is that each sqlite-utils method call is its own unit of work. If several method calls must either all succeed or all fail, use ``db.atomic()`` to turn them into a single unit of work.
|
||||||
|
|
||||||
You never need to call ``commit()``, and you do not need to close the database to persist your changes. There are exactly two situations where you need to think about transactions:
|
You never need to call ``commit()``, and you do not need to close the database to persist your changes. There are exactly two situations where you need to think about transactions:
|
||||||
|
|
||||||
1. You want to group several write operations together, so they either all succeed or all fail - use :ref:`db.atomic() <python_api_atomic>`.
|
1. You want to group several write operations together, so they either all succeed or all fail - use :ref:`db.atomic() <python_api_atomic>`.
|
||||||
2. You are :ref:`managing a transaction yourself <python_api_transactions_manual>` with ``db.begin()``, in which case nothing is committed until you commit - the library will never commit a transaction you opened.
|
2. You are :ref:`managing a transaction yourself <python_api_transactions_manual>` with ``db.begin()``, in which case nothing is committed until you commit - the library will never commit a transaction you opened.
|
||||||
|
|
||||||
|
``with Database(...) as db:`` is not a transaction block. It manages the lifetime of the database connection and closes it on exit. Use ``with db.atomic():`` for a transaction.
|
||||||
|
|
||||||
.. _python_api_atomic:
|
.. _python_api_atomic:
|
||||||
|
|
||||||
Grouping changes with db.atomic()
|
Grouping changes with db.atomic()
|
||||||
|
|
@ -332,6 +350,27 @@ Use ``db.atomic()`` to group multiple operations in a single transaction:
|
||||||
|
|
||||||
The transaction commits when the block exits. If an exception is raised, changes made inside the block will be rolled back.
|
The transaction commits when the block exits. If an exception is raised, changes made inside the block will be rolled back.
|
||||||
|
|
||||||
|
This matters when several operations represent a single logical change. Without ``db.atomic()``, an earlier method call remains committed if a later one fails:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# These are two separate transactions
|
||||||
|
db.table("accounts").update(1, {"balance": 90})
|
||||||
|
db.table("accounts").update(2, {"balance": 110})
|
||||||
|
|
||||||
|
# These updates either both succeed or both fail
|
||||||
|
with db.atomic():
|
||||||
|
db.table("accounts").update(1, {"balance": 90})
|
||||||
|
db.table("accounts").update(2, {"balance": 110})
|
||||||
|
|
||||||
|
Transactions can also improve performance. Calling ``insert()`` repeatedly outside ``db.atomic()`` creates and commits a separate transaction for every call. For bulk inserts, prefer :ref:`insert_all() <python_api_bulk_inserts>`. If you need to call several different methods in a loop, wrap the loop in ``db.atomic()``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with db.atomic():
|
||||||
|
for row in rows:
|
||||||
|
db.table("events").insert(row)
|
||||||
|
|
||||||
``db.atomic()`` can be nested. Nested blocks use SQLite savepoints, so an exception in an inner block can roll back to that savepoint without rolling back the entire outer transaction:
|
``db.atomic()`` can be nested. Nested blocks use SQLite savepoints, so an exception in an inner block can roll back to that savepoint without rolling back the entire outer transaction:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
@ -360,6 +399,8 @@ Write statements executed with :ref:`db.execute() <python_api_execute>` follow t
|
||||||
db.execute("insert into news (headline) values (?)", ["Dog wins award"])
|
db.execute("insert into news (headline) values (?)", ["Dog wins award"])
|
||||||
# Already committed
|
# Already committed
|
||||||
|
|
||||||
|
``db.execute()`` participates in sqlite-utils transaction handling. Calling ``db.conn.execute()`` directly bypasses that policy and leaves transaction handling to Python's underlying ``sqlite3.Connection``. Prefer ``db.execute()`` unless you deliberately need the lower-level API.
|
||||||
|
|
||||||
If a transaction is open - because the call happens inside a ``db.atomic()`` block, or after ``db.begin()`` - the statement becomes part of that transaction instead, and commits when the transaction commits:
|
If a transaction is open - because the call happens inside a ``db.atomic()`` block, or after ``db.begin()`` - the statement becomes part of that transaction instead, and commits when the transaction commits:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
@ -391,9 +432,12 @@ You can take full manual control using the ``db.begin()``, ``db.commit()`` and `
|
||||||
|
|
||||||
The library will never commit a transaction you opened. If you call write methods such as ``insert()`` - or use ``db.atomic()`` - while your transaction is open, they participate in it using SQLite savepoints instead of committing: exiting an ``atomic()`` block releases its savepoint, but nothing is saved to disk until you commit the outer transaction yourself. If you roll back, their changes are rolled back too.
|
The library will never commit a transaction you opened. If you call write methods such as ``insert()`` - or use ``db.atomic()`` - while your transaction is open, they participate in it using SQLite savepoints instead of committing: exiting an ``atomic()`` block releases its savepoint, but nothing is saved to disk until you commit the outer transaction yourself. If you roll back, their changes are rolled back too.
|
||||||
|
|
||||||
Two related safeguards to be aware of:
|
Prefer ``db.atomic()`` or ``db.begin()``, ``db.commit()`` and ``db.rollback()`` over mixing sqlite-utils transaction methods with calls to ``db.conn.commit()``, ``db.conn.rollback()`` or raw transaction-control SQL. Mixing the two layers makes it much harder to tell which layer owns the current transaction.
|
||||||
|
|
||||||
|
Some related safeguards to be aware of:
|
||||||
|
|
||||||
- ``db.enable_wal()`` and ``db.disable_wal()`` raise a ``sqlite_utils.db.TransactionError`` if called while a transaction is open, because changing the journal mode would commit it as a side effect.
|
- ``db.enable_wal()`` and ``db.disable_wal()`` raise a ``sqlite_utils.db.TransactionError`` if called while a transaction is open, because changing the journal mode would commit it as a side effect.
|
||||||
|
- ``table.transform()`` raises a ``sqlite_utils.db.TransactionError`` if called while a transaction is open with ``PRAGMA foreign_keys`` enabled and the table is referenced by foreign keys with destructive ``ON DELETE`` actions, because the pragma cannot be turned off mid-transaction to protect those referencing rows - see :ref:`python_api_transform_foreign_keys_transactions`.
|
||||||
- Closing the database - explicitly with ``db.close()``, or by exiting a ``with Database(...) as db:`` block - rolls back any transaction that is still open, see :ref:`python_api_close`.
|
- Closing the database - explicitly with ``db.close()``, or by exiting a ``with Database(...) as db:`` block - rolls back any transaction that is still open, see :ref:`python_api_close`.
|
||||||
|
|
||||||
.. _python_api_transactions_modes:
|
.. _python_api_transactions_modes:
|
||||||
|
|
@ -401,9 +445,11 @@ Two related safeguards to be aware of:
|
||||||
Supported connection modes
|
Supported connection modes
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
``db.atomic()`` and the automatic per-method transactions require a connection in Python's default transaction handling mode. Passing a connection created with the Python 3.12+ ``sqlite3.connect(..., autocommit=True)`` or ``autocommit=False`` options to ``Database()`` raises a ``sqlite_utils.db.TransactionError``.
|
``db.atomic()`` and the automatic per-method transactions currently require a connection using Python's legacy transaction control mode (``sqlite3.LEGACY_TRANSACTION_CONTROL`` on Python 3.12 and later). Passing a connection created with the Python 3.12+ ``sqlite3.connect(..., autocommit=True)`` or ``autocommit=False`` options to ``Database()`` raises a ``sqlite_utils.db.TransactionError``.
|
||||||
|
|
||||||
This is because ``commit()`` and ``rollback()`` behave differently on those connections - under ``autocommit=True`` they are documented no-ops - which would cause every write made by this library to be silently discarded when the connection closed, rather than failing loudly.
|
Connections using ``autocommit=False`` are not supported because Python keeps a transaction open continuously. sqlite-utils uses ``Connection.in_transaction`` to distinguish its own transactions from transactions opened by its caller, and that distinction is not available in this mode.
|
||||||
|
|
||||||
|
Connections using ``autocommit=True`` are also currently rejected because sqlite-utils has not formally exposed that as a supported configuration.
|
||||||
|
|
||||||
.. _python_api_table:
|
.. _python_api_table:
|
||||||
|
|
||||||
|
|
@ -458,6 +504,9 @@ You can also iterate through the table objects themselves using the ``.tables``
|
||||||
>>> db.tables
|
>>> db.tables
|
||||||
[<Table dogs>]
|
[<Table dogs>]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils tables <cli_tables>`
|
||||||
|
|
||||||
.. _python_api_views:
|
.. _python_api_views:
|
||||||
|
|
||||||
Listing views
|
Listing views
|
||||||
|
|
@ -483,6 +532,9 @@ View objects are similar to Table objects, except that any attempts to insert or
|
||||||
* ``rows_where(where, where_args, order_by, select)``
|
* ``rows_where(where, where_args, order_by, select)``
|
||||||
* ``drop()``
|
* ``drop()``
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils views <cli_views>`
|
||||||
|
|
||||||
.. _python_api_rows:
|
.. _python_api_rows:
|
||||||
|
|
||||||
Listing rows
|
Listing rows
|
||||||
|
|
@ -538,6 +590,9 @@ This method also accepts ``offset=`` and ``limit=`` arguments, for specifying an
|
||||||
... print(row)
|
... print(row)
|
||||||
{'id': 1, 'age': 4, 'name': 'Cleo'}
|
{'id': 1, 'age': 4, 'name': 'Cleo'}
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils rows <cli_rows>`
|
||||||
|
|
||||||
.. _python_api_rows_count_where:
|
.. _python_api_rows_count_where:
|
||||||
|
|
||||||
Counting rows
|
Counting rows
|
||||||
|
|
@ -622,6 +677,9 @@ The ``db.schema`` property returns the full SQL schema for the database as a str
|
||||||
"name" TEXT
|
"name" TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils schema <cli_schema>`
|
||||||
|
|
||||||
.. _python_api_creating_tables:
|
.. _python_api_creating_tables:
|
||||||
|
|
||||||
Creating tables
|
Creating tables
|
||||||
|
|
@ -770,6 +828,9 @@ You can pass ``strict=True`` to create a table in ``STRICT`` mode:
|
||||||
"name": str,
|
"name": str,
|
||||||
}, strict=True)
|
}, strict=True)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils create-table <cli_create_table>`
|
||||||
|
|
||||||
.. _python_api_compound_primary_keys:
|
.. _python_api_compound_primary_keys:
|
||||||
|
|
||||||
Compound primary keys
|
Compound primary keys
|
||||||
|
|
@ -950,6 +1011,9 @@ Here's an example that uses these features:
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils insert --not-null and --default <cli_defaults_not_null>`
|
||||||
|
|
||||||
.. _python_api_rename_table:
|
.. _python_api_rename_table:
|
||||||
|
|
||||||
Renaming a table
|
Renaming a table
|
||||||
|
|
@ -967,6 +1031,9 @@ This executes the following SQL:
|
||||||
|
|
||||||
ALTER TABLE [my_table] RENAME TO [new_name_for_my_table]
|
ALTER TABLE [my_table] RENAME TO [new_name_for_my_table]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils rename-table <cli_renaming_tables>`
|
||||||
|
|
||||||
.. _python_api_duplicate:
|
.. _python_api_duplicate:
|
||||||
|
|
||||||
Duplicating tables
|
Duplicating tables
|
||||||
|
|
@ -982,6 +1049,9 @@ The new ``authors_copy`` table will now contain a duplicate copy of the data fro
|
||||||
|
|
||||||
This method raises ``sqlite_utils.db.NoTable`` if the table does not exist.
|
This method raises ``sqlite_utils.db.NoTable`` if the table does not exist.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils duplicate <cli_duplicate_table>`
|
||||||
|
|
||||||
.. _python_api_bulk_inserts:
|
.. _python_api_bulk_inserts:
|
||||||
|
|
||||||
Bulk inserts
|
Bulk inserts
|
||||||
|
|
@ -1024,6 +1094,9 @@ You can delete all the existing rows in the table before inserting the new recor
|
||||||
|
|
||||||
Pass ``analyze=True`` to run ``ANALYZE`` against the table after inserting the new records.
|
Pass ``analyze=True`` to run ``ANALYZE`` against the table after inserting the new records.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils insert <cli_inserting_data>`
|
||||||
|
|
||||||
.. _python_api_insert_lists:
|
.. _python_api_insert_lists:
|
||||||
|
|
||||||
Inserting data from a list or tuple iterator
|
Inserting data from a list or tuple iterator
|
||||||
|
|
@ -1101,6 +1174,9 @@ To replace any existing records that have a matching primary key, use the ``repl
|
||||||
.. note::
|
.. note::
|
||||||
Prior to sqlite-utils 2.0 the ``.upsert()`` and ``.upsert_all()`` methods worked the same way as ``.insert(replace=True)`` does today. See :ref:`python_api_upsert` for the new behaviour of those methods introduced in 2.0.
|
Prior to sqlite-utils 2.0 the ``.upsert()`` and ``.upsert_all()`` methods worked the same way as ``.insert(replace=True)`` does today. See :ref:`python_api_upsert` for the new behaviour of those methods introduced in 2.0.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils insert --replace <cli_insert_replace>`
|
||||||
|
|
||||||
.. _python_api_update:
|
.. _python_api_update:
|
||||||
|
|
||||||
Updating a specific record
|
Updating a specific record
|
||||||
|
|
@ -1186,6 +1262,9 @@ Every record passed to ``upsert()`` or ``upsert_all()`` must include a value for
|
||||||
.. note::
|
.. note::
|
||||||
``.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.
|
``.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.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils upsert <cli_upsert>`
|
||||||
|
|
||||||
.. _python_api_old_upsert:
|
.. _python_api_old_upsert:
|
||||||
|
|
||||||
Alternative upserts using INSERT OR IGNORE
|
Alternative upserts using INSERT OR IGNORE
|
||||||
|
|
@ -1537,6 +1616,9 @@ You can set a ``NOT NULL DEFAULT 'x'`` constraint on the new column using ``not_
|
||||||
|
|
||||||
db.table("dogs").add_column("friends_count", int, not_null_default=0)
|
db.table("dogs").add_column("friends_count", int, not_null_default=0)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils add-column <cli_add_column>`
|
||||||
|
|
||||||
.. _python_api_add_column_alter:
|
.. _python_api_add_column_alter:
|
||||||
|
|
||||||
Adding columns automatically on insert/update
|
Adding columns automatically on insert/update
|
||||||
|
|
@ -1560,6 +1642,9 @@ You can insert or update data that includes new columns and have the table autom
|
||||||
new_table = db.table("new_table", alter=True)
|
new_table = db.table("new_table", alter=True)
|
||||||
new_table.insert({"name": "Gareth", "age": 32, "shoe_size": 11})
|
new_table.insert({"name": "Gareth", "age": 32, "shoe_size": 11})
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils insert --alter <cli_add_column_alter>`
|
||||||
|
|
||||||
.. _python_api_add_foreign_key:
|
.. _python_api_add_foreign_key:
|
||||||
|
|
||||||
Adding foreign key constraints
|
Adding foreign key constraints
|
||||||
|
|
@ -1618,6 +1703,9 @@ Use ``on_delete=`` and ``on_update=`` to specify ``ON DELETE`` and ``ON UPDATE``
|
||||||
|
|
||||||
This creates a foreign key with an ``ON DELETE CASCADE`` clause, so deleting an author will also delete their books (provided foreign key enforcement is enabled with ``PRAGMA foreign_keys = ON``). Valid actions are ``"SET NULL"``, ``"SET DEFAULT"``, ``"CASCADE"``, ``"RESTRICT"`` and the default ``"NO ACTION"``.
|
This creates a foreign key with an ``ON DELETE CASCADE`` clause, so deleting an author will also delete their books (provided foreign key enforcement is enabled with ``PRAGMA foreign_keys = ON``). Valid actions are ``"SET NULL"``, ``"SET DEFAULT"``, ``"CASCADE"``, ``"RESTRICT"`` and the default ``"NO ACTION"``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils add-foreign-key <cli_add_foreign_key>`
|
||||||
|
|
||||||
.. _python_api_add_foreign_keys:
|
.. _python_api_add_foreign_keys:
|
||||||
|
|
||||||
Adding multiple foreign key constraints at once
|
Adding multiple foreign key constraints at once
|
||||||
|
|
@ -1638,6 +1726,9 @@ This method runs the same checks as ``.add_foreign_keys()`` and will raise ``sql
|
||||||
|
|
||||||
Foreign keys that already exist are silently skipped, so repeated calls are idempotent - but only if they match exactly. Requesting a foreign key that exists with different ``ON DELETE``/``ON UPDATE`` actions raises ``AlterError``: use ``table.transform()`` to change the actions of an existing foreign key.
|
Foreign keys that already exist are silently skipped, so repeated calls are idempotent - but only if they match exactly. Requesting a foreign key that exists with different ``ON DELETE``/``ON UPDATE`` actions raises ``AlterError``: use ``table.transform()`` to change the actions of an existing foreign key.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils add-foreign-keys <cli_add_foreign_keys>`
|
||||||
|
|
||||||
.. _python_api_index_foreign_keys:
|
.. _python_api_index_foreign_keys:
|
||||||
|
|
||||||
Adding indexes for all foreign keys
|
Adding indexes for all foreign keys
|
||||||
|
|
@ -1651,6 +1742,9 @@ If you want to ensure that every foreign key column in your database has a corre
|
||||||
|
|
||||||
Compound foreign keys get a single composite index across their columns.
|
Compound foreign keys get a single composite index across their columns.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils index-foreign-keys <cli_index_foreign_keys>`
|
||||||
|
|
||||||
.. _python_api_drop:
|
.. _python_api_drop:
|
||||||
|
|
||||||
Dropping a table or view
|
Dropping a table or view
|
||||||
|
|
@ -1672,6 +1766,9 @@ Pass ``ignore=True`` if you want to ignore the error caused by the table or view
|
||||||
|
|
||||||
db.table("my_table").drop(ignore=True)
|
db.table("my_table").drop(ignore=True)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils drop-table <cli_drop_table>` and :ref:`sqlite-utils drop-view <cli_drop_view>`
|
||||||
|
|
||||||
.. _python_api_transform:
|
.. _python_api_transform:
|
||||||
|
|
||||||
Transforming a table
|
Transforming a table
|
||||||
|
|
@ -1700,6 +1797,9 @@ To keep the original table around instead of dropping it, pass the ``keep_table=
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils transform <cli_transform_table>`
|
||||||
|
|
||||||
.. _python_api_transform_alter_column_types:
|
.. _python_api_transform_alter_column_types:
|
||||||
|
|
||||||
Altering column types
|
Altering column types
|
||||||
|
|
@ -1714,6 +1814,29 @@ To alter the type of a column, use the ``types=`` argument:
|
||||||
|
|
||||||
See :ref:`python_api_add_column` for a list of available types.
|
See :ref:`python_api_add_column` for a list of available types.
|
||||||
|
|
||||||
|
.. _python_api_transform_strict:
|
||||||
|
|
||||||
|
Changing strict mode
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The optional ``strict=`` parameter can change whether a table uses `SQLite STRICT mode <https://www.sqlite.org/stricttables.html>`__. Pass ``strict=True`` to convert a regular table to a strict table:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
table.transform(strict=True)
|
||||||
|
|
||||||
|
Pass ``strict=False`` to convert a strict table back to a regular non-strict table:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
table.transform(strict=False)
|
||||||
|
|
||||||
|
The default is ``strict=None``, which preserves the table's existing strict mode.
|
||||||
|
|
||||||
|
Passing ``strict=True`` raises ``sqlite_utils.db.TransformError`` if the available SQLite version does not support strict tables.
|
||||||
|
|
||||||
|
Converting to a strict table validates all existing rows as they are copied into the replacement table. If a value is incompatible with its declared column type, SQLite raises ``sqlite3.IntegrityError`` and the transformation is rolled back, leaving the original table and its data unchanged.
|
||||||
|
|
||||||
.. _python_api_transform_rename_columns:
|
.. _python_api_transform_rename_columns:
|
||||||
|
|
||||||
Renaming columns
|
Renaming columns
|
||||||
|
|
@ -1874,6 +1997,36 @@ If you want to do something more advanced, you can call the ``table.transform_sq
|
||||||
|
|
||||||
This method will return a list of SQL statements that should be executed to implement the change. You can then make modifications to that SQL - or add additional SQL statements - before executing it yourself.
|
This method will return a list of SQL statements that should be executed to implement the change. You can then make modifications to that SQL - or add additional SQL statements - before executing it yourself.
|
||||||
|
|
||||||
|
.. _python_api_transform_foreign_keys_transactions:
|
||||||
|
|
||||||
|
Foreign keys and transactions
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Because ``.transform()`` drops the old table, running it with ``PRAGMA foreign_keys`` enabled could fire ``ON DELETE`` actions on any tables that reference it - an inbound ``ON DELETE CASCADE`` foreign key would silently delete those referencing rows. To prevent this, ``.transform()`` turns ``PRAGMA foreign_keys`` off for the duration of the operation and restores it afterwards, running ``PRAGMA foreign_key_check`` before committing.
|
||||||
|
|
||||||
|
``PRAGMA foreign_keys`` cannot be changed inside a transaction, so this protection is impossible if you call ``.transform()`` while a transaction is already open - for example inside a ``with db.atomic():`` block or after ``db.begin()``. If ``PRAGMA foreign_keys`` is on and another table references the table being transformed with a destructive ``ON DELETE`` action - ``CASCADE``, ``SET NULL`` or ``SET DEFAULT`` - the method will refuse to run and raise a ``sqlite_utils.db.TransactionError``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sqlite_utils.db import TransactionError
|
||||||
|
|
||||||
|
try:
|
||||||
|
with db.atomic():
|
||||||
|
db["authors"].transform(types={"id": str})
|
||||||
|
except TransactionError as ex:
|
||||||
|
print("Could not transform in transaction:", ex)
|
||||||
|
|
||||||
|
To transform such a table either call ``.transform()`` outside of the transaction, or execute ``PRAGMA foreign_keys = off`` before opening it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
db.execute("PRAGMA foreign_keys = off")
|
||||||
|
with db.atomic():
|
||||||
|
db["authors"].transform(types={"id": str})
|
||||||
|
db.execute("PRAGMA foreign_keys = on")
|
||||||
|
|
||||||
|
Tables referenced by foreign keys without a destructive action (the default ``NO ACTION``, or ``RESTRICT``) can still be transformed inside a transaction - sqlite-utils uses ``PRAGMA defer_foreign_keys`` to postpone the foreign key checks until the transaction commits.
|
||||||
|
|
||||||
.. _python_api_extract:
|
.. _python_api_extract:
|
||||||
|
|
||||||
Extracting columns into a separate table
|
Extracting columns into a separate table
|
||||||
|
|
@ -2032,6 +2185,9 @@ This produces a lookup table like so:
|
||||||
|
|
||||||
Rows where every extracted column is ``null`` are not extracted: no record is created for them in the lookup table and their foreign key column is left as ``null``. When extracting multiple columns, rows where at least one of the extracted columns has a value will be extracted as usual.
|
Rows where every extracted column is ``null`` are not extracted: no record is created for them in the lookup table and their foreign key column is left as ``null``. When extracting multiple columns, rows where at least one of the extracted columns has a value will be extracted as usual.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils extract <cli_extract>`
|
||||||
|
|
||||||
.. _python_api_hash:
|
.. _python_api_hash:
|
||||||
|
|
||||||
Setting an ID based on the hash of the row contents
|
Setting an ID based on the hash of the row contents
|
||||||
|
|
@ -2093,6 +2249,9 @@ You can pass ``ignore=True`` to silently ignore an existing view and do nothing,
|
||||||
select * from dogs where is_good_dog = 1
|
select * from dogs where is_good_dog = 1
|
||||||
""", replace=True)
|
""", replace=True)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils create-view <cli_create_view>`
|
||||||
|
|
||||||
Storing JSON
|
Storing JSON
|
||||||
============
|
============
|
||||||
|
|
||||||
|
|
@ -2211,6 +2370,9 @@ If you are using ``pysqlite3`` the underlying method may be missing. If you inst
|
||||||
|
|
||||||
pip install sqlite-dump
|
pip install sqlite-dump
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils dump <cli_dump>`
|
||||||
|
|
||||||
.. _python_api_introspection:
|
.. _python_api_introspection:
|
||||||
|
|
||||||
Introspecting tables and views
|
Introspecting tables and views
|
||||||
|
|
@ -2410,6 +2572,9 @@ The ``.indexes`` property returns all indexes created for a table, as a list of
|
||||||
Index(seq=4, name='"Street_Tree_List_qCaretaker"', unique=0, origin='c', partial=0, columns=['qCaretaker']),
|
Index(seq=4, name='"Street_Tree_List_qCaretaker"', unique=0, origin='c', partial=0, columns=['qCaretaker']),
|
||||||
Index(seq=5, name='"Street_Tree_List_PlantType"', unique=0, origin='c', partial=0, columns=['PlantType'])]
|
Index(seq=5, name='"Street_Tree_List_PlantType"', unique=0, origin='c', partial=0, columns=['PlantType'])]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils indexes <cli_indexes>`
|
||||||
|
|
||||||
.. _python_api_introspection_xindexes:
|
.. _python_api_introspection_xindexes:
|
||||||
|
|
||||||
.xindexes
|
.xindexes
|
||||||
|
|
@ -2453,6 +2618,9 @@ The ``.triggers`` property lists database triggers. It can be used on both datab
|
||||||
>>> db.triggers
|
>>> db.triggers
|
||||||
... similar output to db.table("authors").triggers
|
... similar output to db.table("authors").triggers
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils triggers <cli_triggers>`
|
||||||
|
|
||||||
.. _python_api_introspection_triggers_dict:
|
.. _python_api_introspection_triggers_dict:
|
||||||
|
|
||||||
.triggers_dict
|
.triggers_dict
|
||||||
|
|
@ -2601,6 +2769,9 @@ To remove the FTS tables and triggers you created, use the ``disable_fts()`` tab
|
||||||
|
|
||||||
db.table("dogs").disable_fts()
|
db.table("dogs").disable_fts()
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils enable-fts <cli_fts>`
|
||||||
|
|
||||||
.. _python_api_quote_fts:
|
.. _python_api_quote_fts:
|
||||||
|
|
||||||
Quoting characters for use in search
|
Quoting characters for use in search
|
||||||
|
|
@ -2665,6 +2836,9 @@ To return just the title and published columns for three matches for ``"dog"`` w
|
||||||
):
|
):
|
||||||
print(article)
|
print(article)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils search <cli_search>`
|
||||||
|
|
||||||
.. _python_api_fts_search_sql:
|
.. _python_api_fts_search_sql:
|
||||||
|
|
||||||
Building SQL queries with table.search_sql()
|
Building SQL queries with table.search_sql()
|
||||||
|
|
@ -2749,6 +2923,9 @@ This runs the following SQL::
|
||||||
|
|
||||||
INSERT INTO dogs_fts (dogs_fts) VALUES ("rebuild");
|
INSERT INTO dogs_fts (dogs_fts) VALUES ("rebuild");
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils rebuild-fts <cli_fts>`
|
||||||
|
|
||||||
.. _python_api_fts_optimize:
|
.. _python_api_fts_optimize:
|
||||||
|
|
||||||
Optimizing a full-text search table
|
Optimizing a full-text search table
|
||||||
|
|
@ -2764,6 +2941,9 @@ This runs the following SQL::
|
||||||
|
|
||||||
INSERT INTO dogs_fts (dogs_fts) VALUES ("optimize");
|
INSERT INTO dogs_fts (dogs_fts) VALUES ("optimize");
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils optimize <cli_optimize>`
|
||||||
|
|
||||||
.. _python_api_cached_table_counts:
|
.. _python_api_cached_table_counts:
|
||||||
|
|
||||||
Cached table counts using triggers
|
Cached table counts using triggers
|
||||||
|
|
@ -2826,6 +3006,9 @@ If the ``_counts`` table ever becomes out-of-sync with the actual table counts y
|
||||||
|
|
||||||
db.reset_counts()
|
db.reset_counts()
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils enable-counts <cli_enable_counts>`
|
||||||
|
|
||||||
.. _python_api_create_index:
|
.. _python_api_create_index:
|
||||||
|
|
||||||
Creating indexes
|
Creating indexes
|
||||||
|
|
@ -2869,6 +3052,17 @@ Use ``if_not_exists=True`` to do nothing if an index with that name already exis
|
||||||
|
|
||||||
Pass ``analyze=True`` to run ``ANALYZE`` against the new index after creating it.
|
Pass ``analyze=True`` to run ``ANALYZE`` against the new index after creating it.
|
||||||
|
|
||||||
|
You can drop an index from a table using ``.drop_index(index_name)``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
db.table("dogs").drop_index("idx_dogs_name")
|
||||||
|
|
||||||
|
Use ``ignore=True`` to ignore the error if the index does not exist.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils create-index <cli_create_index>` and :ref:`sqlite-utils drop-index <cli_drop_index>`
|
||||||
|
|
||||||
.. _python_api_analyze:
|
.. _python_api_analyze:
|
||||||
|
|
||||||
Optimizing index usage with ANALYZE
|
Optimizing index usage with ANALYZE
|
||||||
|
|
@ -2896,6 +3090,9 @@ To run against all indexes attached to a specific table, you can either pass the
|
||||||
|
|
||||||
db.table("dogs").analyze()
|
db.table("dogs").analyze()
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils analyze <cli_analyze>`
|
||||||
|
|
||||||
.. _python_api_vacuum:
|
.. _python_api_vacuum:
|
||||||
|
|
||||||
Vacuum
|
Vacuum
|
||||||
|
|
@ -2907,6 +3104,9 @@ You can optimize your database by running VACUUM against it like so:
|
||||||
|
|
||||||
Database("my_database.db").vacuum()
|
Database("my_database.db").vacuum()
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils vacuum <cli_vacuum>`
|
||||||
|
|
||||||
.. _python_api_wal:
|
.. _python_api_wal:
|
||||||
|
|
||||||
WAL mode
|
WAL mode
|
||||||
|
|
@ -2934,6 +3134,9 @@ You can check the current journal mode for a database using the ``journal_mode``
|
||||||
|
|
||||||
This will usually be ``wal`` or ``delete`` (meaning WAL is disabled), but can have other values - see the `PRAGMA journal_mode <https://www.sqlite.org/pragma.html#pragma_journal_mode>`__ documentation.
|
This will usually be ``wal`` or ``delete`` (meaning WAL is disabled), but can have other values - see the `PRAGMA journal_mode <https://www.sqlite.org/pragma.html#pragma_journal_mode>`__ documentation.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils enable-wal and disable-wal <cli_wal>`
|
||||||
|
|
||||||
.. _python_api_suggest_column_types:
|
.. _python_api_suggest_column_types:
|
||||||
|
|
||||||
Suggesting column types
|
Suggesting column types
|
||||||
|
|
@ -3074,6 +3277,9 @@ You can cause ``sqlite3`` to return more useful errors, including the traceback
|
||||||
|
|
||||||
sqlite3.enable_callback_tracebacks(True)
|
sqlite3.enable_callback_tracebacks(True)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils query --functions <cli_query_functions>`
|
||||||
|
|
||||||
.. _python_api_quote:
|
.. _python_api_quote:
|
||||||
|
|
||||||
Quoting strings for use in SQL
|
Quoting strings for use in SQL
|
||||||
|
|
@ -3206,6 +3412,9 @@ Initialize SpatiaLite
|
||||||
.. automethod:: sqlite_utils.db.Database.init_spatialite
|
.. automethod:: sqlite_utils.db.Database.init_spatialite
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils create-database --init-spatialite <cli_create_database>`
|
||||||
|
|
||||||
.. _python_api_gis_find_spatialite:
|
.. _python_api_gis_find_spatialite:
|
||||||
|
|
||||||
Finding SpatiaLite
|
Finding SpatiaLite
|
||||||
|
|
@ -3221,6 +3430,9 @@ Adding geometry columns
|
||||||
.. automethod:: sqlite_utils.db.Table.add_geometry_column
|
.. automethod:: sqlite_utils.db.Table.add_geometry_column
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils add-geometry-column <cli_spatialite>`
|
||||||
|
|
||||||
.. _python_api_gis_create_spatial_index:
|
.. _python_api_gis_create_spatial_index:
|
||||||
|
|
||||||
Creating a spatial index
|
Creating a spatial index
|
||||||
|
|
@ -3228,3 +3440,6 @@ Creating a spatial index
|
||||||
|
|
||||||
.. automethod:: sqlite_utils.db.Table.create_spatial_index
|
.. automethod:: sqlite_utils.db.Table.create_spatial_index
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the CLI: :ref:`sqlite-utils create-spatial-index <cli_spatialite_indexes>`
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "sqlite-utils"
|
name = "sqlite-utils"
|
||||||
version = "4.0rc4"
|
version = "4.1.1"
|
||||||
description = "CLI tool and Python library for manipulating SQLite databases"
|
description = "CLI tool and Python library for manipulating SQLite databases"
|
||||||
readme = { file = "README.md", content-type = "text/markdown" }
|
readme = { file = "README.md", content-type = "text/markdown" }
|
||||||
authors = [
|
authors = [
|
||||||
|
|
@ -79,7 +79,14 @@ build-backend = "setuptools.build_meta"
|
||||||
max-line-length = 160
|
max-line-length = 160
|
||||||
# Black compatibility, E203 whitespace before ':':
|
# Black compatibility, E203 whitespace before ':':
|
||||||
extend-ignore = ["E203"]
|
extend-ignore = ["E203"]
|
||||||
extend-exclude = [".venv", "build", "dist", "docs", "sqlite_utils.egg-info"]
|
extend-exclude = [
|
||||||
|
".venv",
|
||||||
|
".claude",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"docs",
|
||||||
|
"sqlite_utils.egg-info",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
sqlite_utils = ["py.typed"]
|
sqlite_utils = ["py.typed"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from .utils import suggest_column_types
|
|
||||||
from .hookspecs import hookimpl
|
|
||||||
from .hookspecs import hookspec
|
|
||||||
from .db import Database
|
from .db import Database
|
||||||
|
from .hookspecs import hookimpl, hookspec
|
||||||
from .migrations import Migrations
|
from .migrations import Migrations
|
||||||
|
from .utils import suggest_column_types
|
||||||
|
|
||||||
__all__ = ["Database", "Migrations", "suggest_column_types", "hookimpl", "hookspec"]
|
__all__ = ["Database", "Migrations", "hookimpl", "hookspec", "suggest_column_types"]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
1131
sqlite_utils/db.py
1131
sqlite_utils/db.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,7 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from pluggy import HookimplMarker
|
from pluggy import HookimplMarker, HookspecMarker
|
||||||
from pluggy import HookspecMarker
|
|
||||||
|
|
||||||
hookspec = HookspecMarker("sqlite_utils")
|
hookspec = HookspecMarker("sqlite_utils")
|
||||||
hookimpl = HookimplMarker("sqlite_utils")
|
hookimpl = HookimplMarker("sqlite_utils")
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
from collections.abc import Iterable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Callable, cast, TYPE_CHECKING
|
from collections.abc import Callable, Iterable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Protocol, TypeVar, cast
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlite_utils.db import Database, Table
|
from sqlite_utils.db import Database, Table
|
||||||
|
|
||||||
|
|
||||||
|
class _MigrationFunction(Protocol):
|
||||||
|
__name__: str
|
||||||
|
|
||||||
|
def __call__(self, db: "Database", /) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
_MigrationFunctionT = TypeVar("_MigrationFunctionT", bound=_MigrationFunction)
|
||||||
|
|
||||||
|
|
||||||
class Migrations:
|
class Migrations:
|
||||||
migrations_table = "_sqlite_migrations"
|
migrations_table = "_sqlite_migrations"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _Migration:
|
class _Migration:
|
||||||
name: str
|
name: str
|
||||||
fn: Callable
|
fn: _MigrationFunction
|
||||||
transactional: bool = True
|
transactional: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -32,7 +41,7 @@ class Migrations:
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self, *, name: str | None = None, transactional: bool = True
|
self, *, name: str | None = None, transactional: bool = True
|
||||||
) -> Callable:
|
) -> Callable[[_MigrationFunctionT], _MigrationFunctionT]:
|
||||||
"""
|
"""
|
||||||
:param name: The name to use for this migration - if not provided,
|
:param name: The name to use for this migration - if not provided,
|
||||||
the name of the function will be used.
|
the name of the function will be used.
|
||||||
|
|
@ -43,13 +52,11 @@ class Migrations:
|
||||||
example those that execute ``VACUUM``.
|
example those that execute ``VACUUM``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def inner(func: Callable) -> Callable:
|
def inner(func: _MigrationFunctionT) -> _MigrationFunctionT:
|
||||||
migration_name = name or getattr(func, "__name__")
|
migration_name = name or func.__name__
|
||||||
if any(m.name == migration_name for m in self._migrations):
|
if any(m.name == migration_name for m in self._migrations):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Migration '{}' is already registered in set '{}'".format(
|
f"Migration '{migration_name}' is already registered in set '{self.name}'"
|
||||||
migration_name, self.name
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self._migrations.append(
|
self._migrations.append(
|
||||||
self._Migration(migration_name, func, transactional)
|
self._Migration(migration_name, func, transactional)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Dict, List, Union
|
import sys
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
import sys
|
|
||||||
from . import hookspecs
|
from . import hookspecs
|
||||||
|
|
||||||
pm: pluggy.PluginManager = pluggy.PluginManager("sqlite_utils")
|
pm: pluggy.PluginManager = pluggy.PluginManager("sqlite_utils")
|
||||||
|
|
@ -17,13 +17,13 @@ def ensure_plugins_loaded() -> None:
|
||||||
_plugins_loaded = True
|
_plugins_loaded = True
|
||||||
|
|
||||||
|
|
||||||
def get_plugins() -> List[Dict[str, Union[str, List[str]]]]:
|
def get_plugins() -> list[dict[str, str | list[str]]]:
|
||||||
ensure_plugins_loaded()
|
ensure_plugins_loaded()
|
||||||
plugins: List[Dict[str, Union[str, List[str]]]] = []
|
plugins: list[dict[str, str | list[str]]] = []
|
||||||
plugin_to_distinfo = dict(pm.list_plugin_distinfo())
|
plugin_to_distinfo = dict(pm.list_plugin_distinfo())
|
||||||
for plugin in pm.get_plugins():
|
for plugin in pm.get_plugins():
|
||||||
hookcallers = pm.get_hookcallers(plugin) or []
|
hookcallers = pm.get_hookcallers(plugin) or []
|
||||||
plugin_info: Dict[str, Union[str, List[str]]] = {
|
plugin_info: dict[str, str | list[str]] = {
|
||||||
"name": plugin.__name__,
|
"name": plugin.__name__,
|
||||||
"hooks": [h.name for h in hookcallers],
|
"hooks": [h.name for h in hookcallers],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Optional
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
import json
|
|
||||||
|
|
||||||
IGNORE: object = object()
|
IGNORE: object = object()
|
||||||
SET_NULL: object = object()
|
SET_NULL: object = object()
|
||||||
|
|
@ -13,8 +13,8 @@ def parsedate(
|
||||||
value: str,
|
value: str,
|
||||||
dayfirst: bool = False,
|
dayfirst: bool = False,
|
||||||
yearfirst: bool = False,
|
yearfirst: bool = False,
|
||||||
errors: Optional[object] = None,
|
errors: object | None = None,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Parse a date and convert it to ISO date format: yyyy-mm-dd
|
Parse a date and convert it to ISO date format: yyyy-mm-dd
|
||||||
\b
|
\b
|
||||||
|
|
@ -44,8 +44,8 @@ def parsedatetime(
|
||||||
value: str,
|
value: str,
|
||||||
dayfirst: bool = False,
|
dayfirst: bool = False,
|
||||||
yearfirst: bool = False,
|
yearfirst: bool = False,
|
||||||
errors: Optional[object] = None,
|
errors: object | None = None,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
|
Parse a datetime and convert it to ISO datetime format: yyyy-mm-ddTHH:MM:SS
|
||||||
\b
|
\b
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,11 @@ import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable, Generator, Iterable, Iterator
|
||||||
from typing import (
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
BinaryIO,
|
BinaryIO,
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Generator,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
TYPE_CHECKING,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
|
|
@ -33,8 +24,8 @@ import click
|
||||||
from . import recipes
|
from . import recipes
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import sqlite3 # noqa: F401
|
import sqlite3
|
||||||
from sqlite3 import dbapi2 # noqa: F401
|
from sqlite3 import dbapi2
|
||||||
|
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
else:
|
else:
|
||||||
|
|
@ -44,7 +35,7 @@ else:
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import sqlite3 # noqa: F401
|
import sqlite3 # noqa: F401
|
||||||
from sqlite3 import dbapi2 # noqa: F401
|
from sqlite3 import dbapi2
|
||||||
|
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
|
|
||||||
|
|
@ -61,8 +52,8 @@ SPATIALITE_PATHS = (
|
||||||
ORIGINAL_CSV_FIELD_SIZE_LIMIT = csv.field_size_limit()
|
ORIGINAL_CSV_FIELD_SIZE_LIMIT = csv.field_size_limit()
|
||||||
|
|
||||||
# Type alias for row dictionaries - values can be various SQLite-compatible types
|
# Type alias for row dictionaries - values can be various SQLite-compatible types
|
||||||
RowValue = Union[None, int, float, str, bytes, bool, List[str]]
|
RowValue = None | int | float | str | bytes | bool | list[str]
|
||||||
Row = Dict[str, RowValue]
|
Row = dict[str, RowValue]
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
@ -103,7 +94,7 @@ def maximize_csv_field_size_limit() -> None:
|
||||||
field_size_limit = int(field_size_limit / 10)
|
field_size_limit = int(field_size_limit / 10)
|
||||||
|
|
||||||
|
|
||||||
def find_spatialite() -> Optional[str]:
|
def find_spatialite() -> str | None:
|
||||||
"""
|
"""
|
||||||
The ``find_spatialite()`` function searches for the `SpatiaLite <https://www.gaia-gis.it/fossil/libspatialite/index>`__
|
The ``find_spatialite()`` function searches for the `SpatiaLite <https://www.gaia-gis.it/fossil/libspatialite/index>`__
|
||||||
SQLite extension in some common places. It returns a string path to the location, or ``None`` if SpatiaLite was not found.
|
SQLite extension in some common places. It returns a string path to the location, or ``None`` if SpatiaLite was not found.
|
||||||
|
|
@ -132,9 +123,9 @@ def find_spatialite() -> Optional[str]:
|
||||||
|
|
||||||
|
|
||||||
def suggest_column_types(
|
def suggest_column_types(
|
||||||
records: Iterable[Dict[str, Any]],
|
records: Iterable[dict[str, Any]],
|
||||||
) -> Dict[str, type]:
|
) -> dict[str, type]:
|
||||||
all_column_types: Dict[str, Set[type]] = {}
|
all_column_types: dict[str, set[type]] = {}
|
||||||
for record in records:
|
for record in records:
|
||||||
for key, value in record.items():
|
for key, value in record.items():
|
||||||
all_column_types.setdefault(key, set()).add(type(value))
|
all_column_types.setdefault(key, set()).add(type(value))
|
||||||
|
|
@ -142,9 +133,9 @@ def suggest_column_types(
|
||||||
|
|
||||||
|
|
||||||
def types_for_column_types(
|
def types_for_column_types(
|
||||||
all_column_types: Dict[str, Set[type]],
|
all_column_types: dict[str, set[type]],
|
||||||
) -> Dict[str, type]:
|
) -> dict[str, type]:
|
||||||
column_types: Dict[str, type] = {}
|
column_types: dict[str, type] = {}
|
||||||
for key, types in all_column_types.items():
|
for key, types in all_column_types.items():
|
||||||
# Ignore null values if at least one other type present:
|
# Ignore null values if at least one other type present:
|
||||||
if len(types) > 1:
|
if len(types) > 1:
|
||||||
|
|
@ -153,7 +144,7 @@ def types_for_column_types(
|
||||||
if {None.__class__} == types:
|
if {None.__class__} == types:
|
||||||
t = str
|
t = str
|
||||||
elif len(types) == 1:
|
elif len(types) == 1:
|
||||||
t = list(types)[0]
|
t = next(iter(types))
|
||||||
# But if it's a subclass of list / tuple / dict, use str
|
# But if it's a subclass of list / tuple / dict, use str
|
||||||
# instead as we will be storing it as JSON in the table
|
# instead as we will be storing it as JSON in the table
|
||||||
for superclass in (list, tuple, dict):
|
for superclass in (list, tuple, dict):
|
||||||
|
|
@ -190,7 +181,7 @@ def column_affinity(column_type: str) -> type:
|
||||||
return float
|
return float
|
||||||
|
|
||||||
|
|
||||||
def decode_base64_values(doc: Dict[str, Any]) -> Dict[str, Any]:
|
def decode_base64_values(doc: dict[str, Any]) -> dict[str, Any]:
|
||||||
# Looks for '{"$base64": true..., "encoded": ...}' values and decodes them
|
# Looks for '{"$base64": true..., "encoded": ...}' values and decodes them
|
||||||
to_fix = [
|
to_fix = [
|
||||||
k
|
k
|
||||||
|
|
@ -263,9 +254,9 @@ class RowError(Exception):
|
||||||
|
|
||||||
|
|
||||||
def _extra_key_strategy(
|
def _extra_key_strategy(
|
||||||
reader: Iterable[Dict[Optional[str], object]],
|
reader: Iterable[dict[str | None, object]],
|
||||||
ignore_extras: Optional[bool] = False,
|
ignore_extras: bool | None = False,
|
||||||
extras_key: Optional[str] = None,
|
extras_key: str | None = None,
|
||||||
) -> Iterable[Row]:
|
) -> Iterable[Row]:
|
||||||
# Logic for handling CSV rows with more values than there are headings
|
# Logic for handling CSV rows with more values than there are headings
|
||||||
for row in reader:
|
for row in reader:
|
||||||
|
|
@ -279,9 +270,7 @@ def _extra_key_strategy(
|
||||||
yield cast(Row, row)
|
yield cast(Row, row)
|
||||||
elif not extras_key:
|
elif not extras_key:
|
||||||
extras = row.pop(None)
|
extras = row.pop(None)
|
||||||
raise RowError(
|
raise RowError(f"Row {row} contained these extra values: {extras}")
|
||||||
"Row {} contained these extra values: {}".format(row, extras)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
extras_value = row.pop(None)
|
extras_value = row.pop(None)
|
||||||
row_out = cast(Row, row)
|
row_out = cast(Row, row)
|
||||||
|
|
@ -291,12 +280,12 @@ def _extra_key_strategy(
|
||||||
|
|
||||||
def rows_from_file(
|
def rows_from_file(
|
||||||
fp: BinaryIO,
|
fp: BinaryIO,
|
||||||
format: Optional[Format] = None,
|
format: Format | None = None,
|
||||||
dialect: Optional[Type[csv.Dialect]] = None,
|
dialect: type[csv.Dialect] | None = None,
|
||||||
encoding: Optional[str] = None,
|
encoding: str | None = None,
|
||||||
ignore_extras: Optional[bool] = False,
|
ignore_extras: bool | None = False,
|
||||||
extras_key: Optional[str] = None,
|
extras_key: str | None = None,
|
||||||
) -> Tuple[Iterable[Row], Format]:
|
) -> tuple[Iterable[Row], Format]:
|
||||||
"""
|
"""
|
||||||
Load a sequence of dictionaries from a file-like object containing one of four different formats.
|
Load a sequence of dictionaries from a file-like object containing one of four different formats.
|
||||||
|
|
||||||
|
|
@ -363,7 +352,7 @@ def rows_from_file(
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
_extra_key_strategy(
|
_extra_key_strategy(
|
||||||
cast(Iterable[Dict[Optional[str], object]], rows),
|
cast(Iterable[dict[str | None, object]], rows),
|
||||||
ignore_extras,
|
ignore_extras,
|
||||||
extras_key,
|
extras_key,
|
||||||
),
|
),
|
||||||
|
|
@ -379,7 +368,7 @@ def rows_from_file(
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"rows_from_file() requires a file-like object that supports peek(), such as io.BytesIO"
|
"rows_from_file() requires a file-like object that supports peek(), such as io.BytesIO"
|
||||||
)
|
)
|
||||||
if first_bytes.startswith(b"[") or first_bytes.startswith(b"{"):
|
if first_bytes.startswith((b"[", b"{")):
|
||||||
# TODO: Detect newline-JSON
|
# TODO: Detect newline-JSON
|
||||||
return rows_from_file(buffered, format=Format.JSON)
|
return rows_from_file(buffered, format=Format.JSON)
|
||||||
else:
|
else:
|
||||||
|
|
@ -393,7 +382,7 @@ def rows_from_file(
|
||||||
detected_format = Format.TSV if dialect.delimiter == "\t" else Format.CSV
|
detected_format = Format.TSV if dialect.delimiter == "\t" else Format.CSV
|
||||||
return (
|
return (
|
||||||
_extra_key_strategy(
|
_extra_key_strategy(
|
||||||
cast(Iterable[Dict[Optional[str], object]], rows),
|
cast(Iterable[dict[str | None, object]], rows),
|
||||||
ignore_extras,
|
ignore_extras,
|
||||||
extras_key,
|
extras_key,
|
||||||
),
|
),
|
||||||
|
|
@ -425,9 +414,9 @@ class TypeTracker:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.trackers: Dict[str, "ValueTracker"] = {}
|
self.trackers: dict[str, ValueTracker] = {}
|
||||||
|
|
||||||
def wrap(self, iterator: Iterable[Dict[str, Any]]) -> Iterable[Dict[str, Any]]:
|
def wrap(self, iterator: Iterable[dict[str, Any]]) -> Iterable[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Use this to loop through an existing iterator, tracking the column types
|
Use this to loop through an existing iterator, tracking the column types
|
||||||
as part of the iteration.
|
as part of the iteration.
|
||||||
|
|
@ -441,7 +430,7 @@ class TypeTracker:
|
||||||
yield row
|
yield row
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def types(self) -> Dict[str, str]:
|
def types(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
A dictionary mapping column names to their detected types. This can be passed
|
A dictionary mapping column names to their detected types. This can be passed
|
||||||
to the ``db[table_name].transform(types=tracker.types)`` method.
|
to the ``db[table_name].transform(types=tracker.types)`` method.
|
||||||
|
|
@ -450,17 +439,15 @@ class TypeTracker:
|
||||||
|
|
||||||
|
|
||||||
class ValueTracker:
|
class ValueTracker:
|
||||||
couldbe: Dict[str, Callable[[object], bool]]
|
couldbe: dict[str, Callable[[object], bool]]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.couldbe = {key: getattr(self, "test_" + key) for key in self.get_tests()}
|
self.couldbe = {key: getattr(self, "test_" + key) for key in self.get_tests()}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_tests(cls) -> List[str]:
|
def get_tests(cls) -> list[str]:
|
||||||
return [
|
return [
|
||||||
key.split("test_")[-1]
|
key.split("test_")[-1] for key in cls.__dict__ if key.startswith("test_")
|
||||||
for key in cls.__dict__.keys()
|
|
||||||
if key.startswith("test_")
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_integer(self, value: object) -> bool:
|
def test_integer(self, value: object) -> bool:
|
||||||
|
|
@ -492,7 +479,7 @@ class ValueTracker:
|
||||||
def evaluate(self, value: object) -> None:
|
def evaluate(self, value: object) -> None:
|
||||||
if not value or not self.couldbe:
|
if not value or not self.couldbe:
|
||||||
return
|
return
|
||||||
not_these: List[str] = []
|
not_these: list[str] = []
|
||||||
for name, test in self.couldbe.items():
|
for name, test in self.couldbe.items():
|
||||||
if not test(value):
|
if not test(value):
|
||||||
not_these.append(name)
|
not_these.append(name)
|
||||||
|
|
@ -524,14 +511,14 @@ def progressbar(*args: Iterable[T], **kwargs: Any) -> Generator[Any, None, None]
|
||||||
def _compile_code(
|
def _compile_code(
|
||||||
code: str, imports: Iterable[str], variable: str = "value"
|
code: str, imports: Iterable[str], variable: str = "value"
|
||||||
) -> Callable[..., Any]:
|
) -> Callable[..., Any]:
|
||||||
globals_dict: Dict[str, Any] = {"r": recipes, "recipes": recipes}
|
globals_dict: dict[str, Any] = {"r": recipes, "recipes": recipes}
|
||||||
# Handle imports first so they're available for all approaches
|
# Handle imports first so they're available for all approaches
|
||||||
for import_ in imports:
|
for import_ in imports:
|
||||||
globals_dict[import_.split(".")[0]] = __import__(import_)
|
globals_dict[import_.split(".")[0]] = __import__(import_)
|
||||||
|
|
||||||
# If user defined a convert() function, return that
|
# If user defined a convert() function, return that
|
||||||
try:
|
try:
|
||||||
exec(code, globals_dict)
|
exec(code, globals_dict) # noqa: S102
|
||||||
return cast(Callable[..., object], globals_dict["convert"])
|
return cast(Callable[..., object], globals_dict["convert"])
|
||||||
except (AttributeError, SyntaxError, NameError, KeyError, TypeError):
|
except (AttributeError, SyntaxError, NameError, KeyError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
@ -542,20 +529,20 @@ def _compile_code(
|
||||||
fn = eval(code, globals_dict)
|
fn = eval(code, globals_dict)
|
||||||
if callable(fn):
|
if callable(fn):
|
||||||
return cast(Callable[..., object], fn)
|
return cast(Callable[..., object], fn)
|
||||||
except Exception:
|
except Exception: # noqa: BLE001, S110
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Try compiling their code as a function instead
|
# Try compiling their code as a function instead
|
||||||
body_variants = [code]
|
body_variants = [code]
|
||||||
# If single line and no 'return', try adding the return
|
# If single line and no 'return', try adding the return
|
||||||
if "\n" not in code and not code.strip().startswith("return "):
|
if "\n" not in code and not code.strip().startswith("return "):
|
||||||
body_variants.insert(0, "return {}".format(code))
|
body_variants.insert(0, f"return {code}")
|
||||||
|
|
||||||
code_o = None
|
code_o = None
|
||||||
for variant in body_variants:
|
for variant in body_variants:
|
||||||
new_code = ["def fn({}):".format(variable)]
|
new_code = [f"def fn({variable}):"]
|
||||||
for line in variant.split("\n"):
|
for line in variant.split("\n"):
|
||||||
new_code.append(" {}".format(line))
|
new_code.append(f" {line}")
|
||||||
try:
|
try:
|
||||||
code_o = compile("\n".join(new_code), "<string>", "exec")
|
code_o = compile("\n".join(new_code), "<string>", "exec")
|
||||||
break
|
break
|
||||||
|
|
@ -566,7 +553,7 @@ def _compile_code(
|
||||||
if code_o is None:
|
if code_o is None:
|
||||||
raise SyntaxError("Could not compile code")
|
raise SyntaxError("Could not compile code")
|
||||||
|
|
||||||
exec(code_o, globals_dict)
|
exec(code_o, globals_dict) # noqa: S102
|
||||||
return cast(Callable[..., object], globals_dict["fn"])
|
return cast(Callable[..., object], globals_dict["fn"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -582,7 +569,7 @@ def chunks(sequence: Iterable[T], size: int) -> Iterable[Iterable[T]]:
|
||||||
yield itertools.chain([item], itertools.islice(iterator, size - 1))
|
yield itertools.chain([item], itertools.islice(iterator, size - 1))
|
||||||
|
|
||||||
|
|
||||||
def hash_record(record: Dict[str, Any], keys: Optional[Iterable[str]] = None) -> str:
|
def hash_record(record: dict[str, Any], keys: Iterable[str] | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
``record`` should be a Python dictionary. Returns a sha1 hash of the
|
``record`` should be a Python dictionary. Returns a sha1 hash of the
|
||||||
keys and values in that record.
|
keys and values in that record.
|
||||||
|
|
@ -603,7 +590,7 @@ def hash_record(record: Dict[str, Any], keys: Optional[Iterable[str]] = None) ->
|
||||||
:param record: Record to generate a hash for
|
:param record: Record to generate a hash for
|
||||||
:param keys: Subset of keys to use for that hash
|
:param keys: Subset of keys to use for that hash
|
||||||
"""
|
"""
|
||||||
to_hash: Dict[str, Any] = record
|
to_hash: dict[str, Any] = record
|
||||||
if keys is not None:
|
if keys is not None:
|
||||||
to_hash = {key: record[key] for key in keys}
|
to_hash = {key: record[key] for key in keys}
|
||||||
return hashlib.sha1(
|
return hashlib.sha1(
|
||||||
|
|
@ -613,7 +600,7 @@ def hash_record(record: Dict[str, Any], keys: Optional[Iterable[str]] = None) ->
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def dedupe_keys(keys: Iterable[str]) -> List[str]:
|
def dedupe_keys(keys: Iterable[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Rename duplicates in a list of column names so every name is unique,
|
Rename duplicates in a list of column names so every name is unique,
|
||||||
by appending ``_2``, ``_3``... to later occurrences - skipping any
|
by appending ``_2``, ``_3``... to later occurrences - skipping any
|
||||||
|
|
@ -636,7 +623,7 @@ def dedupe_keys(keys: Iterable[str]) -> List[str]:
|
||||||
new_key = key
|
new_key = key
|
||||||
suffix = 2
|
suffix = 2
|
||||||
while new_key in seen or new_key in taken:
|
while new_key in seen or new_key in taken:
|
||||||
new_key = "{}_{}".format(key, suffix)
|
new_key = f"{key}_{suffix}"
|
||||||
suffix += 1
|
suffix += 1
|
||||||
key = new_key
|
key = new_key
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
|
|
@ -644,7 +631,7 @@ def dedupe_keys(keys: Iterable[str]) -> List[str]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _flatten(d: Dict[str, Any]) -> Generator[Tuple[str, Any], None, None]:
|
def _flatten(d: dict[str, Any]) -> Generator[tuple[str, Any], None, None]:
|
||||||
for key, value in d.items():
|
for key, value in d.items():
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
for key2, value2 in _flatten(value):
|
for key2, value2 in _flatten(value):
|
||||||
|
|
@ -653,7 +640,7 @@ def _flatten(d: Dict[str, Any]) -> Generator[Tuple[str, Any], None, None]:
|
||||||
yield key, value
|
yield key, value
|
||||||
|
|
||||||
|
|
||||||
def flatten(row: Dict[str, Any]) -> Dict[str, Any]:
|
def flatten(row: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Turn a nested dict e.g. ``{"a": {"b": 1}}`` into a flat dict: ``{"a_b": 1}``
|
Turn a nested dict e.g. ``{"a": {"b": 1}}`` into a flat dict: ``{"a_b": 1}``
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
import pytest
|
|
||||||
|
|
||||||
CREATE_TABLES = """
|
CREATE_TABLES = """
|
||||||
create table Gosh (c1 text, c2 text, c3 text);
|
create table Gosh (c1 text, c2 text, c3 text);
|
||||||
|
|
@ -55,7 +56,7 @@ def close_all_databases():
|
||||||
for db in databases:
|
for db in databases:
|
||||||
try:
|
try:
|
||||||
db.close()
|
db.close()
|
||||||
except Exception:
|
except sqlite3.Error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
from sqlite_utils.db import Database, ColumnDetails
|
|
||||||
from sqlite_utils import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import pytest
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import cli
|
||||||
|
from sqlite_utils.db import ColumnDetails, Database
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def db_to_analyze(fresh_db):
|
def db_to_analyze(fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,13 @@ from sqlite_utils.utils import sqlite3
|
||||||
END;
|
END;
|
||||||
""",
|
""",
|
||||||
[
|
[
|
||||||
"CREATE TRIGGER t_ai AFTER INSERT ON t\n"
|
(
|
||||||
" BEGIN\n"
|
"CREATE TRIGGER t_ai AFTER INSERT ON t\n"
|
||||||
" UPDATE t SET value = 'a;b' WHERE id = new.id;\n"
|
" BEGIN\n"
|
||||||
" INSERT INTO log VALUES ('x;y');\n"
|
" UPDATE t SET value = 'a;b' WHERE id = new.id;\n"
|
||||||
" END;"
|
" INSERT INTO log VALUES ('x;y');\n"
|
||||||
|
" END;"
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -49,10 +51,9 @@ def test_atomic_commits(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_atomic_rolls_back(fresh_db):
|
def test_atomic_rolls_back(fresh_db):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
raise RuntimeError("boom")
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
assert not fresh_db["dogs"].exists()
|
assert not fresh_db["dogs"].exists()
|
||||||
|
|
||||||
|
|
@ -62,10 +63,9 @@ def test_nested_atomic_rolls_back_to_savepoint(fresh_db):
|
||||||
|
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"})
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"})
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
||||||
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
raise RuntimeError("boom")
|
||||||
raise RuntimeError("boom")
|
|
||||||
fresh_db["dogs"].insert({"id": 3, "name": "Marnie"})
|
fresh_db["dogs"].insert({"id": 3, "name": "Marnie"})
|
||||||
|
|
||||||
assert list(fresh_db["dogs"].rows) == [
|
assert list(fresh_db["dogs"].rows) == [
|
||||||
|
|
@ -75,20 +75,18 @@ def test_nested_atomic_rolls_back_to_savepoint(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_outer_atomic_rolls_back_released_savepoint(fresh_db):
|
def test_outer_atomic_rolls_back_released_savepoint(fresh_db):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
||||||
with fresh_db.atomic():
|
raise RuntimeError("boom")
|
||||||
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
assert not fresh_db["dogs"].exists()
|
assert not fresh_db["dogs"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_executescript_does_not_commit_open_atomic_block(fresh_db):
|
def test_executescript_does_not_commit_open_atomic_block(fresh_db):
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db.executescript("""
|
||||||
fresh_db.executescript("""
|
|
||||||
CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT);
|
CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT);
|
||||||
CREATE TRIGGER dogs_ai AFTER INSERT ON dogs
|
CREATE TRIGGER dogs_ai AFTER INSERT ON dogs
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|
@ -97,7 +95,7 @@ def test_executescript_does_not_commit_open_atomic_block(fresh_db):
|
||||||
-- This comment has a semicolon;
|
-- This comment has a semicolon;
|
||||||
INSERT INTO dogs VALUES (1, 'Cleo; the first');
|
INSERT INTO dogs VALUES (1, 'Cleo; the first');
|
||||||
""")
|
""")
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert not fresh_db["dogs"].exists()
|
assert not fresh_db["dogs"].exists()
|
||||||
|
|
||||||
|
|
@ -105,11 +103,10 @@ def test_executescript_does_not_commit_open_atomic_block(fresh_db):
|
||||||
def test_transform_does_not_commit_open_atomic_block(fresh_db):
|
def test_transform_does_not_commit_open_atomic_block(fresh_db):
|
||||||
fresh_db["dogs"].insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")
|
||||||
|
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes", "age": "6"})
|
||||||
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes", "age": "6"})
|
fresh_db["dogs"].transform(rename={"age": "dog_age"})
|
||||||
fresh_db["dogs"].transform(rename={"age": "dog_age"})
|
raise RuntimeError("boom")
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
fresh_db["dogs"].schema
|
fresh_db["dogs"].schema
|
||||||
|
|
@ -149,10 +146,9 @@ def test_transform_parent_table_with_foreign_keys_rolls_back(fresh_db):
|
||||||
foreign_keys={"author_id"},
|
foreign_keys={"author_id"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db["authors"].transform(rename={"name": "full_name"})
|
||||||
fresh_db["authors"].transform(rename={"name": "full_name"})
|
raise RuntimeError("boom")
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
fresh_db["authors"].schema
|
fresh_db["authors"].schema
|
||||||
|
|
@ -354,9 +350,11 @@ def test_atomic_preserves_error_from_transaction_destroying_trigger(fresh_db):
|
||||||
# with "cannot rollback - no transaction is active"
|
# with "cannot rollback - no transaction is active"
|
||||||
fresh_db.execute("create table t (id integer primary key, v text)")
|
fresh_db.execute("create table t (id integer primary key, v text)")
|
||||||
fresh_db.execute(TRIGGER_SQL)
|
fresh_db.execute(TRIGGER_SQL)
|
||||||
with pytest.raises(sqlite3.IntegrityError, match="trigger says no"):
|
with (
|
||||||
with fresh_db.atomic():
|
pytest.raises(sqlite3.IntegrityError, match="trigger says no"),
|
||||||
fresh_db.execute("insert into t (v) values ('bad')")
|
fresh_db.atomic(),
|
||||||
|
):
|
||||||
|
fresh_db.execute("insert into t (v) values ('bad')")
|
||||||
assert not fresh_db.conn.in_transaction
|
assert not fresh_db.conn.in_transaction
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -367,16 +365,17 @@ def test_nested_atomic_preserves_error_from_transaction_destroying_trigger(
|
||||||
# "no such savepoint" from ROLLBACK TO SAVEPOINT
|
# "no such savepoint" from ROLLBACK TO SAVEPOINT
|
||||||
fresh_db.execute("create table t (id integer primary key, v text)")
|
fresh_db.execute("create table t (id integer primary key, v text)")
|
||||||
fresh_db.execute(TRIGGER_SQL)
|
fresh_db.execute(TRIGGER_SQL)
|
||||||
with pytest.raises(sqlite3.IntegrityError, match="trigger says no"):
|
with (
|
||||||
with fresh_db.atomic():
|
pytest.raises(sqlite3.IntegrityError, match="trigger says no"),
|
||||||
with fresh_db.atomic():
|
fresh_db.atomic(),
|
||||||
fresh_db.execute("insert into t (v) values ('bad')")
|
fresh_db.atomic(),
|
||||||
|
):
|
||||||
|
fresh_db.execute("insert into t (v) values ('bad')")
|
||||||
assert not fresh_db.conn.in_transaction
|
assert not fresh_db.conn.in_transaction
|
||||||
|
|
||||||
|
|
||||||
def test_atomic_preserves_error_from_insert_or_rollback(fresh_db):
|
def test_atomic_preserves_error_from_insert_or_rollback(fresh_db):
|
||||||
fresh_db["t"].insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError), fresh_db.atomic():
|
||||||
with fresh_db.atomic():
|
fresh_db.execute("insert or rollback into t (id) values (1)")
|
||||||
fresh_db.execute("insert or rollback into t (id) values (1)")
|
|
||||||
assert not fresh_db.conn.in_transaction
|
assert not fresh_db.conn.in_transaction
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
from sqlite_utils import cli, Database
|
|
||||||
from sqlite_utils.db import Index, ForeignKey
|
|
||||||
from click.testing import CliRunner
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pytest
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
from sqlite_utils.db import ForeignKey, Index
|
||||||
|
|
||||||
|
|
||||||
def write_json(file_path, data):
|
def write_json(file_path, data):
|
||||||
|
|
@ -20,7 +23,7 @@ def _supports_pragma_function_list():
|
||||||
try:
|
try:
|
||||||
db.execute("select * from pragma_function_list()")
|
db.execute("select * from pragma_function_list()")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except sqlite3.DatabaseError:
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
@ -183,9 +186,9 @@ def test_output_table(db_path, options, expected):
|
||||||
db["rows"].insert_all(
|
db["rows"].insert_all(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"c1": "verb{}".format(i),
|
"c1": f"verb{i}",
|
||||||
"c2": "noun{}".format(i),
|
"c2": f"noun{i}",
|
||||||
"c3": "adjective{}".format(i),
|
"c3": f"adjective{i}",
|
||||||
}
|
}
|
||||||
for i in range(4)
|
for i in range(4)
|
||||||
]
|
]
|
||||||
|
|
@ -291,6 +294,24 @@ def test_create_index(db_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_index(db_path):
|
||||||
|
db = Database(db_path)
|
||||||
|
db["Gosh"].create_index(["c1"])
|
||||||
|
assert [index.name for index in db["Gosh"].indexes] == ["idx_Gosh_c1"]
|
||||||
|
result = CliRunner().invoke(cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert db["Gosh"].indexes == []
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1"])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "No index named idx_Gosh_c1" in result.output
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli, ["drop-index", db_path, "Gosh", "idx_Gosh_c1", "--ignore"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
def test_create_index_analyze(db_path):
|
def test_create_index_analyze(db_path):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert "sqlite_stat1" not in db.table_names()
|
assert "sqlite_stat1" not in db.table_names()
|
||||||
|
|
@ -659,9 +680,9 @@ def test_optimize(db_path, tables):
|
||||||
db[table].insert_all(
|
db[table].insert_all(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"c1": "verb{}".format(i),
|
"c1": f"verb{i}",
|
||||||
"c2": "noun{}".format(i),
|
"c2": f"noun{i}",
|
||||||
"c3": "adjective{}".format(i),
|
"c3": f"adjective{i}",
|
||||||
}
|
}
|
||||||
for i in range(10000)
|
for i in range(10000)
|
||||||
]
|
]
|
||||||
|
|
@ -685,9 +706,9 @@ def test_rebuild_fts_fixes_docsize_error(db_path):
|
||||||
db = Database(db_path, recursive_triggers=False)
|
db = Database(db_path, recursive_triggers=False)
|
||||||
records = [
|
records = [
|
||||||
{
|
{
|
||||||
"c1": "verb{}".format(i),
|
"c1": f"verb{i}",
|
||||||
"c2": "noun{}".format(i),
|
"c2": f"noun{i}",
|
||||||
"c3": "adjective{}".format(i),
|
"c3": f"adjective{i}",
|
||||||
}
|
}
|
||||||
for i in range(10000)
|
for i in range(10000)
|
||||||
]
|
]
|
||||||
|
|
@ -782,6 +803,25 @@ def test_query_json(db_path, sql, args, expected):
|
||||||
assert expected == result.output.strip()
|
assert expected == result.output.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_sql_from_stdin(db_path):
|
||||||
|
# https://github.com/simonw/sqlite-utils/issues/765
|
||||||
|
db = Database(db_path)
|
||||||
|
with db.conn:
|
||||||
|
db["dogs"].insert_all(
|
||||||
|
[
|
||||||
|
{"id": 1, "age": 4, "name": "Cleo"},
|
||||||
|
{"id": 2, "age": 2, "name": "Pancakes"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["query", db_path, "-"],
|
||||||
|
input="select name from dogs order by name",
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert json.loads(result.output) == [{"name": "Cleo"}, {"name": "Pancakes"}]
|
||||||
|
|
||||||
|
|
||||||
def test_query_json_empty(db_path):
|
def test_query_json_empty(db_path):
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
|
|
@ -981,16 +1021,14 @@ def test_query_json_binary(db_path):
|
||||||
"data": {
|
"data": {
|
||||||
"$base64": True,
|
"$base64": True,
|
||||||
"encoded": (
|
"encoded": (
|
||||||
(
|
"eJzt0c1xAyEMBeC7q1ABHleR3HxNAQrIjmb4M0gelx+RTY7p4N2WBYT0vmufUknH"
|
||||||
"eJzt0c1xAyEMBeC7q1ABHleR3HxNAQrIjmb4M0gelx+RTY7p4N2WBYT0vmufUknH"
|
"8kq5lz5pqRFXsTOl3pYkE/NJnHXoStruJEVjc0mOCyTqq/ZMJnXEZW1Js2ZvRm5U+"
|
||||||
"8kq5lz5pqRFXsTOl3pYkE/NJnHXoStruJEVjc0mOCyTqq/ZMJnXEZW1Js2ZvRm5U+"
|
"DPKk9hRWqjyvTFx0YfzhT6MpGmN2lR1fzxjyfVMD9dFrS+bnkleMpMam/ZGXgrX1I"
|
||||||
"DPKk9hRWqjyvTFx0YfzhT6MpGmN2lR1fzxjyfVMD9dFrS+bnkleMpMam/ZGXgrX1I"
|
"/K+5Au3S/9lNQRh0k4Gq/RUz8GiKfsQm+7JLsJ6fTo5JhVG00ZU76kZZkxePx49uI"
|
||||||
"/K+5Au3S/9lNQRh0k4Gq/RUz8GiKfsQm+7JLsJ6fTo5JhVG00ZU76kZZkxePx49uI"
|
"jnpNoJyYlWUsoaSl/CcVATje/Kxu13RANnrHweaH3V5Jh4jvGyKCnxJLiXPKhmW3f"
|
||||||
"jnpNoJyYlWUsoaSl/CcVATje/Kxu13RANnrHweaH3V5Jh4jvGyKCnxJLiXPKhmW3f"
|
"iCnG7Jql7RR3UvFo8jJ4z039dtOkTFmWzL1be9lt8A5II471m6vXy+l0BR/4wAc+8"
|
||||||
"iCnG7Jql7RR3UvFo8jJ4z039dtOkTFmWzL1be9lt8A5II471m6vXy+l0BR/4wAc+8"
|
"IEPfOADH/jABz7wgQ984AMf+MAHPvCBD3zgAx/4wAc+8IEPfOADH/jABz7wgQ984A"
|
||||||
"IEPfOADH/jABz7wgQ984AMf+MAHPvCBD3zgAx/4wAc+8IEPfOADH/jABz7wgQ984A"
|
"Mf+MAHPvCBD3zgAx/4wAc+8IEPfOADH/jABz7wgQ984PuP7xubBoN9"
|
||||||
"Mf+MAHPvCBD3zgAx/4wAc+8IEPfOADH/jABz7wgQ984PuP7xubBoN9"
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1216,20 +1254,38 @@ def test_upsert(db_path, tmpdir):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_pk_required(db_path, tmpdir):
|
def test_upsert_pk_inferred_from_existing_table(db_path, tmpdir):
|
||||||
json_path = str(tmpdir / "dogs.json")
|
json_path = str(tmpdir / "dogs.json")
|
||||||
|
db = Database(db_path)
|
||||||
insert_dogs = [
|
insert_dogs = [
|
||||||
{"id": 1, "name": "Cleo", "age": 4},
|
{"id": 1, "name": "Cleo", "age": 4},
|
||||||
{"id": 2, "name": "Nixie", "age": 4},
|
{"id": 2, "name": "Nixie", "age": 4},
|
||||||
]
|
]
|
||||||
write_json(json_path, insert_dogs)
|
write_json(json_path, insert_dogs)
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "dogs", json_path, "--pk", "id"],
|
||||||
|
catch_exceptions=False,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
|
||||||
|
write_json(
|
||||||
|
json_path,
|
||||||
|
[
|
||||||
|
{"id": 1, "age": 5},
|
||||||
|
{"id": 2, "age": 5},
|
||||||
|
],
|
||||||
|
)
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["upsert", db_path, "dogs", json_path],
|
["upsert", db_path, "dogs", json_path],
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 2
|
assert result.exit_code == 0, result.output
|
||||||
assert "Error: Missing option '--pk'" in result.output
|
assert list(db.query("select * from dogs order by id")) == [
|
||||||
|
{"id": 1, "name": "Cleo", "age": 5},
|
||||||
|
{"id": 2, "name": "Nixie", "age": 5},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_analyze(db_path, tmpdir):
|
def test_upsert_analyze(db_path, tmpdir):
|
||||||
|
|
@ -1884,6 +1940,64 @@ def test_transform_sql(db_path):
|
||||||
assert db["dogs"].schema == original_schema
|
assert db["dogs"].schema == original_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"initial_strict,args,expected_strict",
|
||||||
|
(
|
||||||
|
(False, [], False),
|
||||||
|
(True, [], True),
|
||||||
|
(False, ["--strict"], True),
|
||||||
|
(True, ["--no-strict"], False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_transform_strict_option(db_path, initial_strict, args, expected_strict):
|
||||||
|
db = Database(db_path)
|
||||||
|
if not db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
db["dogs"].create({"id": int}, strict=initial_strict)
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli.cli, ["transform", db_path, "dogs"] + args)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert db["dogs"].strict is expected_strict
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"initial_strict,flag,sql_is_strict",
|
||||||
|
(
|
||||||
|
(False, "--strict", True),
|
||||||
|
(True, "--no-strict", False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_transform_strict_option_sql(db_path, initial_strict, flag, sql_is_strict):
|
||||||
|
db = Database(db_path)
|
||||||
|
if not db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
db["dogs"].create({"id": int}, strict=initial_strict)
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli.cli, ["transform", db_path, "dogs", flag, "--sql"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert (") STRICT;" in result.output) is sql_is_strict
|
||||||
|
assert db["dogs"].strict is initial_strict
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_strict_option_with_invalid_data(db_path):
|
||||||
|
db = Database(db_path)
|
||||||
|
if not db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
dogs = db["dogs"]
|
||||||
|
dogs.create({"id": int})
|
||||||
|
dogs.insert({"id": "not-an-integer"})
|
||||||
|
|
||||||
|
result = CliRunner().invoke(cli.cli, ["transform", db_path, "dogs", "--strict"])
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert isinstance(result.exception, sqlite3.IntegrityError)
|
||||||
|
assert dogs.strict is False
|
||||||
|
assert list(dogs.rows) == [{"id": "not-an-integer"}]
|
||||||
|
assert not any(name.startswith("dogs_new_") for name in db.table_names())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"extra_args,expected_schema",
|
"extra_args,expected_schema",
|
||||||
(
|
(
|
||||||
|
|
@ -2000,11 +2114,13 @@ _common_other_schema = (
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
["--rename", "name", "name2"],
|
["--rename", "name", "name2"],
|
||||||
'CREATE TABLE "trees" (\n'
|
(
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
'CREATE TABLE "trees" (\n'
|
||||||
' "address" TEXT,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
' "address" TEXT,\n'
|
||||||
")",
|
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
||||||
|
")"
|
||||||
|
),
|
||||||
'CREATE TABLE "species" (\n "id" INTEGER PRIMARY KEY,\n "species" TEXT\n)',
|
'CREATE TABLE "species" (\n "id" INTEGER PRIMARY KEY,\n "species" TEXT\n)',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -2023,9 +2139,9 @@ def test_extract(db_path, args, expected_table_schema, expected_other_schema):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
schema = db["trees"].schema
|
schema = db["trees"].schema
|
||||||
assert schema == expected_table_schema
|
assert schema == expected_table_schema
|
||||||
other_schema = [t for t in db.tables if t.name not in ("trees", "Gosh", "Gosh2")][
|
other_schema = next(
|
||||||
0
|
t for t in db.tables if t.name not in ("trees", "Gosh", "Gosh2")
|
||||||
].schema
|
).schema
|
||||||
assert other_schema == expected_other_schema
|
assert other_schema == expected_other_schema
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2317,7 +2433,7 @@ def test_long_csv_column_value(tmpdir):
|
||||||
with open(csv_path, "w") as csv_file:
|
with open(csv_path, "w") as csv_file:
|
||||||
long_string = "a" * 131073
|
long_string = "a" * 131073
|
||||||
csv_file.write("id,text\n")
|
csv_file.write("id,text\n")
|
||||||
csv_file.write("1,{}\n".format(long_string))
|
csv_file.write(f"1,{long_string}\n")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert", db_path, "bigtable", csv_path, "--csv"],
|
["insert", db_path, "bigtable", csv_path, "--csv"],
|
||||||
|
|
@ -2343,8 +2459,8 @@ def test_import_no_headers(tmpdir, args, tsv):
|
||||||
csv_path = str(tmpdir / "test.csv")
|
csv_path = str(tmpdir / "test.csv")
|
||||||
with open(csv_path, "w") as csv_file:
|
with open(csv_path, "w") as csv_file:
|
||||||
sep = "\t" if tsv else ","
|
sep = "\t" if tsv else ","
|
||||||
csv_file.write("Cleo{sep}Dog{sep}5\n".format(sep=sep))
|
csv_file.write(f"Cleo{sep}Dog{sep}5\n")
|
||||||
csv_file.write("Tracy{sep}Spider{sep}7\n".format(sep=sep))
|
csv_file.write(f"Tracy{sep}Spider{sep}7\n")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert", db_path, "creatures", csv_path] + args + ["--no-detect-types"],
|
["insert", db_path, "creatures", csv_path] + args + ["--no-detect-types"],
|
||||||
|
|
@ -2576,7 +2692,9 @@ def test_integer_overflow_error(tmpdir):
|
||||||
def test_python_dash_m():
|
def test_python_dash_m():
|
||||||
"Tool can be run using python -m sqlite_utils"
|
"Tool can be run using python -m sqlite_utils"
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "sqlite_utils", "--help"], stdout=subprocess.PIPE
|
[sys.executable, "-m", "sqlite_utils", "--help"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
check=False,
|
||||||
)
|
)
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
assert b"Commands for interacting with a SQLite database" in result.stdout
|
assert b"Commands for interacting with a SQLite database" in result.stdout
|
||||||
|
|
@ -2716,14 +2834,14 @@ def test_load_extension(entrypoint, should_pass, should_fail):
|
||||||
for func in should_pass:
|
for func in should_pass:
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", "select {}()".format(func), "--load-extension", ext],
|
["memory", f"select {func}()", "--load-extension", ext],
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
for func in should_fail:
|
for func in should_fail:
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", "select {}()".format(func), "--load-extension", ext],
|
["memory", f"select {func}()", "--load-extension", ext],
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from click.testing import CliRunner
|
|
||||||
from sqlite_utils import cli, Database
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import pytest
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_db_and_path(tmpdir):
|
def test_db_and_path(tmpdir):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from click.testing import CliRunner
|
|
||||||
from sqlite_utils import cli
|
|
||||||
import sqlite_utils
|
|
||||||
import json
|
import json
|
||||||
import textwrap
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import textwrap
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
import sqlite_utils
|
||||||
|
from sqlite_utils import cli
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -50,7 +52,7 @@ def test_convert_code(fresh_db_and_path, code):
|
||||||
cli.cli, ["convert", db_path, "t", "text", code], catch_exceptions=False
|
cli.cli, ["convert", db_path, "t", "text", code], catch_exceptions=False
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
value = list(db["t"].rows)[0]["text"]
|
value = next(iter(db["t"].rows))["text"]
|
||||||
assert value == "Spooktober"
|
assert value == "Spooktober"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -442,7 +444,7 @@ def test_recipe_jsonsplit(tmpdir, delimiter):
|
||||||
)
|
)
|
||||||
code = "r.jsonsplit(value)"
|
code = "r.jsonsplit(value)"
|
||||||
if delimiter:
|
if delimiter:
|
||||||
code = 'recipes.jsonsplit(value, delimiter="{}")'.format(delimiter)
|
code = f'recipes.jsonsplit(value, delimiter="{delimiter}")'
|
||||||
args = ["convert", db_path, "example", "tags", code]
|
args = ["convert", db_path, "example", "tags", code]
|
||||||
result = CliRunner().invoke(cli.cli, args)
|
result = CliRunner().invoke(cli.cli, args)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -470,7 +472,7 @@ def test_recipe_jsonsplit_type(fresh_db_and_path, type, expected_array):
|
||||||
)
|
)
|
||||||
code = "r.jsonsplit(value)"
|
code = "r.jsonsplit(value)"
|
||||||
if type:
|
if type:
|
||||||
code = "recipes.jsonsplit(value, type={})".format(type)
|
code = f"recipes.jsonsplit(value, type={type})"
|
||||||
args = ["convert", db_path, "example", "records", code]
|
args = ["convert", db_path, "example", "records", code]
|
||||||
result = CliRunner().invoke(cli.cli, args)
|
result = CliRunner().invoke(cli.cli, args)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from sqlite_utils import cli, Database
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
|
||||||
|
|
||||||
def test_insert_simple(tmpdir):
|
def test_insert_simple(tmpdir):
|
||||||
json_path = str(tmpdir / "dog.json")
|
json_path = str(tmpdir / "dog.json")
|
||||||
|
|
@ -99,7 +101,7 @@ def test_insert_with_primary_keys(db_path, tmpdir, args, expected_pks):
|
||||||
|
|
||||||
def test_insert_multiple_with_primary_key(db_path, tmpdir):
|
def test_insert_multiple_with_primary_key(db_path, tmpdir):
|
||||||
json_path = str(tmpdir / "dogs.json")
|
json_path = str(tmpdir / "dogs.json")
|
||||||
dogs = [{"id": i, "name": "Cleo {}".format(i), "age": i + 3} for i in range(1, 21)]
|
dogs = [{"id": i, "name": f"Cleo {i}", "age": i + 3} for i in range(1, 21)]
|
||||||
with open(json_path, "w") as fp:
|
with open(json_path, "w") as fp:
|
||||||
fp.write(json.dumps(dogs))
|
fp.write(json.dumps(dogs))
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
@ -114,7 +116,7 @@ def test_insert_multiple_with_primary_key(db_path, tmpdir):
|
||||||
def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
|
def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
|
||||||
json_path = str(tmpdir / "dogs.json")
|
json_path = str(tmpdir / "dogs.json")
|
||||||
dogs = [
|
dogs = [
|
||||||
{"breed": "mixed", "id": i, "name": "Cleo {}".format(i), "age": i + 3}
|
{"breed": "mixed", "id": i, "name": f"Cleo {i}", "age": i + 3}
|
||||||
for i in range(1, 21)
|
for i in range(1, 21)
|
||||||
]
|
]
|
||||||
with open(json_path, "w") as fp:
|
with open(json_path, "w") as fp:
|
||||||
|
|
@ -140,8 +142,7 @@ def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
|
||||||
def test_insert_not_null_default(db_path, tmpdir):
|
def test_insert_not_null_default(db_path, tmpdir):
|
||||||
json_path = str(tmpdir / "dogs.json")
|
json_path = str(tmpdir / "dogs.json")
|
||||||
dogs = [
|
dogs = [
|
||||||
{"id": i, "name": "Cleo {}".format(i), "age": i + 3, "score": 10}
|
{"id": i, "name": f"Cleo {i}", "age": i + 3, "score": 10} for i in range(1, 21)
|
||||||
for i in range(1, 21)
|
|
||||||
]
|
]
|
||||||
with open(json_path, "w") as fp:
|
with open(json_path, "w") as fp:
|
||||||
fp.write(json.dumps(dogs))
|
fp.write(json.dumps(dogs))
|
||||||
|
|
@ -587,7 +588,7 @@ def test_insert_streaming_batch_size_1(db_path):
|
||||||
return
|
return
|
||||||
tries += 1
|
tries += 1
|
||||||
if tries > 10:
|
if tries > 10:
|
||||||
assert False, "Expected {}, got {}".format(expected, rows)
|
assert False, f"Expected {expected}, got {rows}"
|
||||||
time.sleep(tries * 0.1)
|
time.sleep(tries * 0.1)
|
||||||
|
|
||||||
try_until([{"name": "Azi"}])
|
try_until([{"name": "Azi"}])
|
||||||
|
|
@ -664,6 +665,53 @@ def test_insert_csv_detect_types_new_table(db_path):
|
||||||
assert db["data"].columns_dict == {"name": str, "age": int, "weight": float}
|
assert db["data"].columns_dict == {"name": str, "age": int, "weight": float}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"command,extra_args,input_text,expected_row",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"insert",
|
||||||
|
[],
|
||||||
|
"zipcode,score\n01234,9.5\n",
|
||||||
|
{"zipcode": "01234", "score": 9.5},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"upsert",
|
||||||
|
["--pk", "id"],
|
||||||
|
"id,zipcode,score\n1,01234,9.5\n",
|
||||||
|
{"id": 1, "zipcode": "01234", "score": 9.5},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_insert_upsert_csv_type_overrides_detected_types(
|
||||||
|
db_path, command, extra_args, input_text, expected_row
|
||||||
|
):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
command,
|
||||||
|
db_path,
|
||||||
|
"places",
|
||||||
|
"-",
|
||||||
|
"--csv",
|
||||||
|
]
|
||||||
|
+ extra_args
|
||||||
|
+ [
|
||||||
|
"--type",
|
||||||
|
"zipcode",
|
||||||
|
"text",
|
||||||
|
],
|
||||||
|
catch_exceptions=False,
|
||||||
|
input=input_text,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
db = Database(db_path)
|
||||||
|
expected_columns = {"zipcode": str, "score": float}
|
||||||
|
if command == "upsert":
|
||||||
|
expected_columns = {"id": int, **expected_columns}
|
||||||
|
assert db["places"].columns_dict == expected_columns
|
||||||
|
assert list(db["places"].rows) == [expected_row]
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_csv_detect_types_leaves_existing_table_alone(db_path):
|
def test_upsert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db["places"].insert({"id": 1, "name": "Boston", "zip": "01234"}, pk="id")
|
db["places"].insert({"id": 1, "name": "Boston", "zip": "01234"}, pk="id")
|
||||||
|
|
@ -691,3 +739,153 @@ def test_insert_invalid_pk_clean_error(db_path):
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert result.exception is None or isinstance(result.exception, SystemExit)
|
assert result.exception is None or isinstance(result.exception, SystemExit)
|
||||||
assert result.output.startswith("Error: Invalid primary key column")
|
assert result.output.startswith("Error: Invalid primary key column")
|
||||||
|
|
||||||
|
|
||||||
|
# --code tests, see https://github.com/simonw/sqlite-utils/issues/684
|
||||||
|
CODE_ROWS_FUNCTION = """
|
||||||
|
def rows():
|
||||||
|
yield {"id": 1, "name": "Cleo"}
|
||||||
|
yield {"id": 2, "name": "Suna"}
|
||||||
|
"""
|
||||||
|
|
||||||
|
CODE_ROWS_ITERABLE = """
|
||||||
|
rows = [
|
||||||
|
{"id": 1, "name": "Cleo"},
|
||||||
|
{"id": 2, "name": "Suna"},
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("code", (CODE_ROWS_FUNCTION, CODE_ROWS_ITERABLE))
|
||||||
|
def test_insert_code(tmpdir, code):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", code, "--pk", "id"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
db = Database(db_path)
|
||||||
|
assert db["creatures"].pks == ["id"]
|
||||||
|
assert list(db["creatures"].rows) == [
|
||||||
|
{"id": 1, "name": "Cleo"},
|
||||||
|
{"id": 2, "name": "Suna"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_from_file(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
code_path = str(tmpdir / "gen.py")
|
||||||
|
with open(code_path, "w") as fp:
|
||||||
|
fp.write(CODE_ROWS_FUNCTION)
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", code_path],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert list(Database(db_path)["creatures"].rows) == [
|
||||||
|
{"id": 1, "name": "Cleo"},
|
||||||
|
{"id": 2, "name": "Suna"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_upsert_code(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
db = Database(db_path)
|
||||||
|
db["creatures"].insert_all(
|
||||||
|
[{"id": 1, "name": "old"}, {"id": 2, "name": "Suna"}], pk="id"
|
||||||
|
)
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["upsert", db_path, "creatures", "--code", CODE_ROWS_FUNCTION, "--pk", "id"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert list(db["creatures"].rows) == [
|
||||||
|
{"id": 1, "name": "Cleo"},
|
||||||
|
{"id": 2, "name": "Suna"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_requires_file_or_code(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(cli.cli, ["insert", db_path, "creatures"])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "Provide either a FILE argument or --code" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_mutually_exclusive_with_file(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "-", "--code", CODE_ROWS_FUNCTION],
|
||||||
|
input="{}",
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "--code cannot be used with a FILE argument" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_rejects_input_format_options(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", CODE_ROWS_FUNCTION, "--csv"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "--code cannot be used with input format options" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_missing_rows(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", "x = 1"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "must define a 'rows' function or iterable" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_single_dict(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
"insert",
|
||||||
|
db_path,
|
||||||
|
"creatures",
|
||||||
|
"--code",
|
||||||
|
'rows = {"id": 1, "name": "Cleo"}',
|
||||||
|
"--pk",
|
||||||
|
"id",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert list(Database(db_path)["creatures"].rows) == [{"id": 1, "name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_not_iterable(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", "rows = 5"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "must define a 'rows' function or iterable" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_syntax_error(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", "def rows(:"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "Error in --code" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_code_file_not_found(tmpdir):
|
||||||
|
db_path = str(tmpdir / "dogs.db")
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
["insert", db_path, "creatures", "--code", "missing.py"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "File not found: missing.py" in result.output
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import click
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import click
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|
@ -28,7 +29,7 @@ def test_memory_csv(tmpdir, sql_from, use_stdin):
|
||||||
fp.write(content)
|
fp.write(content)
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", csv_path, "select * from {}".format(sql_from), "--nl"],
|
["memory", csv_path, f"select * from {sql_from}", "--nl"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
@ -53,7 +54,7 @@ def test_memory_tsv(tmpdir, use_stdin):
|
||||||
sql_from = "chickens"
|
sql_from = "chickens"
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", path, "select * from {}".format(sql_from)],
|
["memory", path, f"select * from {sql_from}"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -79,7 +80,7 @@ def test_memory_json(tmpdir, use_stdin):
|
||||||
sql_from = "chickens"
|
sql_from = "chickens"
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", path, "select * from {}".format(sql_from)],
|
["memory", path, f"select * from {sql_from}"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -105,7 +106,7 @@ def test_memory_json_nl(tmpdir, use_stdin):
|
||||||
sql_from = "chickens"
|
sql_from = "chickens"
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", path, "select * from {}".format(sql_from)],
|
["memory", path, f"select * from {sql_from}"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -135,7 +136,7 @@ def test_memory_csv_encoding(tmpdir, use_stdin):
|
||||||
CliRunner()
|
CliRunner()
|
||||||
.invoke(
|
.invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", csv_path, "select * from {}".format(sql_from), "--nl"],
|
["memory", csv_path, f"select * from {sql_from}", "--nl"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
.exit_code
|
.exit_code
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
import sqlite_utils.cli
|
import sqlite_utils.cli
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.utils import column_affinity
|
from sqlite_utils.utils import column_affinity
|
||||||
|
|
||||||
EXAMPLES = [
|
EXAMPLES = [
|
||||||
|
|
@ -41,5 +42,5 @@ def test_column_affinity(column_def, expected_type):
|
||||||
|
|
||||||
@pytest.mark.parametrize("column_def,expected_type", EXAMPLES)
|
@pytest.mark.parametrize("column_def,expected_type", EXAMPLES)
|
||||||
def test_columns_dict(fresh_db, column_def, expected_type):
|
def test_columns_dict(fresh_db, column_def, expected_type):
|
||||||
fresh_db.execute("create table foo (col {})".format(column_def))
|
fresh_db.execute(f"create table foo (col {column_def})")
|
||||||
assert {"col": expected_type} == fresh_db["foo"].columns_dict
|
assert {"col": expected_type} == fresh_db["foo"].columns_dict
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.db import TransactionError
|
from sqlite_utils.db import TransactionError
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
import pytest
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def test_recursive_triggers():
|
def test_recursive_triggers():
|
||||||
|
|
@ -87,3 +89,27 @@ def test_legacy_transaction_control_connection_is_accepted(tmpdir):
|
||||||
db["t"].insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
assert [r["id"] for r in db["t"].rows] == [1]
|
assert [r["id"] for r in db["t"].rows] == [1]
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_attribute_for_memory_true():
|
||||||
|
db = Database(memory=True)
|
||||||
|
assert db.memory is True
|
||||||
|
assert db.memory_name is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_attribute_for_memory_name():
|
||||||
|
db = Database(memory_name="shared_attr")
|
||||||
|
assert db.memory is True
|
||||||
|
assert db.memory_name == "shared_attr"
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_attribute_for_memory_string_path():
|
||||||
|
db = Database(":memory:")
|
||||||
|
assert db.memory is True
|
||||||
|
assert db.memory_name is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_attribute_for_file_path(tmpdir):
|
||||||
|
db = Database(str(tmpdir / "file.db"))
|
||||||
|
assert db.memory is False
|
||||||
|
assert db.memory_name is None
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from sqlite_utils.db import BadMultiValues
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import BadMultiValues
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"columns,fn,expected",
|
"columns,fn,expected",
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
from sqlite_utils.db import (
|
|
||||||
Index,
|
|
||||||
Database,
|
|
||||||
DescIndex,
|
|
||||||
AlterError,
|
|
||||||
InvalidColumns,
|
|
||||||
NoObviousTable,
|
|
||||||
OperationalError,
|
|
||||||
ForeignKey,
|
|
||||||
Table,
|
|
||||||
View,
|
|
||||||
NoTable,
|
|
||||||
NoView,
|
|
||||||
)
|
|
||||||
from sqlite_utils.utils import hash_record, sqlite3
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import pytest
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import (
|
||||||
|
AlterError,
|
||||||
|
Database,
|
||||||
|
DescIndex,
|
||||||
|
ForeignKey,
|
||||||
|
Index,
|
||||||
|
InvalidColumns,
|
||||||
|
NoObviousTable,
|
||||||
|
NoTable,
|
||||||
|
NoView,
|
||||||
|
OperationalError,
|
||||||
|
Table,
|
||||||
|
View,
|
||||||
|
)
|
||||||
|
from sqlite_utils.utils import hash_record, sqlite3
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pandas as pd # type: ignore
|
import pandas as pd # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -699,7 +701,7 @@ def test_bulk_insert_more_than_999_values(fresh_db):
|
||||||
"num_columns,should_error", ((900, False), (999, False), (1000, True))
|
"num_columns,should_error", ((900, False), (999, False), (1000, True))
|
||||||
)
|
)
|
||||||
def test_error_if_more_than_999_columns(fresh_db, num_columns, should_error):
|
def test_error_if_more_than_999_columns(fresh_db, num_columns, should_error):
|
||||||
record = dict([("c{}".format(i), i) for i in range(num_columns)])
|
record = {f"c{i}": i for i in range(num_columns)}
|
||||||
if should_error:
|
if should_error:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
fresh_db["big"].insert(record)
|
fresh_db["big"].insert(record)
|
||||||
|
|
@ -718,17 +720,9 @@ def test_columns_not_in_first_record_should_not_cause_batch_to_be_too_large(fres
|
||||||
records = [
|
records = [
|
||||||
{"c0": "first record"}, # one column in first record -> batch size = 999
|
{"c0": "first record"}, # one column in first record -> batch size = 999
|
||||||
# fill out the batch with 99 records with enough columns to exceed THRESHOLD
|
# fill out the batch with 99 records with enough columns to exceed THRESHOLD
|
||||||
*[
|
*[{f"c{i}": j for i in range(extra_columns)} for j in range(batch_size - 1)],
|
||||||
dict([("c{}".format(i), j) for i in range(extra_columns)])
|
|
||||||
for j in range(batch_size - 1)
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
try:
|
fresh_db["too_many_columns"].insert_all(records, alter=True, batch_size=batch_size)
|
||||||
fresh_db["too_many_columns"].insert_all(
|
|
||||||
records, alter=True, batch_size=batch_size
|
|
||||||
)
|
|
||||||
except sqlite3.OperationalError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -809,6 +803,34 @@ def test_create_index_if_not_exists(fresh_db):
|
||||||
dogs.create_index(["name"], if_not_exists=True)
|
dogs.create_index(["name"], if_not_exists=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_index(fresh_db):
|
||||||
|
dogs = fresh_db["dogs"]
|
||||||
|
dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is_good_dog": True})
|
||||||
|
dogs.create_index(["name"])
|
||||||
|
assert [index.name for index in dogs.indexes] == ["idx_dogs_name"]
|
||||||
|
dogs.drop_index("idx_dogs_name")
|
||||||
|
assert dogs.indexes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_index_ignore(fresh_db):
|
||||||
|
dogs = fresh_db["dogs"]
|
||||||
|
dogs.insert({"name": "Cleo"})
|
||||||
|
with pytest.raises(OperationalError, match="No index named idx_dogs_name"):
|
||||||
|
dogs.drop_index("idx_dogs_name")
|
||||||
|
dogs.drop_index("idx_dogs_name", ignore=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drop_index_wrong_table(fresh_db):
|
||||||
|
dogs = fresh_db["dogs"]
|
||||||
|
cats = fresh_db["cats"]
|
||||||
|
dogs.insert({"name": "Cleo"})
|
||||||
|
cats.insert({"name": "Misty"})
|
||||||
|
dogs.create_index(["name"])
|
||||||
|
with pytest.raises(OperationalError, match="No index named idx_dogs_name"):
|
||||||
|
cats.drop_index("idx_dogs_name")
|
||||||
|
assert [index.name for index in dogs.indexes] == ["idx_dogs_name"]
|
||||||
|
|
||||||
|
|
||||||
def test_create_index_desc(fresh_db):
|
def test_create_index_desc(fresh_db):
|
||||||
dogs = fresh_db["dogs"]
|
dogs = fresh_db["dogs"]
|
||||||
dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is good dog": True})
|
dogs.insert({"name": "Cleo", "twitter": "cleopaws", "age": 3, "is good dog": True})
|
||||||
|
|
@ -882,7 +904,7 @@ def test_insert_list_nested_unicode(fresh_db):
|
||||||
def test_insert_uuid(fresh_db):
|
def test_insert_uuid(fresh_db):
|
||||||
uuid4 = uuid.uuid4()
|
uuid4 = uuid.uuid4()
|
||||||
fresh_db["test"].insert({"uuid": uuid4})
|
fresh_db["test"].insert({"uuid": uuid4})
|
||||||
row = list(fresh_db["test"].rows)[0]
|
row = next(iter(fresh_db["test"].rows))
|
||||||
assert {"uuid"} == row.keys()
|
assert {"uuid"} == row.keys()
|
||||||
assert isinstance(row["uuid"], str)
|
assert isinstance(row["uuid"], str)
|
||||||
assert row["uuid"] == str(uuid4)
|
assert row["uuid"] == str(uuid4)
|
||||||
|
|
@ -890,16 +912,14 @@ def test_insert_uuid(fresh_db):
|
||||||
|
|
||||||
def test_insert_memoryview(fresh_db):
|
def test_insert_memoryview(fresh_db):
|
||||||
fresh_db["test"].insert({"data": memoryview(b"hello")})
|
fresh_db["test"].insert({"data": memoryview(b"hello")})
|
||||||
row = list(fresh_db["test"].rows)[0]
|
row = next(iter(fresh_db["test"].rows))
|
||||||
assert {"data"} == row.keys()
|
assert {"data"} == row.keys()
|
||||||
assert isinstance(row["data"], bytes)
|
assert isinstance(row["data"], bytes)
|
||||||
assert row["data"] == b"hello"
|
assert row["data"] == b"hello"
|
||||||
|
|
||||||
|
|
||||||
def test_insert_thousands_using_generator(fresh_db):
|
def test_insert_thousands_using_generator(fresh_db):
|
||||||
fresh_db["test"].insert_all(
|
fresh_db["test"].insert_all({"i": i, "word": f"word_{i}"} for i in range(10000))
|
||||||
{"i": i, "word": "word_{}".format(i)} for i in range(10000)
|
|
||||||
)
|
|
||||||
assert [{"name": "i", "type": "INTEGER"}, {"name": "word", "type": "TEXT"}] == [
|
assert [{"name": "i", "type": "INTEGER"}, {"name": "word", "type": "TEXT"}] == [
|
||||||
{"name": col.name, "type": col.type} for col in fresh_db["test"].columns
|
{"name": col.name, "type": col.type} for col in fresh_db["test"].columns
|
||||||
]
|
]
|
||||||
|
|
@ -910,7 +930,7 @@ def test_insert_thousands_raises_exception_with_extra_columns_after_first_100(fr
|
||||||
# https://github.com/simonw/sqlite-utils/issues/139
|
# https://github.com/simonw/sqlite-utils/issues/139
|
||||||
with pytest.raises(Exception, match="table test has no column named extra"):
|
with pytest.raises(Exception, match="table test has no column named extra"):
|
||||||
fresh_db["test"].insert_all(
|
fresh_db["test"].insert_all(
|
||||||
[{"i": i, "word": "word_{}".format(i)} for i in range(100)]
|
[{"i": i, "word": f"word_{i}"} for i in range(100)]
|
||||||
+ [{"i": 101, "extra": "This extra column should cause an exception"}],
|
+ [{"i": 101, "extra": "This extra column should cause an exception"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -918,7 +938,7 @@ def test_insert_thousands_raises_exception_with_extra_columns_after_first_100(fr
|
||||||
def test_insert_thousands_adds_extra_columns_after_first_100_with_alter(fresh_db):
|
def test_insert_thousands_adds_extra_columns_after_first_100_with_alter(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/139
|
# https://github.com/simonw/sqlite-utils/issues/139
|
||||||
fresh_db["test"].insert_all(
|
fresh_db["test"].insert_all(
|
||||||
[{"i": i, "word": "word_{}".format(i)} for i in range(100)]
|
[{"i": i, "word": f"word_{i}"} for i in range(100)]
|
||||||
+ [{"i": 101, "extra": "Should trigger ALTER"}],
|
+ [{"i": 101, "extra": "Should trigger ALTER"}],
|
||||||
alter=True,
|
alter=True,
|
||||||
)
|
)
|
||||||
|
|
@ -930,7 +950,7 @@ def test_insert_thousands_adds_extra_columns_after_first_100_with_alter(fresh_db
|
||||||
def test_insert_all_pk_not_in_records_raises(fresh_db, num_rows):
|
def test_insert_all_pk_not_in_records_raises(fresh_db, num_rows):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/732
|
# https://github.com/simonw/sqlite-utils/issues/732
|
||||||
fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
|
fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
|
||||||
rows = [{"a": "x{}".format(i), "b": i} for i in range(num_rows)]
|
rows = [{"a": f"x{i}", "b": i} for i in range(num_rows)]
|
||||||
|
|
||||||
with pytest.raises(InvalidColumns) as ex:
|
with pytest.raises(InvalidColumns) as ex:
|
||||||
fresh_db["t"].insert_all(rows, pk="not_a_column")
|
fresh_db["t"].insert_all(rows, pk="not_a_column")
|
||||||
|
|
@ -947,7 +967,7 @@ def test_insert_all_pk_not_in_records_alter_raises(fresh_db, num_rows):
|
||||||
# known - a pk column that is in neither the table nor the records
|
# known - a pk column that is in neither the table nor the records
|
||||||
# still raises
|
# still raises
|
||||||
fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
|
fresh_db.conn.execute("CREATE TABLE t (a TEXT, b INT, PRIMARY KEY (a, b))")
|
||||||
rows = [{"a": "x{}".format(i), "b": i} for i in range(num_rows)]
|
rows = [{"a": f"x{i}", "b": i} for i in range(num_rows)]
|
||||||
|
|
||||||
with pytest.raises(InvalidColumns) as ex:
|
with pytest.raises(InvalidColumns) as ex:
|
||||||
fresh_db["t"].insert_all(rows, pk="not_a_column", alter=True)
|
fresh_db["t"].insert_all(rows, pk="not_a_column", alter=True)
|
||||||
|
|
@ -990,6 +1010,96 @@ def test_insert_ignore(fresh_db):
|
||||||
assert rows == [{"id": 1, "bar": 2}]
|
assert rows == [{"id": 1, "bar": 2}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_ignore_reports_existing_row(fresh_db):
|
||||||
|
# An ignored insert (row already exists) should point last_rowid and
|
||||||
|
# last_pk at the existing conflicting row - see the Datasette insert API
|
||||||
|
fresh_db["docs"].insert({"id": 1, "title": "Exists"}, pk="id")
|
||||||
|
# Insert a conflicting row with ignore=True and no explicit pk=
|
||||||
|
table = fresh_db["docs"].insert({"id": 1, "title": "One"}, ignore=True)
|
||||||
|
assert table.last_rowid == 1
|
||||||
|
assert table.last_pk == 1
|
||||||
|
assert list(fresh_db["docs"].rows_where("rowid = ?", [table.last_rowid])) == [
|
||||||
|
{"id": 1, "title": "Exists"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rowid_alias", ("rowid", "_rowid_", "oid"))
|
||||||
|
@pytest.mark.parametrize("method", ("upsert", "insert_replace", "insert_ignore"))
|
||||||
|
def test_pk_rowid_alias_on_rowid_table(fresh_db, rowid_alias, method):
|
||||||
|
# rowid and its aliases are valid primary keys for a rowid table even
|
||||||
|
# though they are not listed among the table's columns - see the Datasette
|
||||||
|
# upsert API against tables without an explicit primary key
|
||||||
|
fresh_db["t"].insert({"title": "Hello"})
|
||||||
|
assert fresh_db["t"].pks == ["rowid"]
|
||||||
|
record = {rowid_alias: 1, "title": "Updated"}
|
||||||
|
if method == "upsert":
|
||||||
|
table = fresh_db["t"].upsert(record, pk=rowid_alias)
|
||||||
|
elif method == "insert_replace":
|
||||||
|
table = fresh_db["t"].insert(record, pk=rowid_alias, replace=True)
|
||||||
|
else:
|
||||||
|
table = fresh_db["t"].insert(record, pk=rowid_alias, ignore=True)
|
||||||
|
assert table.last_pk == 1
|
||||||
|
expected_title = "Hello" if method == "insert_ignore" else "Updated"
|
||||||
|
assert list(fresh_db["t"].rows) == [{"title": expected_title}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_ignore_reports_existing_row_compound_pk(fresh_db):
|
||||||
|
# Compound primary key variant of the ignored-insert lookup
|
||||||
|
fresh_db["t"].insert_all([{"a": 1, "b": 2, "note": "first"}], pk=("a", "b"))
|
||||||
|
table = fresh_db["t"].insert(
|
||||||
|
{"a": 1, "b": 2, "note": "second"}, pk=("a", "b"), ignore=True
|
||||||
|
)
|
||||||
|
assert table.last_pk == (1, 2)
|
||||||
|
assert list(fresh_db["t"].rows_where("rowid = ?", [table.last_rowid])) == [
|
||||||
|
{"a": 1, "b": 2, "note": "first"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_ignore_reports_existing_row_list_mode(fresh_db):
|
||||||
|
# List-based iteration variant of the ignored-insert lookup
|
||||||
|
fresh_db["t"].insert_all([["id", "title"], [1, "first"]], pk="id")
|
||||||
|
table = fresh_db["t"].insert_all(
|
||||||
|
[["id", "title"], [1, "second"]], pk="id", ignore=True
|
||||||
|
)
|
||||||
|
assert table.last_pk == 1
|
||||||
|
assert table.last_rowid == 1
|
||||||
|
assert list(fresh_db["t"].rows) == [{"id": 1, "title": "first"}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_ignore_hash_id_reports_pk(fresh_db):
|
||||||
|
# With hash_id the pk is the computed hash; the original record has no id
|
||||||
|
# column to look up so last_rowid is left unset
|
||||||
|
first = fresh_db["dogs"].insert({"name": "Cleo"}, hash_id="id")
|
||||||
|
table = fresh_db["dogs"].insert({"name": "Cleo"}, hash_id="id", ignore=True)
|
||||||
|
assert table.last_pk == first.last_pk
|
||||||
|
assert table.last_rowid is None
|
||||||
|
assert fresh_db["dogs"].count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_ignore_unresolvable_conflict_leaves_pk_unset(fresh_db):
|
||||||
|
# When the conflict cannot be resolved to a primary key lookup, last_pk and
|
||||||
|
# last_rowid are left unset rather than reporting a misleading value
|
||||||
|
|
||||||
|
# rowid table with a UNIQUE column and no primary key: no pk to look up
|
||||||
|
fresh_db["u"].db.execute("create table u (title text unique)")
|
||||||
|
fresh_db["u"].insert({"title": "x"})
|
||||||
|
table = fresh_db["u"].insert({"title": "x"}, ignore=True)
|
||||||
|
assert table.last_pk is None
|
||||||
|
assert table.last_rowid is None
|
||||||
|
assert fresh_db["u"].count == 1
|
||||||
|
|
||||||
|
# Conflict on a UNIQUE column other than the primary key: the pk value from
|
||||||
|
# the record does not match the existing row, so the lookup finds nothing
|
||||||
|
fresh_db["docs"].db.execute(
|
||||||
|
"create table docs (id integer primary key, email text unique)"
|
||||||
|
)
|
||||||
|
fresh_db["docs"].insert({"id": 1, "email": "a"}, pk="id")
|
||||||
|
table = fresh_db["docs"].insert({"id": 2, "email": "a"}, ignore=True)
|
||||||
|
assert table.last_pk is None
|
||||||
|
assert table.last_rowid is None
|
||||||
|
assert fresh_db["docs"].count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_insert_ignore_with_pk_after_other_table_insert(fresh_db):
|
def test_insert_ignore_with_pk_after_other_table_insert(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/554
|
# https://github.com/simonw/sqlite-utils/issues/554
|
||||||
user = {"id": "abc", "name": "david"}
|
user = {"id": "abc", "name": "david"}
|
||||||
|
|
@ -1028,7 +1138,7 @@ def test_insert_hash_id_columns(fresh_db, use_table_factory):
|
||||||
insert_kwargs = {}
|
insert_kwargs = {}
|
||||||
else:
|
else:
|
||||||
dogs = fresh_db["dogs"]
|
dogs = fresh_db["dogs"]
|
||||||
insert_kwargs = dict(hash_id_columns=("name", "twitter"))
|
insert_kwargs = {"hash_id_columns": ("name", "twitter")}
|
||||||
|
|
||||||
id = dogs.insert(
|
id = dogs.insert(
|
||||||
{"name": "Cleo", "twitter": "cleopaws", "age": 5},
|
{"name": "Cleo", "twitter": "cleopaws", "age": 5},
|
||||||
|
|
@ -1536,7 +1646,7 @@ def test_upsert_uses_pk_from_prior_insert_655(fresh_db):
|
||||||
# Upsert should work without specifying pk again
|
# Upsert should work without specifying pk again
|
||||||
table.upsert({"id": 1, "name": "Alice Updated"})
|
table.upsert({"id": 1, "name": "Alice Updated"})
|
||||||
assert table.count == 1
|
assert table.count == 1
|
||||||
assert list(table.rows)[0]["name"] == "Alice Updated"
|
assert next(iter(table.rows))["name"] == "Alice Updated"
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_all_uses_pk_from_prior_insert_655(fresh_db):
|
def test_upsert_all_uses_pk_from_prior_insert_655(fresh_db):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.utils import OperationalError
|
from sqlite_utils.utils import OperationalError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ EXAMPLES = [
|
||||||
|
|
||||||
@pytest.mark.parametrize("column_def,initial_value,expected_value", EXAMPLES)
|
@pytest.mark.parametrize("column_def,initial_value,expected_value", EXAMPLES)
|
||||||
def test_quote_default_value(fresh_db, column_def, initial_value, expected_value):
|
def test_quote_default_value(fresh_db, column_def, initial_value, expected_value):
|
||||||
fresh_db.execute("create table foo (col {})".format(column_def))
|
fresh_db.execute(f"create table foo (col {column_def})")
|
||||||
assert initial_value == fresh_db["foo"].columns[0].default_value
|
assert initial_value == fresh_db["foo"].columns[0].default_value
|
||||||
assert expected_value == fresh_db.quote_default_value(
|
assert expected_value == fresh_db.quote_default_value(
|
||||||
fresh_db["foo"].columns[0].default_value
|
fresh_db["foo"].columns[0].default_value
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import sqlite_utils
|
||||||
|
|
||||||
def test_delete_rowid_table(fresh_db):
|
def test_delete_rowid_table(fresh_db):
|
||||||
table = fresh_db["table"]
|
table = fresh_db["table"]
|
||||||
table.insert({"foo": 1}).last_pk
|
table.insert({"foo": 1})
|
||||||
rowid = table.insert({"foo": 2}).last_pk
|
rowid = table.insert({"foo": 2}).last_pk
|
||||||
table.delete(rowid)
|
table.delete(rowid)
|
||||||
assert [{"foo": 1}] == list(table.rows)
|
assert [{"foo": 1}] == list(table.rows)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from click.testing import CliRunner
|
|
||||||
from sqlite_utils import cli, recipes
|
|
||||||
from pathlib import Path
|
|
||||||
import pytest
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import cli, recipes
|
||||||
|
|
||||||
docs_path = Path(__file__).parent.parent / "docs"
|
docs_path = Path(__file__).parent.parent / "docs"
|
||||||
commands_re = re.compile(r"(?:\$ | )sqlite-utils (\S+)")
|
commands_re = re.compile(r"(?:\$ | )sqlite-utils (\S+)")
|
||||||
|
|
@ -34,7 +36,7 @@ def test_commands_are_documented(documented_commands, command):
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", cli.cli.commands.values())
|
@pytest.mark.parametrize("command", cli.cli.commands.values())
|
||||||
def test_commands_have_help(command):
|
def test_commands_have_help(command):
|
||||||
assert command.help, "{} is missing its help".format(command)
|
assert command.help, f"{command} is missing its help"
|
||||||
|
|
||||||
|
|
||||||
def test_convert_help():
|
def test_convert_help():
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from sqlite_utils.db import NoTable
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import NoTable
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate(fresh_db):
|
def test_duplicate(fresh_db):
|
||||||
# Create table using native Sqlite statement:
|
# Create table using native Sqlite statement:
|
||||||
|
|
@ -12,7 +14,7 @@ def test_duplicate(fresh_db):
|
||||||
"bool_col" INTEGER,
|
"bool_col" INTEGER,
|
||||||
"datetime_col" TEXT)""")
|
"datetime_col" TEXT)""")
|
||||||
# Insert one row of mock data:
|
# Insert one row of mock data:
|
||||||
dt = datetime.datetime.now()
|
dt = datetime.datetime.now(datetime.timezone.utc)
|
||||||
data = {
|
data = {
|
||||||
"text_col": "Cleo",
|
"text_col": "Cleo",
|
||||||
"real_col": 3.14,
|
"real_col": 3.14,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
from sqlite_utils import Database
|
|
||||||
from sqlite_utils import cli
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
|
||||||
|
|
||||||
def test_enable_counts_specific_table(fresh_db):
|
def test_enable_counts_specific_table(fresh_db):
|
||||||
foo = fresh_db["foo"]
|
foo = fresh_db["foo"]
|
||||||
assert fresh_db.table_names() == []
|
assert fresh_db.table_names() == []
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
foo.insert({"name": "item {}".format(i)})
|
foo.insert({"name": f"item {i}"})
|
||||||
assert fresh_db.table_names() == ["foo"]
|
assert fresh_db.table_names() == ["foo"]
|
||||||
assert foo.count == 10
|
assert foo.count == 10
|
||||||
# Now enable counts
|
# Now enable counts
|
||||||
|
|
@ -44,7 +44,7 @@ def test_enable_counts_specific_table(fresh_db):
|
||||||
assert list(fresh_db["_counts"].rows) == [{"count": 10, "table": "foo"}]
|
assert list(fresh_db["_counts"].rows) == [{"count": 10, "table": "foo"}]
|
||||||
# Add some items to test the triggers
|
# Add some items to test the triggers
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
foo.insert({"name": "item {}".format(10 + i)})
|
foo.insert({"name": f"item {10 + i}"})
|
||||||
assert foo.count == 15
|
assert foo.count == 15
|
||||||
assert list(fresh_db["_counts"].rows) == [{"count": 15, "table": "foo"}]
|
assert list(fresh_db["_counts"].rows) == [{"count": 15, "table": "foo"}]
|
||||||
# Delete some items
|
# Delete some items
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
from sqlite_utils.db import InvalidColumns
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import InvalidColumns
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("table", [None, "Species"])
|
@pytest.mark.parametrize("table", [None, "Species"])
|
||||||
@pytest.mark.parametrize("fk_column", [None, "species"])
|
@pytest.mark.parametrize("fk_column", [None, "species"])
|
||||||
def test_extract_single_column(fresh_db, table, fk_column):
|
def test_extract_single_column(fresh_db, table, fk_column):
|
||||||
expected_table = table or "species"
|
expected_table = table or "species"
|
||||||
expected_fk = fk_column or "{}_id".format(expected_table)
|
expected_fk = fk_column or f"{expected_table}_id"
|
||||||
iter_species = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
iter_species = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
||||||
fresh_db["tree"].insert_all(
|
fresh_db["tree"].insert_all(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"id": i,
|
"id": i,
|
||||||
"name": "Tree {}".format(i),
|
"name": f"Tree {i}",
|
||||||
"species": next(iter_species),
|
"species": next(iter_species),
|
||||||
"end": 1,
|
"end": 1,
|
||||||
}
|
}
|
||||||
|
|
@ -26,13 +28,12 @@ def test_extract_single_column(fresh_db, table, fk_column):
|
||||||
'CREATE TABLE "tree" (\n'
|
'CREATE TABLE "tree" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
' "{}" INTEGER REFERENCES "{}"("id"),\n'.format(expected_fk, expected_table)
|
f' "{expected_fk}" INTEGER REFERENCES "{expected_table}"("id"),\n'
|
||||||
+ ' "end" INTEGER\n'
|
+ ' "end" INTEGER\n'
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
assert fresh_db[expected_table].schema == (
|
assert fresh_db[expected_table].schema == (
|
||||||
'CREATE TABLE "{}" (\n'.format(expected_table)
|
f'CREATE TABLE "{expected_table}" (\n' + ' "id" INTEGER PRIMARY KEY,\n'
|
||||||
+ ' "id" INTEGER PRIMARY KEY,\n'
|
|
||||||
' "species" TEXT\n'
|
' "species" TEXT\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
|
|
@ -57,7 +58,7 @@ def test_extract_multiple_columns_with_rename(fresh_db):
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"id": i,
|
"id": i,
|
||||||
"name": "Tree {}".format(i),
|
"name": f"Tree {i}",
|
||||||
"common_name": next(iter_common),
|
"common_name": next(iter_common),
|
||||||
"latin_name": next(iter_latin),
|
"latin_name": next(iter_latin),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
from sqlite_utils.db import Index
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import Index
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"kwargs,expected_table",
|
"kwargs,expected_table",
|
||||||
[
|
[
|
||||||
(dict(extracts={"species_id": "Species"}), "Species"),
|
({"extracts": {"species_id": "Species"}}, "Species"),
|
||||||
(dict(extracts=["species_id"]), "species_id"),
|
({"extracts": ["species_id"]}, "species_id"),
|
||||||
(dict(extracts=("species_id",)), "species_id"),
|
({"extracts": ("species_id",)}, "species_id"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("use_table_factory", [True, False])
|
@pytest.mark.parametrize("use_table_factory", [True, False])
|
||||||
|
|
@ -30,15 +31,11 @@ def test_extracts(fresh_db, kwargs, expected_table, use_table_factory):
|
||||||
# Should now have two tables: Trees and Species
|
# Should now have two tables: Trees and Species
|
||||||
assert {expected_table, "Trees"} == set(fresh_db.table_names())
|
assert {expected_table, "Trees"} == set(fresh_db.table_names())
|
||||||
assert (
|
assert (
|
||||||
'CREATE TABLE "{}" (\n "id" INTEGER PRIMARY KEY,\n "value" TEXT\n)'.format(
|
f'CREATE TABLE "{expected_table}" (\n "id" INTEGER PRIMARY KEY,\n "value" TEXT\n)'
|
||||||
expected_table
|
|
||||||
)
|
|
||||||
== fresh_db[expected_table].schema
|
== fresh_db[expected_table].schema
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
'CREATE TABLE "Trees" (\n "id" INTEGER,\n "species_id" INTEGER REFERENCES "{}"("id")\n)'.format(
|
f'CREATE TABLE "Trees" (\n "id" INTEGER,\n "species_id" INTEGER REFERENCES "{expected_table}"("id")\n)'
|
||||||
expected_table
|
|
||||||
)
|
|
||||||
== fresh_db["Trees"].schema
|
== fresh_db["Trees"].schema
|
||||||
)
|
)
|
||||||
# Should have a foreign key reference
|
# Should have a foreign key reference
|
||||||
|
|
@ -51,7 +48,7 @@ def test_extracts(fresh_db, kwargs, expected_table, use_table_factory):
|
||||||
assert [
|
assert [
|
||||||
Index(
|
Index(
|
||||||
seq=0,
|
seq=0,
|
||||||
name="idx_{}_value".format(expected_table),
|
name=f"idx_{expected_table}_value",
|
||||||
unique=1,
|
unique=1,
|
||||||
origin="c",
|
origin="c",
|
||||||
partial=0,
|
partial=0,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Tests for compound (multi-column) foreign keys - issue #594."""
|
"""Tests for compound (multi-column) foreign keys - issue #594."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.db import AlterError, ForeignKey
|
from sqlite_utils.db import AlterError, ForeignKey
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
|
|
@ -64,7 +65,7 @@ def test_foreign_key_no_longer_unpacks_as_tuple(fresh_db):
|
||||||
fresh_db["books"].add_foreign_key("author_id", "authors", "id")
|
fresh_db["books"].add_foreign_key("author_id", "authors", "id")
|
||||||
fk = fresh_db["books"].foreign_keys[0]
|
fk = fresh_db["books"].foreign_keys[0]
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
table, column, other_table, other_column = fk
|
_table, _column, _other_table, _other_column = fk
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
fk[0]
|
fk[0]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
from unittest.mock import ANY
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
from unittest.mock import ANY
|
|
||||||
|
|
||||||
search_records = [
|
search_records = [
|
||||||
{
|
{
|
||||||
|
|
@ -103,9 +105,10 @@ def test_search_limit_offset(fresh_db):
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
assert len(list(table.search("are"))) == 2
|
assert len(list(table.search("are"))) == 2
|
||||||
assert len(list(table.search("are", limit=1))) == 1
|
assert len(list(table.search("are", limit=1))) == 1
|
||||||
assert list(table.search("are", limit=1, order_by="rowid"))[0]["rowid"] == 1
|
assert next(iter(table.search("are", limit=1, order_by="rowid")))["rowid"] == 1
|
||||||
assert (
|
assert (
|
||||||
list(table.search("are", limit=1, offset=1, order_by="rowid"))[0]["rowid"] == 2
|
next(iter(table.search("are", limit=1, offset=1, order_by="rowid")))["rowid"]
|
||||||
|
== 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -223,20 +226,20 @@ def test_populate_fts_escape_table_names(fresh_db):
|
||||||
|
|
||||||
@pytest.mark.parametrize("fts_version", ("4", "5"))
|
@pytest.mark.parametrize("fts_version", ("4", "5"))
|
||||||
def test_fts_tokenize(fresh_db, fts_version):
|
def test_fts_tokenize(fresh_db, fts_version):
|
||||||
table_name = "searchable_{}".format(fts_version)
|
table_name = f"searchable_{fts_version}"
|
||||||
table = fresh_db[table_name]
|
table = fresh_db[table_name]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
# Test without porter stemming
|
# Test without porter stemming
|
||||||
table.enable_fts(
|
table.enable_fts(
|
||||||
["text", "country"],
|
["text", "country"],
|
||||||
fts_version="FTS{}".format(fts_version),
|
fts_version=f"FTS{fts_version}",
|
||||||
)
|
)
|
||||||
assert [] == list(table.search("bite"))
|
assert [] == list(table.search("bite"))
|
||||||
# Test WITH stemming
|
# Test WITH stemming
|
||||||
table.disable_fts()
|
table.disable_fts()
|
||||||
table.enable_fts(
|
table.enable_fts(
|
||||||
["text", "country"],
|
["text", "country"],
|
||||||
fts_version="FTS{}".format(fts_version),
|
fts_version=f"FTS{fts_version}",
|
||||||
tokenize="porter",
|
tokenize="porter",
|
||||||
)
|
)
|
||||||
rows = list(table.search("bite", order_by="rowid"))
|
rows = list(table.search("bite", order_by="rowid"))
|
||||||
|
|
@ -251,10 +254,10 @@ def test_fts_tokenize(fresh_db, fts_version):
|
||||||
|
|
||||||
def test_optimize_fts(fresh_db):
|
def test_optimize_fts(fresh_db):
|
||||||
for fts_version in ("4", "5"):
|
for fts_version in ("4", "5"):
|
||||||
table_name = "searchable_{}".format(fts_version)
|
table_name = f"searchable_{fts_version}"
|
||||||
table = fresh_db[table_name]
|
table = fresh_db[table_name]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS{}".format(fts_version))
|
table.enable_fts(["text", "country"], fts_version=f"FTS{fts_version}")
|
||||||
# You can call optimize successfully against the tables OR their _fts equivalents:
|
# You can call optimize successfully against the tables OR their _fts equivalents:
|
||||||
for table_name in (
|
for table_name in (
|
||||||
"searchable_4",
|
"searchable_4",
|
||||||
|
|
@ -310,12 +313,12 @@ def test_disable_fts(fresh_db, create_triggers):
|
||||||
expected_triggers = {"searchable_ai", "searchable_ad", "searchable_au"}
|
expected_triggers = {"searchable_ai", "searchable_ad", "searchable_au"}
|
||||||
else:
|
else:
|
||||||
expected_triggers = set()
|
expected_triggers = set()
|
||||||
assert expected_triggers == set(
|
assert expected_triggers == {
|
||||||
r[0]
|
r[0]
|
||||||
for r in fresh_db.execute(
|
for r in fresh_db.execute(
|
||||||
"select name from sqlite_master where type = 'trigger'"
|
"select name from sqlite_master where type = 'trigger'"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
)
|
}
|
||||||
# Now run .disable_fts() and confirm it worked
|
# Now run .disable_fts() and confirm it worked
|
||||||
table.disable_fts()
|
table.disable_fts()
|
||||||
assert (
|
assert (
|
||||||
|
|
@ -424,7 +427,7 @@ def test_enable_fts_replace(kwargs):
|
||||||
db["books"].enable_fts(**kwargs, replace=True)
|
db["books"].enable_fts(**kwargs, replace=True)
|
||||||
# Check that the new configuration is correct
|
# Check that the new configuration is correct
|
||||||
if should_have_changed_columns:
|
if should_have_changed_columns:
|
||||||
assert db["books_fts"].columns_dict.keys() == set(["title"])
|
assert db["books_fts"].columns_dict.keys() == {"title"}
|
||||||
if "create_triggers" in kwargs:
|
if "create_triggers" in kwargs:
|
||||||
assert db["books"].triggers
|
assert db["books"].triggers
|
||||||
if "fts_version" in kwargs:
|
if "fts_version" in kwargs:
|
||||||
|
|
@ -741,6 +744,7 @@ def test_enable_fts_cli_on_view_errors(tmpdir):
|
||||||
db.create_view("v", "select * from t")
|
db.create_view("v", "select * from t")
|
||||||
db.close()
|
db.close()
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from sqlite_utils import cli as cli_module
|
from sqlite_utils import cli as cli_module
|
||||||
|
|
||||||
result = CliRunner().invoke(cli_module.cli, ["enable-fts", db_path, "v", "text"])
|
result = CliRunner().invoke(cli_module.cli, ["enable-fts", db_path, "v", "text"])
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.db import NotFoundError
|
from sqlite_utils.db import NotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import json
|
import json
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from sqlite_utils.cli import cli
|
from sqlite_utils.cli import cli
|
||||||
from sqlite_utils.db import Database
|
from sqlite_utils.db import Database
|
||||||
from sqlite_utils.utils import find_spatialite, sqlite3
|
from sqlite_utils.utils import find_spatialite, sqlite3
|
||||||
|
|
@ -104,7 +105,7 @@ def test_query_load_extension(use_spatialite_shortcut):
|
||||||
[
|
[
|
||||||
":memory:",
|
":memory:",
|
||||||
"select spatialite_version()",
|
"select spatialite_version()",
|
||||||
"--load-extension={}".format(load_extension),
|
f"--load-extension={load_extension}",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from hypothesis import given
|
|
||||||
import hypothesis.strategies as st
|
import hypothesis.strategies as st
|
||||||
|
from hypothesis import given
|
||||||
|
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from sqlite_utils import cli, Database
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import pytest
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("silent", (False, True))
|
@pytest.mark.parametrize("silent", (False, True))
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -44,7 +46,7 @@ def test_insert_files(silent, pk_args, expected_pks):
|
||||||
)
|
)
|
||||||
cols = []
|
cols = []
|
||||||
for coltype in coltypes:
|
for coltype in coltypes:
|
||||||
cols += ["-c", "{}:{}".format(coltype, coltype)]
|
cols += ["-c", f"{coltype}:{coltype}"]
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert-files", db_path, "files", str(tmpdir)]
|
["insert-files", db_path, "files", str(tmpdir)]
|
||||||
|
|
@ -142,7 +144,7 @@ def test_insert_files_stdin(use_text, encoding, input, expected):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
row = list(db["files"].rows)[0]
|
row = next(iter(db["files"].rows))
|
||||||
key = "content"
|
key = "content"
|
||||||
if use_text:
|
if use_text:
|
||||||
key = "content_text"
|
key = "content_text"
|
||||||
|
|
@ -167,5 +169,5 @@ def test_insert_files_bad_text_encoding_error():
|
||||||
)
|
)
|
||||||
assert result.exit_code == 1, result.output
|
assert result.exit_code == 1, result.output
|
||||||
assert result.output.strip().startswith(
|
assert result.output.strip().startswith(
|
||||||
"Error: Could not read file '{}' as text".format(str(latin.resolve()))
|
f"Error: Could not read file '{latin.resolve()!s}' as text"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from sqlite_utils.db import Index, View, Database, XIndex, XIndexColumn
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import Database, Index, View, XIndex, XIndexColumn
|
||||||
|
|
||||||
|
|
||||||
def _check_supports_strict():
|
def _check_supports_strict():
|
||||||
"""Check if SQLite supports strict tables without leaking the database."""
|
"""Check if SQLite supports strict tables without leaking the database."""
|
||||||
|
|
@ -57,8 +58,8 @@ def test_detect_fts_similar_tables(fresh_db, reverse_order):
|
||||||
fresh_db[table2].insert({"title": "Hello"}).enable_fts(
|
fresh_db[table2].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS4"
|
["title"], fts_version="FTS4"
|
||||||
)
|
)
|
||||||
assert fresh_db[table1].detect_fts() == "{}_fts".format(table1)
|
assert fresh_db[table1].detect_fts() == f"{table1}_fts"
|
||||||
assert fresh_db[table2].detect_fts() == "{}_fts".format(table2)
|
assert fresh_db[table2].detect_fts() == f"{table2}_fts"
|
||||||
|
|
||||||
|
|
||||||
def test_tables(existing_db):
|
def test_tables(existing_db):
|
||||||
|
|
@ -311,6 +312,7 @@ def test_table_strict(fresh_db, create_table, expected_strict):
|
||||||
1,
|
1,
|
||||||
1.3,
|
1.3,
|
||||||
"foo",
|
"foo",
|
||||||
|
"O'Brien",
|
||||||
True,
|
True,
|
||||||
b"binary",
|
b"binary",
|
||||||
),
|
),
|
||||||
|
|
@ -323,6 +325,16 @@ def test_table_default_values(fresh_db, value):
|
||||||
assert default_values == {"value": value}
|
assert default_values == {"value": value}
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_default_values_escaped_quotes(fresh_db):
|
||||||
|
# SQLite stores string defaults with single quotes doubled, so
|
||||||
|
# introspection needs to unescape them again
|
||||||
|
fresh_db.execute(
|
||||||
|
"create table t (id integer primary key, name text default 'O''Brien')"
|
||||||
|
)
|
||||||
|
assert "default 'O''Brien'" in fresh_db["t"].schema
|
||||||
|
assert fresh_db["t"].default_values == {"name": "O'Brien"}
|
||||||
|
|
||||||
|
|
||||||
def test_pks_use_primary_key_declaration_order(fresh_db):
|
def test_pks_use_primary_key_declaration_order(fresh_db):
|
||||||
# PRIMARY KEY (a, b) declared against columns stored in order (b, a) -
|
# PRIMARY KEY (a, b) declared against columns stored in order (b, a) -
|
||||||
# pks must follow the declaration order, which is what SQLite uses to
|
# pks must follow the declaration order, which is what SQLite uses to
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Tests for list-based iteration in insert_all and upsert_all
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from sqlite_utils.db import Index
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import Index
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_new_table(fresh_db):
|
def test_lookup_new_table(fresh_db):
|
||||||
species = fresh_db["species"]
|
species = fresh_db["species"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from sqlite_utils.db import ForeignKey, NoObviousTable
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import ForeignKey, NoObviousTable
|
||||||
|
|
||||||
|
|
||||||
def test_insert_m2m_single(fresh_db):
|
def test_insert_m2m_single(fresh_db):
|
||||||
dogs = fresh_db["dogs"]
|
dogs = fresh_db["dogs"]
|
||||||
|
|
@ -65,8 +66,7 @@ def test_insert_m2m_iterable(fresh_db):
|
||||||
iterable_records = ({"id": 1, "name": "Phineas"}, {"id": 2, "name": "Ferb"})
|
iterable_records = ({"id": 1, "name": "Phineas"}, {"id": 2, "name": "Ferb"})
|
||||||
|
|
||||||
def iterable():
|
def iterable():
|
||||||
for record in iterable_records:
|
yield from iterable_records
|
||||||
yield record
|
|
||||||
|
|
||||||
platypuses = fresh_db["platypuses"]
|
platypuses = fresh_db["platypuses"]
|
||||||
platypuses.insert({"id": 1, "name": "Perry"}, pk="id").m2m(
|
platypuses.insert({"id": 1, "name": "Perry"}, pk="id").m2m(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
from sqlite_utils import Migrations
|
from sqlite_utils import Migrations
|
||||||
|
|
||||||
|
|
@ -154,10 +155,9 @@ def test_non_transactional_migration_allows_vacuum(tmpdir):
|
||||||
|
|
||||||
def test_apply_composes_inside_outer_transaction(migrations):
|
def test_apply_composes_inside_outer_transaction(migrations):
|
||||||
db = sqlite_utils.Database(memory=True)
|
db = sqlite_utils.Database(memory=True)
|
||||||
with pytest.raises(ZeroDivisionError):
|
with pytest.raises(ZeroDivisionError), db.atomic():
|
||||||
with db.atomic():
|
migrations.apply(db)
|
||||||
migrations.apply(db)
|
raise ZeroDivisionError
|
||||||
raise ZeroDivisionError
|
|
||||||
# The outer transaction rolled back, taking the migrations with it
|
# The outer transaction rolled back, taking the migrations with it
|
||||||
assert db.table_names() == []
|
assert db.table_names() == []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
from click.testing import CliRunner
|
|
||||||
import click
|
|
||||||
import importlib
|
import importlib
|
||||||
import pytest
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from sqlite_utils import cli, Database, hookimpl, plugins
|
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli, hookimpl, plugins
|
||||||
|
|
||||||
|
|
||||||
def _supports_pragma_function_list():
|
def _supports_pragma_function_list():
|
||||||
|
|
@ -11,7 +14,7 @@ def _supports_pragma_function_list():
|
||||||
try:
|
try:
|
||||||
db.execute("select * from pragma_function_list()")
|
db.execute("select * from pragma_function_list()")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except sqlite3.DatabaseError:
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import pytest
|
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import recipes
|
from sqlite_utils import recipes
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
import json
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from sqlite_utils import Database
|
|
||||||
import sqlite3
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils import Database
|
||||||
|
|
||||||
|
|
||||||
def test_recreate_ignored_for_in_memory():
|
def test_recreate_ignored_for_in_memory():
|
||||||
# None of these should raise an exception:
|
# None of these should raise an exception:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from sqlite_utils.utils import rows_from_file, Format, RowError
|
|
||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.utils import Format, RowError, rows_from_file
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input,expected_format",
|
"input,expected_format",
|
||||||
|
|
@ -29,7 +31,7 @@ def test_rows_from_file_detect_format(input, expected_format):
|
||||||
)
|
)
|
||||||
def test_rows_from_file_extra_fields_strategies(ignore_extras, extras_key, expected):
|
def test_rows_from_file_extra_fields_strategies(ignore_extras, extras_key, expected):
|
||||||
try:
|
try:
|
||||||
rows, format = rows_from_file(
|
rows, _format = rows_from_file(
|
||||||
BytesIO(b"id,name\r\n1,Cleo,oops"),
|
BytesIO(b"id,name\r\n1,Cleo,oops"),
|
||||||
format=Format.CSV,
|
format=Format.CSV,
|
||||||
ignore_extras=ignore_extras,
|
ignore_extras=ignore_extras,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from sqlite_utils import cli, Database
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from sqlite_utils import Database, cli
|
||||||
|
|
||||||
sniff_dir = pathlib.Path(__file__).parent / "sniff"
|
sniff_dir = pathlib.Path(__file__).parent / "sniff"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import pytest
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.utils import suggest_column_types
|
from sqlite_utils.utils import suggest_column_types
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,18 @@ def test_with_tracer():
|
||||||
assert len(collected) == 4
|
assert len(collected) == 4
|
||||||
assert collected == [
|
assert collected == [
|
||||||
(
|
(
|
||||||
"SELECT name FROM sqlite_master\n"
|
(
|
||||||
" WHERE rootpage = 0\n"
|
"SELECT name FROM sqlite_master\n"
|
||||||
" AND (\n"
|
" WHERE rootpage = 0\n"
|
||||||
" sql LIKE :like\n"
|
" AND (\n"
|
||||||
" OR sql LIKE :like2\n"
|
" sql LIKE :like\n"
|
||||||
" OR (\n"
|
" OR sql LIKE :like2\n"
|
||||||
" tbl_name = :table\n"
|
" OR (\n"
|
||||||
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
|
" tbl_name = :table\n"
|
||||||
" )\n"
|
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
|
||||||
" )",
|
" )\n"
|
||||||
|
" )"
|
||||||
|
),
|
||||||
{
|
{
|
||||||
"like": "%VIRTUAL TABLE%USING FTS%content=[dogs]%",
|
"like": "%VIRTUAL TABLE%USING FTS%content=[dogs]%",
|
||||||
"like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
|
"like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
|
||||||
|
|
@ -72,21 +74,23 @@ def test_with_tracer():
|
||||||
("select name from sqlite_master where type = 'view'", None),
|
("select name from sqlite_master where type = 'view'", None),
|
||||||
("select sql from sqlite_master where name = ?", ("dogs_fts",)),
|
("select sql from sqlite_master where name = ?", ("dogs_fts",)),
|
||||||
(
|
(
|
||||||
'with "original" as (\n'
|
(
|
||||||
" select\n"
|
'with "original" as (\n'
|
||||||
" rowid,\n"
|
" select\n"
|
||||||
" *\n"
|
" rowid,\n"
|
||||||
' from "dogs"\n'
|
" *\n"
|
||||||
")\n"
|
' from "dogs"\n'
|
||||||
"select\n"
|
")\n"
|
||||||
' "original".*\n'
|
"select\n"
|
||||||
"from\n"
|
' "original".*\n'
|
||||||
' "original"\n'
|
"from\n"
|
||||||
' join "dogs_fts" on "original".rowid = "dogs_fts".rowid\n'
|
' "original"\n'
|
||||||
"where\n"
|
' join "dogs_fts" on "original".rowid = "dogs_fts".rowid\n'
|
||||||
' "dogs_fts" match :query\n'
|
"where\n"
|
||||||
"order by\n"
|
' "dogs_fts" match :query\n'
|
||||||
' "dogs_fts".rank',
|
"order by\n"
|
||||||
|
' "dogs_fts".rank'
|
||||||
|
),
|
||||||
{"query": "Cleopaws"},
|
{"query": "Cleopaws"},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
from sqlite_utils.db import ForeignKey, TransformError
|
import sqlite3
|
||||||
from sqlite_utils.utils import OperationalError
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils.db import ForeignKey, TransactionError, TransformError
|
||||||
|
from sqlite_utils.utils import OperationalError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"params,expected_sql",
|
"params,expected_sql",
|
||||||
|
|
@ -111,7 +114,7 @@ def test_transform_sql_table_with_primary_key(
|
||||||
if use_pragma_foreign_keys:
|
if use_pragma_foreign_keys:
|
||||||
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")
|
dogs.insert({"id": 1, "name": "Cleo", "age": "5"}, pk="id")
|
||||||
sql = dogs.transform_sql(**{**params, **{"tmp_suffix": "suffix"}})
|
sql = dogs.transform_sql(**{**params, "tmp_suffix": "suffix"})
|
||||||
assert sql == expected_sql
|
assert sql == expected_sql
|
||||||
# Check that .transform() runs without exceptions:
|
# Check that .transform() runs without exceptions:
|
||||||
with fresh_db.tracer(tracer):
|
with fresh_db.tracer(tracer):
|
||||||
|
|
@ -184,7 +187,7 @@ def test_transform_sql_table_with_no_primary_key(
|
||||||
if use_pragma_foreign_keys:
|
if use_pragma_foreign_keys:
|
||||||
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
dogs.insert({"id": 1, "name": "Cleo", "age": "5"})
|
dogs.insert({"id": 1, "name": "Cleo", "age": "5"})
|
||||||
sql = dogs.transform_sql(**{**params, **{"tmp_suffix": "suffix"}})
|
sql = dogs.transform_sql(**{**params, "tmp_suffix": "suffix"})
|
||||||
assert sql == expected_sql
|
assert sql == expected_sql
|
||||||
# Check that .transform() runs without exceptions:
|
# Check that .transform() runs without exceptions:
|
||||||
with fresh_db.tracer(tracer):
|
with fresh_db.tracer(tracer):
|
||||||
|
|
@ -430,6 +433,163 @@ def test_transform_verify_foreign_keys(fresh_db):
|
||||||
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("use_pragma_foreign_keys", [False, True])
|
||||||
|
def test_transform_on_delete_cascade_does_not_delete_records(
|
||||||
|
fresh_db, use_pragma_foreign_keys
|
||||||
|
):
|
||||||
|
# Transforming a table drops and recreates it - if another table references
|
||||||
|
# it with ON DELETE CASCADE and PRAGMA foreign_keys is on, that drop must
|
||||||
|
# not cascade and delete the referencing records
|
||||||
|
if use_pragma_foreign_keys:
|
||||||
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
fresh_db.executescript("""
|
||||||
|
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"})
|
||||||
|
fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1})
|
||||||
|
# Transform the table on the other end of the cascading foreign key
|
||||||
|
fresh_db["authors"].transform(rename={"name": "author_name"})
|
||||||
|
assert list(fresh_db["authors"].rows) == [
|
||||||
|
{"id": 1, "author_name": "Ursula K. Le Guin"}
|
||||||
|
]
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
# Transforming the table with the cascading foreign key should not
|
||||||
|
# delete its records either
|
||||||
|
fresh_db["books"].transform(rename={"title": "book_title"})
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "book_title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
if use_pragma_foreign_keys:
|
||||||
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("on_delete", ["CASCADE", "SET NULL", "SET DEFAULT", "cascade"])
|
||||||
|
def test_transform_in_transaction_refuses_destructive_on_delete(fresh_db, on_delete):
|
||||||
|
# PRAGMA foreign_keys is a no-op inside a transaction, so transforming a
|
||||||
|
# table referenced by ON DELETE CASCADE / SET NULL / SET DEFAULT foreign
|
||||||
|
# keys inside an open transaction would fire those actions when the old
|
||||||
|
# table is dropped - transform() should refuse instead
|
||||||
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
fresh_db.executescript(f"""
|
||||||
|
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author_id INTEGER REFERENCES authors(id) ON DELETE {on_delete}
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"})
|
||||||
|
fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1})
|
||||||
|
previous_schema = fresh_db["authors"].schema
|
||||||
|
with fresh_db.atomic(), pytest.raises(TransactionError) as excinfo:
|
||||||
|
fresh_db["authors"].transform(rename={"name": "author_name"})
|
||||||
|
message = str(excinfo.value)
|
||||||
|
assert "books" in message
|
||||||
|
assert f"ON DELETE {on_delete.upper()}" in message
|
||||||
|
# Nothing should have changed
|
||||||
|
assert fresh_db["authors"].schema == previous_schema
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_in_transaction_refuses_self_referential_cascade(fresh_db):
|
||||||
|
# The copied table carries a foreign key referencing the original table
|
||||||
|
# name, so a self-referential cascade would wipe the copy too
|
||||||
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
fresh_db.executescript("""
|
||||||
|
CREATE TABLE categories (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
parent_id INTEGER REFERENCES categories(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["categories"].insert_all(
|
||||||
|
[
|
||||||
|
{"id": 1, "name": "Fiction", "parent_id": None},
|
||||||
|
{"id": 2, "name": "Science Fiction", "parent_id": 1},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
with fresh_db.atomic(), pytest.raises(TransactionError) as excinfo:
|
||||||
|
fresh_db["categories"].transform(rename={"name": "title"})
|
||||||
|
assert "categories" in str(excinfo.value)
|
||||||
|
assert fresh_db["categories"].count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_in_transaction_allowed_with_no_action_foreign_key(fresh_db):
|
||||||
|
# An inbound foreign key without a destructive ON DELETE action is safe
|
||||||
|
# inside a transaction thanks to PRAGMA defer_foreign_keys
|
||||||
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
fresh_db.executescript("""
|
||||||
|
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author_id INTEGER REFERENCES authors(id)
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"})
|
||||||
|
fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1})
|
||||||
|
with fresh_db.atomic():
|
||||||
|
fresh_db["authors"].transform(rename={"name": "author_name"})
|
||||||
|
assert list(fresh_db["authors"].rows) == [
|
||||||
|
{"id": 1, "author_name": "Ursula K. Le Guin"}
|
||||||
|
]
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_in_transaction_allowed_for_child_table(fresh_db):
|
||||||
|
# The table being transformed only has an outbound foreign key - dropping
|
||||||
|
# it fires no ON DELETE actions, so this is allowed inside a transaction
|
||||||
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
fresh_db.executescript("""
|
||||||
|
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"})
|
||||||
|
fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1})
|
||||||
|
with fresh_db.atomic():
|
||||||
|
fresh_db["books"].transform(rename={"title": "book_title"})
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "book_title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_in_transaction_allowed_with_foreign_keys_off(fresh_db):
|
||||||
|
# With PRAGMA foreign_keys off (the default) no cascades can fire, so
|
||||||
|
# transform inside a transaction is safe even with a CASCADE schema
|
||||||
|
fresh_db.executescript("""
|
||||||
|
CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT);
|
||||||
|
CREATE TABLE books (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
fresh_db["authors"].insert({"id": 1, "name": "Ursula K. Le Guin"})
|
||||||
|
fresh_db["books"].insert({"id": 1, "title": "The Dispossessed", "author_id": 1})
|
||||||
|
with fresh_db.atomic():
|
||||||
|
fresh_db["authors"].transform(rename={"name": "author_name"})
|
||||||
|
assert list(fresh_db["books"].rows) == [
|
||||||
|
{"id": 1, "title": "The Dispossessed", "author_id": 1}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_transform_add_foreign_keys_from_scratch(fresh_db):
|
def test_transform_add_foreign_keys_from_scratch(fresh_db):
|
||||||
_add_country_city_continent(fresh_db)
|
_add_country_city_continent(fresh_db)
|
||||||
fresh_db["places"].insert(_CAVEAU)
|
fresh_db["places"].insert(_CAVEAU)
|
||||||
|
|
@ -554,25 +714,75 @@ def test_transform_preserves_rowids(fresh_db, table_type):
|
||||||
# Now delete and insert a row to mix up the `rowid` sequence
|
# Now delete and insert a row to mix up the `rowid` sequence
|
||||||
fresh_db["places"].delete_where("id = ?", ["2"])
|
fresh_db["places"].delete_where("id = ?", ["2"])
|
||||||
fresh_db["places"].insert({"id": "4", "name": "London", "country": "UK"})
|
fresh_db["places"].insert({"id": "4", "name": "London", "country": "UK"})
|
||||||
previous_rows = list(
|
previous_rows = [
|
||||||
tuple(row) for row in fresh_db.execute("select rowid, id, name from places")
|
tuple(row) for row in fresh_db.execute("select rowid, id, name from places")
|
||||||
)
|
]
|
||||||
# Transform it
|
# Transform it
|
||||||
fresh_db["places"].transform(column_order=("country", "name"))
|
fresh_db["places"].transform(column_order=("country", "name"))
|
||||||
# Should be the same
|
# Should be the same
|
||||||
next_rows = list(
|
next_rows = [
|
||||||
tuple(row) for row in fresh_db.execute("select rowid, id, name from places")
|
tuple(row) for row in fresh_db.execute("select rowid, id, name from places")
|
||||||
)
|
]
|
||||||
assert previous_rows == next_rows
|
assert previous_rows == next_rows
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("strict", (False, True))
|
@pytest.mark.parametrize(
|
||||||
def test_transform_strict(fresh_db, strict):
|
"initial_strict,transform_strict,expected_strict",
|
||||||
dogs = fresh_db.table("dogs", strict=strict)
|
(
|
||||||
|
(False, None, False),
|
||||||
|
(True, None, True),
|
||||||
|
(False, True, True),
|
||||||
|
(True, False, False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_transform_strict(fresh_db, initial_strict, transform_strict, expected_strict):
|
||||||
|
if not fresh_db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
dogs = fresh_db.table("dogs", strict=initial_strict)
|
||||||
dogs.insert({"id": 1, "name": "Cleo"})
|
dogs.insert({"id": 1, "name": "Cleo"})
|
||||||
assert dogs.strict == strict or not fresh_db.supports_strict
|
assert dogs.strict is initial_strict
|
||||||
dogs.transform(not_null={"name"})
|
dogs.transform(strict=transform_strict)
|
||||||
assert dogs.strict == strict or not fresh_db.supports_strict
|
assert dogs.strict is expected_strict
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_to_strict_with_invalid_data(fresh_db):
|
||||||
|
if not fresh_db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
dogs = fresh_db["dogs"]
|
||||||
|
dogs.create({"id": int})
|
||||||
|
dogs.insert({"id": "not-an-integer"})
|
||||||
|
|
||||||
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
|
dogs.transform(strict=True)
|
||||||
|
|
||||||
|
assert dogs.strict is False
|
||||||
|
assert list(dogs.rows) == [{"id": "not-an-integer"}]
|
||||||
|
assert fresh_db.table_names() == ["dogs"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_transform_strict_updates_default(fresh_db):
|
||||||
|
if not fresh_db.supports_strict:
|
||||||
|
pytest.skip("SQLite version does not support strict tables")
|
||||||
|
table = fresh_db.table("items", strict=True)
|
||||||
|
table.create({"id": int})
|
||||||
|
|
||||||
|
table.transform(strict=False)
|
||||||
|
assert table.strict is False
|
||||||
|
|
||||||
|
table.create({"id": int}, replace=True)
|
||||||
|
assert table.strict is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method_name", ("transform", "transform_sql"))
|
||||||
|
def test_transform_to_strict_not_supported(fresh_db, method_name):
|
||||||
|
table = fresh_db["items"]
|
||||||
|
table.create({"id": int})
|
||||||
|
fresh_db._supports_strict = False
|
||||||
|
|
||||||
|
with pytest.raises(TransformError, match="SQLite does not support STRICT tables"):
|
||||||
|
getattr(table, method_name)(strict=True)
|
||||||
|
|
||||||
|
assert table.strict is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ def test_update_compound_pk_table(fresh_db):
|
||||||
)
|
)
|
||||||
def test_update_invalid_pk(fresh_db, pk, update_pk):
|
def test_update_invalid_pk(fresh_db, pk, update_pk):
|
||||||
table = fresh_db["table"]
|
table = fresh_db["table"]
|
||||||
table.insert({"id1": 5, "id2": 3, "v": 1}, pk=pk).last_pk
|
table.insert({"id1": 5, "id2": 3, "v": 1}, pk=pk)
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
table.update(update_pk, {"v": 2})
|
table.update(update_pk, {"v": 2})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from sqlite_utils.db import PrimaryKeyRequired
|
|
||||||
from sqlite_utils import Database
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils import Database
|
||||||
|
from sqlite_utils.db import PrimaryKeyRequired
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
||||||
def test_upsert(use_old_upsert):
|
def test_upsert(use_old_upsert):
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from sqlite_utils import utils
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from sqlite_utils import utils
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input,expected,should_be_is",
|
"input,expected,should_be_is",
|
||||||
|
|
@ -57,7 +59,7 @@ def test_maximize_csv_field_size_limit():
|
||||||
# Reset to default in case other tests have changed it
|
# Reset to default in case other tests have changed it
|
||||||
csv.field_size_limit(utils.ORIGINAL_CSV_FIELD_SIZE_LIMIT)
|
csv.field_size_limit(utils.ORIGINAL_CSV_FIELD_SIZE_LIMIT)
|
||||||
long_value = "a" * 131073
|
long_value = "a" * 131073
|
||||||
long_csv = "id,text\n1,{}".format(long_value)
|
long_csv = f"id,text\n1,{long_value}"
|
||||||
fp = io.BytesIO(long_csv.encode("utf-8"))
|
fp = io.BytesIO(long_csv.encode("utf-8"))
|
||||||
# Using rows_from_file should error
|
# Using rows_from_file should error
|
||||||
with pytest.raises(csv.Error):
|
with pytest.raises(csv.Error):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
from sqlite_utils.db import TransactionError
|
from sqlite_utils.db import TransactionError
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ def db_path_tmpdir(tmpdir):
|
||||||
|
|
||||||
|
|
||||||
def test_enable_disable_wal(db_path_tmpdir):
|
def test_enable_disable_wal(db_path_tmpdir):
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, tmpdir = db_path_tmpdir
|
||||||
assert len(tmpdir.listdir()) == 1
|
assert len(tmpdir.listdir()) == 1
|
||||||
assert "delete" == db.journal_mode
|
assert "delete" == db.journal_mode
|
||||||
assert "test.db-wal" not in [f.basename for f in tmpdir.listdir()]
|
assert "test.db-wal" not in [f.basename for f in tmpdir.listdir()]
|
||||||
|
|
@ -25,12 +26,11 @@ def test_enable_disable_wal(db_path_tmpdir):
|
||||||
|
|
||||||
|
|
||||||
def test_enable_wal_inside_transaction_raises(db_path_tmpdir):
|
def test_enable_wal_inside_transaction_raises(db_path_tmpdir):
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, _tmpdir = db_path_tmpdir
|
||||||
db["test"].insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(TransactionError):
|
with pytest.raises(TransactionError), db.atomic():
|
||||||
with db.atomic():
|
db["test"].insert({"id": 2}, pk="id")
|
||||||
db["test"].insert({"id": 2}, pk="id")
|
db.enable_wal()
|
||||||
db.enable_wal()
|
|
||||||
# The atomic() block must have rolled back cleanly and the
|
# The atomic() block must have rolled back cleanly and the
|
||||||
# journal mode must be unchanged
|
# journal mode must be unchanged
|
||||||
assert db.journal_mode == "delete"
|
assert db.journal_mode == "delete"
|
||||||
|
|
@ -38,19 +38,18 @@ def test_enable_wal_inside_transaction_raises(db_path_tmpdir):
|
||||||
|
|
||||||
|
|
||||||
def test_disable_wal_inside_transaction_raises(db_path_tmpdir):
|
def test_disable_wal_inside_transaction_raises(db_path_tmpdir):
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, _tmpdir = db_path_tmpdir
|
||||||
db.enable_wal()
|
db.enable_wal()
|
||||||
db["test"].insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(TransactionError):
|
with pytest.raises(TransactionError), db.atomic():
|
||||||
with db.atomic():
|
db["test"].insert({"id": 2}, pk="id")
|
||||||
db["test"].insert({"id": 2}, pk="id")
|
db.disable_wal()
|
||||||
db.disable_wal()
|
|
||||||
assert db.journal_mode == "wal"
|
assert db.journal_mode == "wal"
|
||||||
assert [r["id"] for r in db["test"].rows] == [1]
|
assert [r["id"] for r in db["test"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_autocommit_on(db_path_tmpdir):
|
def test_ensure_autocommit_on(db_path_tmpdir):
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, _tmpdir = db_path_tmpdir
|
||||||
previous_isolation_level = db.conn.isolation_level
|
previous_isolation_level = db.conn.isolation_level
|
||||||
assert previous_isolation_level is not None
|
assert previous_isolation_level is not None
|
||||||
with db.ensure_autocommit_on():
|
with db.ensure_autocommit_on():
|
||||||
|
|
@ -63,7 +62,7 @@ def test_ensure_autocommit_on(db_path_tmpdir):
|
||||||
def test_enable_wal_noop_inside_transaction_is_allowed(db_path_tmpdir):
|
def test_enable_wal_noop_inside_transaction_is_allowed(db_path_tmpdir):
|
||||||
# Calling enable_wal() when WAL is already enabled is a no-op,
|
# Calling enable_wal() when WAL is already enabled is a no-op,
|
||||||
# so it is fine inside a transaction
|
# so it is fine inside a transaction
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, _tmpdir = db_path_tmpdir
|
||||||
db.enable_wal()
|
db.enable_wal()
|
||||||
with db.atomic():
|
with db.atomic():
|
||||||
db["test"].insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
|
|
@ -75,13 +74,12 @@ def test_ensure_autocommit_on_inside_transaction_raises(db_path_tmpdir):
|
||||||
# Setting isolation_level commits any pending transaction as a side
|
# Setting isolation_level commits any pending transaction as a side
|
||||||
# effect, silently breaking the caller's rollback guarantee - so
|
# effect, silently breaking the caller's rollback guarantee - so
|
||||||
# entering autocommit mode with a transaction open is an error
|
# entering autocommit mode with a transaction open is an error
|
||||||
db, path, tmpdir = db_path_tmpdir
|
db, _path, _tmpdir = db_path_tmpdir
|
||||||
db["test"].insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
db.begin()
|
db.begin()
|
||||||
db.execute("insert into test (id) values (2)")
|
db.execute("insert into test (id) values (2)")
|
||||||
with pytest.raises(TransactionError):
|
with pytest.raises(TransactionError), db.ensure_autocommit_on():
|
||||||
with db.ensure_autocommit_on():
|
pass
|
||||||
pass
|
|
||||||
# The transaction is still open and can still be rolled back
|
# The transaction is still open and can still be rolled back
|
||||||
assert db.conn.in_transaction
|
assert db.conn.in_transaction
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue