datasette/tests/test_database_watcher.py
Claude 56dd5fe510
Add database file watcher using PRAGMA data_version
Monitors all file-backed databases for changes by polling PRAGMA data_version
via a dedicated sqlite3 connection per database. Detected changes update
datasette._database_updated with the time.monotonic() timestamp. Watchers
start on ASGI startup and are cancelled on shutdown. The /-/databases endpoint
now includes a "last_updated" field for each database.

https://claude.ai/code/session_018W3THaecoPG4K8pFuthz62
2026-03-16 19:21:53 +00:00

110 lines
3.1 KiB
Python

import asyncio
import sqlite3
import tempfile
import time
import pytest
import pytest_asyncio
from datasette.app import Datasette
@pytest_asyncio.fixture
async def ds_with_file_db():
tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
tmp_path = tmp.name
tmp.close()
conn = sqlite3.connect(tmp_path)
conn.execute("CREATE TABLE t (id integer primary key)")
conn.close()
ds = Datasette(files=[tmp_path])
client = ds.client
# Trigger startup (which starts the watchers)
await ds.invoke_startup()
await ds._start_database_watchers()
try:
yield ds, client, tmp_path
finally:
await ds._stop_database_watchers()
@pytest.mark.asyncio
async def test_database_updated_initially_empty(ds_with_file_db):
ds, client, tmp_path = ds_with_file_db
db_name = list(ds.databases.keys())[0]
assert db_name not in ds._database_updated
@pytest.mark.asyncio
async def test_database_updated_after_write(ds_with_file_db):
ds, client, tmp_path = ds_with_file_db
db_name = list(ds.databases.keys())[0]
if db_name == "_memory":
db_name = list(ds.databases.keys())[1]
# Write to the database
conn = sqlite3.connect(tmp_path)
conn.execute("INSERT INTO t (id) VALUES (1)")
conn.commit()
conn.close()
# Wait for the watcher to detect the change (interval is 1s by default)
await asyncio.sleep(1.5)
assert db_name in ds._database_updated
assert isinstance(ds._database_updated[db_name], float)
@pytest.mark.asyncio
async def test_databases_endpoint_includes_last_updated(ds_with_file_db):
ds, client, tmp_path = ds_with_file_db
db_name = list(ds.databases.keys())[0]
if db_name == "_memory":
db_name = list(ds.databases.keys())[1]
# Before any writes, last_updated should be None
response = await client.get("/-/databases.json")
databases = response.json()
file_db = [d for d in databases if d["name"] == db_name][0]
assert file_db["last_updated"] is None
# Write to the database
conn = sqlite3.connect(tmp_path)
conn.execute("INSERT INTO t (id) VALUES (1)")
conn.commit()
conn.close()
# Wait for the watcher to detect the change
await asyncio.sleep(1.5)
response = await client.get("/-/databases.json")
databases = response.json()
file_db = [d for d in databases if d["name"] == db_name][0]
assert file_db["last_updated"] is not None
assert isinstance(file_db["last_updated"], float)
@pytest.mark.asyncio
async def test_database_updated_timestamp_increases(ds_with_file_db):
ds, client, tmp_path = ds_with_file_db
db_name = list(ds.databases.keys())[0]
if db_name == "_memory":
db_name = list(ds.databases.keys())[1]
# First write
conn = sqlite3.connect(tmp_path)
conn.execute("INSERT INTO t (id) VALUES (1)")
conn.commit()
conn.close()
await asyncio.sleep(1.5)
first_ts = ds._database_updated[db_name]
# Second write
conn = sqlite3.connect(tmp_path)
conn.execute("INSERT INTO t (id) VALUES (2)")
conn.commit()
conn.close()
await asyncio.sleep(1.5)
second_ts = ds._database_updated[db_name]
assert second_ts > first_ts