Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support STRICT tables #344

Closed
simonw opened this issue Nov 29, 2021 · 14 comments
Closed

Support STRICT tables #344

simonw opened this issue Nov 29, 2021 · 14 comments
Labels

Comments

@simonw
Copy link
Owner

simonw commented Nov 29, 2021

New in SQLite 3.37.0, released a few days ago: https://www.sqlite.org/stricttables.html

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

From that page:

If you try to open a database containing the STRICT keyword in an earlier version of SQLite, it will not recognize the keyword and will report an error (except as noted below.

[...]

Because of a quirk in the SQL language parser, versions of SQLite prior to 3.37.0 can still read and write STRICT tables if they set "PRAGMA writable_schema=ON" immediately after opening the database file, prior to doing anything else that requires knowing the schema.

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

I'm going to build my own pysqlite3 wheel with the latest SQLite to try this out, following the instructions on https://github.com/coleifer/pysqlite3

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

This worked:

cd /tmp
mkdir sqlite-3.37
cd sqlite-3.37
wget 'https://www.sqlite.org/2021/sqlite-amalgamation-3370000.zip'
unzip sqlite-amalgamation-3370000.zip
git clone https://github.com/coleifer/pysqlite3/
cp sqlite-amalgamation-3370000/sqlite3.[ch] pysqlite3
cd pysqlite3
python3 setup.py build_static build bdist_wheel

This gave me a file here:

pysqlite3 % ls -l dist  
total 1872
-rw-r--r--  1 simon  wheel  956557 Nov 29 12:38 pysqlite3-0.4.6-cp39-cp39-macosx_10_15_x86_64.whl

That wheel only works when installed for Python 3.9 (it failed to install in a Python 3.10 virtual environment) - but pip install /tmp/sqlite-3.37/pysqlite3/dist/pysqlite3-0.4.6-cp39-cp39-macosx_10_15_x86_64.whl gave me a working pysqlite3 - and the following worked:

>>> import pysqlite3
>>> pysqlite3.connect(":memory:").execute("select sqlite_version()").fetchall()
[('3.37.0',)]

And if I install sqlite-utils in the same virtual environment this works:

% sqlite-utils memory 'select sqlite_version()'
[{"sqlite_version()": "3.37.0"}]

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

There are a few places that the strict=True option could go:

  • table.create() and table.create_table_sql()
  • The Database() constructor, to turn it on for all created tables
  • The .insert() / .insert_all() etc family of methods that can implicitly create tables

I'll definitely implement the first one, and likely the second one too. I'm on the fence with regards to the third one.

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

What should happen if you attempt to use strict=True against a SQLite version prior to 3.37.0?

An obvious error would be best... but how about silently ignoring it on older versions instead? That would match how we handle deterministic=True for registering functions:

def register(fn):
name = fn.__name__
arity = len(inspect.signature(fn).parameters)
if not replace and (name, arity) in self._registered_functions:
return fn
kwargs = {}
if deterministic and sys.version_info >= (3, 8):
kwargs["deterministic"] = True
self.conn.create_function(name, arity, fn, **kwargs)

@pytest.mark.skipif(
sys.version_info < (3, 8), reason="deterministic=True was added in Python 3.8"
)
def test_register_function_deterministic_registered(fresh_db):

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

I'm leaning towards silently ignore if SQLite version doesn't support it. That means that the first time you attempt to use strict=True we will need to run a test against the database connection to see what version of SQLite it uses, then cache the result to avoid making the same call again for the same connection.

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

Is there a need for an introspection function for telling if a SQLite table is in strict mode or not? How would that work?

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

From the STRICT docs:

The SQLite parser accepts a comma-separated list of table options after the final close parenthesis in a CREATE TABLE statement. As of this writing (2021-08-23) only two options are recognized:

So I think I need to read the CREATE TABLE statement from the sqlite_master table, split on the last ), split those tokens on , and see if create is in there (case insensitive).

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

Made myself a test strict table like so:

>>> import pysqlite3
>>> conn = pysqlite3.connect("/tmp/strict-table.db")
>>> conn.execute("create table foo (id integer primary key, name text, weight real) strict")
<pysqlite3.dbapi2.Cursor object at 0x104317340>
>>> cursor = conn.cursor()
>>> with conn:
...     cursor.execute("insert into foo (name, weight) values ('Lila', '2.31')")
<pysqlite3.dbapi2.Cursor object at 0x10331e1f0>
>>> conn.close()

Uploaded that to: https://static.simonwillison.net/static/2021/strict-table.db

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

Here's a function that detects if strict is supported or not:

import secrets
import sqlite3

def supports_strict_tables(db = None):
    if db is None:
        db = sqlite3.connect(":memory:")
    try:
        table_name = 't{}'.format(secrets.token_hex(16))
        with db:
            db.execute("create table {} (name text) strict".format(table_name))
            db.execute("drop table {}".format(table_name))
        return True
    except:
        return False

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

I'm going to add that as db.supports_strict.

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

I haven't documented db.supports_strict yet (I documented table.strict) because there wasn't an obvious section of the documentation for it.

I need to remember to document it once I add documentation for the strict=True parameter.

@simonw
Copy link
Owner Author

simonw commented Nov 29, 2021

Need to figure out a good pattern for testing this in CI too - it will currently skip the new tests if it doesn't have SQLite 3.37 or higher.

simonw added a commit that referenced this issue Nov 29, 2021
simonw added a commit that referenced this issue Nov 29, 2021
simonw added a commit that referenced this issue Nov 29, 2021
simonw added a commit that referenced this issue Jan 6, 2022
tkhattra added a commit to tkhattra/sqlite-utils that referenced this issue Nov 17, 2023
Make table.transform() preserve STRICT mode.
@tkhattra
Copy link
Contributor

hello Simon,

I've added more STRICT table support per #344 (comment) in changeset e4b9b58.
It also fixes table.transform() to preserve STRICT mode.
Please pull if you deem appropriate. Thanks!

simonw pushed a commit that referenced this issue Dec 8, 2023
* Add more STRICT table support per #344 (comment).
* Make `table.transform()` preserve STRICT mode.
* Fix mypy failures in PR #604
* Link to SQLITE strict page in a few places
@simonw simonw closed this as completed in f29189a Dec 8, 2023
simonw added a commit that referenced this issue Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants