From 64fb8df262d6e50334c00108c6a08ca942402bb9 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 23 Oct 2026 22:03:42 +0000 Subject: [PATCH] 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 --- db/reorg/01_roles.sql | 17 + db/reorg/02_schemas_move.sql | 142 + db/reorg/03_fixes.sql | 32 + db/reorg/04_meta.sql | 224 + db/reorg/05_lists.sql | 176 + db/reorg/06_hardware_energy_cs.sql | 148 + db/reorg/07_triggers_meta_grants.sql | 79 + db/reorg/08_verify.sql | 68 + db/reorg/09_drizzle_names.sql | 58 + db/reorg/README.md | 21 + db/reorg/run.sh | 24 + drizzle.config.ts | 10 +- drizzle/20261023215112_baseline/migration.sql | 857 ++ drizzle/20261023215112_baseline/snapshot.json | 10307 ++++++++++++++++ scripts/db-migrate.ts | 55 + src/lib/server/db/relations.ts | 28 +- src/lib/server/db/schema/animaldex.ts | 9 +- src/lib/server/db/schema/auth.ts | 11 +- src/lib/server/db/schema/blog.ts | 7 +- src/lib/server/db/schema/catch-em-all.ts | 27 +- src/lib/server/db/schema/cs-prices.ts | 43 +- src/lib/server/db/schema/game-tracker.ts | 9 +- src/lib/server/db/schema/house-tracker.ts | 52 +- src/lib/server/db/schema/index.ts | 3 +- src/lib/server/db/schema/lists.ts | 115 +- src/lib/server/db/schema/money-tracker.ts | 76 +- src/lib/server/db/schema/progress.ts | 11 +- src/lib/server/db/schema/schemas.ts | 14 + src/lib/server/db/schema/tracker-journal.ts | 73 +- 29 files changed, 12507 insertions(+), 189 deletions(-) create mode 100644 db/reorg/01_roles.sql create mode 100644 db/reorg/02_schemas_move.sql create mode 100644 db/reorg/03_fixes.sql create mode 100644 db/reorg/04_meta.sql create mode 100644 db/reorg/05_lists.sql create mode 100644 db/reorg/06_hardware_energy_cs.sql create mode 100644 db/reorg/07_triggers_meta_grants.sql create mode 100644 db/reorg/08_verify.sql create mode 100644 db/reorg/09_drizzle_names.sql create mode 100644 db/reorg/README.md create mode 100755 db/reorg/run.sh create mode 100644 drizzle/20261023215112_baseline/migration.sql create mode 100644 drizzle/20261023215112_baseline/snapshot.json create mode 100644 scripts/db-migrate.ts create mode 100644 src/lib/server/db/schema/schemas.ts diff --git a/db/reorg/01_roles.sql b/db/reorg/01_roles.sql new file mode 100644 index 0000000..763d852 --- /dev/null +++ b/db/reorg/01_roles.sql @@ -0,0 +1,17 @@ +-- 01_roles.sql: cluster-level roles. Run as a superuser (postgres). Idempotent. +-- In prod: needs `sudo docker exec -i 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'; diff --git a/db/reorg/02_schemas_move.sql b/db/reorg/02_schemas_move.sql new file mode 100644 index 0000000..9b3b1e7 --- /dev/null +++ b/db/reorg/02_schemas_move.sql @@ -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; diff --git a/db/reorg/03_fixes.sql b/db/reorg/03_fixes.sql new file mode 100644 index 0000000..ae47b33 --- /dev/null +++ b/db/reorg/03_fixes.sql @@ -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; diff --git a/db/reorg/04_meta.sql b/db/reorg/04_meta.sql new file mode 100644 index 0000000..324681a --- /dev/null +++ b/db/reorg/04_meta.sql @@ -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 diff --git a/db/reorg/05_lists.sql b/db/reorg/05_lists.sql new file mode 100644 index 0000000..6594818 --- /dev/null +++ b/db/reorg/05_lists.sql @@ -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. 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 diff --git a/db/reorg/06_hardware_energy_cs.sql b/db/reorg/06_hardware_energy_cs.sql new file mode 100644 index 0000000..0f37790 --- /dev/null +++ b/db/reorg/06_hardware_energy_cs.sql @@ -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; diff --git a/db/reorg/07_triggers_meta_grants.sql b/db/reorg/07_triggers_meta_grants.sql new file mode 100644 index 0000000..201a473 --- /dev/null +++ b/db/reorg/07_triggers_meta_grants.sql @@ -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; diff --git a/db/reorg/08_verify.sql b/db/reorg/08_verify.sql new file mode 100644 index 0000000..0e530d3 --- /dev/null +++ b/db/reorg/08_verify.sql @@ -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 $$; diff --git a/db/reorg/09_drizzle_names.sql b/db/reorg/09_drizzle_names.sql new file mode 100644 index 0000000..3ff8df9 --- /dev/null +++ b/db/reorg/09_drizzle_names.sql @@ -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; diff --git a/db/reorg/README.md b/db/reorg/README.md new file mode 100644 index 0000000..9fc1e3c --- /dev/null +++ b/db/reorg/README.md @@ -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). diff --git a/db/reorg/run.sh b/db/reorg/run.sh new file mode 100755 index 0000000..43c33ec --- /dev/null +++ b/db/reorg/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# run.sh : 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 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" diff --git a/drizzle.config.ts b/drizzle.config.ts index 6b51eb0..3b7b610 100755 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,12 +1,20 @@ 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({ dialect: 'postgresql', schema: './src/lib/server/db/schema/index.ts', out: './drizzle', + schemaFilter: ['auth', 'money', 'house', 'ref', 'games', 'cs', 'animaldex', 'site', 'meta'], + migrations: { schema: 'drizzle', table: '__drizzle_migrations' }, verbose: true, strict: true, dbCredentials: { - url: process.env.DB_URL! + url: (process.env.DB_MIGRATE_URL || process.env.DB_URL)! } }); diff --git a/drizzle/20261023215112_baseline/migration.sql b/drizzle/20261023215112_baseline/migration.sql new file mode 100644 index 0000000..45e96eb --- /dev/null +++ b/drizzle/20261023215112_baseline/migration.sql @@ -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; \ No newline at end of file diff --git a/drizzle/20261023215112_baseline/snapshot.json b/drizzle/20261023215112_baseline/snapshot.json new file mode 100644 index 0000000..ae9c47b --- /dev/null +++ b/drizzle/20261023215112_baseline/snapshot.json @@ -0,0 +1,10307 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "f166e058-a62b-4994-b4ba-12ee871a8bb0", + "prevIds": [ + "00000000-0000-0000-0000-000000000000" + ], + "ddl": [ + { + "name": "animaldex", + "entityType": "schemas" + }, + { + "name": "auth", + "entityType": "schemas" + }, + { + "name": "cs", + "entityType": "schemas" + }, + { + "name": "games", + "entityType": "schemas" + }, + { + "name": "house", + "entityType": "schemas" + }, + { + "name": "meta", + "entityType": "schemas" + }, + { + "name": "money", + "entityType": "schemas" + }, + { + "name": "ref", + "entityType": "schemas" + }, + { + "name": "site", + "entityType": "schemas" + }, + { + "values": [ + "draft", + "published" + ], + "name": "blog_article_status", + "entityType": "enums", + "schema": "site" + }, + { + "values": [ + "catchable", + "trade_only", + "transfer_only", + "event_only", + "unknown", + "evolve", + "breed" + ], + "name": "availability", + "entityType": "enums", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "auth" + }, + { + "isRlsEnabled": false, + "name": "blog_articles", + "entityType": "tables", + "schema": "site" + }, + { + "isRlsEnabled": false, + "name": "tasks", + "entityType": "tables", + "schema": "site" + }, + { + "isRlsEnabled": false, + "name": "task_topics", + "entityType": "tables", + "schema": "site" + }, + { + "isRlsEnabled": false, + "name": "topics", + "entityType": "tables", + "schema": "site" + }, + { + "isRlsEnabled": false, + "name": "catch_em_all_settings", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "pokemon_games", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_encounters", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_evolutions", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_game_entries", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_locations", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "catch_em_all_entries", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "catch_em_all_games", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "tracker_games", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "tracker_items", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "tracker_progress", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "osu_entries", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "pokemon_catches", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "pokemon_dex_numbers", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_game_research", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_journeys", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "pokemon_learnsets", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_milestones", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "pokemon_moves", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_research", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "pokemon_seen", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "tracker_challenge_progress", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "tracker_challenges", + "entityType": "tables", + "schema": "ref" + }, + { + "isRlsEnabled": false, + "name": "tracker_ranks", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "tracker_weapon_ranks", + "entityType": "tables", + "schema": "games" + }, + { + "isRlsEnabled": false, + "name": "animals", + "entityType": "tables", + "schema": "animaldex" + }, + { + "isRlsEnabled": false, + "name": "sighting_photos", + "entityType": "tables", + "schema": "animaldex" + }, + { + "isRlsEnabled": false, + "name": "sightings", + "entityType": "tables", + "schema": "animaldex" + }, + { + "isRlsEnabled": false, + "name": "columns", + "entityType": "tables", + "schema": "meta" + }, + { + "isRlsEnabled": false, + "name": "tables", + "entityType": "tables", + "schema": "meta" + }, + { + "isRlsEnabled": false, + "name": "accounts", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "forecasts", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "insurances", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "projection_settings", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "recurring", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "transactions", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "utility_readings", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "utility_settings", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "wishlist", + "entityType": "tables", + "schema": "money" + }, + { + "isRlsEnabled": false, + "name": "item_dimensions", + "entityType": "tables", + "schema": "cs" + }, + { + "isRlsEnabled": false, + "name": "items", + "entityType": "tables", + "schema": "cs" + }, + { + "isRlsEnabled": false, + "name": "price_observations", + "entityType": "tables", + "schema": "cs" + }, + { + "isRlsEnabled": false, + "name": "price_streams", + "entityType": "tables", + "schema": "cs" + }, + { + "isRlsEnabled": false, + "name": "alert_rules", + "entityType": "tables", + "schema": "house" + }, + { + "isRlsEnabled": false, + "name": "listings", + "entityType": "tables", + "schema": "house" + }, + { + "isRlsEnabled": false, + "name": "price_history", + "entityType": "tables", + "schema": "house" + }, + { + "isRlsEnabled": false, + "name": "searches", + "entityType": "tables", + "schema": "house" + }, + { + "isRlsEnabled": false, + "name": "worker_runs", + "entityType": "tables", + "schema": "house" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "issuer", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "auth", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'user'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "auth", + "table": "verification" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "blog_article_status", + "typeSchema": "site", + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "site", + "table": "blog_articles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "current_points", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "total_points", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "site", + "table": "tasks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "site", + "table": "task_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "task_id", + "entityType": "columns", + "schema": "site", + "table": "task_topics" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "topic_id", + "entityType": "columns", + "schema": "site", + "table": "task_topics" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "site", + "table": "task_topics" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "site", + "table": "task_topics" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "site", + "table": "topics" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "site", + "table": "topics" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "emoji", + "entityType": "columns", + "schema": "site", + "table": "topics" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "site", + "table": "topics" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "site", + "table": "topics" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sort_order", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "breeding", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_games" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type1", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type2", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "hp", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attack", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "defense", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "special_attack", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "special_defense", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "speed", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "evolves_from", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "evolution_chain", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_baby", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_legendary", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_mythical", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "location_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "area", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "min_level", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "max_level", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rarity", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "form", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "conditions", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pokeapi'", + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "from_pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "to_pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pokeapi'", + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "availability", + "typeSchema": "ref", + "notNull": true, + "dimensions": 0, + "default": "'catchable'", + "generated": null, + "identity": null, + "name": "availability", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "note", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pokeapi'", + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "region", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_key", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_credit", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pokeapi'", + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_locations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_game_entry_id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "franchise", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "short_name", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "release_year", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sort_order", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rules", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "tracker_games" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "post_launch", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sort_order", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "ref", + "table": "tracker_items" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "item_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'done'", + "generated": null, + "identity": null, + "name": "milestone", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_progress" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'osu'", + "generated": null, + "identity": null, + "name": "mode", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "beatmap_url", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "score_url", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pass'", + "generated": null, + "identity": null, + "name": "goal", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'stamina'", + "generated": null, + "identity": null, + "name": "skill", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bpm", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "duration", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accuracy", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "combo", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "misses", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "numeric", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pp", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'NM'", + "generated": null, + "identity": null, + "name": "mods", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "numeric(4,2)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1.00", + "generated": null, + "identity": null, + "name": "rate", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "achieved", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "date", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "played_on", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "osu_entries" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "nickname", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "level", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "moves", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "party_slot", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "shiny", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nature", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ability", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ball", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'caught'", + "generated": null, + "identity": null, + "name": "obtained", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'story'", + "generated": null, + "identity": null, + "name": "stage", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "date", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "caught_on", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_catches" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dex", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dex_name", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "number", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_research" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_group", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_research" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "regions", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_game_research" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'story'", + "generated": null, + "identity": null, + "name": "stage", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version_group", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "moves", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'story'", + "generated": null, + "identity": null, + "name": "stage", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "done", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "power", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accuracy", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pp", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_moves" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_research" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "facts", + "entityType": "columns", + "schema": "ref", + "table": "pokemon_research" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pokemon_id", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "pokemon_seen" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "current", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "done", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "requirement", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reward", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "target", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_url", + "entityType": "columns", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "game_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "level", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "prestige", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_ranks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "item_id", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "level", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "prestige", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "species", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "breed", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animal_name", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ai_breed_suggestion", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ai_breed_confidence", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "submitted_at", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted_at", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "denied_at", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "animaldex", + "table": "animals" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "sighting_id", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "r2_key", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sort_order", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "uploaded_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animal_id", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reporter_name", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seen_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lat", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lng", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "submitted_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "accepted_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "denied_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "animaldex", + "table": "sightings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "byDefault", + "name": "columns_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "table_id", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "column_name", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ui_type", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "options", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hidden", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "help_text", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "meta", + "table": "columns" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "byDefault", + "name": "tables_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schema_name", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "table_name", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "display_name", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "picture_url", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "editable", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "has_done", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "default_sort", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "sort_order", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "archived_at", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "meta", + "table": "tables" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "opening_balance_cents", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'spendable'", + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "accounts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "date", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount_cents", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "forecasts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverage", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "premium_cents", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'monthly'", + "generated": null, + "identity": null, + "name": "cadence", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "insurances" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "investment_account_id", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "100000", + "generated": null, + "identity": null, + "name": "reserve_cents", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "numeric(5,2)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'5.00'", + "generated": null, + "identity": null, + "name": "annual_return_percent", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'excess'", + "generated": null, + "identity": null, + "name": "contribution_mode", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "include_recurring", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "fixed_monthly_cents", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "date", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "end_date", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "projection_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount_cents", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cadence", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "date", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "starts_on", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "date", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ends_on", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "recurring" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "date", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "amount_cents", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "transactions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "date", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "date", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "numeric(12,3)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reading", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "utility_readings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "numeric(12,4)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'0'", + "generated": null, + "identity": null, + "name": "rate_per_unit", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "monthly_advance_cents", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "date", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "billing_start", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "utility_settings" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "estimated_cents", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "money", + "table": "wishlist" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "item_id", + "entityType": "columns", + "schema": "cs", + "table": "item_dimensions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "cs", + "table": "item_dimensions" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "cs", + "table": "item_dimensions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "market_hash_name", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon_url", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "item_type", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "first_seen_at", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "last_seen_at", + "entityType": "columns", + "schema": "cs", + "table": "items" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "price_observations_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "item_id", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "observed_at", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "price_cents", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "volume", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "imported_at", + "entityType": "columns", + "schema": "cs", + "table": "price_observations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "cs", + "table": "price_streams" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "currency", + "entityType": "columns", + "schema": "cs", + "table": "price_streams" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "cs", + "table": "price_streams" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'rent'", + "generated": null, + "identity": null, + "name": "offer", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "min_price", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "max_price", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "min_area", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "min_bedrooms", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "cities", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "property_types", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "house", + "table": "alert_rules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_id", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "external_id", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "offer", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "property_type", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "street", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "postal_code", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "city", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "city_slug", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "neighbourhood", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "price_eur", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "area_m2", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rooms", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bedrooms", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bathrooms", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "construction_year", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "energy_label", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "interior", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deposit_eur", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "available_from", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contract_type", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "agent", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image_url", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "features", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lat", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "double precision", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lng", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "offered_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "first_seen_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'slow'", + "generated": null, + "identity": null, + "name": "first_seen_lane", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "last_seen_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "detail_fetched_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "removed_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "alerted_at", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "notes", + "entityType": "columns", + "schema": "house", + "table": "listings" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "listing_id", + "entityType": "columns", + "schema": "house", + "table": "price_history" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "observed_at", + "entityType": "columns", + "schema": "house", + "table": "price_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "price_eur", + "entityType": "columns", + "schema": "house", + "table": "price_history" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "search_url", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lane", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "first_synced_at", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "last_synced_at", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "last_found", + "entityType": "columns", + "schema": "house", + "table": "searches" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "worker_runs_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lane", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "http_status", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "found", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "duration_ms", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "message", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "house", + "table": "worker_runs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "issuer", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_issuer_account_id_unique", + "entityType": "indexes", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "auth", + "table": "verification" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "pokemon_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_encounters_pokemon_id_game_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "location_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_encounters_location_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_evolutions_game_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_game_entries_game_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "catch_em_all_entries_user_id_index", + "entityType": "indexes", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "catch_em_all_games_user_id_index", + "entityType": "indexes", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tracker_items_game_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "tracker_items" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "tracker_progress_user_id_index", + "entityType": "indexes", + "schema": "games", + "table": "tracker_progress" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "kind", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "osu_entries_user_id_kind_index", + "entityType": "indexes", + "schema": "games", + "table": "osu_entries" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "pokemon_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_catches_user_id_game_id_pokemon_id_index", + "entityType": "indexes", + "schema": "games", + "table": "pokemon_catches" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "game_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_dex_numbers_game_id_index", + "entityType": "indexes", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "pokemon_seen_user_id_index", + "entityType": "indexes", + "schema": "games", + "table": "pokemon_seen" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "table_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "columns_table_id_index", + "entityType": "indexes", + "schema": "meta", + "table": "columns" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "accounts_user_id_index", + "entityType": "indexes", + "schema": "money", + "table": "accounts" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "forecasts_user_id_date_index", + "entityType": "indexes", + "schema": "money", + "table": "forecasts" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "insurances_user_id_index", + "entityType": "indexes", + "schema": "money", + "table": "insurances" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "recurring_user_id_index", + "entityType": "indexes", + "schema": "money", + "table": "recurring" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "transactions_user_id_date_id_index", + "entityType": "indexes", + "schema": "money", + "table": "transactions" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "account_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "transactions_account_id_index", + "entityType": "indexes", + "schema": "money", + "table": "transactions" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "kind", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "date", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "utility_readings_user_id_kind_date_index", + "entityType": "indexes", + "schema": "money", + "table": "utility_readings" + }, + { + "nameExplicit": false, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "wishlist_user_id_index", + "entityType": "indexes", + "schema": "money", + "table": "wishlist" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "value", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "item_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "item_dimensions_filter_idx", + "entityType": "indexes", + "schema": "cs", + "table": "item_dimensions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "market_hash_name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "items_hash_idx", + "entityType": "indexes", + "schema": "cs", + "table": "items" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "items_name_idx", + "entityType": "indexes", + "schema": "cs", + "table": "items" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "item_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "currency", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "observed_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "kind", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "price_observations_unique_idx", + "entityType": "indexes", + "schema": "cs", + "table": "price_observations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "item_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "currency", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "observed_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "price_observations_lookup_idx", + "entityType": "indexes", + "schema": "cs", + "table": "price_observations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "observed_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "price_observations_time_idx", + "entityType": "indexes", + "schema": "cs", + "table": "price_observations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "source_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "listings_source_idx", + "entityType": "indexes", + "schema": "house", + "table": "listings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "offer", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "first_seen_at", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": "removed_at IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "listings_active_idx", + "entityType": "indexes", + "schema": "house", + "table": "listings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "offer", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "city_slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "listings_city_idx", + "entityType": "indexes", + "schema": "house", + "table": "listings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "first_seen_at", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": "detail_fetched_at IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "listings_details_idx", + "entityType": "indexes", + "schema": "house", + "table": "listings" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "worker_runs_created_idx", + "entityType": "indexes", + "schema": "house", + "table": "worker_runs" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "auth", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "task_id" + ], + "schemaTo": "site", + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "task_topics_task_id_tasks_id_fkey", + "entityType": "fks", + "schema": "site", + "table": "task_topics" + }, + { + "nameExplicit": false, + "columns": [ + "topic_id" + ], + "schemaTo": "site", + "tableTo": "topics", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "task_topics_topic_id_topics_id_fkey", + "entityType": "fks", + "schema": "site", + "table": "task_topics" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "catch_em_all_settings_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "catch_em_all_settings" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_encounters_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_encounters_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "nameExplicit": false, + "columns": [ + "location_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_locations", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_encounters_location_id_pokemon_locations_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_encounters" + }, + { + "nameExplicit": false, + "columns": [ + "from_pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_evolutions_from_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "nameExplicit": false, + "columns": [ + "to_pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_evolutions_to_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_evolutions_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_game_entries_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_game_entries_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_game_entry_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_game_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "catch_em_all_entries_HeABXvGNiB6D_fkey", + "entityType": "fks", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "catch_em_all_entries_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "catch_em_all_games_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "catch_em_all_games_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_items_game_id_tracker_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "tracker_items" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_progress_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_progress" + }, + { + "nameExplicit": false, + "columns": [ + "item_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_items", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_progress_item_id_tracker_items_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_progress" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "osu_entries_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "osu_entries" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_catches_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_catches_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_catches_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_dex_numbers_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_dex_numbers_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_game_research_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_game_research" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_journeys_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_journeys_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_learnsets_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_milestones_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_milestones_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_research_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "pokemon_research" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_seen_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_seen" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_seen_game_id_pokemon_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_seen" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id" + ], + "schemaTo": "ref", + "tableTo": "pokemon", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "pokemon_seen_pokemon_id_pokemon_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "pokemon_seen" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_challenge_progress_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "nameExplicit": false, + "columns": [ + "challenge_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_challenges", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_challenge_progress_HtzyFtOyUAhk_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_challenges_game_id_tracker_games_id_fkey", + "entityType": "fks", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_ranks_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_ranks" + }, + { + "nameExplicit": false, + "columns": [ + "game_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_games", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_ranks_game_id_tracker_games_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_ranks" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_weapon_ranks_user_id_user_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "nameExplicit": false, + "columns": [ + "item_id" + ], + "schemaTo": "ref", + "tableTo": "tracker_items", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "tracker_weapon_ranks_item_id_tracker_items_id_fkey", + "entityType": "fks", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "nameExplicit": false, + "columns": [ + "sighting_id" + ], + "schemaTo": "animaldex", + "tableTo": "sightings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sighting_photos_sighting_id_sightings_id_fkey", + "entityType": "fks", + "schema": "animaldex", + "table": "sighting_photos" + }, + { + "nameExplicit": false, + "columns": [ + "animal_id" + ], + "schemaTo": "animaldex", + "tableTo": "animals", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "sightings_animal_id_animals_id_fkey", + "entityType": "fks", + "schema": "animaldex", + "table": "sightings" + }, + { + "nameExplicit": false, + "columns": [ + "table_id" + ], + "schemaTo": "meta", + "tableTo": "tables", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "columns_table_id_tables_id_fkey", + "entityType": "fks", + "schema": "meta", + "table": "columns" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "accounts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "accounts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "forecasts_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "forecasts" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "money", + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "forecasts_account_id_accounts_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "forecasts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "insurances_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "insurances" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "projection_settings_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "projection_settings" + }, + { + "nameExplicit": false, + "columns": [ + "investment_account_id" + ], + "schemaTo": "money", + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "projection_settings_investment_account_id_accounts_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "projection_settings" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "recurring_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "recurring" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "money", + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "recurring_account_id_accounts_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "recurring" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "transactions_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "transactions" + }, + { + "nameExplicit": false, + "columns": [ + "account_id" + ], + "schemaTo": "money", + "tableTo": "accounts", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "transactions_account_id_accounts_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "transactions" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "utility_readings_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "utility_readings" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "utility_settings_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "utility_settings" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "auth", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "wishlist_user_id_user_id_fkey", + "entityType": "fks", + "schema": "money", + "table": "wishlist" + }, + { + "nameExplicit": false, + "columns": [ + "item_id" + ], + "schemaTo": "cs", + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "item_dimensions_item_id_items_id_fkey", + "entityType": "fks", + "schema": "cs", + "table": "item_dimensions" + }, + { + "nameExplicit": false, + "columns": [ + "item_id" + ], + "schemaTo": "cs", + "tableTo": "items", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "price_observations_item_id_items_id_fkey", + "entityType": "fks", + "schema": "cs", + "table": "price_observations" + }, + { + "nameExplicit": false, + "columns": [ + "listing_id" + ], + "schemaTo": "house", + "tableTo": "listings", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "price_history_listing_id_listings_id_fkey", + "entityType": "fks", + "schema": "house", + "table": "price_history" + }, + { + "columns": [ + "item_id", + "key" + ], + "nameExplicit": true, + "name": "item_dimensions_pkey", + "entityType": "pks", + "schema": "cs", + "table": "item_dimensions" + }, + { + "columns": [ + "source", + "currency", + "kind" + ], + "nameExplicit": true, + "name": "price_streams_pkey", + "entityType": "pks", + "schema": "cs", + "table": "price_streams" + }, + { + "columns": [ + "listing_id", + "observed_at" + ], + "nameExplicit": true, + "name": "price_history_pkey", + "entityType": "pks", + "schema": "house", + "table": "price_history" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "auth", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "auth", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "auth", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "auth", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_articles_pkey", + "schema": "site", + "table": "blog_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tasks_pkey", + "schema": "site", + "table": "tasks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "task_topics_pkey", + "schema": "site", + "table": "task_topics", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "topics_pkey", + "schema": "site", + "table": "topics", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "catch_em_all_settings_pkey", + "schema": "games", + "table": "catch_em_all_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_games_pkey", + "schema": "ref", + "table": "pokemon_games", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_pkey", + "schema": "ref", + "table": "pokemon", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_encounters_pkey", + "schema": "ref", + "table": "pokemon_encounters", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_evolutions_pkey", + "schema": "ref", + "table": "pokemon_evolutions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_game_entries_pkey", + "schema": "ref", + "table": "pokemon_game_entries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_locations_pkey", + "schema": "ref", + "table": "pokemon_locations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "catch_em_all_entries_pkey", + "schema": "games", + "table": "catch_em_all_entries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "catch_em_all_games_pkey", + "schema": "games", + "table": "catch_em_all_games", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_games_pkey", + "schema": "ref", + "table": "tracker_games", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_items_pkey", + "schema": "ref", + "table": "tracker_items", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_progress_pkey", + "schema": "games", + "table": "tracker_progress", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "osu_entries_pkey", + "schema": "games", + "table": "osu_entries", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_catches_pkey", + "schema": "games", + "table": "pokemon_catches", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_dex_numbers_pkey", + "schema": "ref", + "table": "pokemon_dex_numbers", + "entityType": "pks" + }, + { + "columns": [ + "game_id" + ], + "nameExplicit": false, + "name": "pokemon_game_research_pkey", + "schema": "ref", + "table": "pokemon_game_research", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_journeys_pkey", + "schema": "games", + "table": "pokemon_journeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_learnsets_pkey", + "schema": "ref", + "table": "pokemon_learnsets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_milestones_pkey", + "schema": "games", + "table": "pokemon_milestones", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_moves_pkey", + "schema": "ref", + "table": "pokemon_moves", + "entityType": "pks" + }, + { + "columns": [ + "pokemon_id" + ], + "nameExplicit": false, + "name": "pokemon_research_pkey", + "schema": "ref", + "table": "pokemon_research", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "pokemon_seen_pkey", + "schema": "games", + "table": "pokemon_seen", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_challenge_progress_pkey", + "schema": "games", + "table": "tracker_challenge_progress", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_challenges_pkey", + "schema": "ref", + "table": "tracker_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_ranks_pkey", + "schema": "games", + "table": "tracker_ranks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tracker_weapon_ranks_pkey", + "schema": "games", + "table": "tracker_weapon_ranks", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "animals_pkey", + "schema": "animaldex", + "table": "animals", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sighting_photos_pkey", + "schema": "animaldex", + "table": "sighting_photos", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sightings_pkey", + "schema": "animaldex", + "table": "sightings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "columns_pkey", + "schema": "meta", + "table": "columns", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tables_pkey", + "schema": "meta", + "table": "tables", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "accounts_pkey", + "schema": "money", + "table": "accounts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "forecasts_pkey", + "schema": "money", + "table": "forecasts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "insurances_pkey", + "schema": "money", + "table": "insurances", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "projection_settings_pkey", + "schema": "money", + "table": "projection_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "recurring_pkey", + "schema": "money", + "table": "recurring", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "transactions_pkey", + "schema": "money", + "table": "transactions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "utility_readings_pkey", + "schema": "money", + "table": "utility_readings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "utility_settings_pkey", + "schema": "money", + "table": "utility_settings", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "wishlist_pkey", + "schema": "money", + "table": "wishlist", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "items_pkey", + "schema": "cs", + "table": "items", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "price_observations_pkey", + "schema": "cs", + "table": "price_observations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "alert_rules_pkey", + "schema": "house", + "table": "alert_rules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "listings_pkey", + "schema": "house", + "table": "listings", + "entityType": "pks" + }, + { + "columns": [ + "search_url" + ], + "nameExplicit": false, + "name": "searches_pkey", + "schema": "house", + "table": "searches", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "worker_runs_pkey", + "schema": "house", + "table": "worker_runs", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "blog_articles_slug_unique", + "entityType": "uniques", + "schema": "site", + "table": "blog_articles" + }, + { + "nameExplicit": false, + "columns": [ + "task_id", + "topic_id" + ], + "nullsNotDistinct": false, + "name": "task_topics_task_id_topic_id_unique", + "entityType": "uniques", + "schema": "site", + "table": "task_topics" + }, + { + "nameExplicit": false, + "columns": [ + "from_pokemon_id", + "to_pokemon_id", + "game_id", + "method" + ], + "nullsNotDistinct": false, + "name": "pokemon_evolutions_from_pokemon_id_to_pokemon_id_game_id_method_unique", + "entityType": "uniques", + "schema": "ref", + "table": "pokemon_evolutions" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id", + "game_id" + ], + "nullsNotDistinct": false, + "name": "pokemon_game_entries_pokemon_id_game_id_unique", + "entityType": "uniques", + "schema": "ref", + "table": "pokemon_game_entries" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_game_entry_id", + "user_id" + ], + "nullsNotDistinct": false, + "name": "catch_em_all_entries_pokemon_game_entry_id_user_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "catch_em_all_entries" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id" + ], + "nullsNotDistinct": false, + "name": "catch_em_all_games_user_id_game_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "catch_em_all_games" + }, + { + "nameExplicit": false, + "columns": [ + "game_id", + "key" + ], + "nullsNotDistinct": false, + "name": "tracker_items_game_id_key_unique", + "entityType": "uniques", + "schema": "ref", + "table": "tracker_items" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "item_id", + "milestone" + ], + "nullsNotDistinct": false, + "name": "tracker_progress_user_id_item_id_milestone_unique", + "entityType": "uniques", + "schema": "games", + "table": "tracker_progress" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id", + "party_slot" + ], + "nullsNotDistinct": false, + "name": "pokemon_catches_user_id_game_id_party_slot_unique", + "entityType": "uniques", + "schema": "games", + "table": "pokemon_catches" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id", + "game_id", + "dex" + ], + "nullsNotDistinct": false, + "name": "pokemon_dex_numbers_pokemon_id_game_id_dex_unique", + "entityType": "uniques", + "schema": "ref", + "table": "pokemon_dex_numbers" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id" + ], + "nullsNotDistinct": false, + "name": "pokemon_journeys_user_id_game_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "nameExplicit": false, + "columns": [ + "pokemon_id", + "version_group" + ], + "nullsNotDistinct": false, + "name": "pokemon_learnsets_pokemon_id_version_group_unique", + "entityType": "uniques", + "schema": "ref", + "table": "pokemon_learnsets" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id", + "key" + ], + "nullsNotDistinct": false, + "name": "pokemon_milestones_user_id_game_id_key_unique", + "entityType": "uniques", + "schema": "games", + "table": "pokemon_milestones" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id", + "pokemon_id" + ], + "nullsNotDistinct": false, + "name": "pokemon_seen_user_id_game_id_pokemon_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "pokemon_seen" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "challenge_id" + ], + "nullsNotDistinct": false, + "name": "tracker_challenge_progress_user_id_challenge_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "nameExplicit": false, + "columns": [ + "game_id", + "key" + ], + "nullsNotDistinct": false, + "name": "tracker_challenges_game_id_key_unique", + "entityType": "uniques", + "schema": "ref", + "table": "tracker_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "game_id" + ], + "nullsNotDistinct": false, + "name": "tracker_ranks_user_id_game_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "tracker_ranks" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "item_id" + ], + "nullsNotDistinct": false, + "name": "tracker_weapon_ranks_user_id_item_id_unique", + "entityType": "uniques", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "nameExplicit": true, + "columns": [ + "table_id", + "column_name" + ], + "nullsNotDistinct": false, + "name": "columns_table_id_column_name_key", + "entityType": "uniques", + "schema": "meta", + "table": "columns" + }, + { + "nameExplicit": true, + "columns": [ + "schema_name", + "table_name" + ], + "nullsNotDistinct": false, + "name": "tables_schema_name_table_name_key", + "entityType": "uniques", + "schema": "meta", + "table": "tables" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "name" + ], + "nullsNotDistinct": false, + "name": "accounts_user_id_name_unique", + "entityType": "uniques", + "schema": "money", + "table": "accounts" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "nullsNotDistinct": false, + "name": "projection_settings_user_id_unique", + "entityType": "uniques", + "schema": "money", + "table": "projection_settings" + }, + { + "nameExplicit": false, + "columns": [ + "user_id", + "kind" + ], + "nullsNotDistinct": false, + "name": "utility_settings_user_id_kind_unique", + "entityType": "uniques", + "schema": "money", + "table": "utility_settings" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "auth", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "auth", + "table": "user", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "nullsNotDistinct": false, + "name": "catch_em_all_settings_user_id_key", + "schema": "games", + "table": "catch_em_all_settings", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "nullsNotDistinct": false, + "name": "pokemon_games_source_id_key", + "schema": "ref", + "table": "pokemon_games", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "pokemon_games_slug_key", + "schema": "ref", + "table": "pokemon_games", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "number" + ], + "nullsNotDistinct": false, + "name": "pokemon_number_key", + "schema": "ref", + "table": "pokemon", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "source_id" + ], + "nullsNotDistinct": false, + "name": "pokemon_locations_source_id_key", + "schema": "ref", + "table": "pokemon_locations", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "tracker_games_slug_key", + "schema": "ref", + "table": "tracker_games", + "entityType": "uniques" + }, + { + "nameExplicit": true, + "columns": [ + "slug" + ], + "nullsNotDistinct": false, + "name": "tables_slug_key", + "schema": "meta", + "table": "tables", + "entityType": "uniques" + }, + { + "value": "\"kind\" IN ('score','goal','training')", + "name": "osu_entries_kind_check", + "entityType": "checks", + "schema": "games", + "table": "osu_entries" + }, + { + "value": "\"level\" between 1 and 100", + "name": "pokemon_catches_level_check", + "entityType": "checks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "value": "\"party_slot\" between 1 and 6", + "name": "pokemon_catches_party_slot_check", + "entityType": "checks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "value": "cardinality(\"moves\") <= 4", + "name": "pokemon_catches_moves_check", + "entityType": "checks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "value": "\"stage\" IN ('story','postgame','collection')", + "name": "pokemon_catches_stage_check", + "entityType": "checks", + "schema": "games", + "table": "pokemon_catches" + }, + { + "value": "\"stage\" IN ('story','postgame','collection')", + "name": "pokemon_journeys_stage_check", + "entityType": "checks", + "schema": "games", + "table": "pokemon_journeys" + }, + { + "value": "\"current\" >= 0", + "name": "tracker_challenge_progress_current_check", + "entityType": "checks", + "schema": "games", + "table": "tracker_challenge_progress" + }, + { + "value": "\"level\" between 1 and 10000", + "name": "tracker_ranks_level_check", + "entityType": "checks", + "schema": "games", + "table": "tracker_ranks" + }, + { + "value": "\"prestige\" between 0 and 1000", + "name": "tracker_ranks_prestige_check", + "entityType": "checks", + "schema": "games", + "table": "tracker_ranks" + }, + { + "value": "\"level\" between 1 and 10000", + "name": "tracker_weapon_ranks_level_check", + "entityType": "checks", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "value": "\"prestige\" between 0 and 1000", + "name": "tracker_weapon_ranks_prestige_check", + "entityType": "checks", + "schema": "games", + "table": "tracker_weapon_ranks" + }, + { + "value": "ui_type IN ('text','longtext','url','image','number','cents','date','datetime','bool','select','multiselect','fk','json')", + "name": "columns_ui_type_check", + "entityType": "checks", + "schema": "meta", + "table": "columns" + }, + { + "value": "slug ~ '^[a-z0-9][a-z0-9-]{0,62}$'", + "name": "tables_slug_check", + "entityType": "checks", + "schema": "meta", + "table": "tables" + }, + { + "value": "kind IN ('list','core','ref')", + "name": "tables_kind_check", + "entityType": "checks", + "schema": "meta", + "table": "tables" + }, + { + "value": "kind IN ('spendable','investment')", + "name": "accounts_kind_check", + "entityType": "checks", + "schema": "money", + "table": "accounts" + }, + { + "value": "cadence IN ('weekly','monthly','yearly')", + "name": "insurances_cadence_check", + "entityType": "checks", + "schema": "money", + "table": "insurances" + }, + { + "value": "contribution_mode IN ('excess','fixed')", + "name": "projection_settings_contribution_mode_check", + "entityType": "checks", + "schema": "money", + "table": "projection_settings" + }, + { + "value": "cadence IN ('weekly','monthly','yearly')", + "name": "recurring_cadence_check", + "entityType": "checks", + "schema": "money", + "table": "recurring" + }, + { + "value": "kind IN ('electricity','gas','water')", + "name": "utility_readings_kind_check", + "entityType": "checks", + "schema": "money", + "table": "utility_readings" + }, + { + "value": "kind IN ('electricity','gas','water')", + "name": "utility_settings_kind_check", + "entityType": "checks", + "schema": "money", + "table": "utility_settings" + }, + { + "value": "offer IN ('rent', 'buy')", + "name": "alert_rules_offer_check", + "entityType": "checks", + "schema": "house", + "table": "alert_rules" + }, + { + "value": "offer IN ('rent', 'buy')", + "name": "listings_offer_check", + "entityType": "checks", + "schema": "house", + "table": "listings" + }, + { + "value": "price_eur IS NULL OR price_eur >= 0", + "name": "listings_price_eur_check", + "entityType": "checks", + "schema": "house", + "table": "listings" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/scripts/db-migrate.ts b/scripts/db-migrate.ts new file mode 100644 index 0000000..1d4806c --- /dev/null +++ b/scripts/db-migrate.ts @@ -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(); +} diff --git a/src/lib/server/db/relations.ts b/src/lib/server/db/relations.ts index 6783e6e..6192d57 100644 --- a/src/lib/server/db/relations.ts +++ b/src/lib/server/db/relations.ts @@ -7,8 +7,7 @@ export const relations = defineRelations(schema, (r) => ({ sessions: r.many.session(), accounts: r.many.account(), entries: r.many.userEntries(), - games: r.many.userGames(), - listCompletions: r.many.listItemCompletions() + games: r.many.userGames() }, session: { user: r.one.user({ @@ -138,25 +137,14 @@ export const relations = defineRelations(schema, (r) => ({ }) }, - // ── Lists ───────────────────────────────────── - lists: { - items: r.many.listItems() + // ── Meta registry (lists live in schema `lists`, see src/lib/server/lists.ts) + metaTables: { + columns: r.many.metaColumns() }, - listItems: { - list: r.one.lists({ - from: r.listItems.listId, - to: r.lists.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 + metaColumns: { + table: r.one.metaTables({ + from: r.metaColumns.tableId, + to: r.metaTables.id }) } })); diff --git a/src/lib/server/db/schema/animaldex.ts b/src/lib/server/db/schema/animaldex.ts index fd27f70..53fa495 100644 --- a/src/lib/server/db/schema/animaldex.ts +++ b/src/lib/server/db/schema/animaldex.ts @@ -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'; -export const animals = pgTable('animals', { +export const animals = animaldexSchema.table('animals', { id: id(), species: text('species').notNull(), @@ -22,7 +23,7 @@ export const animals = pgTable('animals', { ...timestamps }); -export const sightings = pgTable('sightings', { +export const sightings = animaldexSchema.table('sightings', { id: id(), animalId: integer('animal_id') .notNull() @@ -40,7 +41,7 @@ export const sightings = pgTable('sightings', { ...timestamps }); -export const photos = pgTable('photos', { +export const photos = animaldexSchema.table('sighting_photos', { id: id(), sightingId: integer('sighting_id') .notNull() diff --git a/src/lib/server/db/schema/auth.ts b/src/lib/server/db/schema/auth.ts index 9438bd4..3c81461 100644 --- a/src/lib/server/db/schema/auth.ts +++ b/src/lib/server/db/schema/auth.ts @@ -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(), name: text('name').notNull(), role: text('role').notNull().default('user'), @@ -14,7 +15,7 @@ export const user = pgTable('user', { .notNull() }); -export const session = pgTable( +export const session = authSchema.table( 'session', { id: text('id').primaryKey(), @@ -33,7 +34,7 @@ export const session = pgTable( (table) => [index('session_userId_idx').on(table.userId)] ); -export const account = pgTable( +export const account = authSchema.table( 'account', { id: text('id').primaryKey(), @@ -61,7 +62,7 @@ export const account = pgTable( ] ); -export const verification = pgTable( +export const verification = authSchema.table( 'verification', { id: text('id').primaryKey(), diff --git a/src/lib/server/db/schema/blog.ts b/src/lib/server/db/schema/blog.ts index a3479d3..25720bd 100644 --- a/src/lib/server/db/schema/blog.ts +++ b/src/lib/server/db/schema/blog.ts @@ -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'; -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', { id: id(), diff --git a/src/lib/server/db/schema/catch-em-all.ts b/src/lib/server/db/schema/catch-em-all.ts index 9448bf8..17d883d 100644 --- a/src/lib/server/db/schema/catch-em-all.ts +++ b/src/lib/server/db/schema/catch-em-all.ts @@ -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 { user } from './auth'; 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 // but never touches `manual` ones, which are admin corrections. export const catalogSources = ['pokeapi', 'derived', 'manual'] as const; const source = () => text('source', { enum: catalogSources }).notNull().default('pokeapi'); -export const pokemon = pgTable('pokemon', { +export const pokemon = refSchema.table('pokemon', { id: id(), number: integer('number').notNull().unique(), generation: integer('generation').notNull().default(1), @@ -32,7 +33,7 @@ export const pokemon = pgTable('pokemon', { ...timestamps }); -export const games = pgTable('games', { +export const games = refSchema.table('pokemon_games', { id: id(), sourceId: integer('source_id').unique(), slug: text('slug').unique(), @@ -43,7 +44,7 @@ export const games = pgTable('games', { ...timestamps }); -export const pokemonGameEntries = pgTable( +export const pokemonGameEntries = refSchema.table( 'pokemon_game_entries', { id: id(), @@ -62,7 +63,7 @@ export const pokemonGameEntries = pgTable( (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(), sourceId: integer('source_id').unique(), slug: text('slug').notNull(), @@ -76,7 +77,7 @@ export const pokemonLocations = pgTable('pokemon_locations', { ...timestamps }); -export const pokemonEncounters = pgTable( +export const pokemonEncounters = refSchema.table( 'pokemon_encounters', { id: id(), @@ -106,7 +107,7 @@ export const pokemonEncounters = pgTable( // The method that applies in each game, e.g. Magneton → Magnezone needs Mt. // Coronet in Platinum but a Thunder Stone in Sword. -export const pokemonEvolutions = pgTable( +export const pokemonEvolutions = refSchema.table( 'pokemon_evolutions', { id: id(), @@ -129,8 +130,8 @@ export const pokemonEvolutions = pgTable( ] ); -export const userEntries = pgTable( - 'user_entries', +export const userEntries = gamesSchema.table( + 'catch_em_all_entries', { id: id(), pokemonGameEntryId: integer('pokemon_game_entry_id') @@ -145,8 +146,8 @@ export const userEntries = pgTable( ); // which games a user has chosen to track ("rules") -export const userGames = pgTable( - 'user_games', +export const userGames = gamesSchema.table( + 'catch_em_all_games', { id: 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. -export const catchEmAllSettings = pgTable('catch_em_all_settings', { +export const catchEmAllSettings = gamesSchema.table('catch_em_all_settings', { id: id(), userId: text('user_id') .notNull() diff --git a/src/lib/server/db/schema/cs-prices.ts b/src/lib/server/db/schema/cs-prices.ts index 2c2afe4..9bf8f52 100644 --- a/src/lib/server/db/schema/cs-prices.ts +++ b/src/lib/server/db/schema/cs-prices.ts @@ -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'; // A Steam market hash name identifies a listing, including wear and special variants. -export const csMarketItems = pgTable( - 'cs_market_items', +export const csMarketItems = csSchema.table( + 'items', { id: id(), marketHashName: text('market_hash_name').notNull(), @@ -14,14 +23,14 @@ export const csMarketItems = pgTable( lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ - uniqueIndex('cs_market_items_hash_idx').on(table.marketHashName), - index('cs_market_items_name_idx').on(table.name) + uniqueIndex('items_hash_idx').on(table.marketHashName), + index('items_name_idx').on(table.name) ] ); // Keep source observations intact. Aggregation happens only when the chart is requested. -export const csPriceObservations = pgTable( - 'cs_price_observations', +export const csPriceObservations = csSchema.table( + 'price_observations', { id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(), itemId: integer('item_id') @@ -36,25 +45,25 @@ export const csPriceObservations = pgTable( importedAt: timestamp('imported_at', { withTimezone: true }).notNull().defaultNow() }, (table) => [ - uniqueIndex('cs_price_observations_unique_idx').on( + uniqueIndex('price_observations_unique_idx').on( table.itemId, table.source, table.currency, table.observedAt, table.kind ), - index('cs_price_observations_lookup_idx').on( + index('price_observations_lookup_idx').on( table.itemId, table.source, table.currency, table.observedAt ), - index('cs_price_observations_time_idx').on(table.observedAt) + index('price_observations_time_idx').on(table.observedAt) ] ); -export const csItemDimensions = pgTable( - 'cs_item_dimensions', +export const csItemDimensions = csSchema.table( + 'item_dimensions', { itemId: integer('item_id') .notNull() @@ -63,19 +72,19 @@ export const csItemDimensions = pgTable( value: text('value').notNull() }, (table) => [ - uniqueIndex('cs_item_dimensions_unique_idx').on(table.itemId, table.key), - index('cs_item_dimensions_filter_idx').on(table.key, table.value, table.itemId) + primaryKey({ name: 'item_dimensions_pkey', columns: [table.itemId, table.key] }), + index('item_dimensions_filter_idx').on(table.key, table.value, table.itemId) ] ); -export const csPriceStreams = pgTable( - 'cs_price_streams', +export const csPriceStreams = csSchema.table( + 'price_streams', { source: text('source').notNull(), currency: text('currency').notNull(), kind: text('kind').notNull() }, (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] }) ] ); diff --git a/src/lib/server/db/schema/game-tracker.ts b/src/lib/server/db/schema/game-tracker.ts index 0f42c4b..a607cb5 100644 --- a/src/lib/server/db/schema/game-tracker.ts +++ b/src/lib/server/db/schema/game-tracker.ts @@ -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 { user } from './auth'; import { id, timestamps } from './helpers'; // Checklist-style games (Call of Duty and future franchises). Pokémon keeps its // own species × game model in catch-em-all.ts. -export const trackerGames = pgTable('tracker_games', { +export const trackerGames = refSchema.table('tracker_games', { id: id(), slug: text('slug').notNull().unique(), franchise: text('franchise').notNull(), @@ -17,7 +18,7 @@ export const trackerGames = pgTable('tracker_games', { ...timestamps }); -export const trackerItems = pgTable( +export const trackerItems = refSchema.table( 'tracker_items', { id: id(), @@ -41,7 +42,7 @@ export const trackerItems = pgTable( // One row per completed milestone: `done` for achievements, or a weapon // milestone key such as `gold` or `maxLevel`. Derived tiers are never stored. -export const trackerProgress = pgTable( +export const trackerProgress = gamesSchema.table( 'tracker_progress', { id: id(), diff --git a/src/lib/server/db/schema/house-tracker.ts b/src/lib/server/db/schema/house-tracker.ts index 2fefd8b..24d72a5 100644 --- a/src/lib/server/db/schema/house-tracker.ts +++ b/src/lib/server/db/schema/house-tracker.ts @@ -6,16 +6,16 @@ import { index, integer, jsonb, - pgTable, + primaryKey, text, uniqueIndex } from 'drizzle-orm/pg-core'; +import { houseSchema } from './schemas'; import { sql } from 'drizzle-orm'; import { id, timestamptz } from './helpers'; -// Created by scripts/house-tracker-migration.sql; keep both in sync. -export const houseListings = pgTable( - 'house_listings', +export const houseListings = houseSchema.table( + 'listings', { id: id(), source: text('source').notNull(), @@ -58,21 +58,21 @@ export const houseListings = pgTable( notes: text('notes') }, (table) => [ - check('house_listings_offer_check', sql`offer IN ('rent', 'buy')`), - check('house_listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`), - uniqueIndex('house_listings_source_idx').on(table.source, table.sourceId), - index('house_listings_active_idx') - .on(table.offer, table.firstSeenAt.desc()) + check('listings_offer_check', sql`offer IN ('rent', 'buy')`), + check('listings_price_eur_check', sql`price_eur IS NULL OR price_eur >= 0`), + uniqueIndex('listings_source_idx').on(table.source, table.sourceId), + index('listings_active_idx') + .on(table.offer, table.firstSeenAt.desc().nullsFirst()) .where(sql`removed_at IS NULL`), - index('house_listings_city_idx').on(table.source, table.offer, table.citySlug), - index('house_listings_details_idx') - .on(table.firstSeenAt.desc()) + index('listings_city_idx').on(table.source, table.offer, table.citySlug), + index('listings_details_idx') + .on(table.firstSeenAt.desc().nullsFirst()) .where(sql`detail_fetched_at IS NULL`) ] ); -export const housePriceHistory = pgTable( - 'house_price_history', +export const housePriceHistory = houseSchema.table( + 'price_history', { listingId: integer('listing_id') .notNull() @@ -80,10 +80,12 @@ export const housePriceHistory = pgTable( observedAt: timestamptz('observed_at').notNull().defaultNow(), 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(), source: text('source').notNull(), lane: text('lane').notNull(), @@ -92,8 +94,8 @@ export const houseSearches = pgTable('house_searches', { lastFound: integer('last_found').notNull().default(0) }); -export const houseWorkerRuns = pgTable( - 'house_worker_runs', +export const houseWorkerRuns = houseSchema.table( + 'worker_runs', { id: bigint('id', { mode: 'number' }).primaryKey().generatedAlwaysAsIdentity(), source: text('source').notNull(), @@ -106,11 +108,11 @@ export const houseWorkerRuns = pgTable( message: text('message'), 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( - 'house_alert_rules', +export const houseAlertRules = houseSchema.table( + 'alert_rules', { id: id(), name: text('name').notNull(), @@ -122,7 +124,11 @@ export const houseAlertRules = pgTable( minBedrooms: integer('min_bedrooms'), cities: text('cities').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')`)] ); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 71a7726..29f2dd0 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -7,7 +7,8 @@ export * from './catch-em-all'; export * from './game-tracker'; export * from './tracker-journal'; 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 './cs-prices'; export * from './house-tracker'; diff --git a/src/lib/server/db/schema/lists.ts b/src/lib/server/db/schema/lists.ts index d46b39e..3aca525 100644 --- a/src/lib/server/db/schema/lists.ts +++ b/src/lib/server/db/schema/lists.ts @@ -1,47 +1,92 @@ -import { boolean, index, integer, pgTable, text, unique } from 'drizzle-orm/pg-core'; -import { id, timestamps } from './helpers'; -import { user } from './auth'; +import { + boolean, + 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', { - id: id(), - name: text('name').notNull(), - slug: text('slug').notNull().unique(), - pictureUrl: text('picture_url'), - isProgress: boolean('is_progress').notNull().default(false), - ...timestamps -}); +// Registry of GUI-visible tables. Runtime lists live in schema `lists` (one real +// table per list, created through meta.create_list()); that schema is excluded +// from drizzle-kit via schemaFilter. Structure truth is pg_catalog; these rows are +// presentation. See src/lib/server/lists.ts for the list engine. +const ts = (name: string) => timestamp(name, { withTimezone: true }); -export const listItems = pgTable( - 'list_items', +export const metaTables = metaSchema.table( + 'tables', { - id: id(), - listId: integer('list_id') - .notNull() - .references(() => lists.id, { onDelete: 'cascade' }), - name: text('name').notNull(), - pictureUrl: text('picture_url'), + id: integer('id').primaryKey().generatedByDefaultAsIdentity(), + schemaName: text('schema_name').notNull(), + tableName: text('table_name').notNull(), + slug: text('slug').notNull().unique('tables_slug_key'), + displayName: text('display_name').notNull(), 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( - 'list_item_completions', +export const uiTypes = [ + '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(), - itemId: integer('item_id') + id: integer('id').primaryKey().generatedByDefaultAsIdentity(), + tableId: integer('table_id') .notNull() - .references(() => listItems.id, { onDelete: 'cascade' }), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - ...timestamps + .references(() => metaTables.id, { onDelete: 'cascade' }), + columnName: text('column_name').notNull(), + label: text('label'), + uiType: text('ui_type', { enum: uiTypes }).notNull(), + 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 NewList = typeof lists.$inferInsert; -export type ListItem = typeof listItems.$inferSelect; -export type NewListItem = typeof listItems.$inferInsert; +export type MetaTable = typeof metaTables.$inferSelect; +export type MetaColumn = typeof metaColumns.$inferSelect; diff --git a/src/lib/server/db/schema/money-tracker.ts b/src/lib/server/db/schema/money-tracker.ts index 35b64f5..4d9d119 100644 --- a/src/lib/server/db/schema/money-tracker.ts +++ b/src/lib/server/db/schema/money-tracker.ts @@ -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 { user } from './auth'; -export const moneyAccounts = pgTable( - 'money_accounts', +export const moneyAccounts = moneySchema.table( + 'accounts', { id: id(), userId: text('user_id') @@ -15,11 +17,15 @@ export const moneyAccounts = pgTable( color: text('color'), ...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( - 'money_transactions', +export const moneyTransactions = moneySchema.table( + 'transactions', { id: 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)] ); -export const moneyRecurring = pgTable( - 'money_recurring', +export const moneyRecurring = moneySchema.table( + 'recurring', { id: id(), userId: text('user_id') @@ -51,11 +57,14 @@ export const moneyRecurring = pgTable( accountId: integer('account_id').references(() => moneyAccounts.id, { onDelete: 'set null' }), ...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( - 'money_forecasts', +export const moneyForecasts = moneySchema.table( + 'forecasts', { id: id(), userId: text('user_id') @@ -70,8 +79,8 @@ export const moneyForecasts = pgTable( (table) => [index().on(table.userId, table.date)] ); -export const moneyUtilitySettings = pgTable( - 'money_utility_settings', +export const moneyUtilitySettings = moneySchema.table( + 'utility_settings', { id: id(), userId: text('user_id') @@ -83,11 +92,14 @@ export const moneyUtilitySettings = pgTable( billingStart: date('billing_start', { mode: 'string' }), ...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( - 'money_utility_readings', +export const moneyUtilityReadings = moneySchema.table( + 'utility_readings', { id: id(), userId: text('user_id') @@ -98,11 +110,14 @@ export const moneyUtilityReadings = pgTable( reading: numeric('reading', { precision: 12, scale: 3 }).notNull(), ...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( - 'money_wishlist', +export const moneyWishlist = moneySchema.table( + 'wishlist', { id: id(), userId: text('user_id') @@ -117,8 +132,8 @@ export const moneyWishlist = pgTable( (table) => [index().on(table.userId)] ); -export const moneyProjectionSettings = pgTable( - 'money_projection_settings', +export const moneyProjectionSettings = moneySchema.table( + 'projection_settings', { id: id(), userId: text('user_id') @@ -132,16 +147,22 @@ export const moneyProjectionSettings = pgTable( .notNull() .default('5.00'), 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), endDate: date('end_date', { mode: 'string' }), ...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( - 'money_insurances', +export const moneyInsurances = moneySchema.table( + 'insurances', { id: id(), userId: text('user_id') @@ -154,5 +175,8 @@ export const moneyInsurances = pgTable( cadence: text('cadence').notNull().default('monthly'), ...timestamps }, - (table) => [index().on(table.userId)] + (table) => [ + index().on(table.userId), + check('insurances_cadence_check', sql`cadence IN ('weekly','monthly','yearly')`) + ] ); diff --git a/src/lib/server/db/schema/progress.ts b/src/lib/server/db/schema/progress.ts index 1f57ca9..190b7cf 100644 --- a/src/lib/server/db/schema/progress.ts +++ b/src/lib/server/db/schema/progress.ts @@ -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'; -export const tasks = pgTable('tasks', { +export const tasks = siteSchema.table('tasks', { id: id(), name: text('name').notNull(), currentPoints: integer('current_points').notNull().default(0), @@ -10,15 +11,15 @@ export const tasks = pgTable('tasks', { ...timestamps }); -export const topics = pgTable('topics', { +export const topics = siteSchema.table('topics', { id: id(), name: text('name').notNull(), emoji: text('emoji'), ...timestamps }); -export const tasksToTopics = pgTable( - 'tasks_to_topics', +export const tasksToTopics = siteSchema.table( + 'task_topics', { id: id(), taskId: integer('task_id') diff --git a/src/lib/server/db/schema/schemas.ts b/src/lib/server/db/schema/schemas.ts new file mode 100644 index 0000000..b75bb54 --- /dev/null +++ b/src/lib/server/db/schema/schemas.ts @@ -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'); diff --git a/src/lib/server/db/schema/tracker-journal.ts b/src/lib/server/db/schema/tracker-journal.ts index 8b3c5ca..b7024c9 100644 --- a/src/lib/server/db/schema/tracker-journal.ts +++ b/src/lib/server/db/schema/tracker-journal.ts @@ -2,20 +2,21 @@ import { boolean, check, date, + numeric, index, integer, jsonb, - pgTable, text, unique } from 'drizzle-orm/pg-core'; +import { gamesSchema, refSchema } from './schemas'; import { sql } from 'drizzle-orm'; import { user } from './auth'; import { games, pokemon } from './catch-em-all'; import { trackerGames, trackerItems } from './game-tracker'; import { id, timestamps } from './helpers'; -export const pokemonResearch = pgTable('pokemon_research', { +export const pokemonResearch = refSchema.table('pokemon_research', { pokemonId: integer('pokemon_id') .primaryKey() .references(() => pokemon.id, { onDelete: 'cascade' }), @@ -36,7 +37,7 @@ export const pokemonResearch = pgTable('pokemon_research', { .notNull() }); -export const pokemonDexNumbers = pgTable( +export const pokemonDexNumbers = refSchema.table( 'pokemon_dex_numbers', { id: id(), @@ -53,7 +54,7 @@ export const pokemonDexNumbers = pgTable( (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(), name: text('name').notNull(), type: text('type').notNull(), @@ -65,7 +66,7 @@ export const pokemonMoves = pgTable('pokemon_moves', { target: text('target').notNull() }); -export const pokemonLearnsets = pgTable( +export const pokemonLearnsets = refSchema.table( 'pokemon_learnsets', { id: id(), @@ -78,7 +79,7 @@ export const pokemonLearnsets = pgTable( (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') .primaryKey() .references(() => games.id, { onDelete: 'cascade' }), @@ -86,7 +87,7 @@ export const pokemonGameResearch = pgTable('pokemon_game_research', { regions: text('regions').array().notNull() }); -export const pokemonJourneys = pgTable( +export const pokemonJourneys = gamesSchema.table( 'pokemon_journeys', { id: id(), @@ -103,10 +104,13 @@ export const pokemonJourneys = pgTable( completed: boolean('completed').notNull().default(false), ...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', { id: id(), @@ -124,7 +128,7 @@ export const pokemonSeen = pgTable( (t) => [unique().on(t.userId, t.gameId, t.pokemonId), index().on(t.userId)] ); -export const pokemonCatches = pgTable( +export const pokemonCatches = gamesSchema.table( 'pokemon_catches', { id: id(), @@ -157,13 +161,14 @@ export const pokemonCatches = pgTable( (t) => [ index().on(t.userId, t.gameId, t.pokemonId), unique().on(t.userId, t.gameId, t.partySlot), - check('pokemon_catch_level', sql`${t.level} between 1 and 100`), - check('pokemon_party_slot', sql`${t.partySlot} between 1 and 6`), - check('pokemon_four_moves', sql`cardinality(${t.moves}) <= 4`) + check('pokemon_catches_level_check', sql`${t.level} between 1 and 100`), + check('pokemon_catches_party_slot_check', sql`${t.partySlot} between 1 and 6`), + 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', { id: id(), @@ -184,7 +189,7 @@ export const pokemonMilestones = pgTable( (t) => [unique().on(t.userId, t.gameId, t.key)] ); -export const trackerRanks = pgTable( +export const trackerRanks = gamesSchema.table( 'tracker_ranks', { id: id(), @@ -200,14 +205,12 @@ export const trackerRanks = pgTable( }, (t) => [ unique().on(t.userId, t.gameId), - check( - 'tracker_rank_range', - sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000` - ) + check('tracker_ranks_level_check', sql`${t.level} between 1 and 10000`), + check('tracker_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`) ] ); -export const trackerWeaponRanks = pgTable( +export const trackerWeaponRanks = gamesSchema.table( 'tracker_weapon_ranks', { id: id(), @@ -223,14 +226,12 @@ export const trackerWeaponRanks = pgTable( }, (t) => [ unique().on(t.userId, t.itemId), - check( - 'tracker_weapon_rank_range', - sql`${t.level} between 1 and 10000 and ${t.prestige} between 0 and 1000` - ) + check('tracker_weapon_ranks_level_check', sql`${t.level} between 1 and 10000`), + check('tracker_weapon_ranks_prestige_check', sql`${t.prestige} between 0 and 1000`) ] ); -export const trackerChallenges = pgTable( +export const trackerChallenges = refSchema.table( 'tracker_challenges', { id: id(), @@ -249,7 +250,7 @@ export const trackerChallenges = pgTable( (t) => [unique().on(t.gameId, t.key)] ); -export const trackerChallengeProgress = pgTable( +export const trackerChallengeProgress = gamesSchema.table( 'tracker_challenge_progress', { id: id(), @@ -263,10 +264,13 @@ export const trackerChallengeProgress = pgTable( done: boolean('done').notNull().default(false), ...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', { id: id(), @@ -282,18 +286,23 @@ export const osuEntries = pgTable( skill: text('skill').notNull().default('stamina'), bpm: integer('bpm'), duration: integer('duration'), - accuracy: text('accuracy'), + accuracy: numeric('accuracy', { mode: 'number' }), combo: integer('combo'), misses: integer('misses'), - pp: text('pp'), + pp: numeric('pp', { mode: 'number' }), 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), playedOn: date('played_on').notNull(), notes: text('notes').notNull().default(''), ...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;