From e3f108e0f339e3d87ce48541bbca8f891bfaf040 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 29 Nov 2021 14:19:30 -0800 Subject: [PATCH] db.supports_strict and table.strict properties, refs #344 --- docs/python-api.rst | 16 ++++++++++++++-- sqlite_utils/db.py | 21 +++++++++++++++++++++ tests/test_introspect.py | 38 ++++++++++++++++++++++++++++---------- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/docs/python-api.rst b/docs/python-api.rst index 1c0dc60..5d391ee 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -1609,8 +1609,8 @@ A more useful example: if you are working with `SpatiaLite `__. + +:: + + >>> db["ny_times_us_counties"].strict + False + .. _python_api_introspection_indexes: .indexes diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index aebca89..fea1051 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -18,6 +18,7 @@ import json import os import pathlib import re +import secrets from sqlite_fts4 import rank_bm25 # type: ignore import sys import textwrap @@ -521,6 +522,19 @@ class Database: sqls.append(sql) return "\n".join(sqls) + @property + def supports_strict(self): + try: + table_name = "t{}".format(secrets.token_hex(16)) + with self.conn: + self.conn.execute( + "create table {} (name text) strict".format(table_name) + ) + self.conn.execute("drop table {}".format(table_name)) + return True + except Exception as e: + return False + @property def journal_mode(self) -> str: "Current ``journal_mode`` of this database." @@ -1219,6 +1233,13 @@ class Table(Queryable): "``{trigger_name: sql}`` dictionary of triggers defined on this table." return {trigger.name: trigger.sql for trigger in self.triggers} + @property + def strict(self) -> bool: + "Is this a STRICT table?" + table_suffix = self.schema.split(")")[-1].strip().upper() + table_options = [bit.strip() for bit in table_suffix.split(",")] + return "STRICT" in table_options + def create( self, columns: Dict[str, Any], diff --git a/tests/test_introspect.py b/tests/test_introspect.py index dce8afc..28a5009 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -252,15 +252,33 @@ def test_has_counts_triggers(fresh_db): ), ], ) -def test_virtual_table_using(sql, expected_name, expected_using): - db = Database(memory=True) - db.execute(sql) - assert db[expected_name].virtual_table_using == expected_using +def test_virtual_table_using(fresh_db, sql, expected_name, expected_using): + fresh_db.execute(sql) + assert fresh_db[expected_name].virtual_table_using == expected_using -def test_use_rowid(): - db = Database(memory=True) - db["rowid_table"].insert({"name": "Cleo"}) - db["regular_table"].insert({"id": 1, "name": "Cleo"}, pk="id") - assert db["rowid_table"].use_rowid - assert not db["regular_table"].use_rowid +def test_use_rowid(fresh_db): + fresh_db["rowid_table"].insert({"name": "Cleo"}) + fresh_db["regular_table"].insert({"id": 1, "name": "Cleo"}, pk="id") + assert fresh_db["rowid_table"].use_rowid + assert not fresh_db["regular_table"].use_rowid + + +@pytest.mark.skipif( + not Database(memory=True).supports_strict, + reason="Needs SQLite version that supports strict", +) +@pytest.mark.parametrize( + "create_table,expected_strict", + ( + ("create table t (id integer) strict", True), + ("create table t (id integer) STRICT", True), + ("create table t (id integer primary key) StriCt, WITHOUT ROWID", True), + ("create table t (id integer primary key) WITHOUT ROWID", False), + ("create table t (id integer)", False), + ), +) +def test_table_strict(fresh_db, create_table, expected_strict): + fresh_db.execute(create_table) + table = fresh_db["t"] + assert table.strict == expected_strict