Add a skip_on_away flag to quest_definitions (migration 8, additive column defaulting to 1 so existing rows keep today's behaviour). The admin create/edit routes accept it as an optional boolean (create defaults true, an omitted edit field keeps the stored value) and the definition payload returns it. Materialisation generates instances on absent dates when the flag is false. Task: ded75ab8-0219-4d52-aefa-82c09e0b5e6b Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
428 lines
19 KiB
Python
428 lines
19 KiB
Python
"""Declarative schema DDL for the NestQuest SQLite database.
|
|
|
|
Every v1 table is declared here as plain SQL statements so later schema
|
|
tasks can append their own DDL to :data:`SCHEMA_V1_STATEMENTS` without
|
|
touching this module's tables. All statements use
|
|
``CREATE TABLE IF NOT EXISTS`` so application is idempotent; the versioned
|
|
migration runner is a separate later concern.
|
|
|
|
Timestamp policy: all timestamps are UTC ISO-8601 strings, set by the
|
|
caller/DAO rather than a DB default, per the feature guardrails. No
|
|
``length(created_at) = 25`` CHECK is added: the exact string shape
|
|
produced by callers is not fixed yet (``datetime.isoformat()`` may
|
|
include microseconds, changing the length), so enforcing a fixed length
|
|
here would reject valid values. Only non-null is enforced for now.
|
|
|
|
Date policy: ``start_date``/``end_date`` are ISO date strings (YYYY-MM-DD,
|
|
no time component) compared lexically by the ``end_date >= start_date``
|
|
CHECK; due_date/due_time string shapes are caller-validated by the rule
|
|
engine and DAO layers, not by DDL.
|
|
|
|
``weekday_set`` format: comma-separated weekday integers (CSV), 0 =
|
|
Monday .. 6 = Sunday, e.g. ``'0,2,4'`` for Mon/Wed/Fri. Chosen over JSON
|
|
because it sorts, compares and parses trivially in SQL. The format is
|
|
enforced at the DB level: a CHECK matches ``weekday_set`` against GLOB
|
|
patterns for element counts 1..7 of single digits 0-6 (GLOB has no
|
|
repetition quantifier, so the element count is enumerated; more than 7
|
|
elements is impossible for a weekday set). ``''``, ``'x'``, ``'7'`` and
|
|
``'0,x'`` are all rejected; NULL stays allowed for non-weekly rules.
|
|
This keeps the schema layer storage-only — the DAO layer (tasks
|
|
8c72300c+) does not need its own validator, it can rely on the CHECK.
|
|
|
|
Type affinity: SQLite applies column affinity on insert, but CHECKs
|
|
still observe text and fractional values ('abc' stays text, '1.5'
|
|
becomes REAL), so every INTEGER column carrying rule semantics carries
|
|
a ``typeof(...) = 'integer'`` guard in its CHECK.
|
|
|
|
Rule-type coherence: two CHECKs enforce that ``weekly`` carries a
|
|
non-null ``weekday_set``, ``monthly`` carries a ``day_of_month`` or an
|
|
``nth_weekday``, ``yearly`` carries a ``month``, and ``daily``/``custom``
|
|
carry none of the month-specific fields. Relaxed deliberately: the DDL
|
|
does NOT forbid ``month`` on weekly/monthly rules (an "every 2 weeks"
|
|
rule or a monthly rule could later be pinned to a month) nor extra
|
|
fields on weekly/monthly/yearly rules generally, so the CHECKs only
|
|
reject what the rule engine definitively cannot express now.
|
|
|
|
Presence pattern format: ``presence_schedules.pattern`` encodes the
|
|
whole N-week repeating cycle in one TEXT column as pipe-separated
|
|
segments, one per week of the cycle: segment ``i`` is a ``weekday_set``
|
|
CSV (same single-digit 0-6 convention as ``schedule_rules.weekday_set``)
|
|
for week ``i``, e.g. ``'0,2,4|1,3'`` = Mon/Wed/Fri in week 0, Tue/Thu in
|
|
week 1. An empty segment means the child is absent every day of that
|
|
week; a wholly empty pattern with ``cycle_length_weeks = 1`` means
|
|
absent every day of every week, which is distinct from having no
|
|
schedule row at all (a child with no schedule is present every day, per
|
|
the feature guardrails). Chosen over JSON or per-week rows to stay
|
|
consistent with the CSV ``weekday_set`` convention and fully checkable
|
|
by plain CHECKs. DB-level validation is pure CHECK, no triggers or
|
|
custom functions: char-class GLOBs reject every character outside
|
|
digits 0-6, comma and pipe; adjacency GLOBs reject empty elements
|
|
(double comma, comma next to a pipe, leading/trailing comma) and
|
|
two-digit runs; a further GLOB rejects contiguous runs of 8+ comma
|
|
separated digits, which caps a segment at 7 elements — the GLOB cannot
|
|
match across a ``|``, so cross-segment boundaries stay safe; and the
|
|
segment count is pinned to ``cycle_length_weeks`` by
|
|
``LENGTH(pattern) - LENGTH(REPLACE(pattern, '|', '')) + 1``. Per
|
|
weekday_set this is not full CSV-set normalisation (duplicates like
|
|
``'0,0'`` or unsorted sets stay storable, as with ``schedule_rules``);
|
|
the engine treats the segment as a set. A
|
|
one-row-per-``week_index`` normalization was considered and rejected:
|
|
the flat single-column encoding keeps the done-condition's table shape
|
|
and proved fully checkable with plain CHECKs.
|
|
|
|
``cycle_length_weeks`` is capped at 4: 2 (alternating weeks) is the
|
|
common case, and a small cap keeps anchor arithmetic and the encoded
|
|
pattern human-readable. Revisit the cap — and re-run the pattern
|
|
validation matrix in tests — if a longer cycle is ever needed.
|
|
``anchor_date`` is an ISO date string pinning the cycle: the week index
|
|
of a date is (days since anchor) // 7 mod ``cycle_length_weeks``,
|
|
computed by the presence engine (Feature 05) from anchor-date
|
|
arithmetic, never ISO week parity. Like other date columns it carries
|
|
no shape CHECK, matching the established date policy.
|
|
|
|
Presence overrides: ``presence_overrides.end_date`` is NOT NULL,
|
|
unlike ``schedule_rules.end_date`` — an override is always a concrete
|
|
date range; a single-day override stores the same date in both columns.
|
|
``is_present`` 0/1 marks the child absent/present for the whole range;
|
|
``note`` is optional free text.
|
|
|
|
Multi-assignee definitions (D-008): ``quest_definitions`` carries no
|
|
``child_id`` column. Assignment lives in
|
|
``quest_definition_assignees``, one row per (definition, child) with a
|
|
composite primary key and foreign keys to the definition and the child,
|
|
so "brush teeth" is one definition covering three children. The
|
|
migration runner rebuilds a pre-D-008 table into this shape and copies
|
|
its single-assignee column across. The composite primary key makes
|
|
duplicate assignment impossible at the storage layer.
|
|
|
|
Quest windows (D-008): ``quest_definition_windows`` declares which day
|
|
windows a definition spans — one row per (definition, window), the
|
|
composite primary key making a window idempotent per definition.
|
|
``window`` is CHECK-constrained to the three spellings ``const`` owns;
|
|
``due_time`` optionally pins a due time inside the window and is
|
|
caller-validated (strict HH:MM) by the DAO layer, matching the date
|
|
policy above. The window clock ranges live in ``const.py``, not here:
|
|
this layer stores what the caller declares and never interprets clocks.
|
|
|
|
Quest instances: ``quest_instances`` deliberately carries NO completion
|
|
status column. An instance's current state derives from the latest row
|
|
in ``completion_events`` (Feature 08), so materialization (Feature 07)
|
|
can insert instances without knowing anything about completion and the
|
|
append-only log stays the single source of truth. ``definition_id``,
|
|
``child_id``, ``due_date`` and ``window`` snapshot the
|
|
materialization-time facts ("why this instance exists"): assignment
|
|
edits and rule edits change future instances only and never rewrite
|
|
these rows. ``window`` records which declared day window produced the
|
|
instance (D-008), CHECK-constrained to the same three spellings
|
|
``const`` owns. ``due_time`` snapshots the window's optional due time
|
|
at generation for the same reason — the instance keeps its
|
|
generation-time value even if the definition is edited later.
|
|
``generated_at`` is a caller-set UTC ISO-8601 timestamp per the module
|
|
timestamp policy (no DB default, so a generation batch stamps one
|
|
coherent time).
|
|
|
|
Uniqueness: the (definition_id, child_id, due_date, window) tuple is
|
|
declared as a table-level ``UNIQUE`` constraint rather than a separate
|
|
``CREATE UNIQUE INDEX`` statement. SQLite materializes it as an
|
|
implicit unique index (``sqlite_autoindex_quest_instances_1``) — the
|
|
required "unique index prevents duplicate instances" — and the
|
|
constraint form cannot be silently dropped the way a standalone index
|
|
can, so the duplicate door stays shut. Idempotent materialization keys
|
|
on exactly this tuple: the twice-daily case (one definition, two
|
|
windows, one child, one date) yields two rows and the shared case (one
|
|
definition, three assignees, one window) yields three. No secondary
|
|
indexes beyond it are declared yet: history and board queries (Features
|
|
08/10/13) should justify their indexes as a later migration once their
|
|
query shapes exist.
|
|
|
|
No ``ON DELETE`` clauses are declared on any foreign key in this
|
|
module: children and definitions are deactivated, never deleted, so a
|
|
deletion that would orphan instances or events is a bug and must fail
|
|
loudly rather than cascade.
|
|
|
|
Completion events: ``completion_events`` is strictly append-only. That
|
|
property is enforced at the DAO layer — the completion-events DAO
|
|
exposes no UPDATE or DELETE path (task a9ea75fc) — and deliberately NOT
|
|
by database triggers, matching the module's no-trigger policy: a RAISE
|
|
trigger would be the one exception to "pure CHECK, no triggers",
|
|
complicate the later migration runner, and still prove nothing (any
|
|
writer can run raw SQL). ``event_type`` is constrained to
|
|
``completed``/``uncompleted``; a ``missed`` row is intentionally not
|
|
representable — missed is a derived state announced on the HA event bus
|
|
(Feature 10), never an appended event.
|
|
|
|
``child_id`` is deliberately denormalized (it is also derivable via
|
|
``quest_instances``): history reporting (Feature 13) filters and rates
|
|
per child directly against the log, and the log should stay
|
|
self-describing without joins.
|
|
|
|
Actor policy: ``actor_source`` distinguishes an authenticated HA user
|
|
(``'user'``, with ``actor_user_id`` holding the HA user id) from a
|
|
panel tap with no user context (``'panel'``, with ``actor_user_id``
|
|
NULL — the tapped child profile is ``actor_child_id``, D-008). Two
|
|
coherence CHECKs make both shapes non-negotiable: ``'user'`` requires
|
|
``actor_user_id`` and forbids ``actor_child_id``; ``'panel'`` requires
|
|
``actor_child_id`` (the tapped profile) and forbids ``actor_user_id``.
|
|
Other actor sources are rejected; if an unattended system actor is
|
|
ever needed, that is a schema migration, not a silent widening.
|
|
|
|
On-time policy: ``was_on_time`` is required (integer 0 or 1) for
|
|
``completed`` events — at completion time the due date is known and the
|
|
flag is decidable. For ``uncompleted`` events the flag is optional:
|
|
Feature 08 decides whether a reversal carries the reversed completion's
|
|
on-time flag or NULL, and the DDL deliberately accepts both rather than
|
|
guessing.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from .db import NestQuestDatabase
|
|
|
|
#: GLOB patterns matching a valid ``weekday_set`` CSV: one GLOB per
|
|
#: possible element count 1..7 (a weekday set cannot exceed 7 elements).
|
|
#: Each element is a single digit 0-6, separated by a literal comma.
|
|
#: GLOB has no repetition quantifier, so the element count is enumerated.
|
|
#: ``''`` and NULL are deliberately NOT matched: NULL is allowed for
|
|
#: non-weekly rules via a separate ``IS NULL`` arm, and '' is invalid
|
|
#: because weekly rules need at least one weekday.
|
|
WEEKDAY_SET_GLOB_PATTERNS: list[str] = [
|
|
"[0-6]",
|
|
"[0-6],[0-6]",
|
|
"[0-6],[0-6],[0-6]",
|
|
"[0-6],[0-6],[0-6],[0-6]",
|
|
"[0-6],[0-6],[0-6],[0-6],[0-6]",
|
|
"[0-6],[0-6],[0-6],[0-6],[0-6],[0-6]",
|
|
"[0-6],[0-6],[0-6],[0-6],[0-6],[0-6],[0-6]",
|
|
]
|
|
|
|
SCHEMA_V1_CHILDREN_DDL: list[str] = [
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS children (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
display_name TEXT NOT NULL,
|
|
colour TEXT,
|
|
avatar_ref TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0 CHECK (typeof(sort_order) = 'integer'),
|
|
is_active INTEGER NOT NULL DEFAULT 1 CHECK (typeof(is_active) = 'integer' AND is_active IN (0, 1)),
|
|
created_at TEXT NOT NULL
|
|
)
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_ADMIN_USERS_DDL: list[str] = [
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS admin_users (
|
|
ha_user_id TEXT PRIMARY KEY NOT NULL,
|
|
added_at TEXT NOT NULL
|
|
)
|
|
""",
|
|
]
|
|
|
|
_WEEKDAY_SET_CHECK = " OR ".join(
|
|
f"weekday_set GLOB '{pattern}'" for pattern in WEEKDAY_SET_GLOB_PATTERNS
|
|
)
|
|
|
|
SCHEMA_V1_SCHEDULE_RULES_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS schedule_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
rule_type TEXT NOT NULL CHECK (rule_type IN ('daily', 'weekly', 'monthly', 'yearly', 'custom')),
|
|
interval INTEGER NOT NULL DEFAULT 1 CHECK (typeof(interval) = 'integer' AND interval >= 1),
|
|
weekday_set TEXT CHECK (weekday_set IS NULL OR ({_WEEKDAY_SET_CHECK})),
|
|
day_of_month INTEGER CHECK (day_of_month IS NULL OR (typeof(day_of_month) = 'integer' AND day_of_month >= 1 AND day_of_month <= 31)),
|
|
nth_weekday INTEGER CHECK (nth_weekday IS NULL OR (typeof(nth_weekday) = 'integer' AND nth_weekday IN (-1, 1, 2, 3, 4, 5))),
|
|
month INTEGER CHECK (month IS NULL OR (typeof(month) = 'integer' AND month >= 1 AND month <= 12)),
|
|
start_date TEXT NOT NULL,
|
|
end_date TEXT,
|
|
CHECK (end_date IS NULL OR end_date >= start_date),
|
|
CHECK (
|
|
(rule_type = 'weekly' AND weekday_set IS NOT NULL)
|
|
OR (rule_type = 'monthly' AND (day_of_month IS NOT NULL OR nth_weekday IS NOT NULL))
|
|
OR (rule_type = 'yearly' AND month IS NOT NULL)
|
|
OR (rule_type IN ('daily', 'custom') AND day_of_month IS NULL AND nth_weekday IS NULL AND month IS NULL)
|
|
)
|
|
)
|
|
""",
|
|
]
|
|
|
|
#: Column body of ``quest_definitions``, shared between the v1 DDL
|
|
#: below and the multi-assignee rebuild migration, which must recreate
|
|
#: the table in exactly this shape (D-008: one definition, many
|
|
#: assignees via ``quest_definition_assignees`` — no ``child_id``
|
|
#: column on the definition itself).
|
|
QUEST_DEFINITIONS_TABLE_SQL = """(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
icon TEXT,
|
|
schedule_rule_id INTEGER NOT NULL REFERENCES schedule_rules(id),
|
|
due_time TEXT,
|
|
is_active INTEGER NOT NULL DEFAULT 1 CHECK (typeof(is_active) = 'integer' AND is_active IN (0, 1)),
|
|
created_at TEXT NOT NULL,
|
|
skip_on_away INTEGER NOT NULL DEFAULT 1
|
|
)"""
|
|
|
|
SCHEMA_V1_QUEST_DEFINITIONS_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS quest_definitions {QUEST_DEFINITIONS_TABLE_SQL}
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_QUEST_DEFINITION_ASSIGNEES_DDL: list[str] = [
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS quest_definition_assignees (
|
|
definition_id INTEGER NOT NULL REFERENCES quest_definitions(id),
|
|
child_id INTEGER NOT NULL REFERENCES children(id),
|
|
PRIMARY KEY (definition_id, child_id)
|
|
)
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_QUEST_DEFINITION_WINDOWS_DDL: list[str] = [
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS quest_definition_windows (
|
|
definition_id INTEGER NOT NULL REFERENCES quest_definitions(id),
|
|
window TEXT NOT NULL CHECK (window IN ('morning', 'afternoon', 'evening')),
|
|
due_time TEXT,
|
|
PRIMARY KEY (definition_id, window)
|
|
)
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_PRESENCE_SCHEDULES_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS presence_schedules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
child_id INTEGER NOT NULL UNIQUE REFERENCES children(id),
|
|
cycle_length_weeks INTEGER NOT NULL CHECK (typeof(cycle_length_weeks) = 'integer' AND cycle_length_weeks >= 1 AND cycle_length_weeks <= 4),
|
|
anchor_date TEXT NOT NULL,
|
|
pattern TEXT NOT NULL CHECK (
|
|
NOT pattern GLOB '*[^0-6,|]*'
|
|
AND NOT pattern GLOB '*[0-6][0-6]*'
|
|
AND NOT pattern GLOB '*,,*'
|
|
AND NOT pattern GLOB '*,|*'
|
|
AND NOT pattern GLOB '*|,*'
|
|
AND NOT pattern GLOB ',*'
|
|
AND NOT pattern GLOB '*,'
|
|
AND NOT pattern GLOB '*[0-6],[0-6],[0-6],[0-6],[0-6],[0-6],[0-6],[0-6]*'
|
|
AND (LENGTH(pattern) - LENGTH(REPLACE(pattern, '|', '')) + 1) = cycle_length_weeks
|
|
)
|
|
)
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_PRESENCE_OVERRIDES_DDL: list[str] = [
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS presence_overrides (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
child_id INTEGER NOT NULL REFERENCES children(id),
|
|
start_date TEXT NOT NULL,
|
|
end_date TEXT NOT NULL,
|
|
is_present INTEGER NOT NULL CHECK (typeof(is_present) = 'integer' AND is_present IN (0, 1)),
|
|
note TEXT,
|
|
CHECK (end_date >= start_date)
|
|
)
|
|
""",
|
|
]
|
|
|
|
#: Column body of ``quest_instances``, shared between the v1 DDL
|
|
#: below and the instance-key rebuild migration, which must recreate
|
|
#: the table in exactly this shape (D-008: the instance key is
|
|
#: (definition_id, child_id, due_date, window) — one instance per
|
|
#: assigned child per declared window per firing date).
|
|
QUEST_INSTANCES_TABLE_SQL = """(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
definition_id INTEGER NOT NULL REFERENCES quest_definitions(id),
|
|
child_id INTEGER NOT NULL REFERENCES children(id),
|
|
window TEXT NOT NULL CHECK (window IN ('morning', 'afternoon', 'evening')),
|
|
due_date TEXT NOT NULL,
|
|
due_time TEXT,
|
|
generated_at TEXT NOT NULL,
|
|
UNIQUE (definition_id, child_id, due_date, window)
|
|
)"""
|
|
|
|
SCHEMA_V1_QUEST_INSTANCES_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS quest_instances {QUEST_INSTANCES_TABLE_SQL}
|
|
""",
|
|
]
|
|
|
|
#: Column body of ``completion_events``, shared between the v1 DDL
|
|
#: below and the actor-child rebuild migration, which must recreate the
|
|
#: table in exactly this shape (D-008: ``actor_child_id`` records which
|
|
#: panel profile was tapped — set for panel events, NULL for admin
|
|
#: ones; legacy panel rows backfill to ``child_id``, the child they
|
|
#: always were).
|
|
COMPLETION_EVENTS_TABLE_SQL = """(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
instance_id INTEGER NOT NULL REFERENCES quest_instances(id),
|
|
child_id INTEGER NOT NULL REFERENCES children(id),
|
|
event_type TEXT NOT NULL CHECK (event_type IN ('completed', 'uncompleted')),
|
|
actor_source TEXT NOT NULL CHECK (actor_source IN ('user', 'panel')),
|
|
actor_user_id TEXT,
|
|
actor_child_id INTEGER REFERENCES children(id),
|
|
occurred_at TEXT NOT NULL,
|
|
was_on_time INTEGER CHECK (
|
|
was_on_time IS NULL
|
|
OR (typeof(was_on_time) = 'integer' AND was_on_time IN (0, 1))
|
|
),
|
|
CHECK (
|
|
(actor_source = 'user' AND actor_user_id IS NOT NULL)
|
|
OR (actor_source = 'panel' AND actor_user_id IS NULL)
|
|
),
|
|
CHECK (
|
|
(actor_source = 'user' AND actor_child_id IS NULL)
|
|
OR (actor_source = 'panel' AND actor_child_id IS NOT NULL)
|
|
),
|
|
CHECK (
|
|
(event_type = 'completed' AND was_on_time IS NOT NULL)
|
|
OR (event_type = 'uncompleted')
|
|
)
|
|
)"""
|
|
|
|
SCHEMA_V1_COMPLETION_EVENTS_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS completion_events {COMPLETION_EVENTS_TABLE_SQL}
|
|
""",
|
|
]
|
|
|
|
#: Generic integration-owned key/value state (schema version 7,
|
|
#: Feature 11): persistent bookkeeping that belongs to the integration
|
|
#: rather than any domain table — currently the missed-sweep
|
|
#: watermark. ``value`` is a plain string; callers interpret it.
|
|
META_STATE_TABLE_SQL = """(
|
|
key TEXT NOT NULL PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
)"""
|
|
|
|
SCHEMA_V7_META_STATE_DDL: list[str] = [
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS nestquest_meta_state {META_STATE_TABLE_SQL}
|
|
""",
|
|
]
|
|
|
|
SCHEMA_V1_STATEMENTS: list[str] = [
|
|
*SCHEMA_V1_CHILDREN_DDL,
|
|
*SCHEMA_V1_ADMIN_USERS_DDL,
|
|
*SCHEMA_V1_SCHEDULE_RULES_DDL,
|
|
*SCHEMA_V1_QUEST_DEFINITIONS_DDL,
|
|
*SCHEMA_V1_QUEST_DEFINITION_ASSIGNEES_DDL,
|
|
*SCHEMA_V1_QUEST_DEFINITION_WINDOWS_DDL,
|
|
*SCHEMA_V1_PRESENCE_SCHEDULES_DDL,
|
|
*SCHEMA_V1_PRESENCE_OVERRIDES_DDL,
|
|
*SCHEMA_V1_QUEST_INSTANCES_DDL,
|
|
*SCHEMA_V1_COMPLETION_EVENTS_DDL,
|
|
*SCHEMA_V7_META_STATE_DDL,
|
|
]
|
|
|
|
|
|
async def async_apply_ddl(
|
|
database: NestQuestDatabase, statements: list[str]
|
|
) -> None:
|
|
"""Apply each DDL statement inside one transaction on ``database``.
|
|
|
|
Idempotent: every statement is ``CREATE TABLE IF NOT EXISTS``, so
|
|
applying the same list twice leaves the schema and its rows intact.
|
|
"""
|
|
async with database.transaction():
|
|
for sql in statements:
|
|
await database.execute(sql) |