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:
Jon Seager 2026-02-05 21:06:37 +00:00
parent 709adcdd3e
commit b5e91d9d0d
No known key found for this signature in database
25 changed files with 174 additions and 398 deletions

51
migrations/0001_auth.sql Normal file
View 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);

View file

@ -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);

View file

@ -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);

View 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);

View 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);

View file

@ -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);

View file

@ -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);

View 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);

View file

@ -1 +0,0 @@
ALTER TABLE timeline_events ADD COLUMN action TEXT NOT NULL DEFAULT 'added';

View file

@ -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);

View file

@ -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);

View file

@ -1 +0,0 @@
ALTER TABLE gear DROP COLUMN notes;

View file

@ -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);

View file

@ -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)

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -1 +0,0 @@
ALTER TABLE cafes DROP COLUMN notes;

View file

@ -1 +0,0 @@
ALTER TABLE cups DROP COLUMN notes;

View file

@ -1 +0,0 @@
ALTER TABLE roasters DROP COLUMN notes;

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -1 +0,0 @@
ALTER TABLE cups DROP COLUMN rating;

View file

@ -1 +0,0 @@
ALTER TABLE brews ADD COLUMN quick_notes TEXT;