fix(state): skip the display-trigger drop+recreate when the triggers already match
DEFERRED_INDEX_SQL unconditionally DROPs and re-CREATEs the four display triggers on every open so a changed trigger body rolls out; DROP TRIGGER IF EXISTS on an existing trigger takes the write lock, so a settled database still blocked behind a sibling's transaction after the three statement gates. Compare each trigger's stored sql against the desired CREATE text and run the pair only when they differ. CREATE ... IF NOT EXISTS forms are lock-free on an existing object and run as written. The no-writes test now also traces DROP TRIGGER / ALTER.
This commit is contained in:
@@ -282,6 +282,41 @@ class SessionSchemaMixin:
|
||||
high_water = cursor.execute("SELECT COALESCE(MAX(id), 0) FROM messages").fetchone()[0]
|
||||
cursor.execute(_STATE_META_UPSERT_SQL, (FTS_TOOL_FULL_CONTENT_HIGH_WATER_KEY, str(high_water)))
|
||||
|
||||
@staticmethod
|
||||
def _execute_ddl_skipping_settled_triggers(cursor: sqlite3.Cursor, ddl: str) -> None:
|
||||
"""Run *ddl* statement by statement, skipping each ``DROP TRIGGER IF EXISTS x`` /
|
||||
``CREATE TRIGGER IF NOT EXISTS x …`` pair whose trigger already exists with the same body.
|
||||
|
||||
``DROP TRIGGER IF EXISTS`` on an existing trigger takes SQLite's write lock (the
|
||||
``CREATE … IF NOT EXISTS`` forms do not), so the unconditional drop+recreate that lets a
|
||||
changed trigger body roll out would otherwise block every open of a settled database
|
||||
behind a sibling process's transaction. ``sqlite_master.sql`` stores the CREATE text
|
||||
verbatim minus ``IF NOT EXISTS`` and the ``;``, so an exact comparison decides.
|
||||
"""
|
||||
stored = dict(cursor.execute("SELECT name, sql FROM sqlite_master WHERE type = 'trigger'").fetchall())
|
||||
pending_drop: Optional[str] = None
|
||||
statement = ""
|
||||
for line in ddl.splitlines():
|
||||
statement += line + "\n"
|
||||
if not sqlite3.complete_statement(statement):
|
||||
continue
|
||||
statement, current = "", statement.strip()
|
||||
upper = current.upper()
|
||||
if upper.startswith("DROP TRIGGER IF EXISTS "):
|
||||
pending_drop = current
|
||||
continue
|
||||
if upper.startswith("CREATE TRIGGER IF NOT EXISTS "):
|
||||
desired = current.replace("IF NOT EXISTS ", "", 1).rstrip(";")
|
||||
if stored.get(desired.split(None, 3)[2]) == desired:
|
||||
pending_drop = None
|
||||
continue
|
||||
if pending_drop is not None:
|
||||
cursor.execute(pending_drop)
|
||||
pending_drop = None
|
||||
cursor.execute(current)
|
||||
if statement.strip():
|
||||
raise sqlite3.OperationalError("incomplete DDL statement")
|
||||
|
||||
@staticmethod
|
||||
def _execute_ddl_script_transactional(cursor: sqlite3.Cursor, ddl: str) -> None:
|
||||
"""Execute a DDL script without ``executescript``'s implicit commit."""
|
||||
@@ -884,7 +919,7 @@ class SessionSchemaMixin:
|
||||
)
|
||||
except sqlite3.OperationalError as exc:
|
||||
logger.debug("idx_messages_platform_msg_id create skipped: %s", exc)
|
||||
cursor.executescript(DEFERRED_INDEX_SQL) # same ordering constraint (``active``)
|
||||
self._execute_ddl_skipping_settled_triggers(cursor, DEFERRED_INDEX_SQL) # same ordering constraint (``active``)
|
||||
|
||||
# Heal NULL ``active`` rows on every startup: older reconciler builds added ``active``
|
||||
# without NOT NULL DEFAULT 1, so ``WHERE active = 1`` loaders hid whole histories. A
|
||||
|
||||
@@ -640,7 +640,7 @@ class TestMessageStorage:
|
||||
conn = real_connect(*args, **kwargs)
|
||||
conn.set_trace_callback(
|
||||
lambda stmt: writes.append(stmt)
|
||||
if re.match(r"\s*(INSERT|UPDATE|DELETE|REPLACE)\b", stmt, re.I) and "temp." not in stmt
|
||||
if re.match(r"\s*(INSERT|UPDATE|DELETE|REPLACE|ALTER|DROP\s+TRIGGER)\b", stmt, re.I) and "temp." not in stmt
|
||||
else None
|
||||
)
|
||||
return conn
|
||||
|
||||
Reference in New Issue
Block a user