db: Drizzle schemas on domain Postgres schemas, baseline migration, reorg SQL
- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta (lists excluded via schemaFilter; runtime list tables are managed by meta.*) - drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal) - db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy) - scripts/db-migrate.ts: migrator with --init to record the baseline and a guard against running the baseline on a non-empty, unrecorded database
This commit is contained in:
17
db/reorg/01_roles.sql
Normal file
17
db/reorg/01_roles.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- 01_roles.sql: cluster-level roles. Run as a superuser (postgres). Idempotent.
|
||||||
|
-- In prod: needs `sudo docker exec -i <pg-container> psql -U postgres`; set passwords separately (\password), never in this file.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
|
||||||
|
CREATE ROLE personal_app LOGIN; -- app, ned-collector, house worker
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_ddl') THEN
|
||||||
|
CREATE ROLE personal_ddl NOLOGIN; -- owns schema lists; only reachable via SECURITY DEFINER fns
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_ro') THEN
|
||||||
|
CREATE ROLE personal_ro LOGIN; -- read-only for audits/agents/pgAdmin
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
ALTER ROLE personal_ro SET default_transaction_read_only = on;
|
||||||
|
ALTER ROLE personal_app SET search_path = '$user'; -- force schema-qualified names in app SQL
|
||||||
|
ALTER ROLE personal_ro SET search_path = '$user';
|
||||||
142
db/reorg/02_schemas_move.sql
Normal file
142
db/reorg/02_schemas_move.sql
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
-- 02_schemas_move.sql: create schemas, snapshot counts, move + rename all 58 public tables.
|
||||||
|
SET ROLE personal;
|
||||||
|
|
||||||
|
CREATE SCHEMA auth; CREATE SCHEMA money; CREATE SCHEMA house; CREATE SCHEMA ref;
|
||||||
|
CREATE SCHEMA games; CREATE SCHEMA cs; CREATE SCHEMA animaldex; CREATE SCHEMA site;
|
||||||
|
CREATE SCHEMA meta; CREATE SCHEMA hardware; CREATE SCHEMA energy; CREATE SCHEMA legacy;
|
||||||
|
COMMENT ON SCHEMA legacy IS 'Pre-reorg list tables + reorg bookkeeping. Drop 30 days after cutover.';
|
||||||
|
COMMENT ON SCHEMA ref IS 'Seeded third-party catalog data (Pokemon, CoD tracker definitions). Read-only in the GUI; seeders own it.';
|
||||||
|
COMMENT ON SCHEMA games IS 'Stan''s own game progress.';
|
||||||
|
|
||||||
|
-- exact row counts of every public table before anything moves
|
||||||
|
CREATE TABLE legacy.reorg_counts_before (table_name text PRIMARY KEY, n bigint NOT NULL);
|
||||||
|
DO $$
|
||||||
|
DECLARE t text; c bigint;
|
||||||
|
BEGIN
|
||||||
|
FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP
|
||||||
|
EXECUTE format('SELECT count(*) FROM public.%I', t) INTO c;
|
||||||
|
INSERT INTO legacy.reorg_counts_before VALUES (t, c);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE TABLE legacy.reorg_table_map (
|
||||||
|
old_name text PRIMARY KEY,
|
||||||
|
old_prefix text NOT NULL, -- prefix used by its index/constraint/sequence names
|
||||||
|
new_schema text NOT NULL,
|
||||||
|
new_name text NOT NULL,
|
||||||
|
UNIQUE (new_schema, new_name)
|
||||||
|
);
|
||||||
|
INSERT INTO legacy.reorg_table_map (old_name, old_prefix, new_schema, new_name) VALUES
|
||||||
|
('user','user','auth','user'), ('session','session','auth','session'),
|
||||||
|
('account','account','auth','account'), ('verification','verification','auth','verification'),
|
||||||
|
('money_accounts','money_accounts','money','accounts'), ('money_transactions','money_transactions','money','transactions'),
|
||||||
|
('money_recurring','money_recurring','money','recurring'), ('money_forecasts','money_forecasts','money','forecasts'),
|
||||||
|
('money_insurances','money_insurances','money','insurances'), ('money_wishlist','money_wishlist','money','wishlist'),
|
||||||
|
('money_projection_settings','money_projection_settings','money','projection_settings'),
|
||||||
|
('money_utility_settings','money_utility_settings','money','utility_settings'),
|
||||||
|
('money_utility_readings','money_utility_readings','money','utility_readings'),
|
||||||
|
('house_listings','house_listings','house','listings'), ('house_price_history','house_price_history','house','price_history'),
|
||||||
|
('house_searches','house_searches','house','searches'), ('house_worker_runs','house_worker_runs','house','worker_runs'),
|
||||||
|
('house_alert_rules','house_alert_rules','house','alert_rules'),
|
||||||
|
('pokemon','pokemon','ref','pokemon'), ('games','game','ref','pokemon_games'),
|
||||||
|
('pokemon_game_entries','pokemon_game_entry','ref','pokemon_game_entries'),
|
||||||
|
('pokemon_locations','pokemon_locations','ref','pokemon_locations'), ('pokemon_encounters','pokemon_encounters','ref','pokemon_encounters'),
|
||||||
|
('pokemon_evolutions','pokemon_evolutions','ref','pokemon_evolutions'), ('pokemon_dex_numbers','pokemon_dex_numbers','ref','pokemon_dex_numbers'),
|
||||||
|
('pokemon_moves','pokemon_moves','ref','pokemon_moves'), ('pokemon_learnsets','pokemon_learnsets','ref','pokemon_learnsets'),
|
||||||
|
('pokemon_research','pokemon_research','ref','pokemon_research'), ('pokemon_game_research','pokemon_game_research','ref','pokemon_game_research'),
|
||||||
|
('tracker_games','tracker_games','ref','tracker_games'), ('tracker_items','tracker_items','ref','tracker_items'),
|
||||||
|
('tracker_challenges','tracker_challenges','ref','tracker_challenges'),
|
||||||
|
('user_games','user_game','games','catch_em_all_games'), ('user_entries','user_entry','games','catch_em_all_entries'),
|
||||||
|
('catch_em_all_settings','catch_em_all_settings','games','catch_em_all_settings'),
|
||||||
|
('pokemon_journeys','pokemon_journeys','games','pokemon_journeys'), ('pokemon_seen','pokemon_seen','games','pokemon_seen'),
|
||||||
|
('pokemon_catches','pokemon_catches','games','pokemon_catches'), ('pokemon_milestones','pokemon_milestones','games','pokemon_milestones'),
|
||||||
|
('tracker_progress','tracker_progress','games','tracker_progress'), ('tracker_ranks','tracker_ranks','games','tracker_ranks'),
|
||||||
|
('tracker_weapon_ranks','tracker_weapon_ranks','games','tracker_weapon_ranks'),
|
||||||
|
('tracker_challenge_progress','tracker_challenge_progress','games','tracker_challenge_progress'),
|
||||||
|
('osu_entries','osu_entries','games','osu_entries'),
|
||||||
|
('cs_market_items','cs_market_items','cs','items'), ('cs_price_observations','cs_price_observations','cs','price_observations'),
|
||||||
|
('cs_item_dimensions','cs_item_dimensions','cs','item_dimensions'), ('cs_price_streams','cs_price_streams','cs','price_streams'),
|
||||||
|
('animals','animals','animaldex','animals'), ('sightings','sightings','animaldex','sightings'),
|
||||||
|
('photos','photos','animaldex','sighting_photos'),
|
||||||
|
('blog_articles','blog_articles','site','blog_articles'), ('tasks','tasks','site','tasks'),
|
||||||
|
('topics','topics','site','topics'), ('tasks_to_topics','tasks_to_topics','site','task_topics'),
|
||||||
|
('lists','lists','legacy','lists'), ('list_items','list_items','legacy','list_items'),
|
||||||
|
('list_item_completions','list_item_completions','legacy','list_item_completions');
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
missing text;
|
||||||
|
BEGIN
|
||||||
|
SELECT string_agg(tablename, ', ') INTO missing FROM pg_tables
|
||||||
|
WHERE schemaname = 'public' AND tablename NOT IN (SELECT old_name FROM legacy.reorg_table_map);
|
||||||
|
IF missing IS NOT NULL THEN RAISE EXCEPTION 'unmapped public tables: %', missing; END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- types + shared trigger function
|
||||||
|
ALTER TYPE public.availability SET SCHEMA ref;
|
||||||
|
ALTER TYPE public.blog_article_status SET SCHEMA site;
|
||||||
|
ALTER FUNCTION public.site_touch_updated_at() SET SCHEMA meta;
|
||||||
|
ALTER FUNCTION meta.site_touch_updated_at() RENAME TO touch_updated_at;
|
||||||
|
|
||||||
|
-- Duplicate indexes (hand SQL + Drizzle both created one). Keep Drizzle's `_index` / constraint-backed `_key`.
|
||||||
|
DROP INDEX public.games_source_id_unique; -- dup of constraint games_source_id_key
|
||||||
|
DROP INDEX public.pokemon_number_unique; -- dup of constraint pokemon_number_key
|
||||||
|
DROP INDEX public.catch_em_all_settings_user_id_unique;-- dup of constraint ..._user_id_key
|
||||||
|
DROP INDEX public.money_accounts_user_id_idx;
|
||||||
|
DROP INDEX public.money_forecasts_user_date_idx;
|
||||||
|
DROP INDEX public.money_insurances_user_id_idx;
|
||||||
|
DROP INDEX public.money_recurring_user_id_idx;
|
||||||
|
DROP INDEX public.money_transactions_account_id_idx;
|
||||||
|
DROP INDEX public.money_transactions_user_date_idx;
|
||||||
|
DROP INDEX public.money_wishlist_user_id_idx;
|
||||||
|
DROP INDEX public.lists_slug_unique; -- dup of lists_slug_key (legacy table, tidy anyway)
|
||||||
|
|
||||||
|
-- Move + rename tables; rename their constraints, indexes and owned sequences to the new prefix.
|
||||||
|
CREATE FUNCTION pg_temp.reprefix(name text, old_name text, old_prefix text, new_name text) RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE AS $$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN name LIKE old_name || '\_%' THEN new_name || substr(name, length(old_name) + 1)
|
||||||
|
WHEN name LIKE old_prefix || '\_%' THEN new_name || substr(name, length(old_prefix) + 1)
|
||||||
|
ELSE NULL END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
m record; r record; tbl regclass; newn text;
|
||||||
|
BEGIN
|
||||||
|
FOR m IN SELECT * FROM legacy.reorg_table_map ORDER BY old_name LOOP
|
||||||
|
EXECUTE format('ALTER TABLE public.%I SET SCHEMA %I', m.old_name, m.new_schema);
|
||||||
|
IF m.old_name <> m.new_name THEN
|
||||||
|
EXECUTE format('ALTER TABLE %I.%I RENAME TO %I', m.new_schema, m.old_name, m.new_name);
|
||||||
|
END IF;
|
||||||
|
tbl := format('%I.%I', m.new_schema, m.new_name)::regclass;
|
||||||
|
-- constraints (renaming a pk/unique constraint renames its index too)
|
||||||
|
FOR r IN SELECT conname AS n FROM pg_constraint WHERE conrelid = tbl LOOP
|
||||||
|
newn := pg_temp.reprefix(r.n, m.old_name, m.old_prefix, m.new_name);
|
||||||
|
IF newn IS NOT NULL AND newn <> r.n THEN
|
||||||
|
EXECUTE format('ALTER TABLE %s RENAME CONSTRAINT %I TO %I', tbl, r.n, newn);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
-- remaining plain indexes
|
||||||
|
FOR r IN SELECT i.relname AS n FROM pg_index x JOIN pg_class i ON i.oid = x.indexrelid
|
||||||
|
WHERE x.indrelid = tbl
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = x.indexrelid AND c.conrelid = tbl) LOOP
|
||||||
|
newn := pg_temp.reprefix(r.n, m.old_name, m.old_prefix, m.new_name);
|
||||||
|
IF newn IS NOT NULL AND newn <> r.n THEN
|
||||||
|
EXECUTE format('ALTER INDEX %I.%I RENAME TO %I', m.new_schema, r.n, newn);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
-- owned sequences
|
||||||
|
FOR r IN SELECT s.relname AS n FROM pg_class s JOIN pg_depend d ON d.objid = s.oid AND d.deptype IN ('a','i')
|
||||||
|
WHERE s.relkind = 'S' AND d.refobjid = tbl LOOP
|
||||||
|
newn := pg_temp.reprefix(r.n, m.old_name, m.old_prefix, m.new_name);
|
||||||
|
IF newn IS NOT NULL AND newn <> r.n THEN
|
||||||
|
EXECUTE format('ALTER SEQUENCE %I.%I RENAME TO %I', m.new_schema, r.n, newn);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public') THEN
|
||||||
|
RAISE EXCEPTION 'public still has tables';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
RESET ROLE;
|
||||||
32
db/reorg/03_fixes.sql
Normal file
32
db/reorg/03_fixes.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
-- 03_fixes.sql: missing PKs, type fixes, CHECKs, updated_at columns.
|
||||||
|
SET ROLE personal;
|
||||||
|
|
||||||
|
-- missing primary keys (promote the existing unique indexes)
|
||||||
|
ALTER TABLE cs.item_dimensions ADD CONSTRAINT item_dimensions_pkey PRIMARY KEY USING INDEX item_dimensions_unique_idx;
|
||||||
|
ALTER TABLE cs.price_streams ADD CONSTRAINT price_streams_pkey PRIMARY KEY USING INDEX price_streams_unique_idx;
|
||||||
|
ALTER TABLE house.price_history ADD CONSTRAINT price_history_pkey PRIMARY KEY (listing_id, observed_at);
|
||||||
|
DROP INDEX house.price_history_listing_idx; -- now covered by the PK
|
||||||
|
|
||||||
|
-- include_recurring was an int used as a boolean (app code: charts.ts `!== 0`)
|
||||||
|
ALTER TABLE money.projection_settings ALTER COLUMN include_recurring DROP DEFAULT;
|
||||||
|
ALTER TABLE money.projection_settings ALTER COLUMN include_recurring TYPE boolean USING include_recurring <> 0;
|
||||||
|
ALTER TABLE money.projection_settings ALTER COLUMN include_recurring SET DEFAULT true;
|
||||||
|
|
||||||
|
-- osu numbers stored as text (0 rows in prod)
|
||||||
|
ALTER TABLE games.osu_entries ALTER COLUMN pp TYPE numeric USING nullif(pp, '')::numeric;
|
||||||
|
ALTER TABLE games.osu_entries ALTER COLUMN accuracy TYPE numeric USING nullif(replace(accuracy, '%', ''), '')::numeric;
|
||||||
|
ALTER TABLE games.osu_entries ALTER COLUMN rate DROP DEFAULT;
|
||||||
|
ALTER TABLE games.osu_entries ALTER COLUMN rate TYPE numeric(4,2) USING nullif(rate, '')::numeric;
|
||||||
|
ALTER TABLE games.osu_entries ALTER COLUMN rate SET DEFAULT 1.00;
|
||||||
|
|
||||||
|
-- CHECKs matching the values the app already enforces (src/lib/server/money-tracker.ts, money page)
|
||||||
|
ALTER TABLE money.recurring ADD CONSTRAINT recurring_cadence_check CHECK (cadence IN ('weekly','monthly','yearly'));
|
||||||
|
ALTER TABLE money.insurances ADD CONSTRAINT insurances_cadence_check CHECK (cadence IN ('weekly','monthly','yearly'));
|
||||||
|
ALTER TABLE money.accounts ADD CONSTRAINT accounts_kind_check CHECK (kind IN ('spendable','investment'));
|
||||||
|
ALTER TABLE money.utility_settings ADD CONSTRAINT utility_settings_kind_check CHECK (kind IN ('electricity','gas','water'));
|
||||||
|
ALTER TABLE money.utility_readings ADD CONSTRAINT utility_readings_kind_check CHECK (kind IN ('electricity','gas','water'));
|
||||||
|
ALTER TABLE money.projection_settings ADD CONSTRAINT projection_settings_contribution_mode_check CHECK (contribution_mode IN ('excess','fixed'));
|
||||||
|
|
||||||
|
-- user-editable tables without updated_at
|
||||||
|
ALTER TABLE house.alert_rules ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
||||||
|
RESET ROLE;
|
||||||
224
db/reorg/04_meta.sql
Normal file
224
db/reorg/04_meta.sql
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
-- 04_meta.sql: GUI registry (meta.tables / meta.columns), runtime-DDL functions, schema `lists`.
|
||||||
|
SET ROLE personal;
|
||||||
|
|
||||||
|
CREATE TABLE meta.tables (
|
||||||
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
schema_name text NOT NULL,
|
||||||
|
table_name text NOT NULL,
|
||||||
|
slug text NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'),
|
||||||
|
display_name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
icon text,
|
||||||
|
picture_url text,
|
||||||
|
kind text NOT NULL CHECK (kind IN ('list','core','ref')),
|
||||||
|
editable boolean NOT NULL DEFAULT true,
|
||||||
|
has_done boolean NOT NULL DEFAULT false,
|
||||||
|
default_sort text,
|
||||||
|
sort_order integer NOT NULL DEFAULT 0,
|
||||||
|
archived_at timestamptz,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (schema_name, table_name)
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE meta.tables IS 'One row per GUI-visible table (runtime lists + core + ref). Structure truth stays in pg_catalog; this is presentation.';
|
||||||
|
|
||||||
|
CREATE TABLE meta.columns (
|
||||||
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
table_id integer NOT NULL REFERENCES meta.tables(id) ON DELETE CASCADE,
|
||||||
|
column_name text NOT NULL,
|
||||||
|
label text,
|
||||||
|
ui_type text NOT NULL CHECK (ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json')),
|
||||||
|
options jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
position integer NOT NULL DEFAULT 0,
|
||||||
|
hidden boolean NOT NULL DEFAULT false,
|
||||||
|
help_text text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (table_id, column_name)
|
||||||
|
);
|
||||||
|
CREATE INDEX columns_table_id_index ON meta.columns (table_id);
|
||||||
|
|
||||||
|
CREATE TRIGGER touch_updated_at BEFORE UPDATE ON meta.tables FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at();
|
||||||
|
CREATE TRIGGER touch_updated_at BEFORE UPDATE ON meta.columns FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at();
|
||||||
|
|
||||||
|
RESET ROLE;
|
||||||
|
-- schema for runtime lists, owned by personal_ddl (not by the migration owner)
|
||||||
|
CREATE SCHEMA lists AUTHORIZATION personal_ddl;
|
||||||
|
COMMENT ON SCHEMA lists IS 'Runtime-created user lists. Never touched by Drizzle migrations (excluded via schemaFilter). Create/alter only via meta.* functions.';
|
||||||
|
GRANT USAGE, CREATE ON SCHEMA meta TO personal_ddl;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON meta.tables, meta.columns TO personal_ddl;
|
||||||
|
SET ROLE personal_ddl;
|
||||||
|
|
||||||
|
-- guess a ui_type from a column's catalog type (format_type output) and name
|
||||||
|
CREATE FUNCTION meta.guess_ui_type(p_type text, p_col text, p_is_enum boolean DEFAULT false) RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN p_is_enum THEN 'select'
|
||||||
|
WHEN p_type LIKE '%[]' THEN 'multiselect'
|
||||||
|
WHEN p_col IN ('picture_url','image_url','icon_url','image') THEN 'image'
|
||||||
|
WHEN p_col LIKE '%\_url' OR p_col = 'url' THEN 'url'
|
||||||
|
WHEN p_col LIKE '%\_cents' THEN 'cents'
|
||||||
|
WHEN p_col IN ('notes','description','content','note') THEN 'longtext'
|
||||||
|
WHEN p_type IN ('smallint','integer','bigint','real','double precision') OR p_type LIKE 'numeric%' THEN 'number'
|
||||||
|
WHEN p_type = 'boolean' THEN 'bool'
|
||||||
|
WHEN p_type = 'date' THEN 'date'
|
||||||
|
WHEN p_type LIKE 'timestamp%' THEN 'datetime'
|
||||||
|
WHEN p_type IN ('json','jsonb') THEN 'json'
|
||||||
|
ELSE 'text' END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- reconcile meta.columns with pg_catalog for one table (or all registered tables).
|
||||||
|
-- Uses pg_attribute (not information_schema, which hides tables the caller has no grants on).
|
||||||
|
CREATE FUNCTION meta.sync(p_table_id integer DEFAULT NULL) RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE k integer;
|
||||||
|
BEGIN
|
||||||
|
IF to_regclass('pg_temp._meta_cols') IS NULL THEN CREATE TEMP TABLE _meta_cols ON COMMIT DROP AS SELECT 1::integer AS table_id, ''::text AS col, ''::text AS typ, false AS is_enum, 0::integer AS pos, NULL::jsonb AS vals LIMIT 0; END IF;
|
||||||
|
TRUNCATE _meta_cols;
|
||||||
|
INSERT INTO _meta_cols
|
||||||
|
SELECT t.id, a.attname, format_type(a.atttypid, a.atttypmod), ty.typtype = 'e', a.attnum,
|
||||||
|
CASE WHEN ty.typtype = 'e' THEN (SELECT jsonb_agg(e.enumlabel ORDER BY e.enumsortorder) FROM pg_enum e WHERE e.enumtypid = ty.oid) END
|
||||||
|
FROM meta.tables t
|
||||||
|
JOIN pg_class c ON c.relname = t.table_name
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.schema_name
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
|
||||||
|
JOIN pg_type ty ON ty.oid = a.atttypid
|
||||||
|
WHERE p_table_id IS NULL OR t.id = p_table_id;
|
||||||
|
DELETE FROM meta.columns mc
|
||||||
|
WHERE (p_table_id IS NULL OR mc.table_id = p_table_id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM _meta_cols x WHERE x.table_id = mc.table_id AND x.col = mc.column_name);
|
||||||
|
INSERT INTO meta.columns (table_id, column_name, label, ui_type, options, position, hidden)
|
||||||
|
SELECT x.table_id, x.col, initcap(replace(x.col, '_', ' ')), meta.guess_ui_type(x.typ, x.col, x.is_enum),
|
||||||
|
CASE WHEN x.vals IS NOT NULL THEN jsonb_build_object('values', x.vals) ELSE '{}'::jsonb END,
|
||||||
|
x.pos, x.col IN ('id','created_at','updated_at','user_id','sort_order')
|
||||||
|
FROM _meta_cols x
|
||||||
|
ON CONFLICT (table_id, column_name) DO NOTHING;
|
||||||
|
GET DIAGNOSTICS k = ROW_COUNT;
|
||||||
|
RETURN k;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- allowed column types for runtime lists -> SQL type
|
||||||
|
CREATE FUNCTION meta.sql_type(p_ui_type text) RETURNS text
|
||||||
|
LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $$
|
||||||
|
SELECT CASE p_ui_type
|
||||||
|
WHEN 'text' THEN 'text' WHEN 'longtext' THEN 'text' WHEN 'url' THEN 'text' WHEN 'image' THEN 'text'
|
||||||
|
WHEN 'select' THEN 'text' WHEN 'number' THEN 'numeric' WHEN 'cents' THEN 'integer'
|
||||||
|
WHEN 'date' THEN 'date' WHEN 'datetime' THEN 'timestamptz' WHEN 'bool' THEN 'boolean'
|
||||||
|
WHEN 'multiselect' THEN 'text[]' WHEN 'json' THEN 'jsonb' END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION meta.assert_ident(p text) RETURNS void
|
||||||
|
LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog AS $$
|
||||||
|
BEGIN
|
||||||
|
IF p IS NULL OR p !~ '^[a-z][a-z0-9_]{0,62}$' THEN
|
||||||
|
RAISE EXCEPTION 'invalid identifier: %', p USING ERRCODE = '22023';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- add one column to a runtime list. p_col: {"name","ui_type","label"?,"options"?,"default"?}
|
||||||
|
CREATE FUNCTION meta.add_column(p_slug text, p_col jsonb) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE
|
||||||
|
t meta.tables; c text := p_col->>'name'; u text := coalesce(p_col->>'ui_type', 'text');
|
||||||
|
st text := meta.sql_type(u); vals jsonb := p_col->'options'->'values'; ddl text;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO t FROM meta.tables WHERE slug = p_slug AND kind = 'list';
|
||||||
|
IF NOT FOUND THEN RAISE EXCEPTION 'no runtime list %', p_slug; END IF;
|
||||||
|
PERFORM meta.assert_ident(c);
|
||||||
|
IF c IN ('id','name','notes','picture_url','done_at','sort_order','created_at','updated_at') THEN
|
||||||
|
RAISE EXCEPTION 'column % is a standard column', c; END IF;
|
||||||
|
IF st IS NULL THEN RAISE EXCEPTION 'unsupported ui_type %', u; END IF;
|
||||||
|
ddl := format('ALTER TABLE lists.%I ADD COLUMN %I %s', t.table_name, c, st);
|
||||||
|
IF p_col ? 'default' THEN ddl := ddl || format(' DEFAULT %L', p_col->>'default'); END IF;
|
||||||
|
IF u = 'select' AND jsonb_typeof(vals) = 'array' THEN
|
||||||
|
ddl := ddl || format(' CONSTRAINT %I CHECK (%I IN (%s))', t.table_name || '_' || c || '_check', c,
|
||||||
|
(SELECT string_agg(quote_literal(v), ',') FROM jsonb_array_elements_text(vals) v));
|
||||||
|
END IF;
|
||||||
|
EXECUTE ddl;
|
||||||
|
INSERT INTO meta.columns (table_id, column_name, label, ui_type, options, position)
|
||||||
|
VALUES (t.id, c, coalesce(p_col->>'label', initcap(replace(c, '_', ' '))), u,
|
||||||
|
coalesce(p_col->'options', '{}'::jsonb),
|
||||||
|
(SELECT coalesce(max(position), 0) + 1 FROM meta.columns WHERE table_id = t.id));
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- create a runtime list table with the standard columns + extras
|
||||||
|
CREATE FUNCTION meta.create_list(
|
||||||
|
p_slug text, p_table text, p_display_name text,
|
||||||
|
p_has_done boolean DEFAULT true, p_columns jsonb DEFAULT '[]'::jsonb,
|
||||||
|
p_picture_url text DEFAULT NULL, p_description text DEFAULT NULL, p_icon text DEFAULT NULL,
|
||||||
|
p_sort_order integer DEFAULT 0
|
||||||
|
) RETURNS integer
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE tid integer; col jsonb;
|
||||||
|
BEGIN
|
||||||
|
PERFORM meta.assert_ident(p_table);
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'lists' AND tablename = p_table) THEN
|
||||||
|
RAISE EXCEPTION 'lists.% already exists', p_table; END IF;
|
||||||
|
EXECUTE format($f$CREATE TABLE lists.%1$I (
|
||||||
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
name text NOT NULL CHECK (btrim(name) <> ''),
|
||||||
|
notes text,
|
||||||
|
picture_url text,
|
||||||
|
%2$s
|
||||||
|
sort_order integer NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now())$f$,
|
||||||
|
p_table, CASE WHEN p_has_done THEN 'done_at timestamptz,' ELSE '' END);
|
||||||
|
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON lists.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', p_table);
|
||||||
|
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, description, icon, picture_url, kind, editable, has_done, default_sort, sort_order)
|
||||||
|
VALUES ('lists', p_table, p_slug, p_display_name, p_description, p_icon, p_picture_url, 'list', true, p_has_done,
|
||||||
|
CASE WHEN p_has_done THEN 'done_at nulls first, sort_order, name' ELSE 'sort_order, name' END, p_sort_order)
|
||||||
|
RETURNING id INTO tid;
|
||||||
|
PERFORM meta.sync(tid);
|
||||||
|
UPDATE meta.columns SET label = 'Done', ui_type = 'datetime' WHERE table_id = tid AND column_name = 'done_at';
|
||||||
|
FOR col IN SELECT * FROM jsonb_array_elements(p_columns) LOOP
|
||||||
|
PERFORM meta.add_column(p_slug, col);
|
||||||
|
END LOOP;
|
||||||
|
RETURN tid;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE FUNCTION meta.rename_column(p_slug text, p_old text, p_new text) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE t meta.tables;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO t FROM meta.tables WHERE slug = p_slug AND kind = 'list';
|
||||||
|
IF NOT FOUND THEN RAISE EXCEPTION 'no runtime list %', p_slug; END IF;
|
||||||
|
PERFORM meta.assert_ident(p_new);
|
||||||
|
IF p_old IN ('id','name','done_at','created_at','updated_at') THEN RAISE EXCEPTION 'standard column'; END IF;
|
||||||
|
EXECUTE format('ALTER TABLE lists.%I RENAME COLUMN %I TO %I', t.table_name, p_old, p_new);
|
||||||
|
UPDATE meta.columns SET column_name = p_new WHERE table_id = t.id AND column_name = p_old;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE FUNCTION meta.drop_column(p_slug text, p_col text) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE t meta.tables;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO t FROM meta.tables WHERE slug = p_slug AND kind = 'list';
|
||||||
|
IF NOT FOUND THEN RAISE EXCEPTION 'no runtime list %', p_slug; END IF;
|
||||||
|
IF p_col IN ('id','name','notes','picture_url','done_at','sort_order','created_at','updated_at') THEN
|
||||||
|
RAISE EXCEPTION 'standard column'; END IF;
|
||||||
|
EXECUTE format('ALTER TABLE lists.%I DROP COLUMN %I', t.table_name, p_col);
|
||||||
|
DELETE FROM meta.columns WHERE table_id = t.id AND column_name = p_col;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE FUNCTION meta.archive_list(p_slug text) RETURNS void
|
||||||
|
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
UPDATE meta.tables SET archived_at = now() WHERE slug = p_slug AND kind = 'list' AND archived_at IS NULL
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- real DROP only after 7 days in the archive (backups cover the rest)
|
||||||
|
CREATE FUNCTION meta.drop_list(p_slug text) RETURNS void
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, meta AS $$
|
||||||
|
DECLARE t meta.tables;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO t FROM meta.tables WHERE slug = p_slug AND kind = 'list';
|
||||||
|
IF NOT FOUND THEN RAISE EXCEPTION 'no runtime list %', p_slug; END IF;
|
||||||
|
IF t.archived_at IS NULL OR t.archived_at > now() - interval '7 days' THEN
|
||||||
|
RAISE EXCEPTION 'archive % first and wait 7 days', p_slug; END IF;
|
||||||
|
EXECUTE format('DROP TABLE lists.%I', t.table_name);
|
||||||
|
DELETE FROM meta.tables WHERE id = t.id;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
RESET ROLE;
|
||||||
|
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA meta FROM PUBLIC;
|
||||||
|
GRANT EXECUTE ON FUNCTION meta.touch_updated_at() TO PUBLIC; -- trigger fn, must stay callable
|
||||||
176
db/reorg/05_lists.sql
Normal file
176
db/reorg/05_lists.sql
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
-- 05_lists.sql: the 22 legacy lists become real tables in schema `lists`.
|
||||||
|
-- legacy.* stays untouched (rollback reference); typo fixes + dedupes are applied to a temp working copy.
|
||||||
|
-- Runs as superuser (inserts into personal_ddl-owned tables); tables are created via meta.create_list.
|
||||||
|
|
||||||
|
CREATE TEMP TABLE src ON COMMIT DROP AS
|
||||||
|
SELECT i.id, i.list_id, btrim(i.name) AS name, nullif(btrim(i.description), '') AS notes, i.picture_url,
|
||||||
|
c.created_at AS done_at, i.created_at, i.updated_at
|
||||||
|
FROM legacy.list_items i
|
||||||
|
LEFT JOIN legacy.list_item_completions c ON c.item_id = i.id;
|
||||||
|
|
||||||
|
CREATE TEMP TABLE fix_log (item_id integer, list_id integer, action text, before text, after text) ON COMMIT DROP;
|
||||||
|
|
||||||
|
-- ---- dedupes: keep the lower id, carry over done_at / picture / notes, delete the other ----
|
||||||
|
CREATE TEMP TABLE dedupe (keep_id integer, drop_id integer, new_name text) ON COMMIT DROP;
|
||||||
|
INSERT INTO dedupe VALUES
|
||||||
|
(165, 323, NULL), -- places: New York x2
|
||||||
|
(168, 331, NULL), -- places: France x2
|
||||||
|
(156, 271, 'Advanced Programming in the UNIX Environment'), -- books: "...UNIX® Environment" dup
|
||||||
|
(139, 287, 'Building Knowledge Graphs: A Practitioner''s Guide'); -- books: short + full title
|
||||||
|
UPDATE src k SET done_at = coalesce(k.done_at, d.done_at), picture_url = coalesce(k.picture_url, d.picture_url),
|
||||||
|
notes = coalesce(k.notes, d.notes), name = coalesce(x.new_name, k.name)
|
||||||
|
FROM dedupe x JOIN src d ON d.id = x.drop_id WHERE k.id = x.keep_id;
|
||||||
|
INSERT INTO fix_log SELECT s.id, s.list_id, 'dedupe-drop', s.name, 'merged into ' || x.keep_id FROM src s JOIN dedupe x ON x.drop_id = s.id;
|
||||||
|
DELETE FROM src WHERE id IN (SELECT drop_id FROM dedupe);
|
||||||
|
|
||||||
|
-- ---- typo / naming fixes ----
|
||||||
|
CREATE TEMP TABLE renames (id integer PRIMARY KEY, new_name text NOT NULL) ON COMMIT DROP;
|
||||||
|
INSERT INTO renames VALUES
|
||||||
|
-- vinyls (kept as "Artist - Album" here; split below)
|
||||||
|
(343, 'Pearl Jam - Ten'), (346, 'Amy Winehouse - Back to Black'),
|
||||||
|
(27, 'Slipknot - Vol. 3: (The Subliminal Verses)'), (6, 'Them Crooked Vultures - Them Crooked Vultures'),
|
||||||
|
(39, 'Various Artists - Katamari Damacy (OST)'),
|
||||||
|
(40, 'Gojira - The Way of All Flesh'), (370, 'Gojira - The Link'),
|
||||||
|
(80, 'Deftones - Private Music'), (308, 'Kendrick Lamar - DAMN.'),
|
||||||
|
-- movies/shows
|
||||||
|
(98, 'Kim''s Convenience'), (394, 'Azuki-chan'), (111, 'A Minecraft Movie'),
|
||||||
|
-- books
|
||||||
|
(150, 'Domain-Driven Design: Tackling Complexity in the Heart of Software'),
|
||||||
|
(285, 'Molecular Biology of the Cell'), (288, 'Building Microservices'),
|
||||||
|
(147, 'Scrum: The Art of Doing Twice the Work in Half the Time'),
|
||||||
|
-- places
|
||||||
|
(325, 'Curaçao'),
|
||||||
|
-- cars
|
||||||
|
(322, 'Mazda MX-5 Miata'),
|
||||||
|
-- leetcode
|
||||||
|
(217, 'Two Sum'),
|
||||||
|
-- foods
|
||||||
|
(221, 'Sotteok Sotteok'), (230, 'Bibim Naengmyeon'), (240, 'Doenjang Jjigae'), (241, 'Kimchi Jjigae'),
|
||||||
|
-- papers
|
||||||
|
(190, 'Order Matters: Sequence to Sequence for Sets'),
|
||||||
|
(200, 'GPipe: Easy Scaling with Micro-Batch Pipeline Parallelism'),
|
||||||
|
(203, 'Attention Is All You Need'),
|
||||||
|
(201, 'Deep Speech 2: End-to-End Speech Recognition in English and Mandarin'),
|
||||||
|
-- games
|
||||||
|
(385, 'Chrono Trigger');
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF (SELECT count(*) FROM renames r JOIN src s ON s.id = r.id) <> (SELECT count(*) FROM renames) THEN
|
||||||
|
RAISE EXCEPTION 'rename list references missing item ids'; END IF;
|
||||||
|
END $$;
|
||||||
|
INSERT INTO fix_log SELECT s.id, s.list_id, 'rename', s.name, r.new_name FROM src s JOIN renames r ON r.id = s.id WHERE s.name <> r.new_name;
|
||||||
|
UPDATE src s SET name = r.new_name FROM renames r WHERE r.id = s.id;
|
||||||
|
|
||||||
|
-- helper: copy standard columns for one list into lists.<t> and return the row count
|
||||||
|
CREATE FUNCTION pg_temp.copy_std(p_list integer, p_table text, p_has_done boolean) RETURNS bigint LANGUAGE plpgsql AS $$
|
||||||
|
DECLARE n bigint;
|
||||||
|
BEGIN
|
||||||
|
EXECUTE format('INSERT INTO lists.%I (id, name, notes, picture_url, %s sort_order, created_at, updated_at)
|
||||||
|
SELECT id, name, notes, picture_url, %s row_number() OVER (ORDER BY id), created_at, updated_at
|
||||||
|
FROM src WHERE list_id = %s', p_table,
|
||||||
|
CASE WHEN p_has_done THEN 'done_at,' ELSE '' END,
|
||||||
|
CASE WHEN p_has_done THEN 'done_at,' ELSE '' END, p_list);
|
||||||
|
GET DIAGNOSTICS n = ROW_COUNT;
|
||||||
|
EXECUTE format('SELECT setval(pg_get_serial_sequence(%L, ''id''), (SELECT max(id) FROM lists.%I))', 'lists.' || p_table, p_table);
|
||||||
|
RETURN n;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE TEMP TABLE list_map (list_id integer PRIMARY KEY, tbl text NOT NULL, has_done boolean NOT NULL, cols jsonb NOT NULL) ON COMMIT DROP;
|
||||||
|
INSERT INTO list_map VALUES
|
||||||
|
(1, 'vinyls', true, '[{"name":"artist","ui_type":"text"}]'),
|
||||||
|
(2, 'watchlist', true, '[{"name":"kind","ui_type":"select","options":{"values":["movie","show","anime","documentary"]}}]'),
|
||||||
|
(3, 'books', true, '[{"name":"author","ui_type":"text"}]'),
|
||||||
|
(4, 'side_projects', false, '[{"name":"status","ui_type":"select","default":"idea","options":{"values":["idea","active","paused","done","dropped"]}}]'),
|
||||||
|
(5, 'artists_to_see', true, '[]'),
|
||||||
|
(6, 'osu_maps', true, '[{"name":"mods","ui_type":"text"},{"name":"goal","ui_type":"select","options":{"values":["pass","fc"]}}]'),
|
||||||
|
(7, 'animals_to_spot', true, '[]'),
|
||||||
|
(8, 'vehicles_to_spot', true, '[{"name":"kind","ui_type":"select","default":"car","options":{"values":["car","train","plane"]}}]'),
|
||||||
|
(9, 'leetcode', true, '[{"name":"kind","ui_type":"select","default":"problem","options":{"values":["problem","topic"]}},{"name":"url","ui_type":"url"}]'),
|
||||||
|
(10, 'restaurants', true, '[{"name":"city","ui_type":"text"}]'),
|
||||||
|
(11, 'recipes', true, '[{"name":"cuisine","ui_type":"text"},{"name":"recipe_url","ui_type":"url"}]'),
|
||||||
|
(12, 'places', true, '[{"name":"kind","ui_type":"select","options":{"values":["country","region","city","island"]}}]'),
|
||||||
|
(13, 'papers', false, '[{"name":"authors","ui_type":"text"},{"name":"year","ui_type":"number"},{"name":"url","ui_type":"url"}]'),
|
||||||
|
(14, 'piano_songs', true, '[{"name":"artist","ui_type":"text"},{"name":"reference_url","ui_type":"url"}]'),
|
||||||
|
(15, 'games_to_complete', true, '[{"name":"platform","ui_type":"text"}]'),
|
||||||
|
(22, 'things_to_learn', true, '[]'),
|
||||||
|
(23, 'build_from_scratch', true, '[]'),
|
||||||
|
(24, 'tools_to_try', true, '[{"name":"url","ui_type":"url"}]'),
|
||||||
|
(25, 'setup_upgrades', true, '[]'),
|
||||||
|
(26, 'bucket_list', true, '[]'),
|
||||||
|
(27, 'docs_to_read', true, '[{"name":"rfc_number","ui_type":"number"},{"name":"topic","ui_type":"text"},{"name":"starter_pack","ui_type":"bool","default":"false"},{"name":"url","ui_type":"url"}]'),
|
||||||
|
(28, 'banger_songs', false, '[{"name":"artist","ui_type":"text"},{"name":"kind","ui_type":"select","default":"song","options":{"values":["song","playlist"]}},{"name":"url","ui_type":"url"}]');
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM legacy.lists l WHERE NOT EXISTS (SELECT 1 FROM list_map m WHERE m.list_id = l.id))
|
||||||
|
OR EXISTS (SELECT 1 FROM list_map m JOIN legacy.lists l ON l.id = m.list_id WHERE l.is_progress <> m.has_done AND m.list_id NOT IN (4)) THEN
|
||||||
|
RAISE EXCEPTION 'list_map does not cover legacy.lists / has_done mismatch';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- create tables (as personal_ddl via SECURITY DEFINER) and copy standard columns
|
||||||
|
DO $$
|
||||||
|
DECLARE m record; l record; n bigint;
|
||||||
|
BEGIN
|
||||||
|
FOR m IN SELECT * FROM list_map ORDER BY list_id LOOP
|
||||||
|
SELECT * INTO l FROM legacy.lists WHERE id = m.list_id;
|
||||||
|
PERFORM meta.create_list(l.slug, m.tbl, l.name, m.has_done, m.cols, l.picture_url, NULL, NULL, m.list_id);
|
||||||
|
UPDATE meta.tables SET created_at = l.created_at WHERE slug = l.slug;
|
||||||
|
n := pg_temp.copy_std(m.list_id, m.tbl, m.has_done);
|
||||||
|
RAISE NOTICE 'lists.%: % rows', m.tbl, n;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- parsing must not bump updated_at: suppress user triggers for these UPDATEs (superuser only)
|
||||||
|
SET LOCAL session_replication_role = replica;
|
||||||
|
-- ---- per-list parsing into the extra columns (only where it parses cleanly) ----
|
||||||
|
-- vinyls: "Artist - Album"
|
||||||
|
UPDATE lists.vinyls SET artist = split_part(name, ' - ', 1), name = substr(name, strpos(name, ' - ') + 3)
|
||||||
|
WHERE strpos(name, ' - ') > 0;
|
||||||
|
-- books: "Author - Title" only for the 3 personal-name ones (others contain ':' subtitles, never ' - ')
|
||||||
|
UPDATE lists.books SET author = split_part(name, ' - ', 1), name = substr(name, strpos(name, ' - ') + 3)
|
||||||
|
WHERE id IN (28, 29, 306);
|
||||||
|
-- side projects: everything starts as an idea
|
||||||
|
UPDATE lists.side_projects SET status = 'idea';
|
||||||
|
-- osu: trailing mods
|
||||||
|
UPDATE lists.osu_maps SET name = 'Lonely Go', mods = 'HR' WHERE id = 310;
|
||||||
|
UPDATE lists.osu_maps SET name = 'Imagination', mods = 'HR' WHERE id = 317;
|
||||||
|
-- vehicles: all current entries are cars
|
||||||
|
UPDATE lists.vehicles_to_spot SET kind = 'car';
|
||||||
|
-- leetcode
|
||||||
|
UPDATE lists.leetcode SET kind = CASE WHEN name IN ('Two Sum','3Sum') THEN 'problem' ELSE 'topic' END;
|
||||||
|
UPDATE lists.leetcode SET url = 'https://leetcode.com/problems/two-sum/' WHERE name = 'Two Sum';
|
||||||
|
UPDATE lists.leetcode SET url = 'https://leetcode.com/problems/3sum/' WHERE name = '3Sum';
|
||||||
|
-- restaurants: "Name (City)"
|
||||||
|
UPDATE lists.restaurants SET city = substring(name from '\(([^)]+)\)\s*$'), name = btrim(regexp_replace(name, '\s*\([^)]+\)\s*$', ''))
|
||||||
|
WHERE name ~ '\([^)]+\)\s*$';
|
||||||
|
-- recipes
|
||||||
|
UPDATE lists.recipes SET cuisine = CASE WHEN name = 'Yogurt, honey & cruesli' THEN NULL ELSE 'korean' END;
|
||||||
|
-- places
|
||||||
|
UPDATE lists.places SET kind = CASE
|
||||||
|
WHEN name IN ('London','Seattle','Miami','Dubai','New York') THEN 'city'
|
||||||
|
WHEN name IN ('California','Texas') THEN 'region'
|
||||||
|
WHEN name IN ('Curaçao','Aruba','Bonaire') THEN 'island'
|
||||||
|
ELSE 'country' END;
|
||||||
|
-- piano: "artist - title" + URL in notes
|
||||||
|
UPDATE lists.piano_songs SET artist = 'ryo (supercell)', name = 'odds & ends', reference_url = notes, notes = NULL
|
||||||
|
WHERE name = 'ryo/supercell - odds & ends';
|
||||||
|
-- tools: notes that are a bare URL become url
|
||||||
|
UPDATE lists.tools_to_try SET url = notes, notes = NULL WHERE notes ~ '^https?://\S+$';
|
||||||
|
-- docs/RFCs: "RFC n – Title", notes "Topic · starter pack"
|
||||||
|
UPDATE lists.docs_to_read SET
|
||||||
|
rfc_number = substring(name from '^RFC (\d+)')::integer,
|
||||||
|
topic = btrim(split_part(notes, '·', 1)),
|
||||||
|
starter_pack = notes LIKE '%starter pack%',
|
||||||
|
url = 'https://www.rfc-editor.org/rfc/rfc' || substring(name from '^RFC (\d+)'),
|
||||||
|
notes = NULL
|
||||||
|
WHERE name ~ '^RFC \d+';
|
||||||
|
-- banger songs: "Artist - Title" + URL notes; the 2 YT mixes are playlists
|
||||||
|
UPDATE lists.banger_songs SET url = notes, notes = NULL WHERE notes ~ '^https?://\S+$';
|
||||||
|
UPDATE lists.banger_songs SET artist = split_part(name, ' - ', 1), name = substr(name, strpos(name, ' - ') + 3), kind = 'song'
|
||||||
|
WHERE strpos(name, ' - ') > 0;
|
||||||
|
UPDATE lists.banger_songs SET kind = 'playlist' WHERE artist IS NULL;
|
||||||
|
|
||||||
|
SET LOCAL session_replication_role = origin;
|
||||||
|
|
||||||
|
-- persist the fix log for review (legacy schema, dropped with it)
|
||||||
|
CREATE TABLE legacy.reorg_list_fixes AS SELECT * FROM fix_log;
|
||||||
|
ALTER TABLE legacy.reorg_list_fixes OWNER TO personal; -- so `personal` can pg_dump everything
|
||||||
148
db/reorg/06_hardware_energy_cs.sql
Normal file
148
db/reorg/06_hardware_energy_cs.sql
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
-- 06_hardware_energy_cs.sql: bring hardware + energy + CS prices over from dev.
|
||||||
|
-- Expects schemas import_hardware / import_energy / import_cs, staged by run.sh from dumps/dev-import-staged.dump.
|
||||||
|
SET ROLE personal;
|
||||||
|
|
||||||
|
-- ---------- energy (DDL identical to ned-collector/collect.py so its IF NOT EXISTS is a no-op) ----------
|
||||||
|
CREATE TABLE energy.gas_storage_daily (
|
||||||
|
gas_day date NOT NULL,
|
||||||
|
site text NOT NULL,
|
||||||
|
point_name text,
|
||||||
|
volume_twh numeric(12,4),
|
||||||
|
fill_pct numeric(7,3),
|
||||||
|
source text NOT NULL DEFAULT 'ned-json',
|
||||||
|
first_seen timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (gas_day, site)
|
||||||
|
);
|
||||||
|
CREATE TABLE energy.ned_utilization (
|
||||||
|
valid_from timestamptz NOT NULL, valid_to timestamptz NOT NULL,
|
||||||
|
point text NOT NULL, type text NOT NULL, activity text NOT NULL, classification text NOT NULL, granularity text NOT NULL,
|
||||||
|
capacity_kw numeric, volume_kwh numeric, percentage numeric, emission_kg numeric, emission_factor numeric,
|
||||||
|
source_file text,
|
||||||
|
first_seen timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (valid_from, point, type, activity, classification, granularity)
|
||||||
|
);
|
||||||
|
CREATE INDEX ned_utilization_type_time ON energy.ned_utilization (type, granularity, valid_from DESC);
|
||||||
|
CREATE TABLE energy.collector_runs (
|
||||||
|
id bigserial PRIMARY KEY,
|
||||||
|
started_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz,
|
||||||
|
mode text NOT NULL, ok boolean, rows_upserted integer, errors text
|
||||||
|
);
|
||||||
|
CREATE VIEW energy.gas_storage_latest AS
|
||||||
|
SELECT DISTINCT ON (site) site, gas_day, volume_twh, fill_pct, updated_at
|
||||||
|
FROM energy.gas_storage_daily ORDER BY site, gas_day DESC;
|
||||||
|
CREATE VIEW energy.electricity_hourly AS
|
||||||
|
SELECT valid_from AT TIME ZONE 'Europe/Amsterdam' AS hour_local, type, classification,
|
||||||
|
round(volume_kwh / 1000.0, 1) AS mwh, percentage, emission_factor AS kg_co2_per_kwh
|
||||||
|
FROM energy.ned_utilization WHERE granularity = 'Hour';
|
||||||
|
|
||||||
|
INSERT INTO energy.gas_storage_daily SELECT * FROM import_energy.gas_storage_daily;
|
||||||
|
INSERT INTO energy.ned_utilization SELECT * FROM import_energy.ned_utilization;
|
||||||
|
INSERT INTO energy.collector_runs SELECT * FROM import_energy.collector_runs;
|
||||||
|
SELECT setval('energy.collector_runs_id_seq', coalesce((SELECT max(id) FROM energy.collector_runs), 1));
|
||||||
|
|
||||||
|
-- ---------- hardware (prices -> integer cents) ----------
|
||||||
|
CREATE TABLE hardware.devices (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
name text NOT NULL UNIQUE,
|
||||||
|
kind text NOT NULL CHECK (kind IN ('desktop','laptop','phone','vm','monitor')),
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE TABLE hardware.components (
|
||||||
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
device_id bigint NOT NULL REFERENCES hardware.devices(id) ON DELETE CASCADE,
|
||||||
|
category text NOT NULL CHECK (category IN ('case','cpu','motherboard','ram','cpu_cooler','gpu','psu','storage','fan','monitor','lighting','accessory')),
|
||||||
|
brand text,
|
||||||
|
model text NOT NULL,
|
||||||
|
quantity integer NOT NULL DEFAULT 1 CHECK (quantity > 0),
|
||||||
|
specs jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
unit_price_cents integer CHECK (unit_price_cents >= 0),
|
||||||
|
total_price_cents integer CHECK (total_price_cents >= 0),
|
||||||
|
shop text,
|
||||||
|
order_number text,
|
||||||
|
order_date date,
|
||||||
|
warranty_until date,
|
||||||
|
invoice_number text,
|
||||||
|
status text NOT NULL DEFAULT 'in_use' CHECK (status IN ('in_use','retired','sold')),
|
||||||
|
verified boolean NOT NULL DEFAULT true,
|
||||||
|
notes text,
|
||||||
|
source text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX components_category_idx ON hardware.components (category);
|
||||||
|
CREATE INDEX components_device_idx ON hardware.components (device_id);
|
||||||
|
CREATE UNIQUE INDEX components_dedupe_uq ON hardware.components (device_id, model, order_number) NULLS NOT DISTINCT;
|
||||||
|
CREATE INDEX components_specs_gin ON hardware.components USING gin (specs);
|
||||||
|
COMMENT ON SCHEMA hardware IS 'Stan''s hardware inventory: devices (PCs, laptops, phones, VMs, monitors) and their components/peripherals with purchase + warranty details.';
|
||||||
|
COMMENT ON TABLE hardware.devices IS 'A physical or virtual machine Stan owns/uses (e.g. stan-pc). Components hang off a device.';
|
||||||
|
COMMENT ON TABLE hardware.components IS 'One line item (part or peripheral) belonging to a device, with purchase, price and warranty info.';
|
||||||
|
COMMENT ON COLUMN hardware.components.unit_price_cents IS 'Price per unit in EUR cents incl. VAT.';
|
||||||
|
COMMENT ON COLUMN hardware.components.total_price_cents IS 'unit price * quantity in EUR cents incl. VAT (excl. shipping).';
|
||||||
|
COMMENT ON COLUMN hardware.components.verified IS 'false = purchase is real but unconfirmed whether it is (still) part of this setup, or some details are guessed. See notes.';
|
||||||
|
COMMENT ON COLUMN hardware.components.source IS 'Where the data came from: user_list, gmail, manual.';
|
||||||
|
|
||||||
|
INSERT INTO hardware.devices (id, name, kind, notes, created_at, updated_at) OVERRIDING SYSTEM VALUE
|
||||||
|
SELECT id, name, kind, notes, created_at, created_at FROM import_hardware.devices;
|
||||||
|
INSERT INTO hardware.components (id, device_id, category, brand, model, quantity, specs, unit_price_cents, total_price_cents,
|
||||||
|
shop, order_number, order_date, warranty_until, invoice_number, status, verified, notes, source, created_at, updated_at)
|
||||||
|
OVERRIDING SYSTEM VALUE
|
||||||
|
SELECT id, device_id, category, brand, model, quantity, specs, round(unit_price_eur * 100)::integer, round(total_price_eur * 100)::integer,
|
||||||
|
shop, order_number, order_date, warranty_until, invoice_number, status, verified, notes, source, created_at, updated_at
|
||||||
|
FROM import_hardware.components;
|
||||||
|
SELECT setval(pg_get_serial_sequence('hardware.devices', 'id'), (SELECT max(id) FROM hardware.devices));
|
||||||
|
SELECT setval(pg_get_serial_sequence('hardware.components', 'id'), (SELECT max(id) FROM hardware.components));
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF (SELECT sum(total_price_cents) FROM hardware.components) <> (SELECT round(sum(total_price_eur) * 100) FROM import_hardware.components) THEN
|
||||||
|
RAISE EXCEPTION 'hardware cents conversion mismatch'; END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE VIEW hardware.setup_overview AS
|
||||||
|
SELECT d.name AS device, d.kind AS device_kind, c.category, c.brand, c.model, c.quantity,
|
||||||
|
c.total_price_cents, round(c.total_price_cents / 100.0, 2) AS total_price_eur,
|
||||||
|
c.shop, c.order_date, c.warranty_until, (c.warranty_until >= CURRENT_DATE) AS under_warranty,
|
||||||
|
c.status, c.verified, c.specs, c.notes, c.order_number, c.invoice_number, c.source, c.id AS component_id
|
||||||
|
FROM hardware.components c JOIN hardware.devices d ON d.id = c.device_id
|
||||||
|
ORDER BY d.name, array_position(ARRAY['case','cpu','motherboard','ram','cpu_cooler','gpu','psu','storage','fan','lighting','monitor','accessory'], c.category),
|
||||||
|
c.order_date, c.model;
|
||||||
|
COMMENT ON VIEW hardware.setup_overview IS 'Flat view of every component per device, build order, live under_warranty flag. Filter status = ''in_use'' for the current setup.';
|
||||||
|
|
||||||
|
-- ---------- CS prices (prod tables are empty; copy dev) ----------
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM cs.items) OR EXISTS (SELECT 1 FROM cs.price_observations)
|
||||||
|
OR EXISTS (SELECT 1 FROM cs.item_dimensions) OR EXISTS (SELECT 1 FROM cs.price_streams) THEN
|
||||||
|
RAISE EXCEPTION 'cs.* not empty in target; refusing to merge dev data'; END IF;
|
||||||
|
END $$;
|
||||||
|
INSERT INTO cs.items (id, market_hash_name, name, icon_url, item_type, first_seen_at, last_seen_at)
|
||||||
|
SELECT id, market_hash_name, name, icon_url, item_type, first_seen_at, last_seen_at FROM import_cs.cs_market_items;
|
||||||
|
INSERT INTO cs.item_dimensions (item_id, key, value) SELECT item_id, key, value FROM import_cs.cs_item_dimensions;
|
||||||
|
INSERT INTO cs.price_streams (source, currency, kind) SELECT source, currency, kind FROM import_cs.cs_price_streams;
|
||||||
|
INSERT INTO cs.price_observations (id, item_id, source, currency, observed_at, price_cents, volume, kind, imported_at) OVERRIDING SYSTEM VALUE
|
||||||
|
SELECT id, item_id, source, currency, observed_at, price_cents, volume, kind, imported_at FROM import_cs.cs_price_observations;
|
||||||
|
SELECT setval(pg_get_serial_sequence('cs.items', 'id'), coalesce((SELECT max(id) FROM cs.items), 1));
|
||||||
|
SELECT setval(pg_get_serial_sequence('cs.price_observations', 'id'), coalesce((SELECT max(id) FROM cs.price_observations), 1));
|
||||||
|
|
||||||
|
CREATE TABLE legacy.reorg_imports (dst text PRIMARY KEY, src text NOT NULL, n bigint NOT NULL);
|
||||||
|
-- ---------- verify imports against the staged dev copies, then drop staging ----------
|
||||||
|
RESET ROLE;
|
||||||
|
DO $$
|
||||||
|
DECLARE r record; a bigint; b bigint;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT * FROM (VALUES
|
||||||
|
('energy.gas_storage_daily','import_energy.gas_storage_daily'), ('energy.ned_utilization','import_energy.ned_utilization'),
|
||||||
|
('energy.collector_runs','import_energy.collector_runs'), ('hardware.devices','import_hardware.devices'),
|
||||||
|
('hardware.components','import_hardware.components'), ('cs.items','import_cs.cs_market_items'),
|
||||||
|
('cs.item_dimensions','import_cs.cs_item_dimensions'), ('cs.price_streams','import_cs.cs_price_streams'),
|
||||||
|
('cs.price_observations','import_cs.cs_price_observations')) v(dst, srct) LOOP
|
||||||
|
EXECUTE 'SELECT count(*) FROM ' || r.dst INTO a;
|
||||||
|
EXECUTE 'SELECT count(*) FROM ' || r.srct INTO b;
|
||||||
|
IF a <> b THEN RAISE EXCEPTION '% has % rows, dev % has %', r.dst, a, r.srct, b; END IF;
|
||||||
|
INSERT INTO legacy.reorg_imports VALUES (r.dst, r.srct, a);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
DROP SCHEMA import_energy CASCADE;
|
||||||
|
DROP SCHEMA import_hardware CASCADE;
|
||||||
|
DROP SCHEMA import_cs CASCADE;
|
||||||
79
db/reorg/07_triggers_meta_grants.sql
Normal file
79
db/reorg/07_triggers_meta_grants.sql
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
-- 07_triggers_meta_grants.sql: updated_at triggers everywhere, register core tables in meta, grants.
|
||||||
|
SET ROLE personal;
|
||||||
|
|
||||||
|
-- 1) updated_at trigger on every table that has the column but no trigger (27 pre-existing + new ones)
|
||||||
|
DO $$
|
||||||
|
DECLARE r record; n integer := 0;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT n.nspname AS s, c.relname AS t
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = 'updated_at' AND NOT a.attisdropped
|
||||||
|
WHERE c.relkind = 'r'
|
||||||
|
AND n.nspname IN ('auth','money','house','ref','games','cs','animaldex','site','hardware','energy')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM pg_trigger tg WHERE tg.tgrelid = c.oid AND NOT tg.tgisinternal
|
||||||
|
AND tg.tgfoid = 'meta.touch_updated_at()'::regprocedure)
|
||||||
|
LOOP
|
||||||
|
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', r.s, r.t);
|
||||||
|
n := n + 1;
|
||||||
|
END LOOP;
|
||||||
|
RAISE NOTICE 'added updated_at triggers: %', n;
|
||||||
|
END $$;
|
||||||
|
-- the 16 old triggers were all named site_touch_updated_at; normalise the name
|
||||||
|
DO $$
|
||||||
|
DECLARE r record;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT tg.tgname, tg.tgrelid::regclass AS rel FROM pg_trigger tg WHERE tg.tgname = 'site_touch_updated_at' LOOP
|
||||||
|
EXECUTE format('ALTER TRIGGER site_touch_updated_at ON %s RENAME TO touch_updated_at', r.rel);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
RESET ROLE;
|
||||||
|
|
||||||
|
-- 2) register core + ref tables in meta (GUI visibility); auth + bookkeeping are not exposed
|
||||||
|
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order)
|
||||||
|
SELECT n.nspname, c.relname,
|
||||||
|
replace(n.nspname || '-' || c.relname, '_', '-'),
|
||||||
|
initcap(replace(c.relname, '_', ' ')),
|
||||||
|
CASE WHEN n.nspname = 'ref' THEN 'ref' ELSE 'core' END,
|
||||||
|
n.nspname <> 'ref', false, 1000
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind IN ('r','v')
|
||||||
|
AND n.nspname IN ('money','house','ref','games','cs','animaldex','site','hardware','energy')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
UPDATE meta.tables SET editable = false WHERE kind = 'core' AND (schema_name, table_name) IN
|
||||||
|
(SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'v');
|
||||||
|
SELECT meta.sync();
|
||||||
|
|
||||||
|
-- 3) grants
|
||||||
|
GRANT USAGE ON SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy, lists TO personal_app, personal_ro;
|
||||||
|
-- app: full DML on its own data, read-only on ref
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy, lists TO personal_app;
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA ref, meta TO personal_app;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy, lists TO personal_app;
|
||||||
|
-- app may edit presentation fields of the registry directly
|
||||||
|
GRANT UPDATE (slug, display_name, description, icon, picture_url, default_sort, sort_order) ON meta.tables TO personal_app; -- slug: URL only, table name stays
|
||||||
|
GRANT UPDATE (label, ui_type, options, position, hidden, help_text) ON meta.columns TO personal_app;
|
||||||
|
GRANT EXECUTE ON FUNCTION meta.create_list(text, text, text, boolean, jsonb, text, text, text, integer),
|
||||||
|
meta.add_column(text, jsonb), meta.rename_column(text, text, text), meta.drop_column(text, text),
|
||||||
|
meta.archive_list(text), meta.drop_list(text), meta.sync(integer) TO personal_app;
|
||||||
|
-- ro: everything readable
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy, lists TO personal_ro;
|
||||||
|
-- future objects
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA ref, meta GRANT SELECT ON TABLES TO personal_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy
|
||||||
|
GRANT SELECT ON TABLES TO personal_ro;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON TABLES TO personal_ro;
|
||||||
|
-- migration owner can read the runtime lists, so deploy backups (pg_dump as `personal`) cover everything
|
||||||
|
GRANT USAGE ON SCHEMA lists TO personal;
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA lists TO personal;
|
||||||
|
GRANT SELECT ON ALL SEQUENCES IN SCHEMA lists TO personal;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON TABLES TO personal;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON SEQUENCES TO personal;
|
||||||
|
-- nobody creates in public anymore
|
||||||
|
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||||||
68
db/reorg/08_verify.sql
Normal file
68
db/reorg/08_verify.sql
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
-- 08_verify.sql: hard assertions; any failure aborts the whole transaction.
|
||||||
|
DO $$
|
||||||
|
DECLARE r record; a bigint; bad text := '';
|
||||||
|
BEGIN
|
||||||
|
-- every moved table keeps its exact row count
|
||||||
|
-- expected = prod count before + rows imported from dev (cs.* only; prod cs was empty)
|
||||||
|
FOR r IN SELECT m.*, b.n + coalesce(i.n, 0) AS n FROM legacy.reorg_table_map m
|
||||||
|
JOIN legacy.reorg_counts_before b ON b.table_name = m.old_name
|
||||||
|
LEFT JOIN legacy.reorg_imports i ON i.dst = m.new_schema || '.' || m.new_name LOOP
|
||||||
|
EXECUTE format('SELECT count(*) FROM %I.%I', r.new_schema, r.new_name) INTO a;
|
||||||
|
IF a <> r.n THEN bad := bad || format('%s.%s %s<>%s; ', r.new_schema, r.new_name, a, r.n); END IF;
|
||||||
|
END LOOP;
|
||||||
|
IF bad <> '' THEN RAISE EXCEPTION 'row count mismatch: %', bad; END IF;
|
||||||
|
IF (SELECT count(*) FROM legacy.reorg_counts_before) <> 58 THEN RAISE EXCEPTION 'expected 58 source tables'; END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public') THEN RAISE EXCEPTION 'public not empty'; END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- lists: every legacy item is in exactly one lists.* table, except the 4 merged duplicates; done count preserved
|
||||||
|
DO $$
|
||||||
|
DECLARE t record; n bigint; total bigint := 0; done bigint := 0; d bigint;
|
||||||
|
dropped bigint := (SELECT count(*) FROM legacy.reorg_list_fixes WHERE action = 'dedupe-drop');
|
||||||
|
BEGIN
|
||||||
|
FOR t IN SELECT * FROM meta.tables WHERE kind = 'list' LOOP
|
||||||
|
EXECUTE format('SELECT count(*) FROM lists.%I', t.table_name) INTO n;
|
||||||
|
total := total + n;
|
||||||
|
IF t.has_done THEN EXECUTE format('SELECT count(*) FROM lists.%I WHERE done_at IS NOT NULL', t.table_name) INTO d; done := done + d; END IF;
|
||||||
|
END LOOP;
|
||||||
|
IF total + dropped <> (SELECT count(*) FROM legacy.list_items) THEN
|
||||||
|
RAISE EXCEPTION 'lists total % + % dropped <> legacy %', total, dropped, (SELECT count(*) FROM legacy.list_items); END IF;
|
||||||
|
-- expected done = legacy completions minus dedupe pairs where BOTH copies were done (they collapse to one)
|
||||||
|
d := (SELECT count(*) FROM legacy.list_item_completions)
|
||||||
|
- (SELECT count(*) FROM dedupe x
|
||||||
|
WHERE EXISTS (SELECT 1 FROM legacy.list_item_completions c WHERE c.item_id = x.keep_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM legacy.list_item_completions c WHERE c.item_id = x.drop_id));
|
||||||
|
IF done <> d THEN RAISE EXCEPTION 'done count % <> expected %', done, d; END IF;
|
||||||
|
IF (SELECT count(*) FROM meta.tables WHERE kind = 'list') <> (SELECT count(*) FROM legacy.lists) THEN
|
||||||
|
RAISE EXCEPTION 'list count mismatch'; END IF;
|
||||||
|
RAISE NOTICE 'lists OK: % rows (+% merged dupes), % done', total, dropped, done;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- every table with updated_at has the trigger
|
||||||
|
DO $$
|
||||||
|
DECLARE missing text;
|
||||||
|
BEGIN
|
||||||
|
SELECT string_agg(n.nspname || '.' || c.relname, ', ') INTO missing
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = 'updated_at' AND NOT a.attisdropped
|
||||||
|
WHERE c.relkind = 'r' AND n.nspname NOT IN ('legacy','pg_catalog','information_schema') AND n.nspname !~ '^pg_'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM pg_trigger tg WHERE tg.tgrelid = c.oid AND tg.tgfoid = 'meta.touch_updated_at()'::regprocedure);
|
||||||
|
IF missing IS NOT NULL THEN RAISE EXCEPTION 'missing updated_at trigger: %', missing; END IF;
|
||||||
|
-- every table has a PK
|
||||||
|
SELECT string_agg(n.nspname || '.' || c.relname, ', ') INTO missing
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE c.relkind = 'r' AND n.nspname NOT IN ('legacy','pg_catalog','information_schema') AND n.nspname !~ '^pg_'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM pg_constraint k WHERE k.conrelid = c.oid AND k.contype = 'p');
|
||||||
|
IF missing IS NOT NULL THEN RAISE EXCEPTION 'tables without PK: %', missing; END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- list rows keep their original timestamps
|
||||||
|
DO $$
|
||||||
|
DECLARE t record; n bigint; bad bigint := 0;
|
||||||
|
BEGIN
|
||||||
|
FOR t IN SELECT table_name FROM meta.tables WHERE kind = 'list' LOOP
|
||||||
|
EXECUTE format('SELECT count(*) FROM lists.%I x JOIN legacy.list_items i ON i.id = x.id WHERE x.updated_at <> i.updated_at OR x.created_at <> i.created_at', t.table_name) INTO n;
|
||||||
|
bad := bad + n;
|
||||||
|
END LOOP;
|
||||||
|
IF bad > 0 THEN RAISE EXCEPTION '% list rows lost their original timestamps', bad; END IF;
|
||||||
|
END $$;
|
||||||
58
db/reorg/09_drizzle_names.sql
Normal file
58
db/reorg/09_drizzle_names.sql
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
-- 09_drizzle_names.sql: rename constraints/indexes to the names Drizzle generates, so future
|
||||||
|
-- drizzle-kit migrations (which address objects by name) match the database. Pure renames, no data change.
|
||||||
|
-- Generated from a diff of the rehearsed DB against the Drizzle baseline (2026-10-23).
|
||||||
|
SET ROLE personal;
|
||||||
|
ALTER TABLE cs.item_dimensions RENAME CONSTRAINT item_dimensions_item_id_cs_market_items_id_fkey TO item_dimensions_item_id_items_id_fkey;
|
||||||
|
ALTER TABLE cs.price_observations RENAME CONSTRAINT price_observations_item_id_cs_market_items_id_fkey TO price_observations_item_id_items_id_fkey;
|
||||||
|
ALTER TABLE games.catch_em_all_entries RENAME CONSTRAINT catch_em_all_entries_pokemon_game_entry_id_pokemon_game_entry_i TO "catch_em_all_entries_HeABXvGNiB6D_fkey";
|
||||||
|
ALTER TABLE games.catch_em_all_games RENAME CONSTRAINT catch_em_all_games_game_id_game_id_fkey TO catch_em_all_games_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE games.catch_em_all_settings RENAME CONSTRAINT catch_em_all_settings_user_id_fkey TO catch_em_all_settings_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.osu_entries RENAME CONSTRAINT osu_entries_user_id_fkey TO osu_entries_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_catches RENAME CONSTRAINT pokemon_catches_game_id_fkey TO pokemon_catches_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_catches RENAME CONSTRAINT pokemon_catches_pokemon_id_fkey TO pokemon_catches_pokemon_id_pokemon_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_catches RENAME CONSTRAINT pokemon_catches_user_id_fkey TO pokemon_catches_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_catches RENAME CONSTRAINT pokemon_catches_user_id_game_id_party_slot_key TO pokemon_catches_user_id_game_id_party_slot_unique;
|
||||||
|
ALTER TABLE games.pokemon_journeys RENAME CONSTRAINT pokemon_journeys_game_id_fkey TO pokemon_journeys_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_journeys RENAME CONSTRAINT pokemon_journeys_user_id_fkey TO pokemon_journeys_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_journeys RENAME CONSTRAINT pokemon_journeys_user_id_game_id_key TO pokemon_journeys_user_id_game_id_unique;
|
||||||
|
ALTER TABLE games.pokemon_milestones RENAME CONSTRAINT pokemon_milestones_game_id_fkey TO pokemon_milestones_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_milestones RENAME CONSTRAINT pokemon_milestones_user_id_fkey TO pokemon_milestones_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_milestones RENAME CONSTRAINT pokemon_milestones_user_id_game_id_key_key TO pokemon_milestones_user_id_game_id_key_unique;
|
||||||
|
ALTER TABLE games.pokemon_seen RENAME CONSTRAINT pokemon_seen_game_id_fkey TO pokemon_seen_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_seen RENAME CONSTRAINT pokemon_seen_pokemon_id_fkey TO pokemon_seen_pokemon_id_pokemon_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_seen RENAME CONSTRAINT pokemon_seen_user_id_fkey TO pokemon_seen_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.pokemon_seen RENAME CONSTRAINT pokemon_seen_user_id_game_id_pokemon_id_key TO pokemon_seen_user_id_game_id_pokemon_id_unique;
|
||||||
|
ALTER TABLE games.tracker_challenge_progress RENAME CONSTRAINT tracker_challenge_progress_challenge_id_fkey TO "tracker_challenge_progress_HtzyFtOyUAhk_fkey";
|
||||||
|
ALTER TABLE games.tracker_challenge_progress RENAME CONSTRAINT tracker_challenge_progress_user_id_challenge_id_key TO tracker_challenge_progress_user_id_challenge_id_unique;
|
||||||
|
ALTER TABLE games.tracker_challenge_progress RENAME CONSTRAINT tracker_challenge_progress_user_id_fkey TO tracker_challenge_progress_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.tracker_ranks RENAME CONSTRAINT tracker_ranks_game_id_fkey TO tracker_ranks_game_id_tracker_games_id_fkey;
|
||||||
|
ALTER TABLE games.tracker_ranks RENAME CONSTRAINT tracker_ranks_user_id_fkey TO tracker_ranks_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.tracker_ranks RENAME CONSTRAINT tracker_ranks_user_id_game_id_key TO tracker_ranks_user_id_game_id_unique;
|
||||||
|
ALTER TABLE games.tracker_weapon_ranks RENAME CONSTRAINT tracker_weapon_ranks_item_id_fkey TO tracker_weapon_ranks_item_id_tracker_items_id_fkey;
|
||||||
|
ALTER TABLE games.tracker_weapon_ranks RENAME CONSTRAINT tracker_weapon_ranks_user_id_fkey TO tracker_weapon_ranks_user_id_user_id_fkey;
|
||||||
|
ALTER TABLE games.tracker_weapon_ranks RENAME CONSTRAINT tracker_weapon_ranks_user_id_item_id_key TO tracker_weapon_ranks_user_id_item_id_unique;
|
||||||
|
ALTER TABLE house.price_history RENAME CONSTRAINT price_history_listing_id_fkey TO price_history_listing_id_listings_id_fkey;
|
||||||
|
ALTER TABLE meta.columns RENAME CONSTRAINT columns_table_id_fkey TO columns_table_id_tables_id_fkey;
|
||||||
|
ALTER TABLE money.forecasts RENAME CONSTRAINT forecasts_account_id_money_accounts_id_fkey TO forecasts_account_id_accounts_id_fkey;
|
||||||
|
ALTER TABLE money.projection_settings RENAME CONSTRAINT "projection_settings_fqvzThdinM8E_fkey" TO projection_settings_investment_account_id_accounts_id_fkey;
|
||||||
|
ALTER TABLE money.recurring RENAME CONSTRAINT recurring_account_id_money_accounts_id_fkey TO recurring_account_id_accounts_id_fkey;
|
||||||
|
ALTER TABLE money.transactions RENAME CONSTRAINT transactions_account_id_money_accounts_id_fkey TO transactions_account_id_accounts_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_dex_numbers RENAME CONSTRAINT pokemon_dex_numbers_game_id_fkey TO pokemon_dex_numbers_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_dex_numbers RENAME CONSTRAINT pokemon_dex_numbers_pokemon_id_fkey TO pokemon_dex_numbers_pokemon_id_pokemon_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_dex_numbers RENAME CONSTRAINT pokemon_dex_numbers_pokemon_id_game_id_dex_key TO pokemon_dex_numbers_pokemon_id_game_id_dex_unique;
|
||||||
|
ALTER TABLE ref.pokemon_encounters RENAME CONSTRAINT pokemon_encounters_game_id_games_id_fkey TO pokemon_encounters_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_evolutions RENAME CONSTRAINT pokemon_evolutions_game_id_games_id_fkey TO pokemon_evolutions_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_game_entries RENAME CONSTRAINT pokemon_game_entries_game_id_game_id_fkey TO pokemon_game_entries_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_game_research RENAME CONSTRAINT pokemon_game_research_game_id_fkey TO pokemon_game_research_game_id_pokemon_games_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_learnsets RENAME CONSTRAINT pokemon_learnsets_pokemon_id_fkey TO pokemon_learnsets_pokemon_id_pokemon_id_fkey;
|
||||||
|
ALTER TABLE ref.pokemon_learnsets RENAME CONSTRAINT pokemon_learnsets_pokemon_id_version_group_key TO pokemon_learnsets_pokemon_id_version_group_unique;
|
||||||
|
ALTER TABLE ref.pokemon_research RENAME CONSTRAINT pokemon_research_pokemon_id_fkey TO pokemon_research_pokemon_id_pokemon_id_fkey;
|
||||||
|
ALTER TABLE ref.tracker_challenges RENAME CONSTRAINT tracker_challenges_game_id_fkey TO tracker_challenges_game_id_tracker_games_id_fkey;
|
||||||
|
ALTER TABLE ref.tracker_challenges RENAME CONSTRAINT tracker_challenges_game_id_key_key TO tracker_challenges_game_id_key_unique;
|
||||||
|
ALTER TABLE site.blog_articles RENAME CONSTRAINT blog_articles_slug_unique TO blog_articles_slug_key;
|
||||||
|
ALTER INDEX games.osu_entries_owner_idx RENAME TO osu_entries_user_id_kind_index;
|
||||||
|
ALTER INDEX games.pokemon_catches_owner_idx RENAME TO pokemon_catches_user_id_game_id_pokemon_id_index;
|
||||||
|
ALTER INDEX games.pokemon_seen_user_idx RENAME TO pokemon_seen_user_id_index;
|
||||||
|
ALTER INDEX money.utility_readings_user_kind_date_idx RENAME TO utility_readings_user_id_kind_date_index;
|
||||||
|
ALTER INDEX ref.pokemon_dex_game_idx RENAME TO pokemon_dex_numbers_game_id_index;
|
||||||
|
RESET ROLE;
|
||||||
21
db/reorg/README.md
Normal file
21
db/reorg/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# One-time schema reorg (2026-10)
|
||||||
|
|
||||||
|
Hand-written, reviewed SQL that moved the old flat `public.*` tables into the
|
||||||
|
domain schemas (`auth`, `money`, `house`, `ref`, `games`, `cs`, `animaldex`,
|
||||||
|
`site`, `meta`, `lists`, `hardware`, `energy`, `legacy`), created the roles
|
||||||
|
(`personal`, `personal_app`, `personal_ddl`, `personal_ro`), the `meta.*`
|
||||||
|
registry/API and the 22 runtime lists. It is the state that the Drizzle baseline
|
||||||
|
in `drizzle/` describes.
|
||||||
|
|
||||||
|
These files are kept for the record and for the cutover only. They were
|
||||||
|
rehearsed on a copy of prod (PG17) and run in one transaction (`run.sh`,
|
||||||
|
which refuses the real database names unless `ALLOW_REAL=yes`). After the
|
||||||
|
cutover, record the baseline with `bun scripts/db-migrate.ts --init`; from then
|
||||||
|
on every schema change is a normal Drizzle migration (`bun run db:generate`).
|
||||||
|
|
||||||
|
Passwords are never in these files; set them with `\password` in psql.
|
||||||
|
|
||||||
|
`09_drizzle_names.sql` runs just before `08_verify.sql` and only renames 48
|
||||||
|
constraints and 5 indexes to Drizzle's generated names, so a catalog diff of the
|
||||||
|
reorged DB against a DB built from the baseline is empty (columns, defaults,
|
||||||
|
constraints, indexes, enums in the nine Drizzle schemas).
|
||||||
24
db/reorg/run.sh
Executable file
24
db/reorg/run.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# run.sh <target-db> : stage the dev import, then apply 01..08 in ONE transaction as superuser.
|
||||||
|
# Rehearsal: runs against the private PG17 cluster in ~/dev/setup/db/rehearsal (port 5441, unix socket only).
|
||||||
|
# Prod: same files, but psql = `sudo docker exec -i <pg-container> psql -U postgres -d personal` (see PLAN.md §4c).
|
||||||
|
set -euo pipefail
|
||||||
|
DB=${1:?target db}
|
||||||
|
case "$DB" in personal|personal_dev|personal_staging) [ "${ALLOW_REAL:-}" = yes ] || { echo "refusing $DB without ALLOW_REAL=yes"; exit 3; };; esac
|
||||||
|
D=~/dev/setup/db
|
||||||
|
export LD_LIBRARY_PATH=$D/tools/root/usr/lib/x86_64-linux-gnu
|
||||||
|
BIN=$D/tools/root/usr/lib/postgresql/17/bin
|
||||||
|
CONN=(-h "${PGSOCK:-$D/rehearsal/sock}" -p "${PGPORT:-5441}" -U postgres)
|
||||||
|
STAGED=${STAGED:-$D/dumps/dev-import-staged.dump}
|
||||||
|
|
||||||
|
# 1) stage dev data as import_* schemas (owned by personal so 06 can read it)
|
||||||
|
$BIN/psql "${CONN[@]}" -d "$DB" -v ON_ERROR_STOP=1 -qc "DROP SCHEMA IF EXISTS import_energy, import_hardware, import_cs CASCADE"
|
||||||
|
$BIN/pg_restore "${CONN[@]}" -d "$DB" --no-owner --role=personal --single-transaction --exit-on-error "$STAGED"
|
||||||
|
|
||||||
|
# 2) the migration, all-or-nothing
|
||||||
|
cd "$D/migration"
|
||||||
|
$BIN/psql "${CONN[@]}" -d "$DB" -v ON_ERROR_STOP=1 --single-transaction \
|
||||||
|
-c "SET lock_timeout = '5s'" -c "SET statement_timeout = '10min'" \
|
||||||
|
-f 01_roles.sql -f 02_schemas_move.sql -f 03_fixes.sql -f 04_meta.sql -f 05_lists.sql \
|
||||||
|
-f 06_hardware_energy_cs.sql -f 07_triggers_meta_grants.sql -f 09_drizzle_names.sql -f 08_verify.sql
|
||||||
|
echo "migration committed on $DB"
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import { defineConfig } from 'drizzle-kit';
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
// Drizzle owns the core schemas only. Deliberately excluded:
|
||||||
|
// - lists: runtime tables created through meta.create_list() (owned by personal_ddl)
|
||||||
|
// - legacy: pre-reorg tables kept read-only for rollback
|
||||||
|
// - energy: owned by ned-collector
|
||||||
|
// - hardware: not modelled yet
|
||||||
|
// Migrations run as the schema owner (`personal`), never as the app role.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: 'postgresql',
|
dialect: 'postgresql',
|
||||||
schema: './src/lib/server/db/schema/index.ts',
|
schema: './src/lib/server/db/schema/index.ts',
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
|
schemaFilter: ['auth', 'money', 'house', 'ref', 'games', 'cs', 'animaldex', 'site', 'meta'],
|
||||||
|
migrations: { schema: 'drizzle', table: '__drizzle_migrations' },
|
||||||
verbose: true,
|
verbose: true,
|
||||||
strict: true,
|
strict: true,
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DB_URL!
|
url: (process.env.DB_MIGRATE_URL || process.env.DB_URL)!
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
857
drizzle/20261023215112_baseline/migration.sql
Normal file
857
drizzle/20261023215112_baseline/migration.sql
Normal file
@@ -0,0 +1,857 @@
|
|||||||
|
CREATE SCHEMA "animaldex";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "auth";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "cs";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "games";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "house";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "meta";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "money";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "ref";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE SCHEMA "site";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TYPE "site"."blog_article_status" AS ENUM('draft', 'published');--> statement-breakpoint
|
||||||
|
CREATE TYPE "ref"."availability" AS ENUM('catchable', 'trade_only', 'transfer_only', 'event_only', 'unknown', 'evolve', 'breed');--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth"."account" (
|
||||||
|
"id" text PRIMARY KEY,
|
||||||
|
"account_id" text NOT NULL,
|
||||||
|
"issuer" text NOT NULL,
|
||||||
|
"provider_id" text NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"access_token" text,
|
||||||
|
"refresh_token" text,
|
||||||
|
"id_token" text,
|
||||||
|
"access_token_expires_at" timestamp with time zone,
|
||||||
|
"refresh_token_expires_at" timestamp with time zone,
|
||||||
|
"scope" text,
|
||||||
|
"password" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth"."session" (
|
||||||
|
"id" text PRIMARY KEY,
|
||||||
|
"expires_at" timestamp with time zone NOT NULL,
|
||||||
|
"token" text NOT NULL UNIQUE,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone NOT NULL,
|
||||||
|
"ip_address" text,
|
||||||
|
"user_agent" text,
|
||||||
|
"user_id" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth"."user" (
|
||||||
|
"id" text PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"role" text DEFAULT 'user' NOT NULL,
|
||||||
|
"email" text NOT NULL UNIQUE,
|
||||||
|
"email_verified" boolean DEFAULT false NOT NULL,
|
||||||
|
"image" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth"."verification" (
|
||||||
|
"id" text PRIMARY KEY,
|
||||||
|
"identifier" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
"expires_at" timestamp with time zone NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "site"."blog_articles" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"slug" text NOT NULL UNIQUE,
|
||||||
|
"content" text DEFAULT '' NOT NULL,
|
||||||
|
"tags" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
"status" "site"."blog_article_status" DEFAULT 'draft'::"site"."blog_article_status" NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "site"."tasks" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"current_points" integer DEFAULT 0 NOT NULL,
|
||||||
|
"total_points" integer DEFAULT 1 NOT NULL,
|
||||||
|
"notes" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "site"."task_topics" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"task_id" integer NOT NULL,
|
||||||
|
"topic_id" integer NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "task_topics_task_id_topic_id_unique" UNIQUE("task_id","topic_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "site"."topics" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"emoji" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."catch_em_all_settings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL UNIQUE,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_games" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"source_id" integer UNIQUE,
|
||||||
|
"slug" text UNIQUE,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"generation" integer DEFAULT 1 NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"breeding" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"number" integer NOT NULL UNIQUE,
|
||||||
|
"generation" integer DEFAULT 1 NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"type1" text NOT NULL,
|
||||||
|
"type2" text,
|
||||||
|
"hp" integer DEFAULT 0 NOT NULL,
|
||||||
|
"attack" integer DEFAULT 0 NOT NULL,
|
||||||
|
"defense" integer DEFAULT 0 NOT NULL,
|
||||||
|
"special_attack" integer DEFAULT 0 NOT NULL,
|
||||||
|
"special_defense" integer DEFAULT 0 NOT NULL,
|
||||||
|
"speed" integer DEFAULT 0 NOT NULL,
|
||||||
|
"evolves_from" integer,
|
||||||
|
"evolution_chain" integer,
|
||||||
|
"is_baby" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_legendary" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_mythical" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_encounters" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"location_id" integer NOT NULL,
|
||||||
|
"area" text,
|
||||||
|
"method" text NOT NULL,
|
||||||
|
"min_level" integer,
|
||||||
|
"max_level" integer,
|
||||||
|
"rarity" integer,
|
||||||
|
"form" text,
|
||||||
|
"conditions" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
"note" text,
|
||||||
|
"source" text DEFAULT 'pokeapi' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_evolutions" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"from_pokemon_id" integer NOT NULL,
|
||||||
|
"to_pokemon_id" integer NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"method" text NOT NULL,
|
||||||
|
"source" text DEFAULT 'pokeapi' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_evolutions_from_pokemon_id_to_pokemon_id_game_id_method_unique" UNIQUE("from_pokemon_id","to_pokemon_id","game_id","method")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_game_entries" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"availability" "ref"."availability" DEFAULT 'catchable'::"ref"."availability" NOT NULL,
|
||||||
|
"note" text,
|
||||||
|
"source" text DEFAULT 'pokeapi' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_game_entries_pokemon_id_game_id_unique" UNIQUE("pokemon_id","game_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_locations" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"source_id" integer UNIQUE,
|
||||||
|
"slug" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"region" text,
|
||||||
|
"description" text,
|
||||||
|
"image_key" text,
|
||||||
|
"image_credit" text,
|
||||||
|
"source" text DEFAULT 'pokeapi' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."catch_em_all_entries" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"pokemon_game_entry_id" integer NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "catch_em_all_entries_pokemon_game_entry_id_user_id_unique" UNIQUE("pokemon_game_entry_id","user_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."catch_em_all_games" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "catch_em_all_games_user_id_game_id_unique" UNIQUE("user_id","game_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."tracker_games" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"slug" text NOT NULL UNIQUE,
|
||||||
|
"franchise" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"short_name" text NOT NULL,
|
||||||
|
"release_year" integer,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"rules" jsonb NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."tracker_items" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"icon" text,
|
||||||
|
"post_launch" boolean DEFAULT false NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tracker_items_game_id_key_unique" UNIQUE("game_id","key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."tracker_progress" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"item_id" integer NOT NULL,
|
||||||
|
"milestone" text DEFAULT 'done' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tracker_progress_user_id_item_id_milestone_unique" UNIQUE("user_id","item_id","milestone")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."osu_entries" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"mode" text DEFAULT 'osu' NOT NULL,
|
||||||
|
"beatmap_url" text,
|
||||||
|
"score_url" text,
|
||||||
|
"goal" text DEFAULT 'pass' NOT NULL,
|
||||||
|
"skill" text DEFAULT 'stamina' NOT NULL,
|
||||||
|
"bpm" integer,
|
||||||
|
"duration" integer,
|
||||||
|
"accuracy" numeric,
|
||||||
|
"combo" integer,
|
||||||
|
"misses" integer,
|
||||||
|
"pp" numeric,
|
||||||
|
"mods" text DEFAULT 'NM' NOT NULL,
|
||||||
|
"rate" numeric(4,2) DEFAULT 1.00 NOT NULL,
|
||||||
|
"achieved" boolean DEFAULT false NOT NULL,
|
||||||
|
"played_on" date NOT NULL,
|
||||||
|
"notes" text DEFAULT '' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "osu_entries_kind_check" CHECK ("kind" IN ('score','goal','training'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."pokemon_catches" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"nickname" text DEFAULT '' NOT NULL,
|
||||||
|
"level" integer DEFAULT 1 NOT NULL,
|
||||||
|
"moves" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
"party_slot" integer,
|
||||||
|
"shiny" boolean DEFAULT false NOT NULL,
|
||||||
|
"nature" text,
|
||||||
|
"ability" text,
|
||||||
|
"ball" text,
|
||||||
|
"location" text,
|
||||||
|
"obtained" text DEFAULT 'caught' NOT NULL,
|
||||||
|
"stage" text DEFAULT 'story' NOT NULL,
|
||||||
|
"notes" text DEFAULT '' NOT NULL,
|
||||||
|
"caught_on" date,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_catches_user_id_game_id_party_slot_unique" UNIQUE("user_id","game_id","party_slot"),
|
||||||
|
CONSTRAINT "pokemon_catches_level_check" CHECK ("level" between 1 and 100),
|
||||||
|
CONSTRAINT "pokemon_catches_party_slot_check" CHECK ("party_slot" between 1 and 6),
|
||||||
|
CONSTRAINT "pokemon_catches_moves_check" CHECK (cardinality("moves") <= 4),
|
||||||
|
CONSTRAINT "pokemon_catches_stage_check" CHECK ("stage" IN ('story','postgame','collection'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_dex_numbers" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"dex" text NOT NULL,
|
||||||
|
"dex_name" text NOT NULL,
|
||||||
|
"number" integer NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_dex_numbers_pokemon_id_game_id_dex_unique" UNIQUE("pokemon_id","game_id","dex")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_game_research" (
|
||||||
|
"game_id" integer PRIMARY KEY,
|
||||||
|
"version_group" integer NOT NULL,
|
||||||
|
"regions" text[] NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."pokemon_journeys" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"stage" text DEFAULT 'story' NOT NULL,
|
||||||
|
"location" text,
|
||||||
|
"completed" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_journeys_user_id_game_id_unique" UNIQUE("user_id","game_id"),
|
||||||
|
CONSTRAINT "pokemon_journeys_stage_check" CHECK ("stage" IN ('story','postgame','collection'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_learnsets" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"version_group" integer NOT NULL,
|
||||||
|
"moves" jsonb NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_learnsets_pokemon_id_version_group_unique" UNIQUE("pokemon_id","version_group")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."pokemon_milestones" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"stage" text DEFAULT 'story' NOT NULL,
|
||||||
|
"done" boolean DEFAULT false NOT NULL,
|
||||||
|
"notes" text DEFAULT '' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_milestones_user_id_game_id_key_unique" UNIQUE("user_id","game_id","key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_moves" (
|
||||||
|
"id" integer PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"type" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"power" integer,
|
||||||
|
"accuracy" integer,
|
||||||
|
"pp" integer,
|
||||||
|
"priority" integer NOT NULL,
|
||||||
|
"target" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."pokemon_research" (
|
||||||
|
"pokemon_id" integer PRIMARY KEY,
|
||||||
|
"facts" jsonb NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."pokemon_seen" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"pokemon_id" integer NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "pokemon_seen_user_id_game_id_pokemon_id_unique" UNIQUE("user_id","game_id","pokemon_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."tracker_challenge_progress" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"challenge_id" integer NOT NULL,
|
||||||
|
"current" integer DEFAULT 0 NOT NULL,
|
||||||
|
"done" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tracker_challenge_progress_user_id_challenge_id_unique" UNIQUE("user_id","challenge_id"),
|
||||||
|
CONSTRAINT "tracker_challenge_progress_current_check" CHECK ("current" >= 0)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "ref"."tracker_challenges" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"requirement" text NOT NULL,
|
||||||
|
"reward" text,
|
||||||
|
"icon" text,
|
||||||
|
"target" integer,
|
||||||
|
"source_url" text NOT NULL,
|
||||||
|
CONSTRAINT "tracker_challenges_game_id_key_unique" UNIQUE("game_id","key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."tracker_ranks" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"game_id" integer NOT NULL,
|
||||||
|
"level" integer DEFAULT 1 NOT NULL,
|
||||||
|
"prestige" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tracker_ranks_user_id_game_id_unique" UNIQUE("user_id","game_id"),
|
||||||
|
CONSTRAINT "tracker_ranks_level_check" CHECK ("level" between 1 and 10000),
|
||||||
|
CONSTRAINT "tracker_ranks_prestige_check" CHECK ("prestige" between 0 and 1000)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "games"."tracker_weapon_ranks" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"item_id" integer NOT NULL,
|
||||||
|
"level" integer DEFAULT 1 NOT NULL,
|
||||||
|
"prestige" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tracker_weapon_ranks_user_id_item_id_unique" UNIQUE("user_id","item_id"),
|
||||||
|
CONSTRAINT "tracker_weapon_ranks_level_check" CHECK ("level" between 1 and 10000),
|
||||||
|
CONSTRAINT "tracker_weapon_ranks_prestige_check" CHECK ("prestige" between 0 and 1000)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "animaldex"."animals" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"species" text NOT NULL,
|
||||||
|
"breed" text,
|
||||||
|
"animal_name" text,
|
||||||
|
"description" text,
|
||||||
|
"ai_breed_suggestion" text,
|
||||||
|
"ai_breed_confidence" double precision,
|
||||||
|
"submitted_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"accepted_at" timestamp with time zone,
|
||||||
|
"denied_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "animaldex"."sighting_photos" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"sighting_id" integer NOT NULL,
|
||||||
|
"r2_key" text NOT NULL,
|
||||||
|
"url" text NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"uploaded_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "animaldex"."sightings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"animal_id" integer NOT NULL,
|
||||||
|
"reporter_name" text,
|
||||||
|
"seen_at" timestamp with time zone NOT NULL,
|
||||||
|
"lat" double precision NOT NULL,
|
||||||
|
"lng" double precision NOT NULL,
|
||||||
|
"submitted_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"accepted_at" timestamp with time zone,
|
||||||
|
"denied_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "meta"."columns" (
|
||||||
|
"id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "meta"."columns_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"table_id" integer NOT NULL,
|
||||||
|
"column_name" text NOT NULL,
|
||||||
|
"label" text,
|
||||||
|
"ui_type" text NOT NULL,
|
||||||
|
"options" jsonb DEFAULT '{}' NOT NULL,
|
||||||
|
"position" integer DEFAULT 0 NOT NULL,
|
||||||
|
"hidden" boolean DEFAULT false NOT NULL,
|
||||||
|
"help_text" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "columns_table_id_column_name_key" UNIQUE("table_id","column_name"),
|
||||||
|
CONSTRAINT "columns_ui_type_check" CHECK (ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "meta"."tables" (
|
||||||
|
"id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "meta"."tables_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"schema_name" text NOT NULL,
|
||||||
|
"table_name" text NOT NULL,
|
||||||
|
"slug" text NOT NULL CONSTRAINT "tables_slug_key" UNIQUE,
|
||||||
|
"display_name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"icon" text,
|
||||||
|
"picture_url" text,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"editable" boolean DEFAULT true NOT NULL,
|
||||||
|
"has_done" boolean DEFAULT false NOT NULL,
|
||||||
|
"default_sort" text,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"archived_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "tables_schema_name_table_name_key" UNIQUE("schema_name","table_name"),
|
||||||
|
CONSTRAINT "tables_slug_check" CHECK (slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'),
|
||||||
|
CONSTRAINT "tables_kind_check" CHECK (kind IN ('list','core','ref'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."accounts" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"opening_balance_cents" integer DEFAULT 0 NOT NULL,
|
||||||
|
"kind" text DEFAULT 'spendable' NOT NULL,
|
||||||
|
"color" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "accounts_user_id_name_unique" UNIQUE("user_id","name"),
|
||||||
|
CONSTRAINT "accounts_kind_check" CHECK (kind IN ('spendable','investment'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."forecasts" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"date" date NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"amount_cents" integer NOT NULL,
|
||||||
|
"account_id" integer,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."insurances" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"provider" text,
|
||||||
|
"coverage" text,
|
||||||
|
"premium_cents" integer,
|
||||||
|
"cadence" text DEFAULT 'monthly' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "insurances_cadence_check" CHECK (cadence IN ('weekly','monthly','yearly'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."projection_settings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL UNIQUE,
|
||||||
|
"investment_account_id" integer,
|
||||||
|
"reserve_cents" integer DEFAULT 100000 NOT NULL,
|
||||||
|
"annual_return_percent" numeric(5,2) DEFAULT '5.00' NOT NULL,
|
||||||
|
"contribution_mode" text DEFAULT 'excess' NOT NULL,
|
||||||
|
"include_recurring" boolean DEFAULT true NOT NULL,
|
||||||
|
"fixed_monthly_cents" integer DEFAULT 0 NOT NULL,
|
||||||
|
"end_date" date,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "projection_settings_contribution_mode_check" CHECK (contribution_mode IN ('excess','fixed'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."recurring" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"amount_cents" integer NOT NULL,
|
||||||
|
"cadence" text NOT NULL,
|
||||||
|
"starts_on" date,
|
||||||
|
"ends_on" date,
|
||||||
|
"account_id" integer,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "recurring_cadence_check" CHECK (cadence IN ('weekly','monthly','yearly'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."transactions" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"account_id" integer NOT NULL,
|
||||||
|
"date" date NOT NULL,
|
||||||
|
"description" text NOT NULL,
|
||||||
|
"amount_cents" integer NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."utility_readings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"date" date NOT NULL,
|
||||||
|
"reading" numeric(12,3) NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "utility_readings_kind_check" CHECK (kind IN ('electricity','gas','water'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."utility_settings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"rate_per_unit" numeric(12,4) DEFAULT '0' NOT NULL,
|
||||||
|
"monthly_advance_cents" integer DEFAULT 0 NOT NULL,
|
||||||
|
"billing_start" date,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "utility_settings_user_id_kind_unique" UNIQUE("user_id","kind"),
|
||||||
|
CONSTRAINT "utility_settings_kind_check" CHECK (kind IN ('electricity','gas','water'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "money"."wishlist" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"estimated_cents" integer,
|
||||||
|
"priority" integer DEFAULT 3 NOT NULL,
|
||||||
|
"notes" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cs"."item_dimensions" (
|
||||||
|
"item_id" integer,
|
||||||
|
"key" text,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
CONSTRAINT "item_dimensions_pkey" PRIMARY KEY("item_id","key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cs"."items" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"market_hash_name" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"icon_url" text,
|
||||||
|
"item_type" text,
|
||||||
|
"first_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cs"."price_observations" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "cs"."price_observations_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"item_id" integer NOT NULL,
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"currency" text NOT NULL,
|
||||||
|
"observed_at" timestamp with time zone NOT NULL,
|
||||||
|
"price_cents" integer NOT NULL,
|
||||||
|
"volume" integer,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"imported_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cs"."price_streams" (
|
||||||
|
"source" text,
|
||||||
|
"currency" text,
|
||||||
|
"kind" text,
|
||||||
|
CONSTRAINT "price_streams_pkey" PRIMARY KEY("source","currency","kind")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "house"."alert_rules" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"offer" text DEFAULT 'rent' NOT NULL,
|
||||||
|
"min_price" integer,
|
||||||
|
"max_price" integer,
|
||||||
|
"min_area" integer,
|
||||||
|
"min_bedrooms" integer,
|
||||||
|
"cities" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
"property_types" text[] DEFAULT '{}'::text[] NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "alert_rules_offer_check" CHECK (offer IN ('rent', 'buy'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "house"."listings" (
|
||||||
|
"id" serial PRIMARY KEY,
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"source_id" text NOT NULL,
|
||||||
|
"external_id" text,
|
||||||
|
"url" text NOT NULL,
|
||||||
|
"offer" text NOT NULL,
|
||||||
|
"property_type" text NOT NULL,
|
||||||
|
"title" text,
|
||||||
|
"street" text,
|
||||||
|
"postal_code" text,
|
||||||
|
"city" text,
|
||||||
|
"city_slug" text,
|
||||||
|
"neighbourhood" text,
|
||||||
|
"price_eur" integer,
|
||||||
|
"area_m2" integer,
|
||||||
|
"rooms" integer,
|
||||||
|
"bedrooms" integer,
|
||||||
|
"bathrooms" integer,
|
||||||
|
"construction_year" integer,
|
||||||
|
"energy_label" text,
|
||||||
|
"interior" text,
|
||||||
|
"deposit_eur" integer,
|
||||||
|
"available_from" text,
|
||||||
|
"contract_type" text,
|
||||||
|
"agent" text,
|
||||||
|
"image_url" text,
|
||||||
|
"description" text,
|
||||||
|
"features" jsonb DEFAULT '{}' NOT NULL,
|
||||||
|
"lat" double precision,
|
||||||
|
"lng" double precision,
|
||||||
|
"offered_at" timestamp with time zone,
|
||||||
|
"first_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"first_seen_lane" text DEFAULT 'slow' NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"detail_fetched_at" timestamp with time zone,
|
||||||
|
"removed_at" timestamp with time zone,
|
||||||
|
"alerted_at" timestamp with time zone,
|
||||||
|
"status" text,
|
||||||
|
"notes" text,
|
||||||
|
CONSTRAINT "listings_offer_check" CHECK (offer IN ('rent', 'buy')),
|
||||||
|
CONSTRAINT "listings_price_eur_check" CHECK (price_eur IS NULL OR price_eur >= 0)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "house"."price_history" (
|
||||||
|
"listing_id" integer,
|
||||||
|
"observed_at" timestamp with time zone DEFAULT now(),
|
||||||
|
"price_eur" integer NOT NULL,
|
||||||
|
CONSTRAINT "price_history_pkey" PRIMARY KEY("listing_id","observed_at")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "house"."searches" (
|
||||||
|
"search_url" text PRIMARY KEY,
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"lane" text NOT NULL,
|
||||||
|
"first_synced_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"last_synced_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"last_found" integer DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "house"."worker_runs" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "house"."worker_runs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"lane" text NOT NULL,
|
||||||
|
"url" text NOT NULL,
|
||||||
|
"status" text NOT NULL,
|
||||||
|
"http_status" integer,
|
||||||
|
"found" integer,
|
||||||
|
"duration_ms" integer,
|
||||||
|
"message" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "account_userId_idx" ON "auth"."account" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "account_issuer_account_id_unique" ON "auth"."account" ("issuer","account_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "session_userId_idx" ON "auth"."session" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "verification_identifier_idx" ON "auth"."verification" ("identifier");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_encounters_pokemon_id_game_id_index" ON "ref"."pokemon_encounters" ("pokemon_id","game_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_encounters_location_id_index" ON "ref"."pokemon_encounters" ("location_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_evolutions_game_id_index" ON "ref"."pokemon_evolutions" ("game_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_game_entries_game_id_index" ON "ref"."pokemon_game_entries" ("game_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "catch_em_all_entries_user_id_index" ON "games"."catch_em_all_entries" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "catch_em_all_games_user_id_index" ON "games"."catch_em_all_games" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "tracker_items_game_id_index" ON "ref"."tracker_items" ("game_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "tracker_progress_user_id_index" ON "games"."tracker_progress" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "osu_entries_user_id_kind_index" ON "games"."osu_entries" ("user_id","kind");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_catches_user_id_game_id_pokemon_id_index" ON "games"."pokemon_catches" ("user_id","game_id","pokemon_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_dex_numbers_game_id_index" ON "ref"."pokemon_dex_numbers" ("game_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "pokemon_seen_user_id_index" ON "games"."pokemon_seen" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "columns_table_id_index" ON "meta"."columns" ("table_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "accounts_user_id_index" ON "money"."accounts" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "forecasts_user_id_date_index" ON "money"."forecasts" ("user_id","date");--> statement-breakpoint
|
||||||
|
CREATE INDEX "insurances_user_id_index" ON "money"."insurances" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "recurring_user_id_index" ON "money"."recurring" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "transactions_user_id_date_id_index" ON "money"."transactions" ("user_id","date","id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "transactions_account_id_index" ON "money"."transactions" ("account_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "utility_readings_user_id_kind_date_index" ON "money"."utility_readings" ("user_id","kind","date");--> statement-breakpoint
|
||||||
|
CREATE INDEX "wishlist_user_id_index" ON "money"."wishlist" ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "item_dimensions_filter_idx" ON "cs"."item_dimensions" ("key","value","item_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "items_hash_idx" ON "cs"."items" ("market_hash_name");--> statement-breakpoint
|
||||||
|
CREATE INDEX "items_name_idx" ON "cs"."items" ("name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "price_observations_unique_idx" ON "cs"."price_observations" ("item_id","source","currency","observed_at","kind");--> statement-breakpoint
|
||||||
|
CREATE INDEX "price_observations_lookup_idx" ON "cs"."price_observations" ("item_id","source","currency","observed_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "price_observations_time_idx" ON "cs"."price_observations" ("observed_at");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "listings_source_idx" ON "house"."listings" ("source","source_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "listings_active_idx" ON "house"."listings" ("offer","first_seen_at" DESC) WHERE removed_at IS NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX "listings_city_idx" ON "house"."listings" ("source","offer","city_slug");--> statement-breakpoint
|
||||||
|
CREATE INDEX "listings_details_idx" ON "house"."listings" ("first_seen_at" DESC) WHERE detail_fetched_at IS NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX "worker_runs_created_idx" ON "house"."worker_runs" ("created_at" DESC);--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "site"."task_topics" ADD CONSTRAINT "task_topics_task_id_tasks_id_fkey" FOREIGN KEY ("task_id") REFERENCES "site"."tasks"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "site"."task_topics" ADD CONSTRAINT "task_topics_topic_id_topics_id_fkey" FOREIGN KEY ("topic_id") REFERENCES "site"."topics"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."catch_em_all_settings" ADD CONSTRAINT "catch_em_all_settings_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_encounters" ADD CONSTRAINT "pokemon_encounters_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_encounters" ADD CONSTRAINT "pokemon_encounters_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_encounters" ADD CONSTRAINT "pokemon_encounters_location_id_pokemon_locations_id_fkey" FOREIGN KEY ("location_id") REFERENCES "ref"."pokemon_locations"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_evolutions" ADD CONSTRAINT "pokemon_evolutions_from_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("from_pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_evolutions" ADD CONSTRAINT "pokemon_evolutions_to_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("to_pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_evolutions" ADD CONSTRAINT "pokemon_evolutions_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_game_entries" ADD CONSTRAINT "pokemon_game_entries_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_game_entries" ADD CONSTRAINT "pokemon_game_entries_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."catch_em_all_entries" ADD CONSTRAINT "catch_em_all_entries_HeABXvGNiB6D_fkey" FOREIGN KEY ("pokemon_game_entry_id") REFERENCES "ref"."pokemon_game_entries"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."catch_em_all_entries" ADD CONSTRAINT "catch_em_all_entries_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."catch_em_all_games" ADD CONSTRAINT "catch_em_all_games_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."catch_em_all_games" ADD CONSTRAINT "catch_em_all_games_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."tracker_items" ADD CONSTRAINT "tracker_items_game_id_tracker_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."tracker_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_progress" ADD CONSTRAINT "tracker_progress_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_progress" ADD CONSTRAINT "tracker_progress_item_id_tracker_items_id_fkey" FOREIGN KEY ("item_id") REFERENCES "ref"."tracker_items"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."osu_entries" ADD CONSTRAINT "osu_entries_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_catches" ADD CONSTRAINT "pokemon_catches_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_catches" ADD CONSTRAINT "pokemon_catches_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_catches" ADD CONSTRAINT "pokemon_catches_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_dex_numbers" ADD CONSTRAINT "pokemon_dex_numbers_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_dex_numbers" ADD CONSTRAINT "pokemon_dex_numbers_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_game_research" ADD CONSTRAINT "pokemon_game_research_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_journeys" ADD CONSTRAINT "pokemon_journeys_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_journeys" ADD CONSTRAINT "pokemon_journeys_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_learnsets" ADD CONSTRAINT "pokemon_learnsets_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_milestones" ADD CONSTRAINT "pokemon_milestones_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_milestones" ADD CONSTRAINT "pokemon_milestones_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."pokemon_research" ADD CONSTRAINT "pokemon_research_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_seen" ADD CONSTRAINT "pokemon_seen_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_seen" ADD CONSTRAINT "pokemon_seen_game_id_pokemon_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."pokemon_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."pokemon_seen" ADD CONSTRAINT "pokemon_seen_pokemon_id_pokemon_id_fkey" FOREIGN KEY ("pokemon_id") REFERENCES "ref"."pokemon"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_challenge_progress" ADD CONSTRAINT "tracker_challenge_progress_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_challenge_progress" ADD CONSTRAINT "tracker_challenge_progress_HtzyFtOyUAhk_fkey" FOREIGN KEY ("challenge_id") REFERENCES "ref"."tracker_challenges"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "ref"."tracker_challenges" ADD CONSTRAINT "tracker_challenges_game_id_tracker_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."tracker_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_ranks" ADD CONSTRAINT "tracker_ranks_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_ranks" ADD CONSTRAINT "tracker_ranks_game_id_tracker_games_id_fkey" FOREIGN KEY ("game_id") REFERENCES "ref"."tracker_games"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_weapon_ranks" ADD CONSTRAINT "tracker_weapon_ranks_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "games"."tracker_weapon_ranks" ADD CONSTRAINT "tracker_weapon_ranks_item_id_tracker_items_id_fkey" FOREIGN KEY ("item_id") REFERENCES "ref"."tracker_items"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "animaldex"."sighting_photos" ADD CONSTRAINT "sighting_photos_sighting_id_sightings_id_fkey" FOREIGN KEY ("sighting_id") REFERENCES "animaldex"."sightings"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "animaldex"."sightings" ADD CONSTRAINT "sightings_animal_id_animals_id_fkey" FOREIGN KEY ("animal_id") REFERENCES "animaldex"."animals"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "meta"."columns" ADD CONSTRAINT "columns_table_id_tables_id_fkey" FOREIGN KEY ("table_id") REFERENCES "meta"."tables"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."accounts" ADD CONSTRAINT "accounts_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."forecasts" ADD CONSTRAINT "forecasts_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."forecasts" ADD CONSTRAINT "forecasts_account_id_accounts_id_fkey" FOREIGN KEY ("account_id") REFERENCES "money"."accounts"("id") ON DELETE SET NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."insurances" ADD CONSTRAINT "insurances_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."projection_settings" ADD CONSTRAINT "projection_settings_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."projection_settings" ADD CONSTRAINT "projection_settings_investment_account_id_accounts_id_fkey" FOREIGN KEY ("investment_account_id") REFERENCES "money"."accounts"("id") ON DELETE SET NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."recurring" ADD CONSTRAINT "recurring_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."recurring" ADD CONSTRAINT "recurring_account_id_accounts_id_fkey" FOREIGN KEY ("account_id") REFERENCES "money"."accounts"("id") ON DELETE SET NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."transactions" ADD CONSTRAINT "transactions_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."transactions" ADD CONSTRAINT "transactions_account_id_accounts_id_fkey" FOREIGN KEY ("account_id") REFERENCES "money"."accounts"("id");--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."utility_readings" ADD CONSTRAINT "utility_readings_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."utility_settings" ADD CONSTRAINT "utility_settings_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "money"."wishlist" ADD CONSTRAINT "wishlist_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "cs"."item_dimensions" ADD CONSTRAINT "item_dimensions_item_id_items_id_fkey" FOREIGN KEY ("item_id") REFERENCES "cs"."items"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "cs"."price_observations" ADD CONSTRAINT "price_observations_item_id_items_id_fkey" FOREIGN KEY ("item_id") REFERENCES "cs"."items"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "house"."price_history" ADD CONSTRAINT "price_history_listing_id_listings_id_fkey" FOREIGN KEY ("listing_id") REFERENCES "house"."listings"("id") ON DELETE CASCADE;
|
||||||
10307
drizzle/20261023215112_baseline/snapshot.json
Normal file
10307
drizzle/20261023215112_baseline/snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
55
scripts/db-migrate.ts
Normal file
55
scripts/db-migrate.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Applies pending Drizzle migrations from ./drizzle. Runs as the schema owner
|
||||||
|
// (DB_MIGRATE_URL, role `personal`); falls back to DB_URL for local dev.
|
||||||
|
//
|
||||||
|
// bun scripts/db-migrate.ts apply pending migrations
|
||||||
|
// bun scripts/db-migrate.ts --init record the baseline as applied without running it
|
||||||
|
// (once, on a database already reorganised by the
|
||||||
|
// hand-written reorg scripts in db/reorg/)
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
const url = process.env.DB_MIGRATE_URL || process.env.DB_URL;
|
||||||
|
if (!url) throw new Error('Set DB_MIGRATE_URL (schema owner) or DB_URL');
|
||||||
|
const init = process.argv.includes('--init');
|
||||||
|
|
||||||
|
const client = postgres(url, { max: 1, connect_timeout: 10, onnotice: () => {} });
|
||||||
|
try {
|
||||||
|
const db = drizzle({ client });
|
||||||
|
// Safety: never run the baseline on a database that already has data. Existing
|
||||||
|
// databases are brought to the baseline by the reviewed reorg scripts and then
|
||||||
|
// recorded with --init; only a truly empty database gets the baseline executed.
|
||||||
|
const [state] = await client`
|
||||||
|
select
|
||||||
|
to_regclass('drizzle.__drizzle_migrations') is not null as tracked,
|
||||||
|
(select count(*)::int from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
||||||
|
where c.relkind = 'r' and n.nspname not in ('pg_catalog', 'information_schema', 'drizzle')
|
||||||
|
and n.nspname not like 'pg_toast%') as tables`;
|
||||||
|
const recorded = state.tracked
|
||||||
|
? (await client`select count(*)::int as n from drizzle.__drizzle_migrations`)[0].n
|
||||||
|
: 0;
|
||||||
|
if (!init && recorded === 0 && state.tables > 0)
|
||||||
|
throw new Error(
|
||||||
|
`Refusing to run the Drizzle baseline on a non-empty database (${state.tables} tables). ` +
|
||||||
|
'Apply the reorg scripts (db/reorg/) and record the baseline with --init first.'
|
||||||
|
);
|
||||||
|
if (init && state.tables === 0)
|
||||||
|
throw new Error('--init on an empty database: run without --init to create the schema');
|
||||||
|
const result = await migrate(db, {
|
||||||
|
migrationsFolder: new URL('../drizzle', import.meta.url).pathname,
|
||||||
|
migrationsSchema: 'drizzle',
|
||||||
|
migrationsTable: '__drizzle_migrations',
|
||||||
|
...(init ? { init: true } : {})
|
||||||
|
});
|
||||||
|
if (result && 'exitCode' in result) {
|
||||||
|
throw new Error(
|
||||||
|
result.exitCode === 'databaseMigrations'
|
||||||
|
? '--init refused: migrations are already recorded in this database'
|
||||||
|
: '--init refused: more than one local migration exists'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [{ n }] = await client`select count(*)::int as n from drizzle.__drizzle_migrations`;
|
||||||
|
console.log(`${init ? 'Baseline recorded' : 'Migrations up to date'}; ${n} recorded`);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@ export const relations = defineRelations(schema, (r) => ({
|
|||||||
sessions: r.many.session(),
|
sessions: r.many.session(),
|
||||||
accounts: r.many.account(),
|
accounts: r.many.account(),
|
||||||
entries: r.many.userEntries(),
|
entries: r.many.userEntries(),
|
||||||
games: r.many.userGames(),
|
games: r.many.userGames()
|
||||||
listCompletions: r.many.listItemCompletions()
|
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
user: r.one.user({
|
user: r.one.user({
|
||||||
@@ -138,25 +137,14 @@ export const relations = defineRelations(schema, (r) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Lists ─────────────────────────────────────
|
// ── Meta registry (lists live in schema `lists`, see src/lib/server/lists.ts)
|
||||||
lists: {
|
metaTables: {
|
||||||
items: r.many.listItems()
|
columns: r.many.metaColumns()
|
||||||
},
|
},
|
||||||
listItems: {
|
metaColumns: {
|
||||||
list: r.one.lists({
|
table: r.one.metaTables({
|
||||||
from: r.listItems.listId,
|
from: r.metaColumns.tableId,
|
||||||
to: r.lists.id
|
to: r.metaTables.id
|
||||||
}),
|
|
||||||
completions: r.many.listItemCompletions()
|
|
||||||
},
|
|
||||||
listItemCompletions: {
|
|
||||||
item: r.one.listItems({
|
|
||||||
from: r.listItemCompletions.itemId,
|
|
||||||
to: r.listItems.id
|
|
||||||
}),
|
|
||||||
user: r.one.user({
|
|
||||||
from: r.listItemCompletions.userId,
|
|
||||||
to: r.user.id
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { doublePrecision, integer, pgTable, text } from 'drizzle-orm/pg-core';
|
import { doublePrecision, integer, text } from 'drizzle-orm/pg-core';
|
||||||
|
import { animaldexSchema } from './schemas';
|
||||||
import { id, timestamps, timestamptz } from './helpers';
|
import { id, timestamps, timestamptz } from './helpers';
|
||||||
|
|
||||||
export const animals = pgTable('animals', {
|
export const animals = animaldexSchema.table('animals', {
|
||||||
id: id(),
|
id: id(),
|
||||||
|
|
||||||
species: text('species').notNull(),
|
species: text('species').notNull(),
|
||||||
@@ -22,7 +23,7 @@ export const animals = pgTable('animals', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sightings = pgTable('sightings', {
|
export const sightings = animaldexSchema.table('sightings', {
|
||||||
id: id(),
|
id: id(),
|
||||||
animalId: integer('animal_id')
|
animalId: integer('animal_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -40,7 +41,7 @@ export const sightings = pgTable('sightings', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const photos = pgTable('photos', {
|
export const photos = animaldexSchema.table('sighting_photos', {
|
||||||
id: id(),
|
id: id(),
|
||||||
sightingId: integer('sighting_id')
|
sightingId: integer('sighting_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { pgTable, text, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
import { text, timestamp, boolean, index, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
|
import { authSchema } from './schemas';
|
||||||
|
|
||||||
export const user = pgTable('user', {
|
export const user = authSchema.table('user', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
role: text('role').notNull().default('user'),
|
role: text('role').notNull().default('user'),
|
||||||
@@ -14,7 +15,7 @@ export const user = pgTable('user', {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const session = pgTable(
|
export const session = authSchema.table(
|
||||||
'session',
|
'session',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
@@ -33,7 +34,7 @@ export const session = pgTable(
|
|||||||
(table) => [index('session_userId_idx').on(table.userId)]
|
(table) => [index('session_userId_idx').on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const account = pgTable(
|
export const account = authSchema.table(
|
||||||
'account',
|
'account',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
@@ -61,7 +62,7 @@ export const account = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const verification = pgTable(
|
export const verification = authSchema.table(
|
||||||
'verification',
|
'verification',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import { text, unique } from 'drizzle-orm/pg-core';
|
||||||
|
import { siteSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
|
|
||||||
export const blogArticleStatus = pgEnum('blog_article_status', ['draft', 'published']);
|
export const blogArticleStatus = siteSchema.enum('blog_article_status', ['draft', 'published']);
|
||||||
|
|
||||||
export const blogArticles = pgTable(
|
export const blogArticles = siteSchema.table(
|
||||||
'blog_articles',
|
'blog_articles',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { boolean, index, integer, pgEnum, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import { boolean, index, integer, text, unique } from 'drizzle-orm/pg-core';
|
||||||
|
import { gamesSchema, refSchema } from './schemas';
|
||||||
import { availabilityValues } from '../../../availability';
|
import { availabilityValues } from '../../../availability';
|
||||||
import { user } from './auth';
|
import { user } from './auth';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
|
|
||||||
export const availability = pgEnum('availability', availabilityValues);
|
export const availability = refSchema.enum('availability', availabilityValues);
|
||||||
|
|
||||||
// Where a catalog row came from. Seeding replaces `pokeapi` and `derived` rows
|
// Where a catalog row came from. Seeding replaces `pokeapi` and `derived` rows
|
||||||
// but never touches `manual` ones, which are admin corrections.
|
// but never touches `manual` ones, which are admin corrections.
|
||||||
export const catalogSources = ['pokeapi', 'derived', 'manual'] as const;
|
export const catalogSources = ['pokeapi', 'derived', 'manual'] as const;
|
||||||
const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi');
|
const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi');
|
||||||
|
|
||||||
export const pokemon = pgTable('pokemon', {
|
export const pokemon = refSchema.table('pokemon', {
|
||||||
id: id(),
|
id: id(),
|
||||||
number: integer('number').notNull().unique(),
|
number: integer('number').notNull().unique(),
|
||||||
generation: integer('generation').notNull().default(1),
|
generation: integer('generation').notNull().default(1),
|
||||||
@@ -32,7 +33,7 @@ export const pokemon = pgTable('pokemon', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const games = pgTable('games', {
|
export const games = refSchema.table('pokemon_games', {
|
||||||
id: id(),
|
id: id(),
|
||||||
sourceId: integer('source_id').unique(),
|
sourceId: integer('source_id').unique(),
|
||||||
slug: text('slug').unique(),
|
slug: text('slug').unique(),
|
||||||
@@ -43,7 +44,7 @@ export const games = pgTable('games', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pokemonGameEntries = pgTable(
|
export const pokemonGameEntries = refSchema.table(
|
||||||
'pokemon_game_entries',
|
'pokemon_game_entries',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -62,7 +63,7 @@ export const pokemonGameEntries = pgTable(
|
|||||||
(table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)]
|
(table) => [unique().on(table.pokemonId, table.gameId), index().on(table.gameId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonLocations = pgTable('pokemon_locations', {
|
export const pokemonLocations = refSchema.table('pokemon_locations', {
|
||||||
id: id(),
|
id: id(),
|
||||||
sourceId: integer('source_id').unique(),
|
sourceId: integer('source_id').unique(),
|
||||||
slug: text('slug').notNull(),
|
slug: text('slug').notNull(),
|
||||||
@@ -76,7 +77,7 @@ export const pokemonLocations = pgTable('pokemon_locations', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pokemonEncounters = pgTable(
|
export const pokemonEncounters = refSchema.table(
|
||||||
'pokemon_encounters',
|
'pokemon_encounters',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -106,7 +107,7 @@ export const pokemonEncounters = pgTable(
|
|||||||
|
|
||||||
// The method that applies in each game, e.g. Magneton → Magnezone needs Mt.
|
// The method that applies in each game, e.g. Magneton → Magnezone needs Mt.
|
||||||
// Coronet in Platinum but a Thunder Stone in Sword.
|
// Coronet in Platinum but a Thunder Stone in Sword.
|
||||||
export const pokemonEvolutions = pgTable(
|
export const pokemonEvolutions = refSchema.table(
|
||||||
'pokemon_evolutions',
|
'pokemon_evolutions',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -129,8 +130,8 @@ export const pokemonEvolutions = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const userEntries = pgTable(
|
export const userEntries = gamesSchema.table(
|
||||||
'user_entries',
|
'catch_em_all_entries',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
pokemonGameEntryId: integer('pokemon_game_entry_id')
|
pokemonGameEntryId: integer('pokemon_game_entry_id')
|
||||||
@@ -145,8 +146,8 @@ export const userEntries = pgTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// which games a user has chosen to track ("rules")
|
// which games a user has chosen to track ("rules")
|
||||||
export const userGames = pgTable(
|
export const userGames = gamesSchema.table(
|
||||||
'user_games',
|
'catch_em_all_games',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -161,7 +162,7 @@ export const userGames = pgTable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// A row distinguishes an explicit empty game selection from a new account.
|
// A row distinguishes an explicit empty game selection from a new account.
|
||||||
export const catchEmAllSettings = pgTable('catch_em_all_settings', {
|
export const catchEmAllSettings = gamesSchema.table('catch_em_all_settings', {
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { index, integer, pgTable, text, timestamp, uniqueIndex, bigint } from 'drizzle-orm/pg-core';
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
primaryKey,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
bigint
|
||||||
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import { csSchema } from './schemas';
|
||||||
import { id } from './helpers';
|
import { id } from './helpers';
|
||||||
|
|
||||||
// A Steam market hash name identifies a listing, including wear and special variants.
|
// A Steam market hash name identifies a listing, including wear and special variants.
|
||||||
export const csMarketItems = pgTable(
|
export const csMarketItems = csSchema.table(
|
||||||
'cs_market_items',
|
'items',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
marketHashName: text('market_hash_name').notNull(),
|
marketHashName: text('market_hash_name').notNull(),
|
||||||
@@ -14,14 +23,14 @@ export const csMarketItems = pgTable(
|
|||||||
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow()
|
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow()
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex('cs_market_items_hash_idx').on(table.marketHashName),
|
uniqueIndex('items_hash_idx').on(table.marketHashName),
|
||||||
index('cs_market_items_name_idx').on(table.name)
|
index('items_name_idx').on(table.name)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keep source observations intact. Aggregation happens only when the chart is requested.
|
// Keep source observations intact. Aggregation happens only when the chart is requested.
|
||||||
export const csPriceObservations = pgTable(
|
export const csPriceObservations = csSchema.table(
|
||||||
'cs_price_observations',
|
'price_observations',
|
||||||
{
|
{
|
||||||
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
||||||
itemId: integer('item_id')
|
itemId: integer('item_id')
|
||||||
@@ -36,25 +45,25 @@ export const csPriceObservations = pgTable(
|
|||||||
importedAt: timestamp('imported_at', { withTimezone: true }).notNull().defaultNow()
|
importedAt: timestamp('imported_at', { withTimezone: true }).notNull().defaultNow()
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex('cs_price_observations_unique_idx').on(
|
uniqueIndex('price_observations_unique_idx').on(
|
||||||
table.itemId,
|
table.itemId,
|
||||||
table.source,
|
table.source,
|
||||||
table.currency,
|
table.currency,
|
||||||
table.observedAt,
|
table.observedAt,
|
||||||
table.kind
|
table.kind
|
||||||
),
|
),
|
||||||
index('cs_price_observations_lookup_idx').on(
|
index('price_observations_lookup_idx').on(
|
||||||
table.itemId,
|
table.itemId,
|
||||||
table.source,
|
table.source,
|
||||||
table.currency,
|
table.currency,
|
||||||
table.observedAt
|
table.observedAt
|
||||||
),
|
),
|
||||||
index('cs_price_observations_time_idx').on(table.observedAt)
|
index('price_observations_time_idx').on(table.observedAt)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const csItemDimensions = pgTable(
|
export const csItemDimensions = csSchema.table(
|
||||||
'cs_item_dimensions',
|
'item_dimensions',
|
||||||
{
|
{
|
||||||
itemId: integer('item_id')
|
itemId: integer('item_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -63,19 +72,19 @@ export const csItemDimensions = pgTable(
|
|||||||
value: text('value').notNull()
|
value: text('value').notNull()
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex('cs_item_dimensions_unique_idx').on(table.itemId, table.key),
|
primaryKey({ name: 'item_dimensions_pkey', columns: [table.itemId, table.key] }),
|
||||||
index('cs_item_dimensions_filter_idx').on(table.key, table.value, table.itemId)
|
index('item_dimensions_filter_idx').on(table.key, table.value, table.itemId)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const csPriceStreams = pgTable(
|
export const csPriceStreams = csSchema.table(
|
||||||
'cs_price_streams',
|
'price_streams',
|
||||||
{
|
{
|
||||||
source: text('source').notNull(),
|
source: text('source').notNull(),
|
||||||
currency: text('currency').notNull(),
|
currency: text('currency').notNull(),
|
||||||
kind: text('kind').notNull()
|
kind: text('kind').notNull()
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex('cs_price_streams_unique_idx').on(table.source, table.currency, table.kind)
|
primaryKey({ name: 'price_streams_pkey', columns: [table.source, table.currency, table.kind] })
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { boolean, index, integer, jsonb, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import { boolean, index, integer, jsonb, text, unique } from 'drizzle-orm/pg-core';
|
||||||
|
import { gamesSchema, refSchema } from './schemas';
|
||||||
import type { MasteryRules } from '../../../game-tracker/mastery';
|
import type { MasteryRules } from '../../../game-tracker/mastery';
|
||||||
import { user } from './auth';
|
import { user } from './auth';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
|
|
||||||
// Checklist-style games (Call of Duty and future franchises). Pokémon keeps its
|
// Checklist-style games (Call of Duty and future franchises). Pokémon keeps its
|
||||||
// own species × game model in catch-em-all.ts.
|
// own species × game model in catch-em-all.ts.
|
||||||
export const trackerGames = pgTable('tracker_games', {
|
export const trackerGames = refSchema.table('tracker_games', {
|
||||||
id: id(),
|
id: id(),
|
||||||
slug: text('slug').notNull().unique(),
|
slug: text('slug').notNull().unique(),
|
||||||
franchise: text('franchise').notNull(),
|
franchise: text('franchise').notNull(),
|
||||||
@@ -17,7 +18,7 @@ export const trackerGames = pgTable('tracker_games', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const trackerItems = pgTable(
|
export const trackerItems = refSchema.table(
|
||||||
'tracker_items',
|
'tracker_items',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -41,7 +42,7 @@ export const trackerItems = pgTable(
|
|||||||
|
|
||||||
// One row per completed milestone: `done` for achievements, or a weapon
|
// One row per completed milestone: `done` for achievements, or a weapon
|
||||||
// milestone key such as `gold` or `maxLevel`. Derived tiers are never stored.
|
// milestone key such as `gold` or `maxLevel`. Derived tiers are never stored.
|
||||||
export const trackerProgress = pgTable(
|
export const trackerProgress = gamesSchema.table(
|
||||||
'tracker_progress',
|
'tracker_progress',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import {
|
|||||||
index,
|
index,
|
||||||
integer,
|
integer,
|
||||||
jsonb,
|
jsonb,
|
||||||
pgTable,
|
primaryKey,
|
||||||
text,
|
text,
|
||||||
uniqueIndex
|
uniqueIndex
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import { houseSchema } from './schemas';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { id, timestamptz } from './helpers';
|
import { id, timestamptz } from './helpers';
|
||||||
|
|
||||||
// Created by scripts/house-tracker-migration.sql; keep both in sync.
|
export const houseListings = houseSchema.table(
|
||||||
export const houseListings = pgTable(
|
'listings',
|
||||||
'house_listings',
|
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
source: text('source').notNull(),
|
source: text('source').notNull(),
|
||||||
@@ -58,21 +58,21 @@ export const houseListings = pgTable(
|
|||||||
notes: text('notes')
|
notes: text('notes')
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
check('house_listings_offer_check', sql`offer IN ('rent', 'buy')`),
|
check('listings_offer_check', sql`offer IN ('rent', 'buy')`),
|
||||||
check('house_listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`),
|
check('listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`),
|
||||||
uniqueIndex('house_listings_source_idx').on(table.source, table.sourceId),
|
uniqueIndex('listings_source_idx').on(table.source, table.sourceId),
|
||||||
index('house_listings_active_idx')
|
index('listings_active_idx')
|
||||||
.on(table.offer, table.firstSeenAt.desc())
|
.on(table.offer, table.firstSeenAt.desc().nullsFirst())
|
||||||
.where(sql`removed_at IS NULL`),
|
.where(sql`removed_at IS NULL`),
|
||||||
index('house_listings_city_idx').on(table.source, table.offer, table.citySlug),
|
index('listings_city_idx').on(table.source, table.offer, table.citySlug),
|
||||||
index('house_listings_details_idx')
|
index('listings_details_idx')
|
||||||
.on(table.firstSeenAt.desc())
|
.on(table.firstSeenAt.desc().nullsFirst())
|
||||||
.where(sql`detail_fetched_at IS NULL`)
|
.where(sql`detail_fetched_at IS NULL`)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const housePriceHistory = pgTable(
|
export const housePriceHistory = houseSchema.table(
|
||||||
'house_price_history',
|
'price_history',
|
||||||
{
|
{
|
||||||
listingId: integer('listing_id')
|
listingId: integer('listing_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -80,10 +80,12 @@ export const housePriceHistory = pgTable(
|
|||||||
observedAt: timestamptz('observed_at').notNull().defaultNow(),
|
observedAt: timestamptz('observed_at').notNull().defaultNow(),
|
||||||
priceEur: integer('price_eur').notNull()
|
priceEur: integer('price_eur').notNull()
|
||||||
},
|
},
|
||||||
(table) => [index('house_price_history_listing_idx').on(table.listingId, table.observedAt)]
|
(table) => [
|
||||||
|
primaryKey({ name: 'price_history_pkey', columns: [table.listingId, table.observedAt] })
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const houseSearches = pgTable('house_searches', {
|
export const houseSearches = houseSchema.table('searches', {
|
||||||
searchUrl: text('search_url').primaryKey(),
|
searchUrl: text('search_url').primaryKey(),
|
||||||
source: text('source').notNull(),
|
source: text('source').notNull(),
|
||||||
lane: text('lane').notNull(),
|
lane: text('lane').notNull(),
|
||||||
@@ -92,8 +94,8 @@ export const houseSearches = pgTable('house_searches', {
|
|||||||
lastFound: integer('last_found').notNull().default(0)
|
lastFound: integer('last_found').notNull().default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const houseWorkerRuns = pgTable(
|
export const houseWorkerRuns = houseSchema.table(
|
||||||
'house_worker_runs',
|
'worker_runs',
|
||||||
{
|
{
|
||||||
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(),
|
||||||
source: text('source').notNull(),
|
source: text('source').notNull(),
|
||||||
@@ -106,11 +108,11 @@ export const houseWorkerRuns = pgTable(
|
|||||||
message: text('message'),
|
message: text('message'),
|
||||||
createdAt: timestamptz('created_at').notNull().defaultNow()
|
createdAt: timestamptz('created_at').notNull().defaultNow()
|
||||||
},
|
},
|
||||||
(table) => [index('house_worker_runs_created_idx').on(table.createdAt.desc())]
|
(table) => [index('worker_runs_created_idx').on(table.createdAt.desc().nullsFirst())]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const houseAlertRules = pgTable(
|
export const houseAlertRules = houseSchema.table(
|
||||||
'house_alert_rules',
|
'alert_rules',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
@@ -122,7 +124,11 @@ export const houseAlertRules = pgTable(
|
|||||||
minBedrooms: integer('min_bedrooms'),
|
minBedrooms: integer('min_bedrooms'),
|
||||||
cities: text('cities').array().notNull().default([]),
|
cities: text('cities').array().notNull().default([]),
|
||||||
propertyTypes: text('property_types').array().notNull().default([]),
|
propertyTypes: text('property_types').array().notNull().default([]),
|
||||||
createdAt: timestamptz('created_at').notNull().defaultNow()
|
createdAt: timestamptz('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamptz('updated_at')
|
||||||
|
.notNull()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
},
|
},
|
||||||
() => [check('house_alert_rules_offer_check', sql`offer IN ('rent', 'buy')`)]
|
() => [check('alert_rules_offer_check', sql`offer IN ('rent', 'buy')`)]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export * from './catch-em-all';
|
|||||||
export * from './game-tracker';
|
export * from './game-tracker';
|
||||||
export * from './tracker-journal';
|
export * from './tracker-journal';
|
||||||
export * from './animaldex';
|
export * from './animaldex';
|
||||||
export * from './lists';
|
export * from './lists'; // meta registry; the lists themselves are runtime tables
|
||||||
|
export * from './schemas';
|
||||||
export * from './money-tracker';
|
export * from './money-tracker';
|
||||||
export * from './cs-prices';
|
export * from './cs-prices';
|
||||||
export * from './house-tracker';
|
export * from './house-tracker';
|
||||||
|
|||||||
@@ -1,47 +1,92 @@
|
|||||||
import { boolean, index, integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import {
|
||||||
import { id, timestamps } from './helpers';
|
boolean,
|
||||||
import { user } from './auth';
|
check,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
unique
|
||||||
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { metaSchema } from './schemas';
|
||||||
|
|
||||||
export const lists = pgTable('lists', {
|
// Registry of GUI-visible tables. Runtime lists live in schema `lists` (one real
|
||||||
id: id(),
|
// table per list, created through meta.create_list()); that schema is excluded
|
||||||
name: text('name').notNull(),
|
// from drizzle-kit via schemaFilter. Structure truth is pg_catalog; these rows are
|
||||||
slug: text('slug').notNull().unique(),
|
// presentation. See src/lib/server/lists.ts for the list engine.
|
||||||
pictureUrl: text('picture_url'),
|
const ts = (name: string) => timestamp(name, { withTimezone: true });
|
||||||
isProgress: boolean('is_progress').notNull().default(false),
|
|
||||||
...timestamps
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listItems = pgTable(
|
export const metaTables = metaSchema.table(
|
||||||
'list_items',
|
'tables',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
||||||
listId: integer('list_id')
|
schemaName: text('schema_name').notNull(),
|
||||||
.notNull()
|
tableName: text('table_name').notNull(),
|
||||||
.references(() => lists.id, { onDelete: 'cascade' }),
|
slug: text('slug').notNull().unique('tables_slug_key'),
|
||||||
name: text('name').notNull(),
|
displayName: text('display_name').notNull(),
|
||||||
pictureUrl: text('picture_url'),
|
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
...timestamps
|
icon: text('icon'),
|
||||||
|
pictureUrl: text('picture_url'),
|
||||||
|
kind: text('kind', { enum: ['list', 'core', 'ref'] }).notNull(),
|
||||||
|
editable: boolean('editable').notNull().default(true),
|
||||||
|
hasDone: boolean('has_done').notNull().default(false),
|
||||||
|
defaultSort: text('default_sort'),
|
||||||
|
sortOrder: integer('sort_order').notNull().default(0),
|
||||||
|
archivedAt: ts('archived_at'),
|
||||||
|
createdAt: ts('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: ts('updated_at').notNull().defaultNow()
|
||||||
},
|
},
|
||||||
(table) => [index().on(table.listId)]
|
(t) => [
|
||||||
|
unique('tables_schema_name_table_name_key').on(t.schemaName, t.tableName),
|
||||||
|
check('tables_slug_check', sql`slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'`),
|
||||||
|
check('tables_kind_check', sql`kind IN ('list','core','ref')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const listItemCompletions = pgTable(
|
export const uiTypes = [
|
||||||
'list_item_completions',
|
'text',
|
||||||
|
'longtext',
|
||||||
|
'url',
|
||||||
|
'image',
|
||||||
|
'number',
|
||||||
|
'cents',
|
||||||
|
'date',
|
||||||
|
'datetime',
|
||||||
|
'bool',
|
||||||
|
'select',
|
||||||
|
'multiselect',
|
||||||
|
'fk',
|
||||||
|
'json'
|
||||||
|
] as const;
|
||||||
|
export type UiType = (typeof uiTypes)[number];
|
||||||
|
|
||||||
|
export const metaColumns = metaSchema.table(
|
||||||
|
'columns',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
|
||||||
itemId: integer('item_id')
|
tableId: integer('table_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => listItems.id, { onDelete: 'cascade' }),
|
.references(() => metaTables.id, { onDelete: 'cascade' }),
|
||||||
userId: text('user_id')
|
columnName: text('column_name').notNull(),
|
||||||
.notNull()
|
label: text('label'),
|
||||||
.references(() => user.id, { onDelete: 'cascade' }),
|
uiType: text('ui_type', { enum: uiTypes }).notNull(),
|
||||||
...timestamps
|
options: jsonb('options').$type<{ values?: string[] }>().notNull().default({}),
|
||||||
|
position: integer('position').notNull().default(0),
|
||||||
|
hidden: boolean('hidden').notNull().default(false),
|
||||||
|
helpText: text('help_text'),
|
||||||
|
createdAt: ts('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: ts('updated_at').notNull().defaultNow()
|
||||||
},
|
},
|
||||||
(table) => [unique().on(table.itemId, table.userId), index().on(table.userId)]
|
(t) => [
|
||||||
|
unique('columns_table_id_column_name_key').on(t.tableId, t.columnName),
|
||||||
|
index('columns_table_id_index').on(t.tableId),
|
||||||
|
check(
|
||||||
|
'columns_ui_type_check',
|
||||||
|
sql`ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json')`
|
||||||
|
)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export type List = typeof lists.$inferSelect;
|
export type MetaTable = typeof metaTables.$inferSelect;
|
||||||
export type NewList = typeof lists.$inferInsert;
|
export type MetaColumn = typeof metaColumns.$inferSelect;
|
||||||
export type ListItem = typeof listItems.$inferSelect;
|
|
||||||
export type NewListItem = typeof listItems.$inferInsert;
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { date, index, integer, numeric, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import { boolean, check, date, index, integer, numeric, text, unique } from 'drizzle-orm/pg-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { moneySchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
import { user } from './auth';
|
import { user } from './auth';
|
||||||
|
|
||||||
export const moneyAccounts = pgTable(
|
export const moneyAccounts = moneySchema.table(
|
||||||
'money_accounts',
|
'accounts',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -15,11 +17,15 @@ export const moneyAccounts = pgTable(
|
|||||||
color: text('color'),
|
color: text('color'),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [unique().on(table.userId, table.name), index().on(table.userId)]
|
(table) => [
|
||||||
|
unique().on(table.userId, table.name),
|
||||||
|
index().on(table.userId),
|
||||||
|
check('accounts_kind_check', sql`kind IN ('spendable','investment')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyTransactions = pgTable(
|
export const moneyTransactions = moneySchema.table(
|
||||||
'money_transactions',
|
'transactions',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -36,8 +42,8 @@ export const moneyTransactions = pgTable(
|
|||||||
(table) => [index().on(table.userId, table.date, table.id), index().on(table.accountId)]
|
(table) => [index().on(table.userId, table.date, table.id), index().on(table.accountId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyRecurring = pgTable(
|
export const moneyRecurring = moneySchema.table(
|
||||||
'money_recurring',
|
'recurring',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -51,11 +57,14 @@ export const moneyRecurring = pgTable(
|
|||||||
accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }),
|
accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [index().on(table.userId)]
|
(table) => [
|
||||||
|
index().on(table.userId),
|
||||||
|
check('recurring_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyForecasts = pgTable(
|
export const moneyForecasts = moneySchema.table(
|
||||||
'money_forecasts',
|
'forecasts',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -70,8 +79,8 @@ export const moneyForecasts = pgTable(
|
|||||||
(table) => [index().on(table.userId, table.date)]
|
(table) => [index().on(table.userId, table.date)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyUtilitySettings = pgTable(
|
export const moneyUtilitySettings = moneySchema.table(
|
||||||
'money_utility_settings',
|
'utility_settings',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -83,11 +92,14 @@ export const moneyUtilitySettings = pgTable(
|
|||||||
billingStart: date('billing_start', { mode: 'string' }),
|
billingStart: date('billing_start', { mode: 'string' }),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [unique().on(table.userId, table.kind)]
|
(table) => [
|
||||||
|
unique().on(table.userId, table.kind),
|
||||||
|
check('utility_settings_kind_check', sql`kind IN ('electricity','gas','water')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyUtilityReadings = pgTable(
|
export const moneyUtilityReadings = moneySchema.table(
|
||||||
'money_utility_readings',
|
'utility_readings',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -98,11 +110,14 @@ export const moneyUtilityReadings = pgTable(
|
|||||||
reading: numeric('reading', { precision: 12, scale: 3 }).notNull(),
|
reading: numeric('reading', { precision: 12, scale: 3 }).notNull(),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [index().on(table.userId, table.kind, table.date)]
|
(table) => [
|
||||||
|
index().on(table.userId, table.kind, table.date),
|
||||||
|
check('utility_readings_kind_check', sql`kind IN ('electricity','gas','water')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyWishlist = pgTable(
|
export const moneyWishlist = moneySchema.table(
|
||||||
'money_wishlist',
|
'wishlist',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -117,8 +132,8 @@ export const moneyWishlist = pgTable(
|
|||||||
(table) => [index().on(table.userId)]
|
(table) => [index().on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyProjectionSettings = pgTable(
|
export const moneyProjectionSettings = moneySchema.table(
|
||||||
'money_projection_settings',
|
'projection_settings',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -132,16 +147,22 @@ export const moneyProjectionSettings = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default('5.00'),
|
.default('5.00'),
|
||||||
contributionMode: text('contribution_mode').notNull().default('excess'),
|
contributionMode: text('contribution_mode').notNull().default('excess'),
|
||||||
includeRecurring: integer('include_recurring').notNull().default(1),
|
includeRecurring: boolean('include_recurring').notNull().default(true),
|
||||||
fixedMonthlyCents: integer('fixed_monthly_cents').notNull().default(0),
|
fixedMonthlyCents: integer('fixed_monthly_cents').notNull().default(0),
|
||||||
endDate: date('end_date', { mode: 'string' }),
|
endDate: date('end_date', { mode: 'string' }),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [unique().on(table.userId)]
|
(table) => [
|
||||||
|
unique().on(table.userId),
|
||||||
|
check(
|
||||||
|
'projection_settings_contribution_mode_check',
|
||||||
|
sql`contribution_mode IN ('excess','fixed')`
|
||||||
|
)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const moneyInsurances = pgTable(
|
export const moneyInsurances = moneySchema.table(
|
||||||
'money_insurances',
|
'insurances',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
@@ -154,5 +175,8 @@ export const moneyInsurances = pgTable(
|
|||||||
cadence: text('cadence').notNull().default('monthly'),
|
cadence: text('cadence').notNull().default('monthly'),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(table) => [index().on(table.userId)]
|
(table) => [
|
||||||
|
index().on(table.userId),
|
||||||
|
check('insurances_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { integer, pgTable, text, unique } from 'drizzle-orm/pg-core';
|
import { integer, text, unique } from 'drizzle-orm/pg-core';
|
||||||
|
import { siteSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
|
|
||||||
export const tasks = pgTable('tasks', {
|
export const tasks = siteSchema.table('tasks', {
|
||||||
id: id(),
|
id: id(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
currentPoints: integer('current_points').notNull().default(0),
|
currentPoints: integer('current_points').notNull().default(0),
|
||||||
@@ -10,15 +11,15 @@ export const tasks = pgTable('tasks', {
|
|||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const topics = pgTable('topics', {
|
export const topics = siteSchema.table('topics', {
|
||||||
id: id(),
|
id: id(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
emoji: text('emoji'),
|
emoji: text('emoji'),
|
||||||
...timestamps
|
...timestamps
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tasksToTopics = pgTable(
|
export const tasksToTopics = siteSchema.table(
|
||||||
'tasks_to_topics',
|
'task_topics',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
taskId: integer('task_id')
|
taskId: integer('task_id')
|
||||||
|
|||||||
14
src/lib/server/db/schema/schemas.ts
Normal file
14
src/lib/server/db/schema/schemas.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { pgSchema } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
// One Postgres schema per domain (see ~/dev/setup/db/PLAN.md). `lists`, `legacy`,
|
||||||
|
// `energy` and `hardware` are deliberately not modelled here: lists are created at
|
||||||
|
// runtime through meta.create_list(), and energy/hardware belong to other tools.
|
||||||
|
export const authSchema = pgSchema('auth');
|
||||||
|
export const moneySchema = pgSchema('money');
|
||||||
|
export const houseSchema = pgSchema('house');
|
||||||
|
export const refSchema = pgSchema('ref');
|
||||||
|
export const gamesSchema = pgSchema('games');
|
||||||
|
export const csSchema = pgSchema('cs');
|
||||||
|
export const animaldexSchema = pgSchema('animaldex');
|
||||||
|
export const siteSchema = pgSchema('site');
|
||||||
|
export const metaSchema = pgSchema('meta');
|
||||||
@@ -2,20 +2,21 @@ import {
|
|||||||
boolean,
|
boolean,
|
||||||
check,
|
check,
|
||||||
date,
|
date,
|
||||||
|
numeric,
|
||||||
index,
|
index,
|
||||||
integer,
|
integer,
|
||||||
jsonb,
|
jsonb,
|
||||||
pgTable,
|
|
||||||
text,
|
text,
|
||||||
unique
|
unique
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import { gamesSchema, refSchema } from './schemas';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { user } from './auth';
|
import { user } from './auth';
|
||||||
import { games, pokemon } from './catch-em-all';
|
import { games, pokemon } from './catch-em-all';
|
||||||
import { trackerGames, trackerItems } from './game-tracker';
|
import { trackerGames, trackerItems } from './game-tracker';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
|
|
||||||
export const pokemonResearch = pgTable('pokemon_research', {
|
export const pokemonResearch = refSchema.table('pokemon_research', {
|
||||||
pokemonId: integer('pokemon_id')
|
pokemonId: integer('pokemon_id')
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
.references(() => pokemon.id, { onDelete: 'cascade' }),
|
||||||
@@ -36,7 +37,7 @@ export const pokemonResearch = pgTable('pokemon_research', {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pokemonDexNumbers = pgTable(
|
export const pokemonDexNumbers = refSchema.table(
|
||||||
'pokemon_dex_numbers',
|
'pokemon_dex_numbers',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -53,7 +54,7 @@ export const pokemonDexNumbers = pgTable(
|
|||||||
(t) => [unique().on(t.pokemonId, t.gameId, t.dex), index().on(t.gameId)]
|
(t) => [unique().on(t.pokemonId, t.gameId, t.dex), index().on(t.gameId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonMoves = pgTable('pokemon_moves', {
|
export const pokemonMoves = refSchema.table('pokemon_moves', {
|
||||||
id: integer('id').primaryKey(),
|
id: integer('id').primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
type: text('type').notNull(),
|
type: text('type').notNull(),
|
||||||
@@ -65,7 +66,7 @@ export const pokemonMoves = pgTable('pokemon_moves', {
|
|||||||
target: text('target').notNull()
|
target: text('target').notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pokemonLearnsets = pgTable(
|
export const pokemonLearnsets = refSchema.table(
|
||||||
'pokemon_learnsets',
|
'pokemon_learnsets',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -78,7 +79,7 @@ export const pokemonLearnsets = pgTable(
|
|||||||
(t) => [unique().on(t.pokemonId, t.versionGroup)]
|
(t) => [unique().on(t.pokemonId, t.versionGroup)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonGameResearch = pgTable('pokemon_game_research', {
|
export const pokemonGameResearch = refSchema.table('pokemon_game_research', {
|
||||||
gameId: integer('game_id')
|
gameId: integer('game_id')
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.references(() => games.id, { onDelete: 'cascade' }),
|
.references(() => games.id, { onDelete: 'cascade' }),
|
||||||
@@ -86,7 +87,7 @@ export const pokemonGameResearch = pgTable('pokemon_game_research', {
|
|||||||
regions: text('regions').array().notNull()
|
regions: text('regions').array().notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pokemonJourneys = pgTable(
|
export const pokemonJourneys = gamesSchema.table(
|
||||||
'pokemon_journeys',
|
'pokemon_journeys',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -103,10 +104,13 @@ export const pokemonJourneys = pgTable(
|
|||||||
completed: boolean('completed').notNull().default(false),
|
completed: boolean('completed').notNull().default(false),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(t) => [unique().on(t.userId, t.gameId)]
|
(t) => [
|
||||||
|
unique().on(t.userId, t.gameId),
|
||||||
|
check('pokemon_journeys_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonSeen = pgTable(
|
export const pokemonSeen = gamesSchema.table(
|
||||||
'pokemon_seen',
|
'pokemon_seen',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -124,7 +128,7 @@ export const pokemonSeen = pgTable(
|
|||||||
(t) => [unique().on(t.userId, t.gameId, t.pokemonId), index().on(t.userId)]
|
(t) => [unique().on(t.userId, t.gameId, t.pokemonId), index().on(t.userId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonCatches = pgTable(
|
export const pokemonCatches = gamesSchema.table(
|
||||||
'pokemon_catches',
|
'pokemon_catches',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -157,13 +161,14 @@ export const pokemonCatches = pgTable(
|
|||||||
(t) => [
|
(t) => [
|
||||||
index().on(t.userId, t.gameId, t.pokemonId),
|
index().on(t.userId, t.gameId, t.pokemonId),
|
||||||
unique().on(t.userId, t.gameId, t.partySlot),
|
unique().on(t.userId, t.gameId, t.partySlot),
|
||||||
check('pokemon_catch_level', sql`${t.level} between 1 and 100`),
|
check('pokemon_catches_level_check', sql`${t.level} between 1 and 100`),
|
||||||
check('pokemon_party_slot', sql`${t.partySlot} between 1 and 6`),
|
check('pokemon_catches_party_slot_check', sql`${t.partySlot} between 1 and 6`),
|
||||||
check('pokemon_four_moves', sql`cardinality(${t.moves}) <= 4`)
|
check('pokemon_catches_moves_check', sql`cardinality(${t.moves}) <= 4`),
|
||||||
|
check('pokemon_catches_stage_check', sql`${t.stage} IN ('story','postgame','collection')`)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const pokemonMilestones = pgTable(
|
export const pokemonMilestones = gamesSchema.table(
|
||||||
'pokemon_milestones',
|
'pokemon_milestones',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -184,7 +189,7 @@ export const pokemonMilestones = pgTable(
|
|||||||
(t) => [unique().on(t.userId, t.gameId, t.key)]
|
(t) => [unique().on(t.userId, t.gameId, t.key)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const trackerRanks = pgTable(
|
export const trackerRanks = gamesSchema.table(
|
||||||
'tracker_ranks',
|
'tracker_ranks',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -200,14 +205,12 @@ export const trackerRanks = pgTable(
|
|||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
unique().on(t.userId, t.gameId),
|
unique().on(t.userId, t.gameId),
|
||||||
check(
|
check('tracker_ranks_level_check', sql`${t.level} between 1 and 10000`),
|
||||||
'tracker_rank_range',
|
check('tracker_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
|
||||||
sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000`
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const trackerWeaponRanks = pgTable(
|
export const trackerWeaponRanks = gamesSchema.table(
|
||||||
'tracker_weapon_ranks',
|
'tracker_weapon_ranks',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -223,14 +226,12 @@ export const trackerWeaponRanks = pgTable(
|
|||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
unique().on(t.userId, t.itemId),
|
unique().on(t.userId, t.itemId),
|
||||||
check(
|
check('tracker_weapon_ranks_level_check', sql`${t.level} between 1 and 10000`),
|
||||||
'tracker_weapon_rank_range',
|
check('tracker_weapon_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`)
|
||||||
sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000`
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const trackerChallenges = pgTable(
|
export const trackerChallenges = refSchema.table(
|
||||||
'tracker_challenges',
|
'tracker_challenges',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -249,7 +250,7 @@ export const trackerChallenges = pgTable(
|
|||||||
(t) => [unique().on(t.gameId, t.key)]
|
(t) => [unique().on(t.gameId, t.key)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const trackerChallengeProgress = pgTable(
|
export const trackerChallengeProgress = gamesSchema.table(
|
||||||
'tracker_challenge_progress',
|
'tracker_challenge_progress',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -263,10 +264,13 @@ export const trackerChallengeProgress = pgTable(
|
|||||||
done: boolean('done').notNull().default(false),
|
done: boolean('done').notNull().default(false),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(t) => [unique().on(t.userId, t.challengeId)]
|
(t) => [
|
||||||
|
unique().on(t.userId, t.challengeId),
|
||||||
|
check('tracker_challenge_progress_current_check', sql`${t.current} >= 0`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const osuEntries = pgTable(
|
export const osuEntries = gamesSchema.table(
|
||||||
'osu_entries',
|
'osu_entries',
|
||||||
{
|
{
|
||||||
id: id(),
|
id: id(),
|
||||||
@@ -282,18 +286,23 @@ export const osuEntries = pgTable(
|
|||||||
skill: text('skill').notNull().default('stamina'),
|
skill: text('skill').notNull().default('stamina'),
|
||||||
bpm: integer('bpm'),
|
bpm: integer('bpm'),
|
||||||
duration: integer('duration'),
|
duration: integer('duration'),
|
||||||
accuracy: text('accuracy'),
|
accuracy: numeric('accuracy', { mode: 'number' }),
|
||||||
combo: integer('combo'),
|
combo: integer('combo'),
|
||||||
misses: integer('misses'),
|
misses: integer('misses'),
|
||||||
pp: text('pp'),
|
pp: numeric('pp', { mode: 'number' }),
|
||||||
mods: text('mods').notNull().default('NM'),
|
mods: text('mods').notNull().default('NM'),
|
||||||
rate: text('rate').notNull().default('1.00'),
|
rate: numeric('rate', { precision: 4, scale: 2, mode: 'number' })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`1.00`),
|
||||||
achieved: boolean('achieved').notNull().default(false),
|
achieved: boolean('achieved').notNull().default(false),
|
||||||
playedOn: date('played_on').notNull(),
|
playedOn: date('played_on').notNull(),
|
||||||
notes: text('notes').notNull().default(''),
|
notes: text('notes').notNull().default(''),
|
||||||
...timestamps
|
...timestamps
|
||||||
},
|
},
|
||||||
(t) => [index().on(t.userId, t.kind)]
|
(t) => [
|
||||||
|
index().on(t.userId, t.kind),
|
||||||
|
check('osu_entries_kind_check', sql`${t.kind} IN ('score','goal','training')`)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PokemonCatch = typeof pokemonCatches.$inferSelect;
|
export type PokemonCatch = typeof pokemonCatches.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user