refactor(db): consolidate 21 migrations into 4 clean files
No production instances exist, so safe to rewrite from scratch. Drops columns that were added then removed (notes, password_hash, cup rating), standardises timestamp format and primary key style, and creates all tables in their final form without incremental ALTER TABLE churn.
This commit is contained in:
parent
709adcdd3e
commit
b5e91d9d0d
25 changed files with 174 additions and 398 deletions
51
migrations/0001_auth.sql
Normal file
51
migrations/0001_auth.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- Authentication and authorisation tables
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
uuid TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tokens (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
last_used_at TEXT,
|
||||||
|
revoked_at TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_tokens_user_id ON tokens(user_id);
|
||||||
|
CREATE INDEX idx_tokens_token_hash ON tokens(token_hash);
|
||||||
|
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
session_token_hash TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
expires_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_sessions_token_hash ON sessions(session_token_hash);
|
||||||
|
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
||||||
|
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE passkey_credentials (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
credential_json TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL DEFAULT 'default',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
last_used_at TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_passkey_credentials_user_id ON passkey_credentials(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE registration_tokens (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
used_at TEXT,
|
||||||
|
used_by_user_id INTEGER REFERENCES users(id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_registration_tokens_token_hash ON registration_tokens(token_hash);
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
|
|
||||||
CREATE TABLE roasters (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
country TEXT NOT NULL,
|
|
||||||
city TEXT,
|
|
||||||
homepage TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
slug TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_roasters_slug ON roasters(slug);
|
|
||||||
|
|
||||||
CREATE TABLE roasts (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
roaster_id INTEGER NOT NULL REFERENCES roasters(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
origin TEXT,
|
|
||||||
region TEXT,
|
|
||||||
producer TEXT,
|
|
||||||
process TEXT,
|
|
||||||
tasting_notes TEXT,
|
|
||||||
slug TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_roasts_roaster_id ON roasts(roaster_id);
|
|
||||||
CREATE UNIQUE INDEX idx_roasts_roaster_slug ON roasts(roaster_id, slug);
|
|
||||||
|
|
||||||
CREATE TABLE timeline_events (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag')),
|
|
||||||
entity_id INTEGER NOT NULL,
|
|
||||||
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
details_json TEXT,
|
|
||||||
tasting_notes_json TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
|
||||||
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at);
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
-- migrate:up
|
|
||||||
CREATE TABLE users (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE tokens (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
last_used_at TEXT,
|
|
||||||
revoked_at TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_tokens_user_id ON tokens(user_id);
|
|
||||||
CREATE INDEX idx_tokens_token_hash ON tokens(token_hash);
|
|
||||||
51
migrations/0002_coffee.sql
Normal file
51
migrations/0002_coffee.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- Coffee supply chain and equipment tables
|
||||||
|
|
||||||
|
CREATE TABLE roasters (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
country TEXT NOT NULL,
|
||||||
|
city TEXT,
|
||||||
|
homepage TEXT,
|
||||||
|
slug TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX idx_roasters_slug ON roasters(slug);
|
||||||
|
|
||||||
|
CREATE TABLE roasts (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
roaster_id INTEGER NOT NULL REFERENCES roasters(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
origin TEXT,
|
||||||
|
region TEXT,
|
||||||
|
producer TEXT,
|
||||||
|
process TEXT,
|
||||||
|
tasting_notes TEXT,
|
||||||
|
slug TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_roasts_roaster_id ON roasts(roaster_id);
|
||||||
|
CREATE UNIQUE INDEX idx_roasts_roaster_slug ON roasts(roaster_id, slug);
|
||||||
|
|
||||||
|
CREATE TABLE bags (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
roast_id INTEGER NOT NULL REFERENCES roasts(id) ON DELETE CASCADE,
|
||||||
|
roast_date TEXT,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
remaining REAL NOT NULL,
|
||||||
|
closed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
finished_at TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_bags_roast_id ON bags(roast_id);
|
||||||
|
CREATE INDEX idx_bags_closed ON bags(closed);
|
||||||
|
|
||||||
|
CREATE TABLE gear (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
category TEXT NOT NULL CHECK (category IN ('grinder', 'brewer', 'filter_paper')),
|
||||||
|
make TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_gear_category ON gear(category);
|
||||||
42
migrations/0003_activities.sql
Normal file
42
migrations/0003_activities.sql
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
-- Brew, cafe, and cup tables
|
||||||
|
|
||||||
|
CREATE TABLE brews (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
bag_id INTEGER NOT NULL REFERENCES bags(id) ON DELETE CASCADE,
|
||||||
|
coffee_weight REAL NOT NULL,
|
||||||
|
grinder_id INTEGER NOT NULL REFERENCES gear(id) ON DELETE RESTRICT,
|
||||||
|
grind_setting REAL NOT NULL,
|
||||||
|
brewer_id INTEGER NOT NULL REFERENCES gear(id) ON DELETE RESTRICT,
|
||||||
|
filter_paper_id INTEGER REFERENCES gear(id) ON DELETE SET NULL,
|
||||||
|
water_volume INTEGER NOT NULL,
|
||||||
|
water_temp REAL NOT NULL,
|
||||||
|
quick_notes TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_brews_bag_id ON brews(bag_id);
|
||||||
|
CREATE INDEX idx_brews_created_at ON brews(created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE cafes (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
country TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
website TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX idx_cafes_slug ON cafes(slug);
|
||||||
|
|
||||||
|
CREATE TABLE cups (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
roast_id INTEGER NOT NULL REFERENCES roasts(id) ON DELETE RESTRICT,
|
||||||
|
cafe_id INTEGER NOT NULL REFERENCES cafes(id) ON DELETE RESTRICT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_cups_roast_id ON cups(roast_id);
|
||||||
|
CREATE INDEX idx_cups_cafe_id ON cups(cafe_id);
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
-- Add sessions table for web authentication
|
|
||||||
CREATE TABLE IF NOT EXISTS sessions (
|
|
||||||
id INTEGER PRIMARY KEY NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
session_token_hash TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
expires_at TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(session_token_hash);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
CREATE TABLE bags (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
roast_id INTEGER NOT NULL,
|
|
||||||
roast_date DATE,
|
|
||||||
amount REAL NOT NULL,
|
|
||||||
remaining REAL NOT NULL,
|
|
||||||
closed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
finished_at DATE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (roast_id) REFERENCES roasts (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_bags_roast_id ON bags(roast_id);
|
|
||||||
CREATE INDEX idx_bags_closed ON bags(closed);
|
|
||||||
30
migrations/0004_timeline_and_usage.sql
Normal file
30
migrations/0004_timeline_and_usage.sql
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- Timeline events and AI usage tracking
|
||||||
|
|
||||||
|
CREATE TABLE timeline_events (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag', 'gear', 'brew', 'cafe', 'cup')),
|
||||||
|
entity_id INTEGER NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
details_json TEXT,
|
||||||
|
tasting_notes_json TEXT,
|
||||||
|
slug TEXT,
|
||||||
|
roaster_slug TEXT,
|
||||||
|
brew_data_json TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
||||||
|
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE ai_usage (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
endpoint TEXT NOT NULL,
|
||||||
|
prompt_tokens INTEGER NOT NULL,
|
||||||
|
completion_tokens INTEGER NOT NULL,
|
||||||
|
total_tokens INTEGER NOT NULL,
|
||||||
|
cost REAL NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_ai_usage_user_id ON ai_usage(user_id);
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE timeline_events ADD COLUMN action TEXT NOT NULL DEFAULT 'added';
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
CREATE TABLE gear (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
category TEXT NOT NULL CHECK (category IN ('grinder', 'brewer')),
|
|
||||||
make TEXT NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
notes TEXT,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_gear_category ON gear(category);
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
-- Update timeline_events CHECK constraint to include 'gear'
|
|
||||||
-- SQLite requires recreating the table to modify CHECK constraints
|
|
||||||
|
|
||||||
-- Step 1: Rename old table
|
|
||||||
ALTER TABLE timeline_events RENAME TO timeline_events_old;
|
|
||||||
|
|
||||||
-- Step 2: Create new table with updated constraint
|
|
||||||
CREATE TABLE timeline_events (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag', 'gear')),
|
|
||||||
entity_id INTEGER NOT NULL,
|
|
||||||
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
details_json TEXT,
|
|
||||||
tasting_notes_json TEXT,
|
|
||||||
action TEXT NOT NULL DEFAULT 'added'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Step 3: Copy data from old table
|
|
||||||
INSERT INTO timeline_events (id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json, action)
|
|
||||||
SELECT id, entity_type, entity_id, occurred_at, title, details_json, tasting_notes_json, action
|
|
||||||
FROM timeline_events_old;
|
|
||||||
|
|
||||||
-- Step 4: Drop old table
|
|
||||||
DROP TABLE timeline_events_old;
|
|
||||||
|
|
||||||
-- Step 5: Recreate indexes
|
|
||||||
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
|
||||||
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE gear DROP COLUMN notes;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
-- Brews table for logging individual coffee brews
|
|
||||||
CREATE TABLE brews (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
bag_id INTEGER NOT NULL REFERENCES bags(id) ON DELETE CASCADE,
|
|
||||||
coffee_weight REAL NOT NULL,
|
|
||||||
grinder_id INTEGER NOT NULL REFERENCES gear(id) ON DELETE RESTRICT,
|
|
||||||
grind_setting REAL NOT NULL,
|
|
||||||
brewer_id INTEGER NOT NULL REFERENCES gear(id) ON DELETE RESTRICT,
|
|
||||||
water_volume INTEGER NOT NULL,
|
|
||||||
water_temp REAL NOT NULL,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_brews_bag_id ON brews(bag_id);
|
|
||||||
CREATE INDEX idx_brews_created_at ON brews(created_at DESC);
|
|
||||||
|
|
||||||
-- Update timeline constraint to include brew entity type
|
|
||||||
DROP TABLE IF EXISTS timeline_events_new;
|
|
||||||
CREATE TABLE timeline_events_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag', 'gear', 'brew')),
|
|
||||||
entity_id INTEGER NOT NULL,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
details_json TEXT,
|
|
||||||
tasting_notes_json TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO timeline_events_new (id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json)
|
|
||||||
SELECT id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json
|
|
||||||
FROM timeline_events;
|
|
||||||
|
|
||||||
DROP TABLE timeline_events;
|
|
||||||
ALTER TABLE timeline_events_new RENAME TO timeline_events;
|
|
||||||
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
|
||||||
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at DESC);
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
-- Add denormalized slug columns to timeline_events
|
|
||||||
-- This eliminates the need for 9-way JOINs when listing timeline events
|
|
||||||
|
|
||||||
-- Step 1: Add the new columns
|
|
||||||
ALTER TABLE timeline_events ADD COLUMN slug TEXT;
|
|
||||||
ALTER TABLE timeline_events ADD COLUMN roaster_slug TEXT;
|
|
||||||
ALTER TABLE timeline_events ADD COLUMN brew_data_json TEXT;
|
|
||||||
|
|
||||||
-- Step 2: Backfill roaster slugs for roaster events
|
|
||||||
UPDATE timeline_events
|
|
||||||
SET slug = (SELECT slug FROM roasters WHERE roasters.id = timeline_events.entity_id)
|
|
||||||
WHERE entity_type = 'roaster';
|
|
||||||
|
|
||||||
-- Step 3: Backfill slugs for roast events
|
|
||||||
UPDATE timeline_events
|
|
||||||
SET slug = (SELECT slug FROM roasts WHERE roasts.id = timeline_events.entity_id),
|
|
||||||
roaster_slug = (
|
|
||||||
SELECT ro.slug FROM roasts r
|
|
||||||
JOIN roasters ro ON r.roaster_id = ro.id
|
|
||||||
WHERE r.id = timeline_events.entity_id
|
|
||||||
)
|
|
||||||
WHERE entity_type = 'roast';
|
|
||||||
|
|
||||||
-- Step 4: Backfill slugs for bag events
|
|
||||||
UPDATE timeline_events
|
|
||||||
SET slug = (
|
|
||||||
SELECT r.slug FROM bags b
|
|
||||||
JOIN roasts r ON b.roast_id = r.id
|
|
||||||
WHERE b.id = timeline_events.entity_id
|
|
||||||
),
|
|
||||||
roaster_slug = (
|
|
||||||
SELECT ro.slug FROM bags b
|
|
||||||
JOIN roasts r ON b.roast_id = r.id
|
|
||||||
JOIN roasters ro ON r.roaster_id = ro.id
|
|
||||||
WHERE b.id = timeline_events.entity_id
|
|
||||||
)
|
|
||||||
WHERE entity_type = 'bag';
|
|
||||||
|
|
||||||
-- Step 5: Backfill slugs and brew_data for brew events
|
|
||||||
UPDATE timeline_events
|
|
||||||
SET slug = (
|
|
||||||
SELECT r.slug FROM brews br
|
|
||||||
JOIN bags b ON br.bag_id = b.id
|
|
||||||
JOIN roasts r ON b.roast_id = r.id
|
|
||||||
WHERE br.id = timeline_events.entity_id
|
|
||||||
),
|
|
||||||
roaster_slug = (
|
|
||||||
SELECT ro.slug FROM brews br
|
|
||||||
JOIN bags b ON br.bag_id = b.id
|
|
||||||
JOIN roasts r ON b.roast_id = r.id
|
|
||||||
JOIN roasters ro ON r.roaster_id = ro.id
|
|
||||||
WHERE br.id = timeline_events.entity_id
|
|
||||||
),
|
|
||||||
brew_data_json = (
|
|
||||||
SELECT json_object(
|
|
||||||
'bag_id', br.bag_id,
|
|
||||||
'grinder_id', br.grinder_id,
|
|
||||||
'brewer_id', br.brewer_id,
|
|
||||||
'coffee_weight', br.coffee_weight,
|
|
||||||
'grind_setting', br.grind_setting,
|
|
||||||
'water_volume', br.water_volume,
|
|
||||||
'water_temp', br.water_temp
|
|
||||||
)
|
|
||||||
FROM brews br
|
|
||||||
WHERE br.id = timeline_events.entity_id
|
|
||||||
)
|
|
||||||
WHERE entity_type = 'brew';
|
|
||||||
|
|
||||||
-- Gear events have no slug (entity_type = 'gear' gets NULL for both slug columns)
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
-- Add 'filter_paper' gear category and optional filter_paper_id on brews.
|
|
||||||
--
|
|
||||||
-- SQLite does not support altering CHECK constraints, so we recreate the gear
|
|
||||||
-- table with the updated constraint.
|
|
||||||
|
|
||||||
CREATE TABLE gear_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
category TEXT NOT NULL CHECK (category IN ('grinder', 'brewer', 'filter_paper')),
|
|
||||||
make TEXT NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO gear_new (id, category, make, model, created_at, updated_at)
|
|
||||||
SELECT id, category, make, model, created_at, updated_at FROM gear;
|
|
||||||
|
|
||||||
DROP TABLE gear;
|
|
||||||
ALTER TABLE gear_new RENAME TO gear;
|
|
||||||
CREATE INDEX idx_gear_category ON gear(category);
|
|
||||||
|
|
||||||
ALTER TABLE brews ADD COLUMN filter_paper_id INTEGER REFERENCES gear(id) ON DELETE SET NULL;
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
-- Add cafes table for tracking coffee shops
|
|
||||||
CREATE TABLE cafes (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
city TEXT NOT NULL,
|
|
||||||
country TEXT NOT NULL,
|
|
||||||
latitude REAL NOT NULL,
|
|
||||||
longitude REAL NOT NULL,
|
|
||||||
website TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_cafes_slug ON cafes(slug);
|
|
||||||
|
|
||||||
-- Update timeline_events CHECK constraint to include 'cafe'
|
|
||||||
-- SQLite requires recreating the table to modify CHECK constraints
|
|
||||||
DROP TABLE IF EXISTS timeline_events_new;
|
|
||||||
CREATE TABLE timeline_events_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag', 'gear', 'brew', 'cafe')),
|
|
||||||
entity_id INTEGER NOT NULL,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
details_json TEXT,
|
|
||||||
tasting_notes_json TEXT,
|
|
||||||
slug TEXT,
|
|
||||||
roaster_slug TEXT,
|
|
||||||
brew_data_json TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO timeline_events_new (id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json, slug, roaster_slug, brew_data_json)
|
|
||||||
SELECT id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json, slug, roaster_slug, brew_data_json
|
|
||||||
FROM timeline_events;
|
|
||||||
|
|
||||||
DROP TABLE timeline_events;
|
|
||||||
ALTER TABLE timeline_events_new RENAME TO timeline_events;
|
|
||||||
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
|
||||||
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at DESC);
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
-- Add cups table for tracking roasts consumed at cafes
|
|
||||||
CREATE TABLE cups (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
roast_id INTEGER NOT NULL REFERENCES roasts(id) ON DELETE RESTRICT,
|
|
||||||
cafe_id INTEGER NOT NULL REFERENCES cafes(id) ON DELETE RESTRICT,
|
|
||||||
notes TEXT,
|
|
||||||
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_cups_roast_id ON cups(roast_id);
|
|
||||||
CREATE INDEX idx_cups_cafe_id ON cups(cafe_id);
|
|
||||||
|
|
||||||
-- Update timeline_events CHECK constraint to include 'cup'
|
|
||||||
-- SQLite requires recreating the table to modify CHECK constraints
|
|
||||||
DROP TABLE IF EXISTS timeline_events_new;
|
|
||||||
CREATE TABLE timeline_events_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('roaster', 'roast', 'bag', 'gear', 'brew', 'cafe', 'cup')),
|
|
||||||
entity_id INTEGER NOT NULL,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
occurred_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
details_json TEXT,
|
|
||||||
tasting_notes_json TEXT,
|
|
||||||
slug TEXT,
|
|
||||||
roaster_slug TEXT,
|
|
||||||
brew_data_json TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO timeline_events_new (id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json, slug, roaster_slug, brew_data_json)
|
|
||||||
SELECT id, entity_type, entity_id, action, occurred_at, title, details_json, tasting_notes_json, slug, roaster_slug, brew_data_json
|
|
||||||
FROM timeline_events;
|
|
||||||
|
|
||||||
DROP TABLE timeline_events;
|
|
||||||
ALTER TABLE timeline_events_new RENAME TO timeline_events;
|
|
||||||
CREATE INDEX idx_timeline_events_entity ON timeline_events(entity_type, entity_id);
|
|
||||||
CREATE INDEX idx_timeline_events_occurred_at ON timeline_events(occurred_at DESC);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE cafes DROP COLUMN notes;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE cups DROP COLUMN notes;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE roasters DROP COLUMN notes;
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
-- Add UUID column to users for WebAuthn user handle
|
|
||||||
ALTER TABLE users ADD COLUMN uuid TEXT;
|
|
||||||
|
|
||||||
-- Backfill existing users with random v4 UUIDs
|
|
||||||
UPDATE users SET uuid =
|
|
||||||
lower(hex(randomblob(4))) || '-' ||
|
|
||||||
lower(hex(randomblob(2))) || '-' ||
|
|
||||||
'4' || substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
||||||
substr('89ab', abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))), 2) || '-' ||
|
|
||||||
lower(hex(randomblob(6)));
|
|
||||||
|
|
||||||
-- Passkey credential storage
|
|
||||||
CREATE TABLE passkey_credentials (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
credential_json TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL DEFAULT 'default',
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
last_used_at TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_passkey_credentials_user_id ON passkey_credentials(user_id);
|
|
||||||
|
|
||||||
-- One-time registration tokens for bootstrap and invite flows
|
|
||||||
CREATE TABLE registration_tokens (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
token_hash TEXT NOT NULL UNIQUE,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
expires_at TEXT NOT NULL,
|
|
||||||
used_at TEXT,
|
|
||||||
used_by_user_id INTEGER REFERENCES users(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_registration_tokens_token_hash ON registration_tokens(token_hash);
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
-- Remove password_hash column from users (passkey-only auth)
|
|
||||||
-- SQLite 3.35+ supports ALTER TABLE DROP COLUMN
|
|
||||||
ALTER TABLE users DROP COLUMN password_hash;
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
CREATE TABLE ai_usage (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
endpoint TEXT NOT NULL,
|
|
||||||
prompt_tokens INTEGER NOT NULL,
|
|
||||||
completion_tokens INTEGER NOT NULL,
|
|
||||||
total_tokens INTEGER NOT NULL,
|
|
||||||
cost REAL NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_ai_usage_user_id ON ai_usage(user_id);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE cups DROP COLUMN rating;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE brews ADD COLUMN quick_notes TEXT;
|
|
||||||
Loading…
Reference in a new issue