From 58dcedb5109b54140b33a6b776b8fb21baf70bad Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 31 May 2026 15:34:01 -0700 Subject: [PATCH] Skip RETURNING tests if SQLite version does not support it https://github.com/simonw/datasette/pull/2763#issuecomment-4588138314 --- datasette/utils/sqlite.py | 16 ++++++++++++++++ tests/test_internals_database.py | 13 ++++++++++++- tests/test_queries.py | 11 +++++++++++ tests/test_utils.py | 21 ++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/datasette/utils/sqlite.py b/datasette/utils/sqlite.py index 5a7c6c38..4743ae4c 100644 --- a/datasette/utils/sqlite.py +++ b/datasette/utils/sqlite.py @@ -13,6 +13,7 @@ if hasattr(sqlite3, "enable_callback_tracebacks"): sqlite3.enable_callback_tracebacks(True) _cached_sqlite_version = None +_cached_supports_returning = None SQLiteTableType = Literal["table", "view", "virtual", "shadow"] _VIRTUAL_TABLE_MODULE_RE = re.compile( r"\bCREATE\s+VIRTUAL\s+TABLE\b.*?\bUSING\s+([^\s(]+)", @@ -59,6 +60,21 @@ def supports_generated_columns(): return sqlite_version() >= (3, 31, 0) +def supports_returning(): + global _cached_supports_returning + if _cached_supports_returning is None: + conn = sqlite3.connect(":memory:") + try: + conn.execute("create table t (id integer primary key)") + conn.execute("insert into t default values returning id").fetchone() + _cached_supports_returning = True + except sqlite3.DatabaseError: + _cached_supports_returning = False + finally: + conn.close() + return _cached_supports_returning + + def sqlite_table_type( conn, table: str, diff --git a/tests/test_internals_database.py b/tests/test_internals_database.py index c167ac9c..bb209649 100644 --- a/tests/test_internals_database.py +++ b/tests/test_internals_database.py @@ -8,12 +8,16 @@ from datasette.app import Datasette from datasette.database import Database, ExecuteWriteResult, Results, MultipleValues from datasette.database import DatasetteClosedError from datasette.database import _deliver_write_result -from datasette.utils.sqlite import sqlite3 +from datasette.utils.sqlite import sqlite3, supports_returning from datasette.utils import Column import pytest import time import uuid +requires_sqlite_returning = pytest.mark.skipif( + not supports_returning(), reason="SQLite does not support RETURNING" +) + @pytest.fixture def db(app_client): @@ -481,6 +485,7 @@ async def test_execute_write_block_true(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning(db): await db.execute_write( "create table write_returning (id integer primary key, name text)" @@ -502,6 +507,7 @@ async def test_execute_write_with_returning(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_default_limit(db): await db.execute_write( "create table write_returning_limit (id integer primary key)" @@ -524,6 +530,7 @@ async def test_execute_write_with_returning_default_limit(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_custom_limit(db): await db.execute_write( "create table write_returning_custom (id integer primary key)" @@ -544,6 +551,7 @@ async def test_execute_write_with_returning_custom_limit(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_exact_default_limit(db): await db.execute_write( "create table write_returning_exact_limit (id integer primary key)" @@ -563,6 +571,7 @@ async def test_execute_write_with_returning_exact_default_limit(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_one_more_than_default_limit(db): await db.execute_write( "create table write_returning_one_more (id integer primary key)" @@ -582,6 +591,7 @@ async def test_execute_write_with_returning_one_more_than_default_limit(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_return_all(db): await db.execute_write("create table write_returning_all (id integer primary key)") await db.execute_write_many( @@ -611,6 +621,7 @@ async def test_execute_write_block_false(db): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_with_returning_block_false(db): await db.execute_write( "create table write_returning_block_false (id integer primary key, name text)" diff --git a/tests/test_queries.py b/tests/test_queries.py index d6d1185b..cef06d7f 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -8,6 +8,11 @@ from datasette.app import Datasette from datasette.resources import DatabaseResource, QueryResource from datasette.stored_queries import StoredQuery, StoredQueryPage from datasette.utils.asgi import Forbidden +from datasette.utils.sqlite import supports_returning + +requires_sqlite_returning = pytest.mark.skipif( + not supports_returning(), reason="SQLite does not support RETURNING" +) def _template_option_attributes(html, table): @@ -1891,6 +1896,7 @@ async def test_execute_write_post_requires_database_and_table_permissions(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_json_includes_returning_rows(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True @@ -1921,6 +1927,7 @@ async def test_execute_write_json_includes_returning_rows(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_json_returning_rows_can_be_truncated(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True @@ -1953,6 +1960,7 @@ async def test_execute_write_json_returning_rows_can_be_truncated(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_html_displays_returning_rows(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True @@ -1990,6 +1998,7 @@ async def test_execute_write_html_displays_returning_rows(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_execute_write_html_returning_rows_can_be_truncated(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True @@ -3135,6 +3144,7 @@ async def test_user_writable_query_execution_rechecks_table_permissions(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_stored_write_query_with_returning(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True @@ -3164,6 +3174,7 @@ async def test_stored_write_query_with_returning(): @pytest.mark.asyncio +@requires_sqlite_returning async def test_stored_write_query_with_truncated_returning_message(): ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True diff --git a/tests/test_utils.py b/tests/test_utils.py index f6de3b46..64607244 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,12 @@ Tests for various datasette helper functions. from datasette.app import Datasette from datasette import utils from datasette.utils.asgi import Request -from datasette.utils.sqlite import sqlite3, sqlite_hidden_table_names, sqlite_table_type +from datasette.utils.sqlite import ( + sqlite3, + sqlite_hidden_table_names, + sqlite_table_type, + supports_returning, +) import json import os import pathlib @@ -226,6 +231,20 @@ def test_detect_fts_different_table_names(table): conn.close() +def test_supports_returning(): + conn = utils.sqlite3.connect(":memory:") + try: + conn.execute("create table t (id integer primary key)") + conn.execute("insert into t default values returning id").fetchone() + expected = True + except sqlite3.DatabaseError: + expected = False + finally: + conn.close() + + assert supports_returning() is expected + + @pytest.mark.parametrize("use_fallback", (False, True)) def test_sqlite_table_type_detects_virtual_and_shadow_tables(monkeypatch, use_fallback): if use_fallback: