mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-08-02 15:34:10 +02:00
Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2026087a72 | ||
|
|
3c021be968 | ||
|
|
07409df236 | ||
|
|
2453a29081 | ||
|
|
9298ea8ab5 | ||
|
|
966975ff43 | ||
|
|
41807ca7ec | ||
|
|
f221c29393 | ||
|
|
83f8bc2eaa | ||
|
|
4106264983 | ||
|
|
638b8ca871 | ||
|
|
7aca26267f | ||
|
|
bd78966f1e | ||
|
|
25d88cb727 | ||
|
|
eab54c7696 | ||
|
|
532dc4668d |
15 changed files with 212 additions and 71 deletions
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -17,13 +17,8 @@ jobs:
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- uses: actions/cache@v4
|
cache: pip
|
||||||
name: Configure pip caching
|
cache-dependency-path: setup.py
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -e '.[test]'
|
pip install -e '.[test]'
|
||||||
|
|
@ -39,13 +34,8 @@ jobs:
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
- uses: actions/cache@v4
|
cache: pip
|
||||||
name: Configure pip caching
|
cache-dependency-path: setup.py
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-publish-pip-
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install setuptools wheel twine
|
pip install setuptools wheel twine
|
||||||
|
|
|
||||||
9
.github/workflows/spellcheck.yml
vendored
9
.github/workflows/spellcheck.yml
vendored
|
|
@ -11,13 +11,8 @@ jobs:
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- uses: actions/cache@v4
|
cache: pip
|
||||||
name: Configure pip caching
|
cache-dependency-path: setup.py
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -e '.[docs]'
|
pip install -e '.[docs]'
|
||||||
|
|
|
||||||
9
.github/workflows/test-coverage.yml
vendored
9
.github/workflows/test-coverage.yml
vendored
|
|
@ -17,13 +17,8 @@ jobs:
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- uses: actions/cache@v4
|
cache: pip
|
||||||
name: Configure pip caching
|
cache-dependency-path: setup.py
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
- name: Install SpatiaLite
|
- name: Install SpatiaLite
|
||||||
run: sudo apt-get install libsqlite3-mod-spatialite
|
run: sudo apt-get install libsqlite3-mod-spatialite
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
|
|
|
||||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
|
|
@ -10,15 +10,10 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
numpy: [0, 1]
|
numpy: [0, 1]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
|
os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
|
||||||
# Skip 3.8 and 3.9 on macos-14 - it only has 3.10+
|
|
||||||
exclude:
|
exclude:
|
||||||
- python-version: "3.8"
|
|
||||||
os: macos-14
|
|
||||||
- python-version: "3.9"
|
|
||||||
os: macos-14
|
|
||||||
- python-version: "3.13"
|
- python-version: "3.13"
|
||||||
numpy: 1
|
numpy: 1
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -28,13 +23,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
allow-prereleases: true
|
allow-prereleases: true
|
||||||
- uses: actions/cache@v4
|
cache: pip
|
||||||
name: Configure pip caching
|
cache-dependency-path: setup.py
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -e '.[test,mypy,flake8]'
|
pip install -e '.[test,mypy,flake8]'
|
||||||
|
|
@ -64,4 +54,4 @@ jobs:
|
||||||
run: black . --check
|
run: black . --check
|
||||||
- name: Check if cog needs to be run
|
- name: Check if cog needs to be run
|
||||||
run: |
|
run: |
|
||||||
cog --check README.md docs/*.rst
|
cog --check --diff README.md docs/*.rst
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,17 @@
|
||||||
Changelog
|
Changelog
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
.. _v3_39:
|
||||||
|
|
||||||
|
3.39 (2025-11-24)
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
- Fixed a bug with ``sqlite-utils install`` when the tool had been installed using ``uv``. (:issue:`687`)
|
||||||
|
- The ```--functions``` argument now optionally accepts a path to a Python file as an alternative to a string full of code, and can be specified multiple times - see :ref:`cli_query_functions`. (:issue:`659`)
|
||||||
|
- ``sqlite-utils`` now requires on Python 3.10 or higher.
|
||||||
|
|
||||||
|
`sqlite-utils 4.0a1 <https://sqlite-utils.datasette.io/en/latest/changelog.html#a1-2025-11-23>`__ is now available as an alpha with some `minor breaking changes <https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/>`__.
|
||||||
|
|
||||||
.. _v3_38:
|
.. _v3_38:
|
||||||
|
|
||||||
3.38 (2024-11-23)
|
3.38 (2024-11-23)
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ See :ref:`cli_query`.
|
||||||
-r, --raw Raw output, first column of first row
|
-r, --raw Raw output, first column of first row
|
||||||
--raw-lines Raw output, first column of each row
|
--raw-lines Raw output, first column of each row
|
||||||
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
-p, --param <TEXT TEXT>... Named :parameters for SQL query
|
||||||
--functions TEXT Python code defining one or more custom SQL
|
--functions TEXT Python code or file path defining custom SQL
|
||||||
functions
|
functions
|
||||||
--load-extension TEXT Path to SQLite extension, with optional
|
--load-extension TEXT Path to SQLite extension, with optional
|
||||||
:entrypoint
|
:entrypoint
|
||||||
|
|
@ -175,7 +175,7 @@ See :ref:`cli_memory`.
|
||||||
sqlite-utils memory animals.csv --schema
|
sqlite-utils memory animals.csv --schema
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--functions TEXT Python code defining one or more custom SQL
|
--functions TEXT Python code or file path defining custom SQL
|
||||||
functions
|
functions
|
||||||
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
--attach <TEXT FILE>... Additional databases to attach - specify alias and
|
||||||
filepath
|
filepath
|
||||||
|
|
@ -375,7 +375,7 @@ See :ref:`cli_bulk`.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--batch-size INTEGER Commit every X records
|
--batch-size INTEGER Commit every X records
|
||||||
--functions TEXT Python code defining one or more custom SQL functions
|
--functions TEXT Python code or file path defining custom SQL functions
|
||||||
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
--flatten Flatten nested JSON objects, so {"a": {"b": 1}} becomes
|
||||||
{"a_b": 1}
|
{"a_b": 1}
|
||||||
--nl Expect newline-delimited JSON
|
--nl Expect newline-delimited JSON
|
||||||
|
|
@ -1497,7 +1497,7 @@ See :ref:`cli_spatialite`.
|
||||||
paths. To load it from a specific path, use --load-extension.
|
paths. To load it from a specific path, use --load-extension.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-t, --type [POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION|GEOMETRY]
|
-t, --type [point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection|geometry]
|
||||||
Specify a geometry type for this column.
|
Specify a geometry type for this column.
|
||||||
[default: GEOMETRY]
|
[default: GEOMETRY]
|
||||||
--srid INTEGER Spatial Reference ID. See
|
--srid INTEGER Spatial Reference ID. See
|
||||||
|
|
|
||||||
18
docs/cli.rst
18
docs/cli.rst
|
|
@ -368,6 +368,22 @@ This example defines a function which extracts the domain from a URL:
|
||||||
|
|
||||||
Every callable object defined in the block will be registered as a SQL function with the same name, with the exception of functions with names that begin with an underscore.
|
Every callable object defined in the block will be registered as a SQL function with the same name, with the exception of functions with names that begin with an underscore.
|
||||||
|
|
||||||
|
You can also pass the path to a Python file containing function definitions:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils query sites.db "select url, domain(url) from urls" --functions functions.py
|
||||||
|
|
||||||
|
The ``--functions`` option can be used multiple times to load functions from multiple sources:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sqlite-utils query sites.db "select url, domain(url), extract_path(url) from urls" \
|
||||||
|
--functions domain_funcs.py \
|
||||||
|
--functions 'def extract_path(url):
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
return urlparse(url).path'
|
||||||
|
|
||||||
.. _cli_query_extensions:
|
.. _cli_query_extensions:
|
||||||
|
|
||||||
SQLite extensions
|
SQLite extensions
|
||||||
|
|
@ -1128,7 +1144,7 @@ You can insert binary data into a BLOB column by first encoding it using base64
|
||||||
Inserting newline-delimited JSON
|
Inserting newline-delimited JSON
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
You can also import `newline-delimited JSON <http://ndjson.org/>`__ using the ``--nl`` option:
|
You can also import newline-delimited JSON (see `JSON Lines <https://jsonlines.org/>`__) using the ``--nl`` option:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2711,7 +2711,7 @@ By default, the name of the Python function will be used as the name of the SQL
|
||||||
|
|
||||||
print(db.execute('select rev("hello")').fetchone()[0])
|
print(db.execute('select rev("hello")').fetchone()[0])
|
||||||
|
|
||||||
Python 3.8 added the ability to register `deterministic SQLite functions <https://sqlite.org/deterministic.html>`__, allowing you to indicate that a function will return the exact same result for any given inputs and hence allowing SQLite to apply some performance optimizations. You can mark a function as deterministic using ``deterministic=True``, like this:
|
If a function will return the exact same result for any given inputs you can register it as a `deterministic SQLite function <https://sqlite.org/deterministic.html>`__ allowing SQLite to apply some performance optimizations:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
@ -2719,8 +2719,6 @@ Python 3.8 added the ability to register `deterministic SQLite functions <https:
|
||||||
def reverse_string(s):
|
def reverse_string(s):
|
||||||
return "".join(reversed(list(s)))
|
return "".join(reversed(list(s)))
|
||||||
|
|
||||||
If you run this on a version of Python prior to 3.8 your code will still work, but the ``deterministic=True`` parameter will be ignored.
|
|
||||||
|
|
||||||
By default registering a function with the same name and number of arguments will have no effect - the ``Database`` instance keeps track of functions that have already been registered and skips registering them if ``@db.register_function`` is called a second time.
|
By default registering a function with the same name and number of arguments will have no effect - the ``Database`` instance keeps track of functions that have already been registered and skips registering them if ``@db.register_function`` is called a second time.
|
||||||
|
|
||||||
If you want to deliberately replace the registered function with a new implementation, use the ``replace=True`` argument:
|
If you want to deliberately replace the registered function with a new implementation, use the ``replace=True`` argument:
|
||||||
|
|
|
||||||
11
setup.py
11
setup.py
|
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "3.38"
|
VERSION = "3.39"
|
||||||
|
|
||||||
|
|
||||||
def get_long_description():
|
def get_long_description():
|
||||||
|
|
@ -25,11 +25,12 @@ setup(
|
||||||
package_data={"sqlite_utils": ["py.typed"]},
|
package_data={"sqlite_utils": ["py.typed"]},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"sqlite-fts4",
|
"sqlite-fts4",
|
||||||
"click",
|
"click>=8.3.1",
|
||||||
"click-default-group>=1.2.3",
|
"click-default-group>=1.2.3",
|
||||||
"tabulate",
|
"tabulate",
|
||||||
"python-dateutil",
|
"python-dateutil",
|
||||||
"pluggy",
|
"pluggy",
|
||||||
|
"pip",
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
|
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
|
||||||
|
|
@ -64,20 +65,18 @@ setup(
|
||||||
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
"Issues": "https://github.com/simonw/sqlite-utils/issues",
|
||||||
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
"CI": "https://github.com/simonw/sqlite-utils/actions",
|
||||||
},
|
},
|
||||||
python_requires=">=3.8",
|
python_requires=">=3.10",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Intended Audience :: Science/Research",
|
"Intended Audience :: Science/Research",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"Topic :: Database",
|
"Topic :: Database",
|
||||||
"License :: OSI Approved :: Apache Software License",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
],
|
],
|
||||||
# Needed to bundle py.typed so mypy can see it:
|
# Needed to bundle py.typed so mypy can see it:
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import base64
|
import base64
|
||||||
import click
|
import click
|
||||||
from click_default_group import DefaultGroup # type: ignore
|
from click_default_group import DefaultGroup # type: ignore
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
import hashlib
|
import hashlib
|
||||||
import pathlib
|
import pathlib
|
||||||
from runpy import run_module
|
from runpy import run_module
|
||||||
|
|
@ -962,7 +962,7 @@ def insert_upsert_implementation(
|
||||||
db = sqlite_utils.Database(path)
|
db = sqlite_utils.Database(path)
|
||||||
_load_extensions(db, load_extension)
|
_load_extensions(db, load_extension)
|
||||||
if functions:
|
if functions:
|
||||||
_register_functions(db, functions)
|
_register_functions_from_multiple(db, functions)
|
||||||
if (delimiter or quotechar or sniff or no_headers) and not tsv:
|
if (delimiter or quotechar or sniff or no_headers) and not tsv:
|
||||||
csv = True
|
csv = True
|
||||||
if (nl + csv + tsv) >= 2:
|
if (nl + csv + tsv) >= 2:
|
||||||
|
|
@ -1370,7 +1370,9 @@ def upsert(
|
||||||
@click.argument("file", type=click.File("rb"), required=True)
|
@click.argument("file", type=click.File("rb"), required=True)
|
||||||
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
|
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--functions", help="Python code defining one or more custom SQL functions"
|
"--functions",
|
||||||
|
help="Python code or file path defining custom SQL functions",
|
||||||
|
multiple=True,
|
||||||
)
|
)
|
||||||
@import_options
|
@import_options
|
||||||
@load_extension_option
|
@load_extension_option
|
||||||
|
|
@ -1759,7 +1761,9 @@ def drop_view(path, view, ignore, load_extension):
|
||||||
help="Named :parameters for SQL query",
|
help="Named :parameters for SQL query",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--functions", help="Python code defining one or more custom SQL functions"
|
"--functions",
|
||||||
|
help="Python code or file path defining custom SQL functions",
|
||||||
|
multiple=True,
|
||||||
)
|
)
|
||||||
@load_extension_option
|
@load_extension_option
|
||||||
def query(
|
def query(
|
||||||
|
|
@ -1796,7 +1800,7 @@ def query(
|
||||||
db.register_fts4_bm25()
|
db.register_fts4_bm25()
|
||||||
|
|
||||||
if functions:
|
if functions:
|
||||||
_register_functions(db, functions)
|
_register_functions_from_multiple(db, functions)
|
||||||
|
|
||||||
_execute_query(
|
_execute_query(
|
||||||
db,
|
db,
|
||||||
|
|
@ -1824,7 +1828,9 @@ def query(
|
||||||
)
|
)
|
||||||
@click.argument("sql")
|
@click.argument("sql")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--functions", help="Python code defining one or more custom SQL functions"
|
"--functions",
|
||||||
|
help="Python code or file path defining custom SQL functions",
|
||||||
|
multiple=True,
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--attach",
|
"--attach",
|
||||||
|
|
@ -1996,7 +2002,7 @@ def memory(
|
||||||
db.register_fts4_bm25()
|
db.register_fts4_bm25()
|
||||||
|
|
||||||
if functions:
|
if functions:
|
||||||
_register_functions(db, functions)
|
_register_functions_from_multiple(db, functions)
|
||||||
|
|
||||||
if return_db:
|
if return_db:
|
||||||
return db
|
return db
|
||||||
|
|
@ -3203,8 +3209,12 @@ FILE_COLUMNS = {
|
||||||
"ctime": lambda p: p.stat().st_ctime,
|
"ctime": lambda p: p.stat().st_ctime,
|
||||||
"mtime_int": lambda p: int(p.stat().st_mtime),
|
"mtime_int": lambda p: int(p.stat().st_mtime),
|
||||||
"ctime_int": lambda p: int(p.stat().st_ctime),
|
"ctime_int": lambda p: int(p.stat().st_ctime),
|
||||||
"mtime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_mtime).isoformat(),
|
"mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc)
|
||||||
"ctime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_ctime).isoformat(),
|
.replace(tzinfo=None)
|
||||||
|
.isoformat(),
|
||||||
|
"ctime_iso": lambda p: datetime.fromtimestamp(p.stat().st_ctime, timezone.utc)
|
||||||
|
.replace(tzinfo=None)
|
||||||
|
.isoformat(),
|
||||||
"size": lambda p: p.stat().st_size,
|
"size": lambda p: p.stat().st_size,
|
||||||
"stem": lambda p: p.stem,
|
"stem": lambda p: p.stem,
|
||||||
"suffix": lambda p: p.suffix,
|
"suffix": lambda p: p.suffix,
|
||||||
|
|
@ -3281,6 +3291,13 @@ def _load_extensions(db, load_extension):
|
||||||
|
|
||||||
def _register_functions(db, functions):
|
def _register_functions(db, functions):
|
||||||
# Register any Python functions as SQL functions:
|
# Register any Python functions as SQL functions:
|
||||||
|
# Check if this is a file path
|
||||||
|
if "\n" not in functions and functions.endswith(".py"):
|
||||||
|
try:
|
||||||
|
functions = pathlib.Path(functions).read_text()
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise click.ClickException("File not found: {}".format(functions))
|
||||||
|
|
||||||
sqlite3.enable_callback_tracebacks(True)
|
sqlite3.enable_callback_tracebacks(True)
|
||||||
globals = {}
|
globals = {}
|
||||||
try:
|
try:
|
||||||
|
|
@ -3291,3 +3308,12 @@ def _register_functions(db, functions):
|
||||||
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("_"):
|
||||||
db.register_function(value, name=name)
|
db.register_function(value, name=name)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_functions_from_multiple(db, functions_list):
|
||||||
|
"""Register functions from multiple --functions arguments."""
|
||||||
|
if not functions_list:
|
||||||
|
return
|
||||||
|
for functions in functions_list:
|
||||||
|
if isinstance(functions, str) and functions.strip():
|
||||||
|
_register_functions(db, functions)
|
||||||
|
|
|
||||||
|
|
@ -237,36 +237,43 @@ if pd:
|
||||||
|
|
||||||
class AlterError(Exception):
|
class AlterError(Exception):
|
||||||
"Error altering table"
|
"Error altering table"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoObviousTable(Exception):
|
class NoObviousTable(Exception):
|
||||||
"Could not tell which table this operation refers to"
|
"Could not tell which table this operation refers to"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoTable(Exception):
|
class NoTable(Exception):
|
||||||
"Specified table does not exist"
|
"Specified table does not exist"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadPrimaryKey(Exception):
|
class BadPrimaryKey(Exception):
|
||||||
"Table does not have a single obvious primary key"
|
"Table does not have a single obvious primary key"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotFoundError(Exception):
|
class NotFoundError(Exception):
|
||||||
"Record not found"
|
"Record not found"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PrimaryKeyRequired(Exception):
|
class PrimaryKeyRequired(Exception):
|
||||||
"Primary key needs to be specified"
|
"Primary key needs to be specified"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidColumns(Exception):
|
class InvalidColumns(Exception):
|
||||||
"Specified columns do not exist"
|
"Specified columns do not exist"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -368,7 +375,7 @@ class Database:
|
||||||
pm.hook.prepare_connection(conn=self.conn)
|
pm.hook.prepare_connection(conn=self.conn)
|
||||||
self.strict = strict
|
self.strict = strict
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"Close the SQLite connection, and the underlying database file"
|
"Close the SQLite connection, and the underlying database file"
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
|
|
@ -3203,7 +3210,7 @@ class Table(Queryable):
|
||||||
:param not_null: Set of strings specifying columns that should be ``NOT NULL``.
|
:param not_null: Set of strings specifying columns that should be ``NOT NULL``.
|
||||||
:param defaults: Dictionary specifying default values for specific columns.
|
:param defaults: Dictionary specifying default values for specific columns.
|
||||||
:param hash_id: Name of a column to create and use as a primary key, where the
|
:param hash_id: Name of a column to create and use as a primary key, where the
|
||||||
value of thet primary key will be derived as a SHA1 hash of the other column values
|
value of that primary key will be derived as a SHA1 hash of the other column values
|
||||||
in the record. ``hash_id="id"`` is a common column name used for this.
|
in the record. ``hash_id="id"`` is a common column name used for this.
|
||||||
:param alter: Boolean, should any missing columns be added automatically?
|
:param alter: Boolean, should any missing columns be added automatically?
|
||||||
:param ignore: Boolean, if a record already exists with this primary key, ignore this insert.
|
:param ignore: Boolean, if a record already exists with this primary key, ignore this insert.
|
||||||
|
|
@ -3852,7 +3859,7 @@ def jsonify_if_needed(value):
|
||||||
|
|
||||||
|
|
||||||
def resolve_extracts(
|
def resolve_extracts(
|
||||||
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]]
|
extracts: Optional[Union[Dict[str, str], List[str], Tuple[str]]],
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if extracts is None:
|
if extracts is None:
|
||||||
extracts = {}
|
extracts = {}
|
||||||
|
|
|
||||||
|
|
@ -806,6 +806,77 @@ def test_hidden_functions_are_hidden(db_path):
|
||||||
assert "_two" not in functions
|
assert "_two" not in functions
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_functions_from_file(db_path, tmp_path):
|
||||||
|
# Create a temporary file with function definitions
|
||||||
|
functions_file = tmp_path / "my_functions.py"
|
||||||
|
functions_file.write_text(TEST_FUNCTIONS)
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
db_path,
|
||||||
|
"select zero(), one(1), two(1, 2)",
|
||||||
|
"--functions",
|
||||||
|
str(functions_file),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert json.loads(result.output.strip()) == [
|
||||||
|
{"zero()": 0, "one(1)": 1, "two(1, 2)": 3}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_functions_file_not_found(db_path):
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
db_path,
|
||||||
|
"select zero()",
|
||||||
|
"--functions",
|
||||||
|
"nonexistent.py",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "File not found: nonexistent.py" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_functions_multiple_invocations(db_path):
|
||||||
|
# Test using --functions multiple times
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
db_path,
|
||||||
|
"select triple(2), quadruple(2)",
|
||||||
|
"--functions",
|
||||||
|
"def triple(x):\n return x * 3",
|
||||||
|
"--functions",
|
||||||
|
"def quadruple(x):\n return x * 4",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_functions_file_and_inline(db_path, tmp_path):
|
||||||
|
# Test combining file and inline code
|
||||||
|
functions_file = tmp_path / "file_funcs.py"
|
||||||
|
functions_file.write_text("def triple(x):\n return x * 3")
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
db_path,
|
||||||
|
"select triple(2), quadruple(2)",
|
||||||
|
"--functions",
|
||||||
|
str(functions_file),
|
||||||
|
"--functions",
|
||||||
|
"def quadruple(x):\n return x * 4",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert json.loads(result.output.strip()) == [{"triple(2)": 6, "quadruple(2)": 8}]
|
||||||
|
|
||||||
|
|
||||||
LOREM_IPSUM_COMPRESSED = (
|
LOREM_IPSUM_COMPRESSED = (
|
||||||
b"x\x9c\xed\xd1\xcdq\x03!\x0c\x05\xe0\xbb\xabP\x01\x1eW\x91\xdc|M\x01\n\xc8\x8e"
|
b"x\x9c\xed\xd1\xcdq\x03!\x0c\x05\xe0\xbb\xabP\x01\x1eW\x91\xdc|M\x01\n\xc8\x8e"
|
||||||
b"f\xf83H\x1e\x97\x1f\x91M\x8e\xe9\xe0\xdd\x96\x05\x84\xf4\xbek\x9fRI\xc7\xf2J"
|
b"f\xf83H\x1e\x97\x1f\x91M\x8e\xe9\xe0\xdd\x96\x05\x84\xf4\xbek\x9fRI\xc7\xf2J"
|
||||||
|
|
@ -916,7 +987,7 @@ def test_query_json_with_json_cols(db_path):
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"content,is_binary",
|
"content,is_binary",
|
||||||
[(b"\x00\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
||||||
)
|
)
|
||||||
def test_query_raw(db_path, content, is_binary):
|
def test_query_raw(db_path, content, is_binary):
|
||||||
Database(db_path)["files"].insert({"content": content})
|
Database(db_path)["files"].insert({"content": content})
|
||||||
|
|
@ -931,7 +1002,7 @@ def test_query_raw(db_path, content, is_binary):
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"content,is_binary",
|
"content,is_binary",
|
||||||
[(b"\x00\x0Fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
[(b"\x00\x0fbinary", True), ("this is text", False), (1, False), (1.5, False)],
|
||||||
)
|
)
|
||||||
def test_query_raw_lines(db_path, content, is_binary):
|
def test_query_raw_lines(db_path, content, is_binary):
|
||||||
Database(db_path)["files"].insert_all({"content": content} for _ in range(3))
|
Database(db_path)["files"].insert_all({"content": content} for _ in range(3))
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,32 @@ def test_cli_bulk(test_db_and_path):
|
||||||
] == list(db["example"].rows)
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_bulk_multiple_functions(test_db_and_path):
|
||||||
|
db, db_path = test_db_and_path
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
"bulk",
|
||||||
|
db_path,
|
||||||
|
"insert into example (id, name) values (:id, myupper(mylower(:name)))",
|
||||||
|
"-",
|
||||||
|
"--nl",
|
||||||
|
"--functions",
|
||||||
|
"myupper = lambda s: s.upper()",
|
||||||
|
"--functions",
|
||||||
|
"mylower = lambda s: s.lower()",
|
||||||
|
],
|
||||||
|
input='{"id": 3, "name": "ThReE"}\n{"id": 4, "name": "FoUr"}\n',
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert [
|
||||||
|
{"id": 1, "name": "One"},
|
||||||
|
{"id": 2, "name": "Two"},
|
||||||
|
{"id": 3, "name": "THREE"},
|
||||||
|
{"id": 4, "name": "FOUR"},
|
||||||
|
] == list(db["example"].rows)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_bulk_batch_size(test_db_and_path):
|
def test_cli_bulk_batch_size(test_db_and_path):
|
||||||
db, db_path = test_db_and_path
|
db, db_path = test_db_and_path
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,22 @@ def test_memory_functions():
|
||||||
assert result.output.strip() == '[{"hello()": "Hello"}]'
|
assert result.output.strip() == '[{"hello()": "Hello"}]'
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_functions_multiple():
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
cli.cli,
|
||||||
|
[
|
||||||
|
"memory",
|
||||||
|
"select triple(2), quadruple(2)",
|
||||||
|
"--functions",
|
||||||
|
"def triple(x):\n return x * 3",
|
||||||
|
"--functions",
|
||||||
|
"def quadruple(x):\n return x * 4",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.strip() == '[{"triple(2)": 6, "quadruple(2)": 8}]'
|
||||||
|
|
||||||
|
|
||||||
def test_memory_return_db(tmpdir):
|
def test_memory_return_db(tmpdir):
|
||||||
# https://github.com/simonw/sqlite-utils/issues/643
|
# https://github.com/simonw/sqlite-utils/issues/643
|
||||||
from sqlite_utils.cli import cli
|
from sqlite_utils.cli import cli
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ def test_dayfirst_yearfirst(fresh_db, recipe, kwargs, expected):
|
||||||
|
|
||||||
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
@pytest.mark.parametrize("fn", ("parsedate", "parsedatetime"))
|
||||||
@pytest.mark.parametrize("errors", (None, recipes.SET_NULL, recipes.IGNORE))
|
@pytest.mark.parametrize("errors", (None, recipes.SET_NULL, recipes.IGNORE))
|
||||||
|
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
|
||||||
def test_dateparse_errors(fresh_db, fn, errors):
|
def test_dateparse_errors(fresh_db, fn, errors):
|
||||||
fresh_db["example"].insert_all(
|
fresh_db["example"].insert_all(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue