mirror of
https://github.com/simonw/datasette.git
synced 2026-10-19 07:47:00 +02:00
Schema watcher: never recreate deleted files; O(1) derived-table cache key
- write connections reopened after the watcher saw the file replaced or deleted use mode=rw, so a write to a deleted database fails instead of silently creating a new empty file - derived_table_dependencies() keys its cache on the schema_version the watcher stored in the catalog (plus its scan count) instead of running PRAGMA schema_version on a pooled read connection - allowed_resources() calls it for every database on every page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwkLo2rJWidpkDVT3HkvNv
This commit is contained in:
parent
4492d3ee3d
commit
3ec3b490df
2 changed files with 25 additions and 1 deletions
|
|
@ -251,6 +251,11 @@ class Database:
|
|||
assert not (write and not self.is_mutable)
|
||||
if write:
|
||||
qs = ""
|
||||
state = self._watch_state
|
||||
if self._conn_generation or (state is not None and state.missing):
|
||||
# Reopening after the file was replaced or deleted: never
|
||||
# silently create a new empty database in its place
|
||||
qs = "?mode=rw"
|
||||
if self.mode is not None:
|
||||
qs = f"?mode={self.mode}"
|
||||
conn = sqlite3.connect(
|
||||
|
|
@ -1129,7 +1134,22 @@ class Database:
|
|||
|
||||
async def derived_table_dependencies(self):
|
||||
"""Return implementation tables and the tables they derive from."""
|
||||
schema_version = (await self.execute("PRAGMA schema_version")).first()[0]
|
||||
state = self._watch_state
|
||||
if (
|
||||
state is not None
|
||||
and state.catalog_version is not None
|
||||
and not state.needs_scan
|
||||
and not state.missing
|
||||
):
|
||||
# O(1): the schema_version the SchemaWatcher last stored in the
|
||||
# catalog. allowed_resources() calls this for every database on
|
||||
# every page, so a PRAGMA here was a per-request sweep of all
|
||||
# databases (one pooled connection each). The cache is now as
|
||||
# fresh as the catalog itself. The scan count is part of the key
|
||||
# because a replaced file can have the same schema_version.
|
||||
schema_version = ("catalog", state.catalog_version, state.stats["scans"])
|
||||
else:
|
||||
schema_version = (await self.execute("PRAGMA schema_version")).first()[0]
|
||||
if (
|
||||
self._cached_derived_table_dependencies is None
|
||||
or self._cached_derived_table_dependencies[0] != schema_version
|
||||
|
|
|
|||
|
|
@ -440,6 +440,10 @@ async def test_external_file_deleted_and_recreated(tmp_path, db_path):
|
|||
await wait_for(gone)
|
||||
response = await ds.client.get("/ext/t.json")
|
||||
assert response.status_code in (404, 500)
|
||||
# Writes fail instead of silently creating a new empty file
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
await ds.get_database("ext").execute_write("insert into t (a) values ('x')")
|
||||
assert not os.path.exists(db_path)
|
||||
make_db(db_path, sql="create table t (id integer primary key, a text, back text)")
|
||||
|
||||
async def back():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue