Tighten v1 schema keys, weekday checks and ai_runs references
- ai_run_id in suggestion_batches and week_plans now uses the default refusing action instead of ON DELETE SET NULL; the ai_runs purge must clear references explicitly. Tested as a refused parent delete. - Every TEXT primary key (including notification_prefs.event and sessions.id_hash) is NOT NULL; tested that NULL keys are rejected. - Weekday columns use CHECK (col IN (0, 1, 2, 3, 4, 5, 6)) so fractional values are refused; tested with 5.5, 0.5 and 3.5. - Composite UNIQUE tests add positive partial-overlap cases (same date under different plans, same plan_date for different dinners, same event_key on different channels, same period_key for different schedules). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -302,6 +302,9 @@ func TestSchemaEnumChecksRejectInvalidValues(t *testing.T) {
|
|||||||
{"household_settings", "plan_draft_dow", 7},
|
{"household_settings", "plan_draft_dow", 7},
|
||||||
{"household_settings", "suggestions_dow", -1},
|
{"household_settings", "suggestions_dow", -1},
|
||||||
{"household_settings", "chat_autosend_dow", 7},
|
{"household_settings", "chat_autosend_dow", 7},
|
||||||
|
{"household_settings", "plan_draft_dow", 5.5},
|
||||||
|
{"household_settings", "suggestions_dow", 0.5},
|
||||||
|
{"household_settings", "chat_autosend_dow", 3.5},
|
||||||
{"ai_runs", "provider", "openai"},
|
{"ai_runs", "provider", "openai"},
|
||||||
{"ai_runs", "status", "done"},
|
{"ai_runs", "status", "done"},
|
||||||
{"dinners", "status", "deck"},
|
{"dinners", "status", "deck"},
|
||||||
@@ -332,7 +335,7 @@ func TestSchemaEnumChecksRejectInvalidValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
s := seededStore(t)
|
s := seededStore(t)
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.table+"."+c.column, func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s.%s=%v", c.table, c.column, c.bad), func(t *testing.T) {
|
||||||
row := testRow(c.table, 1)
|
row := testRow(c.table, 1)
|
||||||
if slices.Contains(singleRowTables, c.table) {
|
if slices.Contains(singleRowTables, c.table) {
|
||||||
row = testRow(c.table, 0)
|
row = testRow(c.table, 0)
|
||||||
@@ -405,6 +408,82 @@ func TestSchemaUniqueConstraintsRejectDuplicates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSchemaCompositeUniqueAllowsPartialOverlap shows each composite UNIQUE
|
||||||
|
// spans all its columns: a row sharing only some of them with the seeded row
|
||||||
|
// is accepted. (Sharing only the other columns is covered by the distinct row
|
||||||
|
// in TestSchemaUniqueConstraintsRejectDuplicates.)
|
||||||
|
func TestSchemaCompositeUniqueAllowsPartialOverlap(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
table string
|
||||||
|
shared string // column copied from the seeded row
|
||||||
|
parent string // row 1 of this table is inserted first, if set
|
||||||
|
set map[string]any // columns that differ from the seeded row
|
||||||
|
}{
|
||||||
|
{"same date, different plan", "plan_days", "date", "week_plans", map[string]any{"plan_id": rowID("week_plans", 1)}},
|
||||||
|
{"same plan_date, different dinner", "rotation_reviews", "plan_date", "dinners", map[string]any{"dinner_id": rowID("dinners", 1)}},
|
||||||
|
{"same event_key, different channel", "notification_outbox", "event_key", "", map[string]any{"channel": "chat"}},
|
||||||
|
{"same period_key, different schedule", "schedule_runs", "period_key", "", map[string]any{"schedule": "week_rollover"}},
|
||||||
|
}
|
||||||
|
s := seededStore(t)
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.table+": "+c.name, func(t *testing.T) {
|
||||||
|
row := testRow(c.table, 1)
|
||||||
|
row[c.shared] = testRow(c.table, 0)[c.shared]
|
||||||
|
for k, v := range c.set {
|
||||||
|
row[k] = v
|
||||||
|
}
|
||||||
|
inRolledBackTx(t, s, func(tx *sql.Tx) {
|
||||||
|
if c.parent != "" {
|
||||||
|
if err := insertSchemaRow(tx, c.parent, testRow(c.parent, 1)); err != nil {
|
||||||
|
t.Fatalf("insert parent: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := insertSchemaRow(tx, c.table, row); err != nil {
|
||||||
|
t.Fatalf("row rejected: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSchemaTextPrimaryKeysRejectNull guards against SQLite's legacy
|
||||||
|
// acceptance of NULL in non-INTEGER primary keys.
|
||||||
|
func TestSchemaTextPrimaryKeysRejectNull(t *testing.T) {
|
||||||
|
s := seededStore(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, table := range schemaTables {
|
||||||
|
if slices.Contains(singleRowTables, table) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := "id"
|
||||||
|
switch table {
|
||||||
|
case "notification_prefs":
|
||||||
|
key = "event"
|
||||||
|
case "sessions":
|
||||||
|
key = "id_hash"
|
||||||
|
}
|
||||||
|
t.Run(table+"."+key, func(t *testing.T) {
|
||||||
|
inRolledBackTx(t, s, func(tx *sql.Tx) {
|
||||||
|
// Two NULL-key rows: neither may be accepted, so NULL can
|
||||||
|
// never stand in for a key, once or repeatedly.
|
||||||
|
for i := 1; i <= 2; i++ {
|
||||||
|
row := testRow(table, i)
|
||||||
|
row[key] = nil
|
||||||
|
wantConstraintError(t, insertSchemaRow(tx, table, row), "NOT NULL", fmt.Sprintf("%s.%s = NULL (row %d)", table, key, i))
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
if err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT count(*) FROM "%s" WHERE "%s" IS NULL`, table, key)).Scan(&n); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("%d rows with NULL %s", n, key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSchemaSingleRowTablesRejectSecondRow(t *testing.T) {
|
func TestSchemaSingleRowTablesRejectSecondRow(t *testing.T) {
|
||||||
s := migratedStore(t)
|
s := migratedStore(t)
|
||||||
for _, table := range singleRowTables {
|
for _, table := range singleRowTables {
|
||||||
@@ -432,6 +511,9 @@ func TestSchemaDeleteParentWithChildrenRefused(t *testing.T) {
|
|||||||
{"shopping_lists", "shopping_groups", "list_id"},
|
{"shopping_lists", "shopping_groups", "list_id"},
|
||||||
{"dinners", "shopping_groups", "dinner_id"},
|
{"dinners", "shopping_groups", "dinner_id"},
|
||||||
{"shopping_groups", "shopping_items", "group_id"},
|
{"shopping_groups", "shopping_items", "group_id"},
|
||||||
|
// ai_runs too: the purge must clear references before deleting.
|
||||||
|
{"ai_runs", "suggestion_batches", "ai_run_id"},
|
||||||
|
{"ai_runs", "week_plans", "ai_run_id"},
|
||||||
}
|
}
|
||||||
s := seededStore(t)
|
s := seededStore(t)
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -472,20 +554,3 @@ func TestSchemaDeleteParentWithChildrenRefused(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSchemaAIRunPurgeClearsReferences(t *testing.T) {
|
|
||||||
s := seededStore(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
if _, err := s.Writer.ExecContext(ctx, `DELETE FROM ai_runs WHERE id = ?`, rowID("ai_runs", 0)); err != nil {
|
|
||||||
t.Fatalf("purge ai_runs: %v", err)
|
|
||||||
}
|
|
||||||
for _, table := range []string{"suggestion_batches", "week_plans"} {
|
|
||||||
var ref sql.NullString
|
|
||||||
if err := s.Writer.QueryRowContext(ctx, fmt.Sprintf(`SELECT ai_run_id FROM "%s" WHERE id = ?`, table), rowID(table, 0)).Scan(&ref); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if ref.Valid {
|
|
||||||
t.Errorf("%s.ai_run_id = %q after purge, want NULL", table, ref.String)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+32
-30
@@ -1,16 +1,18 @@
|
|||||||
-- 0001_init: the initial v1 schema (CTXD CONTEXT.MD data model).
|
-- 0001_init: the initial v1 schema (CTXD CONTEXT.MD data model).
|
||||||
--
|
--
|
||||||
-- Conventions:
|
-- Conventions:
|
||||||
-- * Row ids are ULIDs stored as TEXT. Single-row settings tables use
|
-- * Row ids are ULIDs stored as TEXT NOT NULL PRIMARY KEY (SQLite would
|
||||||
-- id INTEGER PRIMARY KEY CHECK (id = 1) so a second row is refused.
|
-- otherwise accept NULL in a TEXT primary key). Single-row settings tables
|
||||||
|
-- use id INTEGER PRIMARY KEY CHECK (id = 1) so a second row is refused.
|
||||||
-- * Timestamps are UTC RFC 3339 TEXT. Household-local dates (week_start,
|
-- * Timestamps are UTC RFC 3339 TEXT. Household-local dates (week_start,
|
||||||
-- plan day) are YYYY-MM-DD TEXT; household-local times are HH:MM TEXT.
|
-- plan day) are YYYY-MM-DD TEXT; household-local times are HH:MM TEXT.
|
||||||
-- * Columns ending in _ct only ever hold secretbox ciphertext.
|
-- * Columns ending in _ct only ever hold secretbox ciphertext.
|
||||||
-- * Every enum column has a CHECK. Booleans are INTEGER CHECK (x IN (0, 1)).
|
-- * Every enum column has a CHECK. Booleans are INTEGER CHECK (x IN (0, 1)).
|
||||||
-- Days of the week are INTEGER 0-6, Sunday = 0 (Go time.Weekday).
|
-- Days of the week are INTEGER IN (0, 1, 2, 3, 4, 5, 6), Sunday = 0 (Go
|
||||||
|
-- time.Weekday); IN rather than BETWEEN so fractional values are refused.
|
||||||
-- * Foreign keys use the default NO ACTION, so deleting a parent that still
|
-- * Foreign keys use the default NO ACTION, so deleting a parent that still
|
||||||
-- has children is refused. The one exception is ai_run_id: ai_runs rows
|
-- has children is refused. This includes ai_run_id: the 30-day ai_runs
|
||||||
-- are purged after 30 days, so references to them are ON DELETE SET NULL.
|
-- purge must clear those references explicitly before deleting.
|
||||||
-- * Defaults exist only where CTXD names one (household_settings times and
|
-- * Defaults exist only where CTXD names one (household_settings times and
|
||||||
-- shopping_items.needed).
|
-- shopping_items.needed).
|
||||||
--
|
--
|
||||||
@@ -32,11 +34,11 @@ CREATE TABLE household_settings (
|
|||||||
dinner_time TEXT NOT NULL DEFAULT '17:30',
|
dinner_time TEXT NOT NULL DEFAULT '17:30',
|
||||||
reminder_time TEXT NOT NULL DEFAULT '16:30',
|
reminder_time TEXT NOT NULL DEFAULT '16:30',
|
||||||
review_time TEXT NOT NULL DEFAULT '09:00',
|
review_time TEXT NOT NULL DEFAULT '09:00',
|
||||||
plan_draft_dow INTEGER NOT NULL DEFAULT 5 CHECK (plan_draft_dow BETWEEN 0 AND 6),
|
plan_draft_dow INTEGER NOT NULL DEFAULT 5 CHECK (plan_draft_dow IN (0, 1, 2, 3, 4, 5, 6)),
|
||||||
plan_draft_time TEXT NOT NULL DEFAULT '09:00',
|
plan_draft_time TEXT NOT NULL DEFAULT '09:00',
|
||||||
suggestions_dow INTEGER NOT NULL DEFAULT 0 CHECK (suggestions_dow BETWEEN 0 AND 6),
|
suggestions_dow INTEGER NOT NULL DEFAULT 0 CHECK (suggestions_dow IN (0, 1, 2, 3, 4, 5, 6)),
|
||||||
suggestions_time TEXT NOT NULL DEFAULT '08:00',
|
suggestions_time TEXT NOT NULL DEFAULT '08:00',
|
||||||
chat_autosend_dow INTEGER NOT NULL DEFAULT 6 CHECK (chat_autosend_dow BETWEEN 0 AND 6),
|
chat_autosend_dow INTEGER NOT NULL DEFAULT 6 CHECK (chat_autosend_dow IN (0, 1, 2, 3, 4, 5, 6)),
|
||||||
chat_autosend_time TEXT NOT NULL DEFAULT '08:00',
|
chat_autosend_time TEXT NOT NULL DEFAULT '08:00',
|
||||||
onboarding_completed_at TEXT,
|
onboarding_completed_at TEXT,
|
||||||
updates_seen_at TEXT,
|
updates_seen_at TEXT,
|
||||||
@@ -45,7 +47,7 @@ CREATE TABLE household_settings (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ai_runs (
|
CREATE TABLE ai_runs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
skill TEXT NOT NULL,
|
skill TEXT NOT NULL,
|
||||||
skill_version TEXT NOT NULL,
|
skill_version TEXT NOT NULL,
|
||||||
provider TEXT NOT NULL CHECK (provider IN ('openai_compatible', 'anthropic', 'ollama')),
|
provider TEXT NOT NULL CHECK (provider IN ('openai_compatible', 'anthropic', 'ollama')),
|
||||||
@@ -63,7 +65,7 @@ CREATE TABLE ai_runs (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE recipes (
|
CREATE TABLE recipes (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
source_url TEXT NOT NULL UNIQUE,
|
source_url TEXT NOT NULL UNIQUE,
|
||||||
site_name TEXT,
|
site_name TEXT,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
@@ -78,7 +80,7 @@ CREATE TABLE recipes (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE recipe_ingredients (
|
CREATE TABLE recipe_ingredients (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
recipe_id TEXT NOT NULL REFERENCES recipes (id),
|
recipe_id TEXT NOT NULL REFERENCES recipes (id),
|
||||||
position INTEGER NOT NULL,
|
position INTEGER NOT NULL,
|
||||||
raw_text TEXT NOT NULL,
|
raw_text TEXT NOT NULL,
|
||||||
@@ -91,7 +93,7 @@ CREATE TABLE recipe_ingredients (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE recipe_steps (
|
CREATE TABLE recipe_steps (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
recipe_id TEXT NOT NULL REFERENCES recipes (id),
|
recipe_id TEXT NOT NULL REFERENCES recipes (id),
|
||||||
position INTEGER NOT NULL,
|
position INTEGER NOT NULL,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
@@ -100,7 +102,7 @@ CREATE TABLE recipe_steps (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE photos (
|
CREATE TABLE photos (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
source_url TEXT NOT NULL,
|
source_url TEXT NOT NULL,
|
||||||
local_path TEXT NOT NULL,
|
local_path TEXT NOT NULL,
|
||||||
width INTEGER NOT NULL,
|
width INTEGER NOT NULL,
|
||||||
@@ -112,7 +114,7 @@ CREATE TABLE photos (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE dinners (
|
CREATE TABLE dinners (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
normalized_name TEXT NOT NULL,
|
normalized_name TEXT NOT NULL,
|
||||||
status TEXT NOT NULL CHECK (status IN ('suggested', 'quest_log', 'tried', 'regular', 'archived')),
|
status TEXT NOT NULL CHECK (status IN ('suggested', 'quest_log', 'tried', 'regular', 'archived')),
|
||||||
@@ -131,16 +133,16 @@ CREATE TABLE dinners (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE suggestion_batches (
|
CREATE TABLE suggestion_batches (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
trigger TEXT NOT NULL CHECK (trigger IN ('scheduled', 'manual', 'replenish')),
|
trigger TEXT NOT NULL CHECK (trigger IN ('scheduled', 'manual', 'replenish')),
|
||||||
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
||||||
ai_run_id TEXT REFERENCES ai_runs (id) ON DELETE SET NULL,
|
ai_run_id TEXT REFERENCES ai_runs (id),
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE suggestions (
|
CREATE TABLE suggestions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
||||||
batch_id TEXT NOT NULL REFERENCES suggestion_batches (id),
|
batch_id TEXT NOT NULL REFERENCES suggestion_batches (id),
|
||||||
reason TEXT NOT NULL,
|
reason TEXT NOT NULL,
|
||||||
@@ -152,18 +154,18 @@ CREATE TABLE suggestions (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE week_plans (
|
CREATE TABLE week_plans (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
week_start TEXT NOT NULL UNIQUE,
|
week_start TEXT NOT NULL UNIQUE,
|
||||||
status TEXT NOT NULL CHECK (status IN ('draft', 'synced', 'changed')),
|
status TEXT NOT NULL CHECK (status IN ('draft', 'synced', 'changed')),
|
||||||
ai_note TEXT,
|
ai_note TEXT,
|
||||||
ai_run_id TEXT REFERENCES ai_runs (id) ON DELETE SET NULL,
|
ai_run_id TEXT REFERENCES ai_runs (id),
|
||||||
calendar_synced_at TEXT,
|
calendar_synced_at TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE plan_days (
|
CREATE TABLE plan_days (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
plan_id TEXT NOT NULL REFERENCES week_plans (id),
|
plan_id TEXT NOT NULL REFERENCES week_plans (id),
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
dinner_id TEXT REFERENCES dinners (id),
|
dinner_id TEXT REFERENCES dinners (id),
|
||||||
@@ -175,7 +177,7 @@ CREATE TABLE plan_days (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE rotation_reviews (
|
CREATE TABLE rotation_reviews (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
||||||
plan_date TEXT NOT NULL,
|
plan_date TEXT NOT NULL,
|
||||||
status TEXT NOT NULL CHECK (status IN ('pending', 'decided')),
|
status TEXT NOT NULL CHECK (status IN ('pending', 'decided')),
|
||||||
@@ -188,7 +190,7 @@ CREATE TABLE rotation_reviews (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE shopping_lists (
|
CREATE TABLE shopping_lists (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
week_start TEXT NOT NULL UNIQUE,
|
week_start TEXT NOT NULL UNIQUE,
|
||||||
plan_id TEXT NOT NULL REFERENCES week_plans (id),
|
plan_id TEXT NOT NULL REFERENCES week_plans (id),
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
@@ -200,7 +202,7 @@ CREATE TABLE shopping_lists (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE shopping_groups (
|
CREATE TABLE shopping_groups (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
list_id TEXT NOT NULL REFERENCES shopping_lists (id),
|
list_id TEXT NOT NULL REFERENCES shopping_lists (id),
|
||||||
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
dinner_id TEXT NOT NULL REFERENCES dinners (id),
|
||||||
position INTEGER NOT NULL,
|
position INTEGER NOT NULL,
|
||||||
@@ -209,7 +211,7 @@ CREATE TABLE shopping_groups (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE shopping_items (
|
CREATE TABLE shopping_items (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
group_id TEXT NOT NULL REFERENCES shopping_groups (id),
|
group_id TEXT NOT NULL REFERENCES shopping_groups (id),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
qty TEXT,
|
qty TEXT,
|
||||||
@@ -264,7 +266,7 @@ CREATE TABLE chat_settings (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE notification_prefs (
|
CREATE TABLE notification_prefs (
|
||||||
event TEXT PRIMARY KEY CHECK (event IN ('plan_ready', 'list_ready', 'day_of_reminder', 'new_suggestions', 'rotation_review')),
|
event TEXT NOT NULL PRIMARY KEY CHECK (event IN ('plan_ready', 'list_ready', 'day_of_reminder', 'new_suggestions', 'rotation_review')),
|
||||||
push INTEGER NOT NULL CHECK (push IN (0, 1)),
|
push INTEGER NOT NULL CHECK (push IN (0, 1)),
|
||||||
chat INTEGER NOT NULL CHECK (chat IN (0, 1)),
|
chat INTEGER NOT NULL CHECK (chat IN (0, 1)),
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
@@ -272,7 +274,7 @@ CREATE TABLE notification_prefs (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE push_subscriptions (
|
CREATE TABLE push_subscriptions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
endpoint TEXT NOT NULL UNIQUE,
|
endpoint TEXT NOT NULL UNIQUE,
|
||||||
p256dh TEXT NOT NULL,
|
p256dh TEXT NOT NULL,
|
||||||
auth TEXT NOT NULL,
|
auth TEXT NOT NULL,
|
||||||
@@ -284,7 +286,7 @@ CREATE TABLE push_subscriptions (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE notification_outbox (
|
CREATE TABLE notification_outbox (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
event_key TEXT NOT NULL,
|
event_key TEXT NOT NULL,
|
||||||
channel TEXT NOT NULL CHECK (channel IN ('push', 'chat')),
|
channel TEXT NOT NULL CHECK (channel IN ('push', 'chat')),
|
||||||
payload TEXT NOT NULL, -- JSON
|
payload TEXT NOT NULL, -- JSON
|
||||||
@@ -298,7 +300,7 @@ CREATE TABLE notification_outbox (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE jobs (
|
CREATE TABLE jobs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
||||||
payload TEXT NOT NULL, -- JSON
|
payload TEXT NOT NULL, -- JSON
|
||||||
@@ -311,7 +313,7 @@ CREATE TABLE jobs (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE schedule_runs (
|
CREATE TABLE schedule_runs (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
schedule TEXT NOT NULL CHECK (schedule IN ('suggestions_weekly', 'plan_draft', 'shopping_chat_autosend', 'day_of_reminder', 'rotation_check', 'week_rollover', 'ai_runs_purge')),
|
schedule TEXT NOT NULL CHECK (schedule IN ('suggestions_weekly', 'plan_draft', 'shopping_chat_autosend', 'day_of_reminder', 'rotation_check', 'week_rollover', 'ai_runs_purge')),
|
||||||
period_key TEXT NOT NULL,
|
period_key TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
@@ -319,7 +321,7 @@ CREATE TABLE schedule_runs (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE sessions (
|
CREATE TABLE sessions (
|
||||||
id_hash TEXT PRIMARY KEY,
|
id_hash TEXT NOT NULL PRIMARY KEY,
|
||||||
subject TEXT NOT NULL,
|
subject TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user