Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,16 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
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]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v4
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
cache: pip
cache-dependency-path: setup.py
- name: Install dependencies
run: |
pip install -e '.[test]'
Expand All @@ -39,13 +34,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/cache@v4
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-publish-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-publish-pip-
cache: pip
cache-dependency-path: setup.py
- name: Install dependencies
run: |
pip install setuptools wheel twine
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/spellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/cache@v4
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
cache: pip
cache-dependency-path: setup.py
- name: Install dependencies
run: |
pip install -e '.[docs]'
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/cache@v4
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
cache: pip
cache-dependency-path: setup.py
- name: Install SpatiaLite
run: sudo apt-get install libsqlite3-mod-spatialite
- name: Install Python dependencies
Expand Down
18 changes: 4 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,10 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
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]
os: [ubuntu-latest, macos-latest, windows-latest, macos-14]
# Skip 3.8 and 3.9 on macos-14 - it only has 3.10+
exclude:
- python-version: "3.8"
os: macos-14
- python-version: "3.9"
os: macos-14
- python-version: "3.13"
numpy: 1
steps:
Expand All @@ -28,13 +23,8 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- uses: actions/cache@v4
name: Configure pip caching
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
cache: pip
cache-dependency-path: setup.py
- name: Install dependencies
run: |
pip install -e '.[test,mypy,flake8]'
Expand Down Expand Up @@ -64,4 +54,4 @@ jobs:
run: black . --check
- name: Check if cog needs to be run
run: |
cog --check README.md docs/*.rst
cog --check --diff README.md docs/*.rst
8 changes: 4 additions & 4 deletions docs/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ See :ref:`cli_query`.
-r, --raw Raw output, first column of first row
--raw-lines Raw output, first column of each row
-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
--load-extension TEXT Path to SQLite extension, with optional
:entrypoint
Expand Down Expand Up @@ -175,7 +175,7 @@ See :ref:`cli_memory`.
sqlite-utils memory animals.csv --schema

Options:
--functions TEXT Python code defining one or more custom SQL
--functions TEXT Python code or file path defining custom SQL
functions
--attach <TEXT FILE>... Additional databases to attach - specify alias and
filepath
Expand Down Expand Up @@ -375,7 +375,7 @@ See :ref:`cli_bulk`.

Options:
--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
{"a_b": 1}
--nl Expect newline-delimited JSON
Expand Down Expand Up @@ -1497,7 +1497,7 @@ See :ref:`cli_spatialite`.
paths. To load it from a specific path, use --load-extension.

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.
[default: GEOMETRY]
--srid INTEGER Spatial Reference ID. See
Expand Down
18 changes: 17 additions & 1 deletion docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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:

SQLite extensions
Expand Down Expand Up @@ -1128,7 +1144,7 @@ You can insert binary data into a BLOB column by first encoding it using base64
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

Expand Down
4 changes: 1 addition & 3 deletions docs/python-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2711,16 +2711,14 @@ 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])

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

@db.register_function(deterministic=True)
def reverse_string(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.

If you want to deliberately replace the registered function with a new implementation, use the ``replace=True`` argument:
Expand Down
9 changes: 4 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ def get_long_description():
package_data={"sqlite_utils": ["py.typed"]},
install_requires=[
"sqlite-fts4",
"click",
"click>=8.3.1",
"click-default-group>=1.2.3",
"tabulate",
"python-dateutil",
"pluggy",
"pip",
],
extras_require={
"test": ["pytest", "black>=24.1.1", "hypothesis", "cogapp"],
Expand Down Expand Up @@ -64,20 +65,18 @@ def get_long_description():
"Issues": "https://github.com/simonw/sqlite-utils/issues",
"CI": "https://github.com/simonw/sqlite-utils/actions",
},
python_requires=">=3.8",
python_requires=">=3.10",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Intended Audience :: End Users/Desktop",
"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.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
],
# Needed to bundle py.typed so mypy can see it:
zip_safe=False,
Expand Down
44 changes: 35 additions & 9 deletions sqlite_utils/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import base64
import click
from click_default_group import DefaultGroup # type: ignore
from datetime import datetime
from datetime import datetime, timezone
import hashlib
import pathlib
from runpy import run_module
Expand Down Expand Up @@ -962,7 +962,7 @@ def insert_upsert_implementation(
db = sqlite_utils.Database(path)
_load_extensions(db, load_extension)
if functions:
_register_functions(db, functions)
_register_functions_from_multiple(db, functions)
if (delimiter or quotechar or sniff or no_headers) and not tsv:
csv = True
if (nl + csv + tsv) >= 2:
Expand Down Expand Up @@ -1370,7 +1370,9 @@ def upsert(
@click.argument("file", type=click.File("rb"), required=True)
@click.option("--batch-size", type=int, default=100, help="Commit every X records")
@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
@load_extension_option
Expand Down Expand Up @@ -1759,7 +1761,9 @@ def drop_view(path, view, ignore, load_extension):
help="Named :parameters for SQL query",
)
@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
def query(
Expand Down Expand Up @@ -1796,7 +1800,7 @@ def query(
db.register_fts4_bm25()

if functions:
_register_functions(db, functions)
_register_functions_from_multiple(db, functions)

_execute_query(
db,
Expand Down Expand Up @@ -1824,7 +1828,9 @@ def query(
)
@click.argument("sql")
@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(
"--attach",
Expand Down Expand Up @@ -1996,7 +2002,7 @@ def memory(
db.register_fts4_bm25()

if functions:
_register_functions(db, functions)
_register_functions_from_multiple(db, functions)

if return_db:
return db
Expand Down Expand Up @@ -3203,8 +3209,12 @@ def __init__(self, exception, path):
"ctime": lambda p: p.stat().st_ctime,
"mtime_int": lambda p: int(p.stat().st_mtime),
"ctime_int": lambda p: int(p.stat().st_ctime),
"mtime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_mtime).isoformat(),
"ctime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_ctime).isoformat(),
"mtime_iso": lambda p: datetime.fromtimestamp(p.stat().st_mtime, timezone.utc)
.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,
"stem": lambda p: p.stem,
"suffix": lambda p: p.suffix,
Expand Down Expand Up @@ -3281,6 +3291,13 @@ def _load_extensions(db, load_extension):

def _register_functions(db, 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)
globals = {}
try:
Expand All @@ -3291,3 +3308,12 @@ def _register_functions(db, functions):
for name, value in globals.items():
if callable(value) and not name.startswith("_"):
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)
Loading