Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf4b876a5 | ||
|
|
8e0baaa5d9 |
@@ -42,111 +42,6 @@ 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
|
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
|
fields on weekly/monthly/yearly rules generally, so the CHECKs only
|
||||||
reject what the rule engine definitively cannot express now.
|
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.
|
|
||||||
|
|
||||||
Task instances: ``task_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``,
|
|
||||||
``due_date`` and ``child_id`` snapshot the materialization-time facts
|
|
||||||
("why this instance exists"): reassignment and rule edits change future
|
|
||||||
instances only and never rewrite these rows. ``due_time`` snapshots
|
|
||||||
the definition'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, due_date) pair 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_task_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 pair. 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
|
|
||||||
``task_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 already ``child_id``). A coherence
|
|
||||||
CHECK makes the pair shape non-negotiable: ``'user'`` requires
|
|
||||||
``actor_user_id``, ``'panel'`` forbids it. 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 __future__ import annotations
|
||||||
|
|
||||||
@@ -235,91 +130,11 @@ SCHEMA_V1_TASK_DEFINITIONS_DDL: list[str] = [
|
|||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
]
|
|
||||||
|
|
||||||
SCHEMA_V1_TASK_INSTANCES_DDL: list[str] = [
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS task_instances (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
definition_id INTEGER NOT NULL REFERENCES task_definitions(id),
|
|
||||||
child_id INTEGER NOT NULL REFERENCES children(id),
|
|
||||||
due_date TEXT NOT NULL,
|
|
||||||
due_time TEXT,
|
|
||||||
generated_at TEXT NOT NULL,
|
|
||||||
UNIQUE (definition_id, due_date)
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
]
|
|
||||||
|
|
||||||
SCHEMA_V1_COMPLETION_EVENTS_DDL: list[str] = [
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS completion_events (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
instance_id INTEGER NOT NULL REFERENCES task_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,
|
|
||||||
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 (
|
|
||||||
(event_type = 'completed' AND was_on_time IS NOT NULL)
|
|
||||||
OR (event_type = 'uncompleted')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
]
|
|
||||||
|
|
||||||
SCHEMA_V1_STATEMENTS: list[str] = [
|
SCHEMA_V1_STATEMENTS: list[str] = [
|
||||||
*SCHEMA_V1_CHILDREN_DDL,
|
*SCHEMA_V1_CHILDREN_DDL,
|
||||||
*SCHEMA_V1_ADMIN_USERS_DDL,
|
*SCHEMA_V1_ADMIN_USERS_DDL,
|
||||||
*SCHEMA_V1_SCHEDULE_RULES_DDL,
|
*SCHEMA_V1_SCHEDULE_RULES_DDL,
|
||||||
*SCHEMA_V1_TASK_DEFINITIONS_DDL,
|
*SCHEMA_V1_TASK_DEFINITIONS_DDL,
|
||||||
*SCHEMA_V1_PRESENCE_SCHEDULES_DDL,
|
|
||||||
*SCHEMA_V1_PRESENCE_OVERRIDES_DDL,
|
|
||||||
*SCHEMA_V1_TASK_INSTANCES_DDL,
|
|
||||||
*SCHEMA_V1_COMPLETION_EVENTS_DDL,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1121
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user