mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-08-17 23:04:09 +02:00
Compare commits
2 commits
main
...
claude/tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ca780c30d |
||
|
|
89b3724871 |
71 changed files with 2361 additions and 5329 deletions
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
|
|
@ -43,9 +43,6 @@ jobs:
|
||||||
run: pytest --sqlite-autocommit
|
run: pytest --sqlite-autocommit
|
||||||
- name: run mypy
|
- name: run mypy
|
||||||
run: mypy sqlite_utils tests
|
run: mypy sqlite_utils tests
|
||||||
- name: run pyright regression checks
|
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
|
|
||||||
run: pyright sqlite_utils tests
|
|
||||||
- name: run flake8
|
- name: run flake8
|
||||||
run: flake8
|
run: flake8
|
||||||
- name: run ty
|
- name: run ty
|
||||||
|
|
@ -53,11 +50,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
pip install uv
|
pip install uv
|
||||||
uv run ty check sqlite_utils
|
uv run ty check sqlite_utils
|
||||||
- name: Check no accidental dev= dependencies needed
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
pip install uv
|
|
||||||
uv run --no-default-groups sqlite-utils --help
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: black . --check
|
run: black . --check
|
||||||
- name: Check if cog needs to be run
|
- name: Check if cog needs to be run
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,7 +15,6 @@ venv
|
||||||
.schema
|
.schema
|
||||||
.vscode
|
.vscode
|
||||||
.hypothesis
|
.hypothesis
|
||||||
.claude/
|
|
||||||
Pipfile
|
Pipfile
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
|
||||||
9
Justfile
9
Justfile
|
|
@ -2,25 +2,20 @@
|
||||||
@default: test lint
|
@default: test lint
|
||||||
|
|
||||||
# Run pytest with supplied options
|
# Run pytest with supplied options
|
||||||
@test *options: test-no-dev-dependencies
|
@test *options:
|
||||||
uv run pytest {{options}}
|
uv run pytest {{options}}
|
||||||
|
|
||||||
@test-no-dev-dependencies:
|
|
||||||
uv run --isolated --no-default-groups sqlite-utils --help > /dev/null
|
|
||||||
|
|
||||||
@run *options:
|
@run *options:
|
||||||
uv run -- {{options}}
|
uv run -- {{options}}
|
||||||
|
|
||||||
# Run linters: black, flake8, mypy, pyright, ty, cog
|
# Run linters: black, flake8, mypy, ty, cog
|
||||||
@lint:
|
@lint:
|
||||||
just run black . --check
|
just run black . --check
|
||||||
uv run flake8
|
uv run flake8
|
||||||
uv run mypy sqlite_utils tests
|
uv run mypy sqlite_utils tests
|
||||||
uv run pyright sqlite_utils tests
|
|
||||||
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,55 +4,6 @@
|
||||||
Changelog
|
Changelog
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. _v4_2_1:
|
|
||||||
|
|
||||||
4.2.1 (2026-08-13)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Fix for ``No module named 'typing_extensions'`` crashing bug accidentally shipped in version 4.2. (:issue:`842`)
|
|
||||||
|
|
||||||
.. _v4_2:
|
|
||||||
|
|
||||||
4.2 (2026-08-13)
|
|
||||||
----------------
|
|
||||||
|
|
||||||
- New ``table.checks``, ``table.column_checks`` and ``table.table_checks`` introspection properties expose column-level and table-level ``CHECK`` constraints. (:issue:`834`)
|
|
||||||
- New ``sqlite_utils.ANY`` marker type for creating and introspecting SQLite ``ANY`` columns. The Python API and CLI can create, add and transform these columns, and ``table.transform()`` and ``table.extract()`` now preserve ``ANY`` columns and their values in ``STRICT`` tables. (:issue:`790`)
|
|
||||||
- ``table.default_values`` now unescapes doubled single quotes in string defaults, so a default such as ``'O''Brien'`` is returned as ``"O'Brien"``. Thanks, `ikatyal2110 <https://github.com/ikatyal2110>`__. (`#811 <https://github.com/simonw/sqlite-utils/pull/811>`__)
|
|
||||||
- ``table.default_values`` now decodes unquoted ``TRUE``, ``FALSE`` and ``NULL`` default literals as ``True``, ``False`` and ``None`` respectively. (:issue:`836`)
|
|
||||||
- ``table.enable_fts(..., tokenize=...)`` and ``sqlite-utils enable-fts --tokenize`` now safely quote the tokenizer argument, preventing a crafted value from injecting additional SQL. Thanks, `Bunlong Heng <https://github.com/bunlongheng>`__. (`#828 <https://github.com/simonw/sqlite-utils/pull/828>`__)
|
|
||||||
- ``rows_where()``, ``pks_and_rows_where()``, ``search()`` and ``search_sql()`` now support ``offset=`` without requiring ``limit=``. The ``sqlite-utils rows --offset`` option now works without ``--limit`` too. Thanks, `ethanhawkes-gif <https://github.com/ethanhawkes-gif>`__. (:issue:`816`, `#821 <https://github.com/simonw/sqlite-utils/pull/821>`__)
|
|
||||||
- Empty or whitespace-only input passed to ``rows_from_file()`` is now handled as an empty CSV file instead of raising ``csv.Error``. Thanks, `Rami Abdelrazzaq <https://github.com/RamiNoodle733>`__. (:issue:`808`, `#837 <https://github.com/simonw/sqlite-utils/pull/837>`__)
|
|
||||||
- ``sqlite-utils convert --dry-run`` now works for table and column names containing closing square brackets. (:issue:`829`)
|
|
||||||
- ``table.indexes`` and ``table.xindexes`` now work for table, index and column names containing double quotes. This also fixes ``table.transform()`` for tables with those identifiers. Thanks, `nyxst4ck <https://github.com/nyxst4ck>`__. (:issue:`824`, `#825 <https://github.com/simonw/sqlite-utils/pull/825>`__)
|
|
||||||
- Improved type annotations throughout the package and added Pyright regression checks to CI. (:issue:`833`)
|
|
||||||
- Changing a ``TEXT`` column to ``INTEGER``, ``FLOAT`` or ``REAL`` using ``table.transform()`` or ``sqlite-utils transform`` now converts exact empty strings to ``NULL``. Previously they remained empty strings in the numeric column. Thanks, `ikatyal2110 <https://github.com/ikatyal2110>`__. (:issue:`488`, `#805 <https://github.com/simonw/sqlite-utils/pull/805>`__)
|
|
||||||
|
|
||||||
``table.transform()`` can handle many more edge-cases:
|
|
||||||
|
|
||||||
- ``table.transform()`` now preserves column-level and composite ``UNIQUE`` constraints, including constraint names, collations, sort order and ``ON CONFLICT`` behavior. Renaming columns updates those constraints, while dropping any constituent column removes the entire constraint. (:issue:`762`)
|
|
||||||
- ``table.transform()`` now preserves ``AUTOINCREMENT`` primary keys and their sequence high-water marks. Previously a transform removed ``AUTOINCREMENT`` and could reuse deleted row IDs. (:issue:`602`)
|
|
||||||
- ``table.transform()`` now preserves ``CHECK`` constraints, including comments within their expressions. Renaming a column rewrites identifier references in checks without changing string literals or function names. Dropping a column drops a check owned by that column, and raises ``TransformError`` if a remaining check depends on it. (:issue:`762`)
|
|
||||||
- ``table.transform()`` now preserves comments immediately before or after column definitions. These comments move with the column if it is renamed or reordered, and are removed if the column is dropped. (:issue:`762`)
|
|
||||||
- ``table.transform(rename=...)`` now preserves explicit indexes on renamed columns by dropping and recreating those indexes against the new column names. Previously this raised a ``TransformError``. (:issue:`822`)
|
|
||||||
- ``table.transform()`` now works for tables that are referenced by views. Previously the ``ALTER TABLE ... RENAME TO`` step raised ``no such table`` if a view referenced the table being transformed. View definitions are left unchanged - see :ref:`python_api_transform_views`. This also fixes a bug where ``transform(keep_table=...)`` silently rewrote dependent views to point at the frozen backup table instead of the live one. (:issue:`831`)
|
|
||||||
|
|
||||||
|
|
||||||
.. _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:
|
.. _v4_1:
|
||||||
|
|
||||||
4.1 (2026-07-11)
|
4.1 (2026-07-11)
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,7 @@ See :ref:`cli_transform_table`.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <TEXT CHOICE>... Change column type to INTEGER, TEXT, FLOAT,
|
--type <TEXT CHOICE>... Change column type to INTEGER, TEXT, FLOAT,
|
||||||
REAL, BLOB or ANY
|
REAL or BLOB
|
||||||
--drop TEXT Drop this column
|
--drop TEXT Drop this column
|
||||||
--rename <TEXT TEXT>... Rename this column to X
|
--rename <TEXT TEXT>... Rename this column to X
|
||||||
-o, --column-order TEXT Reorder columns
|
-o, --column-order TEXT Reorder columns
|
||||||
|
|
@ -662,7 +662,7 @@ 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: 'object | None' = None) -> 'str | None'
|
errors: 'Optional[object]' = None) -> 'Optional[str]'
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -671,7 +671,7 @@ See :ref:`cli_convert`.
|
||||||
- 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: 'object | None' = None) -> 'str | None'
|
False, errors: 'Optional[object]' = None) -> 'Optional[str]'
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -963,7 +963,7 @@ See :ref:`cli_create_table`.
|
||||||
height real \
|
height real \
|
||||||
photo blob --pk id
|
photo blob --pk id
|
||||||
|
|
||||||
Valid column types are text, integer, real, float, blob and any.
|
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
|
||||||
|
|
@ -1257,7 +1257,7 @@ See :ref:`cli_add_column`.
|
||||||
::
|
::
|
||||||
|
|
||||||
Usage: sqlite-utils add-column [OPTIONS] PATH TABLE COL_NAME
|
Usage: sqlite-utils add-column [OPTIONS] PATH TABLE COL_NAME
|
||||||
[integer|int|float|real|text|str|blob|bytes|any]
|
[integer|int|float|real|text|str|blob|bytes]
|
||||||
|
|
||||||
Add a column to the specified table
|
Add a column to the specified table
|
||||||
|
|
||||||
|
|
|
||||||
21
docs/cli.rst
21
docs/cli.rst
|
|
@ -1390,14 +1390,7 @@ Use ``--type column-name type`` to override the type automatically chosen when t
|
||||||
|
|
||||||
This is useful for values such as ZIP codes, which may look like integers but should be stored as ``TEXT`` to preserve leading zeros.
|
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``, ``BLOB`` or ``ANY``. Column types are matched case-insensitively.
|
The column type should be one of ``TEXT``, ``INTEGER``, ``FLOAT``, ``REAL`` or ``BLOB``. Column types are matched case-insensitively.
|
||||||
|
|
||||||
``ANY`` is especially useful with ``--strict``. An ``ANY`` column in a strict table preserves values without coercion, so text such as ``000123`` remains text instead of being converted to an integer:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
sqlite-utils insert events.db events events.csv --csv --strict \
|
|
||||||
--type payload any
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
@ -2148,12 +2141,6 @@ You can create a table in `SQLite STRICT mode <https://www.sqlite.org/stricttabl
|
||||||
|
|
||||||
sqlite-utils create-table mydb.db mytable id integer name text --strict
|
sqlite-utils create-table mydb.db mytable id integer name text --strict
|
||||||
|
|
||||||
Use the ``any`` type for a strict column that should accept integers, floating point values, text, binary data or null without coercion:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
sqlite-utils create-table events.db events id integer payload any --strict
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sqlite-utils tables mydb.db --schema -t
|
sqlite-utils tables mydb.db --schema -t
|
||||||
|
|
@ -2236,7 +2223,7 @@ The ``transform`` command allows you to apply complex transformations to a table
|
||||||
Every option for this table (with the exception of ``--pk-none``) can be specified multiple times. The options are as follows:
|
Every option for this table (with the exception of ``--pk-none``) can be specified multiple times. The options are as follows:
|
||||||
|
|
||||||
``--type column-name new-type``
|
``--type column-name new-type``
|
||||||
Change the type of the specified column. Valid types are ``integer``, ``text``, ``float``, ``real``, ``blob`` and ``any``. Changing a ``TEXT`` column to ``INTEGER``, ``FLOAT`` or ``REAL`` converts exact empty-string values to ``NULL``.
|
Change the type of the specified column. Valid types are ``integer``, ``text``, ``float``, ``blob``.
|
||||||
|
|
||||||
``--drop column-name``
|
``--drop column-name``
|
||||||
Drop the specified column.
|
Drop the specified column.
|
||||||
|
|
@ -2301,11 +2288,7 @@ If you want to see the SQL that will be executed to make the change without actu
|
||||||
INSERT INTO "roadside_attractions_new_4033a60276b9" ("longitude", "latitude", "id", "name")
|
INSERT INTO "roadside_attractions_new_4033a60276b9" ("longitude", "latitude", "id", "name")
|
||||||
SELECT "longitude", "latitude", "pk", "name" FROM "roadside_attractions";
|
SELECT "longitude", "latitude", "pk", "name" FROM "roadside_attractions";
|
||||||
DROP TABLE "roadside_attractions";
|
DROP TABLE "roadside_attractions";
|
||||||
PRAGMA legacy_alter_table=ON;
|
|
||||||
ALTER TABLE "roadside_attractions_new_4033a60276b9" RENAME TO "roadside_attractions";
|
ALTER TABLE "roadside_attractions_new_4033a60276b9" RENAME TO "roadside_attractions";
|
||||||
PRAGMA legacy_alter_table=OFF;
|
|
||||||
|
|
||||||
Tables that are referenced by views can be transformed - the view definitions are left unchanged, see :ref:`python_api_transform_views` for details.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
In Python: :ref:`table.transform() <python_api_transform>` CLI reference: :ref:`sqlite-utils transform <cli_ref_transform>`
|
In Python: :ref:`table.transform() <python_api_transform>` CLI reference: :ref:`sqlite-utils transform <cli_ref_transform>`
|
||||||
|
|
|
||||||
11
docs/conf.py
11
docs/conf.py
|
|
@ -1,7 +1,10 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import PIPE, CalledProcessError, Popen, check_output
|
from subprocess import Popen, PIPE, check_output
|
||||||
|
import sys
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
@ -47,7 +50,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 (CalledProcessError, OSError):
|
except Exception:
|
||||||
return "main"
|
return "main"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,7 +79,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 (OSError, TypeError, ValueError):
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if source_file is None:
|
if source_file is None:
|
||||||
|
|
|
||||||
|
|
@ -434,10 +434,9 @@ The library will never commit a transaction you opened. If you call write method
|
||||||
|
|
||||||
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.
|
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:
|
Two 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:
|
||||||
|
|
@ -828,19 +827,6 @@ You can pass ``strict=True`` to create a table in ``STRICT`` mode:
|
||||||
"name": str,
|
"name": str,
|
||||||
}, strict=True)
|
}, strict=True)
|
||||||
|
|
||||||
SQLite ``STRICT`` tables can use the ``ANY`` column type for values that should retain their exact SQLite storage class without coercion. Use the ``sqlite_utils.ANY`` marker type:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import sqlite_utils
|
|
||||||
|
|
||||||
db.table("events").create({
|
|
||||||
"id": int,
|
|
||||||
"payload": sqlite_utils.ANY,
|
|
||||||
}, pk="id", strict=True)
|
|
||||||
|
|
||||||
An ``ANY`` column can store integers, floating point values, text, binary data or ``None``. In a ``STRICT`` table a text value such as ``"000123"`` remains text with its leading zeroes intact. SQLite also accepts ``ANY`` columns in ordinary non-``STRICT`` tables, but those columns apply numeric affinity and would store that same value as the integer ``123``.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
In the CLI: :ref:`sqlite-utils create-table <cli_create_table>`
|
In the CLI: :ref:`sqlite-utils create-table <cli_create_table>`
|
||||||
|
|
||||||
|
|
@ -1582,7 +1568,7 @@ You can specify the ``col_type`` argument either using a SQLite type as a string
|
||||||
|
|
||||||
The ``col_type`` is optional - if you omit it the type of ``TEXT`` will be used.
|
The ``col_type`` is optional - if you omit it the type of ``TEXT`` will be used.
|
||||||
|
|
||||||
SQLite types you can specify are ``"TEXT"``, ``"INTEGER"``, ``"FLOAT"``, ``"REAL"``, ``"BLOB"`` or ``"ANY"``. You can use the ``sqlite_utils.ANY`` marker instead of the ``"ANY"`` string.
|
SQLite types you can specify are ``"TEXT"``, ``"INTEGER"``, ``"FLOAT"``, ``"REAL"`` or ``"BLOB"``.
|
||||||
|
|
||||||
If you pass a Python type, it will be mapped to SQLite types as shown here::
|
If you pass a Python type, it will be mapped to SQLite types as shown here::
|
||||||
|
|
||||||
|
|
@ -1595,7 +1581,6 @@ If you pass a Python type, it will be mapped to SQLite types as shown here::
|
||||||
datetime.date: "TEXT"
|
datetime.date: "TEXT"
|
||||||
datetime.time: "TEXT"
|
datetime.time: "TEXT"
|
||||||
datetime.timedelta: "TEXT"
|
datetime.timedelta: "TEXT"
|
||||||
sqlite_utils.ANY: "ANY"
|
|
||||||
|
|
||||||
# If numpy is installed
|
# If numpy is installed
|
||||||
np.int8: "INTEGER"
|
np.int8: "INTEGER"
|
||||||
|
|
@ -1826,8 +1811,6 @@ To alter the type of a column, use the ``types=`` argument:
|
||||||
# Convert the 'age' column to an integer, and 'weight' to a float
|
# Convert the 'age' column to an integer, and 'weight' to a float
|
||||||
table.transform(types={"age": int, "weight": float})
|
table.transform(types={"age": int, "weight": float})
|
||||||
|
|
||||||
When a ``TEXT`` column is changed to ``INTEGER``, ``FLOAT`` or ``REAL``, exact empty-string values are stored as ``NULL``. Other values, including whitespace-only strings, are copied normally.
|
|
||||||
|
|
||||||
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:
|
.. _python_api_transform_strict:
|
||||||
|
|
@ -1847,8 +1830,6 @@ Pass ``strict=False`` to convert a strict table back to a regular non-strict tab
|
||||||
|
|
||||||
table.transform(strict=False)
|
table.transform(strict=False)
|
||||||
|
|
||||||
If the table has ``ANY`` columns, converting it to non-strict mode can coerce text values that look numeric. For example, SQLite converts ``"000123"`` to the integer ``123`` when copying it into an ordinary ``ANY`` column. This is SQLite's documented distinction between `STRICT and ordinary ANY columns <https://www.sqlite.org/stricttables.html#the_any_datatype>`__.
|
|
||||||
|
|
||||||
The default is ``strict=None``, which preserves the table's existing strict mode.
|
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.
|
Passing ``strict=True`` raises ``sqlite_utils.db.TransformError`` if the available SQLite version does not support strict tables.
|
||||||
|
|
@ -2004,28 +1985,6 @@ A bare column name drops any foreign key that column participates in, including
|
||||||
|
|
||||||
Renaming a column with ``rename=`` updates any foreign keys that use it, and dropping a column with ``drop=`` also drops any foreign keys it participates in - for a compound foreign key this removes the whole constraint.
|
Renaming a column with ``rename=`` updates any foreign keys that use it, and dropping a column with ``drop=`` also drops any foreign keys it participates in - for a compound foreign key this removes the whole constraint.
|
||||||
|
|
||||||
.. _python_api_transform_check_constraints:
|
|
||||||
|
|
||||||
CHECK constraints
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
``.transform()`` preserves both column-level and table-level ``CHECK`` constraints. If a column is renamed, references to that column in the check expression are renamed too.
|
|
||||||
|
|
||||||
A column-level check is removed if its owning column is dropped. Dropping a column referenced by any remaining check raises ``TransformError`` instead of creating an invalid or unexpectedly weakened schema.
|
|
||||||
|
|
||||||
Comments immediately before or after a column definition are preserved too. They move with that column if it is renamed or reordered, and are removed if the column is dropped. A comment between two column definitions is treated as belonging to the following column.
|
|
||||||
|
|
||||||
.. _python_api_transform_views:
|
|
||||||
|
|
||||||
Tables referenced by views
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
Tables that are referenced by views can be safely transformed - the view definitions are left byte-for-byte unchanged, and views continue to read from the live table even when ``keep_table=`` is used to keep a copy of the original around.
|
|
||||||
|
|
||||||
A view that references a column which the transform renamed or dropped will remain defined but will raise a ``no such column`` error when it is next queried. This is inherent to SQLite views, whose SQL is stored as text - if you rename or drop columns that a view depends on you should update that view definition yourself.
|
|
||||||
|
|
||||||
To achieve this, the SQL produced by ``transform_sql()`` turns on ``PRAGMA legacy_alter_table`` for its ``ALTER TABLE ... RENAME TO`` statements, then restores the pragma to the value it had when the SQL was generated - without this, SQLite would attempt to rewrite references to the renamed table in every view definition, which fails when a view references the table that was just dropped.
|
|
||||||
|
|
||||||
.. _python_api_transform_sql:
|
.. _python_api_transform_sql:
|
||||||
|
|
||||||
Custom transformations with .transform_sql()
|
Custom transformations with .transform_sql()
|
||||||
|
|
@ -2037,36 +1996,6 @@ 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
|
||||||
|
|
@ -2476,11 +2405,6 @@ The ``.columns_dict`` property returns a dictionary version of the columns with
|
||||||
>>> db.table("PlantType").columns_dict
|
>>> db.table("PlantType").columns_dict
|
||||||
{'id': <class 'int'>, 'value': <class 'str'>}
|
{'id': <class 'int'>, 'value': <class 'str'>}
|
||||||
|
|
||||||
SQLite ``ANY`` columns are represented by the ``sqlite_utils.ANY`` marker type::
|
|
||||||
|
|
||||||
>>> db.table("events").columns_dict
|
|
||||||
{'id': <class 'int'>, 'payload': <class 'sqlite_utils.utils.ANY'>}
|
|
||||||
|
|
||||||
.. _python_api_introspection_default_values:
|
.. _python_api_introspection_default_values:
|
||||||
|
|
||||||
.default_values
|
.default_values
|
||||||
|
|
@ -2514,43 +2438,6 @@ Almost all SQLite tables have a ``rowid`` column, but a table with no explicitly
|
||||||
False
|
False
|
||||||
|
|
||||||
|
|
||||||
.. _python_api_introspection_checks:
|
|
||||||
|
|
||||||
.checks
|
|
||||||
-------
|
|
||||||
|
|
||||||
The ``.checks`` property returns the column-level and table-level ``CHECK`` constraints defined on a table, as a list of ``Check`` objects. Each object has ``check`` (the expression inside ``CHECK (...)``), ``name``, ``column`` and ``options`` attributes. ``column`` is an empty string for a table-level check. ``options`` contains a list of values only when a column check consists entirely of ``column IN (literal, ...)``. The original constraint fragment is available as ``sql``; ``start`` and ``end`` are its offsets within ``table.schema``.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
>>> db["scores"].checks
|
|
||||||
[Check(check='score > 0', name='positive', column='score', options=None),
|
|
||||||
Check(check='score <= maximum', name='within_maximum', column='', options=None)]
|
|
||||||
|
|
||||||
.. _python_api_introspection_column_checks:
|
|
||||||
|
|
||||||
.column_checks
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The ``.column_checks`` property returns the column-level checks grouped by column name:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
>>> db["scores"].column_checks
|
|
||||||
{'score': [Check(check='score > 0', name='positive', column='score', options=None)]}
|
|
||||||
|
|
||||||
.. _python_api_introspection_table_checks:
|
|
||||||
|
|
||||||
.table_checks
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The ``.table_checks`` property returns only the table-level checks:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
>>> db["scores"].table_checks
|
|
||||||
[Check(check='score <= maximum', name='within_maximum', column='', options=None)]
|
|
||||||
|
|
||||||
.. _python_api_introspection_foreign_keys:
|
.. _python_api_introspection_foreign_keys:
|
||||||
|
|
||||||
.foreign_keys
|
.foreign_keys
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "sqlite-utils"
|
name = "sqlite-utils"
|
||||||
version = "4.2.1"
|
version = "4.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 = [
|
||||||
|
|
@ -48,7 +48,6 @@ dev = [
|
||||||
# flake8
|
# flake8
|
||||||
"flake8",
|
"flake8",
|
||||||
"flake8-pyproject",
|
"flake8-pyproject",
|
||||||
"pyright>=1.1.411",
|
|
||||||
"ty>=0.0.37",
|
"ty>=0.0.37",
|
||||||
# For stable cog:
|
# For stable cog:
|
||||||
"tabulate>=0.10.0",
|
"tabulate>=0.10.0",
|
||||||
|
|
@ -80,14 +79,7 @@ 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 = [
|
extend-exclude = [".venv", "build", "dist", "docs", "sqlite_utils.egg-info"]
|
||||||
".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,13 +1,7 @@
|
||||||
|
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 ANY, suggest_column_types
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ["Database", "Migrations", "suggest_column_types", "hookimpl", "hookspec"]
|
||||||
"ANY",
|
|
||||||
"Database",
|
|
||||||
"Migrations",
|
|
||||||
"hookimpl",
|
|
||||||
"hookspec",
|
|
||||||
"suggest_column_types",
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,17 @@
|
||||||
import base64
|
import base64
|
||||||
import csv as csv_std
|
|
||||||
import difflib
|
import difflib
|
||||||
import hashlib
|
|
||||||
import inspect
|
|
||||||
import io
|
|
||||||
import itertools
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import pdb # noqa: T100
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from runpy import run_module
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import tabulate
|
|
||||||
from click_default_group import DefaultGroup
|
from click_default_group import DefaultGroup
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import hashlib
|
||||||
|
import pathlib
|
||||||
|
from runpy import run_module
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
from sqlite_utils import recipes
|
|
||||||
from sqlite_utils.db import (
|
from sqlite_utils.db import (
|
||||||
DEFAULT,
|
|
||||||
AlterError,
|
AlterError,
|
||||||
BadMultiValues,
|
BadMultiValues,
|
||||||
|
DEFAULT,
|
||||||
DescIndex,
|
DescIndex,
|
||||||
InvalidColumns,
|
InvalidColumns,
|
||||||
NoTable,
|
NoTable,
|
||||||
|
|
@ -32,28 +19,36 @@ from sqlite_utils.db import (
|
||||||
PrimaryKeyRequired,
|
PrimaryKeyRequired,
|
||||||
quote_identifier,
|
quote_identifier,
|
||||||
)
|
)
|
||||||
from sqlite_utils.plugins import ensure_plugins_loaded, get_plugins, pm
|
from sqlite_utils.plugins import ensure_plugins_loaded, pm, get_plugins
|
||||||
from sqlite_utils.utils import maximize_csv_field_size_limit
|
from sqlite_utils.utils import maximize_csv_field_size_limit
|
||||||
|
from sqlite_utils import recipes
|
||||||
|
import textwrap
|
||||||
|
import inspect
|
||||||
|
import io
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pdb
|
||||||
|
import sys
|
||||||
|
import csv as csv_std
|
||||||
|
import tabulate
|
||||||
from .utils import (
|
from .utils import (
|
||||||
Format,
|
|
||||||
OperationalError,
|
OperationalError,
|
||||||
TypeTracker,
|
|
||||||
_compile_code,
|
_compile_code,
|
||||||
chunks,
|
chunks,
|
||||||
decode_base64_values,
|
|
||||||
dedupe_keys,
|
dedupe_keys,
|
||||||
file_progress,
|
file_progress,
|
||||||
find_spatialite,
|
find_spatialite,
|
||||||
|
flatten as _flatten,
|
||||||
|
sqlite3,
|
||||||
|
decode_base64_values,
|
||||||
progressbar,
|
progressbar,
|
||||||
rows_from_file,
|
rows_from_file,
|
||||||
sqlite3,
|
Format,
|
||||||
)
|
TypeTracker,
|
||||||
from .utils import (
|
|
||||||
flatten as _flatten,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
def _register_db_for_cleanup(db):
|
def _register_db_for_cleanup(db):
|
||||||
|
|
@ -72,11 +67,11 @@ def _close_databases(ctx):
|
||||||
for db in ctx.meta.get("_databases_to_close", []):
|
for db in ctx.meta.get("_databases_to_close", []):
|
||||||
try:
|
try:
|
||||||
db.close()
|
db.close()
|
||||||
except sqlite3.Error:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
VALID_COLUMN_TYPES = ("INTEGER", "TEXT", "FLOAT", "REAL", "BLOB", "ANY")
|
VALID_COLUMN_TYPES = ("INTEGER", "TEXT", "FLOAT", "REAL", "BLOB")
|
||||||
|
|
||||||
UNICODE_ERROR = """
|
UNICODE_ERROR = """
|
||||||
{}
|
{}
|
||||||
|
|
@ -179,6 +174,7 @@ def functions_option(fn):
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def cli():
|
def cli():
|
||||||
"Commands for interacting with a SQLite database"
|
"Commands for interacting with a SQLite database"
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
|
@ -489,17 +485,7 @@ def dump(path, load_extension):
|
||||||
@click.argument(
|
@click.argument(
|
||||||
"col_type",
|
"col_type",
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
[
|
["integer", "int", "float", "real", "text", "str", "blob", "bytes"],
|
||||||
"integer",
|
|
||||||
"int",
|
|
||||||
"float",
|
|
||||||
"real",
|
|
||||||
"text",
|
|
||||||
"str",
|
|
||||||
"blob",
|
|
||||||
"bytes",
|
|
||||||
"any",
|
|
||||||
],
|
|
||||||
case_sensitive=False,
|
case_sensitive=False,
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
|
|
@ -905,7 +891,7 @@ def enable_counts(path, tables, load_extension):
|
||||||
# Check all tables exist
|
# Check all tables exist
|
||||||
bad_tables = [table for table in tables if not db[table].exists()]
|
bad_tables = [table for table in tables if not db[table].exists()]
|
||||||
if bad_tables:
|
if bad_tables:
|
||||||
raise click.ClickException(f"Invalid tables: {bad_tables}")
|
raise click.ClickException("Invalid tables: {}".format(bad_tables))
|
||||||
for table in tables:
|
for table in tables:
|
||||||
db.table(table).enable_counts()
|
db.table(table).enable_counts()
|
||||||
|
|
||||||
|
|
@ -1103,7 +1089,7 @@ def insert_upsert_implementation(
|
||||||
column_type_overrides = {column: ctype.upper() for column, ctype in (types or [])}
|
column_type_overrides = {column: ctype.upper() for column, ctype in (types or [])}
|
||||||
|
|
||||||
def _insert_docs(docs, tracker=None):
|
def _insert_docs(docs, tracker=None):
|
||||||
extra_kwargs: dict[str, Any] = {
|
extra_kwargs = {
|
||||||
"ignore": ignore,
|
"ignore": ignore,
|
||||||
"replace": replace,
|
"replace": replace,
|
||||||
"truncate": truncate,
|
"truncate": truncate,
|
||||||
|
|
@ -1154,7 +1140,9 @@ def insert_upsert_implementation(
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"{e.args[0]}\n\nTry using --alter to add additional columns"
|
"{}\n\nTry using --alter to add additional columns".format(
|
||||||
|
e.args[0]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# If we can find sql= and parameters= arguments, show those
|
# If we can find sql= and parameters= arguments, show those
|
||||||
variables = _find_variables(e.__traceback__, ["sql", "parameters"])
|
variables = _find_variables(e.__traceback__, ["sql", "parameters"])
|
||||||
|
|
@ -1252,7 +1240,7 @@ def insert_upsert_implementation(
|
||||||
reader = csv_std.reader(decoded, **csv_reader_args) # type: ignore
|
reader = csv_std.reader(decoded, **csv_reader_args) # type: ignore
|
||||||
first_row = next(reader)
|
first_row = next(reader)
|
||||||
if no_headers:
|
if no_headers:
|
||||||
headers = [f"untitled_{i + 1}" for i in range(len(first_row))]
|
headers = ["untitled_{}".format(i + 1) for i in range(len(first_row))]
|
||||||
reader = itertools.chain([first_row], reader)
|
reader = itertools.chain([first_row], reader)
|
||||||
else:
|
else:
|
||||||
headers = first_row
|
headers = first_row
|
||||||
|
|
@ -1281,7 +1269,9 @@ def insert_upsert_implementation(
|
||||||
docs = [docs]
|
docs = [docs]
|
||||||
except json.decoder.JSONDecodeError as ex:
|
except json.decoder.JSONDecodeError as ex:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Invalid JSON - use --csv for CSV or --tsv for TSV files\n\nJSON error: {ex}"
|
"Invalid JSON - use --csv for CSV or --tsv for TSV files\n\nJSON error: {}".format(
|
||||||
|
ex
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if flatten:
|
if flatten:
|
||||||
docs = (_flatten(doc) for doc in docs)
|
docs = (_flatten(doc) for doc in docs)
|
||||||
|
|
@ -1300,7 +1290,7 @@ def insert_upsert_implementation(
|
||||||
docs = (fn(doc["line"]) for doc in docs)
|
docs = (fn(doc["line"]) for doc in docs)
|
||||||
elif text:
|
elif text:
|
||||||
# Special case: this is allowed to be an iterable
|
# Special case: this is allowed to be an iterable
|
||||||
text_value = next(iter(docs))["text"]
|
text_value = list(docs)[0]["text"]
|
||||||
fn_return = fn(text_value)
|
fn_return = fn(text_value)
|
||||||
if isinstance(fn_return, dict):
|
if isinstance(fn_return, dict):
|
||||||
docs = [fn_return]
|
docs = [fn_return]
|
||||||
|
|
@ -1768,7 +1758,7 @@ def create_table(
|
||||||
height real \\
|
height real \\
|
||||||
photo blob --pk id
|
photo blob --pk id
|
||||||
|
|
||||||
Valid column types are text, integer, real, float, blob and any.
|
Valid column types are text, integer, real, float and blob.
|
||||||
"""
|
"""
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
_register_db_for_cleanup(db)
|
_register_db_for_cleanup(db)
|
||||||
|
|
@ -1784,13 +1774,16 @@ def create_table(
|
||||||
ctype = columns.pop(0)
|
ctype = columns.pop(0)
|
||||||
if ctype.upper() not in VALID_COLUMN_TYPES:
|
if ctype.upper() not in VALID_COLUMN_TYPES:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"column types must be one of {VALID_COLUMN_TYPES}"
|
"column types must be one of {}".format(VALID_COLUMN_TYPES)
|
||||||
)
|
)
|
||||||
coltypes[name] = ctype.upper()
|
coltypes[name] = ctype.upper()
|
||||||
# Does table already exist?
|
# Does table already exist?
|
||||||
if table in db.table_names() and not ignore and not replace and not transform:
|
if table in db.table_names():
|
||||||
|
if not ignore and not replace and not transform:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f'Table "{table}" already exists. Use --replace to delete and replace it.'
|
'Table "{}" already exists. Use --replace to delete and replace it.'.format(
|
||||||
|
table
|
||||||
|
)
|
||||||
)
|
)
|
||||||
db.table(table).create(
|
db.table(table).create(
|
||||||
coltypes,
|
coltypes,
|
||||||
|
|
@ -1826,7 +1819,7 @@ def duplicate(path, table, new_table, ignore, load_extension):
|
||||||
db.table(table).duplicate(new_table)
|
db.table(table).duplicate(new_table)
|
||||||
except NoTable:
|
except NoTable:
|
||||||
if not ignore:
|
if not ignore:
|
||||||
raise click.ClickException(f'Table "{table}" does not exist')
|
raise click.ClickException('Table "{}" does not exist'.format(table))
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="rename-table")
|
@cli.command(name="rename-table")
|
||||||
|
|
@ -1850,7 +1843,9 @@ def rename_table(path, table, new_name, ignore, load_extension):
|
||||||
db.rename_table(table, new_name)
|
db.rename_table(table, new_name)
|
||||||
except sqlite3.OperationalError as ex:
|
except sqlite3.OperationalError as ex:
|
||||||
if not ignore:
|
if not ignore:
|
||||||
raise click.ClickException(f'Table "{table}" could not be renamed. {ex!s}')
|
raise click.ClickException(
|
||||||
|
'Table "{}" could not be renamed. {}'.format(table, str(ex))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="drop-table")
|
@cli.command(name="drop-table")
|
||||||
|
|
@ -1879,10 +1874,10 @@ def drop_table(path, table, ignore, load_extension):
|
||||||
# A view exists with this name
|
# A view exists with this name
|
||||||
if not ignore:
|
if not ignore:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f'"{table}" is a view, not a table - use drop-view to drop it'
|
'"{}" is a view, not a table - use drop-view to drop it'.format(table)
|
||||||
)
|
)
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
raise click.ClickException(f'Table "{table}" does not exist')
|
raise click.ClickException('Table "{}" does not exist'.format(table))
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="create-view")
|
@cli.command(name="create-view")
|
||||||
|
|
@ -1924,7 +1919,9 @@ def create_view(path, view, select, ignore, replace, load_extension):
|
||||||
db.view(view).drop()
|
db.view(view).drop()
|
||||||
else:
|
else:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f'View "{view}" already exists. Use --replace to delete and replace it.'
|
'View "{}" already exists. Use --replace to delete and replace it.'.format(
|
||||||
|
view
|
||||||
|
)
|
||||||
)
|
)
|
||||||
db.create_view(view, select)
|
db.create_view(view, select)
|
||||||
|
|
||||||
|
|
@ -1956,9 +1953,9 @@ def drop_view(path, view, ignore, load_extension):
|
||||||
return
|
return
|
||||||
if view in db.table_names():
|
if view in db.table_names():
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f'"{view}" is a table, not a view - use drop-table to drop it'
|
'"{}" is a table, not a view - use drop-table to drop it'.format(view)
|
||||||
)
|
)
|
||||||
raise click.ClickException(f'View "{view}" does not exist')
|
raise click.ClickException('View "{}" does not exist'.format(view))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
|
@ -2180,7 +2177,7 @@ def memory(
|
||||||
file_path = pathlib.Path(path)
|
file_path = pathlib.Path(path)
|
||||||
stem = file_path.stem
|
stem = file_path.stem
|
||||||
if stem_counts.get(stem):
|
if stem_counts.get(stem):
|
||||||
file_table = f"{stem}_{stem_counts[stem]}"
|
file_table = "{}_{}".format(stem, stem_counts[stem])
|
||||||
else:
|
else:
|
||||||
file_table = stem
|
file_table = stem
|
||||||
stem_counts[stem] = stem_counts.get(stem, 1) + 1
|
stem_counts[stem] = stem_counts.get(stem, 1) + 1
|
||||||
|
|
@ -2199,14 +2196,14 @@ def memory(
|
||||||
if tracker is not None and db.table(file_table).exists():
|
if tracker is not None and db.table(file_table).exists():
|
||||||
db.table(file_table).transform(types=tracker.types)
|
db.table(file_table).transform(types=tracker.types)
|
||||||
# Add convenient t / t1 / t2 views
|
# Add convenient t / t1 / t2 views
|
||||||
view_names = [f"t{i + 1}"]
|
view_names = ["t{}".format(i + 1)]
|
||||||
if i == 0:
|
if i == 0:
|
||||||
view_names.append("t")
|
view_names.append("t")
|
||||||
for view_name in view_names:
|
for view_name in view_names:
|
||||||
if not db[view_name].exists():
|
if not db[view_name].exists():
|
||||||
db.create_view(
|
db.create_view(
|
||||||
view_name,
|
view_name,
|
||||||
f"select * from {quote_identifier(file_table)}",
|
"select * from {}".format(quote_identifier(file_table)),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if should_close_fp and fp:
|
if should_close_fp and fp:
|
||||||
|
|
@ -2376,17 +2373,19 @@ def search(
|
||||||
# Check table exists
|
# Check table exists
|
||||||
table_obj = db.table(dbtable)
|
table_obj = db.table(dbtable)
|
||||||
if not table_obj.exists():
|
if not table_obj.exists():
|
||||||
raise click.ClickException(f"Table '{dbtable}' does not exist")
|
raise click.ClickException("Table '{}' does not exist".format(dbtable))
|
||||||
if not table_obj.detect_fts():
|
if not table_obj.detect_fts():
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Table '{dbtable}' is not configured for full-text search"
|
"Table '{}' is not configured for full-text search".format(dbtable)
|
||||||
)
|
)
|
||||||
if column:
|
if column:
|
||||||
# Check they all exist
|
# Check they all exist
|
||||||
table_columns = table_obj.columns_dict
|
table_columns = table_obj.columns_dict
|
||||||
for c in column:
|
for c in column:
|
||||||
if c not in table_columns:
|
if c not in table_columns:
|
||||||
raise click.ClickException(f"Table '{dbtable}' has no column '{c}")
|
raise click.ClickException(
|
||||||
|
"Table '{}' has no column '{}".format(dbtable, c)
|
||||||
|
)
|
||||||
sql = table_obj.search_sql(columns=column, order_by=order, limit=limit)
|
sql = table_obj.search_sql(columns=column, order_by=order, limit=limit)
|
||||||
if show_sql:
|
if show_sql:
|
||||||
click.echo(sql)
|
click.echo(sql)
|
||||||
|
|
@ -2413,7 +2412,7 @@ def search(
|
||||||
except click.ClickException as e:
|
except click.ClickException as e:
|
||||||
if "malformed MATCH expression" in str(e) or "unterminated string" in str(e):
|
if "malformed MATCH expression" in str(e) or "unterminated string" in str(e):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"{e!s}\n\nTry running this again with the --quote option"
|
"{}\n\nTry running this again with the --quote option".format(str(e))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
@ -2480,17 +2479,15 @@ def rows(
|
||||||
columns = "*"
|
columns = "*"
|
||||||
if column:
|
if column:
|
||||||
columns = ", ".join(quote_identifier(c) for c in column)
|
columns = ", ".join(quote_identifier(c) for c in column)
|
||||||
sql = f"select {columns} from {quote_identifier(dbtable)}"
|
sql = "select {} from {}".format(columns, quote_identifier(dbtable))
|
||||||
if where:
|
if where:
|
||||||
sql += " where " + where
|
sql += " where " + where
|
||||||
if order:
|
if order:
|
||||||
sql += " order by " + order
|
sql += " order by " + order
|
||||||
if limit:
|
if limit:
|
||||||
sql += f" limit {limit}"
|
sql += " limit {}".format(limit)
|
||||||
if offset:
|
if offset:
|
||||||
if not limit:
|
sql += " offset {}".format(offset)
|
||||||
sql += " limit -1"
|
|
||||||
sql += f" offset {offset}"
|
|
||||||
ctx.invoke(
|
ctx.invoke(
|
||||||
query,
|
query,
|
||||||
path=path,
|
path=path,
|
||||||
|
|
@ -2678,10 +2675,12 @@ def schema(
|
||||||
"--type",
|
"--type",
|
||||||
type=(
|
type=(
|
||||||
str,
|
str,
|
||||||
click.Choice(list(VALID_COLUMN_TYPES), case_sensitive=False),
|
click.Choice(
|
||||||
|
["INTEGER", "TEXT", "FLOAT", "REAL", "BLOB"], case_sensitive=False
|
||||||
|
),
|
||||||
),
|
),
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Change column type to INTEGER, TEXT, FLOAT, REAL, BLOB or ANY",
|
help="Change column type to INTEGER, TEXT, FLOAT, REAL or BLOB",
|
||||||
)
|
)
|
||||||
@click.option("--drop", type=str, multiple=True, help="Drop this column")
|
@click.option("--drop", type=str, multiple=True, help="Drop this column")
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|
@ -2761,7 +2760,7 @@ def transform(
|
||||||
for column, ctype in type:
|
for column, ctype in type:
|
||||||
if ctype.upper() not in VALID_COLUMN_TYPES:
|
if ctype.upper() not in VALID_COLUMN_TYPES:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"column types must be one of {VALID_COLUMN_TYPES}"
|
"column types must be one of {}".format(VALID_COLUMN_TYPES)
|
||||||
)
|
)
|
||||||
types[column] = ctype.upper()
|
types[column] = ctype.upper()
|
||||||
|
|
||||||
|
|
@ -2859,12 +2858,12 @@ def extract(
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
_register_db_for_cleanup(db)
|
_register_db_for_cleanup(db)
|
||||||
_load_extensions(db, load_extension)
|
_load_extensions(db, load_extension)
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = dict(
|
||||||
"columns": columns,
|
columns=columns,
|
||||||
"table": other_table,
|
table=other_table,
|
||||||
"fk_column": fk_column,
|
fk_column=fk_column,
|
||||||
"rename": dict(rename),
|
rename=dict(rename),
|
||||||
}
|
)
|
||||||
try:
|
try:
|
||||||
db.table(table).extract(**kwargs)
|
db.table(table).extract(**kwargs)
|
||||||
except (NoTable, InvalidColumns) as e:
|
except (NoTable, InvalidColumns) as e:
|
||||||
|
|
@ -2959,7 +2958,7 @@ def insert_files(
|
||||||
with progressbar(paths_and_relative_paths, silent=silent) as bar:
|
with progressbar(paths_and_relative_paths, silent=silent) as bar:
|
||||||
|
|
||||||
def to_insert():
|
def to_insert():
|
||||||
for file_path, relative_path in bar:
|
for path, relative_path in bar:
|
||||||
row = {}
|
row = {}
|
||||||
# content_text is special case as it considers 'encoding'
|
# content_text is special case as it considers 'encoding'
|
||||||
|
|
||||||
|
|
@ -2971,21 +2970,19 @@ def insert_files(
|
||||||
raise UnicodeDecodeErrorForPath(e, resolved)
|
raise UnicodeDecodeErrorForPath(e, resolved)
|
||||||
|
|
||||||
lookups = dict(FILE_COLUMNS, content_text=_content_text)
|
lookups = dict(FILE_COLUMNS, content_text=_content_text)
|
||||||
if file_path == "-":
|
if path == "-":
|
||||||
stdin_data = sys.stdin.buffer.read()
|
stdin_data = sys.stdin.buffer.read()
|
||||||
# We only support a subset of columns for this case
|
# We only support a subset of columns for this case
|
||||||
lookups = {
|
lookups = {
|
||||||
"name": lambda p: name or "-",
|
"name": lambda p: name or "-",
|
||||||
"path": lambda p: name or "-",
|
"path": lambda p: name or "-",
|
||||||
"content": lambda p, data=stdin_data: data,
|
"content": lambda p: stdin_data,
|
||||||
"content_text": lambda p, data=stdin_data: data.decode(
|
"content_text": lambda p: stdin_data.decode(
|
||||||
encoding or "utf-8"
|
encoding or "utf-8"
|
||||||
),
|
),
|
||||||
"sha256": lambda p, data=stdin_data: hashlib.sha256(
|
"sha256": lambda p: hashlib.sha256(stdin_data).hexdigest(),
|
||||||
data
|
"md5": lambda p: hashlib.md5(stdin_data).hexdigest(),
|
||||||
).hexdigest(),
|
"size": lambda p: len(stdin_data),
|
||||||
"md5": lambda p, data=stdin_data: hashlib.md5(data).hexdigest(),
|
|
||||||
"size": lambda p, data=stdin_data: len(data),
|
|
||||||
}
|
}
|
||||||
for coldef in column:
|
for coldef in column:
|
||||||
if ":" in coldef:
|
if ":" in coldef:
|
||||||
|
|
@ -2993,7 +2990,7 @@ def insert_files(
|
||||||
else:
|
else:
|
||||||
colname, coltype = coldef, coldef
|
colname, coltype = coldef, coldef
|
||||||
try:
|
try:
|
||||||
value = lookups[coltype](file_path)
|
value = lookups[coltype](path)
|
||||||
row[colname] = value
|
row[colname] = value
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
|
|
@ -3021,7 +3018,7 @@ def insert_files(
|
||||||
except UnicodeDecodeErrorForPath as e:
|
except UnicodeDecodeErrorForPath as e:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
UNICODE_ERROR.format(
|
UNICODE_ERROR.format(
|
||||||
f"Could not read file '{e.path}' as text\n\n{e.exception}"
|
"Could not read file '{}' as text\n\n{}".format(e.path, e.exception)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3199,7 +3196,7 @@ def _generate_convert_help():
|
||||||
for name in recipe_names:
|
for name in recipe_names:
|
||||||
fn = getattr(recipes, name)
|
fn = getattr(recipes, name)
|
||||||
doc = textwrap.dedent(fn.__doc__.rstrip()).replace("\b\n", "")
|
doc = textwrap.dedent(fn.__doc__.rstrip()).replace("\b\n", "")
|
||||||
help += f"\n\nr.{name}{inspect.signature(fn)!s}\n\n\b{doc}"
|
help += "\n\nr.{}{}\n\n\b{}".format(name, str(inspect.signature(fn)), doc)
|
||||||
help += "\n\n"
|
help += "\n\n"
|
||||||
help += textwrap.dedent("""
|
help += textwrap.dedent("""
|
||||||
You can use these recipes like so:
|
You can use these recipes like so:
|
||||||
|
|
@ -3283,21 +3280,26 @@ def convert(
|
||||||
raise click.ClickException(str(e))
|
raise click.ClickException(str(e))
|
||||||
if dry_run:
|
if dry_run:
|
||||||
# Pull first 20 values for first column and preview them
|
# Pull first 20 values for first column and preview them
|
||||||
def preview(v):
|
|
||||||
if multi:
|
if multi:
|
||||||
|
|
||||||
|
def preview(v):
|
||||||
return json.dumps(fn(v), default=repr, ensure_ascii=False) if v else v
|
return json.dumps(fn(v), default=repr, ensure_ascii=False) if v else v
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def preview(v):
|
||||||
return fn(v) if v else v
|
return fn(v) if v else v
|
||||||
|
|
||||||
db.conn.create_function("preview_transform", 1, preview)
|
db.conn.create_function("preview_transform", 1, preview)
|
||||||
sql = """
|
sql = """
|
||||||
select
|
select
|
||||||
{column} as value,
|
[{column}] as value,
|
||||||
preview_transform({column}) as preview
|
preview_transform([{column}]) as preview
|
||||||
from {table}{where} limit 10
|
from [{table}]{where} limit 10
|
||||||
""".format(
|
""".format(
|
||||||
column=quote_identifier(columns[0]),
|
column=columns[0],
|
||||||
table=quote_identifier(table),
|
table=table,
|
||||||
where=f" where {where}" if where is not None else "",
|
where=" where {}".format(where) if where is not None else "",
|
||||||
)
|
)
|
||||||
for row in db.conn.execute(sql, where_args).fetchall():
|
for row in db.conn.execute(sql, where_args).fetchall():
|
||||||
click.echo(str(row[0]))
|
click.echo(str(row[0]))
|
||||||
|
|
@ -3317,7 +3319,7 @@ def convert(
|
||||||
def wrapped_fn(value):
|
def wrapped_fn(value):
|
||||||
try:
|
try:
|
||||||
return fn_(value)
|
return fn_(value)
|
||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex:
|
||||||
print("\nException raised, dropping into pdb...:", ex)
|
print("\nException raised, dropping into pdb...:", ex)
|
||||||
pdb.post_mortem(ex.__traceback__)
|
pdb.post_mortem(ex.__traceback__)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
@ -3337,7 +3339,9 @@ def convert(
|
||||||
)
|
)
|
||||||
except BadMultiValues as e:
|
except BadMultiValues as e:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"When using --multi code must return a Python dictionary - returned: {e.values!r}"
|
"When using --multi code must return a Python dictionary - returned: {}".format(
|
||||||
|
repr(e.values)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3455,7 +3459,7 @@ def create_spatial_index(db_path, table, column_name, load_extension):
|
||||||
|
|
||||||
def _find_migration_files(migrations):
|
def _find_migration_files(migrations):
|
||||||
if not migrations:
|
if not migrations:
|
||||||
migrations = [pathlib.Path.cwd()]
|
migrations = [pathlib.Path(".").resolve()]
|
||||||
files = set()
|
files = set()
|
||||||
for path_str in migrations:
|
for path_str in migrations:
|
||||||
path = pathlib.Path(path_str)
|
path = pathlib.Path(path_str)
|
||||||
|
|
@ -3480,7 +3484,7 @@ def _load_migration_sets(files):
|
||||||
"__file__": str(filepath),
|
"__file__": str(filepath),
|
||||||
"__name__": "__sqlite_utils_migration__",
|
"__name__": "__sqlite_utils_migration__",
|
||||||
}
|
}
|
||||||
exec(code, namespace) # noqa: S102
|
exec(code, namespace)
|
||||||
migration_sets.extend(
|
migration_sets.extend(
|
||||||
obj for obj in namespace.values() if _compatible_migration_set(obj)
|
obj for obj in namespace.values() if _compatible_migration_set(obj)
|
||||||
)
|
)
|
||||||
|
|
@ -3489,17 +3493,17 @@ def _load_migration_sets(files):
|
||||||
|
|
||||||
def _display_migration_list(db, migration_sets):
|
def _display_migration_list(db, migration_sets):
|
||||||
for migration_set in migration_sets:
|
for migration_set in migration_sets:
|
||||||
click.echo(f"Migrations for: {migration_set.name}")
|
click.echo("Migrations for: {}".format(migration_set.name))
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(" Applied:")
|
click.echo(" Applied:")
|
||||||
for migration in migration_set.applied(db):
|
for migration in migration_set.applied(db):
|
||||||
click.echo(f" {migration.name} - {migration.applied_at}")
|
click.echo(" {} - {}".format(migration.name, migration.applied_at))
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(" Pending:")
|
click.echo(" Pending:")
|
||||||
output = False
|
output = False
|
||||||
for migration in migration_set.pending(db):
|
for migration in migration_set.pending(db):
|
||||||
output = True
|
output = True
|
||||||
click.echo(f" {migration.name}")
|
click.echo(" {}".format(migration.name))
|
||||||
if not output:
|
if not output:
|
||||||
click.echo(" (none)")
|
click.echo(" (none)")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
@ -3579,7 +3583,7 @@ def migrate(db_path, migrations, stop_before, list_, verbose):
|
||||||
|
|
||||||
prev_schema = db.schema
|
prev_schema = db.schema
|
||||||
if verbose:
|
if verbose:
|
||||||
click.echo(f"Migrating {db_path}")
|
click.echo("Migrating {}".format(db_path))
|
||||||
click.echo("\nSchema before:\n")
|
click.echo("\nSchema before:\n")
|
||||||
click.echo(textwrap.indent(prev_schema, " ") or " (empty)")
|
click.echo(textwrap.indent(prev_schema, " ") or " (empty)")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
@ -3590,7 +3594,9 @@ def migrate(db_path, migrations, stop_before, list_, verbose):
|
||||||
names = {m.name for m in migration_set.pending(db)}
|
names = {m.name for m in migration_set.pending(db)}
|
||||||
names.update(m.name for m in migration_set.applied(db))
|
names.update(m.name for m in migration_set.applied(db))
|
||||||
known_names.update(names)
|
known_names.update(names)
|
||||||
known_names.update(f"{migration_set.name}:{name}" for name in names)
|
known_names.update(
|
||||||
|
"{}:{}".format(migration_set.name, name) for name in names
|
||||||
|
)
|
||||||
unknown = [value for value in stop_before if value not in known_names]
|
unknown = [value for value in stop_before if value not in known_names]
|
||||||
if unknown:
|
if unknown:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
|
|
@ -3646,7 +3652,7 @@ def _render_common(title, values):
|
||||||
return ""
|
return ""
|
||||||
lines = [title]
|
lines = [title]
|
||||||
for value, count in values:
|
for value, count in values:
|
||||||
lines.append(f" {count}: {value}")
|
lines.append(" {}: {}".format(count, value))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3716,7 +3722,7 @@ def maybe_json(value):
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
stripped = value.strip()
|
stripped = value.strip()
|
||||||
if not (stripped.startswith(("{", "["))):
|
if not (stripped.startswith("{") or stripped.startswith("[")):
|
||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
return json.loads(stripped)
|
return json.loads(stripped)
|
||||||
|
|
@ -3734,7 +3740,7 @@ def json_binary(value):
|
||||||
def verify_is_dict(doc):
|
def verify_is_dict(doc):
|
||||||
if not isinstance(doc, dict):
|
if not isinstance(doc, dict):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Rows must all be dictionaries, got: {repr(doc)[:1000]}"
|
"Rows must all be dictionaries, got: {}".format(repr(doc)[:1000])
|
||||||
)
|
)
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
@ -3762,14 +3768,14 @@ def _register_functions(db, functions):
|
||||||
try:
|
try:
|
||||||
functions = pathlib.Path(functions).read_text()
|
functions = pathlib.Path(functions).read_text()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise click.ClickException(f"File not found: {functions}")
|
raise click.ClickException("File not found: {}".format(functions))
|
||||||
|
|
||||||
sqlite3.enable_callback_tracebacks(True)
|
sqlite3.enable_callback_tracebacks(True)
|
||||||
globals = {}
|
globals = {}
|
||||||
try:
|
try:
|
||||||
exec(functions, globals) # noqa: S102
|
exec(functions, globals)
|
||||||
except SyntaxError as ex:
|
except SyntaxError as ex:
|
||||||
raise click.ClickException(f"Error in functions definition: {ex}")
|
raise click.ClickException("Error in functions definition: {}".format(ex))
|
||||||
# Register all callables in the locals dict:
|
# Register all callables in the locals dict:
|
||||||
for name, value in globals.items():
|
for name, value in globals.items():
|
||||||
if callable(value) and not name.startswith("_"):
|
if callable(value) and not name.startswith("_"):
|
||||||
|
|
@ -3790,13 +3796,13 @@ def _rows_from_code(code):
|
||||||
try:
|
try:
|
||||||
code = pathlib.Path(code).read_text()
|
code = pathlib.Path(code).read_text()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise click.ClickException(f"File not found: {code}")
|
raise click.ClickException("File not found: {}".format(code))
|
||||||
namespace: dict[str, Any] = {}
|
namespace = {}
|
||||||
try:
|
try:
|
||||||
exec(code, namespace) # noqa: S102
|
exec(code, namespace)
|
||||||
except SyntaxError as ex:
|
except SyntaxError as ex:
|
||||||
raise click.ClickException(f"Error in --code: {ex}")
|
raise click.ClickException("Error in --code: {}".format(ex))
|
||||||
rows: Any = namespace.get("rows")
|
rows = namespace.get("rows")
|
||||||
if callable(rows):
|
if callable(rows):
|
||||||
rows = rows()
|
rows = rows()
|
||||||
if isinstance(rows, dict):
|
if isinstance(rows, dict):
|
||||||
|
|
|
||||||
|
|
@ -1,897 +0,0 @@
|
||||||
"""Helpers for parsing constraints from SQLite CREATE TABLE SQL.
|
|
||||||
|
|
||||||
SQLite does not expose CHECK constraints through a pragma, so preserving them
|
|
||||||
across a table rebuild requires reading ``sqlite_schema.sql``. This module is
|
|
||||||
deliberately small, but it uses a real lexer: strings, quoted identifiers and
|
|
||||||
comments are opaque, every token retains its source span and malformed input is
|
|
||||||
reported instead of being silently under-parsed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Check:
|
|
||||||
check: str
|
|
||||||
name: str = ""
|
|
||||||
column: str = ""
|
|
||||||
options: list[Any] | None = None
|
|
||||||
# Source details are excluded from equality and repr so callers can compare
|
|
||||||
# semantic constraints while still having the original SQL available for
|
|
||||||
# diagnostics or future lossless edits.
|
|
||||||
sql: str = field(default="", compare=False, repr=False)
|
|
||||||
start: int = field(default=-1, compare=False, repr=False)
|
|
||||||
end: int = field(default=-1, compare=False, repr=False)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ColumnComments:
|
|
||||||
before: str = ""
|
|
||||||
after: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class UniqueColumn:
|
|
||||||
name: str
|
|
||||||
collation: str = ""
|
|
||||||
order: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Unique:
|
|
||||||
columns: tuple[UniqueColumn, ...]
|
|
||||||
name: str = ""
|
|
||||||
column: str = ""
|
|
||||||
conflict: str = ""
|
|
||||||
sql: str = field(default="", compare=False, repr=False)
|
|
||||||
start: int = field(default=-1, compare=False, repr=False)
|
|
||||||
end: int = field(default=-1, compare=False, repr=False)
|
|
||||||
|
|
||||||
|
|
||||||
class ParseError(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class _Token:
|
|
||||||
kind: str
|
|
||||||
text: str
|
|
||||||
start: int
|
|
||||||
end: int
|
|
||||||
|
|
||||||
def is_keyword(self, keyword: str) -> bool:
|
|
||||||
return self.kind == "word" and self.text.upper() == keyword
|
|
||||||
|
|
||||||
|
|
||||||
_PUNCTUATION = frozenset("(),.;+-*/%<>=!~|&?:")
|
|
||||||
_TRIVIA = frozenset(("whitespace", "comment"))
|
|
||||||
_TABLE_CONSTRAINT_KEYWORDS = frozenset(("PRIMARY", "UNIQUE", "CHECK", "FOREIGN"))
|
|
||||||
_OTHER_COLUMN_CONSTRAINT_KEYWORDS = frozenset(
|
|
||||||
("PRIMARY", "UNIQUE", "REFERENCES", "DEFAULT", "NOT", "COLLATE", "GENERATED")
|
|
||||||
)
|
|
||||||
_SQLITE_KEYWORDS = frozenset(
|
|
||||||
(
|
|
||||||
"ABORT",
|
|
||||||
"ACTION",
|
|
||||||
"ADD",
|
|
||||||
"AFTER",
|
|
||||||
"ALL",
|
|
||||||
"ALTER",
|
|
||||||
"ANALYZE",
|
|
||||||
"AND",
|
|
||||||
"AS",
|
|
||||||
"ASC",
|
|
||||||
"ATTACH",
|
|
||||||
"AUTOINCREMENT",
|
|
||||||
"BEFORE",
|
|
||||||
"BEGIN",
|
|
||||||
"BETWEEN",
|
|
||||||
"BY",
|
|
||||||
"CASCADE",
|
|
||||||
"CASE",
|
|
||||||
"CAST",
|
|
||||||
"CHECK",
|
|
||||||
"COLLATE",
|
|
||||||
"COLUMN",
|
|
||||||
"COMMIT",
|
|
||||||
"CONFLICT",
|
|
||||||
"CONSTRAINT",
|
|
||||||
"CREATE",
|
|
||||||
"CROSS",
|
|
||||||
"CURRENT_DATE",
|
|
||||||
"CURRENT_TIME",
|
|
||||||
"CURRENT_TIMESTAMP",
|
|
||||||
"DATABASE",
|
|
||||||
"DEFAULT",
|
|
||||||
"DEFERRABLE",
|
|
||||||
"DEFERRED",
|
|
||||||
"DELETE",
|
|
||||||
"DESC",
|
|
||||||
"DETACH",
|
|
||||||
"DISTINCT",
|
|
||||||
"DO",
|
|
||||||
"DROP",
|
|
||||||
"EACH",
|
|
||||||
"ELSE",
|
|
||||||
"END",
|
|
||||||
"ESCAPE",
|
|
||||||
"EXCEPT",
|
|
||||||
"EXCLUDE",
|
|
||||||
"EXCLUSIVE",
|
|
||||||
"EXISTS",
|
|
||||||
"EXPLAIN",
|
|
||||||
"FAIL",
|
|
||||||
"FALSE",
|
|
||||||
"FILTER",
|
|
||||||
"FIRST",
|
|
||||||
"FOLLOWING",
|
|
||||||
"FOR",
|
|
||||||
"FOREIGN",
|
|
||||||
"FROM",
|
|
||||||
"FULL",
|
|
||||||
"GENERATED",
|
|
||||||
"GLOB",
|
|
||||||
"GROUP",
|
|
||||||
"GROUPS",
|
|
||||||
"HAVING",
|
|
||||||
"IF",
|
|
||||||
"IGNORE",
|
|
||||||
"IMMEDIATE",
|
|
||||||
"IN",
|
|
||||||
"INDEX",
|
|
||||||
"INDEXED",
|
|
||||||
"INITIALLY",
|
|
||||||
"INNER",
|
|
||||||
"INSERT",
|
|
||||||
"INSTEAD",
|
|
||||||
"INTERSECT",
|
|
||||||
"INTO",
|
|
||||||
"IS",
|
|
||||||
"ISNULL",
|
|
||||||
"JOIN",
|
|
||||||
"KEY",
|
|
||||||
"LAST",
|
|
||||||
"LEFT",
|
|
||||||
"LIKE",
|
|
||||||
"LIMIT",
|
|
||||||
"MATCH",
|
|
||||||
"MATERIALIZED",
|
|
||||||
"NATURAL",
|
|
||||||
"NO",
|
|
||||||
"NOT",
|
|
||||||
"NOTHING",
|
|
||||||
"NOTNULL",
|
|
||||||
"NULL",
|
|
||||||
"NULLS",
|
|
||||||
"OF",
|
|
||||||
"OFFSET",
|
|
||||||
"ON",
|
|
||||||
"OR",
|
|
||||||
"ORDER",
|
|
||||||
"OTHERS",
|
|
||||||
"OUTER",
|
|
||||||
"OVER",
|
|
||||||
"PARTITION",
|
|
||||||
"PLAN",
|
|
||||||
"PRAGMA",
|
|
||||||
"PRECEDING",
|
|
||||||
"PRIMARY",
|
|
||||||
"QUERY",
|
|
||||||
"RAISE",
|
|
||||||
"RANGE",
|
|
||||||
"RECURSIVE",
|
|
||||||
"REFERENCES",
|
|
||||||
"REGEXP",
|
|
||||||
"REINDEX",
|
|
||||||
"RELEASE",
|
|
||||||
"RENAME",
|
|
||||||
"REPLACE",
|
|
||||||
"RESTRICT",
|
|
||||||
"RETURNING",
|
|
||||||
"RIGHT",
|
|
||||||
"ROLLBACK",
|
|
||||||
"ROW",
|
|
||||||
"ROWS",
|
|
||||||
"SAVEPOINT",
|
|
||||||
"SELECT",
|
|
||||||
"SET",
|
|
||||||
"STRICT",
|
|
||||||
"TABLE",
|
|
||||||
"TEMP",
|
|
||||||
"TEMPORARY",
|
|
||||||
"THEN",
|
|
||||||
"TIES",
|
|
||||||
"TO",
|
|
||||||
"TRANSACTION",
|
|
||||||
"TRIGGER",
|
|
||||||
"TRUE",
|
|
||||||
"UNBOUNDED",
|
|
||||||
"UNION",
|
|
||||||
"UNIQUE",
|
|
||||||
"UPDATE",
|
|
||||||
"USING",
|
|
||||||
"VACUUM",
|
|
||||||
"VALUES",
|
|
||||||
"VIEW",
|
|
||||||
"VIRTUAL",
|
|
||||||
"WHEN",
|
|
||||||
"WHERE",
|
|
||||||
"WINDOW",
|
|
||||||
"WITH",
|
|
||||||
"WITHOUT",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
_INTEGER_RE = re.compile(r"[+-]?(?:0[xX][0-9a-fA-F]+|[0-9]+)\Z")
|
|
||||||
_FLOAT_RE = re.compile(
|
|
||||||
r"[+-]?(?:(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|"
|
|
||||||
r"[0-9]+[eE][+-]?[0-9]+)\Z"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _lex(sql: str) -> list[_Token]:
|
|
||||||
tokens: list[_Token] = []
|
|
||||||
i = 0
|
|
||||||
while i < len(sql):
|
|
||||||
start = i
|
|
||||||
char = sql[i]
|
|
||||||
if char.isspace():
|
|
||||||
i += 1
|
|
||||||
while i < len(sql) and sql[i].isspace():
|
|
||||||
i += 1
|
|
||||||
tokens.append(_Token("whitespace", sql[start:i], start, i))
|
|
||||||
continue
|
|
||||||
if sql.startswith("--", i):
|
|
||||||
newline = sql.find("\n", i + 2)
|
|
||||||
i = len(sql) if newline == -1 else newline + 1
|
|
||||||
tokens.append(_Token("comment", sql[start:i], start, i))
|
|
||||||
continue
|
|
||||||
if sql.startswith("/*", i):
|
|
||||||
end = sql.find("*/", i + 2)
|
|
||||||
if end == -1:
|
|
||||||
raise ParseError("Unterminated SQL comment")
|
|
||||||
i = end + 2
|
|
||||||
tokens.append(_Token("comment", sql[start:i], start, i))
|
|
||||||
continue
|
|
||||||
if char in ("'", '"', "`"):
|
|
||||||
quote = char
|
|
||||||
i += 1
|
|
||||||
while i < len(sql):
|
|
||||||
if sql[i] == quote:
|
|
||||||
if i + 1 < len(sql) and sql[i + 1] == quote:
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
i += 1
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
raise ParseError(f"Unterminated {quote} quoted token")
|
|
||||||
kind = "string" if quote == "'" else "identifier"
|
|
||||||
tokens.append(_Token(kind, sql[start:i], start, i))
|
|
||||||
continue
|
|
||||||
if char == "[":
|
|
||||||
end = sql.find("]", i + 1)
|
|
||||||
if end == -1:
|
|
||||||
raise ParseError("Unterminated [ quoted identifier")
|
|
||||||
i = end + 1
|
|
||||||
tokens.append(_Token("identifier", sql[start:i], start, i))
|
|
||||||
continue
|
|
||||||
if char in _PUNCTUATION:
|
|
||||||
i += 1
|
|
||||||
tokens.append(_Token("punct", char, start, i))
|
|
||||||
continue
|
|
||||||
# SQLite accepts any character >= U+0080 in a bare identifier. More
|
|
||||||
# generally, consume until a lexical delimiter rather than relying on
|
|
||||||
# Python's narrower definition of an alphanumeric character.
|
|
||||||
i += 1
|
|
||||||
while i < len(sql):
|
|
||||||
if sql[i].isspace() or sql[i] in _PUNCTUATION or sql[i] in "'\"`[":
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
tokens.append(_Token("word", sql[start:i], start, i))
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _meaningful(tokens: list[_Token]) -> list[_Token]:
|
|
||||||
return [token for token in tokens if token.kind not in _TRIVIA]
|
|
||||||
|
|
||||||
|
|
||||||
def _unquote(token: str) -> str:
|
|
||||||
if len(token) >= 2 and token[0] in ("'", '"', "`") and token[-1] == token[0]:
|
|
||||||
return token[1:-1].replace(token[0] * 2, token[0])
|
|
||||||
if len(token) >= 2 and token[0] == "[" and token[-1] == "]":
|
|
||||||
return token[1:-1]
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def _matching_paren(tokens: list[_Token], open_index: int) -> int:
|
|
||||||
if tokens[open_index].text != "(":
|
|
||||||
raise ParseError("Expected an opening parenthesis")
|
|
||||||
depth = 0
|
|
||||||
for index in range(open_index, len(tokens)):
|
|
||||||
if tokens[index].text == "(":
|
|
||||||
depth += 1
|
|
||||||
elif tokens[index].text == ")":
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
return index
|
|
||||||
raise ParseError("Unbalanced parentheses")
|
|
||||||
|
|
||||||
|
|
||||||
def _split_spans(sql: str, tokens: list[_Token]) -> list[tuple[str, int, int]]:
|
|
||||||
if not tokens:
|
|
||||||
return []
|
|
||||||
items: list[tuple[str, int, int]] = []
|
|
||||||
depth = 0
|
|
||||||
start = tokens[0].start
|
|
||||||
for token in tokens:
|
|
||||||
if token.text == "(":
|
|
||||||
depth += 1
|
|
||||||
elif token.text == ")":
|
|
||||||
depth -= 1
|
|
||||||
if depth < 0:
|
|
||||||
raise ParseError("Unbalanced parentheses")
|
|
||||||
elif token.text == "," and depth == 0:
|
|
||||||
raw = sql[start : token.start]
|
|
||||||
item = raw.strip()
|
|
||||||
if item:
|
|
||||||
item_start = start + len(raw) - len(raw.lstrip())
|
|
||||||
items.append((item, item_start, item_start + len(item)))
|
|
||||||
start = token.end
|
|
||||||
if depth:
|
|
||||||
raise ParseError("Unbalanced parentheses")
|
|
||||||
raw = sql[start : tokens[-1].end]
|
|
||||||
item = raw.strip()
|
|
||||||
if item:
|
|
||||||
item_start = start + len(raw) - len(raw.lstrip())
|
|
||||||
items.append((item, item_start, item_start + len(item)))
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def _split_ranges(sql: str, tokens: list[_Token]) -> list[str]:
|
|
||||||
return [item for item, _, _ in _split_spans(sql, tokens)]
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_outer_parens(tokens: list[_Token]) -> list[_Token]:
|
|
||||||
while tokens and tokens[0].text == "(":
|
|
||||||
close = _matching_paren(tokens, 0)
|
|
||||||
if close != len(tokens) - 1:
|
|
||||||
break
|
|
||||||
tokens = tokens[1:-1]
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
|
|
||||||
_NO_LITERAL = object()
|
|
||||||
|
|
||||||
|
|
||||||
def _literal_value(text: str) -> Any:
|
|
||||||
tokens = _meaningful(_lex(text))
|
|
||||||
if len(tokens) == 1 and tokens[0].kind == "string":
|
|
||||||
return _unquote(tokens[0].text)
|
|
||||||
raw = "".join(token.text for token in tokens)
|
|
||||||
if raw.upper() == "NULL":
|
|
||||||
return None
|
|
||||||
if raw.upper() == "TRUE":
|
|
||||||
return True
|
|
||||||
if raw.upper() == "FALSE":
|
|
||||||
return False
|
|
||||||
if _INTEGER_RE.fullmatch(raw):
|
|
||||||
try:
|
|
||||||
return (
|
|
||||||
int(raw, 16) if raw.lower().lstrip("+-").startswith("0x") else int(raw)
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return _NO_LITERAL
|
|
||||||
if _FLOAT_RE.fullmatch(raw):
|
|
||||||
try:
|
|
||||||
return float(raw)
|
|
||||||
except ValueError:
|
|
||||||
return _NO_LITERAL
|
|
||||||
return _NO_LITERAL
|
|
||||||
|
|
||||||
|
|
||||||
def _ascii_fold(identifier: str) -> str:
|
|
||||||
return identifier.translate(
|
|
||||||
str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_options(expression: str, column: str) -> list[Any] | None:
|
|
||||||
tokens = _strip_outer_parens(_meaningful(_lex(expression)))
|
|
||||||
if len(tokens) < 4:
|
|
||||||
return None
|
|
||||||
lhs = tokens[0]
|
|
||||||
if lhs.kind not in ("word", "identifier"):
|
|
||||||
return None
|
|
||||||
if column and _ascii_fold(_unquote(lhs.text)) != _ascii_fold(column):
|
|
||||||
return None
|
|
||||||
if not tokens[1].is_keyword("IN") or tokens[2].text != "(":
|
|
||||||
return None
|
|
||||||
close = _matching_paren(tokens, 2)
|
|
||||||
if close != len(tokens) - 1:
|
|
||||||
return None
|
|
||||||
inner = expression[tokens[2].end : tokens[close].start]
|
|
||||||
inner_tokens = _lex(inner)
|
|
||||||
if not _meaningful(inner_tokens):
|
|
||||||
return []
|
|
||||||
values = []
|
|
||||||
for item in _split_ranges(inner, inner_tokens):
|
|
||||||
value = _literal_value(item)
|
|
||||||
if value is _NO_LITERAL:
|
|
||||||
return None
|
|
||||||
values.append(value)
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def _check_after(
|
|
||||||
item: str,
|
|
||||||
tokens: list[_Token],
|
|
||||||
check_index: int,
|
|
||||||
name: str,
|
|
||||||
column: str,
|
|
||||||
constraint_start: int,
|
|
||||||
base_offset: int,
|
|
||||||
) -> tuple[Check, int]:
|
|
||||||
if check_index + 1 >= len(tokens) or tokens[check_index + 1].text != "(":
|
|
||||||
raise ParseError("CHECK must be followed by a parenthesized expression")
|
|
||||||
close = _matching_paren(tokens, check_index + 1)
|
|
||||||
expression = item[tokens[check_index + 1].end : tokens[close].start].strip()
|
|
||||||
source_start = tokens[constraint_start].start
|
|
||||||
source_end = tokens[close].end
|
|
||||||
return (
|
|
||||||
Check(
|
|
||||||
expression,
|
|
||||||
name=name,
|
|
||||||
column=column,
|
|
||||||
options=_parse_options(expression, column),
|
|
||||||
sql=item[source_start:source_end],
|
|
||||||
start=base_offset + source_start,
|
|
||||||
end=base_offset + source_end,
|
|
||||||
),
|
|
||||||
close + 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _column_checks(
|
|
||||||
item: str, tokens: list[_Token], column: str, base_offset: int
|
|
||||||
) -> list[Check]:
|
|
||||||
checks: list[Check] = []
|
|
||||||
pending_name = ""
|
|
||||||
pending_start: int | None = None
|
|
||||||
index = 1
|
|
||||||
while index < len(tokens):
|
|
||||||
token = tokens[index]
|
|
||||||
if token.text == "(":
|
|
||||||
index = _matching_paren(tokens, index) + 1
|
|
||||||
continue
|
|
||||||
if token.is_keyword("CONSTRAINT"):
|
|
||||||
if index + 1 >= len(tokens):
|
|
||||||
raise ParseError("CONSTRAINT is missing its name")
|
|
||||||
pending_name = _unquote(tokens[index + 1].text)
|
|
||||||
pending_start = index
|
|
||||||
index += 2
|
|
||||||
continue
|
|
||||||
if token.is_keyword("CHECK"):
|
|
||||||
check, index = _check_after(
|
|
||||||
item,
|
|
||||||
tokens,
|
|
||||||
index,
|
|
||||||
pending_name,
|
|
||||||
column,
|
|
||||||
pending_start if pending_start is not None else index,
|
|
||||||
base_offset,
|
|
||||||
)
|
|
||||||
checks.append(check)
|
|
||||||
pending_name = ""
|
|
||||||
pending_start = None
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
token.kind == "word"
|
|
||||||
and token.text.upper() in _OTHER_COLUMN_CONSTRAINT_KEYWORDS
|
|
||||||
):
|
|
||||||
pending_name = ""
|
|
||||||
pending_start = None
|
|
||||||
index += 1
|
|
||||||
return checks
|
|
||||||
|
|
||||||
|
|
||||||
def _table_body(create_sql: str) -> tuple[str, int] | None:
|
|
||||||
all_tokens = _lex(create_sql)
|
|
||||||
tokens = _meaningful(all_tokens)
|
|
||||||
if not tokens or not tokens[0].is_keyword("CREATE"):
|
|
||||||
raise ParseError("Expected CREATE TABLE")
|
|
||||||
index = 1
|
|
||||||
if index < len(tokens) and (
|
|
||||||
tokens[index].is_keyword("TEMP") or tokens[index].is_keyword("TEMPORARY")
|
|
||||||
):
|
|
||||||
index += 1
|
|
||||||
if index < len(tokens) and tokens[index].is_keyword("VIRTUAL"):
|
|
||||||
return None
|
|
||||||
if index >= len(tokens) or not tokens[index].is_keyword("TABLE"):
|
|
||||||
raise ParseError("Expected CREATE TABLE")
|
|
||||||
index += 1
|
|
||||||
if (
|
|
||||||
index + 2 < len(tokens)
|
|
||||||
and tokens[index].is_keyword("IF")
|
|
||||||
and tokens[index + 1].is_keyword("NOT")
|
|
||||||
and tokens[index + 2].is_keyword("EXISTS")
|
|
||||||
):
|
|
||||||
index += 3
|
|
||||||
if index >= len(tokens):
|
|
||||||
raise ParseError("CREATE TABLE is missing its table name")
|
|
||||||
index += 1
|
|
||||||
if index + 1 < len(tokens) and tokens[index].text == ".":
|
|
||||||
index += 2
|
|
||||||
if index < len(tokens) and tokens[index].is_keyword("AS"):
|
|
||||||
return None
|
|
||||||
if index >= len(tokens) or tokens[index].text != "(":
|
|
||||||
raise ParseError("CREATE TABLE is missing its column list")
|
|
||||||
close = _matching_paren(tokens, index)
|
|
||||||
trailing = tokens[close + 1 :]
|
|
||||||
allowed_trailing = {"STRICT", "WITHOUT", "ROWID", ",", ";"}
|
|
||||||
if any(token.text.upper() not in allowed_trailing for token in trailing):
|
|
||||||
raise ParseError("Unexpected SQL after CREATE TABLE column list")
|
|
||||||
|
|
||||||
body_start = tokens[index].end
|
|
||||||
body_end = tokens[close].start
|
|
||||||
return create_sql[body_start:body_end], body_start
|
|
||||||
|
|
||||||
|
|
||||||
def parse_checks(create_sql: str) -> list[Check]:
|
|
||||||
"""Return CHECK constraints from a valid SQLite CREATE TABLE statement."""
|
|
||||||
body_info = _table_body(create_sql)
|
|
||||||
if body_info is None:
|
|
||||||
return []
|
|
||||||
body, body_start = body_info
|
|
||||||
body_tokens = _lex(body)
|
|
||||||
checks: list[Check] = []
|
|
||||||
for item, item_start, _ in _split_spans(body, body_tokens):
|
|
||||||
item_tokens = _meaningful(_lex(item))
|
|
||||||
if not item_tokens:
|
|
||||||
continue
|
|
||||||
item_index = 0
|
|
||||||
constraint_name = ""
|
|
||||||
if item_tokens[item_index].is_keyword("CONSTRAINT"):
|
|
||||||
if len(item_tokens) < 2:
|
|
||||||
raise ParseError("CONSTRAINT is missing its name")
|
|
||||||
constraint_name = _unquote(item_tokens[1].text)
|
|
||||||
item_index = 2
|
|
||||||
head = item_tokens[item_index] if item_index < len(item_tokens) else None
|
|
||||||
if (
|
|
||||||
head
|
|
||||||
and head.kind == "word"
|
|
||||||
and head.text.upper() in _TABLE_CONSTRAINT_KEYWORDS
|
|
||||||
):
|
|
||||||
if head.is_keyword("CHECK"):
|
|
||||||
check, _ = _check_after(
|
|
||||||
item,
|
|
||||||
item_tokens,
|
|
||||||
item_index,
|
|
||||||
constraint_name,
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
body_start + item_start,
|
|
||||||
)
|
|
||||||
checks.append(check)
|
|
||||||
continue
|
|
||||||
column = _unquote(item_tokens[0].text)
|
|
||||||
checks.extend(
|
|
||||||
_column_checks(item, item_tokens, column, body_start + item_start)
|
|
||||||
)
|
|
||||||
return checks
|
|
||||||
|
|
||||||
|
|
||||||
def parse_autoincrement(create_sql: str) -> str | None:
|
|
||||||
"""Return the AUTOINCREMENT column from a valid CREATE TABLE statement."""
|
|
||||||
body_info = _table_body(create_sql)
|
|
||||||
if body_info is None:
|
|
||||||
return None
|
|
||||||
body, _ = body_info
|
|
||||||
for item, _, _ in _split_spans(body, _lex(body)):
|
|
||||||
item_tokens = _meaningful(_lex(item))
|
|
||||||
if not item_tokens:
|
|
||||||
continue
|
|
||||||
head = item_tokens[0]
|
|
||||||
if (
|
|
||||||
head.kind == "word" and head.text.upper() in _TABLE_CONSTRAINT_KEYWORDS
|
|
||||||
) or head.is_keyword("CONSTRAINT"):
|
|
||||||
continue
|
|
||||||
column = _unquote(head.text)
|
|
||||||
index = 1
|
|
||||||
while index < len(item_tokens):
|
|
||||||
token = item_tokens[index]
|
|
||||||
if token.text == "(":
|
|
||||||
index = _matching_paren(item_tokens, index) + 1
|
|
||||||
continue
|
|
||||||
if token.is_keyword("AUTOINCREMENT"):
|
|
||||||
return column
|
|
||||||
index += 1
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
_CONFLICT_ACTIONS = frozenset(("ROLLBACK", "ABORT", "FAIL", "IGNORE", "REPLACE"))
|
|
||||||
|
|
||||||
|
|
||||||
def _conflict_after(tokens: list[_Token], index: int) -> tuple[str, int]:
|
|
||||||
if index >= len(tokens) or not tokens[index].is_keyword("ON"):
|
|
||||||
return "", index
|
|
||||||
if index + 2 >= len(tokens) or not tokens[index + 1].is_keyword("CONFLICT"):
|
|
||||||
raise ParseError("ON after UNIQUE must be followed by CONFLICT and an action")
|
|
||||||
action = tokens[index + 2].text.upper()
|
|
||||||
if tokens[index + 2].kind != "word" or action not in _CONFLICT_ACTIONS:
|
|
||||||
raise ParseError("Invalid UNIQUE ON CONFLICT action")
|
|
||||||
return action, index + 3
|
|
||||||
|
|
||||||
|
|
||||||
def _unique_columns(
|
|
||||||
item: str, tokens: list[_Token], open_index: int
|
|
||||||
) -> tuple[tuple[UniqueColumn, ...], int]:
|
|
||||||
close = _matching_paren(tokens, open_index)
|
|
||||||
inner = item[tokens[open_index].end : tokens[close].start]
|
|
||||||
columns: list[UniqueColumn] = []
|
|
||||||
for raw_column in _split_ranges(inner, _lex(inner)):
|
|
||||||
column_tokens = _meaningful(_lex(raw_column))
|
|
||||||
if not column_tokens or column_tokens[0].kind not in (
|
|
||||||
"word",
|
|
||||||
"identifier",
|
|
||||||
"string",
|
|
||||||
):
|
|
||||||
raise ParseError("UNIQUE constraint has an invalid column")
|
|
||||||
name = _unquote(column_tokens[0].text)
|
|
||||||
collation = ""
|
|
||||||
order = ""
|
|
||||||
index = 1
|
|
||||||
if index < len(column_tokens) and column_tokens[index].is_keyword("COLLATE"):
|
|
||||||
if index + 1 >= len(column_tokens):
|
|
||||||
raise ParseError("COLLATE in UNIQUE constraint is missing its name")
|
|
||||||
collation = _unquote(column_tokens[index + 1].text)
|
|
||||||
index += 2
|
|
||||||
if index < len(column_tokens) and (
|
|
||||||
column_tokens[index].is_keyword("ASC")
|
|
||||||
or column_tokens[index].is_keyword("DESC")
|
|
||||||
):
|
|
||||||
order = column_tokens[index].text.upper()
|
|
||||||
index += 1
|
|
||||||
if index != len(column_tokens):
|
|
||||||
raise ParseError("UNIQUE constraint has an invalid indexed column")
|
|
||||||
columns.append(UniqueColumn(name, collation=collation, order=order))
|
|
||||||
if not columns:
|
|
||||||
raise ParseError("UNIQUE constraint must include at least one column")
|
|
||||||
return tuple(columns), close + 1
|
|
||||||
|
|
||||||
|
|
||||||
def _column_uniques(
|
|
||||||
item: str, tokens: list[_Token], column: str, base_offset: int
|
|
||||||
) -> list[Unique]:
|
|
||||||
uniques: list[Unique] = []
|
|
||||||
collation = ""
|
|
||||||
collation_index = 1
|
|
||||||
while collation_index < len(tokens):
|
|
||||||
token = tokens[collation_index]
|
|
||||||
if token.text == "(":
|
|
||||||
collation_index = _matching_paren(tokens, collation_index) + 1
|
|
||||||
continue
|
|
||||||
if token.is_keyword("COLLATE"):
|
|
||||||
if collation_index + 1 >= len(tokens):
|
|
||||||
raise ParseError("COLLATE is missing its name")
|
|
||||||
collation = _unquote(tokens[collation_index + 1].text)
|
|
||||||
collation_index += 2
|
|
||||||
continue
|
|
||||||
collation_index += 1
|
|
||||||
pending_name = ""
|
|
||||||
pending_start: int | None = None
|
|
||||||
index = 1
|
|
||||||
while index < len(tokens):
|
|
||||||
token = tokens[index]
|
|
||||||
if token.text == "(":
|
|
||||||
index = _matching_paren(tokens, index) + 1
|
|
||||||
continue
|
|
||||||
if token.is_keyword("CONSTRAINT"):
|
|
||||||
if index + 1 >= len(tokens):
|
|
||||||
raise ParseError("CONSTRAINT is missing its name")
|
|
||||||
pending_name = _unquote(tokens[index + 1].text)
|
|
||||||
pending_start = index
|
|
||||||
index += 2
|
|
||||||
continue
|
|
||||||
if token.is_keyword("UNIQUE"):
|
|
||||||
source_start = tokens[
|
|
||||||
pending_start if pending_start is not None else index
|
|
||||||
].start
|
|
||||||
conflict, next_index = _conflict_after(tokens, index + 1)
|
|
||||||
source_end = tokens[next_index - 1].end
|
|
||||||
uniques.append(
|
|
||||||
Unique(
|
|
||||||
(UniqueColumn(column, collation=collation),),
|
|
||||||
name=pending_name,
|
|
||||||
column=column,
|
|
||||||
conflict=conflict,
|
|
||||||
sql=item[source_start:source_end],
|
|
||||||
start=base_offset + source_start,
|
|
||||||
end=base_offset + source_end,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pending_name = ""
|
|
||||||
pending_start = None
|
|
||||||
index = next_index
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
token.kind == "word"
|
|
||||||
and token.text.upper() in _OTHER_COLUMN_CONSTRAINT_KEYWORDS
|
|
||||||
):
|
|
||||||
pending_name = ""
|
|
||||||
pending_start = None
|
|
||||||
index += 1
|
|
||||||
return uniques
|
|
||||||
|
|
||||||
|
|
||||||
def parse_uniques(create_sql: str) -> list[Unique]:
|
|
||||||
"""Return column-level and table-level UNIQUE constraints."""
|
|
||||||
body_info = _table_body(create_sql)
|
|
||||||
if body_info is None:
|
|
||||||
return []
|
|
||||||
body, body_start = body_info
|
|
||||||
uniques: list[Unique] = []
|
|
||||||
for item, item_start, _ in _split_spans(body, _lex(body)):
|
|
||||||
item_tokens = _meaningful(_lex(item))
|
|
||||||
if not item_tokens:
|
|
||||||
continue
|
|
||||||
item_index = 0
|
|
||||||
constraint_name = ""
|
|
||||||
if item_tokens[item_index].is_keyword("CONSTRAINT"):
|
|
||||||
if len(item_tokens) < 2:
|
|
||||||
raise ParseError("CONSTRAINT is missing its name")
|
|
||||||
constraint_name = _unquote(item_tokens[1].text)
|
|
||||||
item_index = 2
|
|
||||||
head = item_tokens[item_index] if item_index < len(item_tokens) else None
|
|
||||||
if head and head.is_keyword("UNIQUE"):
|
|
||||||
if (
|
|
||||||
item_index + 1 >= len(item_tokens)
|
|
||||||
or item_tokens[item_index + 1].text != "("
|
|
||||||
):
|
|
||||||
raise ParseError("Table UNIQUE must be followed by a column list")
|
|
||||||
columns, next_index = _unique_columns(item, item_tokens, item_index + 1)
|
|
||||||
conflict, next_index = _conflict_after(item_tokens, next_index)
|
|
||||||
if next_index != len(item_tokens):
|
|
||||||
raise ParseError("Unexpected SQL after UNIQUE constraint")
|
|
||||||
source_start = item_tokens[0].start
|
|
||||||
source_end = item_tokens[next_index - 1].end
|
|
||||||
uniques.append(
|
|
||||||
Unique(
|
|
||||||
columns,
|
|
||||||
name=constraint_name,
|
|
||||||
conflict=conflict,
|
|
||||||
sql=item[source_start:source_end],
|
|
||||||
start=body_start + item_start + source_start,
|
|
||||||
end=body_start + item_start + source_end,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
head
|
|
||||||
and head.kind == "word"
|
|
||||||
and head.text.upper() in _TABLE_CONSTRAINT_KEYWORDS
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
column = _unquote(item_tokens[0].text)
|
|
||||||
uniques.extend(
|
|
||||||
_column_uniques(
|
|
||||||
item,
|
|
||||||
item_tokens,
|
|
||||||
column,
|
|
||||||
body_start + item_start,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return uniques
|
|
||||||
|
|
||||||
|
|
||||||
def parse_column_comments(create_sql: str) -> dict[str, ColumnComments]:
|
|
||||||
"""Return comments immediately before and after each column definition."""
|
|
||||||
body_info = _table_body(create_sql)
|
|
||||||
if body_info is None:
|
|
||||||
return {}
|
|
||||||
body, _ = body_info
|
|
||||||
comments: dict[str, ColumnComments] = {}
|
|
||||||
for item, _, _ in _split_spans(body, _lex(body)):
|
|
||||||
item_tokens = _meaningful(_lex(item))
|
|
||||||
if not item_tokens:
|
|
||||||
continue
|
|
||||||
item_index = 0
|
|
||||||
if item_tokens[item_index].is_keyword("CONSTRAINT"):
|
|
||||||
item_index = 2
|
|
||||||
head = item_tokens[item_index] if item_index < len(item_tokens) else None
|
|
||||||
if (
|
|
||||||
head
|
|
||||||
and head.kind == "word"
|
|
||||||
and head.text.upper() in _TABLE_CONSTRAINT_KEYWORDS
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
column = _unquote(item_tokens[0].text)
|
|
||||||
before = item[: item_tokens[0].start].strip()
|
|
||||||
after = item[item_tokens[-1].end :].strip()
|
|
||||||
if before or after:
|
|
||||||
comments[column] = ColumnComments(before=before, after=after)
|
|
||||||
return comments
|
|
||||||
|
|
||||||
|
|
||||||
def _is_identifier_token(tokens: list[_Token], index: int) -> bool:
|
|
||||||
token = tokens[index]
|
|
||||||
if index + 1 < len(tokens) and tokens[index + 1].text in ("(", "."):
|
|
||||||
return False
|
|
||||||
if index and (
|
|
||||||
tokens[index - 1].is_keyword("COLLATE") or tokens[index - 1].is_keyword("AS")
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
if token.kind == "identifier":
|
|
||||||
return True
|
|
||||||
if token.kind != "word" or token.text.upper() in _SQLITE_KEYWORDS:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def check_references_identifier(expression: str, identifier: str) -> bool:
|
|
||||||
tokens = _meaningful(_lex(expression))
|
|
||||||
folded = _ascii_fold(identifier)
|
|
||||||
return any(
|
|
||||||
_is_identifier_token(tokens, index)
|
|
||||||
and _ascii_fold(_unquote(token.text)) == folded
|
|
||||||
for index, token in enumerate(tokens)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def sql_ends_in_line_comment(sql: str) -> bool:
|
|
||||||
"""Return True if appended SQL would be swallowed by a ``--`` comment."""
|
|
||||||
tokens = _lex(sql)
|
|
||||||
if not tokens:
|
|
||||||
return False
|
|
||||||
final = tokens[-1]
|
|
||||||
return (
|
|
||||||
final.kind == "comment"
|
|
||||||
and final.text.startswith("--")
|
|
||||||
and not final.text.endswith(("\n", "\r"))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_bare_identifier(identifier: str) -> bool:
|
|
||||||
if not identifier or identifier.upper() in _SQLITE_KEYWORDS:
|
|
||||||
return False
|
|
||||||
first = identifier[0]
|
|
||||||
if not (first == "_" or first.isalpha() or ord(first) >= 0x80):
|
|
||||||
return False
|
|
||||||
return all(
|
|
||||||
char == "_" or char == "$" or char.isalnum() or ord(char) >= 0x80
|
|
||||||
for char in identifier[1:]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _quote_replacement(original: str, replacement: str) -> str:
|
|
||||||
if original.startswith('"'):
|
|
||||||
return '"{}"'.format(replacement.replace('"', '""'))
|
|
||||||
if original.startswith("`"):
|
|
||||||
return "`{}`".format(replacement.replace("`", "``"))
|
|
||||||
if original.startswith("[") and "]" not in replacement:
|
|
||||||
return f"[{replacement}]"
|
|
||||||
if _valid_bare_identifier(replacement):
|
|
||||||
return replacement
|
|
||||||
return '"{}"'.format(replacement.replace('"', '""'))
|
|
||||||
|
|
||||||
|
|
||||||
def rewrite_check_expression(expression: str, rename: dict[str, str]) -> str:
|
|
||||||
"""Rewrite column identifiers in a CHECK expression, preserving trivia."""
|
|
||||||
if not rename:
|
|
||||||
return expression
|
|
||||||
tokens = _lex(expression)
|
|
||||||
meaningful = _meaningful(tokens)
|
|
||||||
replacements = {_ascii_fold(key): value for key, value in rename.items()}
|
|
||||||
edits: list[tuple[int, int, str]] = []
|
|
||||||
for index, token in enumerate(meaningful):
|
|
||||||
if not _is_identifier_token(meaningful, index):
|
|
||||||
continue
|
|
||||||
replacement = replacements.get(_ascii_fold(_unquote(token.text)))
|
|
||||||
if replacement is not None:
|
|
||||||
edits.append(
|
|
||||||
(token.start, token.end, _quote_replacement(token.text, replacement))
|
|
||||||
)
|
|
||||||
for start, end, replacement in reversed(edits):
|
|
||||||
expression = expression[:start] + replacement + expression[end:]
|
|
||||||
return expression
|
|
||||||
1585
sqlite_utils/db.py
1585
sqlite_utils/db.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,8 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from pluggy import HookimplMarker, HookspecMarker
|
from pluggy import HookimplMarker
|
||||||
|
from pluggy import HookspecMarker
|
||||||
|
|
||||||
hookspec = HookspecMarker("sqlite_utils")
|
hookspec = HookspecMarker("sqlite_utils")
|
||||||
hookimpl = HookimplMarker("sqlite_utils")
|
hookimpl = HookimplMarker("sqlite_utils")
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,19 @@
|
||||||
import datetime
|
from collections.abc import Iterable
|
||||||
from collections.abc import Callable, Iterable
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Protocol, TypeVar, cast
|
import datetime
|
||||||
|
from typing import Callable, cast, TYPE_CHECKING
|
||||||
|
|
||||||
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: _MigrationFunction
|
fn: Callable
|
||||||
transactional: bool = True
|
transactional: bool = True
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -41,7 +32,7 @@ class Migrations:
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self, *, name: str | None = None, transactional: bool = True
|
self, *, name: str | None = None, transactional: bool = True
|
||||||
) -> Callable[[_MigrationFunctionT], _MigrationFunctionT]:
|
) -> Callable:
|
||||||
"""
|
"""
|
||||||
: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.
|
||||||
|
|
@ -52,11 +43,13 @@ class Migrations:
|
||||||
example those that execute ``VACUUM``.
|
example those that execute ``VACUUM``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def inner(func: _MigrationFunctionT) -> _MigrationFunctionT:
|
def inner(func: Callable) -> Callable:
|
||||||
migration_name = name or func.__name__
|
migration_name = name or getattr(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(
|
||||||
f"Migration '{migration_name}' is already registered in set '{self.name}'"
|
"Migration '{}' is already registered in set '{}'".format(
|
||||||
|
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 @@
|
||||||
import sys
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
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, str | list[str]]]:
|
def get_plugins() -> List[Dict[str, Union[str, List[str]]]]:
|
||||||
ensure_plugins_loaded()
|
ensure_plugins_loaded()
|
||||||
plugins: list[dict[str, str | list[str]]] = []
|
plugins: List[Dict[str, Union[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, str | list[str]] = {
|
plugin_info: Dict[str, Union[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
|
||||||
|
|
||||||
import json
|
from typing import Callable, Optional
|
||||||
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: object | None = None,
|
errors: Optional[object] = None,
|
||||||
) -> str | None:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
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: object | None = None,
|
errors: Optional[object] = None,
|
||||||
) -> str | None:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
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,12 +9,20 @@ 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,
|
||||||
Generic,
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Generator,
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TYPE_CHECKING,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
|
|
@ -25,8 +33,8 @@ import click
|
||||||
from . import recipes
|
from . import recipes
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import sqlite3
|
import sqlite3 # noqa: F401
|
||||||
from sqlite3 import dbapi2
|
from sqlite3 import dbapi2 # noqa: F401
|
||||||
|
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
else:
|
else:
|
||||||
|
|
@ -36,7 +44,7 @@ else:
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import sqlite3 # noqa: F401
|
import sqlite3 # noqa: F401
|
||||||
from sqlite3 import dbapi2
|
from sqlite3 import dbapi2 # noqa: F401
|
||||||
|
|
||||||
OperationalError = dbapi2.OperationalError
|
OperationalError = dbapi2.OperationalError
|
||||||
|
|
||||||
|
|
@ -53,16 +61,12 @@ 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 = None | int | float | str | bytes | bool | list[str]
|
RowValue = Union[None, int, float, str, bytes, bool, List[str]]
|
||||||
Row = dict[str, RowValue]
|
Row = Dict[str, RowValue]
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class ANY:
|
|
||||||
"""Marker type for an SQLite ``ANY`` column."""
|
|
||||||
|
|
||||||
|
|
||||||
class _CloseableIterator(Iterator[Row]):
|
class _CloseableIterator(Iterator[Row]):
|
||||||
"""Iterator wrapper that closes a file when iteration is complete."""
|
"""Iterator wrapper that closes a file when iteration is complete."""
|
||||||
|
|
||||||
|
|
@ -99,7 +103,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() -> str | None:
|
def find_spatialite() -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
|
@ -128,9 +132,9 @@ def find_spatialite() -> str | None:
|
||||||
|
|
||||||
|
|
||||||
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))
|
||||||
|
|
@ -138,9 +142,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:
|
||||||
|
|
@ -149,7 +153,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 = next(iter(types))
|
t = list(types)[0]
|
||||||
# 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):
|
||||||
|
|
@ -182,13 +186,11 @@ def column_affinity(column_type: str) -> type:
|
||||||
return bytes
|
return bytes
|
||||||
if "REAL" in column_type or "FLOA" in column_type or "DOUB" in column_type:
|
if "REAL" in column_type or "FLOA" in column_type or "DOUB" in column_type:
|
||||||
return float
|
return float
|
||||||
if column_type == "ANY":
|
|
||||||
return ANY
|
|
||||||
# Default is 'NUMERIC', which we currently also treat as float
|
# Default is 'NUMERIC', which we currently also treat as float
|
||||||
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
|
||||||
|
|
@ -261,9 +263,9 @@ class RowError(Exception):
|
||||||
|
|
||||||
|
|
||||||
def _extra_key_strategy(
|
def _extra_key_strategy(
|
||||||
reader: Iterable[dict[str | None, object]],
|
reader: Iterable[Dict[Optional[str], object]],
|
||||||
ignore_extras: bool | None = False,
|
ignore_extras: Optional[bool] = False,
|
||||||
extras_key: str | None = None,
|
extras_key: Optional[str] = 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:
|
||||||
|
|
@ -277,7 +279,9 @@ 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(f"Row {row} contained these extra values: {extras}")
|
raise RowError(
|
||||||
|
"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)
|
||||||
|
|
@ -287,12 +291,12 @@ def _extra_key_strategy(
|
||||||
|
|
||||||
def rows_from_file(
|
def rows_from_file(
|
||||||
fp: BinaryIO,
|
fp: BinaryIO,
|
||||||
format: Format | None = None,
|
format: Optional[Format] = None,
|
||||||
dialect: type[csv.Dialect] | None = None,
|
dialect: Optional[Type[csv.Dialect]] = None,
|
||||||
encoding: str | None = None,
|
encoding: Optional[str] = None,
|
||||||
ignore_extras: bool | None = False,
|
ignore_extras: Optional[bool] = False,
|
||||||
extras_key: str | None = None,
|
extras_key: Optional[str] = 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.
|
||||||
|
|
||||||
|
|
@ -351,11 +355,7 @@ def rows_from_file(
|
||||||
reader = csv.DictReader(decoded_fp, dialect=dialect)
|
reader = csv.DictReader(decoded_fp, dialect=dialect)
|
||||||
else:
|
else:
|
||||||
reader = csv.DictReader(decoded_fp)
|
reader = csv.DictReader(decoded_fp)
|
||||||
rows = _extra_key_strategy(
|
rows = _extra_key_strategy(reader, ignore_extras, extras_key)
|
||||||
cast(Iterable[dict[str | None, object]], reader),
|
|
||||||
ignore_extras,
|
|
||||||
extras_key,
|
|
||||||
)
|
|
||||||
return _CloseableIterator(iter(rows), decoded_fp), Format.CSV
|
return _CloseableIterator(iter(rows), decoded_fp), Format.CSV
|
||||||
elif format == Format.TSV:
|
elif format == Format.TSV:
|
||||||
rows, _ = rows_from_file(
|
rows, _ = rows_from_file(
|
||||||
|
|
@ -363,7 +363,7 @@ def rows_from_file(
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
_extra_key_strategy(
|
_extra_key_strategy(
|
||||||
cast(Iterable[dict[str | None, object]], rows),
|
cast(Iterable[Dict[Optional[str], object]], rows),
|
||||||
ignore_extras,
|
ignore_extras,
|
||||||
extras_key,
|
extras_key,
|
||||||
),
|
),
|
||||||
|
|
@ -379,9 +379,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 not first_bytes:
|
if first_bytes.startswith(b"[") or first_bytes.startswith(b"{"):
|
||||||
return (), Format.CSV
|
|
||||||
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:
|
||||||
|
|
@ -395,7 +393,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[str | None, object]], rows),
|
cast(Iterable[Dict[Optional[str], object]], rows),
|
||||||
ignore_extras,
|
ignore_extras,
|
||||||
extras_key,
|
extras_key,
|
||||||
),
|
),
|
||||||
|
|
@ -427,9 +425,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.
|
||||||
|
|
@ -443,7 +441,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.
|
||||||
|
|
@ -452,15 +450,17 @@ 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] for key in cls.__dict__ if key.startswith("test_")
|
key.split("test_")[-1]
|
||||||
|
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 +492,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)
|
||||||
|
|
@ -500,12 +500,12 @@ class ValueTracker:
|
||||||
del self.couldbe[key]
|
del self.couldbe[key]
|
||||||
|
|
||||||
|
|
||||||
class NullProgressBar(Generic[T]):
|
class NullProgressBar:
|
||||||
def __init__(self, *args: Iterable[T]) -> None:
|
def __init__(self, *args: Iterable[T]) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[T]:
|
def __iter__(self) -> Iterator[T]:
|
||||||
yield from self.args[0]
|
yield from self.args[0] # type: ignore
|
||||||
|
|
||||||
def update(self, value: int) -> None:
|
def update(self, value: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
@ -524,14 +524,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) # noqa: S102
|
exec(code, globals_dict)
|
||||||
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 +542,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: # noqa: BLE001, S110
|
except Exception:
|
||||||
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, f"return {code}")
|
body_variants.insert(0, "return {}".format(code))
|
||||||
|
|
||||||
code_o = None
|
code_o = None
|
||||||
for variant in body_variants:
|
for variant in body_variants:
|
||||||
new_code = [f"def fn({variable}):"]
|
new_code = ["def fn({}):".format(variable)]
|
||||||
for line in variant.split("\n"):
|
for line in variant.split("\n"):
|
||||||
new_code.append(f" {line}")
|
new_code.append(" {}".format(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 +566,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) # noqa: S102
|
exec(code_o, globals_dict)
|
||||||
return cast(Callable[..., object], globals_dict["fn"])
|
return cast(Callable[..., object], globals_dict["fn"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -582,7 +582,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: Iterable[str] | None = None) -> str:
|
def hash_record(record: Dict[str, Any], keys: Optional[Iterable[str]] = 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 +603,7 @@ def hash_record(record: dict[str, Any], keys: Iterable[str] | None = None) -> st
|
||||||
: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 +613,7 @@ def hash_record(record: dict[str, Any], keys: Iterable[str] | None = None) -> st
|
||||||
).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 +636,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 = f"{key}_{suffix}"
|
new_key = "{}_{}".format(key, suffix)
|
||||||
suffix += 1
|
suffix += 1
|
||||||
key = new_key
|
key = new_key
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
|
|
@ -644,7 +644,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 +653,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,7 +1,6 @@
|
||||||
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);
|
||||||
|
|
@ -56,7 +55,7 @@ def close_all_databases():
|
||||||
for db in databases:
|
for db in databases:
|
||||||
try:
|
try:
|
||||||
db.close()
|
db.close()
|
||||||
except sqlite3.Error:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ import pytest
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def db(fresh_db):
|
def db(fresh_db):
|
||||||
fresh_db.table("one_index").insert({"id": 1, "name": "Cleo"}, pk="id")
|
fresh_db["one_index"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
fresh_db.table("one_index").create_index(["name"])
|
fresh_db["one_index"].create_index(["name"])
|
||||||
fresh_db.table("two_indexes").insert(
|
fresh_db["two_indexes"].insert({"id": 1, "name": "Cleo", "species": "dog"}, pk="id")
|
||||||
{"id": 1, "name": "Cleo", "species": "dog"}, pk="id"
|
fresh_db["two_indexes"].create_index(["name"])
|
||||||
)
|
fresh_db["two_indexes"].create_index(["species"])
|
||||||
fresh_db.table("two_indexes").create_index(["name"])
|
|
||||||
fresh_db.table("two_indexes").create_index(["species"])
|
|
||||||
return fresh_db
|
return fresh_db
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,7 +17,7 @@ def test_analyze_whole_database(db):
|
||||||
assert set(db.table_names()).issuperset(
|
assert set(db.table_names()).issuperset(
|
||||||
{"one_index", "two_indexes", "sqlite_stat1"}
|
{"one_index", "two_indexes", "sqlite_stat1"}
|
||||||
)
|
)
|
||||||
assert list(db.table("sqlite_stat1").rows) == [
|
assert list(db["sqlite_stat1"].rows) == [
|
||||||
{"tbl": "two_indexes", "idx": "idx_two_indexes_species", "stat": "1 1"},
|
{"tbl": "two_indexes", "idx": "idx_two_indexes_species", "stat": "1 1"},
|
||||||
{"tbl": "two_indexes", "idx": "idx_two_indexes_name", "stat": "1 1"},
|
{"tbl": "two_indexes", "idx": "idx_two_indexes_name", "stat": "1 1"},
|
||||||
{"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"},
|
{"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"},
|
||||||
|
|
@ -32,12 +30,12 @@ def test_analyze_one_table(db, method):
|
||||||
if method == "db_method_with_name":
|
if method == "db_method_with_name":
|
||||||
db.analyze("one_index")
|
db.analyze("one_index")
|
||||||
elif method == "table_method":
|
elif method == "table_method":
|
||||||
db.table("one_index").analyze()
|
db["one_index"].analyze()
|
||||||
|
|
||||||
assert set(db.table_names()).issuperset(
|
assert set(db.table_names()).issuperset(
|
||||||
{"one_index", "two_indexes", "sqlite_stat1"}
|
{"one_index", "two_indexes", "sqlite_stat1"}
|
||||||
)
|
)
|
||||||
assert list(db.table("sqlite_stat1").rows) == [
|
assert list(db["sqlite_stat1"].rows) == [
|
||||||
{"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"}
|
{"tbl": "one_index", "idx": "idx_one_index_name", "stat": "1 1"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -48,6 +46,6 @@ def test_analyze_index_by_name(db):
|
||||||
assert set(db.table_names()).issuperset(
|
assert set(db.table_names()).issuperset(
|
||||||
{"one_index", "two_indexes", "sqlite_stat1"}
|
{"one_index", "two_indexes", "sqlite_stat1"}
|
||||||
)
|
)
|
||||||
assert list(db.table("sqlite_stat1").rows) == [
|
assert list(db["sqlite_stat1"].rows) == [
|
||||||
{"tbl": "two_indexes", "idx": "idx_two_indexes_species", "stat": "1 1"},
|
{"tbl": "two_indexes", "idx": "idx_two_indexes_species", "stat": "1 1"},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import sqlite3
|
from sqlite_utils.db import Database, ColumnDetails
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
from sqlite_utils import cli
|
from sqlite_utils import cli
|
||||||
from sqlite_utils.db import ColumnDetails, Database
|
from click.testing import CliRunner
|
||||||
|
import pytest
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def db_to_analyze(fresh_db):
|
def db_to_analyze(fresh_db):
|
||||||
stuff = fresh_db.table("stuff")
|
stuff = fresh_db["stuff"]
|
||||||
stuff.insert_all(
|
stuff.insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "owner": "Terryterryterry", "size": 5},
|
{"id": 1, "owner": "Terryterryterry", "size": 5},
|
||||||
|
|
@ -45,7 +43,7 @@ def big_db_to_analyze_path(tmpdir):
|
||||||
"all_null": None,
|
"all_null": None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
db.table("stuff").insert_all(to_insert)
|
db["stuff"].insert_all(to_insert)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -126,7 +124,7 @@ def big_db_to_analyze_path(tmpdir):
|
||||||
)
|
)
|
||||||
def test_analyze_column(db_to_analyze, column, extra_kwargs, expected):
|
def test_analyze_column(db_to_analyze, column, extra_kwargs, expected):
|
||||||
assert (
|
assert (
|
||||||
db_to_analyze.table("stuff").analyze_column(
|
db_to_analyze["stuff"].analyze_column(
|
||||||
column, common_limit=2, value_truncate=5, **extra_kwargs
|
column, common_limit=2, value_truncate=5, **extra_kwargs
|
||||||
)
|
)
|
||||||
== expected
|
== expected
|
||||||
|
|
@ -186,7 +184,7 @@ def test_analyze_table_save(db_to_analyze_path):
|
||||||
cli.cli, ["analyze-tables", db_to_analyze_path, "--save"]
|
cli.cli, ["analyze-tables", db_to_analyze_path, "--save"]
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
rows = list(Database(db_to_analyze_path).table("_analyze_tables_").rows)
|
rows = list(Database(db_to_analyze_path)["_analyze_tables_"].rows)
|
||||||
assert rows == [
|
assert rows == [
|
||||||
{
|
{
|
||||||
"table": "stuff",
|
"table": "stuff",
|
||||||
|
|
@ -248,7 +246,7 @@ def test_analyze_table_save_no_most_no_least_options(
|
||||||
args.append("--no-least")
|
args.append("--no-least")
|
||||||
result = CliRunner().invoke(cli.cli, args)
|
result = CliRunner().invoke(cli.cli, args)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
rows = list(Database(big_db_to_analyze_path).table("_analyze_tables_").rows)
|
rows = list(Database(big_db_to_analyze_path)["_analyze_tables_"].rows)
|
||||||
expected = {
|
expected = {
|
||||||
"table": "stuff",
|
"table": "stuff",
|
||||||
"column": "category",
|
"column": "category",
|
||||||
|
|
@ -297,13 +295,13 @@ def test_analyze_table_column_all_nulls(big_db_to_analyze_path):
|
||||||
def test_analyze_table_validate_columns(tmpdir, args, expected_error):
|
def test_analyze_table_validate_columns(tmpdir, args, expected_error):
|
||||||
path = str(tmpdir / "test_validate_columns.db")
|
path = str(tmpdir / "test_validate_columns.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("one").insert(
|
db["one"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "one",
|
"name": "one",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
db.table("two").insert(
|
db["two"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"age": 5,
|
"age": 5,
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,11 @@ from sqlite_utils.utils import sqlite3
|
||||||
END;
|
END;
|
||||||
""",
|
""",
|
||||||
[
|
[
|
||||||
(
|
|
||||||
"CREATE TRIGGER t_ai AFTER INSERT ON t\n"
|
"CREATE TRIGGER t_ai AFTER INSERT ON t\n"
|
||||||
" BEGIN\n"
|
" BEGIN\n"
|
||||||
" UPDATE t SET value = 'a;b' WHERE id = new.id;\n"
|
" UPDATE t SET value = 'a;b' WHERE id = new.id;\n"
|
||||||
" INSERT INTO log VALUES ('x;y');\n"
|
" INSERT INTO log VALUES ('x;y');\n"
|
||||||
" END;"
|
" END;"
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -45,47 +43,51 @@ def test_iter_complete_sql_statements(sql, expected):
|
||||||
|
|
||||||
def test_atomic_commits(fresh_db):
|
def test_atomic_commits(fresh_db):
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
|
|
||||||
assert list(fresh_db.table("dogs").rows) == [{"id": 1, "name": "Cleo"}]
|
assert list(fresh_db["dogs"].rows) == [{"id": 1, "name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
def test_atomic_rolls_back(fresh_db):
|
def test_atomic_rolls_back(fresh_db):
|
||||||
with pytest.raises(RuntimeError), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
fresh_db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
|
with fresh_db.atomic():
|
||||||
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert not fresh_db.table("dogs").exists()
|
assert not fresh_db["dogs"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_nested_atomic_rolls_back_to_savepoint(fresh_db):
|
def test_nested_atomic_rolls_back_to_savepoint(fresh_db):
|
||||||
fresh_db.table("dogs").create({"id": int, "name": str}, pk="id")
|
fresh_db["dogs"].create({"id": int, "name": str}, pk="id")
|
||||||
|
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("dogs").insert({"id": 1, "name": "Cleo"})
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"})
|
||||||
with pytest.raises(RuntimeError), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
fresh_db.table("dogs").insert({"id": 2, "name": "Pancakes"})
|
with fresh_db.atomic():
|
||||||
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
fresh_db.table("dogs").insert({"id": 3, "name": "Marnie"})
|
fresh_db["dogs"].insert({"id": 3, "name": "Marnie"})
|
||||||
|
|
||||||
assert list(fresh_db.table("dogs").rows) == [
|
assert list(fresh_db["dogs"].rows) == [
|
||||||
{"id": 1, "name": "Cleo"},
|
{"id": 1, "name": "Cleo"},
|
||||||
{"id": 3, "name": "Marnie"},
|
{"id": 3, "name": "Marnie"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_outer_atomic_rolls_back_released_savepoint(fresh_db):
|
def test_outer_atomic_rolls_back_released_savepoint(fresh_db):
|
||||||
with pytest.raises(RuntimeError), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
fresh_db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
|
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("dogs").insert({"id": 2, "name": "Pancakes"})
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
|
with fresh_db.atomic():
|
||||||
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes"})
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert not fresh_db.table("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), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
|
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
|
||||||
|
|
@ -97,41 +99,42 @@ def test_executescript_does_not_commit_open_atomic_block(fresh_db):
|
||||||
""")
|
""")
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert not fresh_db.table("dogs").exists()
|
assert not fresh_db["dogs"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_transform_does_not_commit_open_atomic_block(fresh_db):
|
def test_transform_does_not_commit_open_atomic_block(fresh_db):
|
||||||
fresh_db.table("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), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
fresh_db.table("dogs").insert({"id": 2, "name": "Pancakes", "age": "6"})
|
with fresh_db.atomic():
|
||||||
fresh_db.table("dogs").transform(rename={"age": "dog_age"})
|
fresh_db["dogs"].insert({"id": 2, "name": "Pancakes", "age": "6"})
|
||||||
|
fresh_db["dogs"].transform(rename={"age": "dog_age"})
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
fresh_db.table("dogs").schema
|
fresh_db["dogs"].schema
|
||||||
== 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n)'
|
== 'CREATE TABLE "dogs" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT,\n "age" TEXT\n)'
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("dogs").rows) == [
|
assert list(fresh_db["dogs"].rows) == [
|
||||||
{"id": 1, "name": "Cleo", "age": "5"},
|
{"id": 1, "name": "Cleo", "age": "5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_transform_parent_table_with_foreign_keys_in_atomic(fresh_db):
|
def test_transform_parent_table_with_foreign_keys_in_atomic(fresh_db):
|
||||||
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Tina"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Tina"}, pk="id")
|
||||||
fresh_db.table("books").insert(
|
fresh_db["books"].insert(
|
||||||
{"id": 1, "title": "Book", "author_id": 1},
|
{"id": 1, "title": "Book", "author_id": 1},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys={"author_id"},
|
foreign_keys={"author_id"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("authors").transform(rename={"name": "full_name"})
|
fresh_db["authors"].transform(rename={"name": "full_name"})
|
||||||
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
fresh_db.table("authors").schema
|
fresh_db["authors"].schema
|
||||||
== 'CREATE TABLE "authors" (\n "id" INTEGER PRIMARY KEY,\n "full_name" TEXT\n)'
|
== 'CREATE TABLE "authors" (\n "id" INTEGER PRIMARY KEY,\n "full_name" TEXT\n)'
|
||||||
)
|
)
|
||||||
assert fresh_db.execute("PRAGMA foreign_key_check").fetchall() == []
|
assert fresh_db.execute("PRAGMA foreign_key_check").fetchall() == []
|
||||||
|
|
@ -139,19 +142,20 @@ def test_transform_parent_table_with_foreign_keys_in_atomic(fresh_db):
|
||||||
|
|
||||||
def test_transform_parent_table_with_foreign_keys_rolls_back(fresh_db):
|
def test_transform_parent_table_with_foreign_keys_rolls_back(fresh_db):
|
||||||
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Tina"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Tina"}, pk="id")
|
||||||
fresh_db.table("books").insert(
|
fresh_db["books"].insert(
|
||||||
{"id": 1, "title": "Book", "author_id": 1},
|
{"id": 1, "title": "Book", "author_id": 1},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys={"author_id"},
|
foreign_keys={"author_id"},
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError), fresh_db.atomic():
|
with pytest.raises(RuntimeError):
|
||||||
fresh_db.table("authors").transform(rename={"name": "full_name"})
|
with fresh_db.atomic():
|
||||||
|
fresh_db["authors"].transform(rename={"name": "full_name"})
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
fresh_db.table("authors").schema
|
fresh_db["authors"].schema
|
||||||
== 'CREATE TABLE "authors" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n)'
|
== 'CREATE TABLE "authors" (\n "id" INTEGER PRIMARY KEY,\n "name" TEXT\n)'
|
||||||
)
|
)
|
||||||
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
@ -160,51 +164,49 @@ def test_transform_parent_table_with_foreign_keys_rolls_back(fresh_db):
|
||||||
|
|
||||||
def test_transform_detects_foreign_key_check_violations(fresh_db):
|
def test_transform_detects_foreign_key_check_violations(fresh_db):
|
||||||
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
fresh_db.conn.execute("PRAGMA foreign_keys=ON")
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Tina"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Tina"}, pk="id")
|
||||||
fresh_db.table("books").insert({"id": 1, "author_id": 2}, pk="id")
|
fresh_db["books"].insert({"id": 1, "author_id": 2}, pk="id")
|
||||||
|
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
fresh_db.table("books").transform(
|
fresh_db["books"].transform(add_foreign_keys=(("author_id", "authors", "id"),))
|
||||||
add_foreign_keys=(("author_id", "authors", "id"),)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert fresh_db.table("books").foreign_keys == []
|
assert fresh_db["books"].foreign_keys == []
|
||||||
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
assert fresh_db.conn.execute("PRAGMA foreign_keys").fetchone()[0]
|
||||||
|
|
||||||
|
|
||||||
def test_atomic_inside_manual_transaction_uses_savepoint(fresh_db):
|
def test_atomic_inside_manual_transaction_uses_savepoint(fresh_db):
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.execute("begin")
|
fresh_db.execute("begin")
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("t").insert({"id": 2}, pk="id")
|
fresh_db["t"].insert({"id": 2}, pk="id")
|
||||||
# Nothing is committed until the user's own transaction commits
|
# Nothing is committed until the user's own transaction commits
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1]
|
||||||
# And with a commit instead, the atomic block's writes persist
|
# And with a commit instead, the atomic block's writes persist
|
||||||
fresh_db.execute("begin")
|
fresh_db.execute("begin")
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.table("t").insert({"id": 3}, pk="id")
|
fresh_db["t"].insert({"id": 3}, pk="id")
|
||||||
fresh_db.commit()
|
fresh_db.commit()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1, 3]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1, 3]
|
||||||
|
|
||||||
|
|
||||||
def test_begin_commit_rollback(tmpdir):
|
def test_begin_commit_rollback(tmpdir):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("t").insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
db.begin()
|
db.begin()
|
||||||
db.table("t").insert({"id": 2}, pk="id")
|
db["t"].insert({"id": 2}, pk="id")
|
||||||
assert db.conn.in_transaction
|
assert db.conn.in_transaction
|
||||||
db.rollback()
|
db.rollback()
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
assert [r["id"] for r in db.table("t").rows] == [1]
|
assert [r["id"] for r in db["t"].rows] == [1]
|
||||||
db.begin()
|
db.begin()
|
||||||
db.table("t").insert({"id": 3}, pk="id")
|
db["t"].insert({"id": 3}, pk="id")
|
||||||
db.commit()
|
db.commit()
|
||||||
db.close()
|
db.close()
|
||||||
db2 = Database(path)
|
db2 = Database(path)
|
||||||
assert [r["id"] for r in db2.table("t").rows] == [1, 3]
|
assert [r["id"] for r in db2["t"].rows] == [1, 3]
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -224,7 +226,7 @@ def test_commit_and_rollback_without_transaction_are_noops(fresh_db):
|
||||||
def test_execute_write_commits_immediately(tmpdir):
|
def test_execute_write_commits_immediately(tmpdir):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("t").insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
db.execute("insert into t (id) values (2)")
|
db.execute("insert into t (id) values (2)")
|
||||||
# No implicit transaction is left open
|
# No implicit transaction is left open
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
|
|
@ -236,24 +238,24 @@ def test_execute_write_commits_immediately(tmpdir):
|
||||||
|
|
||||||
|
|
||||||
def test_execute_write_respects_explicit_transaction(fresh_db):
|
def test_execute_write_respects_explicit_transaction(fresh_db):
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
fresh_db.execute("insert into t (id) values (2)")
|
fresh_db.execute("insert into t (id) values (2)")
|
||||||
# Still inside the explicit transaction - not committed
|
# Still inside the explicit transaction - not committed
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
def test_execute_comment_prefixed_begin_leaves_transaction_open(fresh_db):
|
def test_execute_comment_prefixed_begin_leaves_transaction_open(fresh_db):
|
||||||
# A BEGIN hidden behind a leading comment must not be auto-committed
|
# A BEGIN hidden behind a leading comment must not be auto-committed
|
||||||
# out from under the caller
|
# out from under the caller
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.execute("-- start a transaction\nbegin")
|
fresh_db.execute("-- start a transaction\nbegin")
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.execute("insert into t (id) values (2)")
|
fresh_db.execute("insert into t (id) values (2)")
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
def _sqlite_accepts_bom():
|
def _sqlite_accepts_bom():
|
||||||
|
|
@ -271,12 +273,12 @@ def test_execute_prefixed_begin_leaves_transaction_open(fresh_db, begin_sql):
|
||||||
# out from under the caller
|
# out from under the caller
|
||||||
if begin_sql.startswith("\ufeff") and not _sqlite_accepts_bom():
|
if begin_sql.startswith("\ufeff") and not _sqlite_accepts_bom():
|
||||||
pytest.skip("This SQLite version rejects a leading byte order mark")
|
pytest.skip("This SQLite version rejects a leading byte order mark")
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.execute(begin_sql)
|
fresh_db.execute(begin_sql)
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.execute("insert into t (id) values (2)")
|
fresh_db.execute("insert into t (id) values (2)")
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
def test_execute_failed_write_rolls_back_implicit_transaction(tmpdir):
|
def test_execute_failed_write_rolls_back_implicit_transaction(tmpdir):
|
||||||
|
|
@ -284,40 +286,40 @@ def test_execute_failed_write_rolls_back_implicit_transaction(tmpdir):
|
||||||
# that would silently disable auto-commit for every subsequent write
|
# that would silently disable auto-commit for every subsequent write
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("t").insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
db.execute("insert into t (id) values (1)")
|
db.execute("insert into t (id) values (1)")
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
# Subsequent writes commit as normal and survive closing the connection
|
# Subsequent writes commit as normal and survive closing the connection
|
||||||
db.table("other").insert({"id": 2})
|
db["other"].insert({"id": 2})
|
||||||
db.close()
|
db.close()
|
||||||
db2 = Database(path)
|
db2 = Database(path)
|
||||||
assert db2.table("other").exists()
|
assert db2["other"].exists()
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
def test_execute_failed_write_preserves_explicit_transaction(fresh_db):
|
def test_execute_failed_write_preserves_explicit_transaction(fresh_db):
|
||||||
# A failed write inside an explicit transaction must not roll back
|
# A failed write inside an explicit transaction must not roll back
|
||||||
# the caller's earlier work - only the caller decides that
|
# the caller's earlier work - only the caller decides that
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
fresh_db.execute("insert into t (id) values (2)")
|
fresh_db.execute("insert into t (id) values (2)")
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
fresh_db.execute("insert into t (id) values (1)")
|
fresh_db.execute("insert into t (id) values (1)")
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.commit()
|
fresh_db.commit()
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1, 2]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1, 2]
|
||||||
|
|
||||||
|
|
||||||
def test_execute_failed_write_inside_atomic_preserves_block(fresh_db):
|
def test_execute_failed_write_inside_atomic_preserves_block(fresh_db):
|
||||||
# A caught failure inside an atomic() block must leave the block's
|
# A caught failure inside an atomic() block must leave the block's
|
||||||
# transaction open so its other work still commits
|
# transaction open so its other work still commits
|
||||||
fresh_db.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
with fresh_db.atomic():
|
with fresh_db.atomic():
|
||||||
fresh_db.execute("insert into t (id) values (2)")
|
fresh_db.execute("insert into t (id) values (2)")
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
fresh_db.execute("insert into t (id) values (1)")
|
fresh_db.execute("insert into t (id) values (1)")
|
||||||
assert [r["id"] for r in fresh_db.table("t").rows] == [1, 2]
|
assert [r["id"] for r in fresh_db["t"].rows] == [1, 2]
|
||||||
|
|
||||||
|
|
||||||
def test_query_returning_commits_after_iteration(tmpdir):
|
def test_query_returning_commits_after_iteration(tmpdir):
|
||||||
|
|
@ -327,7 +329,7 @@ def test_query_returning_commits_after_iteration(tmpdir):
|
||||||
_pytest.skip("RETURNING requires SQLite 3.35.0 or higher")
|
_pytest.skip("RETURNING requires SQLite 3.35.0 or higher")
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("t").insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
rows = list(db.query("insert into t (id) values (2) returning id"))
|
rows = list(db.query("insert into t (id) values (2) returning id"))
|
||||||
assert rows == [{"id": 2}]
|
assert rows == [{"id": 2}]
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
|
|
@ -352,10 +354,8 @@ 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 (
|
with pytest.raises(sqlite3.IntegrityError, match="trigger says no"):
|
||||||
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.execute("insert into t (v) values ('bad')")
|
||||||
assert not fresh_db.conn.in_transaction
|
assert not fresh_db.conn.in_transaction
|
||||||
|
|
||||||
|
|
@ -367,17 +367,16 @@ 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 (
|
with pytest.raises(sqlite3.IntegrityError, match="trigger says no"):
|
||||||
pytest.raises(sqlite3.IntegrityError, match="trigger says no"),
|
with fresh_db.atomic():
|
||||||
fresh_db.atomic(),
|
with fresh_db.atomic():
|
||||||
fresh_db.atomic(),
|
|
||||||
):
|
|
||||||
fresh_db.execute("insert into t (v) values ('bad')")
|
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.table("t").insert({"id": 1}, pk="id")
|
fresh_db["t"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(sqlite3.IntegrityError), fresh_db.atomic():
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ def test_attach(tmpdir):
|
||||||
bar_path = str(tmpdir / "bar.db")
|
bar_path = str(tmpdir / "bar.db")
|
||||||
db = Database(foo_path)
|
db = Database(foo_path)
|
||||||
with db.conn:
|
with db.conn:
|
||||||
db.table("foo").insert({"id": 1, "text": "foo"})
|
db["foo"].insert({"id": 1, "text": "foo"})
|
||||||
db2 = Database(bar_path)
|
db2 = Database(bar_path)
|
||||||
with db2.conn:
|
with db2.conn:
|
||||||
db2.table("bar").insert({"id": 1, "text": "bar"})
|
db2["bar"].insert({"id": 1, "text": "bar"})
|
||||||
db.attach("bar", bar_path)
|
db.attach("bar", bar_path)
|
||||||
assert db.execute(
|
assert db.execute(
|
||||||
"select * from foo union all select * from bar.bar"
|
"select * from foo union all select * from bar.bar"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,17 @@
|
||||||
|
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):
|
||||||
db_path = str(pathlib.Path(tmpdir) / "data.db")
|
db_path = str(pathlib.Path(tmpdir) / "data.db")
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("example").insert_all(
|
db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "name": "One"},
|
{"id": 1, "name": "One"},
|
||||||
{"id": 2, "name": "Two"},
|
{"id": 2, "name": "Two"},
|
||||||
|
|
@ -44,7 +42,7 @@ def test_cli_bulk(test_db_and_path):
|
||||||
{"id": 2, "name": "Two"},
|
{"id": 2, "name": "Two"},
|
||||||
{"id": 3, "name": "THREE"},
|
{"id": 3, "name": "THREE"},
|
||||||
{"id": 4, "name": "FOUR"},
|
{"id": 4, "name": "FOUR"},
|
||||||
] == list(db.table("example").rows)
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_bulk_multiple_functions(test_db_and_path):
|
def test_cli_bulk_multiple_functions(test_db_and_path):
|
||||||
|
|
@ -70,7 +68,7 @@ def test_cli_bulk_multiple_functions(test_db_and_path):
|
||||||
{"id": 2, "name": "Two"},
|
{"id": 2, "name": "Two"},
|
||||||
{"id": 3, "name": "THREE"},
|
{"id": 3, "name": "THREE"},
|
||||||
{"id": 4, "name": "FOUR"},
|
{"id": 4, "name": "FOUR"},
|
||||||
] == list(db.table("example").rows)
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_bulk_batch_size(test_db_and_path):
|
def test_cli_bulk_batch_size(test_db_and_path):
|
||||||
|
|
@ -91,18 +89,17 @@ def test_cli_bulk_batch_size(test_db_and_path):
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
)
|
)
|
||||||
assert proc.stdin is not None
|
|
||||||
# Writing one record should not commit
|
# Writing one record should not commit
|
||||||
proc.stdin.write(b'{"id": 3, "name": "Three"}\n\n')
|
proc.stdin.write(b'{"id": 3, "name": "Three"}\n\n')
|
||||||
proc.stdin.flush()
|
proc.stdin.flush()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
assert db.table("example").count == 2
|
assert db["example"].count == 2
|
||||||
|
|
||||||
# Writing another should trigger a commit:
|
# Writing another should trigger a commit:
|
||||||
proc.stdin.write(b'{"id": 4, "name": "Four"}\n\n')
|
proc.stdin.write(b'{"id": 4, "name": "Four"}\n\n')
|
||||||
proc.stdin.flush()
|
proc.stdin.flush()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
assert db.table("example").count == 4
|
assert db["example"].count == 4
|
||||||
|
|
||||||
proc.stdin.close()
|
proc.stdin.close()
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
import sqlite_utils
|
|
||||||
from sqlite_utils import cli
|
from sqlite_utils import cli
|
||||||
|
import sqlite_utils
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
import pathlib
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_db_and_path(fresh_db_and_path):
|
def test_db_and_path(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("example").insert_all(
|
db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "dt": "5th October 2019 12:04"},
|
{"id": 1, "dt": "5th October 2019 12:04"},
|
||||||
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
||||||
|
|
@ -47,12 +45,12 @@ def fresh_db_and_path(tmpdir):
|
||||||
)
|
)
|
||||||
def test_convert_code(fresh_db_and_path, code):
|
def test_convert_code(fresh_db_and_path, code):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("t").insert({"text": "October"})
|
db["t"].insert({"text": "October"})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
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 = next(iter(db.table("t").rows))["text"]
|
value = list(db["t"].rows)[0]["text"]
|
||||||
assert value == "Spooktober"
|
assert value == "Spooktober"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,7 +63,7 @@ def test_convert_code(fresh_db_and_path, code):
|
||||||
)
|
)
|
||||||
def test_convert_code_errors(fresh_db_and_path, bad_code):
|
def test_convert_code_errors(fresh_db_and_path, bad_code):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("t").insert({"text": "October"})
|
db["t"].insert({"text": "October"})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli, ["convert", db_path, "t", "text", bad_code], catch_exceptions=False
|
cli.cli, ["convert", db_path, "t", "text", bad_code], catch_exceptions=False
|
||||||
)
|
)
|
||||||
|
|
@ -93,12 +91,12 @@ def test_convert_import(test_db_and_path):
|
||||||
{"id": 2, "dt": "6th OXXober 2019 00:05:06"},
|
{"id": 2, "dt": "6th OXXober 2019 00:05:06"},
|
||||||
{"id": 3, "dt": ""},
|
{"id": 3, "dt": ""},
|
||||||
{"id": 4, "dt": None},
|
{"id": 4, "dt": None},
|
||||||
] == list(db.table("example").rows)
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_import_nested(fresh_db_and_path):
|
def test_convert_import_nested(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("example").insert({"xml": '<item name="Cleo" />'})
|
db["example"].insert({"xml": '<item name="Cleo" />'})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
[
|
[
|
||||||
|
|
@ -114,7 +112,7 @@ def test_convert_import_nested(fresh_db_and_path):
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert [
|
assert [
|
||||||
{"xml": "Cleo"},
|
{"xml": "Cleo"},
|
||||||
] == list(db.table("example").rows)
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_dryrun(test_db_and_path):
|
def test_convert_dryrun(test_db_and_path):
|
||||||
|
|
@ -152,7 +150,7 @@ def test_convert_dryrun(test_db_and_path):
|
||||||
"Would affect 4 rows"
|
"Would affect 4 rows"
|
||||||
)
|
)
|
||||||
# But it should not have actually modified the table data
|
# But it should not have actually modified the table data
|
||||||
assert list(db.table("example").rows) == [
|
assert list(db["example"].rows) == [
|
||||||
{"id": 1, "dt": "5th October 2019 12:04"},
|
{"id": 1, "dt": "5th October 2019 12:04"},
|
||||||
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
||||||
{"id": 3, "dt": ""},
|
{"id": 3, "dt": ""},
|
||||||
|
|
@ -181,34 +179,6 @@ def test_convert_dryrun(test_db_and_path):
|
||||||
assert result.output.strip().split("\n")[-1] == "Would affect 1 row"
|
assert result.output.strip().split("\n")[-1] == "Would affect 1 row"
|
||||||
|
|
||||||
|
|
||||||
def test_convert_dryrun_table_and_column_names_containing_closing_bracket(
|
|
||||||
fresh_db_and_path,
|
|
||||||
):
|
|
||||||
db, db_path = fresh_db_and_path
|
|
||||||
table_name = "table]name"
|
|
||||||
column_name = "column]name"
|
|
||||||
db[table_name].insert({column_name: "hello"})
|
|
||||||
|
|
||||||
result = CliRunner().invoke(
|
|
||||||
cli.cli,
|
|
||||||
[
|
|
||||||
"convert",
|
|
||||||
db_path,
|
|
||||||
table_name,
|
|
||||||
column_name,
|
|
||||||
"value.upper()",
|
|
||||||
"--dry-run",
|
|
||||||
],
|
|
||||||
catch_exceptions=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert result.output.strip() == (
|
|
||||||
"hello\n --- becomes:\nHELLO\n\nWould affect 1 row"
|
|
||||||
)
|
|
||||||
assert list(db[table_name].rows) == [{column_name: "hello"}]
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi_dryrun(test_db_and_path):
|
def test_convert_multi_dryrun(test_db_and_path):
|
||||||
db_path = test_db_and_path[1]
|
db_path = test_db_and_path[1]
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
@ -297,7 +267,7 @@ def test_convert_output_column(test_db_and_path, drop):
|
||||||
if drop:
|
if drop:
|
||||||
for row in expected:
|
for row in expected:
|
||||||
del row["dt"]
|
del row["dt"]
|
||||||
assert list(db.table("example").rows) == expected
|
assert list(db["example"].rows) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -380,7 +350,7 @@ def test_convert_output_error(test_db_and_path, options, expected_error):
|
||||||
@pytest.mark.parametrize("drop", (True, False))
|
@pytest.mark.parametrize("drop", (True, False))
|
||||||
def test_convert_multi(fresh_db_and_path, drop):
|
def test_convert_multi(fresh_db_and_path, drop):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("creatures").insert_all(
|
db["creatures"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "name": "Simon"},
|
{"id": 1, "name": "Simon"},
|
||||||
{"id": 2, "name": "Cleo"},
|
{"id": 2, "name": "Cleo"},
|
||||||
|
|
@ -406,12 +376,12 @@ def test_convert_multi(fresh_db_and_path, drop):
|
||||||
if drop:
|
if drop:
|
||||||
for row in expected:
|
for row in expected:
|
||||||
del row["name"]
|
del row["name"]
|
||||||
assert list(db.table("creatures").rows) == expected
|
assert list(db["creatures"].rows) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi_complex_column_types(fresh_db_and_path):
|
def test_convert_multi_complex_column_types(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("rows").insert_all(
|
db["rows"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1},
|
{"id": 1},
|
||||||
{"id": 2},
|
{"id": 2},
|
||||||
|
|
@ -440,7 +410,7 @@ def test_convert_multi_complex_column_types(fresh_db_and_path):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("rows").rows) == [
|
assert list(db["rows"].rows) == [
|
||||||
{"id": 1, "is_str": "", "is_float": 1.2, "is_int": None, "is_bytes": None},
|
{"id": 1, "is_str": "", "is_float": 1.2, "is_int": None, "is_bytes": None},
|
||||||
{"id": 2, "is_str": None, "is_float": 1.0, "is_int": 12, "is_bytes": None},
|
{"id": 2, "is_str": None, "is_float": 1.0, "is_int": 12, "is_bytes": None},
|
||||||
{
|
{
|
||||||
|
|
@ -452,7 +422,7 @@ def test_convert_multi_complex_column_types(fresh_db_and_path):
|
||||||
},
|
},
|
||||||
{"id": 4, "is_str": None, "is_float": None, "is_int": None, "is_bytes": None},
|
{"id": 4, "is_str": None, "is_float": None, "is_int": None, "is_bytes": None},
|
||||||
]
|
]
|
||||||
assert db.table("rows").schema == (
|
assert db["rows"].schema == (
|
||||||
'CREATE TABLE "rows" (\n'
|
'CREATE TABLE "rows" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY\n'
|
' "id" INTEGER PRIMARY KEY\n'
|
||||||
', "is_str" TEXT, "is_float" REAL, "is_int" INTEGER, "is_bytes" BLOB)'
|
', "is_str" TEXT, "is_float" REAL, "is_int" INTEGER, "is_bytes" BLOB)'
|
||||||
|
|
@ -463,7 +433,7 @@ def test_convert_multi_complex_column_types(fresh_db_and_path):
|
||||||
def test_recipe_jsonsplit(tmpdir, delimiter):
|
def test_recipe_jsonsplit(tmpdir, delimiter):
|
||||||
db_path = str(pathlib.Path(tmpdir) / "data.db")
|
db_path = str(pathlib.Path(tmpdir) / "data.db")
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
db.table("example").insert_all(
|
db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "tags": (delimiter or ",").join(["foo", "bar"])},
|
{"id": 1, "tags": (delimiter or ",").join(["foo", "bar"])},
|
||||||
{"id": 2, "tags": (delimiter or ",").join(["bar", "baz"])},
|
{"id": 2, "tags": (delimiter or ",").join(["bar", "baz"])},
|
||||||
|
|
@ -472,11 +442,11 @@ def test_recipe_jsonsplit(tmpdir, delimiter):
|
||||||
)
|
)
|
||||||
code = "r.jsonsplit(value)"
|
code = "r.jsonsplit(value)"
|
||||||
if delimiter:
|
if delimiter:
|
||||||
code = f'recipes.jsonsplit(value, delimiter="{delimiter}")'
|
code = 'recipes.jsonsplit(value, delimiter="{}")'.format(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
|
||||||
assert list(db.table("example").rows) == [
|
assert list(db["example"].rows) == [
|
||||||
{"id": 1, "tags": '["foo", "bar"]'},
|
{"id": 1, "tags": '["foo", "bar"]'},
|
||||||
{"id": 2, "tags": '["bar", "baz"]'},
|
{"id": 2, "tags": '["bar", "baz"]'},
|
||||||
]
|
]
|
||||||
|
|
@ -492,7 +462,7 @@ def test_recipe_jsonsplit(tmpdir, delimiter):
|
||||||
)
|
)
|
||||||
def test_recipe_jsonsplit_type(fresh_db_and_path, type, expected_array):
|
def test_recipe_jsonsplit_type(fresh_db_and_path, type, expected_array):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("example").insert_all(
|
db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "records": "1,2,3"},
|
{"id": 1, "records": "1,2,3"},
|
||||||
],
|
],
|
||||||
|
|
@ -500,17 +470,17 @@ 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 = f"recipes.jsonsplit(value, type={type})"
|
code = "recipes.jsonsplit(value, type={})".format(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
|
||||||
assert json.loads(db.table("example").get(1)["records"]) == expected_array
|
assert json.loads(db["example"].get(1)["records"]) == expected_array
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("drop", (True, False))
|
@pytest.mark.parametrize("drop", (True, False))
|
||||||
def test_recipe_jsonsplit_output(fresh_db_and_path, drop):
|
def test_recipe_jsonsplit_output(fresh_db_and_path, drop):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("example").insert_all(
|
db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "records": "1,2,3"},
|
{"id": 1, "records": "1,2,3"},
|
||||||
],
|
],
|
||||||
|
|
@ -529,7 +499,7 @@ def test_recipe_jsonsplit_output(fresh_db_and_path, drop):
|
||||||
}
|
}
|
||||||
if drop:
|
if drop:
|
||||||
del expected["records"]
|
del expected["records"]
|
||||||
assert db.table("example").get(1) == expected
|
assert db["example"].get(1) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_use_drop_without_multi_or_output(fresh_db_and_path):
|
def test_cannot_use_drop_without_multi_or_output(fresh_db_and_path):
|
||||||
|
|
@ -586,7 +556,7 @@ def test_convert_where(test_db_and_path):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("example").rows) == [
|
assert list(db["example"].rows) == [
|
||||||
{"id": 1, "dt": "5th October 2019 12:04"},
|
{"id": 1, "dt": "5th October 2019 12:04"},
|
||||||
{"id": 2, "dt": "6TH OCTOBER 2019 00:05:06"},
|
{"id": 2, "dt": "6TH OCTOBER 2019 00:05:06"},
|
||||||
{"id": 3, "dt": ""},
|
{"id": 3, "dt": ""},
|
||||||
|
|
@ -596,7 +566,7 @@ def test_convert_where(test_db_and_path):
|
||||||
|
|
||||||
def test_convert_where_multi(fresh_db_and_path):
|
def test_convert_where_multi(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("names").insert_all(
|
db["names"].insert_all(
|
||||||
[{"id": 1, "name": "Cleo"}, {"id": 2, "name": "Bants"}], pk="id"
|
[{"id": 1, "name": "Cleo"}, {"id": 2, "name": "Bants"}], pk="id"
|
||||||
)
|
)
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
@ -616,7 +586,7 @@ def test_convert_where_multi(fresh_db_and_path):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("names").rows) == [
|
assert list(db["names"].rows) == [
|
||||||
{"id": 1, "name": "Cleo", "upper": None},
|
{"id": 1, "name": "Cleo", "upper": None},
|
||||||
{"id": 2, "name": "Bants", "upper": "BANTS"},
|
{"id": 2, "name": "Bants", "upper": "BANTS"},
|
||||||
]
|
]
|
||||||
|
|
@ -624,7 +594,7 @@ def test_convert_where_multi(fresh_db_and_path):
|
||||||
|
|
||||||
def test_convert_code_standard_input(fresh_db_and_path):
|
def test_convert_code_standard_input(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("names").insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
db["names"].insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
[
|
[
|
||||||
|
|
@ -637,27 +607,27 @@ def test_convert_code_standard_input(fresh_db_and_path):
|
||||||
input="value.upper()",
|
input="value.upper()",
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("names").rows) == [
|
assert list(db["names"].rows) == [
|
||||||
{"id": 1, "name": "CLEO"},
|
{"id": 1, "name": "CLEO"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_hyphen_workaround(fresh_db_and_path):
|
def test_convert_hyphen_workaround(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("names").insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
db["names"].insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["convert", db_path, "names", "name", '"-"'],
|
["convert", db_path, "names", "name", '"-"'],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("names").rows) == [
|
assert list(db["names"].rows) == [
|
||||||
{"id": 1, "name": "-"},
|
{"id": 1, "name": "-"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_initialization_pattern(fresh_db_and_path):
|
def test_convert_initialization_pattern(fresh_db_and_path):
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("names").insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
db["names"].insert_all([{"id": 1, "name": "Cleo"}], pk="id")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
[
|
[
|
||||||
|
|
@ -670,7 +640,7 @@ def test_convert_initialization_pattern(fresh_db_and_path):
|
||||||
input="import random\nrandom.seed(1)\ndef convert(value): return random.randint(0, 100)",
|
input="import random\nrandom.seed(1)\ndef convert(value): return random.randint(0, 100)",
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("names").rows) == [
|
assert list(db["names"].rows) == [
|
||||||
{"id": 1, "name": "17"},
|
{"id": 1, "name": "17"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -685,13 +655,13 @@ def test_convert_handles_falsey_values(fresh_db_and_path):
|
||||||
"x",
|
"x",
|
||||||
"-",
|
"-",
|
||||||
]
|
]
|
||||||
db.table("t").insert_all([{"x": 0}, {"x": 1}])
|
db["t"].insert_all([{"x": 0}, {"x": 1}])
|
||||||
assert db.table("t").get(1)["x"] == 0
|
assert db["t"].get(1)["x"] == 0
|
||||||
assert db.table("t").get(2)["x"] == 1
|
assert db["t"].get(2)["x"] == 1
|
||||||
result = CliRunner().invoke(cli.cli, args, input="value + 1")
|
result = CliRunner().invoke(cli.cli, args, input="value + 1")
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert db.table("t").get(1)["x"] == 1
|
assert db["t"].get(1)["x"] == 1
|
||||||
assert db.table("t").get(2)["x"] == 2
|
assert db["t"].get(2)["x"] == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -712,7 +682,7 @@ def test_convert_callable_reference(test_db_and_path, code):
|
||||||
cli.cli, ["convert", db_path, "example", "dt", code], catch_exceptions=False
|
cli.cli, ["convert", db_path, "example", "dt", code], catch_exceptions=False
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
rows = list(db.table("example").rows)
|
rows = list(db["example"].rows)
|
||||||
assert rows[0]["dt"] == "2019-10-05"
|
assert rows[0]["dt"] == "2019-10-05"
|
||||||
assert rows[1]["dt"] == "2019-10-06"
|
assert rows[1]["dt"] == "2019-10-06"
|
||||||
assert rows[2]["dt"] == ""
|
assert rows[2]["dt"] == ""
|
||||||
|
|
@ -722,7 +692,7 @@ def test_convert_callable_reference(test_db_and_path, code):
|
||||||
def test_convert_callable_reference_with_import(fresh_db_and_path):
|
def test_convert_callable_reference_with_import(fresh_db_and_path):
|
||||||
"""Test callable reference from an imported module"""
|
"""Test callable reference from an imported module"""
|
||||||
db, db_path = fresh_db_and_path
|
db, db_path = fresh_db_and_path
|
||||||
db.table("example").insert({"id": 1, "data": '{"name": "test"}'})
|
db["example"].insert({"id": 1, "data": '{"name": "test"}'})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
[
|
[
|
||||||
|
|
@ -738,5 +708,5 @@ def test_convert_callable_reference_with_import(fresh_db_and_path):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
# json.loads returns a dict, which sqlite stores as JSON string
|
# json.loads returns a dict, which sqlite stores as JSON string
|
||||||
row = db.table("example").get(1)
|
row = db["example"].get(1)
|
||||||
assert row["data"] == '{"name": "test"}'
|
assert row["data"] == '{"name": "test"}'
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
|
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")
|
||||||
|
|
@ -21,7 +19,7 @@ def test_insert_simple(tmpdir):
|
||||||
)
|
)
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert ["dogs"] == db.table_names()
|
assert ["dogs"] == db.table_names()
|
||||||
assert [] == db.table("dogs").indexes
|
assert [] == db["dogs"].indexes
|
||||||
|
|
||||||
|
|
||||||
def test_insert_from_stdin(tmpdir):
|
def test_insert_from_stdin(tmpdir):
|
||||||
|
|
@ -96,12 +94,12 @@ def test_insert_with_primary_keys(db_path, tmpdir, args, expected_pks):
|
||||||
Database(db_path).query("select * from dogs")
|
Database(db_path).query("select * from dogs")
|
||||||
)
|
)
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert db.table("dogs").pks == expected_pks
|
assert db["dogs"].pks == 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": f"Cleo {i}", "age": i + 3} for i in range(1, 21)]
|
dogs = [{"id": i, "name": "Cleo {}".format(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(
|
||||||
|
|
@ -110,13 +108,13 @@ def test_insert_multiple_with_primary_key(db_path, tmpdir):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert dogs == list(db.query("select * from dogs order by id"))
|
assert dogs == list(db.query("select * from dogs order by id"))
|
||||||
assert ["id"] == db.table("dogs").pks
|
assert ["id"] == db["dogs"].pks
|
||||||
|
|
||||||
|
|
||||||
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": f"Cleo {i}", "age": i + 3}
|
{"breed": "mixed", "id": i, "name": "Cleo {}".format(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:
|
||||||
|
|
@ -127,7 +125,7 @@ def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert dogs == list(db.query("select * from dogs order by breed, id"))
|
assert dogs == list(db.query("select * from dogs order by breed, id"))
|
||||||
assert {"breed", "id"} == set(db.table("dogs").pks)
|
assert {"breed", "id"} == set(db["dogs"].pks)
|
||||||
assert (
|
assert (
|
||||||
'CREATE TABLE "dogs" (\n'
|
'CREATE TABLE "dogs" (\n'
|
||||||
' "breed" TEXT,\n'
|
' "breed" TEXT,\n'
|
||||||
|
|
@ -136,13 +134,14 @@ def test_insert_multiple_with_compound_primary_key(db_path, tmpdir):
|
||||||
' "age" INTEGER,\n'
|
' "age" INTEGER,\n'
|
||||||
' PRIMARY KEY ("id", "breed")\n'
|
' PRIMARY KEY ("id", "breed")\n'
|
||||||
")"
|
")"
|
||||||
) == db.table("dogs").schema
|
) == db["dogs"].schema
|
||||||
|
|
||||||
|
|
||||||
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": f"Cleo {i}", "age": i + 3, "score": 10} for i in range(1, 21)
|
{"id": i, "name": "Cleo {}".format(i), "age": i + 3, "score": 10}
|
||||||
|
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))
|
||||||
|
|
@ -160,7 +159,7 @@ def test_insert_not_null_default(db_path, tmpdir):
|
||||||
' "name" TEXT NOT NULL,\n'
|
' "name" TEXT NOT NULL,\n'
|
||||||
" \"age\" INTEGER NOT NULL DEFAULT '1',\n"
|
" \"age\" INTEGER NOT NULL DEFAULT '1',\n"
|
||||||
" \"score\" INTEGER DEFAULT '5'\n)"
|
" \"score\" INTEGER DEFAULT '5'\n)"
|
||||||
) == db.table("dogs").schema
|
) == db["dogs"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_insert_binary_base64(db_path):
|
def test_insert_binary_base64(db_path):
|
||||||
|
|
@ -191,7 +190,7 @@ def test_insert_newline_delimited(db_path):
|
||||||
|
|
||||||
def test_insert_ignore(db_path, tmpdir):
|
def test_insert_ignore(db_path, tmpdir):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
|
db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
json_path = str(tmpdir / "dogs.json")
|
json_path = str(tmpdir / "dogs.json")
|
||||||
with open(json_path, "w") as fp:
|
with open(json_path, "w") as fp:
|
||||||
fp.write(json.dumps([{"id": 1, "name": "Bailey"}]))
|
fp.write(json.dumps([{"id": 1, "name": "Bailey"}]))
|
||||||
|
|
@ -232,7 +231,7 @@ def test_insert_csv_tsv(content, options, db_path, tmpdir):
|
||||||
catch_exceptions=False,
|
catch_exceptions=False,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert [{"foo": "1", "bar": "2", "baz": "cat,dog"}] == list(db.table("data").rows)
|
assert [{"foo": "1", "bar": "2", "baz": "cat,dog"}] == list(db["data"].rows)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("empty_null", (True, False))
|
@pytest.mark.parametrize("empty_null", (True, False))
|
||||||
|
|
@ -248,7 +247,7 @@ def test_insert_csv_empty_null(db_path, empty_null):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert [r for r in db.table("data").rows] == [
|
assert [r for r in db["data"].rows] == [
|
||||||
{"foo": "1", "bar": None if empty_null else "", "baz": "cat"}
|
{"foo": "1", "bar": None if empty_null else "", "baz": "cat"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -302,7 +301,7 @@ def test_insert_replace(db_path, tmpdir):
|
||||||
test_insert_multiple_with_primary_key(db_path, tmpdir)
|
test_insert_multiple_with_primary_key(db_path, tmpdir)
|
||||||
json_path = str(tmpdir / "insert-replace.json")
|
json_path = str(tmpdir / "insert-replace.json")
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert db.table("dogs").count == 20
|
assert db["dogs"].count == 20
|
||||||
insert_replace_dogs = [
|
insert_replace_dogs = [
|
||||||
{"id": 1, "name": "Insert replaced 1", "age": 4},
|
{"id": 1, "name": "Insert replaced 1", "age": 4},
|
||||||
{"id": 2, "name": "Insert replaced 2", "age": 4},
|
{"id": 2, "name": "Insert replaced 2", "age": 4},
|
||||||
|
|
@ -314,7 +313,7 @@ def test_insert_replace(db_path, tmpdir):
|
||||||
cli.cli, ["insert", db_path, "dogs", json_path, "--pk", "id", "--replace"]
|
cli.cli, ["insert", db_path, "dogs", json_path, "--pk", "id", "--replace"]
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert db.table("dogs").count == 21
|
assert db["dogs"].count == 21
|
||||||
assert (
|
assert (
|
||||||
list(db.query("select * from dogs where id in (1, 2, 21) order by id"))
|
list(db.query("select * from dogs where id in (1, 2, 21) order by id"))
|
||||||
== insert_replace_dogs
|
== insert_replace_dogs
|
||||||
|
|
@ -377,7 +376,7 @@ def test_insert_alter(db_path, tmpdir):
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
# Soundness check the database itself
|
# Soundness check the database itself
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert {"foo": str, "n": int, "baz": int} == db.table("from_json_nl").columns_dict
|
assert {"foo": str, "n": int, "baz": int} == db["from_json_nl"].columns_dict
|
||||||
assert [
|
assert [
|
||||||
{"foo": "bar", "n": 1, "baz": None},
|
{"foo": "bar", "n": 1, "baz": None},
|
||||||
{"foo": "baz", "n": 2, "baz": None},
|
{"foo": "baz", "n": 2, "baz": None},
|
||||||
|
|
@ -387,8 +386,8 @@ def test_insert_alter(db_path, tmpdir):
|
||||||
|
|
||||||
def test_insert_analyze(db_path):
|
def test_insert_analyze(db_path):
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("rows").insert({"foo": "x", "n": 3})
|
db["rows"].insert({"foo": "x", "n": 3})
|
||||||
db.table("rows").create_index(["n"])
|
db["rows"].create_index(["n"])
|
||||||
assert "sqlite_stat1" not in db.table_names()
|
assert "sqlite_stat1" not in db.table_names()
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
|
|
@ -577,19 +576,18 @@ def test_insert_streaming_batch_size_1(db_path):
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
)
|
)
|
||||||
assert proc.stdin is not None
|
|
||||||
proc.stdin.write(b'{"name": "Azi"}\n')
|
proc.stdin.write(b'{"name": "Azi"}\n')
|
||||||
proc.stdin.flush()
|
proc.stdin.flush()
|
||||||
|
|
||||||
def try_until(expected):
|
def try_until(expected):
|
||||||
tries = 0
|
tries = 0
|
||||||
while True:
|
while True:
|
||||||
rows = list(Database(db_path).table("rows").rows)
|
rows = list(Database(db_path)["rows"].rows)
|
||||||
if rows == expected:
|
if rows == expected:
|
||||||
return
|
return
|
||||||
tries += 1
|
tries += 1
|
||||||
if tries > 10:
|
if tries > 10:
|
||||||
assert False, f"Expected {expected}, got {rows}"
|
assert False, "Expected {}, got {}".format(expected, rows)
|
||||||
time.sleep(tries * 0.1)
|
time.sleep(tries * 0.1)
|
||||||
|
|
||||||
try_until([{"name": "Azi"}])
|
try_until([{"name": "Azi"}])
|
||||||
|
|
@ -616,13 +614,13 @@ def test_insert_csv_headers_only(tmpdir):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
# Table should not exist since there were no data rows
|
# Table should not exist since there were no data rows
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert not db.table("data").exists()
|
assert not db["data"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_insert_into_view_errors(tmpdir):
|
def test_insert_into_view_errors(tmpdir):
|
||||||
db_path = str(tmpdir / "test.db")
|
db_path = str(tmpdir / "test.db")
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("t").insert({"id": 1})
|
db["t"].insert({"id": 1})
|
||||||
db.create_view("v", "select * from t")
|
db.create_view("v", "select * from t")
|
||||||
db.close()
|
db.close()
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
@ -638,7 +636,7 @@ def test_insert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||||
# table would rewrite its column types and corrupt data such as
|
# table would rewrite its column types and corrupt data such as
|
||||||
# TEXT zip codes with leading zeros
|
# TEXT zip codes with leading zeros
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("places").insert({"name": "Boston", "zip": "01234"})
|
db["places"].insert({"name": "Boston", "zip": "01234"})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert", db_path, "places", "-", "--csv"],
|
["insert", db_path, "places", "-", "--csv"],
|
||||||
|
|
@ -646,8 +644,8 @@ def test_insert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||||
input="name,zip\nSF,94107",
|
input="name,zip\nSF,94107",
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert db.table("places").columns_dict["zip"] is str
|
assert db["places"].columns_dict["zip"] is str
|
||||||
assert list(db.table("places").rows) == [
|
assert list(db["places"].rows) == [
|
||||||
{"name": "Boston", "zip": "01234"},
|
{"name": "Boston", "zip": "01234"},
|
||||||
{"name": "SF", "zip": "94107"},
|
{"name": "SF", "zip": "94107"},
|
||||||
]
|
]
|
||||||
|
|
@ -663,7 +661,7 @@ def test_insert_csv_detect_types_new_table(db_path):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert db.table("data").columns_dict == {"name": str, "age": int, "weight": float}
|
assert db["data"].columns_dict == {"name": str, "age": int, "weight": float}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -709,13 +707,13 @@ def test_insert_upsert_csv_type_overrides_detected_types(
|
||||||
expected_columns = {"zipcode": str, "score": float}
|
expected_columns = {"zipcode": str, "score": float}
|
||||||
if command == "upsert":
|
if command == "upsert":
|
||||||
expected_columns = {"id": int, **expected_columns}
|
expected_columns = {"id": int, **expected_columns}
|
||||||
assert db.table("places").columns_dict == expected_columns
|
assert db["places"].columns_dict == expected_columns
|
||||||
assert list(db.table("places").rows) == [expected_row]
|
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.table("places").insert({"id": 1, "name": "Boston", "zip": "01234"}, pk="id")
|
db["places"].insert({"id": 1, "name": "Boston", "zip": "01234"}, pk="id")
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["upsert", db_path, "places", "-", "--csv", "--pk", "id"],
|
["upsert", db_path, "places", "-", "--csv", "--pk", "id"],
|
||||||
|
|
@ -723,15 +721,15 @@ def test_upsert_csv_detect_types_leaves_existing_table_alone(db_path):
|
||||||
input="id,name,zip\n2,SF,94107",
|
input="id,name,zip\n2,SF,94107",
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert db.table("places").columns_dict["zip"] is str
|
assert db["places"].columns_dict["zip"] is str
|
||||||
assert db.table("places").get(1)["zip"] == "01234"
|
assert db["places"].get(1)["zip"] == "01234"
|
||||||
|
|
||||||
|
|
||||||
def test_insert_invalid_pk_clean_error(db_path):
|
def test_insert_invalid_pk_clean_error(db_path):
|
||||||
# An invalid --pk against an existing table should be a clean CLI
|
# An invalid --pk against an existing table should be a clean CLI
|
||||||
# error, not a raw InvalidColumns traceback
|
# error, not a raw InvalidColumns traceback
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("t").insert({"a": 1})
|
db["t"].insert({"a": 1})
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["insert", db_path, "t", "-", "--pk", "badcol"],
|
["insert", db_path, "t", "-", "--pk", "badcol"],
|
||||||
|
|
@ -766,8 +764,8 @@ def test_insert_code(tmpdir, code):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert db.table("creatures").pks == ["id"]
|
assert db["creatures"].pks == ["id"]
|
||||||
assert list(db.table("creatures").rows) == [
|
assert list(db["creatures"].rows) == [
|
||||||
{"id": 1, "name": "Cleo"},
|
{"id": 1, "name": "Cleo"},
|
||||||
{"id": 2, "name": "Suna"},
|
{"id": 2, "name": "Suna"},
|
||||||
]
|
]
|
||||||
|
|
@ -783,7 +781,7 @@ def test_insert_code_from_file(tmpdir):
|
||||||
["insert", db_path, "creatures", "--code", code_path],
|
["insert", db_path, "creatures", "--code", code_path],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(Database(db_path).table("creatures").rows) == [
|
assert list(Database(db_path)["creatures"].rows) == [
|
||||||
{"id": 1, "name": "Cleo"},
|
{"id": 1, "name": "Cleo"},
|
||||||
{"id": 2, "name": "Suna"},
|
{"id": 2, "name": "Suna"},
|
||||||
]
|
]
|
||||||
|
|
@ -792,7 +790,7 @@ def test_insert_code_from_file(tmpdir):
|
||||||
def test_upsert_code(tmpdir):
|
def test_upsert_code(tmpdir):
|
||||||
db_path = str(tmpdir / "dogs.db")
|
db_path = str(tmpdir / "dogs.db")
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("creatures").insert_all(
|
db["creatures"].insert_all(
|
||||||
[{"id": 1, "name": "old"}, {"id": 2, "name": "Suna"}], pk="id"
|
[{"id": 1, "name": "old"}, {"id": 2, "name": "Suna"}], pk="id"
|
||||||
)
|
)
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
@ -800,7 +798,7 @@ def test_upsert_code(tmpdir):
|
||||||
["upsert", db_path, "creatures", "--code", CODE_ROWS_FUNCTION, "--pk", "id"],
|
["upsert", db_path, "creatures", "--code", CODE_ROWS_FUNCTION, "--pk", "id"],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(db.table("creatures").rows) == [
|
assert list(db["creatures"].rows) == [
|
||||||
{"id": 1, "name": "Cleo"},
|
{"id": 1, "name": "Cleo"},
|
||||||
{"id": 2, "name": "Suna"},
|
{"id": 2, "name": "Suna"},
|
||||||
]
|
]
|
||||||
|
|
@ -859,9 +857,7 @@ def test_insert_code_single_dict(tmpdir):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
assert list(Database(db_path).table("creatures").rows) == [
|
assert list(Database(db_path)["creatures"].rows) == [{"id": 1, "name": "Cleo"}]
|
||||||
{"id": 1, "name": "Cleo"}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_code_not_iterable(tmpdir):
|
def test_insert_code_not_iterable(tmpdir):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import json
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
|
@ -29,7 +28,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, f"select * from {sql_from}", "--nl"],
|
["memory", csv_path, "select * from {}".format(sql_from), "--nl"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
@ -54,7 +53,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, f"select * from {sql_from}"],
|
["memory", path, "select * from {}".format(sql_from)],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -80,7 +79,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, f"select * from {sql_from}"],
|
["memory", path, "select * from {}".format(sql_from)],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -106,7 +105,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, f"select * from {sql_from}"],
|
["memory", path, "select * from {}".format(sql_from)],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
|
|
@ -136,7 +135,7 @@ def test_memory_csv_encoding(tmpdir, use_stdin):
|
||||||
CliRunner()
|
CliRunner()
|
||||||
.invoke(
|
.invoke(
|
||||||
cli.cli,
|
cli.cli,
|
||||||
["memory", csv_path, f"select * from {sql_from}", "--nl"],
|
["memory", csv_path, "select * from {}".format(sql_from), "--nl"],
|
||||||
input=input,
|
input=input,
|
||||||
)
|
)
|
||||||
.exit_code
|
.exit_code
|
||||||
|
|
@ -228,7 +227,7 @@ def test_memory_save(tmpdir, extra_args):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
db = Database(save_to)
|
db = Database(save_to)
|
||||||
assert list(db.table("stdin").rows) == [
|
assert list(db["stdin"].rows) == [
|
||||||
{"id": 1, "name": "Cleo"},
|
{"id": 1, "name": "Cleo"},
|
||||||
{"id": 2, "name": "Bants"},
|
{"id": 2, "name": "Bants"},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
import pytest
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
import sqlite_utils.cli
|
import sqlite_utils.cli
|
||||||
|
|
||||||
|
|
@ -13,11 +12,11 @@ m = Migrations("hello")
|
||||||
|
|
||||||
@m()
|
@m()
|
||||||
def foo(db):
|
def foo(db):
|
||||||
db.table("foo").insert({"hello": "world"})
|
db["foo"].insert({"hello": "world"})
|
||||||
|
|
||||||
@m()
|
@m()
|
||||||
def bar(db):
|
def bar(db):
|
||||||
db.table("bar").insert({"hello": "world"})
|
db["bar"].insert({"hello": "world"})
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,21 +41,21 @@ creatures = Migrations("creatures")
|
||||||
|
|
||||||
@creatures()
|
@creatures()
|
||||||
def create_table(db):
|
def create_table(db):
|
||||||
db.table("creatures").insert({"name": "Cleo"})
|
db["creatures"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
@creatures()
|
@creatures()
|
||||||
def add_weight(db):
|
def add_weight(db):
|
||||||
db.table("creature_weights").insert({"weight": 4.2})
|
db["creature_weights"].insert({"weight": 4.2})
|
||||||
|
|
||||||
sales = Migrations("sales")
|
sales = Migrations("sales")
|
||||||
|
|
||||||
@sales()
|
@sales()
|
||||||
def create_table(db):
|
def create_table(db):
|
||||||
db.table("sales").insert({"id": 1})
|
db["sales"].insert({"id": 1})
|
||||||
|
|
||||||
@sales()
|
@sales()
|
||||||
def add_weight(db):
|
def add_weight(db):
|
||||||
db.table("sales_weights").insert({"weight": 10})
|
db["sales_weights"].insert({"weight": 10})
|
||||||
""",
|
""",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
)
|
)
|
||||||
|
|
@ -99,10 +98,10 @@ def test_basic(two_migrations, arg):
|
||||||
assert " Pending:\n (none)" in list_output
|
assert " Pending:\n (none)" in list_output
|
||||||
|
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert db.table("foo").exists()
|
assert db["foo"].exists()
|
||||||
assert db.table("bar").exists()
|
assert db["bar"].exists()
|
||||||
assert db.table("_sqlite_migrations").exists()
|
assert db["_sqlite_migrations"].exists()
|
||||||
rows = list(db.table("_sqlite_migrations").rows)
|
rows = list(db["_sqlite_migrations"].rows)
|
||||||
assert len(rows) == 2
|
assert len(rows) == 2
|
||||||
assert rows[0]["name"] == "foo"
|
assert rows[0]["name"] == "foo"
|
||||||
assert rows[1]["name"] == "bar"
|
assert rows[1]["name"] == "bar"
|
||||||
|
|
@ -113,13 +112,13 @@ def test_list_same_migration_names_in_different_sets(capsys):
|
||||||
|
|
||||||
@applied(name="foo")
|
@applied(name="foo")
|
||||||
def applied_foo(db):
|
def applied_foo(db):
|
||||||
db.table("applied").insert({"hello": "world"})
|
db["applied"].insert({"hello": "world"})
|
||||||
|
|
||||||
pending = sqlite_utils.Migrations("pending")
|
pending = sqlite_utils.Migrations("pending")
|
||||||
|
|
||||||
@pending(name="foo")
|
@pending(name="foo")
|
||||||
def pending_foo(db):
|
def pending_foo(db):
|
||||||
db.table("pending").insert({"hello": "world"})
|
db["pending"].insert({"hello": "world"})
|
||||||
|
|
||||||
db = sqlite_utils.Database(memory=True)
|
db = sqlite_utils.Database(memory=True)
|
||||||
applied.apply(db)
|
applied.apply(db)
|
||||||
|
|
@ -144,7 +143,7 @@ m = Migrations("hello")
|
||||||
|
|
||||||
@m()
|
@m()
|
||||||
def foo(db):
|
def foo(db):
|
||||||
db.table("dogs").insert({"id": 1, "name": "Cleo"})
|
db["dogs"].insert({"id": 1, "name": "Cleo"})
|
||||||
""",
|
""",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
)
|
)
|
||||||
|
|
@ -184,9 +183,9 @@ Schema after:
|
||||||
new_migration = """
|
new_migration = """
|
||||||
@m()
|
@m()
|
||||||
def bar(db):
|
def bar(db):
|
||||||
db.table("dogs").add_column("age", int)
|
db["dogs"].add_column("age", int)
|
||||||
db.table("dogs").add_column("weight", float)
|
db["dogs"].add_column("weight", float)
|
||||||
db.table("dogs").transform()
|
db["dogs"].transform()
|
||||||
"""
|
"""
|
||||||
migrations_py.write_text(migrations_py.read_text("utf-8") + new_migration)
|
migrations_py.write_text(migrations_py.read_text("utf-8") + new_migration)
|
||||||
|
|
||||||
|
|
@ -224,8 +223,8 @@ def test_stop_before(two_migrations):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert db.table("foo").exists()
|
assert db["foo"].exists()
|
||||||
assert not db.table("bar").exists()
|
assert not db["bar"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_stop_before_multiple_sets_unqualified(two_migrations):
|
def test_stop_before_multiple_sets_unqualified(two_migrations):
|
||||||
|
|
@ -239,7 +238,7 @@ m = Migrations("hello2")
|
||||||
|
|
||||||
@m()
|
@m()
|
||||||
def foo(db):
|
def foo(db):
|
||||||
db.table("foo").insert({"hello": "world"})
|
db["foo"].insert({"hello": "world"})
|
||||||
""",
|
""",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
)
|
)
|
||||||
|
|
@ -257,7 +256,7 @@ def foo(db):
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert db.table_names() == ["_sqlite_migrations"]
|
assert db.table_names() == ["_sqlite_migrations"]
|
||||||
assert list(db.table("_sqlite_migrations").rows) == []
|
assert list(db["_sqlite_migrations"].rows) == []
|
||||||
|
|
||||||
|
|
||||||
def test_stop_before_qualified_only_affects_named_set(two_sets_same_migration_name):
|
def test_stop_before_qualified_only_affects_named_set(two_sets_same_migration_name):
|
||||||
|
|
@ -275,10 +274,10 @@ def test_stop_before_qualified_only_affects_named_set(two_sets_same_migration_na
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert db.table("creatures").exists()
|
assert db["creatures"].exists()
|
||||||
assert not db.table("creature_weights").exists()
|
assert not db["creature_weights"].exists()
|
||||||
assert db.table("sales").exists()
|
assert db["sales"].exists()
|
||||||
assert db.table("sales_weights").exists()
|
assert db["sales_weights"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_stop_before_multiple_qualified(two_sets_same_migration_name):
|
def test_stop_before_multiple_qualified(two_sets_same_migration_name):
|
||||||
|
|
@ -298,10 +297,10 @@ def test_stop_before_multiple_qualified(two_sets_same_migration_name):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.output
|
assert result.exit_code == 0, result.output
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert db.table("creatures").exists()
|
assert db["creatures"].exists()
|
||||||
assert not db.table("creature_weights").exists()
|
assert not db["creature_weights"].exists()
|
||||||
assert db.table("sales").exists()
|
assert db["sales"].exists()
|
||||||
assert not db.table("sales_weights").exists()
|
assert not db["sales_weights"].exists()
|
||||||
|
|
||||||
|
|
||||||
LEGACY_MIGRATIONS = """
|
LEGACY_MIGRATIONS = """
|
||||||
|
|
@ -331,7 +330,7 @@ class LegacyMigrations:
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
def ensure_migrations_table(self, db):
|
def ensure_migrations_table(self, db):
|
||||||
db.table(self.migrations_table).create(
|
db[self.migrations_table].create(
|
||||||
{"migration_set": str, "name": str, "applied_at": str},
|
{"migration_set": str, "name": str, "applied_at": str},
|
||||||
pk=("migration_set", "name"),
|
pk=("migration_set", "name"),
|
||||||
if_not_exists=True,
|
if_not_exists=True,
|
||||||
|
|
@ -341,7 +340,7 @@ class LegacyMigrations:
|
||||||
self.ensure_migrations_table(db)
|
self.ensure_migrations_table(db)
|
||||||
return [
|
return [
|
||||||
_Applied(row["name"], row["applied_at"])
|
_Applied(row["name"], row["applied_at"])
|
||||||
for row in db.table(self.migrations_table).rows_where(
|
for row in db[self.migrations_table].rows_where(
|
||||||
"migration_set = ?", [self.name]
|
"migration_set = ?", [self.name]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
@ -355,7 +354,7 @@ class LegacyMigrations:
|
||||||
if migration.name == stop_before:
|
if migration.name == stop_before:
|
||||||
return
|
return
|
||||||
migration.fn(db)
|
migration.fn(db)
|
||||||
db.table(self.migrations_table).insert(
|
db[self.migrations_table].insert(
|
||||||
{
|
{
|
||||||
"migration_set": self.name,
|
"migration_set": self.name,
|
||||||
"name": migration.name,
|
"name": migration.name,
|
||||||
|
|
@ -369,11 +368,11 @@ legacy = LegacyMigrations("legacy_set")
|
||||||
|
|
||||||
@legacy
|
@legacy
|
||||||
def first(db):
|
def first(db):
|
||||||
db.table("first").insert({"hello": "world"})
|
db["first"].insert({"hello": "world"})
|
||||||
|
|
||||||
@legacy
|
@legacy
|
||||||
def second(db):
|
def second(db):
|
||||||
db.table("second").insert({"hello": "world"})
|
db["second"].insert({"hello": "world"})
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -446,11 +445,11 @@ def test_list_does_not_upgrade_legacy_migrations_table(two_migrations):
|
||||||
path, _ = two_migrations
|
path, _ = two_migrations
|
||||||
db_path = str(path / "test.db")
|
db_path = str(path / "test.db")
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
db.table("_sqlite_migrations").create(
|
db["_sqlite_migrations"].create(
|
||||||
{"migration_set": str, "name": str, "applied_at": str},
|
{"migration_set": str, "name": str, "applied_at": str},
|
||||||
pk=("migration_set", "name"),
|
pk=("migration_set", "name"),
|
||||||
)
|
)
|
||||||
db.table("_sqlite_migrations").insert(
|
db["_sqlite_migrations"].insert(
|
||||||
{"migration_set": "hello", "name": "foo", "applied_at": "x"}
|
{"migration_set": "hello", "name": "foo", "applied_at": "x"}
|
||||||
)
|
)
|
||||||
db.close()
|
db.close()
|
||||||
|
|
@ -462,7 +461,7 @@ def test_list_does_not_upgrade_legacy_migrations_table(two_migrations):
|
||||||
assert "foo - x" in result.output
|
assert "foo - x" in result.output
|
||||||
# --list must not perform the one-way legacy schema upgrade
|
# --list must not perform the one-way legacy schema upgrade
|
||||||
db2 = sqlite_utils.Database(db_path)
|
db2 = sqlite_utils.Database(db_path)
|
||||||
assert db2.table("_sqlite_migrations").pks == ["migration_set", "name"]
|
assert db2["_sqlite_migrations"].pks == ["migration_set", "name"]
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -485,7 +484,7 @@ def test_stop_before_applied_migration_errors(two_migrations):
|
||||||
assert result.exit_code != 0
|
assert result.exit_code != 0
|
||||||
assert "already been applied" in result.output
|
assert "already been applied" in result.output
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
assert not db.table("bar").exists()
|
assert not db["bar"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_list_with_legacy_class_is_read_only(tmpdir):
|
def test_list_with_legacy_class_is_read_only(tmpdir):
|
||||||
|
|
@ -496,7 +495,7 @@ def test_list_with_legacy_class_is_read_only(tmpdir):
|
||||||
(path / "migrations.py").write_text(LEGACY_MIGRATIONS, "utf-8")
|
(path / "migrations.py").write_text(LEGACY_MIGRATIONS, "utf-8")
|
||||||
db_path = str(path / "test.db")
|
db_path = str(path / "test.db")
|
||||||
db = sqlite_utils.Database(db_path)
|
db = sqlite_utils.Database(db_path)
|
||||||
db.table("existing").insert({"id": 1})
|
db["existing"].insert({"id": 1})
|
||||||
db.close()
|
db.close()
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
sqlite_utils.cli.cli, ["migrate", db_path, str(path), "--list"]
|
sqlite_utils.cli.cli, ["migrate", db_path, str(path), "--list"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils import ANY
|
|
||||||
from sqlite_utils.utils import column_affinity
|
from sqlite_utils.utils import column_affinity
|
||||||
|
|
||||||
EXAMPLES = [
|
EXAMPLES = [
|
||||||
|
|
@ -27,8 +25,6 @@ EXAMPLES = [
|
||||||
("DOUBLE", float),
|
("DOUBLE", float),
|
||||||
("DOUBLE PRECISION", float),
|
("DOUBLE PRECISION", float),
|
||||||
("FLOAT", float),
|
("FLOAT", float),
|
||||||
("ANY", ANY),
|
|
||||||
("any", ANY),
|
|
||||||
# Numeric, treated as float:
|
# Numeric, treated as float:
|
||||||
("NUMERIC", float),
|
("NUMERIC", float),
|
||||||
("DECIMAL(10,5)", float),
|
("DECIMAL(10,5)", float),
|
||||||
|
|
@ -45,5 +41,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(f"create table foo (col {column_def})")
|
fresh_db.execute("create table foo (col {})".format(column_def))
|
||||||
assert {"col": expected_type} == fresh_db.table("foo").columns_dict
|
assert {"col": expected_type} == fresh_db["foo"].columns_dict
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ from sqlite_utils.db import ForeignKey
|
||||||
|
|
||||||
|
|
||||||
def test_insert_populates_last_pk_case_insensitively(fresh_db):
|
def test_insert_populates_last_pk_case_insensitively(fresh_db):
|
||||||
books = fresh_db.table("books")
|
books = fresh_db["books"]
|
||||||
books.create({"Id": int, "Title": str}, pk="Id")
|
books.create({"Id": int, "Title": str}, pk="Id")
|
||||||
books.insert({"Id": 1, "Title": "One"}, pk="id")
|
books.insert({"Id": 1, "Title": "One"}, pk="id")
|
||||||
assert books.last_pk == 1
|
assert books.last_pk == 1
|
||||||
|
|
||||||
|
|
||||||
def test_insert_populates_last_pk_compound_pk_case_insensitively(fresh_db):
|
def test_insert_populates_last_pk_compound_pk_case_insensitively(fresh_db):
|
||||||
books = fresh_db.table("books")
|
books = fresh_db["books"]
|
||||||
books.create({"Author": str, "Position": int, "Title": str})
|
books.create({"Author": str, "Position": int, "Title": str})
|
||||||
books.insert(
|
books.insert(
|
||||||
{"Author": "Sue", "Position": 1, "Title": "One"}, pk=("author", "position")
|
{"Author": "Sue", "Position": 1, "Title": "One"}, pk=("author", "position")
|
||||||
|
|
@ -31,7 +31,7 @@ def test_insert_populates_last_pk_compound_pk_case_insensitively(fresh_db):
|
||||||
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
||||||
def test_upsert_pk_case_differs_from_schema(use_old_upsert):
|
def test_upsert_pk_case_differs_from_schema(use_old_upsert):
|
||||||
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
||||||
books = db.table("books")
|
books = db["books"]
|
||||||
books.create({"Id": int, "Title": str}, pk="Id")
|
books.create({"Id": int, "Title": str}, pk="Id")
|
||||||
books.insert({"Id": 1, "Title": "One"})
|
books.insert({"Id": 1, "Title": "One"})
|
||||||
books.upsert({"id": 1, "title": "Won"}, pk="id")
|
books.upsert({"id": 1, "title": "Won"}, pk="id")
|
||||||
|
|
@ -43,7 +43,7 @@ def test_upsert_pk_case_differs_from_schema(use_old_upsert):
|
||||||
def test_upsert_record_key_case_differs_from_pk(use_old_upsert):
|
def test_upsert_record_key_case_differs_from_pk(use_old_upsert):
|
||||||
# all_columns comes from the record keys, pk= from the caller
|
# all_columns comes from the record keys, pk= from the caller
|
||||||
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
||||||
books = db.table("books")
|
books = db["books"]
|
||||||
books.create({"Id": int, "Title": str}, pk="Id")
|
books.create({"Id": int, "Title": str}, pk="Id")
|
||||||
books.upsert({"ID": 1, "Title": "One"}, pk="id")
|
books.upsert({"ID": 1, "Title": "One"}, pk="id")
|
||||||
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
||||||
|
|
@ -52,7 +52,7 @@ def test_upsert_record_key_case_differs_from_pk(use_old_upsert):
|
||||||
|
|
||||||
def test_upsert_inferred_pk_case_differs_from_record_keys(fresh_db):
|
def test_upsert_inferred_pk_case_differs_from_record_keys(fresh_db):
|
||||||
# pk is inferred from the existing schema as "Id", records use "id"
|
# pk is inferred from the existing schema as "Id", records use "id"
|
||||||
books = fresh_db.table("books")
|
books = fresh_db["books"]
|
||||||
books.create({"Id": int, "Title": str}, pk="Id")
|
books.create({"Id": int, "Title": str}, pk="Id")
|
||||||
books.upsert({"id": 1, "title": "One"})
|
books.upsert({"id": 1, "title": "One"})
|
||||||
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
||||||
|
|
@ -60,7 +60,7 @@ def test_upsert_inferred_pk_case_differs_from_record_keys(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_list_mode_pk_case_insensitive(fresh_db):
|
def test_upsert_list_mode_pk_case_insensitive(fresh_db):
|
||||||
books = fresh_db.table("books")
|
books = fresh_db["books"]
|
||||||
books.create({"Id": int, "Title": str}, pk="Id")
|
books.create({"Id": int, "Title": str}, pk="Id")
|
||||||
books.upsert_all([["id", "title"], [1, "One"]], pk="Id")
|
books.upsert_all([["id", "title"], [1, "One"]], pk="Id")
|
||||||
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
assert list(books.rows) == [{"Id": 1, "Title": "One"}]
|
||||||
|
|
@ -68,84 +68,84 @@ def test_upsert_list_mode_pk_case_insensitive(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_pk_case_insensitive(fresh_db):
|
def test_lookup_pk_case_insensitive(fresh_db):
|
||||||
fresh_db.table("species").create({"ID": int, "Name": str}, pk="ID")
|
fresh_db["species"].create({"ID": int, "Name": str}, pk="ID")
|
||||||
fresh_db.table("species").insert({"ID": 5, "Name": "Palm"})
|
fresh_db["species"].insert({"ID": 5, "Name": "Palm"})
|
||||||
fresh_db.table("species").create_index(["Name"], unique=True)
|
fresh_db["species"].create_index(["Name"], unique=True)
|
||||||
assert fresh_db.table("species").lookup({"Name": "Palm"}, pk="id") == 5
|
assert fresh_db["species"].lookup({"Name": "Palm"}, pk="id") == 5
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_does_not_create_redundant_index(fresh_db):
|
def test_lookup_does_not_create_redundant_index(fresh_db):
|
||||||
fresh_db.table("species").create({"id": int, "Name": str}, pk="id")
|
fresh_db["species"].create({"id": int, "Name": str}, pk="id")
|
||||||
fresh_db.table("species").create_index(["Name"], unique=True)
|
fresh_db["species"].create_index(["Name"], unique=True)
|
||||||
fresh_db.table("species").lookup({"name": "Palm"})
|
fresh_db["species"].lookup({"name": "Palm"})
|
||||||
assert len(fresh_db.table("species").indexes) == 1
|
assert len(fresh_db["species"].indexes) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_transform_same_columns_different_case(fresh_db):
|
def test_create_table_transform_same_columns_different_case(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str, "Age": int})
|
fresh_db["t"].create({"Name": str, "Age": int})
|
||||||
fresh_db.table("t").insert({"Name": "Cleo", "Age": 5})
|
fresh_db["t"].insert({"Name": "Cleo", "Age": 5})
|
||||||
fresh_db.create_table("t", {"name": str, "age": int}, transform=True)
|
fresh_db.create_table("t", {"name": str, "age": int}, transform=True)
|
||||||
# Schema casing is preserved - SQLite considers these the same columns
|
# Schema casing is preserved - SQLite considers these the same columns
|
||||||
assert fresh_db.table("t").columns_dict == {"Name": str, "Age": int}
|
assert fresh_db["t"].columns_dict == {"Name": str, "Age": int}
|
||||||
assert list(fresh_db.table("t").rows) == [{"Name": "Cleo", "Age": 5}]
|
assert list(fresh_db["t"].rows) == [{"Name": "Cleo", "Age": 5}]
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_transform_case_insensitive_with_changes(fresh_db):
|
def test_create_table_transform_case_insensitive_with_changes(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str, "Age": int})
|
fresh_db["t"].create({"Name": str, "Age": int})
|
||||||
fresh_db.create_table("t", {"name": str, "age": str, "size": int}, transform=True)
|
fresh_db.create_table("t", {"name": str, "age": str, "size": int}, transform=True)
|
||||||
# age changed type, size added, Name untouched
|
# age changed type, size added, Name untouched
|
||||||
assert fresh_db.table("t").columns_dict == {"Name": str, "Age": str, "size": int}
|
assert fresh_db["t"].columns_dict == {"Name": str, "Age": str, "size": int}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_types_case_insensitive(fresh_db):
|
def test_transform_types_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str, "Age": str})
|
fresh_db["t"].create({"Name": str, "Age": str})
|
||||||
fresh_db.table("t").transform(types={"age": int})
|
fresh_db["t"].transform(types={"age": int})
|
||||||
assert fresh_db.table("t").columns_dict == {"Name": str, "Age": int}
|
assert fresh_db["t"].columns_dict == {"Name": str, "Age": int}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_rename_case_insensitive(fresh_db):
|
def test_transform_rename_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str})
|
fresh_db["t"].create({"Name": str})
|
||||||
fresh_db.table("t").transform(rename={"name": "title"})
|
fresh_db["t"].transform(rename={"name": "title"})
|
||||||
assert fresh_db.table("t").columns_dict == {"title": str}
|
assert fresh_db["t"].columns_dict == {"title": str}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_drop_case_insensitive(fresh_db):
|
def test_transform_drop_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str, "Age": int})
|
fresh_db["t"].create({"Name": str, "Age": int})
|
||||||
fresh_db.table("t").transform(drop=["name"])
|
fresh_db["t"].transform(drop=["name"])
|
||||||
assert fresh_db.table("t").columns_dict == {"Age": int}
|
assert fresh_db["t"].columns_dict == {"Age": int}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_not_null_and_defaults_case_insensitive(fresh_db):
|
def test_transform_not_null_and_defaults_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Name": str, "Age": int})
|
fresh_db["t"].create({"Name": str, "Age": int})
|
||||||
fresh_db.table("t").transform(not_null={"name"}, defaults={"age": 3})
|
fresh_db["t"].transform(not_null={"name"}, defaults={"age": 3})
|
||||||
columns = {c.name: c for c in fresh_db.table("t").columns}
|
columns = {c.name: c for c in fresh_db["t"].columns}
|
||||||
assert columns["Name"].notnull
|
assert columns["Name"].notnull
|
||||||
assert fresh_db.table("t").default_values == {"Age": 3}
|
assert fresh_db["t"].default_values == {"Age": 3}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_pk_case_insensitive(fresh_db):
|
def test_transform_pk_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Id": int, "Name": str})
|
fresh_db["t"].create({"Id": int, "Name": str})
|
||||||
fresh_db.table("t").transform(pk="id")
|
fresh_db["t"].transform(pk="id")
|
||||||
assert fresh_db.table("t").pks == ["Id"]
|
assert fresh_db["t"].pks == ["Id"]
|
||||||
assert fresh_db.table("t").columns_dict == {"Id": int, "Name": str}
|
assert fresh_db["t"].columns_dict == {"Id": int, "Name": str}
|
||||||
|
|
||||||
|
|
||||||
def test_transform_drop_foreign_keys_case_insensitive(fresh_db):
|
def test_transform_drop_foreign_keys_case_insensitive(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create(
|
fresh_db["child"].create(
|
||||||
{"id": int, "Parent_ID": int},
|
{"id": int, "Parent_ID": int},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[("Parent_ID", "parent", "Id")],
|
foreign_keys=[("Parent_ID", "parent", "Id")],
|
||||||
)
|
)
|
||||||
fresh_db.table("child").transform(drop_foreign_keys=["parent_id"])
|
fresh_db["child"].transform(drop_foreign_keys=["parent_id"])
|
||||||
assert fresh_db.table("child").foreign_keys == []
|
assert fresh_db["child"].foreign_keys == []
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_key_case_insensitive(fresh_db):
|
def test_add_foreign_key_case_insensitive(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create({"id": int, "Parent_ID": int}, pk="id")
|
fresh_db["child"].create({"id": int, "Parent_ID": int}, pk="id")
|
||||||
fresh_db.table("child").add_foreign_key("parent_id", "parent", "id")
|
fresh_db["child"].add_foreign_key("parent_id", "parent", "id")
|
||||||
fks = fresh_db.table("child").foreign_keys
|
fks = fresh_db["child"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
# The foreign key should use the schema casing of the columns
|
# The foreign key should use the schema casing of the columns
|
||||||
assert fks[0].column == "Parent_ID"
|
assert fks[0].column == "Parent_ID"
|
||||||
|
|
@ -153,83 +153,79 @@ def test_add_foreign_key_case_insensitive(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_keys_case_insensitive(fresh_db):
|
def test_add_foreign_keys_case_insensitive(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create({"id": int, "Parent_ID": int}, pk="id")
|
fresh_db["child"].create({"id": int, "Parent_ID": int}, pk="id")
|
||||||
fresh_db.add_foreign_keys([("child", "parent_id", "parent", "id")])
|
fresh_db.add_foreign_keys([("child", "parent_id", "parent", "id")])
|
||||||
fks = fresh_db.table("child").foreign_keys
|
fks = fresh_db["child"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
assert fks[0].column == "Parent_ID"
|
assert fks[0].column == "Parent_ID"
|
||||||
assert fks[0].other_column == "Id"
|
assert fks[0].other_column == "Id"
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_key_detects_existing_case_insensitively(fresh_db):
|
def test_add_foreign_key_detects_existing_case_insensitively(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create(
|
fresh_db["child"].create(
|
||||||
{"id": int, "Parent_ID": int},
|
{"id": int, "Parent_ID": int},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[("Parent_ID", "parent", "Id")],
|
foreign_keys=[("Parent_ID", "parent", "Id")],
|
||||||
)
|
)
|
||||||
# ignore=True should treat this as already existing, not add a duplicate
|
# ignore=True should treat this as already existing, not add a duplicate
|
||||||
fresh_db.table("child").add_foreign_key("parent_id", "parent", "id", ignore=True)
|
fresh_db["child"].add_foreign_key("parent_id", "parent", "id", ignore=True)
|
||||||
assert len(fresh_db.table("child").foreign_keys) == 1
|
assert len(fresh_db["child"].foreign_keys) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_add_column_fk_col_case_insensitive(fresh_db):
|
def test_add_column_fk_col_case_insensitive(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create({"id": int}, pk="id")
|
fresh_db["child"].create({"id": int}, pk="id")
|
||||||
fresh_db.table("child").add_column("parent_id", int, fk="parent", fk_col="id")
|
fresh_db["child"].add_column("parent_id", int, fk="parent", fk_col="id")
|
||||||
fks = fresh_db.table("child").foreign_keys
|
fks = fresh_db["child"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
assert fks[0].other_column == "Id"
|
assert fks[0].other_column == "Id"
|
||||||
|
|
||||||
|
|
||||||
def test_extract_case_insensitive(fresh_db):
|
def test_extract_case_insensitive(fresh_db):
|
||||||
fresh_db.table("trees").insert({"id": 1, "Species": "Palm"}, pk="id")
|
fresh_db["trees"].insert({"id": 1, "Species": "Palm"}, pk="id")
|
||||||
fresh_db.table("trees").extract("species")
|
fresh_db["trees"].extract("species")
|
||||||
assert fresh_db.table("trees").columns_dict == {"id": int, "Species_id": int}
|
assert fresh_db["trees"].columns_dict == {"id": int, "Species_id": int}
|
||||||
assert list(fresh_db.table("Species").rows) == [{"id": 1, "Species": "Palm"}]
|
assert list(fresh_db["Species"].rows) == [{"id": 1, "Species": "Palm"}]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi_case_insensitive(fresh_db):
|
def test_convert_multi_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").insert({"id": 1, "Name": "Cleo"}, pk="id")
|
fresh_db["t"].insert({"id": 1, "Name": "Cleo"}, pk="id")
|
||||||
fresh_db.table("t").convert("name", lambda v: {"upper": v.upper()}, multi=True)
|
fresh_db["t"].convert("name", lambda v: {"upper": v.upper()}, multi=True)
|
||||||
assert list(fresh_db.table("t").rows) == [
|
assert list(fresh_db["t"].rows) == [{"id": 1, "Name": "Cleo", "upper": "CLEO"}]
|
||||||
{"id": 1, "Name": "Cleo", "upper": "CLEO"}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_convert_output_case_insensitive(fresh_db):
|
def test_convert_output_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").insert({"id": 1, "Name": "Cleo", "Upper": None}, pk="id")
|
fresh_db["t"].insert({"id": 1, "Name": "Cleo", "Upper": None}, pk="id")
|
||||||
fresh_db.table("t").convert("name", lambda v: v.upper(), output="upper")
|
fresh_db["t"].convert("name", lambda v: v.upper(), output="upper")
|
||||||
assert list(fresh_db.table("t").rows) == [
|
assert list(fresh_db["t"].rows) == [{"id": 1, "Name": "Cleo", "Upper": "CLEO"}]
|
||||||
{"id": 1, "Name": "Cleo", "Upper": "CLEO"}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_sql_pk_case_insensitive(fresh_db):
|
def test_create_table_sql_pk_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create({"Id": int, "Name": str}, pk="id")
|
fresh_db["t"].create({"Id": int, "Name": str}, pk="id")
|
||||||
# Should not have created an extra lowercase "id" column
|
# Should not have created an extra lowercase "id" column
|
||||||
assert fresh_db.table("t").columns_dict == {"Id": int, "Name": str}
|
assert fresh_db["t"].columns_dict == {"Id": int, "Name": str}
|
||||||
assert fresh_db.table("t").pks == ["Id"]
|
assert fresh_db["t"].pks == ["Id"]
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_not_null_and_defaults_case_insensitive(fresh_db):
|
def test_create_table_not_null_and_defaults_case_insensitive(fresh_db):
|
||||||
fresh_db.table("t").create(
|
fresh_db["t"].create(
|
||||||
{"Name": str, "Age": int}, not_null={"name"}, defaults={"age": 1}
|
{"Name": str, "Age": int}, not_null={"name"}, defaults={"age": 1}
|
||||||
)
|
)
|
||||||
columns = {c.name: c for c in fresh_db.table("t").columns}
|
columns = {c.name: c for c in fresh_db["t"].columns}
|
||||||
assert columns["Name"].notnull
|
assert columns["Name"].notnull
|
||||||
assert fresh_db.table("t").default_values == {"Age": 1}
|
assert fresh_db["t"].default_values == {"Age": 1}
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_foreign_keys_case_insensitive(fresh_db):
|
def test_create_table_foreign_keys_case_insensitive(fresh_db):
|
||||||
fresh_db.table("parent").create({"Id": int}, pk="Id")
|
fresh_db["parent"].create({"Id": int}, pk="Id")
|
||||||
fresh_db.table("child").create(
|
fresh_db["child"].create(
|
||||||
{"id": int, "Parent_ID": int},
|
{"id": int, "Parent_ID": int},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[("parent_id", "parent", "id")],
|
foreign_keys=[("parent_id", "parent", "id")],
|
||||||
)
|
)
|
||||||
fks = fresh_db.table("child").foreign_keys
|
fks = fresh_db["child"].foreign_keys
|
||||||
assert fks == [
|
assert fks == [
|
||||||
ForeignKey(
|
ForeignKey(
|
||||||
table="child", column="Parent_ID", other_table="parent", other_column="Id"
|
table="child", column="Parent_ID", other_table="parent", other_column="Id"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
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():
|
||||||
|
|
@ -20,8 +18,8 @@ def test_recursive_triggers_off():
|
||||||
def test_memory_name():
|
def test_memory_name():
|
||||||
db1 = Database(memory_name="shared")
|
db1 = Database(memory_name="shared")
|
||||||
db2 = Database(memory_name="shared")
|
db2 = Database(memory_name="shared")
|
||||||
db1.table("dogs").insert({"name": "Cleo"})
|
db1["dogs"].insert({"name": "Cleo"})
|
||||||
assert list(db2.table("dogs").rows) == [{"name": "Cleo"}]
|
assert list(db2["dogs"].rows) == [{"name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_version():
|
def test_sqlite_version():
|
||||||
|
|
@ -36,7 +34,7 @@ def test_sqlite_version():
|
||||||
def test_database_context_manager(tmpdir):
|
def test_database_context_manager(tmpdir):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
with Database(path) as db:
|
with Database(path) as db:
|
||||||
db.table("t").insert({"id": 1})
|
db["t"].insert({"id": 1})
|
||||||
# Raw writes commit automatically too
|
# Raw writes commit automatically too
|
||||||
db.execute("insert into t (id) values (2)")
|
db.execute("insert into t (id) values (2)")
|
||||||
# An explicitly opened transaction left uncommitted on purpose:
|
# An explicitly opened transaction left uncommitted on purpose:
|
||||||
|
|
@ -47,7 +45,7 @@ def test_database_context_manager(tmpdir):
|
||||||
db.execute("select 1")
|
db.execute("select 1")
|
||||||
# ... and the open explicit transaction was rolled back, not committed
|
# ... and the open explicit transaction was rolled back, not committed
|
||||||
db2 = Database(path)
|
db2 = Database(path)
|
||||||
assert [r["id"] for r in db2.table("t").rows] == [1, 2]
|
assert [r["id"] for r in db2["t"].rows] == [1, 2]
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -83,12 +81,11 @@ def test_autocommit_connections_are_rejected(tmpdir, autocommit):
|
||||||
)
|
)
|
||||||
def test_legacy_transaction_control_connection_is_accepted(tmpdir):
|
def test_legacy_transaction_control_connection_is_accepted(tmpdir):
|
||||||
conn = sqlite3.connect(
|
conn = sqlite3.connect(
|
||||||
str(tmpdir / "test.db"),
|
str(tmpdir / "test.db"), autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL
|
||||||
autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL, # type: ignore[arg-type]
|
|
||||||
)
|
)
|
||||||
db = Database(conn)
|
db = Database(conn)
|
||||||
db.table("t").insert({"id": 1}, pk="id")
|
db["t"].insert({"id": 1}, pk="id")
|
||||||
assert [r["id"] for r in db.table("t").rows] == [1]
|
assert [r["id"] for r in db["t"].rows] == [1]
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
def test_insert_conversion(fresh_db):
|
def test_insert_conversion(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"foo": "bar"}, conversions={"foo": "upper(?)"})
|
table.insert({"foo": "bar"}, conversions={"foo": "upper(?)"})
|
||||||
assert [{"foo": "BAR"}] == list(table.rows)
|
assert [{"foo": "BAR"}] == list(table.rows)
|
||||||
|
|
||||||
|
|
||||||
def test_insert_all_conversion(fresh_db):
|
def test_insert_all_conversion(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert_all([{"foo": "bar"}], conversions={"foo": "upper(?)"})
|
table.insert_all([{"foo": "bar"}], conversions={"foo": "upper(?)"})
|
||||||
assert [{"foo": "BAR"}] == list(table.rows)
|
assert [{"foo": "BAR"}] == list(table.rows)
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_conversion(fresh_db):
|
def test_upsert_conversion(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert({"id": 1, "foo": "bar"}, pk="id", conversions={"foo": "upper(?)"})
|
table.upsert({"id": 1, "foo": "bar"}, pk="id", conversions={"foo": "upper(?)"})
|
||||||
assert [{"id": 1, "foo": "BAR"}] == list(table.rows)
|
assert [{"id": 1, "foo": "BAR"}] == list(table.rows)
|
||||||
table.upsert(
|
table.upsert(
|
||||||
|
|
@ -21,7 +21,7 @@ def test_upsert_conversion(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_all_conversion(fresh_db):
|
def test_upsert_all_conversion(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert_all(
|
table.upsert_all(
|
||||||
[{"id": 1, "foo": "bar"}], pk="id", conversions={"foo": "upper(?)"}
|
[{"id": 1, "foo": "bar"}], pk="id", conversions={"foo": "upper(?)"}
|
||||||
)
|
)
|
||||||
|
|
@ -29,7 +29,7 @@ def test_upsert_all_conversion(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_update_conversion(fresh_db):
|
def test_update_conversion(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"id": 5, "foo": "bar"}, pk="id")
|
table.insert({"id": 5, "foo": "bar"}, pk="id")
|
||||||
table.update(5, {"foo": "baz"}, conversions={"foo": "upper(?)"})
|
table.update(5, {"foo": "baz"}, conversions={"foo": "upper(?)"})
|
||||||
assert [{"id": 5, "foo": "BAZ"}] == list(table.rows)
|
assert [{"id": 5, "foo": "BAZ"}] == list(table.rows)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils.db import BadMultiValues
|
from sqlite_utils.db import BadMultiValues
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -27,7 +26,7 @@ from sqlite_utils.db import BadMultiValues
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_convert(fresh_db, columns, fn, expected):
|
def test_convert(fresh_db, columns, fn, expected):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"title": "Mixed Case", "abstract": "Abstract"})
|
table.insert({"title": "Mixed Case", "abstract": "Abstract"})
|
||||||
table.convert(columns, fn)
|
table.convert(columns, fn)
|
||||||
assert list(table.rows) == [expected]
|
assert list(table.rows) == [expected]
|
||||||
|
|
@ -37,7 +36,7 @@ def test_convert(fresh_db, columns, fn, expected):
|
||||||
"where,where_args", (("id > 1", None), ("id > :id", {"id": 1}), ("id > ?", [1]))
|
"where,where_args", (("id > 1", None), ("id > :id", {"id": 1}), ("id > ?", [1]))
|
||||||
)
|
)
|
||||||
def test_convert_where(fresh_db, where, where_args):
|
def test_convert_where(fresh_db, where, where_args):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert_all(
|
table.insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "title": "One"},
|
{"id": 1, "title": "One"},
|
||||||
|
|
@ -53,7 +52,7 @@ def test_convert_where(fresh_db, where, where_args):
|
||||||
|
|
||||||
def test_convert_handles_falsey_values(fresh_db):
|
def test_convert_handles_falsey_values(fresh_db):
|
||||||
# Falsey values like 0 should be converted (issue #527)
|
# Falsey values like 0 should be converted (issue #527)
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert_all([{"x": 0}, {"x": 1}])
|
table.insert_all([{"x": 0}, {"x": 1}])
|
||||||
assert table.get(1)["x"] == 0
|
assert table.get(1)["x"] == 0
|
||||||
assert table.get(2)["x"] == 1
|
assert table.get(2)["x"] == 1
|
||||||
|
|
@ -70,14 +69,14 @@ def test_convert_handles_falsey_values(fresh_db):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_convert_output(fresh_db, drop, expected):
|
def test_convert_output(fresh_db, drop, expected):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"title": "Mixed Case"})
|
table.insert({"title": "Mixed Case"})
|
||||||
table.convert("title", lambda v: v.upper(), output="other", drop=drop)
|
table.convert("title", lambda v: v.upper(), output="other", drop=drop)
|
||||||
assert list(table.rows) == [expected]
|
assert list(table.rows) == [expected]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_output_multiple_column_error(fresh_db):
|
def test_convert_output_multiple_column_error(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError) as excinfo:
|
||||||
table.convert(["title", "other"], lambda v: v, output="out")
|
table.convert(["title", "other"], lambda v: v, output="out")
|
||||||
assert "output= can only be used with a single column" in str(excinfo.value)
|
assert "output= can only be used with a single column" in str(excinfo.value)
|
||||||
|
|
@ -91,14 +90,14 @@ def test_convert_output_multiple_column_error(fresh_db):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_convert_output_type(fresh_db, type, expected):
|
def test_convert_output_type(fresh_db, type, expected):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"number": "123"})
|
table.insert({"number": "123"})
|
||||||
table.convert("number", lambda v: v, output="other", output_type=type, drop=True)
|
table.convert("number", lambda v: v, output="other", output_type=type, drop=True)
|
||||||
assert list(table.rows) == [expected]
|
assert list(table.rows) == [expected]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi(fresh_db):
|
def test_convert_multi(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"title": "Mixed Case"})
|
table.insert({"title": "Mixed Case"})
|
||||||
table.convert(
|
table.convert(
|
||||||
"title",
|
"title",
|
||||||
|
|
@ -123,7 +122,7 @@ def test_convert_multi(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi_where(fresh_db):
|
def test_convert_multi_where(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert_all(
|
table.insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "title": "One"},
|
{"id": 1, "title": "One"},
|
||||||
|
|
@ -145,14 +144,14 @@ def test_convert_multi_where(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_convert_multi_exception(fresh_db):
|
def test_convert_multi_exception(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"title": "Mixed Case"})
|
table.insert({"title": "Mixed Case"})
|
||||||
with pytest.raises(BadMultiValues):
|
with pytest.raises(BadMultiValues):
|
||||||
table.convert("title", lambda v: v.upper(), multi=True)
|
table.convert("title", lambda v: v.upper(), multi=True)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_repeated(fresh_db):
|
def test_convert_repeated(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
col = "num"
|
col = "num"
|
||||||
table.insert({col: 1})
|
table.insert({col: 1})
|
||||||
table.convert(col, lambda x: x * 2)
|
table.convert(col, lambda x: x * 2)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,243 +0,0 @@
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
import hypothesis.strategies as st
|
|
||||||
import pytest
|
|
||||||
from hypothesis import given
|
|
||||||
|
|
||||||
from sqlite_utils.create_table_parser import (
|
|
||||||
Check,
|
|
||||||
ColumnComments,
|
|
||||||
ParseError,
|
|
||||||
Unique,
|
|
||||||
UniqueColumn,
|
|
||||||
parse_autoincrement,
|
|
||||||
parse_checks,
|
|
||||||
parse_column_comments,
|
|
||||||
parse_uniques,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_column_and_table_checks():
|
|
||||||
sql = """
|
|
||||||
CREATE TABLE people (
|
|
||||||
age INTEGER CONSTRAINT positive CHECK (age > 0),
|
|
||||||
status TEXT CHECK(status IN ('active', 'inactive')),
|
|
||||||
CONSTRAINT adult CHECK(age >= 18)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
assert parse_checks(sql) == [
|
|
||||||
Check("age > 0", name="positive", column="age"),
|
|
||||||
Check(
|
|
||||||
"status IN ('active', 'inactive')",
|
|
||||||
column="status",
|
|
||||||
options=["active", "inactive"],
|
|
||||||
),
|
|
||||||
Check("age >= 18", name="adult"),
|
|
||||||
]
|
|
||||||
checks = parse_checks(sql)
|
|
||||||
assert checks[0].sql == "CONSTRAINT positive CHECK (age > 0)"
|
|
||||||
assert sql[checks[0].start : checks[0].end] == checks[0].sql
|
|
||||||
assert checks[1].sql == "CHECK(status IN ('active', 'inactive'))"
|
|
||||||
assert sql[checks[2].start : checks[2].end] == checks[2].sql
|
|
||||||
|
|
||||||
|
|
||||||
def test_comments_are_trivia_not_constraints():
|
|
||||||
sql = """
|
|
||||||
CREATE /* fake CHECK (nope), ( */ TABLE t (
|
|
||||||
a INTEGER /* CHECK (a < 0), phantom */,
|
|
||||||
b INTEGER CHECK /* between keyword and expression */ (b > 0),
|
|
||||||
/* CHECK (also_fake) */ CONSTRAINT upper CHECK(b < 10)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_checks(sql) == [
|
|
||||||
Check("b > 0", column="b"),
|
|
||||||
Check("b < 10", name="upper"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_comments_owned_by_columns():
|
|
||||||
sql = """
|
|
||||||
CREATE TABLE t (
|
|
||||||
-- Before id
|
|
||||||
id /* Between name and type */ INTEGER /* After id */,
|
|
||||||
/* Between column definitions */
|
|
||||||
value TEXT CHECK(value != '') /* After value */,
|
|
||||||
/* Before a table constraint, not a column */
|
|
||||||
CHECK(value != 'forbidden')
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
assert parse_column_comments(sql) == {
|
|
||||||
"id": ColumnComments(before="-- Before id", after="/* After id */"),
|
|
||||||
"value": ColumnComments(
|
|
||||||
before="/* Between column definitions */",
|
|
||||||
after="/* After value */",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"expression,expected",
|
|
||||||
[
|
|
||||||
("value IN ('one', 'two')", ["one", "two"]),
|
|
||||||
("((value IN ('one', 'two')))", ["one", "two"]),
|
|
||||||
("value NOT IN ('one', 'two')", None),
|
|
||||||
("value IN ('one', 'two') OR enabled", None),
|
|
||||||
("other IN ('one', 'two')", None),
|
|
||||||
("value IN (lower('one'), 'two')", None),
|
|
||||||
('value IN ("other")', None),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_options_only_for_exact_literal_in_check(expression, expected):
|
|
||||||
sql = f"CREATE TABLE t(value TEXT CHECK({expression}), enabled INTEGER, other TEXT)"
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_checks(sql)[0].options == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("column", ["💩x", "e\u0301"])
|
|
||||||
def test_unquoted_unicode_identifiers(column):
|
|
||||||
sql = f"CREATE TABLE t({column} INTEGER CHECK({column} > 0))"
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_checks(sql) == [Check(f"{column} > 0", column=column)]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"sql",
|
|
||||||
[
|
|
||||||
"SELECT CHECK(x > 0)",
|
|
||||||
"CREATE TABLE t(x INTEGER CHECK(x > 0)",
|
|
||||||
"CREATE TABLE t(x TEXT CHECK(x != 'unterminated))",
|
|
||||||
"CREATE TABLE t(x INTEGER /* unterminated)",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_invalid_sql_raises_parse_error(sql):
|
|
||||||
with pytest.raises(ParseError):
|
|
||||||
parse_checks(sql)
|
|
||||||
|
|
||||||
|
|
||||||
def test_virtual_table_has_no_checks():
|
|
||||||
assert (
|
|
||||||
parse_checks("CREATE /* comment */ VIRTUAL TABLE search USING fts5(text)") == []
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"sql,expected",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)",
|
|
||||||
"id",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'CREATE TABLE t("quoted id" INTEGER PRIMARY KEY AUTOINCREMENT)',
|
|
||||||
"quoted id",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'CREATE TABLE t("autoincrement" INTEGER PRIMARY KEY, value TEXT)',
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"CREATE TABLE t(id INTEGER PRIMARY KEY /* AUTOINCREMENT */, value TEXT)",
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"CREATE TABLE t(id INTEGER PRIMARY KEY, value TEXT CHECK(value != 'AUTOINCREMENT'))",
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_parse_autoincrement(sql, expected):
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_autoincrement(sql) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_column_and_table_uniques():
|
|
||||||
sql = """
|
|
||||||
CREATE TABLE memberships (
|
|
||||||
email TEXT COLLATE RTRIM CONSTRAINT unique_email UNIQUE ON CONFLICT IGNORE,
|
|
||||||
account_id INTEGER,
|
|
||||||
CONSTRAINT unique_membership UNIQUE (
|
|
||||||
account_id DESC,
|
|
||||||
email COLLATE NOCASE ASC
|
|
||||||
) ON CONFLICT REPLACE
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_uniques(sql) == [
|
|
||||||
Unique(
|
|
||||||
(UniqueColumn("email", collation="RTRIM"),),
|
|
||||||
name="unique_email",
|
|
||||||
column="email",
|
|
||||||
conflict="IGNORE",
|
|
||||||
),
|
|
||||||
Unique(
|
|
||||||
(
|
|
||||||
UniqueColumn("account_id", order="DESC"),
|
|
||||||
UniqueColumn("email", collation="NOCASE", order="ASC"),
|
|
||||||
),
|
|
||||||
name="unique_membership",
|
|
||||||
conflict="REPLACE",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
uniques = parse_uniques(sql)
|
|
||||||
assert uniques[0].sql == "CONSTRAINT unique_email UNIQUE ON CONFLICT IGNORE"
|
|
||||||
assert sql[uniques[1].start : uniques[1].end] == uniques[1].sql
|
|
||||||
|
|
||||||
|
|
||||||
def test_unique_like_text_in_comments_and_checks_is_ignored():
|
|
||||||
sql = """
|
|
||||||
CREATE TABLE t (
|
|
||||||
value TEXT /* UNIQUE ON CONFLICT REPLACE */
|
|
||||||
CHECK(value != 'UNIQUE(other)'),
|
|
||||||
other TEXT
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
sqlite3.connect(":memory:").execute(sql)
|
|
||||||
assert parse_uniques(sql) == []
|
|
||||||
|
|
||||||
|
|
||||||
comment_or_space = st.sampled_from(
|
|
||||||
[
|
|
||||||
" ",
|
|
||||||
"\n ",
|
|
||||||
"/* comment with , ( ) and CHECK(fake) */",
|
|
||||||
"-- comment with , ( ) and CHECK(fake)\n",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given(gaps=st.lists(comment_or_space, min_size=5, max_size=5))
|
|
||||||
def test_comments_and_whitespace_can_separate_check_tokens(gaps):
|
|
||||||
sql = (
|
|
||||||
f"CREATE{gaps[0]}TABLE{gaps[1]}t{gaps[2]}("
|
|
||||||
f"value INTEGER CHECK{gaps[3]}(value{gaps[4]}> 0))"
|
|
||||||
)
|
|
||||||
connection = sqlite3.connect(":memory:")
|
|
||||||
connection.execute(sql)
|
|
||||||
stored_sql = connection.execute(
|
|
||||||
"select sql from sqlite_master where name = 't'"
|
|
||||||
).fetchone()[0]
|
|
||||||
assert parse_checks(stored_sql) == [Check(f"value{gaps[4]}> 0", column="value")]
|
|
||||||
|
|
||||||
|
|
||||||
safe_string_text = st.text(
|
|
||||||
alphabet=st.characters(
|
|
||||||
blacklist_categories=("Cc", "Cs"),
|
|
||||||
blacklist_characters=("'",),
|
|
||||||
),
|
|
||||||
max_size=40,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given(value=safe_string_text)
|
|
||||||
def test_check_like_text_inside_strings_is_opaque(value):
|
|
||||||
sql = f"CREATE TABLE t(value TEXT CHECK(value != '{value}'))"
|
|
||||||
connection = sqlite3.connect(":memory:")
|
|
||||||
connection.execute(sql)
|
|
||||||
stored_sql = connection.execute(
|
|
||||||
"select sql from sqlite_master where name = 't'"
|
|
||||||
).fetchone()[0]
|
|
||||||
checks = parse_checks(stored_sql)
|
|
||||||
assert len(checks) == 1
|
|
||||||
assert checks[0].column == "value"
|
|
||||||
assert checks[0].check == f"value != '{value}'"
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.utils import OperationalError
|
from sqlite_utils.utils import OperationalError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ 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(f"create table foo (col {column_def})")
|
fresh_db.execute("create table foo (col {})".format(column_def))
|
||||||
assert initial_value == fresh_db.table("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.table("foo").columns[0].default_value
|
fresh_db["foo"].columns[0].default_value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ def test_insert_empty_record_uses_default_values(fresh_db):
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
table = fresh_db.table("has_defaults")
|
table = fresh_db["has_defaults"]
|
||||||
table.insert({})
|
table.insert({})
|
||||||
|
|
||||||
rows = list(table.rows)
|
rows = list(table.rows)
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@ import sqlite_utils
|
||||||
|
|
||||||
|
|
||||||
def test_delete_rowid_table(fresh_db):
|
def test_delete_rowid_table(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"foo": 1})
|
table.insert({"foo": 1}).last_pk
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_pk_table(fresh_db):
|
def test_delete_pk_table(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"id": 1}, pk="id")
|
table.insert({"id": 1}, pk="id")
|
||||||
table.insert({"id": 2}, pk="id")
|
table.insert({"id": 2}, pk="id")
|
||||||
table.delete(1)
|
table.delete(1)
|
||||||
|
|
@ -18,7 +18,7 @@ def test_delete_pk_table(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_delete_where(fresh_db):
|
def test_delete_where(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
for i in range(1, 11):
|
for i in range(1, 11):
|
||||||
table.insert({"id": i}, pk="id")
|
table.insert({"id": i}, pk="id")
|
||||||
assert table.count == 10
|
assert table.count == 10
|
||||||
|
|
@ -27,7 +27,7 @@ def test_delete_where(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_delete_where_all(fresh_db):
|
def test_delete_where_all(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
for i in range(1, 11):
|
for i in range(1, 11):
|
||||||
table.insert({"id": i}, pk="id")
|
table.insert({"id": i}, pk="id")
|
||||||
assert table.count == 10
|
assert table.count == 10
|
||||||
|
|
@ -38,27 +38,27 @@ def test_delete_where_all(fresh_db):
|
||||||
def test_delete_where_commits(tmpdir):
|
def test_delete_where_commits(tmpdir):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
db.table("table").insert_all([{"id": i} for i in range(5)], pk="id")
|
db["table"].insert_all([{"id": i} for i in range(5)], pk="id")
|
||||||
db.table("table").delete_where("id > ?", [2])
|
db["table"].delete_where("id > ?", [2])
|
||||||
# The connection must not be left inside an open transaction,
|
# The connection must not be left inside an open transaction,
|
||||||
# otherwise subsequent atomic() blocks never commit either
|
# otherwise subsequent atomic() blocks never commit either
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
db.table("table").insert({"id": 100})
|
db["table"].insert({"id": 100})
|
||||||
db.close()
|
db.close()
|
||||||
db2 = sqlite_utils.Database(path)
|
db2 = sqlite_utils.Database(path)
|
||||||
assert [r["id"] for r in db2.table("table").rows] == [0, 1, 2, 100]
|
assert [r["id"] for r in db2["table"].rows] == [0, 1, 2, 100]
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
def test_delete_where_analyze(fresh_db):
|
def test_delete_where_analyze(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert_all(({"id": i, "i": i} for i in range(10)), pk="id")
|
table.insert_all(({"id": i, "i": i} for i in range(10)), pk="id")
|
||||||
table.create_index(["i"], analyze=True)
|
table.create_index(["i"], analyze=True)
|
||||||
assert "sqlite_stat1" in fresh_db.table_names()
|
assert "sqlite_stat1" in fresh_db.table_names()
|
||||||
assert list(fresh_db.table("sqlite_stat1").rows) == [
|
assert list(fresh_db["sqlite_stat1"].rows) == [
|
||||||
{"tbl": "table", "idx": "idx_table_i", "stat": "10 1"}
|
{"tbl": "table", "idx": "idx_table_i", "stat": "10 1"}
|
||||||
]
|
]
|
||||||
table.delete_where("id > ?", [5], analyze=True)
|
table.delete_where("id > ?", [5], analyze=True)
|
||||||
assert list(fresh_db.table("sqlite_stat1").rows) == [
|
assert list(fresh_db["sqlite_stat1"].rows) == [
|
||||||
{"tbl": "table", "idx": "idx_table_i", "stat": "6 1"}
|
{"tbl": "table", "idx": "idx_table_i", "stat": "6 1"}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from sqlite_utils import cli, recipes
|
from sqlite_utils import cli, recipes
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
import re
|
||||||
|
|
||||||
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+)")
|
||||||
|
|
@ -36,7 +34,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, f"{command} is missing its help"
|
assert command.help, "{} is missing its help".format(command)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_help():
|
def test_convert_help():
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils.db import NoTable
|
from sqlite_utils.db import NoTable
|
||||||
|
import datetime
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate(fresh_db):
|
def test_duplicate(fresh_db):
|
||||||
|
|
@ -14,7 +12,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(datetime.timezone.utc)
|
dt = datetime.datetime.now()
|
||||||
data = {
|
data = {
|
||||||
"text_col": "Cleo",
|
"text_col": "Cleo",
|
||||||
"real_col": 3.14,
|
"real_col": 3.14,
|
||||||
|
|
@ -22,7 +20,7 @@ def test_duplicate(fresh_db):
|
||||||
"bool_col": True,
|
"bool_col": True,
|
||||||
"datetime_col": str(dt),
|
"datetime_col": str(dt),
|
||||||
}
|
}
|
||||||
table1 = fresh_db.table("table1")
|
table1 = fresh_db["table1"]
|
||||||
row_id = table1.insert(data).last_rowid
|
row_id = table1.insert(data).last_rowid
|
||||||
# Duplicate table:
|
# Duplicate table:
|
||||||
table2 = table1.duplicate("table2")
|
table2 = table1.duplicate("table2")
|
||||||
|
|
@ -40,4 +38,4 @@ def test_duplicate(fresh_db):
|
||||||
|
|
||||||
def test_duplicate_fails_if_table_does_not_exist(fresh_db):
|
def test_duplicate_fails_if_table_does_not_exist(fresh_db):
|
||||||
with pytest.raises(NoTable):
|
with pytest.raises(NoTable):
|
||||||
fresh_db.table("not_a_table").duplicate("duplicated")
|
fresh_db["not_a_table"].duplicate("duplicated")
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import pytest
|
from sqlite_utils import Database
|
||||||
|
from sqlite_utils import cli
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
import pytest
|
||||||
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.table("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": f"item {i}"})
|
foo.insert({"name": "item {}".format(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
|
||||||
|
|
@ -41,24 +41,24 @@ def test_enable_counts_specific_table(fresh_db):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
assert fresh_db.table_names() == ["foo", "_counts"]
|
assert fresh_db.table_names() == ["foo", "_counts"]
|
||||||
assert list(fresh_db.table("_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": f"item {10 + i}"})
|
foo.insert({"name": "item {}".format(10 + i)})
|
||||||
assert foo.count == 15
|
assert foo.count == 15
|
||||||
assert list(fresh_db.table("_counts").rows) == [{"count": 15, "table": "foo"}]
|
assert list(fresh_db["_counts"].rows) == [{"count": 15, "table": "foo"}]
|
||||||
# Delete some items
|
# Delete some items
|
||||||
foo.delete_where("rowid < 7")
|
foo.delete_where("rowid < 7")
|
||||||
assert foo.count == 9
|
assert foo.count == 9
|
||||||
assert list(fresh_db.table("_counts").rows) == [{"count": 9, "table": "foo"}]
|
assert list(fresh_db["_counts"].rows) == [{"count": 9, "table": "foo"}]
|
||||||
foo.delete_where()
|
foo.delete_where()
|
||||||
assert foo.count == 0
|
assert foo.count == 0
|
||||||
assert list(fresh_db.table("_counts").rows) == [{"count": 0, "table": "foo"}]
|
assert list(fresh_db["_counts"].rows) == [{"count": 0, "table": "foo"}]
|
||||||
|
|
||||||
|
|
||||||
def test_enable_counts_all_tables(fresh_db):
|
def test_enable_counts_all_tables(fresh_db):
|
||||||
foo = fresh_db.table("foo")
|
foo = fresh_db["foo"]
|
||||||
bar = fresh_db.table("bar")
|
bar = fresh_db["bar"]
|
||||||
foo.insert({"name": "Cleo"})
|
foo.insert({"name": "Cleo"})
|
||||||
bar.insert({"name": "Cleo"})
|
bar.insert({"name": "Cleo"})
|
||||||
foo.enable_fts(["name"])
|
foo.enable_fts(["name"])
|
||||||
|
|
@ -73,7 +73,7 @@ def test_enable_counts_all_tables(fresh_db):
|
||||||
"foo_fts_config",
|
"foo_fts_config",
|
||||||
"_counts",
|
"_counts",
|
||||||
}
|
}
|
||||||
assert list(fresh_db.table("_counts").rows) == [
|
assert list(fresh_db["_counts"].rows) == [
|
||||||
{"count": 1, "table": "foo"},
|
{"count": 1, "table": "foo"},
|
||||||
{"count": 1, "table": "bar"},
|
{"count": 1, "table": "bar"},
|
||||||
{"count": 3, "table": "foo_fts_data"},
|
{"count": 3, "table": "foo_fts_data"},
|
||||||
|
|
@ -87,10 +87,10 @@ def test_enable_counts_all_tables(fresh_db):
|
||||||
def counts_db_path(tmpdir):
|
def counts_db_path(tmpdir):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("foo").insert({"name": "bar"})
|
db["foo"].insert({"name": "bar"})
|
||||||
db.table("bar").insert({"name": "bar"})
|
db["bar"].insert({"name": "bar"})
|
||||||
db.table("bar").insert({"name": "bar"})
|
db["bar"].insert({"name": "bar"})
|
||||||
db.table("baz").insert({"name": "bar"})
|
db["baz"].insert({"name": "bar"})
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -163,25 +163,25 @@ def test_uses_counts_after_enable_counts(counts_db_path):
|
||||||
|
|
||||||
def test_reset_counts(counts_db_path):
|
def test_reset_counts(counts_db_path):
|
||||||
db = Database(counts_db_path)
|
db = Database(counts_db_path)
|
||||||
db.table("foo").enable_counts()
|
db["foo"].enable_counts()
|
||||||
db.table("bar").enable_counts()
|
db["bar"].enable_counts()
|
||||||
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
||||||
# Corrupt the value
|
# Corrupt the value
|
||||||
db.table("_counts").update("foo", {"count": 3})
|
db["_counts"].update("foo", {"count": 3})
|
||||||
assert db.cached_counts() == {"foo": 3, "bar": 2}
|
assert db.cached_counts() == {"foo": 3, "bar": 2}
|
||||||
assert db.table("foo").count == 3
|
assert db["foo"].count == 3
|
||||||
# Reset them
|
# Reset them
|
||||||
db.reset_counts()
|
db.reset_counts()
|
||||||
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
||||||
assert db.table("foo").count == 1
|
assert db["foo"].count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_reset_counts_cli(counts_db_path):
|
def test_reset_counts_cli(counts_db_path):
|
||||||
db = Database(counts_db_path)
|
db = Database(counts_db_path)
|
||||||
db.table("foo").enable_counts()
|
db["foo"].enable_counts()
|
||||||
db.table("bar").enable_counts()
|
db["bar"].enable_counts()
|
||||||
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
||||||
db.table("_counts").update("foo", {"count": 3})
|
db["_counts"].update("foo", {"count": 3})
|
||||||
result = CliRunner().invoke(cli.cli, ["reset-counts", counts_db_path])
|
result = CliRunner().invoke(cli.cli, ["reset-counts", counts_db_path])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
assert db.cached_counts() == {"foo": 1, "bar": 2}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import itertools
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils import ANY
|
|
||||||
from sqlite_utils.db import InvalidColumns
|
from sqlite_utils.db import InvalidColumns
|
||||||
|
import itertools
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@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 f"{expected_table}_id"
|
expected_fk = fk_column or "{}_id".format(expected_table)
|
||||||
iter_species = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
iter_species = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
||||||
fresh_db.table("tree").insert_all(
|
fresh_db["tree"].insert_all(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"id": i,
|
"id": i,
|
||||||
"name": f"Tree {i}",
|
"name": "Tree {}".format(i),
|
||||||
"species": next(iter_species),
|
"species": next(iter_species),
|
||||||
"end": 1,
|
"end": 1,
|
||||||
}
|
}
|
||||||
|
|
@ -24,27 +21,28 @@ def test_extract_single_column(fresh_db, table, fk_column):
|
||||||
),
|
),
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("tree").extract("species", table=table, fk_column=fk_column)
|
fresh_db["tree"].extract("species", table=table, fk_column=fk_column)
|
||||||
assert fresh_db.table("tree").schema == (
|
assert fresh_db["tree"].schema == (
|
||||||
'CREATE TABLE "tree" (\n'
|
'CREATE TABLE "tree" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
f' "{expected_fk}" INTEGER REFERENCES "{expected_table}"("id"),\n'
|
' "{}" INTEGER REFERENCES "{}"("id"),\n'.format(expected_fk, expected_table)
|
||||||
+ ' "end" INTEGER\n'
|
+ ' "end" INTEGER\n'
|
||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
assert fresh_db.table(expected_table).schema == (
|
assert fresh_db[expected_table].schema == (
|
||||||
f'CREATE TABLE "{expected_table}" (\n' + ' "id" INTEGER PRIMARY KEY,\n'
|
'CREATE TABLE "{}" (\n'.format(expected_table)
|
||||||
|
+ ' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "species" TEXT\n'
|
' "species" TEXT\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table(expected_table).rows) == [
|
assert list(fresh_db[expected_table].rows) == [
|
||||||
{"id": 1, "species": "Palm"},
|
{"id": 1, "species": "Palm"},
|
||||||
{"id": 2, "species": "Spruce"},
|
{"id": 2, "species": "Spruce"},
|
||||||
{"id": 3, "species": "Mangrove"},
|
{"id": 3, "species": "Mangrove"},
|
||||||
{"id": 4, "species": "Oak"},
|
{"id": 4, "species": "Oak"},
|
||||||
]
|
]
|
||||||
assert list(itertools.islice(fresh_db.table("tree").rows, 0, 4)) == [
|
assert list(itertools.islice(fresh_db["tree"].rows, 0, 4)) == [
|
||||||
{"id": 1, "name": "Tree 1", expected_fk: 1, "end": 1},
|
{"id": 1, "name": "Tree 1", expected_fk: 1, "end": 1},
|
||||||
{"id": 2, "name": "Tree 2", expected_fk: 2, "end": 1},
|
{"id": 2, "name": "Tree 2", expected_fk: 2, "end": 1},
|
||||||
{"id": 3, "name": "Tree 3", expected_fk: 3, "end": 1},
|
{"id": 3, "name": "Tree 3", expected_fk: 3, "end": 1},
|
||||||
|
|
@ -55,11 +53,11 @@ def test_extract_single_column(fresh_db, table, fk_column):
|
||||||
def test_extract_multiple_columns_with_rename(fresh_db):
|
def test_extract_multiple_columns_with_rename(fresh_db):
|
||||||
iter_common = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
iter_common = itertools.cycle(["Palm", "Spruce", "Mangrove", "Oak"])
|
||||||
iter_latin = itertools.cycle(["Arecaceae", "Picea", "Rhizophora", "Quercus"])
|
iter_latin = itertools.cycle(["Arecaceae", "Picea", "Rhizophora", "Quercus"])
|
||||||
fresh_db.table("tree").insert_all(
|
fresh_db["tree"].insert_all(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"id": i,
|
"id": i,
|
||||||
"name": f"Tree {i}",
|
"name": "Tree {}".format(i),
|
||||||
"common_name": next(iter_common),
|
"common_name": next(iter_common),
|
||||||
"latin_name": next(iter_latin),
|
"latin_name": next(iter_latin),
|
||||||
}
|
}
|
||||||
|
|
@ -68,30 +66,30 @@ def test_extract_multiple_columns_with_rename(fresh_db):
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
|
|
||||||
fresh_db.table("tree").extract(
|
fresh_db["tree"].extract(
|
||||||
["common_name", "latin_name"], rename={"common_name": "name"}
|
["common_name", "latin_name"], rename={"common_name": "name"}
|
||||||
)
|
)
|
||||||
assert fresh_db.table("tree").schema == (
|
assert fresh_db["tree"].schema == (
|
||||||
'CREATE TABLE "tree" (\n'
|
'CREATE TABLE "tree" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
|
' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
assert fresh_db.table("common_name_latin_name").schema == (
|
assert fresh_db["common_name_latin_name"].schema == (
|
||||||
'CREATE TABLE "common_name_latin_name" (\n'
|
'CREATE TABLE "common_name_latin_name" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
' "latin_name" TEXT\n'
|
' "latin_name" TEXT\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("common_name_latin_name").rows) == [
|
assert list(fresh_db["common_name_latin_name"].rows) == [
|
||||||
{"name": "Palm", "id": 1, "latin_name": "Arecaceae"},
|
{"name": "Palm", "id": 1, "latin_name": "Arecaceae"},
|
||||||
{"name": "Spruce", "id": 2, "latin_name": "Picea"},
|
{"name": "Spruce", "id": 2, "latin_name": "Picea"},
|
||||||
{"name": "Mangrove", "id": 3, "latin_name": "Rhizophora"},
|
{"name": "Mangrove", "id": 3, "latin_name": "Rhizophora"},
|
||||||
{"name": "Oak", "id": 4, "latin_name": "Quercus"},
|
{"name": "Oak", "id": 4, "latin_name": "Quercus"},
|
||||||
]
|
]
|
||||||
assert list(itertools.islice(fresh_db.table("tree").rows, 0, 4)) == [
|
assert list(itertools.islice(fresh_db["tree"].rows, 0, 4)) == [
|
||||||
{"id": 1, "name": "Tree 1", "common_name_latin_name_id": 1},
|
{"id": 1, "name": "Tree 1", "common_name_latin_name_id": 1},
|
||||||
{"id": 2, "name": "Tree 2", "common_name_latin_name_id": 2},
|
{"id": 2, "name": "Tree 2", "common_name_latin_name_id": 2},
|
||||||
{"id": 3, "name": "Tree 3", "common_name_latin_name_id": 3},
|
{"id": 3, "name": "Tree 3", "common_name_latin_name_id": 3},
|
||||||
|
|
@ -100,7 +98,7 @@ def test_extract_multiple_columns_with_rename(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_extract_invalid_columns(fresh_db):
|
def test_extract_invalid_columns(fresh_db):
|
||||||
fresh_db.table("tree").insert(
|
fresh_db["tree"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Tree 1",
|
"name": "Tree 1",
|
||||||
|
|
@ -110,19 +108,19 @@ def test_extract_invalid_columns(fresh_db):
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
with pytest.raises(InvalidColumns):
|
with pytest.raises(InvalidColumns):
|
||||||
fresh_db.table("tree").extract(["bad_column"])
|
fresh_db["tree"].extract(["bad_column"])
|
||||||
|
|
||||||
|
|
||||||
def test_extract_rowid_table(fresh_db):
|
def test_extract_rowid_table(fresh_db):
|
||||||
fresh_db.table("tree").insert(
|
fresh_db["tree"].insert(
|
||||||
{
|
{
|
||||||
"name": "Tree 1",
|
"name": "Tree 1",
|
||||||
"common_name": "Palm",
|
"common_name": "Palm",
|
||||||
"latin_name": "Arecaceae",
|
"latin_name": "Arecaceae",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
fresh_db.table("tree").extract(["common_name", "latin_name"])
|
fresh_db["tree"].extract(["common_name", "latin_name"])
|
||||||
assert fresh_db.table("tree").schema == (
|
assert fresh_db["tree"].schema == (
|
||||||
'CREATE TABLE "tree" (\n'
|
'CREATE TABLE "tree" (\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
|
' "common_name_latin_name_id" INTEGER REFERENCES "common_name_latin_name"("id")\n'
|
||||||
|
|
@ -140,68 +138,68 @@ def test_extract_rowid_table(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_reuse_lookup_table(fresh_db):
|
def test_reuse_lookup_table(fresh_db):
|
||||||
fresh_db.table("species").insert({"id": 1, "name": "Wolf"}, pk="id")
|
fresh_db["species"].insert({"id": 1, "name": "Wolf"}, pk="id")
|
||||||
fresh_db.table("sightings").insert({"id": 10, "species": "Wolf"}, pk="id")
|
fresh_db["sightings"].insert({"id": 10, "species": "Wolf"}, pk="id")
|
||||||
fresh_db.table("individuals").insert(
|
fresh_db["individuals"].insert(
|
||||||
{"id": 10, "name": "Terriana", "species": "Fox"}, pk="id"
|
{"id": 10, "name": "Terriana", "species": "Fox"}, pk="id"
|
||||||
)
|
)
|
||||||
fresh_db.table("sightings").extract("species", rename={"species": "name"})
|
fresh_db["sightings"].extract("species", rename={"species": "name"})
|
||||||
fresh_db.table("individuals").extract("species", rename={"species": "name"})
|
fresh_db["individuals"].extract("species", rename={"species": "name"})
|
||||||
assert fresh_db.table("sightings").schema == (
|
assert fresh_db["sightings"].schema == (
|
||||||
'CREATE TABLE "sightings" (\n'
|
'CREATE TABLE "sightings" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
assert fresh_db.table("individuals").schema == (
|
assert fresh_db["individuals"].schema == (
|
||||||
'CREATE TABLE "individuals" (\n'
|
'CREATE TABLE "individuals" (\n'
|
||||||
' "id" INTEGER PRIMARY KEY,\n'
|
' "id" INTEGER PRIMARY KEY,\n'
|
||||||
' "name" TEXT,\n'
|
' "name" TEXT,\n'
|
||||||
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
' "species_id" INTEGER REFERENCES "species"("id")\n'
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("species").rows) == [
|
assert list(fresh_db["species"].rows) == [
|
||||||
{"id": 1, "name": "Wolf"},
|
{"id": 1, "name": "Wolf"},
|
||||||
{"id": 2, "name": "Fox"},
|
{"id": 2, "name": "Fox"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_extract_error_on_incompatible_existing_lookup_table(fresh_db):
|
def test_extract_error_on_incompatible_existing_lookup_table(fresh_db):
|
||||||
fresh_db.table("species").insert({"id": 1})
|
fresh_db["species"].insert({"id": 1})
|
||||||
fresh_db.table("tree").insert({"name": "Tree 1", "common_name": "Palm"})
|
fresh_db["tree"].insert({"name": "Tree 1", "common_name": "Palm"})
|
||||||
with pytest.raises(InvalidColumns):
|
with pytest.raises(InvalidColumns):
|
||||||
fresh_db.table("tree").extract("common_name", table="species")
|
fresh_db["tree"].extract("common_name", table="species")
|
||||||
|
|
||||||
# Try again with incompatible existing column type
|
# Try again with incompatible existing column type
|
||||||
fresh_db.table("species2").insert({"id": 1, "common_name": 3.5})
|
fresh_db["species2"].insert({"id": 1, "common_name": 3.5})
|
||||||
with pytest.raises(InvalidColumns):
|
with pytest.raises(InvalidColumns):
|
||||||
fresh_db.table("tree").extract("common_name", table="species2")
|
fresh_db["tree"].extract("common_name", table="species2")
|
||||||
|
|
||||||
|
|
||||||
def test_extract_works_with_null_values(fresh_db):
|
def test_extract_works_with_null_values(fresh_db):
|
||||||
fresh_db.table("listens").insert_all(
|
fresh_db["listens"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "track_title": "foo", "album_title": "bar"},
|
{"id": 1, "track_title": "foo", "album_title": "bar"},
|
||||||
{"id": 2, "track_title": "baz", "album_title": None},
|
{"id": 2, "track_title": "baz", "album_title": None},
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("listens").extract(
|
fresh_db["listens"].extract(
|
||||||
columns=["album_title"], table="albums", fk_column="album_id"
|
columns=["album_title"], table="albums", fk_column="album_id"
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("listens").rows) == [
|
assert list(fresh_db["listens"].rows) == [
|
||||||
{"id": 1, "track_title": "foo", "album_id": 1},
|
{"id": 1, "track_title": "foo", "album_id": 1},
|
||||||
{"id": 2, "track_title": "baz", "album_id": None},
|
{"id": 2, "track_title": "baz", "album_id": None},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("albums").rows) == [
|
assert list(fresh_db["albums"].rows) == [
|
||||||
{"id": 1, "album_title": "bar"},
|
{"id": 1, "album_title": "bar"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_extract_null_values_single_column(fresh_db):
|
def test_extract_null_values_single_column(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/186
|
# https://github.com/simonw/sqlite-utils/issues/186
|
||||||
fresh_db.table("species").insert({"id": 1, "species": "Wolf"}, pk="id")
|
fresh_db["species"].insert({"id": 1, "species": "Wolf"}, pk="id")
|
||||||
fresh_db.table("individuals").insert_all(
|
fresh_db["individuals"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 10, "name": "Terriana", "species": "Fox"},
|
{"id": 10, "name": "Terriana", "species": "Fox"},
|
||||||
{"id": 11, "name": "Spenidorm", "species": None},
|
{"id": 11, "name": "Spenidorm", "species": None},
|
||||||
|
|
@ -211,13 +209,13 @@ def test_extract_null_values_single_column(fresh_db):
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("individuals").extract("species")
|
fresh_db["individuals"].extract("species")
|
||||||
# No null row should have been added to species
|
# No null row should have been added to species
|
||||||
assert list(fresh_db.table("species").rows) == [
|
assert list(fresh_db["species"].rows) == [
|
||||||
{"id": 1, "species": "Wolf"},
|
{"id": 1, "species": "Wolf"},
|
||||||
{"id": 2, "species": "Fox"},
|
{"id": 2, "species": "Fox"},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("individuals").rows) == [
|
assert list(fresh_db["individuals"].rows) == [
|
||||||
{"id": 10, "name": "Terriana", "species_id": 2},
|
{"id": 10, "name": "Terriana", "species_id": 2},
|
||||||
{"id": 11, "name": "Spenidorm", "species_id": None},
|
{"id": 11, "name": "Spenidorm", "species_id": None},
|
||||||
{"id": 12, "name": "Grantheim", "species_id": 1},
|
{"id": 12, "name": "Grantheim", "species_id": 1},
|
||||||
|
|
@ -229,7 +227,7 @@ def test_extract_null_values_single_column(fresh_db):
|
||||||
def test_extract_null_values_multiple_columns(fresh_db):
|
def test_extract_null_values_multiple_columns(fresh_db):
|
||||||
# A row should be extracted if at least one column is not null -
|
# A row should be extracted if at least one column is not null -
|
||||||
# only rows where ALL extracted columns are null are left alone
|
# only rows where ALL extracted columns are null are left alone
|
||||||
fresh_db.table("circulation").insert_all(
|
fresh_db["circulation"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "title": "title one", "creator": "creator one", "year": 2018},
|
{"id": 1, "title": "title one", "creator": "creator one", "year": 2018},
|
||||||
{"id": 2, "title": "title two", "creator": None, "year": 2019},
|
{"id": 2, "title": "title two", "creator": None, "year": 2019},
|
||||||
|
|
@ -238,14 +236,14 @@ def test_extract_null_values_multiple_columns(fresh_db):
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("circulation").extract(
|
fresh_db["circulation"].extract(
|
||||||
["title", "creator"], table="books", fk_column="book_id"
|
["title", "creator"], table="books", fk_column="book_id"
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("books").rows) == [
|
assert list(fresh_db["books"].rows) == [
|
||||||
{"id": 1, "title": "title one", "creator": "creator one"},
|
{"id": 1, "title": "title one", "creator": "creator one"},
|
||||||
{"id": 2, "title": "title two", "creator": None},
|
{"id": 2, "title": "title two", "creator": None},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("circulation").rows) == [
|
assert list(fresh_db["circulation"].rows) == [
|
||||||
{"id": 1, "book_id": 1, "year": 2018},
|
{"id": 1, "book_id": 1, "year": 2018},
|
||||||
{"id": 2, "book_id": 2, "year": 2019},
|
{"id": 2, "book_id": 2, "year": 2019},
|
||||||
{"id": 3, "book_id": None, "year": 2020},
|
{"id": 3, "book_id": None, "year": 2020},
|
||||||
|
|
@ -256,20 +254,20 @@ def test_extract_null_values_multiple_columns(fresh_db):
|
||||||
def test_extract_null_values_existing_lookup_table_with_null_row(fresh_db):
|
def test_extract_null_values_existing_lookup_table_with_null_row(fresh_db):
|
||||||
# Even if the lookup table already contains an all-null row, rows where
|
# Even if the lookup table already contains an all-null row, rows where
|
||||||
# every extracted column is null should keep a null foreign key
|
# every extracted column is null should keep a null foreign key
|
||||||
fresh_db.table("species").insert({"id": 1, "species": None}, pk="id")
|
fresh_db["species"].insert({"id": 1, "species": None}, pk="id")
|
||||||
fresh_db.table("individuals").insert_all(
|
fresh_db["individuals"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 10, "name": "Terriana", "species": "Fox"},
|
{"id": 10, "name": "Terriana", "species": "Fox"},
|
||||||
{"id": 11, "name": "Spenidorm", "species": None},
|
{"id": 11, "name": "Spenidorm", "species": None},
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("individuals").extract("species")
|
fresh_db["individuals"].extract("species")
|
||||||
assert list(fresh_db.table("species").rows) == [
|
assert list(fresh_db["species"].rows) == [
|
||||||
{"id": 1, "species": None},
|
{"id": 1, "species": None},
|
||||||
{"id": 2, "species": "Fox"},
|
{"id": 2, "species": "Fox"},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("individuals").rows) == [
|
assert list(fresh_db["individuals"].rows) == [
|
||||||
{"id": 10, "name": "Terriana", "species_id": 2},
|
{"id": 10, "name": "Terriana", "species_id": 2},
|
||||||
{"id": 11, "name": "Spenidorm", "species_id": None},
|
{"id": 11, "name": "Spenidorm", "species_id": None},
|
||||||
]
|
]
|
||||||
|
|
@ -280,19 +278,17 @@ def test_extract_repeated_into_shared_lookup_with_nulls(fresh_db):
|
||||||
# cannot dedupe NULL-containing rows against the existing lookup
|
# cannot dedupe NULL-containing rows against the existing lookup
|
||||||
# table - extracting a second table into the same lookup previously
|
# table - extracting a second table into the same lookup previously
|
||||||
# inserted duplicate rows that nothing pointed to
|
# inserted duplicate rows that nothing pointed to
|
||||||
fresh_db.table("t1").insert_all(
|
fresh_db["t1"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "species": None, "common": "X"},
|
{"id": 1, "species": None, "common": "X"},
|
||||||
{"id": 2, "species": "Oak", "common": "Oak"},
|
{"id": 2, "species": "Oak", "common": "Oak"},
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("t2").insert_all(
|
fresh_db["t2"].insert_all([{"id": 1, "species": None, "common": "X"}], pk="id")
|
||||||
[{"id": 1, "species": None, "common": "X"}], pk="id"
|
fresh_db["t1"].extract(["species", "common"], table="lk")
|
||||||
)
|
fresh_db["t2"].extract(["species", "common"], table="lk")
|
||||||
fresh_db.table("t1").extract(["species", "common"], table="lk")
|
assert fresh_db["lk"].count == 2
|
||||||
fresh_db.table("t2").extract(["species", "common"], table="lk")
|
|
||||||
assert fresh_db.table("lk").count == 2
|
|
||||||
# Both tables point at the same lookup row
|
# Both tables point at the same lookup row
|
||||||
t1_fk = fresh_db.execute("select lk_id from t1 where id = 1").fetchone()[0]
|
t1_fk = fresh_db.execute("select lk_id from t1 where id = 1").fetchone()[0]
|
||||||
t2_fk = fresh_db.execute("select lk_id from t2 where id = 1").fetchone()[0]
|
t2_fk = fresh_db.execute("select lk_id from t2 where id = 1").fetchone()[0]
|
||||||
|
|
@ -301,43 +297,8 @@ def test_extract_repeated_into_shared_lookup_with_nulls(fresh_db):
|
||||||
|
|
||||||
def test_extract_repeated_into_shared_lookup_no_nulls(fresh_db):
|
def test_extract_repeated_into_shared_lookup_no_nulls(fresh_db):
|
||||||
# Non-NULL rows were already deduped by the unique index - keep it so
|
# Non-NULL rows were already deduped by the unique index - keep it so
|
||||||
fresh_db.table("t1").insert_all([{"id": 1, "species": "Oak"}], pk="id")
|
fresh_db["t1"].insert_all([{"id": 1, "species": "Oak"}], pk="id")
|
||||||
fresh_db.table("t2").insert_all([{"id": 1, "species": "Oak"}], pk="id")
|
fresh_db["t2"].insert_all([{"id": 1, "species": "Oak"}], pk="id")
|
||||||
fresh_db.table("t1").extract(["species"], table="lk")
|
fresh_db["t1"].extract(["species"], table="lk")
|
||||||
fresh_db.table("t2").extract(["species"], table="lk")
|
fresh_db["t2"].extract(["species"], table="lk")
|
||||||
assert fresh_db.table("lk").count == 1
|
assert fresh_db["lk"].count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_extract_preserves_strict_any(fresh_db):
|
|
||||||
if not fresh_db.supports_strict:
|
|
||||||
pytest.skip("SQLite version does not support strict tables")
|
|
||||||
fresh_db.execute("create table items (id integer primary key, data any) strict")
|
|
||||||
fresh_db.execute("insert into items values (1, ?)", ("000123",))
|
|
||||||
|
|
||||||
fresh_db["items"].extract("data", table="data_values")
|
|
||||||
|
|
||||||
lookup = fresh_db["data_values"]
|
|
||||||
assert lookup.strict is True
|
|
||||||
assert lookup.columns_dict == {"id": int, "data": ANY}
|
|
||||||
assert fresh_db.execute(
|
|
||||||
"select typeof(data), data from data_values"
|
|
||||||
).fetchone() == ("text", "000123")
|
|
||||||
|
|
||||||
|
|
||||||
def test_extract_strict_any_rejects_non_strict_lookup(fresh_db):
|
|
||||||
if not fresh_db.supports_strict:
|
|
||||||
pytest.skip("SQLite version does not support strict tables")
|
|
||||||
fresh_db.execute("create table items (data any) strict")
|
|
||||||
fresh_db.execute("insert into items values (?)", ("000123",))
|
|
||||||
fresh_db.execute("create table data_values (id integer primary key, data any)")
|
|
||||||
|
|
||||||
with pytest.raises(
|
|
||||||
InvalidColumns,
|
|
||||||
match="is not STRICT, so it cannot preserve ANY column values",
|
|
||||||
):
|
|
||||||
fresh_db["items"].extract("data", table="data_values")
|
|
||||||
|
|
||||||
assert fresh_db.execute("select typeof(data), data from items").fetchone() == (
|
|
||||||
"text",
|
|
||||||
"000123",
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils.db import Index
|
from sqlite_utils.db import Index
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"kwargs,expected_table",
|
"kwargs,expected_table",
|
||||||
[
|
[
|
||||||
({"extracts": {"species_id": "Species"}}, "Species"),
|
(dict(extracts={"species_id": "Species"}), "Species"),
|
||||||
({"extracts": ["species_id"]}, "species_id"),
|
(dict(extracts=["species_id"]), "species_id"),
|
||||||
({"extracts": ("species_id",)}, "species_id"),
|
(dict(extracts=("species_id",)), "species_id"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("use_table_factory", [True, False])
|
@pytest.mark.parametrize("use_table_factory", [True, False])
|
||||||
|
|
@ -31,16 +30,20 @@ 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 (
|
||||||
f'CREATE TABLE "{expected_table}" (\n "id" INTEGER PRIMARY KEY,\n "value" TEXT\n)'
|
'CREATE TABLE "{}" (\n "id" INTEGER PRIMARY KEY,\n "value" TEXT\n)'.format(
|
||||||
== fresh_db.table(expected_table).schema
|
expected_table
|
||||||
|
)
|
||||||
|
== fresh_db[expected_table].schema
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
f'CREATE TABLE "Trees" (\n "id" INTEGER,\n "species_id" INTEGER REFERENCES "{expected_table}"("id")\n)'
|
'CREATE TABLE "Trees" (\n "id" INTEGER,\n "species_id" INTEGER REFERENCES "{}"("id")\n)'.format(
|
||||||
== fresh_db.table("Trees").schema
|
expected_table
|
||||||
|
)
|
||||||
|
== fresh_db["Trees"].schema
|
||||||
)
|
)
|
||||||
# Should have a foreign key reference
|
# Should have a foreign key reference
|
||||||
assert len(fresh_db.table("Trees").foreign_keys) == 1
|
assert len(fresh_db["Trees"].foreign_keys) == 1
|
||||||
fk = fresh_db.table("Trees").foreign_keys[0]
|
fk = fresh_db["Trees"].foreign_keys[0]
|
||||||
assert fk.table == "Trees"
|
assert fk.table == "Trees"
|
||||||
assert fk.column == "species_id"
|
assert fk.column == "species_id"
|
||||||
|
|
||||||
|
|
@ -48,28 +51,28 @@ def test_extracts(fresh_db, kwargs, expected_table, use_table_factory):
|
||||||
assert [
|
assert [
|
||||||
Index(
|
Index(
|
||||||
seq=0,
|
seq=0,
|
||||||
name=f"idx_{expected_table}_value",
|
name="idx_{}_value".format(expected_table),
|
||||||
unique=1,
|
unique=1,
|
||||||
origin="c",
|
origin="c",
|
||||||
partial=0,
|
partial=0,
|
||||||
columns=["value"],
|
columns=["value"],
|
||||||
)
|
)
|
||||||
] == fresh_db.table(expected_table).indexes
|
] == fresh_db[expected_table].indexes
|
||||||
# Finally, check the rows
|
# Finally, check the rows
|
||||||
assert [{"id": 1, "value": "Oak"}, {"id": 2, "value": "Palm"}] == list(
|
assert [{"id": 1, "value": "Oak"}, {"id": 2, "value": "Palm"}] == list(
|
||||||
fresh_db.table(expected_table).rows
|
fresh_db[expected_table].rows
|
||||||
)
|
)
|
||||||
assert [
|
assert [
|
||||||
{"id": 1, "species_id": 1},
|
{"id": 1, "species_id": 1},
|
||||||
{"id": 2, "species_id": 1},
|
{"id": 2, "species_id": 1},
|
||||||
{"id": 3, "species_id": 2},
|
{"id": 3, "species_id": 2},
|
||||||
] == list(fresh_db.table("Trees").rows)
|
] == list(fresh_db["Trees"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_extracts_null_values(fresh_db):
|
def test_extracts_null_values(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/186
|
# https://github.com/simonw/sqlite-utils/issues/186
|
||||||
# Null values should stay null, not be extracted into the lookup table
|
# Null values should stay null, not be extracted into the lookup table
|
||||||
fresh_db.table("Trees").insert_all(
|
fresh_db["Trees"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "species_id": "Oak"},
|
{"id": 1, "species_id": "Oak"},
|
||||||
{"id": 2, "species_id": None},
|
{"id": 2, "species_id": None},
|
||||||
|
|
@ -78,11 +81,11 @@ def test_extracts_null_values(fresh_db):
|
||||||
],
|
],
|
||||||
extracts={"species_id": "Species"},
|
extracts={"species_id": "Species"},
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("Species").rows) == [
|
assert list(fresh_db["Species"].rows) == [
|
||||||
{"id": 1, "value": "Oak"},
|
{"id": 1, "value": "Oak"},
|
||||||
{"id": 2, "value": "Palm"},
|
{"id": 2, "value": "Palm"},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("Trees").rows) == [
|
assert list(fresh_db["Trees"].rows) == [
|
||||||
{"id": 1, "species_id": 1},
|
{"id": 1, "species_id": 1},
|
||||||
{"id": 2, "species_id": None},
|
{"id": 2, "species_id": None},
|
||||||
{"id": 3, "species_id": 2},
|
{"id": 3, "species_id": 2},
|
||||||
|
|
@ -92,7 +95,7 @@ def test_extracts_null_values(fresh_db):
|
||||||
|
|
||||||
def test_extracts_null_values_list_mode(fresh_db):
|
def test_extracts_null_values_list_mode(fresh_db):
|
||||||
# Same as test_extracts_null_values but for list-based records
|
# Same as test_extracts_null_values but for list-based records
|
||||||
fresh_db.table("Trees").insert_all(
|
fresh_db["Trees"].insert_all(
|
||||||
[
|
[
|
||||||
["id", "species_id"],
|
["id", "species_id"],
|
||||||
[1, "Oak"],
|
[1, "Oak"],
|
||||||
|
|
@ -102,11 +105,11 @@ def test_extracts_null_values_list_mode(fresh_db):
|
||||||
],
|
],
|
||||||
extracts={"species_id": "Species"},
|
extracts={"species_id": "Species"},
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("Species").rows) == [
|
assert list(fresh_db["Species"].rows) == [
|
||||||
{"id": 1, "value": "Oak"},
|
{"id": 1, "value": "Oak"},
|
||||||
{"id": 2, "value": "Palm"},
|
{"id": 2, "value": "Palm"},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("Trees").rows) == [
|
assert list(fresh_db["Trees"].rows) == [
|
||||||
{"id": 1, "species_id": 1},
|
{"id": 1, "species_id": 1},
|
||||||
{"id": 2, "species_id": None},
|
{"id": 2, "species_id": None},
|
||||||
{"id": 3, "species_id": 2},
|
{"id": 3, "species_id": 2},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""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
|
||||||
|
|
@ -32,7 +31,7 @@ def compound_db():
|
||||||
|
|
||||||
|
|
||||||
def test_compound_foreign_key(compound_db):
|
def test_compound_foreign_key(compound_db):
|
||||||
fks = compound_db.table("courses").foreign_keys
|
fks = compound_db["courses"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
fk = fks[0]
|
fk = fks[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
|
|
@ -46,10 +45,10 @@ def test_compound_foreign_key(compound_db):
|
||||||
|
|
||||||
|
|
||||||
def test_single_foreign_key_gets_columns_fields(fresh_db):
|
def test_single_foreign_key_gets_columns_fields(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Sally"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Sally"}, pk="id")
|
||||||
fresh_db.table("books").insert({"title": "Hedgehogs", "author_id": 1})
|
fresh_db["books"].insert({"title": "Hedgehogs", "author_id": 1})
|
||||||
fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
|
fresh_db["books"].add_foreign_key("author_id", "authors", "id")
|
||||||
fk = fresh_db.table("books").foreign_keys[0]
|
fk = fresh_db["books"].foreign_keys[0]
|
||||||
assert fk.is_compound is False
|
assert fk.is_compound is False
|
||||||
assert fk.column == "author_id"
|
assert fk.column == "author_id"
|
||||||
assert fk.other_column == "id"
|
assert fk.other_column == "id"
|
||||||
|
|
@ -60,29 +59,27 @@ def test_single_foreign_key_gets_columns_fields(fresh_db):
|
||||||
def test_foreign_key_no_longer_unpacks_as_tuple(fresh_db):
|
def test_foreign_key_no_longer_unpacks_as_tuple(fresh_db):
|
||||||
# Clean break in 4.0: ForeignKey is a dataclass, not a namedtuple, so the
|
# Clean break in 4.0: ForeignKey is a dataclass, not a namedtuple, so the
|
||||||
# old tuple unpacking and indexing patterns now fail hard.
|
# old tuple unpacking and indexing patterns now fail hard.
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Sally"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Sally"}, pk="id")
|
||||||
fresh_db.table("books").insert({"title": "Hedgehogs", "author_id": 1})
|
fresh_db["books"].insert({"title": "Hedgehogs", "author_id": 1})
|
||||||
fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
|
fresh_db["books"].add_foreign_key("author_id", "authors", "id")
|
||||||
fk = fresh_db.table("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]
|
||||||
|
|
||||||
|
|
||||||
def test_foreign_keys_are_sortable(fresh_db):
|
def test_foreign_keys_are_sortable(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1, "name": "Sally"}, pk="id")
|
fresh_db["authors"].insert({"id": 1, "name": "Sally"}, pk="id")
|
||||||
fresh_db.table("categories").insert({"id": 1, "name": "Wildlife"}, pk="id")
|
fresh_db["categories"].insert({"id": 1, "name": "Wildlife"}, pk="id")
|
||||||
fresh_db.table("books").insert(
|
fresh_db["books"].insert({"title": "Hedgehogs", "author_id": 1, "category_id": 1})
|
||||||
{"title": "Hedgehogs", "author_id": 1, "category_id": 1}
|
|
||||||
)
|
|
||||||
fresh_db.add_foreign_keys(
|
fresh_db.add_foreign_keys(
|
||||||
[
|
[
|
||||||
("books", "author_id", "authors", "id"),
|
("books", "author_id", "authors", "id"),
|
||||||
("books", "category_id", "categories", "id"),
|
("books", "category_id", "categories", "id"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
fks = sorted(fresh_db.table("books").foreign_keys)
|
fks = sorted(fresh_db["books"].foreign_keys)
|
||||||
assert fks[0].column == "author_id"
|
assert fks[0].column == "author_id"
|
||||||
assert fks[1].column == "category_id"
|
assert fks[1].column == "category_id"
|
||||||
|
|
||||||
|
|
@ -107,7 +104,7 @@ def test_mixed_compound_and_single_foreign_keys_are_sortable():
|
||||||
REFERENCES departments(campus_name, dept_code)
|
REFERENCES departments(campus_name, dept_code)
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
fks = db.table("courses").foreign_keys
|
fks = db["courses"].foreign_keys
|
||||||
assert len(fks) == 2
|
assert len(fks) == 2
|
||||||
assert {fk.is_compound for fk in fks} == {True, False}
|
assert {fk.is_compound for fk in fks} == {True, False}
|
||||||
fks_sorted = sorted(fks)
|
fks_sorted = sorted(fks)
|
||||||
|
|
@ -165,8 +162,8 @@ def test_create_table_with_compound_foreign_key(departments_db, foreign_keys):
|
||||||
pk="course_code",
|
pk="course_code",
|
||||||
foreign_keys=foreign_keys,
|
foreign_keys=foreign_keys,
|
||||||
)
|
)
|
||||||
assert departments_db.table("courses").schema == EXPECTED_COURSES_SCHEMA
|
assert departments_db["courses"].schema == EXPECTED_COURSES_SCHEMA
|
||||||
fks = departments_db.table("courses").foreign_keys
|
fks = departments_db["courses"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
fk = fks[0]
|
fk = fks[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
|
|
@ -183,10 +180,10 @@ def test_create_table_compound_foreign_key_enforced(departments_db):
|
||||||
pk="course_code",
|
pk="course_code",
|
||||||
foreign_keys=[(("campus_name", "dept_code"), "departments")],
|
foreign_keys=[(("campus_name", "dept_code"), "departments")],
|
||||||
)
|
)
|
||||||
departments_db.table("departments").insert(
|
departments_db["departments"].insert(
|
||||||
{"campus_name": "Berkeley", "dept_code": "CS", "dept_name": "Computer Science"}
|
{"campus_name": "Berkeley", "dept_code": "CS", "dept_name": "Computer Science"}
|
||||||
)
|
)
|
||||||
departments_db.table("courses").insert(
|
departments_db["courses"].insert(
|
||||||
{"course_code": "CS101", "campus_name": "Berkeley", "dept_code": "CS"}
|
{"course_code": "CS101", "campus_name": "Berkeley", "dept_code": "CS"}
|
||||||
)
|
)
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
|
|
@ -209,8 +206,8 @@ def test_create_table_compound_foreign_key_missing_other_column(departments_db):
|
||||||
|
|
||||||
|
|
||||||
def test_transform_preserves_compound_foreign_key(compound_db):
|
def test_transform_preserves_compound_foreign_key(compound_db):
|
||||||
compound_db.table("courses").transform(rename={"course_name": "title"})
|
compound_db["courses"].transform(rename={"course_name": "title"})
|
||||||
fks = compound_db.table("courses").foreign_keys
|
fks = compound_db["courses"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
fk = fks[0]
|
fk = fks[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
|
|
@ -220,8 +217,8 @@ def test_transform_preserves_compound_foreign_key(compound_db):
|
||||||
|
|
||||||
|
|
||||||
def test_transform_rename_member_column_updates_compound_foreign_key(compound_db):
|
def test_transform_rename_member_column_updates_compound_foreign_key(compound_db):
|
||||||
compound_db.table("courses").transform(rename={"campus_name": "campus"})
|
compound_db["courses"].transform(rename={"campus_name": "campus"})
|
||||||
fks = compound_db.table("courses").foreign_keys
|
fks = compound_db["courses"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
fk = fks[0]
|
fk = fks[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
|
|
@ -233,9 +230,9 @@ def test_transform_rename_member_column_updates_compound_foreign_key(compound_db
|
||||||
def test_transform_drop_member_column_drops_compound_foreign_key(compound_db):
|
def test_transform_drop_member_column_drops_compound_foreign_key(compound_db):
|
||||||
# Matches single-column behavior: dropping the column silently
|
# Matches single-column behavior: dropping the column silently
|
||||||
# drops the foreign key that used it
|
# drops the foreign key that used it
|
||||||
compound_db.table("courses").transform(drop={"dept_code"})
|
compound_db["courses"].transform(drop={"dept_code"})
|
||||||
assert compound_db.table("courses").foreign_keys == []
|
assert compound_db["courses"].foreign_keys == []
|
||||||
assert "FOREIGN KEY" not in compound_db.table("courses").schema
|
assert "FOREIGN KEY" not in compound_db["courses"].schema
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -248,11 +245,11 @@ def test_transform_drop_member_column_drops_compound_foreign_key(compound_db):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_transform_drop_compound_foreign_key(compound_db, drop_foreign_keys):
|
def test_transform_drop_compound_foreign_key(compound_db, drop_foreign_keys):
|
||||||
compound_db.table("courses").transform(drop_foreign_keys=drop_foreign_keys)
|
compound_db["courses"].transform(drop_foreign_keys=drop_foreign_keys)
|
||||||
assert compound_db.table("courses").foreign_keys == []
|
assert compound_db["courses"].foreign_keys == []
|
||||||
# The columns themselves survive
|
# The columns themselves survive
|
||||||
assert {"campus_name", "dept_code"} <= set(
|
assert {"campus_name", "dept_code"} <= set(
|
||||||
compound_db.table("courses").columns_dict.keys()
|
compound_db["courses"].columns_dict.keys()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -267,12 +264,12 @@ def courses_db(departments_db):
|
||||||
|
|
||||||
|
|
||||||
def test_add_compound_foreign_key(courses_db):
|
def test_add_compound_foreign_key(courses_db):
|
||||||
t = courses_db.table("courses").add_foreign_key(
|
t = courses_db["courses"].add_foreign_key(
|
||||||
("campus_name", "dept_code"), "departments", ("campus_name", "dept_code")
|
("campus_name", "dept_code"), "departments", ("campus_name", "dept_code")
|
||||||
)
|
)
|
||||||
# Returns self
|
# Returns self
|
||||||
assert t.name == "courses"
|
assert t.name == "courses"
|
||||||
fks = courses_db.table("courses").foreign_keys
|
fks = courses_db["courses"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
fk = fks[0]
|
fk = fks[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
|
|
@ -283,33 +280,27 @@ def test_add_compound_foreign_key(courses_db):
|
||||||
|
|
||||||
def test_add_compound_foreign_key_guesses_other_columns(courses_db):
|
def test_add_compound_foreign_key_guesses_other_columns(courses_db):
|
||||||
# Lists work here too, though tuples are the documented form
|
# Lists work here too, though tuples are the documented form
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(["campus_name", "dept_code"], "departments")
|
||||||
["campus_name", "dept_code"], "departments"
|
fk = courses_db["courses"].foreign_keys[0]
|
||||||
)
|
|
||||||
fk = courses_db.table("courses").foreign_keys[0]
|
|
||||||
assert fk.other_columns == ("campus_name", "dept_code")
|
assert fk.other_columns == ("campus_name", "dept_code")
|
||||||
|
|
||||||
|
|
||||||
def test_add_compound_foreign_key_error_if_already_exists(courses_db):
|
def test_add_compound_foreign_key_error_if_already_exists(courses_db):
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(("campus_name", "dept_code"), "departments")
|
||||||
("campus_name", "dept_code"), "departments"
|
|
||||||
)
|
|
||||||
with pytest.raises(AlterError) as ex:
|
with pytest.raises(AlterError) as ex:
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(
|
||||||
("campus_name", "dept_code"), "departments"
|
("campus_name", "dept_code"), "departments"
|
||||||
)
|
)
|
||||||
assert "already exists" in ex.value.args[0]
|
assert "already exists" in ex.value.args[0]
|
||||||
# ignore=True should not raise
|
# ignore=True should not raise
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(
|
||||||
("campus_name", "dept_code"), "departments", ignore=True
|
("campus_name", "dept_code"), "departments", ignore=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_add_compound_foreign_key_error_if_column_missing(courses_db):
|
def test_add_compound_foreign_key_error_if_column_missing(courses_db):
|
||||||
with pytest.raises(AlterError):
|
with pytest.raises(AlterError):
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(("campus_name", "nope"), "departments")
|
||||||
("campus_name", "nope"), "departments"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_db_add_foreign_keys_compound(courses_db):
|
def test_db_add_foreign_keys_compound(courses_db):
|
||||||
|
|
@ -323,14 +314,14 @@ def test_db_add_foreign_keys_compound(courses_db):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
fk = courses_db.table("courses").foreign_keys[0]
|
fk = courses_db["courses"].foreign_keys[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
assert fk.columns == ("campus_name", "dept_code")
|
assert fk.columns == ("campus_name", "dept_code")
|
||||||
|
|
||||||
|
|
||||||
def test_index_foreign_keys_compound_creates_composite_index(compound_db):
|
def test_index_foreign_keys_compound_creates_composite_index(compound_db):
|
||||||
compound_db.index_foreign_keys()
|
compound_db.index_foreign_keys()
|
||||||
index_columns = [i.columns for i in compound_db.table("courses").indexes]
|
index_columns = [i.columns for i in compound_db["courses"].indexes]
|
||||||
assert ["campus_name", "dept_code"] in index_columns
|
assert ["campus_name", "dept_code"] in index_columns
|
||||||
# No separate single-column indexes for the members
|
# No separate single-column indexes for the members
|
||||||
assert ["campus_name"] not in index_columns
|
assert ["campus_name"] not in index_columns
|
||||||
|
|
@ -347,22 +338,22 @@ def test_foreign_key_captures_on_delete_and_on_update():
|
||||||
ON DELETE CASCADE ON UPDATE RESTRICT
|
ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
fk = db.table("books").foreign_keys[0]
|
fk = db["books"].foreign_keys[0]
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert fk.on_update == "RESTRICT"
|
assert fk.on_update == "RESTRICT"
|
||||||
|
|
||||||
|
|
||||||
def test_foreign_key_on_delete_defaults_to_no_action(fresh_db):
|
def test_foreign_key_on_delete_defaults_to_no_action(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").insert({"id": 1, "author_id": 1}, pk="id")
|
fresh_db["books"].insert({"id": 1, "author_id": 1}, pk="id")
|
||||||
fresh_db.table("books").add_foreign_key("author_id", "authors", "id")
|
fresh_db["books"].add_foreign_key("author_id", "authors", "id")
|
||||||
fk = fresh_db.table("books").foreign_keys[0]
|
fk = fresh_db["books"].foreign_keys[0]
|
||||||
assert fk.on_delete == "NO ACTION"
|
assert fk.on_delete == "NO ACTION"
|
||||||
assert fk.on_update == "NO ACTION"
|
assert fk.on_update == "NO ACTION"
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_foreign_key_with_on_delete(fresh_db):
|
def test_create_table_foreign_key_with_on_delete(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.create_table(
|
fresh_db.create_table(
|
||||||
"books",
|
"books",
|
||||||
{"id": int, "author_id": int},
|
{"id": int, "author_id": int},
|
||||||
|
|
@ -377,8 +368,8 @@ def test_create_table_foreign_key_with_on_delete(fresh_db):
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert "ON DELETE CASCADE" in fresh_db.table("books").schema
|
assert "ON DELETE CASCADE" in fresh_db["books"].schema
|
||||||
assert fresh_db.table("books").foreign_keys[0].on_delete == "CASCADE"
|
assert fresh_db["books"].foreign_keys[0].on_delete == "CASCADE"
|
||||||
|
|
||||||
|
|
||||||
def test_transform_preserves_on_delete_cascade():
|
def test_transform_preserves_on_delete_cascade():
|
||||||
|
|
@ -391,11 +382,11 @@ def test_transform_preserves_on_delete_cascade():
|
||||||
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE
|
author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
db.table("books").transform(rename={"title": "book_title"})
|
db["books"].transform(rename={"title": "book_title"})
|
||||||
fk = db.table("books").foreign_keys[0]
|
fk = db["books"].foreign_keys[0]
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert fk.on_update == "NO ACTION"
|
assert fk.on_update == "NO ACTION"
|
||||||
assert "ON DELETE CASCADE" in db.table("books").schema
|
assert "ON DELETE CASCADE" in db["books"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_transform_preserves_compound_foreign_key_on_delete():
|
def test_transform_preserves_compound_foreign_key_on_delete():
|
||||||
|
|
@ -414,11 +405,11 @@ def test_transform_preserves_compound_foreign_key_on_delete():
|
||||||
REFERENCES departments(campus_name, dept_code) ON DELETE CASCADE
|
REFERENCES departments(campus_name, dept_code) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
db.table("courses").transform(rename={"course_code": "code"})
|
db["courses"].transform(rename={"course_code": "code"})
|
||||||
fk = db.table("courses").foreign_keys[0]
|
fk = db["courses"].foreign_keys[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert "ON DELETE CASCADE" in db.table("courses").schema
|
assert "ON DELETE CASCADE" in db["courses"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_implicit_primary_key_reference_is_resolved():
|
def test_implicit_primary_key_reference_is_resolved():
|
||||||
|
|
@ -432,7 +423,7 @@ def test_implicit_primary_key_reference_is_resolved():
|
||||||
author_id INTEGER REFERENCES authors
|
author_id INTEGER REFERENCES authors
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
fk = db.table("books").foreign_keys[0]
|
fk = db["books"].foreign_keys[0]
|
||||||
assert fk.is_compound is False
|
assert fk.is_compound is False
|
||||||
assert fk.other_column == "author_id"
|
assert fk.other_column == "author_id"
|
||||||
assert fk.other_columns == ("author_id",)
|
assert fk.other_columns == ("author_id",)
|
||||||
|
|
@ -453,7 +444,7 @@ def test_implicit_compound_primary_key_reference_is_resolved():
|
||||||
FOREIGN KEY (campus_name, dept_code) REFERENCES departments
|
FOREIGN KEY (campus_name, dept_code) REFERENCES departments
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
fk = db.table("courses").foreign_keys[0]
|
fk = db["courses"].foreign_keys[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
assert fk.other_columns == ("campus_name", "dept_code")
|
assert fk.other_columns == ("campus_name", "dept_code")
|
||||||
|
|
||||||
|
|
@ -478,14 +469,14 @@ def test_add_foreign_keys_preserves_actions(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/594 review finding:
|
# https://github.com/simonw/sqlite-utils/issues/594 review finding:
|
||||||
# ForeignKey objects passed to db.add_foreign_keys() were flattened
|
# ForeignKey objects passed to db.add_foreign_keys() were flattened
|
||||||
# to plain tuples, losing on_delete/on_update
|
# to plain tuples, losing on_delete/on_update
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").insert({"id": 1, "author_id": 1}, pk="id")
|
fresh_db["books"].insert({"id": 1, "author_id": 1}, pk="id")
|
||||||
fresh_db.add_foreign_keys(
|
fresh_db.add_foreign_keys(
|
||||||
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
||||||
)
|
)
|
||||||
fk = fresh_db.table("books").foreign_keys[0]
|
fk = fresh_db["books"].foreign_keys[0]
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert "ON DELETE CASCADE" in fresh_db.table("books").schema
|
assert "ON DELETE CASCADE" in fresh_db["books"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_keys_preserves_actions_compound(courses_db):
|
def test_add_foreign_keys_preserves_actions_compound(courses_db):
|
||||||
|
|
@ -503,36 +494,36 @@ def test_add_foreign_keys_preserves_actions_compound(courses_db):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
fk = courses_db.table("courses").foreign_keys[0]
|
fk = courses_db["courses"].foreign_keys[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert "ON DELETE CASCADE" in courses_db.table("courses").schema
|
assert "ON DELETE CASCADE" in courses_db["courses"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_key_on_delete_on_update(fresh_db):
|
def test_add_foreign_key_on_delete_on_update(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").insert({"id": 1, "author_id": 1}, pk="id")
|
fresh_db["books"].insert({"id": 1, "author_id": 1}, pk="id")
|
||||||
fresh_db.table("books").add_foreign_key(
|
fresh_db["books"].add_foreign_key(
|
||||||
"author_id", "authors", "id", on_delete="CASCADE", on_update="RESTRICT"
|
"author_id", "authors", "id", on_delete="CASCADE", on_update="RESTRICT"
|
||||||
)
|
)
|
||||||
fk = fresh_db.table("books").foreign_keys[0]
|
fk = fresh_db["books"].foreign_keys[0]
|
||||||
assert fk.on_delete == "CASCADE"
|
assert fk.on_delete == "CASCADE"
|
||||||
assert fk.on_update == "RESTRICT"
|
assert fk.on_update == "RESTRICT"
|
||||||
assert "ON UPDATE RESTRICT ON DELETE CASCADE" in fresh_db.table("books").schema
|
assert "ON UPDATE RESTRICT ON DELETE CASCADE" in fresh_db["books"].schema
|
||||||
# The cascade should actually fire
|
# The cascade should actually fire
|
||||||
fresh_db.execute("PRAGMA foreign_keys = ON")
|
fresh_db.execute("PRAGMA foreign_keys = ON")
|
||||||
fresh_db.execute("delete from authors where id = 1")
|
fresh_db.execute("delete from authors where id = 1")
|
||||||
assert fresh_db.table("books").count == 0
|
assert fresh_db["books"].count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_add_compound_foreign_key_on_delete(courses_db):
|
def test_add_compound_foreign_key_on_delete(courses_db):
|
||||||
courses_db.table("courses").add_foreign_key(
|
courses_db["courses"].add_foreign_key(
|
||||||
("campus_name", "dept_code"), "departments", on_delete="SET NULL"
|
("campus_name", "dept_code"), "departments", on_delete="SET NULL"
|
||||||
)
|
)
|
||||||
fk = courses_db.table("courses").foreign_keys[0]
|
fk = courses_db["courses"].foreign_keys[0]
|
||||||
assert fk.is_compound is True
|
assert fk.is_compound is True
|
||||||
assert fk.on_delete == "SET NULL"
|
assert fk.on_delete == "SET NULL"
|
||||||
assert "ON DELETE SET NULL" in courses_db.table("courses").schema
|
assert "ON DELETE SET NULL" in courses_db["courses"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_implicit_compound_foreign_key_resolves_pk_declaration_order(fresh_db):
|
def test_implicit_compound_foreign_key_resolves_pk_declaration_order(fresh_db):
|
||||||
|
|
@ -544,7 +535,7 @@ def test_implicit_compound_foreign_key_resolves_pk_declaration_order(fresh_db):
|
||||||
fresh_db.execute(
|
fresh_db.execute(
|
||||||
"create table child (x text, y text, foreign key (x, y) references other)"
|
"create table child (x text, y text, foreign key (x, y) references other)"
|
||||||
)
|
)
|
||||||
fk = fresh_db.table("child").foreign_keys[0]
|
fk = fresh_db["child"].foreign_keys[0]
|
||||||
assert fk.other_columns == ("a", "b")
|
assert fk.other_columns == ("a", "b")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -557,46 +548,46 @@ def test_transform_implicit_compound_foreign_key_stays_valid(fresh_db):
|
||||||
"create table child (x text, y text, foreign key (x, y) references other)"
|
"create table child (x text, y text, foreign key (x, y) references other)"
|
||||||
)
|
)
|
||||||
fresh_db.execute("PRAGMA foreign_keys = ON")
|
fresh_db.execute("PRAGMA foreign_keys = ON")
|
||||||
fresh_db.table("other").insert({"a": "A", "b": "B"})
|
fresh_db["other"].insert({"a": "A", "b": "B"})
|
||||||
fresh_db.table("child").insert({"x": "A", "y": "B"})
|
fresh_db["child"].insert({"x": "A", "y": "B"})
|
||||||
fresh_db.table("child").transform(types={"x": str})
|
fresh_db["child"].transform(types={"x": str})
|
||||||
assert fresh_db.table("child").foreign_keys[0].other_columns == ("a", "b")
|
assert fresh_db["child"].foreign_keys[0].other_columns == ("a", "b")
|
||||||
# The constraint still points the right way around
|
# The constraint still points the right way around
|
||||||
fresh_db.table("child").insert({"x": "A", "y": "B"})
|
fresh_db["child"].insert({"x": "A", "y": "B"})
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
fresh_db.table("child").insert({"x": "B", "y": "A"})
|
fresh_db["child"].insert({"x": "B", "y": "A"})
|
||||||
|
|
||||||
|
|
||||||
def test_create_compound_foreign_key_guesses_pk_declaration_order(fresh_db):
|
def test_create_compound_foreign_key_guesses_pk_declaration_order(fresh_db):
|
||||||
fresh_db.execute("create table other (b text, a text, primary key (a, b))")
|
fresh_db.execute("create table other (b text, a text, primary key (a, b))")
|
||||||
fresh_db.table("other").insert({"a": "A", "b": "B"})
|
fresh_db["other"].insert({"a": "A", "b": "B"})
|
||||||
fresh_db.table("child").create(
|
fresh_db["child"].create(
|
||||||
{"id": int, "x": str, "y": str},
|
{"id": int, "x": str, "y": str},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[(("x", "y"), "other")],
|
foreign_keys=[(("x", "y"), "other")],
|
||||||
)
|
)
|
||||||
assert fresh_db.table("child").foreign_keys[0].other_columns == ("a", "b")
|
assert fresh_db["child"].foreign_keys[0].other_columns == ("a", "b")
|
||||||
fresh_db.execute("PRAGMA foreign_keys = ON")
|
fresh_db.execute("PRAGMA foreign_keys = ON")
|
||||||
fresh_db.table("child").insert({"id": 1, "x": "A", "y": "B"})
|
fresh_db["child"].insert({"id": 1, "x": "A", "y": "B"})
|
||||||
with pytest.raises(sqlite3.IntegrityError):
|
with pytest.raises(sqlite3.IntegrityError):
|
||||||
fresh_db.table("child").insert({"id": 2, "x": "B", "y": "A"})
|
fresh_db["child"].insert({"id": 2, "x": "B", "y": "A"})
|
||||||
|
|
||||||
|
|
||||||
def test_add_compound_foreign_key_guesses_pk_declaration_order(fresh_db):
|
def test_add_compound_foreign_key_guesses_pk_declaration_order(fresh_db):
|
||||||
fresh_db.execute("create table other (b text, a text, primary key (a, b))")
|
fresh_db.execute("create table other (b text, a text, primary key (a, b))")
|
||||||
fresh_db.table("child").insert({"id": 1, "x": "A", "y": "B"}, pk="id")
|
fresh_db["child"].insert({"id": 1, "x": "A", "y": "B"}, pk="id")
|
||||||
fresh_db.table("child").add_foreign_key(("x", "y"), "other")
|
fresh_db["child"].add_foreign_key(("x", "y"), "other")
|
||||||
assert fresh_db.table("child").foreign_keys[0].other_columns == ("a", "b")
|
assert fresh_db["child"].foreign_keys[0].other_columns == ("a", "b")
|
||||||
|
|
||||||
|
|
||||||
def test_foreign_keys_are_hashable(fresh_db):
|
def test_foreign_keys_are_hashable(fresh_db):
|
||||||
# set() over foreign_keys worked with the 3.x namedtuple and must
|
# set() over foreign_keys worked with the 3.x namedtuple and must
|
||||||
# keep working with the dataclass
|
# keep working with the dataclass
|
||||||
fresh_db.table("p").insert({"id": 1}, pk="id")
|
fresh_db["p"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("c").insert(
|
fresh_db["c"].insert(
|
||||||
{"id": 1, "pid": 1}, pk="id", foreign_keys=[("pid", "p", "id")]
|
{"id": 1, "pid": 1}, pk="id", foreign_keys=[("pid", "p", "id")]
|
||||||
)
|
)
|
||||||
fks = set(fresh_db.table("c").foreign_keys)
|
fks = set(fresh_db["c"].foreign_keys)
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
assert ForeignKey("c", "pid", "p", "id") in fks
|
assert ForeignKey("c", "pid", "p", "id") in fks
|
||||||
# Usable as dict keys too
|
# Usable as dict keys too
|
||||||
|
|
@ -608,7 +599,7 @@ def test_foreign_key_is_immutable():
|
||||||
|
|
||||||
fk = ForeignKey("c", "pid", "p", "id")
|
fk = ForeignKey("c", "pid", "p", "id")
|
||||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||||
setattr(fk, "table", "other")
|
fk.table = "other"
|
||||||
|
|
||||||
|
|
||||||
def test_foreign_key_equality_and_hash_include_actions():
|
def test_foreign_key_equality_and_hash_include_actions():
|
||||||
|
|
@ -625,9 +616,9 @@ def test_create_table_mixed_foreign_keys_list(fresh_db):
|
||||||
# 3.x accepted a mix of ForeignKey objects, tuples and bare column
|
# 3.x accepted a mix of ForeignKey objects, tuples and bare column
|
||||||
# strings in foreign_keys= (ForeignKey was a namedtuple, so it passed
|
# strings in foreign_keys= (ForeignKey was a namedtuple, so it passed
|
||||||
# the tuple check) - keep accepting the mix
|
# the tuple check) - keep accepting the mix
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("publishers").insert({"id": 1}, pk="id")
|
fresh_db["publishers"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").create(
|
fresh_db["books"].create(
|
||||||
{"id": int, "author_id": int, "publisher_id": int},
|
{"id": int, "author_id": int, "publisher_id": int},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[
|
foreign_keys=[
|
||||||
|
|
@ -635,14 +626,14 @@ def test_create_table_mixed_foreign_keys_list(fresh_db):
|
||||||
("publisher_id", "publishers", "id"),
|
("publisher_id", "publishers", "id"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
fks = {fk.column: fk.other_table for fk in fresh_db.table("books").foreign_keys}
|
fks = {fk.column: fk.other_table for fk in fresh_db["books"].foreign_keys}
|
||||||
assert fks == {"author_id": "authors", "publisher_id": "publishers"}
|
assert fks == {"author_id": "authors", "publisher_id": "publishers"}
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_mixed_foreign_keys_with_string(fresh_db):
|
def test_create_table_mixed_foreign_keys_with_string(fresh_db):
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("publishers").insert({"id": 1}, pk="id")
|
fresh_db["publishers"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").create(
|
fresh_db["books"].create(
|
||||||
{"id": int, "author_id": int, "publisher_id": int},
|
{"id": int, "author_id": int, "publisher_id": int},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[
|
foreign_keys=[
|
||||||
|
|
@ -650,15 +641,15 @@ def test_create_table_mixed_foreign_keys_with_string(fresh_db):
|
||||||
("publisher_id", "publishers", "id"),
|
("publisher_id", "publishers", "id"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
fks = {fk.column: fk.other_table for fk in fresh_db.table("books").foreign_keys}
|
fks = {fk.column: fk.other_table for fk in fresh_db["books"].foreign_keys}
|
||||||
assert fks == {"author_id": "authors", "publisher_id": "publishers"}
|
assert fks == {"author_id": "authors", "publisher_id": "publishers"}
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_keys_existing_with_different_actions_errors(fresh_db):
|
def test_add_foreign_keys_existing_with_different_actions_errors(fresh_db):
|
||||||
# Requesting an existing foreign key with different ON DELETE/ON UPDATE
|
# Requesting an existing foreign key with different ON DELETE/ON UPDATE
|
||||||
# actions was silently skipped, dropping the requested change
|
# actions was silently skipped, dropping the requested change
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").insert(
|
fresh_db["books"].insert(
|
||||||
{"id": 1, "author_id": 1},
|
{"id": 1, "author_id": 1},
|
||||||
pk="id",
|
pk="id",
|
||||||
foreign_keys=[("author_id", "authors", "id")],
|
foreign_keys=[("author_id", "authors", "id")],
|
||||||
|
|
@ -668,21 +659,19 @@ def test_add_foreign_keys_existing_with_different_actions_errors(fresh_db):
|
||||||
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
||||||
)
|
)
|
||||||
assert "ON DELETE" in str(ex.value)
|
assert "ON DELETE" in str(ex.value)
|
||||||
assert fresh_db.table("books").foreign_keys[0].on_delete == "NO ACTION"
|
assert fresh_db["books"].foreign_keys[0].on_delete == "NO ACTION"
|
||||||
|
|
||||||
|
|
||||||
def test_add_foreign_keys_identical_existing_is_noop(fresh_db):
|
def test_add_foreign_keys_identical_existing_is_noop(fresh_db):
|
||||||
# An exact match, including actions, is silently skipped so repeated
|
# An exact match, including actions, is silently skipped so repeated
|
||||||
# calls stay idempotent
|
# calls stay idempotent
|
||||||
fresh_db.table("authors").insert({"id": 1}, pk="id")
|
fresh_db["authors"].insert({"id": 1}, pk="id")
|
||||||
fresh_db.table("books").insert({"id": 1, "author_id": 1}, pk="id")
|
fresh_db["books"].insert({"id": 1, "author_id": 1}, pk="id")
|
||||||
fresh_db.table("books").add_foreign_key(
|
fresh_db["books"].add_foreign_key("author_id", "authors", "id", on_delete="CASCADE")
|
||||||
"author_id", "authors", "id", on_delete="CASCADE"
|
|
||||||
)
|
|
||||||
fresh_db.add_foreign_keys(
|
fresh_db.add_foreign_keys(
|
||||||
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
[ForeignKey("books", "author_id", "authors", "id", on_delete="CASCADE")]
|
||||||
)
|
)
|
||||||
fks = fresh_db.table("books").foreign_keys
|
fks = fresh_db["books"].foreign_keys
|
||||||
assert len(fks) == 1
|
assert len(fks) == 1
|
||||||
assert fks[0].on_delete == "CASCADE"
|
assert fks[0].on_delete == "CASCADE"
|
||||||
|
|
||||||
|
|
@ -690,13 +679,13 @@ def test_add_foreign_keys_identical_existing_is_noop(fresh_db):
|
||||||
def test_add_foreign_keys_compound_column_count_mismatch_errors(fresh_db):
|
def test_add_foreign_keys_compound_column_count_mismatch_errors(fresh_db):
|
||||||
# Previously the extra other-column was silently discarded, creating
|
# Previously the extra other-column was silently discarded, creating
|
||||||
# a single-column foreign key to just ("id")
|
# a single-column foreign key to just ("id")
|
||||||
fresh_db.table("departments").insert(
|
fresh_db["departments"].insert(
|
||||||
{"campus": "north", "code": "cs"}, pk=("campus", "code")
|
{"campus": "north", "code": "cs"}, pk=("campus", "code")
|
||||||
)
|
)
|
||||||
fresh_db.table("courses").insert({"id": 1, "campus": "north"}, pk="id")
|
fresh_db["courses"].insert({"id": 1, "campus": "north"}, pk="id")
|
||||||
with pytest.raises(ValueError) as ex:
|
with pytest.raises(ValueError) as ex:
|
||||||
fresh_db.add_foreign_keys(
|
fresh_db.add_foreign_keys(
|
||||||
[("courses", ("campus",), "departments", ("campus", "code"))]
|
[("courses", ("campus",), "departments", ("campus", "code"))]
|
||||||
)
|
)
|
||||||
assert "same number of columns" in str(ex.value)
|
assert "same number of columns" in str(ex.value)
|
||||||
assert fresh_db.table("courses").foreign_keys == []
|
assert fresh_db["courses"].foreign_keys == []
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
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 = [
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +18,7 @@ search_records = [
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fts(fresh_db):
|
def test_enable_fts(fresh_db):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
assert ["searchable"] == fresh_db.table_names()
|
assert ["searchable"] == fresh_db.table_names()
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
|
|
@ -54,7 +52,7 @@ def test_enable_fts(fresh_db):
|
||||||
def test_enable_fts_escape_table_names(fresh_db):
|
def test_enable_fts_escape_table_names(fresh_db):
|
||||||
# Table names with restricted chars are handled correctly.
|
# Table names with restricted chars are handled correctly.
|
||||||
# colons and dots are restricted characters for table names.
|
# colons and dots are restricted characters for table names.
|
||||||
table = fresh_db.table("http://example.com")
|
table = fresh_db["http://example.com"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
assert ["http://example.com"] == fresh_db.table_names()
|
assert ["http://example.com"] == fresh_db.table_names()
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
|
|
@ -87,7 +85,7 @@ def test_enable_fts_escape_table_names(fresh_db):
|
||||||
|
|
||||||
def test_search_duplicate_columns_are_deduped(fresh_db):
|
def test_search_duplicate_columns_are_deduped(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/624
|
# https://github.com/simonw/sqlite-utils/issues/624
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
rows = list(table.search("tanuki", columns=["text", "text"]))
|
rows = list(table.search("tanuki", columns=["text", "text"]))
|
||||||
|
|
@ -100,32 +98,20 @@ def test_search_duplicate_columns_are_deduped(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_search_limit_offset(fresh_db):
|
def test_search_limit_offset(fresh_db):
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
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 next(iter(table.search("are", limit=1, order_by="rowid")))["rowid"] == 1
|
assert list(table.search("are", limit=1, order_by="rowid"))[0]["rowid"] == 1
|
||||||
assert (
|
assert (
|
||||||
next(iter(table.search("are", limit=1, offset=1, order_by="rowid")))["rowid"]
|
list(table.search("are", limit=1, offset=1, order_by="rowid"))[0]["rowid"] == 2
|
||||||
== 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_search_offset_without_limit(fresh_db):
|
|
||||||
table = fresh_db.table("t")
|
|
||||||
table.insert_all(search_records)
|
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
|
||||||
assert [row["rowid"] for row in table.search("are", order_by="rowid")] == [1, 2]
|
|
||||||
assert [
|
|
||||||
row["rowid"] for row in table.search("are", offset=1, order_by="rowid")
|
|
||||||
] == [2]
|
|
||||||
assert table.search_sql(offset=1).strip().endswith("limit -1 offset 1")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fts_version", ("FTS4", "FTS5"))
|
@pytest.mark.parametrize("fts_version", ("FTS4", "FTS5"))
|
||||||
def test_search_where(fresh_db, fts_version):
|
def test_search_where(fresh_db, fts_version):
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"], fts_version=fts_version)
|
table.enable_fts(["text", "country"], fts_version=fts_version)
|
||||||
results = list(
|
results = list(
|
||||||
|
|
@ -142,7 +128,7 @@ def test_search_where(fresh_db, fts_version):
|
||||||
|
|
||||||
|
|
||||||
def test_search_where_args_disallows_query(fresh_db):
|
def test_search_where_args_disallows_query(fresh_db):
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
with pytest.raises(ValueError) as ex:
|
with pytest.raises(ValueError) as ex:
|
||||||
list(
|
list(
|
||||||
table.search(
|
table.search(
|
||||||
|
|
@ -156,7 +142,7 @@ def test_search_where_args_disallows_query(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_search_include_rank(fresh_db):
|
def test_search_include_rank(fresh_db):
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS5")
|
table.enable_fts(["text", "country"], fts_version="FTS5")
|
||||||
results = list(table.search("are", include_rank=True))
|
results = list(table.search("are", include_rank=True))
|
||||||
|
|
@ -182,7 +168,7 @@ def test_search_include_rank(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fts_table_names_containing_spaces(fresh_db):
|
def test_enable_fts_table_names_containing_spaces(fresh_db):
|
||||||
table = fresh_db.table("test")
|
table = fresh_db["test"]
|
||||||
table.insert({"column with spaces": "in its name"})
|
table.insert({"column with spaces": "in its name"})
|
||||||
table.enable_fts(["column with spaces"])
|
table.enable_fts(["column with spaces"])
|
||||||
assert [
|
assert [
|
||||||
|
|
@ -196,7 +182,7 @@ def test_enable_fts_table_names_containing_spaces(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_populate_fts(fresh_db):
|
def test_populate_fts(fresh_db):
|
||||||
table = fresh_db.table("populatable")
|
table = fresh_db["populatable"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
assert [] == list(table.search("trash pandas"))
|
assert [] == list(table.search("trash pandas"))
|
||||||
|
|
@ -217,7 +203,7 @@ def test_populate_fts(fresh_db):
|
||||||
|
|
||||||
def test_populate_fts_escape_table_names(fresh_db):
|
def test_populate_fts_escape_table_names(fresh_db):
|
||||||
# Restricted characters such as colon and dots should be escaped.
|
# Restricted characters such as colon and dots should be escaped.
|
||||||
table = fresh_db.table("http://example.com")
|
table = fresh_db["http://example.com"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4")
|
table.enable_fts(["text", "country"], fts_version="FTS4")
|
||||||
assert [] == list(table.search("trash pandas"))
|
assert [] == list(table.search("trash pandas"))
|
||||||
|
|
@ -237,20 +223,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 = f"searchable_{fts_version}"
|
table_name = "searchable_{}".format(fts_version)
|
||||||
table = fresh_db.table(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=f"FTS{fts_version}",
|
fts_version="FTS{}".format(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=f"FTS{fts_version}",
|
fts_version="FTS{}".format(fts_version),
|
||||||
tokenize="porter",
|
tokenize="porter",
|
||||||
)
|
)
|
||||||
rows = list(table.search("bite", order_by="rowid"))
|
rows = list(table.search("bite", order_by="rowid"))
|
||||||
|
|
@ -263,24 +249,12 @@ def test_fts_tokenize(fresh_db, fts_version):
|
||||||
}.items() <= rows[0].items()
|
}.items() <= rows[0].items()
|
||||||
|
|
||||||
|
|
||||||
def test_fts_tokenize_escaped(fresh_db):
|
|
||||||
# A malicious tokenize value must not be able to break out of the
|
|
||||||
# string literal in the CREATE VIRTUAL TABLE statement.
|
|
||||||
table = fresh_db.table("searchable")
|
|
||||||
table.insert_all(search_records)
|
|
||||||
malicious = "porter'); CREATE TABLE injected(x); --"
|
|
||||||
with pytest.raises(Exception):
|
|
||||||
table.enable_fts(["text"], tokenize=malicious)
|
|
||||||
# The injected statement must not have executed
|
|
||||||
assert "injected" not in fresh_db.table_names()
|
|
||||||
|
|
||||||
|
|
||||||
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 = f"searchable_{fts_version}"
|
table_name = "searchable_{}".format(fts_version)
|
||||||
table = fresh_db.table(table_name)
|
table = fresh_db[table_name]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"], fts_version=f"FTS{fts_version}")
|
table.enable_fts(["text", "country"], fts_version="FTS{}".format(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",
|
||||||
|
|
@ -288,11 +262,11 @@ def test_optimize_fts(fresh_db):
|
||||||
"searchable_4_fts",
|
"searchable_4_fts",
|
||||||
"searchable_5_fts",
|
"searchable_5_fts",
|
||||||
):
|
):
|
||||||
fresh_db.table(table_name).optimize()
|
fresh_db[table_name].optimize()
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fts_with_triggers(fresh_db):
|
def test_enable_fts_with_triggers(fresh_db):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"], fts_version="FTS4", create_triggers=True)
|
table.enable_fts(["text", "country"], fts_version="FTS4", create_triggers=True)
|
||||||
rows1 = list(table.search("tanuki"))
|
rows1 = list(table.search("tanuki"))
|
||||||
|
|
@ -321,7 +295,7 @@ def test_enable_fts_with_triggers(fresh_db):
|
||||||
|
|
||||||
@pytest.mark.parametrize("create_triggers", [True, False])
|
@pytest.mark.parametrize("create_triggers", [True, False])
|
||||||
def test_disable_fts(fresh_db, create_triggers):
|
def test_disable_fts(fresh_db, create_triggers):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"], create_triggers=create_triggers)
|
table.enable_fts(["text", "country"], create_triggers=create_triggers)
|
||||||
assert {
|
assert {
|
||||||
|
|
@ -336,12 +310,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 == {
|
assert expected_triggers == set(
|
||||||
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 (
|
||||||
|
|
@ -354,7 +328,7 @@ def test_disable_fts(fresh_db, create_triggers):
|
||||||
|
|
||||||
|
|
||||||
def test_rebuild_fts(fresh_db):
|
def test_rebuild_fts(fresh_db):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"])
|
table.enable_fts(["text", "country"])
|
||||||
# Run a search
|
# Run a search
|
||||||
|
|
@ -380,7 +354,7 @@ def test_rebuild_fts(fresh_db):
|
||||||
def test_optimize_and_rebuild_fts_commit(tmpdir, method):
|
def test_optimize_and_rebuild_fts_commit(tmpdir, method):
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
table = db.table("searchable")
|
table = db["searchable"]
|
||||||
table.insert(search_records[0])
|
table.insert(search_records[0])
|
||||||
table.enable_fts(["text", "country"])
|
table.enable_fts(["text", "country"])
|
||||||
getattr(table, method)()
|
getattr(table, method)()
|
||||||
|
|
@ -390,16 +364,16 @@ def test_optimize_and_rebuild_fts_commit(tmpdir, method):
|
||||||
table.insert(search_records[1])
|
table.insert(search_records[1])
|
||||||
db.close()
|
db.close()
|
||||||
db2 = Database(path)
|
db2 = Database(path)
|
||||||
assert db2.table("searchable").count == 2
|
assert db2["searchable"].count == 2
|
||||||
db2.close()
|
db2.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("invalid_table", ["does_not_exist", "not_searchable"])
|
@pytest.mark.parametrize("invalid_table", ["does_not_exist", "not_searchable"])
|
||||||
def test_rebuild_fts_invalid(fresh_db, invalid_table):
|
def test_rebuild_fts_invalid(fresh_db, invalid_table):
|
||||||
fresh_db.table("not_searchable").insert({"foo": "bar"})
|
fresh_db["not_searchable"].insert({"foo": "bar"})
|
||||||
# Raise OperationalError on invalid table
|
# Raise OperationalError on invalid table
|
||||||
with pytest.raises(sqlite3.OperationalError):
|
with pytest.raises(sqlite3.OperationalError):
|
||||||
fresh_db.table(invalid_table).rebuild_fts()
|
fresh_db[invalid_table].rebuild_fts()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fts_version", ["FTS4", "FTS5"])
|
@pytest.mark.parametrize("fts_version", ["FTS4", "FTS5"])
|
||||||
|
|
@ -408,17 +382,15 @@ def test_rebuild_removes_junk_docsize_rows(tmpdir, fts_version):
|
||||||
path = tmpdir / "test.db"
|
path = tmpdir / "test.db"
|
||||||
db = Database(str(path), recursive_triggers=False)
|
db = Database(str(path), recursive_triggers=False)
|
||||||
licenses = [{"key": "apache2", "name": "Apache 2"}, {"key": "bsd", "name": "BSD"}]
|
licenses = [{"key": "apache2", "name": "Apache 2"}, {"key": "bsd", "name": "BSD"}]
|
||||||
db.table("licenses").insert_all(licenses, pk="key", replace=True)
|
db["licenses"].insert_all(licenses, pk="key", replace=True)
|
||||||
db.table("licenses").enable_fts(
|
db["licenses"].enable_fts(["name"], create_triggers=True, fts_version=fts_version)
|
||||||
["name"], create_triggers=True, fts_version=fts_version
|
assert db["licenses_fts_docsize"].count == 2
|
||||||
)
|
|
||||||
assert db.table("licenses_fts_docsize").count == 2
|
|
||||||
# Bug: insert with replace increases the number of rows in _docsize:
|
# Bug: insert with replace increases the number of rows in _docsize:
|
||||||
db.table("licenses").insert_all(licenses, pk="key", replace=True)
|
db["licenses"].insert_all(licenses, pk="key", replace=True)
|
||||||
assert db.table("licenses_fts_docsize").count == 4
|
assert db["licenses_fts_docsize"].count == 4
|
||||||
# rebuild should fix this:
|
# rebuild should fix this:
|
||||||
db.table("licenses_fts").rebuild_fts()
|
db["licenses_fts"].rebuild_fts()
|
||||||
assert db.table("licenses_fts_docsize").count == 2
|
assert db["licenses_fts_docsize"].count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -432,7 +404,7 @@ def test_rebuild_removes_junk_docsize_rows(tmpdir, fts_version):
|
||||||
)
|
)
|
||||||
def test_enable_fts_replace(kwargs):
|
def test_enable_fts_replace(kwargs):
|
||||||
db = Database(memory=True)
|
db = Database(memory=True)
|
||||||
db.table("books").insert(
|
db["books"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "Habits of Australian Marsupials",
|
"title": "Habits of Australian Marsupials",
|
||||||
|
|
@ -440,31 +412,31 @@ def test_enable_fts_replace(kwargs):
|
||||||
},
|
},
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
db.table("books").enable_fts(["title", "author"])
|
db["books"].enable_fts(["title", "author"])
|
||||||
assert not db.table("books").triggers
|
assert not db["books"].triggers
|
||||||
assert db.table("books_fts").columns_dict.keys() == {"title", "author"}
|
assert db["books_fts"].columns_dict.keys() == {"title", "author"}
|
||||||
assert "FTS5" in db.table("books_fts").schema
|
assert "FTS5" in db["books_fts"].schema
|
||||||
assert "porter" not in db.table("books_fts").schema
|
assert "porter" not in db["books_fts"].schema
|
||||||
# Now modify the FTS configuration
|
# Now modify the FTS configuration
|
||||||
should_have_changed_columns = "columns" in kwargs
|
should_have_changed_columns = "columns" in kwargs
|
||||||
if "columns" not in kwargs:
|
if "columns" not in kwargs:
|
||||||
kwargs["columns"] = ["title", "author"]
|
kwargs["columns"] = ["title", "author"]
|
||||||
db.table("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.table("books_fts").columns_dict.keys() == {"title"}
|
assert db["books_fts"].columns_dict.keys() == set(["title"])
|
||||||
if "create_triggers" in kwargs:
|
if "create_triggers" in kwargs:
|
||||||
assert db.table("books").triggers
|
assert db["books"].triggers
|
||||||
if "fts_version" in kwargs:
|
if "fts_version" in kwargs:
|
||||||
assert "FTS4" in db.table("books_fts").schema
|
assert "FTS4" in db["books_fts"].schema
|
||||||
if "tokenize" in kwargs:
|
if "tokenize" in kwargs:
|
||||||
assert "porter" in db.table("books_fts").schema
|
assert "porter" in db["books_fts"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fts_replace_does_nothing_if_args_the_same():
|
def test_enable_fts_replace_does_nothing_if_args_the_same():
|
||||||
queries = []
|
queries = []
|
||||||
db = Database(memory=True, tracer=lambda sql, params: queries.append((sql, params)))
|
db = Database(memory=True, tracer=lambda sql, params: queries.append((sql, params)))
|
||||||
db.table("books").insert(
|
db["books"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "Habits of Australian Marsupials",
|
"title": "Habits of Australian Marsupials",
|
||||||
|
|
@ -472,19 +444,17 @@ def test_enable_fts_replace_does_nothing_if_args_the_same():
|
||||||
},
|
},
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
db.table("books").enable_fts(["title", "author"], create_triggers=True)
|
db["books"].enable_fts(["title", "author"], create_triggers=True)
|
||||||
queries.clear()
|
queries.clear()
|
||||||
# Running that again shouldn't run much SQL:
|
# Running that again shouldn't run much SQL:
|
||||||
db.table("books").enable_fts(
|
db["books"].enable_fts(["title", "author"], create_triggers=True, replace=True)
|
||||||
["title", "author"], create_triggers=True, replace=True
|
|
||||||
)
|
|
||||||
# The only SQL that executed should be select statements
|
# The only SQL that executed should be select statements
|
||||||
assert all(q[0].startswith("select ") for q in queries)
|
assert all(q[0].startswith("select ") for q in queries)
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fts_replace_handles_legacy_bracket_quoted_content_table():
|
def test_enable_fts_replace_handles_legacy_bracket_quoted_content_table():
|
||||||
db = Database(memory=True)
|
db = Database(memory=True)
|
||||||
db.table("books").insert(
|
db["books"].insert(
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "Habits of Australian Marsupials",
|
"title": "Habits of Australian Marsupials",
|
||||||
|
|
@ -499,10 +469,10 @@ def test_enable_fts_replace_handles_legacy_bracket_quoted_content_table():
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
db.table("books").enable_fts(["title", "author"], replace=True)
|
db["books"].enable_fts(["title", "author"], replace=True)
|
||||||
|
|
||||||
assert db.table("books_fts").columns_dict.keys() == {"title", "author"}
|
assert db["books_fts"].columns_dict.keys() == {"title", "author"}
|
||||||
assert 'content="books"' in db.table("books_fts").schema
|
assert 'content="books"' in db["books_fts"].schema
|
||||||
|
|
||||||
|
|
||||||
def test_view_has_no_enable_fts():
|
def test_view_has_no_enable_fts():
|
||||||
|
|
@ -510,7 +480,7 @@ def test_view_has_no_enable_fts():
|
||||||
db.create_view("hello", "select 1 + 1")
|
db.create_view("hello", "select 1 + 1")
|
||||||
# Views deliberately do not have an enable_fts() method
|
# Views deliberately do not have an enable_fts() method
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
db.view("hello").enable_fts() # type: ignore[attr-defined]
|
db["hello"].enable_fts() # type: ignore[union-attr]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -716,14 +686,14 @@ def test_view_has_no_enable_fts():
|
||||||
)
|
)
|
||||||
def test_search_sql(kwargs, fts, expected):
|
def test_search_sql(kwargs, fts, expected):
|
||||||
db = Database(memory=True)
|
db = Database(memory=True)
|
||||||
db.table("books").insert(
|
db["books"].insert(
|
||||||
{
|
{
|
||||||
"title": "Habits of Australian Marsupials",
|
"title": "Habits of Australian Marsupials",
|
||||||
"author": "Marlee Hawkins",
|
"author": "Marlee Hawkins",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
db.table("books").enable_fts(["title", "author"], fts_version=fts)
|
db["books"].enable_fts(["title", "author"], fts_version=fts)
|
||||||
sql = db.table("books").search_sql(**kwargs)
|
sql = db["books"].search_sql(**kwargs)
|
||||||
assert sql == expected
|
assert sql == expected
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -744,7 +714,7 @@ def test_search_sql(kwargs, fts, expected):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_quote_fts_query(fresh_db, input, expected):
|
def test_quote_fts_query(fresh_db, input, expected):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"])
|
table.enable_fts(["text", "country"])
|
||||||
quoted = fresh_db.quote_fts(input)
|
quoted = fresh_db.quote_fts(input)
|
||||||
|
|
@ -754,7 +724,7 @@ def test_quote_fts_query(fresh_db, input, expected):
|
||||||
|
|
||||||
|
|
||||||
def test_search_quote(fresh_db):
|
def test_search_quote(fresh_db):
|
||||||
table = fresh_db.table("searchable")
|
table = fresh_db["searchable"]
|
||||||
table.insert_all(search_records)
|
table.insert_all(search_records)
|
||||||
table.enable_fts(["text", "country"])
|
table.enable_fts(["text", "country"])
|
||||||
query = "cat's"
|
query = "cat's"
|
||||||
|
|
@ -767,11 +737,10 @@ def test_search_quote(fresh_db):
|
||||||
def test_enable_fts_cli_on_view_errors(tmpdir):
|
def test_enable_fts_cli_on_view_errors(tmpdir):
|
||||||
db_path = str(tmpdir / "test.db")
|
db_path = str(tmpdir / "test.db")
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
db.table("t").insert({"text": "hello"})
|
db["t"].insert({"text": "hello"})
|
||||||
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,17 +1,16 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.db import NotFoundError
|
from sqlite_utils.db import NotFoundError
|
||||||
|
|
||||||
|
|
||||||
def test_get_rowid(fresh_db):
|
def test_get_rowid(fresh_db):
|
||||||
dogs = fresh_db.table("dogs")
|
dogs = fresh_db["dogs"]
|
||||||
cleo = {"name": "Cleo", "age": 4}
|
cleo = {"name": "Cleo", "age": 4}
|
||||||
row_id = dogs.insert(cleo).last_rowid
|
row_id = dogs.insert(cleo).last_rowid
|
||||||
assert cleo == dogs.get(row_id)
|
assert cleo == dogs.get(row_id)
|
||||||
|
|
||||||
|
|
||||||
def test_get_primary_key(fresh_db):
|
def test_get_primary_key(fresh_db):
|
||||||
dogs = fresh_db.table("dogs")
|
dogs = fresh_db["dogs"]
|
||||||
cleo = {"name": "Cleo", "age": 4, "id": 5}
|
cleo = {"name": "Cleo", "age": 4, "id": 5}
|
||||||
last_pk = dogs.insert(cleo, pk="id").last_pk
|
last_pk = dogs.insert(cleo, pk="id").last_pk
|
||||||
assert 5 == last_pk
|
assert 5 == last_pk
|
||||||
|
|
@ -23,10 +22,10 @@ def test_get_primary_key(fresh_db):
|
||||||
[(100, None), (None, None), ((1, 2), "Need 1 primary key value"), ("2", None)],
|
[(100, None), (None, None), ((1, 2), "Need 1 primary key value"), ("2", None)],
|
||||||
)
|
)
|
||||||
def test_get_not_found(argument, expected_msg, fresh_db):
|
def test_get_not_found(argument, expected_msg, fresh_db):
|
||||||
fresh_db.table("dogs").insert(
|
fresh_db["dogs"].insert(
|
||||||
{"id": 1, "name": "Cleo", "age": 4, "is_good": True}, pk="id"
|
{"id": 1, "name": "Cleo", "age": 4, "is_good": True}, pk="id"
|
||||||
)
|
)
|
||||||
with pytest.raises(NotFoundError) as excinfo:
|
with pytest.raises(NotFoundError) as excinfo:
|
||||||
fresh_db.table("dogs").get(argument)
|
fresh_db["dogs"].get(argument)
|
||||||
if expected_msg is not None:
|
if expected_msg is not None:
|
||||||
assert expected_msg == excinfo.value.args[0]
|
assert expected_msg == excinfo.value.args[0]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
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
|
||||||
|
|
@ -45,7 +44,7 @@ def test_add_geometry_column():
|
||||||
coord_dimension="XY",
|
coord_dimension="XY",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert db.table("geometry_columns").get(["locations", "geometry"]) == {
|
assert db["geometry_columns"].get(["locations", "geometry"]) == {
|
||||||
"f_table_name": "locations",
|
"f_table_name": "locations",
|
||||||
"f_geometry_column": "geometry",
|
"f_geometry_column": "geometry",
|
||||||
"geometry_type": 1, # point
|
"geometry_type": 1, # point
|
||||||
|
|
@ -105,7 +104,7 @@ def test_query_load_extension(use_spatialite_shortcut):
|
||||||
[
|
[
|
||||||
":memory:",
|
":memory:",
|
||||||
"select spatialite_version()",
|
"select spatialite_version()",
|
||||||
f"--load-extension={load_extension}",
|
"--load-extension={}".format(load_extension),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
|
|
@ -133,7 +132,7 @@ def test_cli_add_geometry_column(tmpdir):
|
||||||
db = Database(str(db_path))
|
db = Database(str(db_path))
|
||||||
db.init_spatialite()
|
db.init_spatialite()
|
||||||
|
|
||||||
table = db.table("locations").create({"name": str})
|
table = db["locations"].create({"name": str})
|
||||||
|
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli,
|
cli,
|
||||||
|
|
@ -149,7 +148,7 @@ def test_cli_add_geometry_column(tmpdir):
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
assert db.table("geometry_columns").get(["locations", "geometry"]) == {
|
assert db["geometry_columns"].get(["locations", "geometry"]) == {
|
||||||
"f_table_name": "locations",
|
"f_table_name": "locations",
|
||||||
"f_geometry_column": "geometry",
|
"f_geometry_column": "geometry",
|
||||||
"geometry_type": 1, # point
|
"geometry_type": 1, # point
|
||||||
|
|
@ -164,7 +163,7 @@ def test_cli_add_geometry_column_options(tmpdir):
|
||||||
db_path = tmpdir / "spatial.db"
|
db_path = tmpdir / "spatial.db"
|
||||||
db = Database(str(db_path))
|
db = Database(str(db_path))
|
||||||
db.init_spatialite()
|
db.init_spatialite()
|
||||||
table = db.table("locations").create({"name": str})
|
table = db["locations"].create({"name": str})
|
||||||
|
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli,
|
cli,
|
||||||
|
|
@ -183,7 +182,7 @@ def test_cli_add_geometry_column_options(tmpdir):
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
assert db.table("geometry_columns").get(["locations", "geometry"]) == {
|
assert db["geometry_columns"].get(["locations", "geometry"]) == {
|
||||||
"f_table_name": "locations",
|
"f_table_name": "locations",
|
||||||
"f_geometry_column": "geometry",
|
"f_geometry_column": "geometry",
|
||||||
"geometry_type": 3, # polygon
|
"geometry_type": 3, # polygon
|
||||||
|
|
@ -202,7 +201,7 @@ def test_cli_add_geometry_column_invalid_type(tmpdir):
|
||||||
db = Database(str(db_path))
|
db = Database(str(db_path))
|
||||||
db.init_spatialite()
|
db.init_spatialite()
|
||||||
|
|
||||||
table = db.table("locations").create({"name": str})
|
table = db["locations"].create({"name": str})
|
||||||
|
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
cli,
|
cli,
|
||||||
|
|
@ -225,7 +224,7 @@ def test_cli_create_spatial_index(tmpdir):
|
||||||
db = Database(str(db_path))
|
db = Database(str(db_path))
|
||||||
db.init_spatialite()
|
db.init_spatialite()
|
||||||
|
|
||||||
table = db.table("locations").create({"name": str})
|
table = db["locations"].create({"name": str})
|
||||||
table.add_geometry_column("geometry", "POINT")
|
table.add_geometry_column("geometry", "POINT")
|
||||||
|
|
||||||
result = CliRunner().invoke(
|
result = CliRunner().invoke(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import hypothesis.strategies as st
|
|
||||||
from hypothesis import given
|
from hypothesis import given
|
||||||
|
import hypothesis.strategies as st
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,8 +10,8 @@ def test_roundtrip_integers(integer):
|
||||||
row = {
|
row = {
|
||||||
"integer": integer,
|
"integer": integer,
|
||||||
}
|
}
|
||||||
db.table("test").insert(row)
|
db["test"].insert(row)
|
||||||
assert list(db.table("test").rows) == [row]
|
assert list(db["test"].rows) == [row]
|
||||||
|
|
||||||
|
|
||||||
@given(st.text())
|
@given(st.text())
|
||||||
|
|
@ -21,8 +20,8 @@ def test_roundtrip_text(text):
|
||||||
row = {
|
row = {
|
||||||
"text": text,
|
"text": text,
|
||||||
}
|
}
|
||||||
db.table("test").insert(row)
|
db["test"].insert(row)
|
||||||
assert list(db.table("test").rows) == [row]
|
assert list(db["test"].rows) == [row]
|
||||||
|
|
||||||
|
|
||||||
@given(st.binary(max_size=1024 * 1024))
|
@given(st.binary(max_size=1024 * 1024))
|
||||||
|
|
@ -31,8 +30,8 @@ def test_roundtrip_binary(binary):
|
||||||
row = {
|
row = {
|
||||||
"binary": binary,
|
"binary": binary,
|
||||||
}
|
}
|
||||||
db.table("test").insert(row)
|
db["test"].insert(row)
|
||||||
assert list(db.table("test").rows) == [row]
|
assert list(db["test"].rows) == [row]
|
||||||
|
|
||||||
|
|
||||||
@given(st.floats(allow_nan=False))
|
@given(st.floats(allow_nan=False))
|
||||||
|
|
@ -41,5 +40,5 @@ def test_roundtrip_floats(floats):
|
||||||
row = {
|
row = {
|
||||||
"floats": floats,
|
"floats": floats,
|
||||||
}
|
}
|
||||||
db.table("test").insert(row)
|
db["test"].insert(row)
|
||||||
assert list(db.table("test").rows) == [row]
|
assert list(db["test"].rows) == [row]
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
|
from sqlite_utils import cli, Database
|
||||||
|
from click.testing import CliRunner
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
import sys
|
||||||
|
|
||||||
from sqlite_utils import Database, cli
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("silent", (False, True))
|
@pytest.mark.parametrize("silent", (False, True))
|
||||||
|
|
@ -46,7 +44,7 @@ def test_insert_files(silent, pk_args, expected_pks):
|
||||||
)
|
)
|
||||||
cols = []
|
cols = []
|
||||||
for coltype in coltypes:
|
for coltype in coltypes:
|
||||||
cols += ["-c", f"{coltype}:{coltype}"]
|
cols += ["-c", "{}:{}".format(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)]
|
||||||
|
|
@ -57,7 +55,7 @@ def test_insert_files(silent, pk_args, expected_pks):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
rows_by_path = {r["path"]: r for r in db.table("files").rows}
|
rows_by_path = {r["path"]: r for r in db["files"].rows}
|
||||||
one, two, three = (
|
one, two, three = (
|
||||||
rows_by_path["one.txt"],
|
rows_by_path["one.txt"],
|
||||||
rows_by_path["two.txt"],
|
rows_by_path["two.txt"],
|
||||||
|
|
@ -114,7 +112,7 @@ def test_insert_files(silent, pk_args, expected_pks):
|
||||||
for colname, expected_type in expected_types.items():
|
for colname, expected_type in expected_types.items():
|
||||||
for row in (one, two, three):
|
for row in (one, two, three):
|
||||||
assert isinstance(row[colname], expected_type)
|
assert isinstance(row[colname], expected_type)
|
||||||
assert set(db.table("files").pks) == set(expected_pks)
|
assert set(db["files"].pks) == set(expected_pks)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -144,7 +142,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 = next(iter(db.table("files").rows))
|
row = list(db["files"].rows)[0]
|
||||||
key = "content"
|
key = "content"
|
||||||
if use_text:
|
if use_text:
|
||||||
key = "content_text"
|
key = "content_text"
|
||||||
|
|
@ -169,5 +167,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(
|
||||||
f"Error: Could not read file '{latin.resolve()!s}' as text"
|
"Error: Could not read file '{}' as text".format(str(latin.resolve()))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
from sqlite_utils.db import Index, View, Database, XIndex, XIndexColumn
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sqlite_utils.db import Check, Database, Index, Table, 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."""
|
||||||
|
|
@ -21,10 +20,10 @@ def test_view_names(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_table_names_fts4(existing_db):
|
def test_table_names_fts4(existing_db):
|
||||||
existing_db.table("woo").insert({"title": "Hello"}).enable_fts(
|
existing_db["woo"].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS4"
|
["title"], fts_version="FTS4"
|
||||||
)
|
)
|
||||||
existing_db.table("woo2").insert({"title": "Hello"}).enable_fts(
|
existing_db["woo2"].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS5"
|
["title"], fts_version="FTS5"
|
||||||
)
|
)
|
||||||
assert ["woo_fts"] == existing_db.table_names(fts4=True)
|
assert ["woo_fts"] == existing_db.table_names(fts4=True)
|
||||||
|
|
@ -32,17 +31,17 @@ def test_table_names_fts4(existing_db):
|
||||||
|
|
||||||
|
|
||||||
def test_detect_fts(existing_db):
|
def test_detect_fts(existing_db):
|
||||||
existing_db.table("woo").insert({"title": "Hello"}).enable_fts(
|
existing_db["woo"].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS4"
|
["title"], fts_version="FTS4"
|
||||||
)
|
)
|
||||||
existing_db.table("woo2").insert({"title": "Hello"}).enable_fts(
|
existing_db["woo2"].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS5"
|
["title"], fts_version="FTS5"
|
||||||
)
|
)
|
||||||
assert "woo_fts" == existing_db.table("woo").detect_fts()
|
assert "woo_fts" == existing_db["woo"].detect_fts()
|
||||||
assert "woo_fts" == existing_db.table("woo_fts").detect_fts()
|
assert "woo_fts" == existing_db["woo_fts"].detect_fts()
|
||||||
assert "woo2_fts" == existing_db.table("woo2").detect_fts()
|
assert "woo2_fts" == existing_db["woo2"].detect_fts()
|
||||||
assert "woo2_fts" == existing_db.table("woo2_fts").detect_fts()
|
assert "woo2_fts" == existing_db["woo2_fts"].detect_fts()
|
||||||
assert existing_db.table("foo").detect_fts() is None
|
assert existing_db["foo"].detect_fts() is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("reverse_order", (True, False))
|
@pytest.mark.parametrize("reverse_order", (True, False))
|
||||||
|
|
@ -52,14 +51,14 @@ def test_detect_fts_similar_tables(fresh_db, reverse_order):
|
||||||
if reverse_order:
|
if reverse_order:
|
||||||
table1, table2 = table2, table1
|
table1, table2 = table2, table1
|
||||||
|
|
||||||
fresh_db.table(table1).insert({"title": "Hello"}).enable_fts(
|
fresh_db[table1].insert({"title": "Hello"}).enable_fts(
|
||||||
["title"], fts_version="FTS4"
|
["title"], fts_version="FTS4"
|
||||||
)
|
)
|
||||||
fresh_db.table(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.table(table1).detect_fts() == f"{table1}_fts"
|
assert fresh_db[table1].detect_fts() == "{}_fts".format(table1)
|
||||||
assert fresh_db.table(table2).detect_fts() == f"{table2}_fts"
|
assert fresh_db[table2].detect_fts() == "{}_fts".format(table2)
|
||||||
|
|
||||||
|
|
||||||
def test_tables(existing_db):
|
def test_tables(existing_db):
|
||||||
|
|
@ -77,34 +76,26 @@ def test_views(fresh_db):
|
||||||
assert view.columns_dict == {"1": str}
|
assert view.columns_dict == {"1": str}
|
||||||
|
|
||||||
|
|
||||||
def test_getitem_returns_table_or_view(fresh_db):
|
|
||||||
fresh_db.table("items").insert({"id": 1}, pk="id")
|
|
||||||
fresh_db.create_view("item_ids", "select id from items")
|
|
||||||
|
|
||||||
assert isinstance(fresh_db["items"], Table)
|
|
||||||
assert isinstance(fresh_db["item_ids"], View)
|
|
||||||
|
|
||||||
|
|
||||||
def test_count(existing_db):
|
def test_count(existing_db):
|
||||||
assert existing_db.table("foo").count == 3
|
assert existing_db["foo"].count == 3
|
||||||
assert existing_db.table("foo").count_where() == 3
|
assert existing_db["foo"].count_where() == 3
|
||||||
assert existing_db.table("foo").execute_count() == 3
|
assert existing_db["foo"].execute_count() == 3
|
||||||
|
|
||||||
|
|
||||||
def test_count_where(existing_db):
|
def test_count_where(existing_db):
|
||||||
assert existing_db.table("foo").count_where("text != ?", ["two"]) == 2
|
assert existing_db["foo"].count_where("text != ?", ["two"]) == 2
|
||||||
assert existing_db.table("foo").count_where("text != :t", {"t": "two"}) == 2
|
assert existing_db["foo"].count_where("text != :t", {"t": "two"}) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_columns(existing_db):
|
def test_columns(existing_db):
|
||||||
table = existing_db.table("foo")
|
table = existing_db["foo"]
|
||||||
assert [{"name": "text", "type": "TEXT"}] == [
|
assert [{"name": "text", "type": "TEXT"}] == [
|
||||||
{"name": col.name, "type": col.type} for col in table.columns
|
{"name": col.name, "type": col.type} for col in table.columns
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_table_schema(existing_db):
|
def test_table_schema(existing_db):
|
||||||
assert existing_db.table("foo").schema == "CREATE TABLE foo (text TEXT)"
|
assert existing_db["foo"].schema == "CREATE TABLE foo (text TEXT)"
|
||||||
|
|
||||||
|
|
||||||
def test_database_schema(existing_db):
|
def test_database_schema(existing_db):
|
||||||
|
|
@ -112,9 +103,9 @@ def test_database_schema(existing_db):
|
||||||
|
|
||||||
|
|
||||||
def test_table_repr(fresh_db):
|
def test_table_repr(fresh_db):
|
||||||
table = fresh_db.table("dogs").insert({"name": "Cleo", "age": 4})
|
table = fresh_db["dogs"].insert({"name": "Cleo", "age": 4})
|
||||||
assert "<Table dogs (name, age)>" == repr(table)
|
assert "<Table dogs (name, age)>" == repr(table)
|
||||||
assert "<Table cats (does not exist yet)>" == repr(fresh_db.table("cats"))
|
assert "<Table cats (does not exist yet)>" == repr(fresh_db["cats"])
|
||||||
|
|
||||||
|
|
||||||
def test_indexes(fresh_db):
|
def test_indexes(fresh_db):
|
||||||
|
|
@ -133,7 +124,7 @@ def test_indexes(fresh_db):
|
||||||
columns=["c2", "c3"],
|
columns=["c2", "c3"],
|
||||||
),
|
),
|
||||||
Index(seq=1, name="Gosh_c1", unique=0, origin="c", partial=0, columns=["c1"]),
|
Index(seq=1, name="Gosh_c1", unique=0, origin="c", partial=0, columns=["c1"]),
|
||||||
] == fresh_db.table("Gosh").indexes
|
] == fresh_db["Gosh"].indexes
|
||||||
|
|
||||||
|
|
||||||
def test_xindexes(fresh_db):
|
def test_xindexes(fresh_db):
|
||||||
|
|
@ -142,7 +133,7 @@ def test_xindexes(fresh_db):
|
||||||
create index Gosh_c1 on Gosh(c1);
|
create index Gosh_c1 on Gosh(c1);
|
||||||
create index Gosh_c2c3 on Gosh(c2, c3 desc);
|
create index Gosh_c2c3 on Gosh(c2, c3 desc);
|
||||||
""")
|
""")
|
||||||
assert fresh_db.table("Gosh").xindexes == [
|
assert fresh_db["Gosh"].xindexes == [
|
||||||
XIndex(
|
XIndex(
|
||||||
name="Gosh_c2c3",
|
name="Gosh_c2c3",
|
||||||
columns=[
|
columns=[
|
||||||
|
|
@ -161,31 +152,6 @@ def test_xindexes(fresh_db):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_indexes_with_double_quotes_in_identifiers(fresh_db):
|
|
||||||
fresh_db['Go"sh'].insert({"id": 1, 'c"1': 2}, pk="id")
|
|
||||||
fresh_db['Go"sh'].create_index(['c"1'])
|
|
||||||
assert [(index.name, index.columns) for index in fresh_db['Go"sh'].indexes] == [
|
|
||||||
('idx_Go"sh_c"1', ['c"1'])
|
|
||||||
]
|
|
||||||
assert fresh_db['Go"sh'].xindexes == [
|
|
||||||
XIndex(
|
|
||||||
name='idx_Go"sh_c"1',
|
|
||||||
columns=[
|
|
||||||
XIndexColumn(seqno=0, cid=1, name='c"1', desc=0, coll="BINARY", key=1),
|
|
||||||
XIndexColumn(seqno=1, cid=-1, name=None, desc=0, coll="BINARY", key=0),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_transform_table_with_double_quotes_in_identifiers(fresh_db):
|
|
||||||
fresh_db['Go"sh'].insert({"id": 1, 'c"1': 2, "c2": 3}, pk="id")
|
|
||||||
fresh_db['Go"sh'].create_index(['c"1'])
|
|
||||||
fresh_db['Go"sh'].transform(types={"c2": str})
|
|
||||||
assert fresh_db['Go"sh'].columns_dict["c2"] is str
|
|
||||||
assert [index.columns for index in fresh_db['Go"sh'].indexes] == [['c"1']]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"column,expected_table_guess",
|
"column,expected_table_guess",
|
||||||
(
|
(
|
||||||
|
|
@ -199,55 +165,30 @@ def test_transform_table_with_double_quotes_in_identifiers(fresh_db):
|
||||||
def test_guess_foreign_table(fresh_db, column, expected_table_guess):
|
def test_guess_foreign_table(fresh_db, column, expected_table_guess):
|
||||||
fresh_db.create_table("authors", {"name": str})
|
fresh_db.create_table("authors", {"name": str})
|
||||||
fresh_db.create_table("genre", {"name": str})
|
fresh_db.create_table("genre", {"name": str})
|
||||||
assert expected_table_guess == fresh_db.table("books").guess_foreign_table(column)
|
assert expected_table_guess == fresh_db["books"].guess_foreign_table(column)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"pk,expected", ((None, ["rowid"]), ("id", ["id"]), (["id", "id2"], ["id", "id2"]))
|
"pk,expected", ((None, ["rowid"]), ("id", ["id"]), (["id", "id2"], ["id", "id2"]))
|
||||||
)
|
)
|
||||||
def test_pks(fresh_db, pk, expected):
|
def test_pks(fresh_db, pk, expected):
|
||||||
fresh_db.table("foo").insert_all([{"id": 1, "id2": 2}], pk=pk)
|
fresh_db["foo"].insert_all([{"id": 1, "id2": 2}], pk=pk)
|
||||||
assert expected == fresh_db.table("foo").pks
|
assert expected == fresh_db["foo"].pks
|
||||||
|
|
||||||
|
|
||||||
def test_checks(fresh_db):
|
|
||||||
fresh_db.execute("""
|
|
||||||
CREATE TABLE scores (
|
|
||||||
score INTEGER CONSTRAINT positive CHECK(score > 0),
|
|
||||||
maximum INTEGER,
|
|
||||||
CONSTRAINT within_maximum CHECK(score <= maximum)
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
scores = fresh_db.table("scores")
|
|
||||||
expected_column = Check("score > 0", name="positive", column="score")
|
|
||||||
expected_table = Check("score <= maximum", name="within_maximum")
|
|
||||||
assert scores.checks == [expected_column, expected_table]
|
|
||||||
assert scores.column_checks == {"score": [expected_column]}
|
|
||||||
assert scores.table_checks == [expected_table]
|
|
||||||
assert scores.checks[0].sql == "CONSTRAINT positive CHECK(score > 0)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_checks_nonexistent_and_virtual_tables(fresh_db):
|
|
||||||
assert fresh_db.table("does_not_exist").checks == []
|
|
||||||
fresh_db.table("searchable").insert({"text": "hello"}).enable_fts(
|
|
||||||
["text"], fts_version="FTS5"
|
|
||||||
)
|
|
||||||
assert fresh_db.table("searchable_fts").checks == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_triggers_and_triggers_dict(fresh_db):
|
def test_triggers_and_triggers_dict(fresh_db):
|
||||||
assert [] == fresh_db.triggers
|
assert [] == fresh_db.triggers
|
||||||
authors = fresh_db.table("authors")
|
authors = fresh_db["authors"]
|
||||||
authors.insert_all(
|
authors.insert_all(
|
||||||
[
|
[
|
||||||
{"name": "Frank Herbert", "famous_works": "Dune"},
|
{"name": "Frank Herbert", "famous_works": "Dune"},
|
||||||
{"name": "Neal Stephenson", "famous_works": "Cryptonomicon"},
|
{"name": "Neal Stephenson", "famous_works": "Cryptonomicon"},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
fresh_db.table("other").insert({"foo": "bar"})
|
fresh_db["other"].insert({"foo": "bar"})
|
||||||
assert authors.triggers == []
|
assert authors.triggers == []
|
||||||
assert authors.triggers_dict == {}
|
assert authors.triggers_dict == {}
|
||||||
assert fresh_db.table("other").triggers == []
|
assert fresh_db["other"].triggers == []
|
||||||
assert fresh_db.triggers_dict == {}
|
assert fresh_db.triggers_dict == {}
|
||||||
authors.enable_fts(
|
authors.enable_fts(
|
||||||
["name", "famous_works"], fts_version="FTS4", create_triggers=True
|
["name", "famous_works"], fts_version="FTS4", create_triggers=True
|
||||||
|
|
@ -259,7 +200,7 @@ def test_triggers_and_triggers_dict(fresh_db):
|
||||||
}
|
}
|
||||||
assert expected_triggers == {(t.name, t.table) for t in fresh_db.triggers}
|
assert expected_triggers == {(t.name, t.table) for t in fresh_db.triggers}
|
||||||
assert expected_triggers == {
|
assert expected_triggers == {
|
||||||
(t.name, t.table) for t in fresh_db.table("authors").triggers
|
(t.name, t.table) for t in fresh_db["authors"].triggers
|
||||||
}
|
}
|
||||||
expected_triggers = {
|
expected_triggers = {
|
||||||
"authors_ai": (
|
"authors_ai": (
|
||||||
|
|
@ -279,13 +220,13 @@ def test_triggers_and_triggers_dict(fresh_db):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
assert authors.triggers_dict == expected_triggers
|
assert authors.triggers_dict == expected_triggers
|
||||||
assert fresh_db.table("other").triggers == []
|
assert fresh_db["other"].triggers == []
|
||||||
assert fresh_db.table("other").triggers_dict == {}
|
assert fresh_db["other"].triggers_dict == {}
|
||||||
assert fresh_db.triggers_dict == expected_triggers
|
assert fresh_db.triggers_dict == expected_triggers
|
||||||
|
|
||||||
|
|
||||||
def test_has_counts_triggers(fresh_db):
|
def test_has_counts_triggers(fresh_db):
|
||||||
authors = fresh_db.table("authors")
|
authors = fresh_db["authors"]
|
||||||
authors.insert({"name": "Frank Herbert"})
|
authors.insert({"name": "Frank Herbert"})
|
||||||
assert not authors.has_counts_triggers
|
assert not authors.has_counts_triggers
|
||||||
authors.enable_counts()
|
authors.enable_counts()
|
||||||
|
|
@ -334,14 +275,14 @@ def test_has_counts_triggers(fresh_db):
|
||||||
)
|
)
|
||||||
def test_virtual_table_using(fresh_db, sql, expected_name, expected_using):
|
def test_virtual_table_using(fresh_db, sql, expected_name, expected_using):
|
||||||
fresh_db.execute(sql)
|
fresh_db.execute(sql)
|
||||||
assert fresh_db.table(expected_name).virtual_table_using == expected_using
|
assert fresh_db[expected_name].virtual_table_using == expected_using
|
||||||
|
|
||||||
|
|
||||||
def test_use_rowid(fresh_db):
|
def test_use_rowid(fresh_db):
|
||||||
fresh_db.table("rowid_table").insert({"name": "Cleo"})
|
fresh_db["rowid_table"].insert({"name": "Cleo"})
|
||||||
fresh_db.table("regular_table").insert({"id": 1, "name": "Cleo"}, pk="id")
|
fresh_db["regular_table"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
assert fresh_db.table("rowid_table").use_rowid
|
assert fresh_db["rowid_table"].use_rowid
|
||||||
assert not fresh_db.table("regular_table").use_rowid
|
assert not fresh_db["regular_table"].use_rowid
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
@ -360,7 +301,7 @@ def test_use_rowid(fresh_db):
|
||||||
)
|
)
|
||||||
def test_table_strict(fresh_db, create_table, expected_strict):
|
def test_table_strict(fresh_db, create_table, expected_strict):
|
||||||
fresh_db.execute(create_table)
|
fresh_db.execute(create_table)
|
||||||
table = fresh_db.table("t")
|
table = fresh_db["t"]
|
||||||
assert table.strict == expected_strict
|
assert table.strict == expected_strict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -370,54 +311,28 @@ 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",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_table_default_values(fresh_db, value):
|
def test_table_default_values(fresh_db, value):
|
||||||
fresh_db.table("default_values").insert(
|
fresh_db["default_values"].insert(
|
||||||
{"nodefault": 1, "value": value}, defaults={"value": value}
|
{"nodefault": 1, "value": value}, defaults={"value": value}
|
||||||
)
|
)
|
||||||
default_values = fresh_db.table("default_values").default_values
|
default_values = fresh_db["default_values"].default_values
|
||||||
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.table("t").schema
|
|
||||||
assert fresh_db.table("t").default_values == {"name": "O'Brien"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_table_default_values_keyword_literals(fresh_db):
|
|
||||||
fresh_db.execute(
|
|
||||||
"create table t ("
|
|
||||||
"enabled integer default TRUE, "
|
|
||||||
"disabled integer default false, "
|
|
||||||
"nullable text default NULL"
|
|
||||||
")"
|
|
||||||
)
|
|
||||||
assert fresh_db.table("t").default_values == {
|
|
||||||
"enabled": True,
|
|
||||||
"disabled": False,
|
|
||||||
"nullable": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
# resolve implicit foreign key references and compound pk lookups
|
# resolve implicit foreign key references and compound pk lookups
|
||||||
fresh_db.execute("create table t (b text, a text, primary key (a, b))")
|
fresh_db.execute("create table t (b text, a text, primary key (a, b))")
|
||||||
assert fresh_db.table("t").pks == ["a", "b"]
|
assert fresh_db["t"].pks == ["a", "b"]
|
||||||
|
|
||||||
|
|
||||||
def test_transform_preserves_compound_pk_declaration_order(fresh_db):
|
def test_transform_preserves_compound_pk_declaration_order(fresh_db):
|
||||||
fresh_db.execute("create table t (a text, b text, c text, primary key (b, a))")
|
fresh_db.execute("create table t (a text, b text, c text, primary key (b, a))")
|
||||||
fresh_db.table("t").transform(drop={"c"})
|
fresh_db["t"].transform(drop={"c"})
|
||||||
assert fresh_db.table("t").pks == ["b", "a"]
|
assert fresh_db["t"].pks == ["b", "a"]
|
||||||
assert 'PRIMARY KEY ("b", "a")' in fresh_db.table("t").schema
|
assert 'PRIMARY KEY ("b", "a")' in fresh_db["t"].schema
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,9 +18,9 @@ def test_insert_all_list_mode_basic():
|
||||||
yield [2, "Bob", 25]
|
yield [2, "Bob", 25]
|
||||||
yield [3, "Charlie", 35]
|
yield [3, "Charlie", 35]
|
||||||
|
|
||||||
db.table("people").insert_all(data_generator())
|
db["people"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("people").rows)
|
rows = list(db["people"].rows)
|
||||||
assert len(rows) == 3
|
assert len(rows) == 3
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
||||||
|
|
@ -37,10 +36,10 @@ def test_insert_all_list_mode_with_pk():
|
||||||
yield [1, "Alice", 95]
|
yield [1, "Alice", 95]
|
||||||
yield [2, "Bob", 87]
|
yield [2, "Bob", 87]
|
||||||
|
|
||||||
db.table("scores").insert_all(data_generator(), pk="id")
|
db["scores"].insert_all(data_generator(), pk="id")
|
||||||
|
|
||||||
assert db.table("scores").pks == ["id"]
|
assert db["scores"].pks == ["id"]
|
||||||
rows = list(db.table("scores").rows)
|
rows = list(db["scores"].rows)
|
||||||
assert len(rows) == 2
|
assert len(rows) == 2
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,7 +53,7 @@ def test_upsert_all_list_mode():
|
||||||
yield [1, "Alice", 100]
|
yield [1, "Alice", 100]
|
||||||
yield [2, "Bob", 200]
|
yield [2, "Bob", 200]
|
||||||
|
|
||||||
db.table("data").insert_all(initial_data(), pk="id")
|
db["data"].insert_all(initial_data(), pk="id")
|
||||||
|
|
||||||
# Upsert with some updates and new records
|
# Upsert with some updates and new records
|
||||||
def upsert_data():
|
def upsert_data():
|
||||||
|
|
@ -62,9 +61,9 @@ def test_upsert_all_list_mode():
|
||||||
yield [1, "Alice", 150] # Update existing
|
yield [1, "Alice", 150] # Update existing
|
||||||
yield [3, "Charlie", 300] # Insert new
|
yield [3, "Charlie", 300] # Insert new
|
||||||
|
|
||||||
db.table("data").upsert_all(upsert_data(), pk="id")
|
db["data"].upsert_all(upsert_data(), pk="id")
|
||||||
|
|
||||||
rows = list(db.table("data").rows_where(order_by="id"))
|
rows = list(db["data"].rows_where(order_by="id"))
|
||||||
assert len(rows) == 3
|
assert len(rows) == 3
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "value": 150}
|
assert rows[0] == {"id": 1, "name": "Alice", "value": 150}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "value": 200}
|
assert rows[1] == {"id": 2, "name": "Bob", "value": 200}
|
||||||
|
|
@ -81,9 +80,9 @@ def test_list_mode_with_various_types():
|
||||||
yield [2, "Bob", 87.3, False]
|
yield [2, "Bob", 87.3, False]
|
||||||
yield [3, "Charlie", None, True]
|
yield [3, "Charlie", None, True]
|
||||||
|
|
||||||
db.table("mixed").insert_all(data_generator())
|
db["mixed"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("mixed").rows)
|
rows = list(db["mixed"].rows)
|
||||||
assert len(rows) == 3
|
assert len(rows) == 3
|
||||||
assert rows[0]["score"] == 95.5
|
assert rows[0]["score"] == 95.5
|
||||||
assert rows[1]["active"] == 0 # SQLite stores boolean as int
|
assert rows[1]["active"] == 0 # SQLite stores boolean as int
|
||||||
|
|
@ -99,7 +98,7 @@ def test_list_mode_error_non_string_columns():
|
||||||
yield ["a", "b", "c"]
|
yield ["a", "b", "c"]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="must be a list of column name strings"):
|
with pytest.raises(ValueError, match="must be a list of column name strings"):
|
||||||
db.table("bad").insert_all(bad_data()) # type: ignore[arg-type]
|
db["bad"].insert_all(bad_data())
|
||||||
|
|
||||||
|
|
||||||
def test_list_mode_error_mixed_types():
|
def test_list_mode_error_mixed_types():
|
||||||
|
|
@ -111,7 +110,7 @@ def test_list_mode_error_mixed_types():
|
||||||
yield {"id": 1, "name": "Alice"} # Should be a list, not dict
|
yield {"id": 1, "name": "Alice"} # Should be a list, not dict
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="must also be lists"):
|
with pytest.raises(ValueError, match="must also be lists"):
|
||||||
db.table("bad").insert_all(bad_data()) # type: ignore[arg-type]
|
db["bad"].insert_all(bad_data())
|
||||||
|
|
||||||
|
|
||||||
def test_list_mode_empty_after_headers():
|
def test_list_mode_empty_after_headers():
|
||||||
|
|
@ -122,9 +121,9 @@ def test_list_mode_empty_after_headers():
|
||||||
yield ["id", "name", "age"]
|
yield ["id", "name", "age"]
|
||||||
# No data rows
|
# No data rows
|
||||||
|
|
||||||
result = db.table("people").insert_all(data_generator())
|
result = db["people"].insert_all(data_generator())
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert not db.table("people").exists()
|
assert not db["people"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_list_mode_batch_processing():
|
def test_list_mode_batch_processing():
|
||||||
|
|
@ -136,7 +135,7 @@ def test_list_mode_batch_processing():
|
||||||
for i in range(1000):
|
for i in range(1000):
|
||||||
yield [i, f"value_{i}"]
|
yield [i, f"value_{i}"]
|
||||||
|
|
||||||
db.table("large").insert_all(large_data(), batch_size=100)
|
db["large"].insert_all(large_data(), batch_size=100)
|
||||||
|
|
||||||
count = db.execute("SELECT COUNT(*) as c FROM large").fetchone()[0]
|
count = db.execute("SELECT COUNT(*) as c FROM large").fetchone()[0]
|
||||||
assert count == 1000
|
assert count == 1000
|
||||||
|
|
@ -152,9 +151,9 @@ def test_list_mode_shorter_rows():
|
||||||
yield [2, "Bob"] # Missing age and city
|
yield [2, "Bob"] # Missing age and city
|
||||||
yield [3, "Charlie", 35] # Missing city
|
yield [3, "Charlie", 35] # Missing city
|
||||||
|
|
||||||
db.table("people").insert_all(data_generator())
|
db["people"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("people").rows_where(order_by="id"))
|
rows = list(db["people"].rows_where(order_by="id"))
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "age": None, "city": None}
|
assert rows[1] == {"id": 2, "name": "Bob", "age": None, "city": None}
|
||||||
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35, "city": None}
|
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35, "city": None}
|
||||||
|
|
@ -170,9 +169,9 @@ def test_backwards_compatibility_dict_mode():
|
||||||
{"id": 2, "name": "Bob", "age": 25},
|
{"id": 2, "name": "Bob", "age": 25},
|
||||||
]
|
]
|
||||||
|
|
||||||
db.table("people").insert_all(data)
|
db["people"].insert_all(data)
|
||||||
|
|
||||||
rows = list(db.table("people").rows)
|
rows = list(db["people"].rows)
|
||||||
assert len(rows) == 2
|
assert len(rows) == 2
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
||||||
|
|
||||||
|
|
@ -189,9 +188,9 @@ def test_insert_all_tuple_mode_basic():
|
||||||
yield (2, "Bob", 25)
|
yield (2, "Bob", 25)
|
||||||
yield (3, "Charlie", 35)
|
yield (3, "Charlie", 35)
|
||||||
|
|
||||||
db.table("people").insert_all(data_generator())
|
db["people"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("people").rows)
|
rows = list(db["people"].rows)
|
||||||
assert len(rows) == 3
|
assert len(rows) == 3
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
||||||
|
|
@ -211,9 +210,9 @@ def test_insert_all_mixed_list_tuple():
|
||||||
yield [3, "Charlie", 35]
|
yield [3, "Charlie", 35]
|
||||||
yield (4, "Diana", 40)
|
yield (4, "Diana", 40)
|
||||||
|
|
||||||
db.table("people").insert_all(data_generator())
|
db["people"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("people").rows)
|
rows = list(db["people"].rows)
|
||||||
assert len(rows) == 4
|
assert len(rows) == 4
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
assert rows[1] == {"id": 2, "name": "Bob", "age": 25}
|
||||||
|
|
@ -231,7 +230,7 @@ def test_upsert_all_tuple_mode():
|
||||||
yield (1, "Alice", 100)
|
yield (1, "Alice", 100)
|
||||||
yield (2, "Bob", 200)
|
yield (2, "Bob", 200)
|
||||||
|
|
||||||
db.table("data").insert_all(initial_data(), pk="id")
|
db["data"].insert_all(initial_data(), pk="id")
|
||||||
|
|
||||||
# Upsert with tuples
|
# Upsert with tuples
|
||||||
def upsert_data():
|
def upsert_data():
|
||||||
|
|
@ -239,9 +238,9 @@ def test_upsert_all_tuple_mode():
|
||||||
yield (1, "Alice", 150) # Update existing
|
yield (1, "Alice", 150) # Update existing
|
||||||
yield (3, "Charlie", 300) # Insert new
|
yield (3, "Charlie", 300) # Insert new
|
||||||
|
|
||||||
db.table("data").upsert_all(upsert_data(), pk="id")
|
db["data"].upsert_all(upsert_data(), pk="id")
|
||||||
|
|
||||||
rows = list(db.table("data").rows_where(order_by="id"))
|
rows = list(db["data"].rows_where(order_by="id"))
|
||||||
assert len(rows) == 3
|
assert len(rows) == 3
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "value": 150}
|
assert rows[0] == {"id": 1, "name": "Alice", "value": 150}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "value": 200}
|
assert rows[1] == {"id": 2, "name": "Bob", "value": 200}
|
||||||
|
|
@ -258,9 +257,9 @@ def test_tuple_mode_shorter_rows():
|
||||||
yield 2, "Bob" # Missing age and city
|
yield 2, "Bob" # Missing age and city
|
||||||
yield 3, "Charlie", 35 # Missing city
|
yield 3, "Charlie", 35 # Missing city
|
||||||
|
|
||||||
db.table("people").insert_all(data_generator())
|
db["people"].insert_all(data_generator())
|
||||||
|
|
||||||
rows = list(db.table("people").rows_where(order_by="id"))
|
rows = list(db["people"].rows_where(order_by="id"))
|
||||||
assert rows[0] == {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}
|
assert rows[0] == {"id": 1, "name": "Alice", "age": 30, "city": "NYC"}
|
||||||
assert rows[1] == {"id": 2, "name": "Bob", "age": None, "city": None}
|
assert rows[1] == {"id": 2, "name": "Bob", "age": None, "city": None}
|
||||||
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35, "city": None}
|
assert rows[2] == {"id": 3, "name": "Charlie", "age": 35, "city": None}
|
||||||
|
|
@ -271,18 +270,18 @@ def test_list_mode_single_record_upsert_last_pk():
|
||||||
db = Database(memory=True)
|
db = Database(memory=True)
|
||||||
|
|
||||||
# Create table first
|
# Create table first
|
||||||
db.table("data").insert({"id": 1, "name": "Alice", "value": 100}, pk="id")
|
db["data"].insert({"id": 1, "name": "Alice", "value": 100}, pk="id")
|
||||||
|
|
||||||
# Now upsert a single record using list mode
|
# Now upsert a single record using list mode
|
||||||
def upsert_data():
|
def upsert_data():
|
||||||
yield ["id", "name", "value"]
|
yield ["id", "name", "value"]
|
||||||
yield [1, "Alice", 150] # Update existing
|
yield [1, "Alice", 150] # Update existing
|
||||||
|
|
||||||
table = db.table("data")
|
table = db["data"]
|
||||||
table.upsert_all(upsert_data(), pk="id")
|
table.upsert_all(upsert_data(), pk="id")
|
||||||
|
|
||||||
# Verify the data was updated
|
# Verify the data was updated
|
||||||
rows = list(db.table("data").rows)
|
rows = list(db["data"].rows)
|
||||||
assert rows == [{"id": 1, "name": "Alice", "value": 150}]
|
assert rows == [{"id": 1, "name": "Alice", "value": 150}]
|
||||||
|
|
||||||
# Verify last_pk is populated correctly
|
# Verify last_pk is populated correctly
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils.db import Index
|
from sqlite_utils.db import Index
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_new_table(fresh_db):
|
def test_lookup_new_table(fresh_db):
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
palm_id = species.lookup({"name": "Palm"})
|
palm_id = species.lookup({"name": "Palm"})
|
||||||
oak_id = species.lookup({"name": "Oak"})
|
oak_id = species.lookup({"name": "Oak"})
|
||||||
cherry_id = species.lookup({"name": "Cherry"})
|
cherry_id = species.lookup({"name": "Cherry"})
|
||||||
|
|
@ -26,7 +25,7 @@ def test_lookup_new_table(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_new_table_compound_key(fresh_db):
|
def test_lookup_new_table_compound_key(fresh_db):
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
palm_id = species.lookup({"name": "Palm", "type": "Tree"})
|
palm_id = species.lookup({"name": "Palm", "type": "Tree"})
|
||||||
oak_id = species.lookup({"name": "Oak", "type": "Tree"})
|
oak_id = species.lookup({"name": "Oak", "type": "Tree"})
|
||||||
assert palm_id == species.lookup({"name": "Palm", "type": "Tree"})
|
assert palm_id == species.lookup({"name": "Palm", "type": "Tree"})
|
||||||
|
|
@ -70,7 +69,7 @@ def test_lookup_fails_if_constraint_cannot_be_added(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_with_extra_values(fresh_db):
|
def test_lookup_with_extra_values(fresh_db):
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
id = species.lookup({"name": "Palm", "type": "Tree"}, {"first_seen": "2020-01-01"})
|
id = species.lookup({"name": "Palm", "type": "Tree"}, {"first_seen": "2020-01-01"})
|
||||||
assert species.get(id) == {
|
assert species.get(id) == {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -90,9 +89,9 @@ def test_lookup_with_extra_values(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_with_extra_insert_parameters(fresh_db):
|
def test_lookup_with_extra_insert_parameters(fresh_db):
|
||||||
other_table = fresh_db.table("other_table")
|
other_table = fresh_db["other_table"]
|
||||||
other_table.insert({"id": 1, "name": "Name"}, pk="id")
|
other_table.insert({"id": 1, "name": "Name"}, pk="id")
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
id = species.lookup(
|
id = species.lookup(
|
||||||
{"name": "Palm", "type": "Tree"},
|
{"name": "Palm", "type": "Tree"},
|
||||||
{
|
{
|
||||||
|
|
@ -156,15 +155,15 @@ def test_lookup_with_extra_insert_parameters(fresh_db):
|
||||||
|
|
||||||
@pytest.mark.parametrize("strict", (False, True))
|
@pytest.mark.parametrize("strict", (False, True))
|
||||||
def test_lookup_new_table_strict(fresh_db, strict):
|
def test_lookup_new_table_strict(fresh_db, strict):
|
||||||
fresh_db.table("species").lookup({"name": "Palm"}, strict=strict)
|
fresh_db["species"].lookup({"name": "Palm"}, strict=strict)
|
||||||
assert fresh_db.table("species").strict == strict or not fresh_db.supports_strict
|
assert fresh_db["species"].strict == strict or not fresh_db.supports_strict
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_null_value_idempotent(fresh_db):
|
def test_lookup_null_value_idempotent(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/186
|
# https://github.com/simonw/sqlite-utils/issues/186
|
||||||
# Repeated lookups of a null value should return the same row,
|
# Repeated lookups of a null value should return the same row,
|
||||||
# not insert a duplicate row each time
|
# not insert a duplicate row each time
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
first_id = species.lookup({"name": None})
|
first_id = species.lookup({"name": None})
|
||||||
second_id = species.lookup({"name": None})
|
second_id = species.lookup({"name": None})
|
||||||
assert first_id == second_id
|
assert first_id == second_id
|
||||||
|
|
@ -172,7 +171,7 @@ def test_lookup_null_value_idempotent(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_lookup_compound_key_with_null_idempotent(fresh_db):
|
def test_lookup_compound_key_with_null_idempotent(fresh_db):
|
||||||
species = fresh_db.table("species")
|
species = fresh_db["species"]
|
||||||
palm_id = species.lookup({"name": "Palm", "type": None})
|
palm_id = species.lookup({"name": "Palm", "type": None})
|
||||||
oak_id = species.lookup({"name": "Oak", "type": "Tree"})
|
oak_id = species.lookup({"name": "Oak", "type": "Tree"})
|
||||||
assert palm_id == species.lookup({"name": "Palm", "type": None})
|
assert palm_id == species.lookup({"name": "Palm", "type": None})
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,47 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils.db import ForeignKey, NoObviousTable
|
from sqlite_utils.db import ForeignKey, NoObviousTable
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_insert_m2m_single(fresh_db):
|
def test_insert_m2m_single(fresh_db):
|
||||||
dogs = fresh_db.table("dogs")
|
dogs = fresh_db["dogs"]
|
||||||
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
||||||
"humans", {"id": 1, "name": "Natalie D"}, pk="id"
|
"humans", {"id": 1, "name": "Natalie D"}, pk="id"
|
||||||
)
|
)
|
||||||
assert {"dogs_humans", "humans", "dogs"} == set(fresh_db.table_names())
|
assert {"dogs_humans", "humans", "dogs"} == set(fresh_db.table_names())
|
||||||
humans = fresh_db.table("humans")
|
humans = fresh_db["humans"]
|
||||||
dogs_humans = fresh_db.table("dogs_humans")
|
dogs_humans = fresh_db["dogs_humans"]
|
||||||
assert [{"id": 1, "name": "Natalie D"}] == list(humans.rows)
|
assert [{"id": 1, "name": "Natalie D"}] == list(humans.rows)
|
||||||
assert [{"humans_id": 1, "dogs_id": 1}] == list(dogs_humans.rows)
|
assert [{"humans_id": 1, "dogs_id": 1}] == list(dogs_humans.rows)
|
||||||
|
|
||||||
|
|
||||||
def test_insert_m2m_alter(fresh_db):
|
def test_insert_m2m_alter(fresh_db):
|
||||||
dogs = fresh_db.table("dogs")
|
dogs = fresh_db["dogs"]
|
||||||
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
||||||
"humans", {"id": 1, "name": "Natalie D"}, pk="id"
|
"humans", {"id": 1, "name": "Natalie D"}, pk="id"
|
||||||
)
|
)
|
||||||
dogs.update(1).m2m(
|
dogs.update(1).m2m(
|
||||||
"humans", {"id": 2, "name": "Simon W", "nerd": True}, pk="id", alter=True
|
"humans", {"id": 2, "name": "Simon W", "nerd": True}, pk="id", alter=True
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("humans").rows) == [
|
assert list(fresh_db["humans"].rows) == [
|
||||||
{"id": 1, "name": "Natalie D", "nerd": None},
|
{"id": 1, "name": "Natalie D", "nerd": None},
|
||||||
{"id": 2, "name": "Simon W", "nerd": 1},
|
{"id": 2, "name": "Simon W", "nerd": 1},
|
||||||
]
|
]
|
||||||
assert list(fresh_db.table("dogs_humans").rows) == [
|
assert list(fresh_db["dogs_humans"].rows) == [
|
||||||
{"humans_id": 1, "dogs_id": 1},
|
{"humans_id": 1, "dogs_id": 1},
|
||||||
{"humans_id": 2, "dogs_id": 1},
|
{"humans_id": 2, "dogs_id": 1},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_insert_m2m_list(fresh_db):
|
def test_insert_m2m_list(fresh_db):
|
||||||
dogs = fresh_db.table("dogs")
|
dogs = fresh_db["dogs"]
|
||||||
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
dogs.insert({"id": 1, "name": "Cleo"}, pk="id").m2m(
|
||||||
"humans",
|
"humans",
|
||||||
[{"id": 1, "name": "Natalie D"}, {"id": 2, "name": "Simon W"}],
|
[{"id": 1, "name": "Natalie D"}, {"id": 2, "name": "Simon W"}],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
assert {"dogs", "humans", "dogs_humans"} == set(fresh_db.table_names())
|
assert {"dogs", "humans", "dogs_humans"} == set(fresh_db.table_names())
|
||||||
humans = fresh_db.table("humans")
|
humans = fresh_db["humans"]
|
||||||
dogs_humans = fresh_db.table("dogs_humans")
|
dogs_humans = fresh_db["dogs_humans"]
|
||||||
assert [{"humans_id": 1, "dogs_id": 1}, {"humans_id": 2, "dogs_id": 1}] == list(
|
assert [{"humans_id": 1, "dogs_id": 1}, {"humans_id": 2, "dogs_id": 1}] == list(
|
||||||
dogs_humans.rows
|
dogs_humans.rows
|
||||||
)
|
)
|
||||||
|
|
@ -66,9 +65,10 @@ 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():
|
||||||
yield from iterable_records
|
for record in iterable_records:
|
||||||
|
yield record
|
||||||
|
|
||||||
platypuses = fresh_db.table("platypuses")
|
platypuses = fresh_db["platypuses"]
|
||||||
platypuses.insert({"id": 1, "name": "Perry"}, pk="id").m2m(
|
platypuses.insert({"id": 1, "name": "Perry"}, pk="id").m2m(
|
||||||
"humans",
|
"humans",
|
||||||
iterable(),
|
iterable(),
|
||||||
|
|
@ -76,8 +76,8 @@ def test_insert_m2m_iterable(fresh_db):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert {"platypuses", "humans", "humans_platypuses"} == set(fresh_db.table_names())
|
assert {"platypuses", "humans", "humans_platypuses"} == set(fresh_db.table_names())
|
||||||
humans = fresh_db.table("humans")
|
humans = fresh_db["humans"]
|
||||||
humans_platypuses = fresh_db.table("humans_platypuses")
|
humans_platypuses = fresh_db["humans_platypuses"]
|
||||||
assert [
|
assert [
|
||||||
{"humans_id": 1, "platypuses_id": 1},
|
{"humans_id": 1, "platypuses_id": 1},
|
||||||
{"humans_id": 2, "platypuses_id": 1},
|
{"humans_id": 2, "platypuses_id": 1},
|
||||||
|
|
@ -111,14 +111,14 @@ def test_m2m_with_table_objects(fresh_db):
|
||||||
assert expected_tables == set(fresh_db.table_names())
|
assert expected_tables == set(fresh_db.table_names())
|
||||||
assert dogs.count == 1
|
assert dogs.count == 1
|
||||||
assert humans.count == 2
|
assert humans.count == 2
|
||||||
assert fresh_db.table("dogs_humans").count == 2
|
assert fresh_db["dogs_humans"].count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_m2m_lookup(fresh_db):
|
def test_m2m_lookup(fresh_db):
|
||||||
people = fresh_db.table("people", pk="id")
|
people = fresh_db.table("people", pk="id")
|
||||||
people.insert({"name": "Wahyu"}).m2m("tags", lookup={"tag": "Coworker"})
|
people.insert({"name": "Wahyu"}).m2m("tags", lookup={"tag": "Coworker"})
|
||||||
people_tags = fresh_db.table("people_tags")
|
people_tags = fresh_db["people_tags"]
|
||||||
tags = fresh_db.table("tags")
|
tags = fresh_db["tags"]
|
||||||
assert people_tags.exists()
|
assert people_tags.exists()
|
||||||
assert tags.exists()
|
assert tags.exists()
|
||||||
assert [
|
assert [
|
||||||
|
|
@ -150,9 +150,9 @@ def test_m2m_explicit_table_name_argument(fresh_db):
|
||||||
people.insert({"name": "Wahyu"}).m2m(
|
people.insert({"name": "Wahyu"}).m2m(
|
||||||
"tags", lookup={"tag": "Coworker"}, m2m_table="tagged"
|
"tags", lookup={"tag": "Coworker"}, m2m_table="tagged"
|
||||||
)
|
)
|
||||||
assert fresh_db.table("tags").exists
|
assert fresh_db["tags"].exists
|
||||||
assert fresh_db.table("tagged").exists
|
assert fresh_db["tagged"].exists
|
||||||
assert not fresh_db.table("people_tags").exists()
|
assert not fresh_db["people_tags"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_m2m_table_candidates(fresh_db):
|
def test_m2m_table_candidates(fresh_db):
|
||||||
|
|
@ -181,25 +181,25 @@ def test_uses_existing_m2m_table_if_exists(fresh_db):
|
||||||
# Code should look for an existing table with fks to both tables
|
# Code should look for an existing table with fks to both tables
|
||||||
# and use that if it exists.
|
# and use that if it exists.
|
||||||
people = fresh_db.create_table("people", {"id": int, "name": str}, pk="id")
|
people = fresh_db.create_table("people", {"id": int, "name": str}, pk="id")
|
||||||
fresh_db.table("tags").lookup({"tag": "Coworker"})
|
fresh_db["tags"].lookup({"tag": "Coworker"})
|
||||||
fresh_db.create_table(
|
fresh_db.create_table(
|
||||||
"tagged",
|
"tagged",
|
||||||
{"people_id": int, "tags_id": int},
|
{"people_id": int, "tags_id": int},
|
||||||
foreign_keys=["people_id", "tags_id"],
|
foreign_keys=["people_id", "tags_id"],
|
||||||
)
|
)
|
||||||
people.insert({"name": "Wahyu"}).m2m("tags", lookup={"tag": "Coworker"})
|
people.insert({"name": "Wahyu"}).m2m("tags", lookup={"tag": "Coworker"})
|
||||||
assert fresh_db.table("tags").exists()
|
assert fresh_db["tags"].exists()
|
||||||
assert fresh_db.table("tagged").exists()
|
assert fresh_db["tagged"].exists()
|
||||||
assert not fresh_db.table("people_tags").exists()
|
assert not fresh_db["people_tags"].exists()
|
||||||
assert not fresh_db.table("tags_people").exists()
|
assert not fresh_db["tags_people"].exists()
|
||||||
assert [{"people_id": 1, "tags_id": 1}] == list(fresh_db.table("tagged").rows)
|
assert [{"people_id": 1, "tags_id": 1}] == list(fresh_db["tagged"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_requires_explicit_m2m_table_if_multiple_options(fresh_db):
|
def test_requires_explicit_m2m_table_if_multiple_options(fresh_db):
|
||||||
# If the code scans for m2m tables and finds more than one candidate
|
# If the code scans for m2m tables and finds more than one candidate
|
||||||
# it should require that the m2m_table=x argument is used
|
# it should require that the m2m_table=x argument is used
|
||||||
people = fresh_db.create_table("people", {"id": int, "name": str}, pk="id")
|
people = fresh_db.create_table("people", {"id": int, "name": str}, pk="id")
|
||||||
fresh_db.table("tags").lookup({"tag": "Coworker"})
|
fresh_db["tags"].lookup({"tag": "Coworker"})
|
||||||
fresh_db.create_table(
|
fresh_db.create_table(
|
||||||
"tagged",
|
"tagged",
|
||||||
{"people_id": int, "tags_id": int},
|
{"people_id": int, "tags_id": int},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sqlite_utils
|
import sqlite_utils
|
||||||
from sqlite_utils import Migrations
|
from sqlite_utils import Migrations
|
||||||
|
|
||||||
|
|
@ -10,11 +9,11 @@ def migrations():
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m001(db):
|
def m001(db):
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m002(db):
|
def m002(db):
|
||||||
db.table("cats").create({"name": str})
|
db["cats"].create({"name": str})
|
||||||
db.execute("insert into dogs (name) values ('Pancakes')")
|
db.execute("insert into dogs (name) values ('Pancakes')")
|
||||||
|
|
||||||
return migrations
|
return migrations
|
||||||
|
|
@ -28,11 +27,11 @@ def migrations_not_ordered_alphabetically():
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m002(db):
|
def m002(db):
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m001(db):
|
def m001(db):
|
||||||
db.table("cats").create({"name": str})
|
db["cats"].create({"name": str})
|
||||||
db.execute("insert into dogs (name) values ('Pancakes')")
|
db.execute("insert into dogs (name) values ('Pancakes')")
|
||||||
|
|
||||||
return migrations
|
return migrations
|
||||||
|
|
@ -44,7 +43,7 @@ def migrations2():
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m001(db):
|
def m001(db):
|
||||||
db.table("dogs2").insert({"name": "Cleo"})
|
db["dogs2"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
return migrations
|
return migrations
|
||||||
|
|
||||||
|
|
@ -96,7 +95,7 @@ def test_applied_at_is_a_string(migrations):
|
||||||
def test_failing_migration_rolls_back(migrations):
|
def test_failing_migration_rolls_back(migrations):
|
||||||
@migrations()
|
@migrations()
|
||||||
def m003(db):
|
def m003(db):
|
||||||
db.table("birds").create({"name": str})
|
db["birds"].create({"name": str})
|
||||||
db.execute("insert into dogs (name) values ('Dozer')")
|
db.execute("insert into dogs (name) values ('Dozer')")
|
||||||
raise ValueError("boom")
|
raise ValueError("boom")
|
||||||
|
|
||||||
|
|
@ -105,7 +104,7 @@ def test_failing_migration_rolls_back(migrations):
|
||||||
migrations.apply(db)
|
migrations.apply(db)
|
||||||
# m001 and m002 committed before the failure and stay applied
|
# m001 and m002 committed before the failure and stay applied
|
||||||
assert set(db.table_names()) == {"_sqlite_migrations", "dogs", "cats"}
|
assert set(db.table_names()) == {"_sqlite_migrations", "dogs", "cats"}
|
||||||
assert [r["name"] for r in db.table("dogs").rows] == ["Cleo", "Pancakes"]
|
assert [r["name"] for r in db["dogs"].rows] == ["Cleo", "Pancakes"]
|
||||||
assert [m.name for m in migrations.applied(db)] == ["m001", "m002"]
|
assert [m.name for m in migrations.applied(db)] == ["m001", "m002"]
|
||||||
# Everything m003 did was rolled back and it is still pending
|
# Everything m003 did was rolled back and it is still pending
|
||||||
assert [m.name for m in migrations.pending(db)] == ["m003"]
|
assert [m.name for m in migrations.pending(db)] == ["m003"]
|
||||||
|
|
@ -117,11 +116,11 @@ def test_rerun_after_failure_applies_each_migration_once():
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m001(db):
|
def m001(db):
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m002(db):
|
def m002(db):
|
||||||
db.table("dogs").insert({"name": "Pancakes"})
|
db["dogs"].insert({"name": "Pancakes"})
|
||||||
if state["fail"]:
|
if state["fail"]:
|
||||||
raise ValueError("boom")
|
raise ValueError("boom")
|
||||||
|
|
||||||
|
|
@ -131,7 +130,7 @@ def test_rerun_after_failure_applies_each_migration_once():
|
||||||
state["fail"] = False
|
state["fail"] = False
|
||||||
migrations.apply(db)
|
migrations.apply(db)
|
||||||
# m001 must not have been re-applied, m002 applied exactly once
|
# m001 must not have been re-applied, m002 applied exactly once
|
||||||
assert [r["name"] for r in db.table("dogs").rows] == ["Cleo", "Pancakes"]
|
assert [r["name"] for r in db["dogs"].rows] == ["Cleo", "Pancakes"]
|
||||||
assert [m.name for m in migrations.applied(db)] == ["m001", "m002"]
|
assert [m.name for m in migrations.applied(db)] == ["m001", "m002"]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -142,7 +141,7 @@ def test_non_transactional_migration_allows_vacuum(tmpdir):
|
||||||
|
|
||||||
@migrations()
|
@migrations()
|
||||||
def m001(db):
|
def m001(db):
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
|
|
||||||
@migrations(transactional=False)
|
@migrations(transactional=False)
|
||||||
def m002(db):
|
def m002(db):
|
||||||
|
|
@ -155,7 +154,8 @@ 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), db.atomic():
|
with pytest.raises(ZeroDivisionError):
|
||||||
|
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
|
||||||
|
|
@ -185,13 +185,11 @@ def test_apply_composes_inside_outer_transaction(migrations):
|
||||||
)
|
)
|
||||||
def test_upgrades_sqlite_migrations(migrations, create_table, pk):
|
def test_upgrades_sqlite_migrations(migrations, create_table, pk):
|
||||||
db = sqlite_utils.Database(memory=True)
|
db = sqlite_utils.Database(memory=True)
|
||||||
db.table("_sqlite_migrations").create(create_table, pk=pk)
|
db["_sqlite_migrations"].create(create_table, pk=pk)
|
||||||
assert db.table_names() == ["_sqlite_migrations"]
|
assert db.table_names() == ["_sqlite_migrations"]
|
||||||
assert db.table("_sqlite_migrations").pks == (
|
assert db["_sqlite_migrations"].pks == ([pk] if isinstance(pk, str) else list(pk))
|
||||||
[pk] if isinstance(pk, str) else list(pk)
|
|
||||||
)
|
|
||||||
migrations.apply(db)
|
migrations.apply(db)
|
||||||
assert db.table("_sqlite_migrations").pks == ["id"]
|
assert db["_sqlite_migrations"].pks == ["id"]
|
||||||
|
|
||||||
|
|
||||||
def test_pending_and_applied_are_read_only(migrations):
|
def test_pending_and_applied_are_read_only(migrations):
|
||||||
|
|
@ -229,7 +227,7 @@ def test_stop_before_applied_migration_errors(migrations):
|
||||||
assert "m001" in str(ex.value)
|
assert "m001" in str(ex.value)
|
||||||
assert "already been applied" in str(ex.value)
|
assert "already been applied" in str(ex.value)
|
||||||
# Nothing else was applied
|
# Nothing else was applied
|
||||||
assert not db.table("cats").exists()
|
assert not db["cats"].exists()
|
||||||
|
|
||||||
|
|
||||||
def test_stop_before_applied_migration_errors_before_any_apply(migrations):
|
def test_stop_before_applied_migration_errors_before_any_apply(migrations):
|
||||||
|
|
@ -240,9 +238,9 @@ def test_stop_before_applied_migration_errors_before_any_apply(migrations):
|
||||||
|
|
||||||
@only_second()
|
@only_second()
|
||||||
def m002(db):
|
def m002(db):
|
||||||
db.table("cats").create({"name": str})
|
db["cats"].create({"name": str})
|
||||||
|
|
||||||
only_second.apply(db) # m002 applied, m001 still pending
|
only_second.apply(db) # m002 applied, m001 still pending
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
migrations.apply(db, stop_before="m002")
|
migrations.apply(db, stop_before="m002")
|
||||||
assert not db.table("dogs").exists()
|
assert not db["dogs"].exists()
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils import Database
|
|
||||||
from sqlite_utils.utils import sqlite3
|
|
||||||
|
|
||||||
BASELINE_ROWS = [(1, "one"), (2, "two")]
|
|
||||||
|
|
||||||
|
|
||||||
def insert(table):
|
|
||||||
table.insert({"id": 3, "value": "three"}, pk="id")
|
|
||||||
|
|
||||||
|
|
||||||
def insert_all(table):
|
|
||||||
table.insert_all(
|
|
||||||
[
|
|
||||||
{"id": 3, "value": "three"},
|
|
||||||
{"id": 4, "value": "four"},
|
|
||||||
],
|
|
||||||
pk="id",
|
|
||||||
batch_size=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def upsert(table):
|
|
||||||
table.upsert({"id": 2, "value": "TWO"}, pk="id")
|
|
||||||
|
|
||||||
|
|
||||||
def upsert_all(table):
|
|
||||||
table.upsert_all(
|
|
||||||
[
|
|
||||||
{"id": 2, "value": "TWO"},
|
|
||||||
{"id": 3, "value": "three"},
|
|
||||||
],
|
|
||||||
pk="id",
|
|
||||||
batch_size=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update(table):
|
|
||||||
table.update(2, {"value": "TWO"})
|
|
||||||
|
|
||||||
|
|
||||||
def delete(table):
|
|
||||||
table.delete(2)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_where(table):
|
|
||||||
table.delete_where("id > ?", [1])
|
|
||||||
|
|
||||||
|
|
||||||
MUTATOR_CASES = (
|
|
||||||
pytest.param(
|
|
||||||
insert,
|
|
||||||
[(1, "one"), (2, "two"), (3, "three")],
|
|
||||||
id="insert",
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
insert_all,
|
|
||||||
[(1, "one"), (2, "two"), (3, "three"), (4, "four")],
|
|
||||||
id="insert_all",
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
upsert,
|
|
||||||
[(1, "one"), (2, "TWO")],
|
|
||||||
id="upsert",
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
upsert_all,
|
|
||||||
[(1, "one"), (2, "TWO"), (3, "three")],
|
|
||||||
id="upsert_all",
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
update,
|
|
||||||
[(1, "one"), (2, "TWO")],
|
|
||||||
id="update",
|
|
||||||
),
|
|
||||||
pytest.param(delete, [(1, "one")], id="delete"),
|
|
||||||
pytest.param(delete_where, [(1, "one")], id="delete_where"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RollbackTest(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def seed_database(path):
|
|
||||||
conn = sqlite3.connect(str(path))
|
|
||||||
try:
|
|
||||||
conn.execute("create table items (id integer primary key, value text)")
|
|
||||||
conn.executemany("insert into items values (?, ?)", BASELINE_ROWS)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
return Database(path)
|
|
||||||
|
|
||||||
|
|
||||||
def current_rows(db):
|
|
||||||
return db.conn.execute("select id, value from items order by id").fetchall()
|
|
||||||
|
|
||||||
|
|
||||||
def persisted_rows(path):
|
|
||||||
conn = sqlite3.connect(str(path))
|
|
||||||
try:
|
|
||||||
return conn.execute("select id, value from items order by id").fetchall()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mutate,expected_rows", MUTATOR_CASES)
|
|
||||||
def test_mutator_commits_by_default(tmp_path, mutate, expected_rows):
|
|
||||||
path = tmp_path / "default.db"
|
|
||||||
db = seed_database(path)
|
|
||||||
|
|
||||||
assert not db.conn.in_transaction
|
|
||||||
mutate(db.table("items"))
|
|
||||||
assert current_rows(db) == expected_rows
|
|
||||||
assert not db.conn.in_transaction
|
|
||||||
|
|
||||||
db.close()
|
|
||||||
assert persisted_rows(path) == expected_rows
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mutate,expected_rows", MUTATOR_CASES)
|
|
||||||
def test_mutator_commits_with_outer_atomic(tmp_path, mutate, expected_rows):
|
|
||||||
path = tmp_path / "atomic.db"
|
|
||||||
db = seed_database(path)
|
|
||||||
|
|
||||||
with db.atomic():
|
|
||||||
assert db.conn.in_transaction
|
|
||||||
mutate(db.table("items"))
|
|
||||||
assert current_rows(db) == expected_rows
|
|
||||||
assert db.conn.in_transaction
|
|
||||||
|
|
||||||
assert current_rows(db) == expected_rows
|
|
||||||
assert not db.conn.in_transaction
|
|
||||||
db.close()
|
|
||||||
assert persisted_rows(path) == expected_rows
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mutate,expected_rows", MUTATOR_CASES)
|
|
||||||
def test_mutator_rolls_back_outer_atomic(tmp_path, mutate, expected_rows):
|
|
||||||
path = tmp_path / "rollback.db"
|
|
||||||
db = seed_database(path)
|
|
||||||
|
|
||||||
with pytest.raises(RollbackTest), db.atomic():
|
|
||||||
mutate(db.table("items"))
|
|
||||||
assert current_rows(db) == expected_rows
|
|
||||||
assert db.conn.in_transaction
|
|
||||||
raise RollbackTest
|
|
||||||
|
|
||||||
assert current_rows(db) == BASELINE_ROWS
|
|
||||||
assert not db.conn.in_transaction
|
|
||||||
db.close()
|
|
||||||
assert persisted_rows(path) == BASELINE_ROWS
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import importlib
|
|
||||||
import sqlite3
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import click
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
import click
|
||||||
from sqlite_utils import Database, cli, hookimpl, plugins
|
import importlib
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
from sqlite_utils import cli, Database, hookimpl, plugins
|
||||||
|
|
||||||
|
|
||||||
def _supports_pragma_function_list():
|
def _supports_pragma_function_list():
|
||||||
|
|
@ -14,7 +11,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 sqlite3.DatabaseError:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import types
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import types
|
||||||
|
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
|
|
||||||
|
|
||||||
def test_query(fresh_db):
|
def test_query(fresh_db):
|
||||||
fresh_db.table("dogs").insert_all([{"name": "Cleo"}, {"name": "Pancakes"}])
|
fresh_db["dogs"].insert_all([{"name": "Cleo"}, {"name": "Pancakes"}])
|
||||||
results = fresh_db.query("select * from dogs order by name desc")
|
results = fresh_db.query("select * from dogs order by name desc")
|
||||||
assert isinstance(results, types.GeneratorType)
|
assert isinstance(results, types.GeneratorType)
|
||||||
assert list(results) == [{"name": "Pancakes"}, {"name": "Cleo"}]
|
assert list(results) == [{"name": "Pancakes"}, {"name": "Cleo"}]
|
||||||
|
|
@ -20,13 +19,13 @@ def test_query_executes_eagerly(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_query_rejects_statements_that_return_no_rows(fresh_db):
|
def test_query_rejects_statements_that_return_no_rows(fresh_db):
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
with pytest.raises(ValueError) as ex:
|
with pytest.raises(ValueError) as ex:
|
||||||
fresh_db.query("update dogs set name = 'Cleopaws'")
|
fresh_db.query("update dogs set name = 'Cleopaws'")
|
||||||
assert "execute()" in str(ex.value)
|
assert "execute()" in str(ex.value)
|
||||||
# The rejected update was rolled back, and no transaction is left open
|
# The rejected update was rolled back, and no transaction is left open
|
||||||
assert not fresh_db.conn.in_transaction
|
assert not fresh_db.conn.in_transaction
|
||||||
assert [row["name"] for row in fresh_db.table("dogs").rows] == ["Cleo"]
|
assert [row["name"] for row in fresh_db["dogs"].rows] == ["Cleo"]
|
||||||
|
|
||||||
|
|
||||||
def test_query_rejected_ddl_is_rolled_back(fresh_db):
|
def test_query_rejected_ddl_is_rolled_back(fresh_db):
|
||||||
|
|
@ -37,7 +36,7 @@ def test_query_rejected_ddl_is_rolled_back(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_query_rejected_write_inside_transaction_is_rolled_back(fresh_db):
|
def test_query_rejected_write_inside_transaction_is_rolled_back(fresh_db):
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
@ -45,7 +44,7 @@ def test_query_rejected_write_inside_transaction_is_rolled_back(fresh_db):
|
||||||
# The transaction is still open and the earlier insert is intact
|
# The transaction is still open and the earlier insert is intact
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.commit()
|
fresh_db.commit()
|
||||||
assert [row["name"] for row in fresh_db.table("dogs").rows] == ["Cleo", "Pancakes"]
|
assert [row["name"] for row in fresh_db["dogs"].rows] == ["Cleo", "Pancakes"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -77,7 +76,7 @@ def test_query_comment_prefixed_commit_does_not_commit_transaction(fresh_db):
|
||||||
# A COMMIT hidden behind a leading comment must not slip past the
|
# A COMMIT hidden behind a leading comment must not slip past the
|
||||||
# keyword check - previously it committed the caller's open
|
# keyword check - previously it committed the caller's open
|
||||||
# transaction before the ValueError was raised
|
# transaction before the ValueError was raised
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
@ -85,7 +84,7 @@ def test_query_comment_prefixed_commit_does_not_commit_transaction(fresh_db):
|
||||||
# The explicit transaction is still open and can still be rolled back
|
# The explicit transaction is still open and can still be rolled back
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [row["name"] for row in fresh_db.table("dogs").rows] == ["Cleo"]
|
assert [row["name"] for row in fresh_db["dogs"].rows] == ["Cleo"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("sql", ["; COMMIT", "\ufeffCOMMIT"])
|
@pytest.mark.parametrize("sql", ["; COMMIT", "\ufeffCOMMIT"])
|
||||||
|
|
@ -94,7 +93,7 @@ def test_query_prefixed_commit_does_not_commit_transaction(fresh_db, sql):
|
||||||
# real token, so the keyword scanner must skip them too - previously
|
# real token, so the keyword scanner must skip them too - previously
|
||||||
# '; COMMIT' slipped past the check and committed the caller's open
|
# '; COMMIT' slipped past the check and committed the caller's open
|
||||||
# transaction before raising OperationalError
|
# transaction before raising OperationalError
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
fresh_db.execute("insert into dogs (name) values ('Pancakes')")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
|
@ -102,7 +101,7 @@ def test_query_prefixed_commit_does_not_commit_transaction(fresh_db, sql):
|
||||||
# The explicit transaction is still open and can still be rolled back
|
# The explicit transaction is still open and can still be rolled back
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [row["name"] for row in fresh_db.table("dogs").rows] == ["Cleo"]
|
assert [row["name"] for row in fresh_db["dogs"].rows] == ["Cleo"]
|
||||||
|
|
||||||
|
|
||||||
def test_query_error_leaves_no_transaction_open(fresh_db):
|
def test_query_error_leaves_no_transaction_open(fresh_db):
|
||||||
|
|
@ -190,12 +189,12 @@ def test_first_keyword(sql, expected):
|
||||||
reason="RETURNING requires SQLite 3.35.0 or higher",
|
reason="RETURNING requires SQLite 3.35.0 or higher",
|
||||||
)
|
)
|
||||||
def test_query_insert_returning(fresh_db):
|
def test_query_insert_returning(fresh_db):
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
rows = list(
|
rows = list(
|
||||||
fresh_db.query("insert into dogs (name) values ('Pancakes') returning name")
|
fresh_db.query("insert into dogs (name) values ('Pancakes') returning name")
|
||||||
)
|
)
|
||||||
assert rows == [{"name": "Pancakes"}]
|
assert rows == [{"name": "Pancakes"}]
|
||||||
assert fresh_db.table("dogs").count == 2
|
assert fresh_db["dogs"].count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
@ -207,7 +206,7 @@ def test_query_insert_returning_commits_without_iteration(tmpdir):
|
||||||
|
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
# Never iterate over the results
|
# Never iterate over the results
|
||||||
db.query("insert into dogs (name) values ('Pancakes') returning name")
|
db.query("insert into dogs (name) values ('Pancakes') returning name")
|
||||||
assert not db.conn.in_transaction
|
assert not db.conn.in_transaction
|
||||||
|
|
@ -227,7 +226,7 @@ def test_query_insert_returning_partial_iteration_still_commits(tmpdir):
|
||||||
|
|
||||||
path = str(tmpdir / "test.db")
|
path = str(tmpdir / "test.db")
|
||||||
db = Database(path)
|
db = Database(path)
|
||||||
db.table("dogs").insert({"name": "Cleo"})
|
db["dogs"].insert({"name": "Cleo"})
|
||||||
row = next(
|
row = next(
|
||||||
db.query(
|
db.query(
|
||||||
"insert into dogs (name) values ('Pancakes'), ('Marnie') returning name"
|
"insert into dogs (name) values ('Pancakes'), ('Marnie') returning name"
|
||||||
|
|
@ -246,7 +245,7 @@ def test_query_insert_returning_partial_iteration_still_commits(tmpdir):
|
||||||
reason="RETURNING requires SQLite 3.35.0 or higher",
|
reason="RETURNING requires SQLite 3.35.0 or higher",
|
||||||
)
|
)
|
||||||
def test_query_insert_returning_respects_explicit_transaction(fresh_db):
|
def test_query_insert_returning_respects_explicit_transaction(fresh_db):
|
||||||
fresh_db.table("dogs").insert({"name": "Cleo"})
|
fresh_db["dogs"].insert({"name": "Cleo"})
|
||||||
fresh_db.begin()
|
fresh_db.begin()
|
||||||
rows = list(
|
rows = list(
|
||||||
fresh_db.query("insert into dogs (name) values ('Pancakes') returning name")
|
fresh_db.query("insert into dogs (name) values ('Pancakes') returning name")
|
||||||
|
|
@ -255,13 +254,13 @@ def test_query_insert_returning_respects_explicit_transaction(fresh_db):
|
||||||
# Still inside the explicit transaction - not committed
|
# Still inside the explicit transaction - not committed
|
||||||
assert fresh_db.conn.in_transaction
|
assert fresh_db.conn.in_transaction
|
||||||
fresh_db.rollback()
|
fresh_db.rollback()
|
||||||
assert [row["name"] for row in fresh_db.table("dogs").rows] == ["Cleo"]
|
assert [row["name"] for row in fresh_db["dogs"].rows] == ["Cleo"]
|
||||||
|
|
||||||
|
|
||||||
def test_query_duplicate_column_names_are_deduped(fresh_db):
|
def test_query_duplicate_column_names_are_deduped(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/624
|
# https://github.com/simonw/sqlite-utils/issues/624
|
||||||
fresh_db.table("one").insert({"id": 1, "value": "left"})
|
fresh_db["one"].insert({"id": 1, "value": "left"})
|
||||||
fresh_db.table("two").insert({"id": 2, "value": "right"})
|
fresh_db["two"].insert({"id": 2, "value": "right"})
|
||||||
rows = list(
|
rows = list(
|
||||||
fresh_db.query("select one.id, two.id, one.value, two.value from one, two")
|
fresh_db.query("select one.id, two.id, one.value, two.value from one, two")
|
||||||
)
|
)
|
||||||
|
|
@ -277,7 +276,7 @@ def test_query_deduped_column_avoids_existing_names(fresh_db):
|
||||||
def test_execute_returning_dicts(fresh_db):
|
def test_execute_returning_dicts(fresh_db):
|
||||||
# Like db.query() but returns a list, included for backwards compatibility
|
# Like db.query() but returns a list, included for backwards compatibility
|
||||||
# see https://github.com/simonw/sqlite-utils/issues/290
|
# see https://github.com/simonw/sqlite-utils/issues/290
|
||||||
fresh_db.table("test").insert({"id": 1, "bar": 2}, pk="id")
|
fresh_db["test"].insert({"id": 1, "bar": 2}, pk="id")
|
||||||
assert fresh_db.execute_returning_dicts("select * from test") == [
|
assert fresh_db.execute_returning_dicts("select * from test") == [
|
||||||
{"id": 1, "bar": 2}
|
{"id": 1, "bar": 2}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
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
|
||||||
def dates_db(fresh_db):
|
def dates_db(fresh_db):
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "dt": "5th October 2019 12:04"},
|
{"id": 1, "dt": "5th October 2019 12:04"},
|
||||||
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
{"id": 2, "dt": "6th October 2019 00:05:06"},
|
||||||
|
|
@ -21,8 +19,8 @@ def dates_db(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_parsedate(dates_db):
|
def test_parsedate(dates_db):
|
||||||
dates_db.table("example").convert("dt", recipes.parsedate)
|
dates_db["example"].convert("dt", recipes.parsedate)
|
||||||
assert list(dates_db.table("example").rows) == [
|
assert list(dates_db["example"].rows) == [
|
||||||
{"id": 1, "dt": "2019-10-05"},
|
{"id": 1, "dt": "2019-10-05"},
|
||||||
{"id": 2, "dt": "2019-10-06"},
|
{"id": 2, "dt": "2019-10-06"},
|
||||||
{"id": 3, "dt": ""},
|
{"id": 3, "dt": ""},
|
||||||
|
|
@ -31,8 +29,8 @@ def test_parsedate(dates_db):
|
||||||
|
|
||||||
|
|
||||||
def test_parsedatetime(dates_db):
|
def test_parsedatetime(dates_db):
|
||||||
dates_db.table("example").convert("dt", recipes.parsedatetime)
|
dates_db["example"].convert("dt", recipes.parsedatetime)
|
||||||
assert list(dates_db.table("example").rows) == [
|
assert list(dates_db["example"].rows) == [
|
||||||
{"id": 1, "dt": "2019-10-05T12:04:00"},
|
{"id": 1, "dt": "2019-10-05T12:04:00"},
|
||||||
{"id": 2, "dt": "2019-10-06T00:05:06"},
|
{"id": 2, "dt": "2019-10-06T00:05:06"},
|
||||||
{"id": 3, "dt": ""},
|
{"id": 3, "dt": ""},
|
||||||
|
|
@ -50,16 +48,16 @@ def test_parsedatetime(dates_db):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "dt": "03/04/05"},
|
{"id": 1, "dt": "03/04/05"},
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("example").convert(
|
fresh_db["example"].convert(
|
||||||
"dt", lambda value: getattr(recipes, recipe)(value, **kwargs)
|
"dt", lambda value: getattr(recipes, recipe)(value, **kwargs)
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("example").rows) == [
|
assert list(fresh_db["example"].rows) == [
|
||||||
{"id": 1, "dt": expected},
|
{"id": 1, "dt": expected},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -68,7 +66,7 @@ def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
||||||
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
||||||
def test_dateparse_errors_raises(fresh_db, fn):
|
def test_dateparse_errors_raises(fresh_db, fn):
|
||||||
"""Test that invalid dates raise errors when errors=None"""
|
"""Test that invalid dates raise errors when errors=None"""
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "dt": "invalid"},
|
{"id": 1, "dt": "invalid"},
|
||||||
],
|
],
|
||||||
|
|
@ -76,32 +74,30 @@ def test_dateparse_errors_raises(fresh_db, fn):
|
||||||
)
|
)
|
||||||
# Exception in SQLite callback surfaces as OperationalError
|
# Exception in SQLite callback surfaces as OperationalError
|
||||||
with pytest.raises(sqlite3.OperationalError):
|
with pytest.raises(sqlite3.OperationalError):
|
||||||
fresh_db.table("example").convert(
|
fresh_db["example"].convert("dt", lambda value: getattr(recipes, fn)(value))
|
||||||
"dt", lambda value: getattr(recipes, fn)(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
||||||
@pytest.mark.parametrize("errors", (recipes.SET_NULL, recipes.IGNORE))
|
@pytest.mark.parametrize("errors", (recipes.SET_NULL, recipes.IGNORE))
|
||||||
def test_dateparse_errors_handled(fresh_db, fn, errors):
|
def test_dateparse_errors_handled(fresh_db, fn, errors):
|
||||||
"""Test error handling modes for invalid dates"""
|
"""Test error handling modes for invalid dates"""
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "dt": "invalid"},
|
{"id": 1, "dt": "invalid"},
|
||||||
],
|
],
|
||||||
pk="id",
|
pk="id",
|
||||||
)
|
)
|
||||||
fresh_db.table("example").convert(
|
fresh_db["example"].convert(
|
||||||
"dt", lambda value: getattr(recipes, fn)(value, errors=errors)
|
"dt", lambda value: getattr(recipes, fn)(value, errors=errors)
|
||||||
)
|
)
|
||||||
rows = list(fresh_db.table("example").rows)
|
rows = list(fresh_db["example"].rows)
|
||||||
expected = [{"id": 1, "dt": None if errors is recipes.SET_NULL else "invalid"}]
|
expected = [{"id": 1, "dt": None if errors is recipes.SET_NULL else "invalid"}]
|
||||||
assert rows == expected
|
assert rows == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("delimiter", [None, ";", "-"])
|
@pytest.mark.parametrize("delimiter", [None, ";", "-"])
|
||||||
def test_jsonsplit(fresh_db, delimiter):
|
def test_jsonsplit(fresh_db, delimiter):
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "tags": (delimiter or ",").join(["foo", "bar"])},
|
{"id": 1, "tags": (delimiter or ",").join(["foo", "bar"])},
|
||||||
{"id": 2, "tags": (delimiter or ",").join(["bar", "baz"])},
|
{"id": 2, "tags": (delimiter or ",").join(["bar", "baz"])},
|
||||||
|
|
@ -116,8 +112,8 @@ def test_jsonsplit(fresh_db, delimiter):
|
||||||
else:
|
else:
|
||||||
fn = recipes.jsonsplit
|
fn = recipes.jsonsplit
|
||||||
|
|
||||||
fresh_db.table("example").convert("tags", fn)
|
fresh_db["example"].convert("tags", fn)
|
||||||
assert list(fresh_db.table("example").rows) == [
|
assert list(fresh_db["example"].rows) == [
|
||||||
{"id": 1, "tags": '["foo", "bar"]'},
|
{"id": 1, "tags": '["foo", "bar"]'},
|
||||||
{"id": 2, "tags": '["bar", "baz"]'},
|
{"id": 2, "tags": '["bar", "baz"]'},
|
||||||
]
|
]
|
||||||
|
|
@ -132,7 +128,7 @@ def test_jsonsplit(fresh_db, delimiter):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_jsonsplit_type(fresh_db, type, expected):
|
def test_jsonsplit_type(fresh_db, type, expected):
|
||||||
fresh_db.table("example").insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "records": "1,2,3"},
|
{"id": 1, "records": "1,2,3"},
|
||||||
],
|
],
|
||||||
|
|
@ -146,5 +142,5 @@ def test_jsonsplit_type(fresh_db, type, expected):
|
||||||
else:
|
else:
|
||||||
fn = recipes.jsonsplit
|
fn = recipes.jsonsplit
|
||||||
|
|
||||||
fresh_db.table("example").convert("records", fn)
|
fresh_db["example"].convert("records", fn)
|
||||||
assert json.loads(fresh_db.table("example").get(1)["records"]) == expected
|
assert json.loads(fresh_db["example"].get(1)["records"]) == expected
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import pathlib
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils import Database
|
from sqlite_utils import Database
|
||||||
|
import sqlite3
|
||||||
|
import pathlib
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_recreate_ignored_for_in_memory():
|
def test_recreate_ignored_for_in_memory():
|
||||||
|
|
@ -33,8 +31,8 @@ def test_recreate(tmp_path, use_path, create_file_first):
|
||||||
filepath = pathlib.Path(filepath)
|
filepath = pathlib.Path(filepath)
|
||||||
if create_file_first:
|
if create_file_first:
|
||||||
db = Database(filepath)
|
db = Database(filepath)
|
||||||
db.table("t1").insert({"foo": "bar"})
|
db["t1"].insert({"foo": "bar"})
|
||||||
assert ["t1"] == db.table_names()
|
assert ["t1"] == db.table_names()
|
||||||
db.close()
|
db.close()
|
||||||
Database(filepath, recreate=True).table("t2").insert({"foo": "bar"})
|
Database(filepath, recreate=True)["t2"].insert({"foo": "bar"})
|
||||||
assert ["t2"] == Database(filepath).table_names()
|
assert ["t2"] == Database(filepath).table_names()
|
||||||
|
|
|
||||||
|
|
@ -86,21 +86,21 @@ def test_register_function_deterministic_tries_again_if_exception_raised(fresh_d
|
||||||
|
|
||||||
def test_register_function_replace(fresh_db):
|
def test_register_function_replace(fresh_db):
|
||||||
@fresh_db.register_function()
|
@fresh_db.register_function()
|
||||||
def one(): # pyright: ignore[reportRedeclaration]
|
def one():
|
||||||
return "one"
|
return "one"
|
||||||
|
|
||||||
assert "one" == fresh_db.execute("select one()").fetchone()[0]
|
assert "one" == fresh_db.execute("select one()").fetchone()[0]
|
||||||
|
|
||||||
# This will silently fail to replaec the function
|
# This will silently fail to replaec the function
|
||||||
@fresh_db.register_function()
|
@fresh_db.register_function()
|
||||||
def one(): # pyright: ignore[reportRedeclaration]
|
def one(): # noqa
|
||||||
return "two"
|
return "two"
|
||||||
|
|
||||||
assert "one" == fresh_db.execute("select one()").fetchone()[0]
|
assert "one" == fresh_db.execute("select one()").fetchone()[0]
|
||||||
|
|
||||||
# This will replace it
|
# This will replace it
|
||||||
@fresh_db.register_function(replace=True)
|
@fresh_db.register_function(replace=True)
|
||||||
def one(): # pyright: ignore[reportRedeclaration]
|
def one(): # noqa
|
||||||
return "two"
|
return "two"
|
||||||
|
|
||||||
assert "two" == fresh_db.execute("select one()").fetchone()[0]
|
assert "two" == fresh_db.execute("select one()").fetchone()[0]
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import pytest
|
||||||
|
|
||||||
def test_rows(existing_db):
|
def test_rows(existing_db):
|
||||||
assert [{"text": "one"}, {"text": "two"}, {"text": "three"}] == list(
|
assert [{"text": "one"}, {"text": "two"}, {"text": "three"}] == list(
|
||||||
existing_db.table("foo").rows
|
existing_db["foo"].rows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ def test_rows(existing_db):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rows_where(where, where_args, expected_ids, fresh_db):
|
def test_rows_where(where, where_args, expected_ids, fresh_db):
|
||||||
table = fresh_db.table("dogs")
|
table = fresh_db["dogs"]
|
||||||
table.insert_all(
|
table.insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "name": "Cleo", "age": 4, "is_good": True},
|
{"id": 1, "name": "Cleo", "age": 4, "is_good": True},
|
||||||
|
|
@ -41,7 +41,7 @@ def test_rows_where(where, where_args, expected_ids, fresh_db):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rows_where_order_by(where, order_by, expected_ids, fresh_db):
|
def test_rows_where_order_by(where, order_by, expected_ids, fresh_db):
|
||||||
table = fresh_db.table("dogs")
|
table = fresh_db["dogs"]
|
||||||
table.insert_all(
|
table.insert_all(
|
||||||
[
|
[
|
||||||
{"id": 1, "name": "Cleo", "age": 4},
|
{"id": 1, "name": "Cleo", "age": 4},
|
||||||
|
|
@ -59,13 +59,10 @@ def test_rows_where_order_by(where, order_by, expected_ids, fresh_db):
|
||||||
(None, 3, [1, 2, 3]),
|
(None, 3, [1, 2, 3]),
|
||||||
(0, 3, [1, 2, 3]),
|
(0, 3, [1, 2, 3]),
|
||||||
(3, 3, [4, 5, 6]),
|
(3, 3, [4, 5, 6]),
|
||||||
# offset without limit should return every remaining row
|
|
||||||
(97, None, [98, 99, 100]),
|
|
||||||
(0, None, list(range(1, 101))),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rows_where_offset_limit(fresh_db, offset, limit, expected):
|
def test_rows_where_offset_limit(fresh_db, offset, limit, expected):
|
||||||
table = fresh_db.table("rows")
|
table = fresh_db["rows"]
|
||||||
table.insert_all([{"id": id} for id in range(1, 101)], pk="id")
|
table.insert_all([{"id": id} for id in range(1, 101)], pk="id")
|
||||||
assert table.count == 100
|
assert table.count == 100
|
||||||
assert expected == [
|
assert expected == [
|
||||||
|
|
@ -73,14 +70,8 @@ def test_rows_where_offset_limit(fresh_db, offset, limit, expected):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_pks_and_rows_where_offset_without_limit(fresh_db):
|
|
||||||
table = fresh_db.table("rows")
|
|
||||||
table.insert_all([{"id": id} for id in range(1, 6)], pk="id")
|
|
||||||
assert [pk for pk, _ in table.pks_and_rows_where(offset=3, order_by="id")] == [4, 5]
|
|
||||||
|
|
||||||
|
|
||||||
def test_pks_and_rows_where_rowid(fresh_db):
|
def test_pks_and_rows_where_rowid(fresh_db):
|
||||||
table = fresh_db.table("rowid_table")
|
table = fresh_db["rowid_table"]
|
||||||
table.insert_all({"number": i + 10} for i in range(3))
|
table.insert_all({"number": i + 10} for i in range(3))
|
||||||
pks_and_rows = list(table.pks_and_rows_where())
|
pks_and_rows = list(table.pks_and_rows_where())
|
||||||
assert pks_and_rows == [
|
assert pks_and_rows == [
|
||||||
|
|
@ -91,7 +82,7 @@ def test_pks_and_rows_where_rowid(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_pks_and_rows_where_simple_pk(fresh_db):
|
def test_pks_and_rows_where_simple_pk(fresh_db):
|
||||||
table = fresh_db.table("simple_pk_table")
|
table = fresh_db["simple_pk_table"]
|
||||||
table.insert_all(({"id": i + 10} for i in range(3)), pk="id")
|
table.insert_all(({"id": i + 10} for i in range(3)), pk="id")
|
||||||
pks_and_rows = list(table.pks_and_rows_where())
|
pks_and_rows = list(table.pks_and_rows_where())
|
||||||
assert pks_and_rows == [
|
assert pks_and_rows == [
|
||||||
|
|
@ -102,7 +93,7 @@ def test_pks_and_rows_where_simple_pk(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_pks_and_rows_where_compound_pk(fresh_db):
|
def test_pks_and_rows_where_compound_pk(fresh_db):
|
||||||
table = fresh_db.table("compound_pk_table")
|
table = fresh_db["compound_pk_table"]
|
||||||
table.insert_all(
|
table.insert_all(
|
||||||
({"type": "number", "number": i, "plusone": i + 1} for i in range(3)),
|
({"type": "number", "number": i, "plusone": i + 1} for i in range(3)),
|
||||||
pk=("type", "number"),
|
pk=("type", "number"),
|
||||||
|
|
@ -117,8 +108,8 @@ def test_pks_and_rows_where_compound_pk(fresh_db):
|
||||||
|
|
||||||
def test_rows_where_duplicate_select_columns_are_deduped(fresh_db):
|
def test_rows_where_duplicate_select_columns_are_deduped(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/624
|
# https://github.com/simonw/sqlite-utils/issues/624
|
||||||
fresh_db.table("t").insert({"id": 1, "name": "Cleo"})
|
fresh_db["t"].insert({"id": 1, "name": "Cleo"})
|
||||||
rows = list(fresh_db.table("t").rows_where(select="id, id, name"))
|
rows = list(fresh_db["t"].rows_where(select="id, id, name"))
|
||||||
assert rows == [{"id": 1, "id_2": 1, "name": "Cleo"}]
|
assert rows == [{"id": 1, "id_2": 1, "name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,10 +121,10 @@ def test_pks_and_rows_where_view(fresh_db):
|
||||||
# an AttributeError from View lacking Table-only properties
|
# an AttributeError from View lacking Table-only properties
|
||||||
from sqlite_utils.utils import sqlite3
|
from sqlite_utils.utils import sqlite3
|
||||||
|
|
||||||
fresh_db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
|
fresh_db["dogs"].insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
fresh_db.create_view("dog_names", "select name from dogs")
|
fresh_db.create_view("dog_names", "select name from dogs")
|
||||||
try:
|
try:
|
||||||
result = list(fresh_db.view("dog_names").pks_and_rows_where())
|
result = list(fresh_db["dog_names"].pks_and_rows_where())
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # SQLite 3.36+: no such column: rowid
|
pass # SQLite 3.36+: no such column: rowid
|
||||||
else:
|
else:
|
||||||
|
|
@ -144,6 +135,6 @@ def test_pks_and_rows_where_view(fresh_db):
|
||||||
def test_pks_and_rows_where_compound_pk_declaration_order(fresh_db):
|
def test_pks_and_rows_where_compound_pk_declaration_order(fresh_db):
|
||||||
# Compound pks are returned in PRIMARY KEY declaration order
|
# Compound pks are returned in PRIMARY KEY declaration order
|
||||||
fresh_db.execute("create table t (b text, a text, primary key (a, b))")
|
fresh_db.execute("create table t (b text, a text, primary key (a, b))")
|
||||||
fresh_db.table("t").insert({"a": "A", "b": "B"})
|
fresh_db["t"].insert({"a": "A", "b": "B"})
|
||||||
pks_and_rows = list(fresh_db.table("t").pks_and_rows_where())
|
pks_and_rows = list(fresh_db["t"].pks_and_rows_where())
|
||||||
assert pks_and_rows == [(("A", "B"), {"b": "B", "a": "A"})]
|
assert pks_and_rows == [(("A", "B"), {"b": "B", "a": "A"})]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
|
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",
|
||||||
|
|
@ -20,13 +18,6 @@ def test_rows_from_file_detect_format(input, expected_format):
|
||||||
assert rows_list == [{"id": "1", "name": "Cleo"}]
|
assert rows_list == [{"id": "1", "name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input", (b"", b" \n\t"))
|
|
||||||
def test_rows_from_file_empty_input(input):
|
|
||||||
rows, format = rows_from_file(BytesIO(input))
|
|
||||||
assert format == Format.CSV
|
|
||||||
assert list(rows) == []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"ignore_extras,extras_key,expected",
|
"ignore_extras,extras_key,expected",
|
||||||
(
|
(
|
||||||
|
|
@ -38,7 +29,7 @@ def test_rows_from_file_empty_input(input):
|
||||||
)
|
)
|
||||||
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,9 +1,7 @@
|
||||||
import pathlib
|
from sqlite_utils import cli, Database
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
import pathlib
|
||||||
from sqlite_utils import Database, cli
|
import pytest
|
||||||
|
|
||||||
sniff_dir = pathlib.Path(__file__).parent / "sniff"
|
sniff_dir = pathlib.Path(__file__).parent / "sniff"
|
||||||
|
|
||||||
|
|
@ -19,7 +17,7 @@ def test_sniff(tmpdir, filepath):
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0, result.stdout
|
assert result.exit_code == 0, result.stdout
|
||||||
db = Database(db_path)
|
db = Database(db_path)
|
||||||
assert list(db.table("creatures").rows) == [
|
assert list(db["creatures"].rows) == [
|
||||||
{"id": "1", "species": "dog", "name": "Cleo", "age": "5"},
|
{"id": "1", "species": "dog", "name": "Cleo", "age": "5"},
|
||||||
{"id": "2", "species": "dog", "name": "Pancakes", "age": "4"},
|
{"id": "2", "species": "dog", "name": "Pancakes", "age": "4"},
|
||||||
{"id": "3", "species": "cat", "name": "Mozie", "age": "8"},
|
{"id": "3", "species": "cat", "name": "Mozie", "age": "8"},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from collections import OrderedDict
|
||||||
from sqlite_utils.utils import suggest_column_types
|
from sqlite_utils.utils import suggest_column_types
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ def test_with_tracer():
|
||||||
|
|
||||||
assert len(collected) == 4
|
assert len(collected) == 4
|
||||||
assert collected == [
|
assert collected == [
|
||||||
(
|
|
||||||
(
|
(
|
||||||
"SELECT name FROM sqlite_master\n"
|
"SELECT name FROM sqlite_master\n"
|
||||||
" WHERE rootpage = 0\n"
|
" WHERE rootpage = 0\n"
|
||||||
|
|
@ -63,8 +62,7 @@ def test_with_tracer():
|
||||||
" tbl_name = :table\n"
|
" tbl_name = :table\n"
|
||||||
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
|
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
|
||||||
" )\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"%',
|
||||||
|
|
@ -73,7 +71,6 @@ 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'
|
'with "original" as (\n'
|
||||||
" select\n"
|
" select\n"
|
||||||
|
|
@ -89,8 +86,7 @@ def test_with_tracer():
|
||||||
"where\n"
|
"where\n"
|
||||||
' "dogs_fts" match :query\n'
|
' "dogs_fts" match :query\n'
|
||||||
"order by\n"
|
"order by\n"
|
||||||
' "dogs_fts".rank'
|
' "dogs_fts".rank',
|
||||||
),
|
|
||||||
{"query": "Cleopaws"},
|
{"query": "Cleopaws"},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,14 +7,14 @@ from sqlite_utils.db import NotFoundError
|
||||||
|
|
||||||
|
|
||||||
def test_update_rowid_table(fresh_db):
|
def test_update_rowid_table(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
rowid = table.insert({"foo": "bar"}).last_pk
|
rowid = table.insert({"foo": "bar"}).last_pk
|
||||||
table.update(rowid, {"foo": "baz"})
|
table.update(rowid, {"foo": "baz"})
|
||||||
assert [{"foo": "baz"}] == list(table.rows)
|
assert [{"foo": "baz"}] == list(table.rows)
|
||||||
|
|
||||||
|
|
||||||
def test_update_pk_table(fresh_db):
|
def test_update_pk_table(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
pk = table.insert({"foo": "bar", "id": 5}, pk="id").last_pk
|
pk = table.insert({"foo": "bar", "id": 5}, pk="id").last_pk
|
||||||
assert 5 == pk
|
assert 5 == pk
|
||||||
table.update(pk, {"foo": "baz"})
|
table.update(pk, {"foo": "baz"})
|
||||||
|
|
@ -22,7 +22,7 @@ def test_update_pk_table(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_update_compound_pk_table(fresh_db):
|
def test_update_compound_pk_table(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
pk = table.insert({"id1": 5, "id2": 3, "v": 1}, pk=("id1", "id2")).last_pk
|
pk = table.insert({"id1": 5, "id2": 3, "v": 1}, pk=("id1", "id2")).last_pk
|
||||||
assert (5, 3) == pk
|
assert (5, 3) == pk
|
||||||
table.update(pk, {"v": 2})
|
table.update(pk, {"v": 2})
|
||||||
|
|
@ -42,14 +42,14 @@ 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")
|
table = fresh_db["table"]
|
||||||
table.insert({"id1": 5, "id2": 3, "v": 1}, pk=pk)
|
table.insert({"id1": 5, "id2": 3, "v": 1}, pk=pk).last_pk
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
table.update(update_pk, {"v": 2})
|
table.update(update_pk, {"v": 2})
|
||||||
|
|
||||||
|
|
||||||
def test_update_alter(fresh_db):
|
def test_update_alter(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
rowid = table.insert({"foo": "bar"}).last_pk
|
rowid = table.insert({"foo": "bar"}).last_pk
|
||||||
table.update(rowid, {"new_col": 1.2}, alter=True)
|
table.update(rowid, {"new_col": 1.2}, alter=True)
|
||||||
assert [{"foo": "bar", "new_col": 1.2}] == list(table.rows)
|
assert [{"foo": "bar", "new_col": 1.2}] == list(table.rows)
|
||||||
|
|
@ -72,7 +72,7 @@ def test_update_alter(fresh_db):
|
||||||
|
|
||||||
def test_update_alter_with_special_column_characters(fresh_db):
|
def test_update_alter_with_special_column_characters(fresh_db):
|
||||||
# With double-quote escaping, columns with special characters are now valid
|
# With double-quote escaping, columns with special characters are now valid
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
rowid = table.insert({"foo": "bar"}).last_pk
|
rowid = table.insert({"foo": "bar"}).last_pk
|
||||||
table.update(rowid, {"new_col[abc]": 1.2}, alter=True)
|
table.update(rowid, {"new_col[abc]": 1.2}, alter=True)
|
||||||
assert list(table.rows) == [{"foo": "bar", "new_col[abc]": 1.2}]
|
assert list(table.rows) == [{"foo": "bar", "new_col[abc]": 1.2}]
|
||||||
|
|
@ -106,8 +106,8 @@ def test_update_with_no_values_sets_last_pk(fresh_db):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_update_dictionaries_and_lists_as_json(fresh_db, data_structure):
|
def test_update_dictionaries_and_lists_as_json(fresh_db, data_structure):
|
||||||
fresh_db.table("test").insert({"id": 1, "data": ""}, pk="id")
|
fresh_db["test"].insert({"id": 1, "data": ""}, pk="id")
|
||||||
fresh_db.table("test").update(1, {"data": data_structure})
|
fresh_db["test"].update(1, {"data": data_structure})
|
||||||
row = fresh_db.execute("select id, data from test").fetchone()
|
row = fresh_db.execute("select id, data from test").fetchone()
|
||||||
assert row[0] == 1
|
assert row[0] == 1
|
||||||
assert data_structure == json.loads(row[1])
|
assert data_structure == json.loads(row[1])
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sqlite_utils import Database
|
|
||||||
from sqlite_utils.db import PrimaryKeyRequired
|
from sqlite_utils.db import PrimaryKeyRequired
|
||||||
|
from sqlite_utils import Database
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
||||||
table = db.table("table")
|
table = db["table"]
|
||||||
table.insert_all([{"id": 1, "name": "Cleo"}], pk="id", replace=True)
|
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
table.upsert({"id": 1, "age": 5}, pk="id", alter=True)
|
table.upsert({"id": 1, "age": 5}, pk="id", alter=True)
|
||||||
assert list(table.rows) == [{"id": 1, "name": "Cleo", "age": 5}]
|
assert list(table.rows) == [{"id": 1, "name": "Cleo", "age": 5}]
|
||||||
assert table.last_pk == 1
|
assert table.last_pk == 1
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_all(fresh_db):
|
def test_upsert_all(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert_all([{"id": 1, "name": "Cleo"}, {"id": 2, "name": "Nixie"}], pk="id")
|
table.upsert_all([{"id": 1, "name": "Cleo"}, {"id": 2, "name": "Nixie"}], pk="id")
|
||||||
table.upsert_all([{"id": 1, "age": 5}, {"id": 2, "age": 5}], pk="id", alter=True)
|
table.upsert_all([{"id": 1, "age": 5}, {"id": 2, "age": 5}], pk="id", alter=True)
|
||||||
assert list(table.rows) == [
|
assert list(table.rows) == [
|
||||||
|
|
@ -26,7 +25,7 @@ def test_upsert_all(fresh_db):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_all_single_column(fresh_db):
|
def test_upsert_all_single_column(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert_all([{"name": "Cleo"}], pk="name")
|
table.upsert_all([{"name": "Cleo"}], pk="name")
|
||||||
assert list(table.rows) == [{"name": "Cleo"}]
|
assert list(table.rows) == [{"name": "Cleo"}]
|
||||||
assert table.pks == ["name"]
|
assert table.pks == ["name"]
|
||||||
|
|
@ -34,16 +33,16 @@ def test_upsert_all_single_column(fresh_db):
|
||||||
|
|
||||||
def test_upsert_all_not_null(fresh_db):
|
def test_upsert_all_not_null(fresh_db):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/538
|
# https://github.com/simonw/sqlite-utils/issues/538
|
||||||
fresh_db.table("comments").upsert_all(
|
fresh_db["comments"].upsert_all(
|
||||||
[{"id": 1, "name": "Cleo"}],
|
[{"id": 1, "name": "Cleo"}],
|
||||||
pk="id",
|
pk="id",
|
||||||
not_null=["name"],
|
not_null=["name"],
|
||||||
)
|
)
|
||||||
assert list(fresh_db.table("comments").rows) == [{"id": 1, "name": "Cleo"}]
|
assert list(fresh_db["comments"].rows) == [{"id": 1, "name": "Cleo"}]
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_error_if_no_pk(fresh_db):
|
def test_upsert_error_if_no_pk(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
with pytest.raises(PrimaryKeyRequired):
|
with pytest.raises(PrimaryKeyRequired):
|
||||||
table.upsert_all([{"id": 1, "name": "Cleo"}])
|
table.upsert_all([{"id": 1, "name": "Cleo"}])
|
||||||
with pytest.raises(PrimaryKeyRequired):
|
with pytest.raises(PrimaryKeyRequired):
|
||||||
|
|
@ -53,7 +52,7 @@ def test_upsert_error_if_no_pk(fresh_db):
|
||||||
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
||||||
def test_upsert_empty_record_errors(use_old_upsert):
|
def test_upsert_empty_record_errors(use_old_upsert):
|
||||||
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
||||||
table = db.table("table")
|
table = db["table"]
|
||||||
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
with pytest.raises(PrimaryKeyRequired):
|
with pytest.raises(PrimaryKeyRequired):
|
||||||
table.upsert({}, pk="id")
|
table.upsert({}, pk="id")
|
||||||
|
|
@ -66,7 +65,7 @@ def test_upsert_empty_record_errors(use_old_upsert):
|
||||||
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
@pytest.mark.parametrize("use_old_upsert", (False, True))
|
||||||
def test_upsert_missing_pk_value_errors(use_old_upsert):
|
def test_upsert_missing_pk_value_errors(use_old_upsert):
|
||||||
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
db = Database(memory=True, use_old_upsert=use_old_upsert)
|
||||||
table = db.table("table")
|
table = db["table"]
|
||||||
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
table.insert({"id": 1, "name": "Cleo"}, pk="id")
|
||||||
# Records that omit the pk column entirely
|
# Records that omit the pk column entirely
|
||||||
with pytest.raises(PrimaryKeyRequired):
|
with pytest.raises(PrimaryKeyRequired):
|
||||||
|
|
@ -78,7 +77,7 @@ def test_upsert_missing_pk_value_errors(use_old_upsert):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_missing_compound_pk_value_errors(fresh_db):
|
def test_upsert_missing_compound_pk_value_errors(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.insert({"a": "x", "b": "y", "v": 1}, pk=("a", "b"))
|
table.insert({"a": "x", "b": "y", "v": 1}, pk=("a", "b"))
|
||||||
# Missing one component of the detected compound primary key
|
# Missing one component of the detected compound primary key
|
||||||
with pytest.raises(PrimaryKeyRequired):
|
with pytest.raises(PrimaryKeyRequired):
|
||||||
|
|
@ -105,7 +104,7 @@ def test_upsert_uses_compound_pk_from_existing_table(use_old_upsert):
|
||||||
primary key (Source, Object, Category)
|
primary key (Source, Object, Category)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
table = db.table("summary")
|
table = db["summary"]
|
||||||
table.upsert(
|
table.upsert(
|
||||||
{
|
{
|
||||||
"Source": "Client A",
|
"Source": "Client A",
|
||||||
|
|
@ -134,7 +133,7 @@ def test_upsert_uses_compound_pk_from_existing_table(use_old_upsert):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_with_hash_id(fresh_db):
|
def test_upsert_with_hash_id(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert({"foo": "bar"}, hash_id="pk")
|
table.upsert({"foo": "bar"}, hash_id="pk")
|
||||||
assert [{"pk": "a5e744d0164540d33b1d7ea616c28f2fa97e754a", "foo": "bar"}] == list(
|
assert [{"pk": "a5e744d0164540d33b1d7ea616c28f2fa97e754a", "foo": "bar"}] == list(
|
||||||
table.rows
|
table.rows
|
||||||
|
|
@ -144,7 +143,7 @@ def test_upsert_with_hash_id(fresh_db):
|
||||||
|
|
||||||
@pytest.mark.parametrize("hash_id", (None, "custom_id"))
|
@pytest.mark.parametrize("hash_id", (None, "custom_id"))
|
||||||
def test_upsert_with_hash_id_columns(fresh_db, hash_id):
|
def test_upsert_with_hash_id_columns(fresh_db, hash_id):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert({"a": 1, "b": 2, "c": 3}, hash_id=hash_id, hash_id_columns=("a", "b"))
|
table.upsert({"a": 1, "b": 2, "c": 3}, hash_id=hash_id, hash_id_columns=("a", "b"))
|
||||||
assert list(table.rows) == [
|
assert list(table.rows) == [
|
||||||
{
|
{
|
||||||
|
|
@ -167,7 +166,7 @@ def test_upsert_with_hash_id_columns(fresh_db, hash_id):
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_compound_primary_key(fresh_db):
|
def test_upsert_compound_primary_key(fresh_db):
|
||||||
table = fresh_db.table("table")
|
table = fresh_db["table"]
|
||||||
table.upsert_all(
|
table.upsert_all(
|
||||||
[
|
[
|
||||||
{"species": "dog", "id": 1, "name": "Cleo", "age": 4},
|
{"species": "dog", "id": 1, "name": "Cleo", "age": 4},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
|
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",
|
||||||
|
|
@ -59,7 +57,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 = f"id,text\n1,{long_value}"
|
long_csv = "id,text\n1,{}".format(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,5 +1,4 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -12,13 +11,13 @@ 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()]
|
||||||
db.enable_wal()
|
db.enable_wal()
|
||||||
assert "wal" == db.journal_mode
|
assert "wal" == db.journal_mode
|
||||||
db.table("test").insert({"foo": "bar"})
|
db["test"].insert({"foo": "bar"})
|
||||||
assert "test.db-wal" in [f.basename for f in tmpdir.listdir()]
|
assert "test.db-wal" in [f.basename for f in tmpdir.listdir()]
|
||||||
db.disable_wal()
|
db.disable_wal()
|
||||||
assert "delete" == db.journal_mode
|
assert "delete" == db.journal_mode
|
||||||
|
|
@ -26,30 +25,32 @@ 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.table("test").insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(TransactionError), db.atomic():
|
with pytest.raises(TransactionError):
|
||||||
db.table("test").insert({"id": 2}, pk="id")
|
with db.atomic():
|
||||||
|
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"
|
||||||
assert [r["id"] for r in db.table("test").rows] == [1]
|
assert [r["id"] for r in db["test"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
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.table("test").insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
with pytest.raises(TransactionError), db.atomic():
|
with pytest.raises(TransactionError):
|
||||||
db.table("test").insert({"id": 2}, pk="id")
|
with db.atomic():
|
||||||
|
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.table("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():
|
||||||
|
|
@ -62,25 +63,26 @@ 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.table("test").insert({"id": 1}, pk="id")
|
db["test"].insert({"id": 1}, pk="id")
|
||||||
db.enable_wal()
|
db.enable_wal()
|
||||||
assert [r["id"] for r in db.table("test").rows] == [1]
|
assert [r["id"] for r in db["test"].rows] == [1]
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_autocommit_on_inside_transaction_raises(db_path_tmpdir):
|
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.table("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), db.ensure_autocommit_on():
|
with pytest.raises(TransactionError):
|
||||||
|
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()
|
||||||
assert [r["id"] for r in db.table("test").rows] == [1]
|
assert [r["id"] for r in db["test"].rows] == [1]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue