From 4f148f91fc6c9a0793aceaf3c77c78335c36dcfe Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 13 Jun 2026 21:35:44 -0700 Subject: [PATCH 1/3] fix(schema): correct ON DELETE behavior for 5 drifted FKs (#1126) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five FK constraints on `wxyc_schema.flowsheet`, `wxyc_schema.rotation`, and `wxyc_schema.reviews` were created with `ON DELETE NO ACTION` in `0000_rare_prima.sql` and recreated unchanged by `0016_nervous_hydra.sql`, but the Drizzle schema source declares them as `SET NULL` (flowsheet) and `CASCADE` (rotation, reviews). Because `meta/0093_snapshot.json` already records the schema-source values, `drizzle-kit generate` produced no fix migration — the drift was invisible to the normal authoring loop. Migration 0094 drops + recreates the five FKs with the intended ON DELETE behaviour, following the pattern in `0048_fix-fk-on-delete-set-null.sql` (the predecessor that patched the analogous drift for `schedule`, `shift_covers`, `shows.primary_dj_id` per #433). The schema source already encodes the desired state, so no `schema.ts` change is needed; the new snapshot is byte-identical to 0093 except for `id` / `prevId`. A new integration spec (`tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js`) asserts `pg_constraint.confdeltype` for all five FKs and exercises the SET NULL behaviour via a parent-row DELETE inside a rolled-back transaction. Complements the existing `tests/unit/database/schema.fk-cascades.test.ts` schema-side guard. Closes #1126. --- ...k-on-delete-flowsheet-rotation-reviews.sql | 43 + .../src/migrations/meta/0097_snapshot.json | 4608 +++++++++++++++++ .../src/migrations/meta/_journal.json | 7 + .../src/migrations/meta/applied-hashes.json | 4 +- ...-delete-flowsheet-rotation-reviews.spec.js | 130 + 5 files changed, 4789 insertions(+), 3 deletions(-) create mode 100644 shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql create mode 100644 shared/database/src/migrations/meta/0097_snapshot.json create mode 100644 tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js diff --git a/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql new file mode 100644 index 00000000..3c4d6fc5 --- /dev/null +++ b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql @@ -0,0 +1,43 @@ +-- precondition-guard: not-required (DROP + ADD CONSTRAINT on existing FKs is a +-- pure constraint-shape change; no data invariant is asserted by the new +-- ON DELETE actions and no rows can violate the redefined FK as long as the +-- referenced parent rows still exist — which they already do because the +-- old NO ACTION constraint enforced exactly that) +-- @no-precondition-needed: ON DELETE behaviour change is forward-looking; it +-- governs future DELETEs on the parent table, not the present FK shape. +-- 0094 — Fix FK ON DELETE drift on flowsheet / rotation / reviews. +-- +-- Five FK constraints were created with ON DELETE NO ACTION in +-- `0000_rare_prima.sql` and recreated unchanged by `0016_nervous_hydra.sql`, +-- but the Drizzle schema source declares them as SET NULL (flowsheet) and +-- CASCADE (rotation, reviews). The most-recent snapshot +-- (`meta/0093_snapshot.json`) records the schema-source values, masking the +-- drift from `drizzle-kit generate` — no subsequent migration patched the +-- production DB to match, so new environments diverge from prod. +-- +-- This migration follows the pattern in `0048_fix-fk-on-delete-set-null.sql` +-- (the predecessor that patched the analogous drift for schedule / +-- shift_covers / shows.primary_dj_id; see #433). The five constraints below +-- were missed by 0048. +-- +-- See WXYC/Backend-Service#1126 for the full drift table and reproduction. + +-- flowsheet.show_id → shows.id : NO ACTION → SET NULL +ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_show_id_shows_id_fk"; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_show_id_shows_id_fk" FOREIGN KEY ("show_id") REFERENCES "wxyc_schema"."shows"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- flowsheet.album_id → library.id : NO ACTION → SET NULL +ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_album_id_library_id_fk"; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- flowsheet.rotation_id → rotation.id : NO ACTION → SET NULL +ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_rotation_id_rotation_id_fk"; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_rotation_id_rotation_id_fk" FOREIGN KEY ("rotation_id") REFERENCES "wxyc_schema"."rotation"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- rotation.album_id → library.id : NO ACTION → CASCADE +ALTER TABLE "wxyc_schema"."rotation" DROP CONSTRAINT "rotation_album_id_library_id_fk"; +ALTER TABLE "wxyc_schema"."rotation" ADD CONSTRAINT "rotation_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- reviews.album_id → library.id : NO ACTION → CASCADE +ALTER TABLE "wxyc_schema"."reviews" DROP CONSTRAINT "reviews_album_id_library_id_fk"; +ALTER TABLE "wxyc_schema"."reviews" ADD CONSTRAINT "reviews_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/shared/database/src/migrations/meta/0097_snapshot.json b/shared/database/src/migrations/meta/0097_snapshot.json new file mode 100644 index 00000000..cb231e35 --- /dev/null +++ b/shared/database/src/migrations/meta/0097_snapshot.json @@ -0,0 +1,4608 @@ +{ + "id": "4d4ed747-aeba-4120-8063-80dc520a1cc5", + "prevId": "c234d50f-46dc-4d1d-8e48-3687f1cedaaa", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_account": { + "name": "auth_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auth_account_provider_account_key": { + "name": "auth_account_provider_account_key", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.album_metadata": { + "name": "album_metadata", + "schema": "wxyc_schema", + "columns": { + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "artwork_url": { + "name": "artwork_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "discogs_url": { + "name": "discogs_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "release_year": { + "name": "release_year", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "spotify_url": { + "name": "spotify_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "apple_music_url": { + "name": "apple_music_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "youtube_music_url": { + "name": "youtube_music_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "bandcamp_url": { + "name": "bandcamp_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "soundcloud_url": { + "name": "soundcloud_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "artist_bio": { + "name": "artist_bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_wikipedia_url": { + "name": "artist_wikipedia_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "album_metadata_album_id_library_id_fk": { + "name": "album_metadata_album_id_library_id_fk", + "tableFrom": "album_metadata", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anonymous_devices": { + "name": "anonymous_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "blocked": { + "name": "blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "anonymous_devices_device_id_key": { + "name": "anonymous_devices_device_id_key", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.artist_crossreference": { + "name": "artist_crossreference", + "schema": "wxyc_schema", + "columns": { + "source_artist_id": { + "name": "source_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_artist_id": { + "name": "target_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "artist_crossref_source_target": { + "name": "artist_crossref_source_target", + "columns": [ + { + "expression": "source_artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artist_crossreference_source_artist_id_artists_id_fk": { + "name": "artist_crossreference_source_artist_id_artists_id_fk", + "tableFrom": "artist_crossreference", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "source_artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artist_crossreference_target_artist_id_artists_id_fk": { + "name": "artist_crossreference_target_artist_id_artists_id_fk", + "tableFrom": "artist_crossreference", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "target_artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.artist_library_crossreference": { + "name": "artist_library_crossreference", + "schema": "wxyc_schema", + "columns": { + "artist_id": { + "name": "artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "library_id_artist_id": { + "name": "library_id_artist_id", + "columns": [ + { + "expression": "artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artist_library_crossreference_artist_id_artists_id_fk": { + "name": "artist_library_crossreference_artist_id_artists_id_fk", + "tableFrom": "artist_library_crossreference", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artist_library_crossreference_library_id_library_id_fk": { + "name": "artist_library_crossreference_library_id_library_id_fk", + "tableFrom": "artist_library_crossreference", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "library_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.artist_search_alias": { + "name": "artist_search_alias", + "schema": "wxyc_schema", + "columns": { + "artist_id": { + "name": "artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "related_artist_id": { + "name": "related_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_subject_id": { + "name": "external_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_object_id": { + "name": "external_object_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "artist_search_alias_variant_trgm_idx": { + "name": "artist_search_alias_variant_trgm_idx", + "columns": [ + { + "expression": "\"variant\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "artist_search_alias_artist_id_artists_id_fk": { + "name": "artist_search_alias_artist_id_artists_id_fk", + "tableFrom": "artist_search_alias", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "artist_search_alias_related_artist_id_artists_id_fk": { + "name": "artist_search_alias_related_artist_id_artists_id_fk", + "tableFrom": "artist_search_alias", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "related_artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artist_search_alias_pkey": { + "name": "artist_search_alias_pkey", + "columns": [ + "artist_id", + "source", + "variant" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "artist_search_alias_confidence_range": { + "name": "artist_search_alias_confidence_range", + "value": "\"wxyc_schema\".\"artist_search_alias\".\"confidence\" BETWEEN 0 AND 1" + }, + "artist_search_alias_variant_nonblank": { + "name": "artist_search_alias_variant_nonblank", + "value": "length(trim(\"wxyc_schema\".\"artist_search_alias\".\"variant\")) > 0" + } + }, + "isRLSEnabled": false + }, + "wxyc_schema.artists": { + "name": "artists", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "alphabetical_name": { + "name": "alphabetical_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "code_letters": { + "name": "code_letters", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "discogs_artist_id": { + "name": "discogs_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_artist_id": { + "name": "musicbrainz_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "wikidata_qid": { + "name": "wikidata_qid", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "spotify_artist_id": { + "name": "spotify_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "apple_music_artist_id": { + "name": "apple_music_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "bandcamp_id": { + "name": "bandcamp_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "artist_name_trgm_idx": { + "name": "artist_name_trgm_idx", + "columns": [ + { + "expression": "\"artist_name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "code_letters_idx": { + "name": "code_letters_idx", + "columns": [ + { + "expression": "code_letters", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.banned_fingerprints": { + "name": "banned_fingerprints", + "schema": "wxyc_schema", + "columns": { + "fingerprint": { + "name": "fingerprint", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "banned_at": { + "name": "banned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ban_expires_at": { + "name": "ban_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "banned_by_user_id": { + "name": "banned_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "banned_fingerprints_ban_expires_at_idx": { + "name": "banned_fingerprints_ban_expires_at_idx", + "columns": [ + { + "expression": "ban_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"banned_fingerprints\".\"ban_expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "banned_fingerprints_banned_by_user_id_auth_user_id_fk": { + "name": "banned_fingerprints_banned_by_user_id_auth_user_id_fk", + "tableFrom": "banned_fingerprints", + "tableTo": "auth_user", + "columnsFrom": [ + "banned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.bins": { + "name": "bins", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "dj_id": { + "name": "dj_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "track_title": { + "name": "track_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bins_dj_id_auth_user_id_fk": { + "name": "bins_dj_id_auth_user_id_fk", + "tableFrom": "bins", + "tableTo": "auth_user", + "columnsFrom": [ + "dj_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bins_album_id_library_id_fk": { + "name": "bins_album_id_library_id_fk", + "tableFrom": "bins", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.compilation_track_artist": { + "name": "compilation_track_artist", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "track_title": { + "name": "track_title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "track_position": { + "name": "track_position", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "cta_library_id_idx": { + "name": "cta_library_id_idx", + "columns": [ + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cta_artist_name_idx": { + "name": "cta_artist_name_idx", + "columns": [ + { + "expression": "artist_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cta_unique_idx": { + "name": "cta_unique_idx", + "columns": [ + { + "expression": "library_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "artist_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "track_title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "compilation_track_artist_library_id_library_id_fk": { + "name": "compilation_track_artist_library_id_library_id_fk", + "tableFrom": "compilation_track_artist", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "library_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.concerts": { + "name": "concerts", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "concert_source_enum", + "typeSchema": "wxyc_schema", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "venue_id": { + "name": "venue_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "headlining_artist_raw": { + "name": "headlining_artist_raw", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "headlining_artist_id": { + "name": "headlining_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supporting_artists_raw": { + "name": "supporting_artists_raw", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "ticket_url": { + "name": "ticket_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "concert_status_enum", + "typeSchema": "wxyc_schema", + "primaryKey": false, + "notNull": true, + "default": "'on_sale'" + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scraped_at": { + "name": "scraped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "first_scraped_at": { + "name": "first_scraped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "concerts_source_source_id_idx": { + "name": "concerts_source_source_id_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "concerts_venue_starts_at_idx": { + "name": "concerts_venue_starts_at_idx", + "columns": [ + { + "expression": "venue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "concerts_headlining_artist_starts_at_idx": { + "name": "concerts_headlining_artist_starts_at_idx", + "columns": [ + { + "expression": "headlining_artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "concerts_venue_id_venues_id_fk": { + "name": "concerts_venue_id_venues_id_fk", + "tableFrom": "concerts", + "tableTo": "venues", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "venue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "concerts_headlining_artist_id_artists_id_fk": { + "name": "concerts_headlining_artist_id_artists_id_fk", + "tableFrom": "concerts", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "headlining_artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.cronjob_runs": { + "name": "cronjob_runs", + "schema": "wxyc_schema", + "columns": { + "job_name": { + "name": "job_name", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "last_run": { + "name": "last_run", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.dj_stats": { + "name": "dj_stats", + "schema": "wxyc_schema", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "shows_covered": { + "name": "shows_covered", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "dj_stats_user_id_auth_user_id_fk": { + "name": "dj_stats_user_id_auth_user_id_fk", + "tableFrom": "dj_stats", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.flowsheet": { + "name": "flowsheet", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "show_id": { + "name": "show_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rotation_id": { + "name": "rotation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_entry_id": { + "name": "legacy_entry_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_release_id": { + "name": "legacy_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entry_type": { + "name": "entry_type", + "type": "flowsheet_entry_type", + "typeSchema": "wxyc_schema", + "primaryKey": false, + "notNull": true, + "default": "'track'" + }, + "track_title": { + "name": "track_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "track_position": { + "name": "track_position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "album_title": { + "name": "album_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "record_label": { + "name": "record_label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "label_id": { + "name": "label_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "play_order": { + "name": "play_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_flag": { + "name": "request_flag", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "segue": { + "name": "segue", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "varchar(250)", + "primaryKey": false, + "notNull": false + }, + "add_time": { + "name": "add_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "artwork_url": { + "name": "artwork_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "discogs_url": { + "name": "discogs_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "release_year": { + "name": "release_year", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "spotify_url": { + "name": "spotify_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "apple_music_url": { + "name": "apple_music_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "youtube_music_url": { + "name": "youtube_music_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "bandcamp_url": { + "name": "bandcamp_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "soundcloud_url": { + "name": "soundcloud_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "artist_bio": { + "name": "artist_bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_wikipedia_url": { + "name": "artist_wikipedia_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "dj_name": { + "name": "dj_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkage_source": { + "name": "linkage_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linkage_confidence": { + "name": "linkage_confidence", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "legacy_link_attempted_at": { + "name": "legacy_link_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata_attempt_at": { + "name": "metadata_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata_status": { + "name": "metadata_status", + "type": "metadata_status_enum", + "typeSchema": "wxyc_schema", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "enriching_since": { + "name": "enriching_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "search_doc": { + "name": "search_doc", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(\"artist_name\", '')), 'A') || setweight(to_tsvector('simple', coalesce(\"track_title\", '')), 'B') || setweight(to_tsvector('simple', coalesce(\"dj_name\", '')), 'B') || setweight(to_tsvector('simple', coalesce(\"album_title\", '')), 'C') || setweight(to_tsvector('simple', coalesce(\"record_label\", '')), 'D')", + "type": "stored" + } + } + }, + "indexes": { + "flowsheet_legacy_entry_id_idx": { + "name": "flowsheet_legacy_entry_id_idx", + "columns": [ + { + "expression": "legacy_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_legacy_release_id_idx": { + "name": "flowsheet_legacy_release_id_idx", + "columns": [ + { + "expression": "legacy_release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_artist_name_trgm_idx": { + "name": "flowsheet_artist_name_trgm_idx", + "columns": [ + { + "expression": "\"artist_name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "flowsheet_track_title_trgm_idx": { + "name": "flowsheet_track_title_trgm_idx", + "columns": [ + { + "expression": "\"track_title\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "flowsheet_album_title_trgm_idx": { + "name": "flowsheet_album_title_trgm_idx", + "columns": [ + { + "expression": "\"album_title\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "flowsheet_record_label_trgm_idx": { + "name": "flowsheet_record_label_trgm_idx", + "columns": [ + { + "expression": "\"record_label\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "flowsheet_track_add_time_idx": { + "name": "flowsheet_track_add_time_idx", + "columns": [ + { + "expression": "\"add_time\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"entry_type\" = 'track'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_search_doc_idx": { + "name": "flowsheet_search_doc_idx", + "columns": [ + { + "expression": "\"search_doc\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "flowsheet_album_link_lookup_idx": { + "name": "flowsheet_album_link_lookup_idx", + "columns": [ + { + "expression": "(lower(trim(\"artist_name\")) || '-' || lower(trim(coalesce(\"album_title\", ''))))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"album_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_show_id_idx": { + "name": "flowsheet_show_id_idx", + "columns": [ + { + "expression": "show_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_play_order_idx": { + "name": "flowsheet_play_order_idx", + "columns": [ + { + "expression": "\"play_order\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_updated_at_idx": { + "name": "flowsheet_updated_at_idx", + "columns": [ + { + "expression": "\"updated_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_metadata_attempt_pending_idx": { + "name": "flowsheet_metadata_attempt_pending_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"entry_type\" = 'track' AND \"wxyc_schema\".\"flowsheet\".\"artist_name\" IS NOT NULL AND \"wxyc_schema\".\"flowsheet\".\"metadata_attempt_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_metadata_attempt_pending_covering_idx": { + "name": "flowsheet_metadata_attempt_pending_covering_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"entry_type\" = 'track' AND \"wxyc_schema\".\"flowsheet\".\"artist_name\" IS NOT NULL AND \"wxyc_schema\".\"flowsheet\".\"metadata_attempt_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_metadata_status_pending_idx": { + "name": "flowsheet_metadata_status_pending_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"entry_type\" = 'track' AND \"wxyc_schema\".\"flowsheet\".\"artist_name\" IS NOT NULL AND \"wxyc_schema\".\"flowsheet\".\"metadata_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_metadata_status_enriching_stale_idx": { + "name": "flowsheet_metadata_status_enriching_stale_idx", + "columns": [ + { + "expression": "enriching_since", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"metadata_status\" = 'enriching'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowsheet_album_id_enriched_idx": { + "name": "flowsheet_album_id_enriched_idx", + "columns": [ + { + "expression": "album_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet\".\"album_id\" IS NOT NULL AND \"wxyc_schema\".\"flowsheet\".\"metadata_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "flowsheet_show_id_shows_id_fk": { + "name": "flowsheet_show_id_shows_id_fk", + "tableFrom": "flowsheet", + "tableTo": "shows", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "show_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "flowsheet_album_id_library_id_fk": { + "name": "flowsheet_album_id_library_id_fk", + "tableFrom": "flowsheet", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "flowsheet_rotation_id_rotation_id_fk": { + "name": "flowsheet_rotation_id_rotation_id_fk", + "tableFrom": "flowsheet", + "tableTo": "rotation", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "rotation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "flowsheet_label_id_labels_id_fk": { + "name": "flowsheet_label_id_labels_id_fk", + "tableFrom": "flowsheet", + "tableTo": "labels", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.flowsheet_linkage_review": { + "name": "flowsheet_linkage_review", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "flowsheet_id": { + "name": "flowsheet_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "candidate_library_ids": { + "name": "candidate_library_ids", + "type": "integer[]", + "primaryKey": false, + "notNull": true + }, + "candidate_confidences": { + "name": "candidate_confidences", + "type": "real[]", + "primaryKey": false, + "notNull": true + }, + "suggested_action": { + "name": "suggested_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_decision": { + "name": "reviewed_decision", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "flowsheet_linkage_review_unreviewed_idx": { + "name": "flowsheet_linkage_review_unreviewed_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"wxyc_schema\".\"flowsheet_linkage_review\".\"reviewed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "flowsheet_linkage_review_flowsheet_id_flowsheet_id_fk": { + "name": "flowsheet_linkage_review_flowsheet_id_flowsheet_id_fk", + "tableFrom": "flowsheet_linkage_review", + "tableTo": "flowsheet", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "flowsheet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "flowsheet_linkage_review_flowsheet_id_unique": { + "name": "flowsheet_linkage_review_flowsheet_id_unique", + "nullsNotDistinct": false, + "columns": [ + "flowsheet_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.flowsheet_watermark": { + "name": "flowsheet_watermark", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "last_modified_at": { + "name": "last_modified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "flowsheet_watermark_singleton": { + "name": "flowsheet_watermark_singleton", + "value": "\"id\" = true" + } + }, + "isRLSEnabled": false + }, + "wxyc_schema.format": { + "name": "format", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "format_name": { + "name": "format_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.genre_artist_crossreference": { + "name": "genre_artist_crossreference", + "schema": "wxyc_schema", + "columns": { + "artist_id": { + "name": "artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "genre_id": { + "name": "genre_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "artist_genre_code": { + "name": "artist_genre_code", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "artist_genre_key": { + "name": "artist_genre_key", + "columns": [ + { + "expression": "artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "genre_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "genre_artist_crossreference_artist_id_artists_id_fk": { + "name": "genre_artist_crossreference_artist_id_artists_id_fk", + "tableFrom": "genre_artist_crossreference", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "genre_artist_crossreference_genre_id_genres_id_fk": { + "name": "genre_artist_crossreference_genre_id_genres_id_fk", + "tableFrom": "genre_artist_crossreference", + "tableTo": "genres", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "genre_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.genres": { + "name": "genres", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "genre_name": { + "name": "genre_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plays": { + "name": "plays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_invitation": { + "name": "auth_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auth_invitation_email_idx": { + "name": "auth_invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_invitation_organization_id_auth_organization_id_fk": { + "name": "auth_invitation_organization_id_auth_organization_id_fk", + "tableFrom": "auth_invitation", + "tableTo": "auth_organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_invitation_inviter_id_auth_user_id_fk": { + "name": "auth_invitation_inviter_id_auth_user_id_fk", + "tableFrom": "auth_invitation", + "tableTo": "auth_user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.labels": { + "name": "labels", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label_name": { + "name": "label_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "parent_label_id": { + "name": "parent_label_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "labels_label_name_unique": { + "name": "labels_label_name_unique", + "nullsNotDistinct": false, + "columns": [ + "label_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.library": { + "name": "library", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "artist_id": { + "name": "artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "genre_id": { + "name": "genre_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "format_id": { + "name": "format_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "alternate_artist_name": { + "name": "alternate_artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "album_artist": { + "name": "album_artist", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "album_title": { + "name": "album_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "label_id": { + "name": "label_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "code_number": { + "name": "code_number", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "code_volume_letters": { + "name": "code_volume_letters", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false + }, + "disc_quantity": { + "name": "disc_quantity", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "plays": { + "name": "plays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "legacy_release_id": { + "name": "legacy_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "add_date": { + "name": "add_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_lost": { + "name": "date_lost", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_found": { + "name": "date_found", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "on_streaming": { + "name": "on_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "artwork_url": { + "name": "artwork_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "canonical_entity_id": { + "name": "canonical_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_entity_confidence": { + "name": "canonical_entity_confidence", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "canonical_entity_resolved_at": { + "name": "canonical_entity_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "search_doc": { + "name": "search_doc", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "setweight(to_tsvector('simple', coalesce(\"artist_name\", '')), 'A') || setweight(to_tsvector('simple', coalesce(\"album_title\", '')), 'B')", + "type": "stored" + } + } + }, + "indexes": { + "title_trgm_idx": { + "name": "title_trgm_idx", + "columns": [ + { + "expression": "\"album_title\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "genre_id_idx": { + "name": "genre_id_idx", + "columns": [ + { + "expression": "genre_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "format_id_idx": { + "name": "format_id_idx", + "columns": [ + { + "expression": "format_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artist_id_idx": { + "name": "artist_id_idx", + "columns": [ + { + "expression": "artist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "library_legacy_release_id_idx": { + "name": "library_legacy_release_id_idx", + "columns": [ + { + "expression": "legacy_release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "album_artist_trgm_idx": { + "name": "album_artist_trgm_idx", + "columns": [ + { + "expression": "\"album_artist\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "library_artist_name_trgm_idx": { + "name": "library_artist_name_trgm_idx", + "columns": [ + { + "expression": "\"artist_name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "library_search_doc_idx": { + "name": "library_search_doc_idx", + "columns": [ + { + "expression": "\"search_doc\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "library_canonical_entity_id_idx": { + "name": "library_canonical_entity_id_idx", + "columns": [ + { + "expression": "canonical_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "library_artist_id_artists_id_fk": { + "name": "library_artist_id_artists_id_fk", + "tableFrom": "library", + "tableTo": "artists", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "artist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "library_genre_id_genres_id_fk": { + "name": "library_genre_id_genres_id_fk", + "tableFrom": "library", + "tableTo": "genres", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "genre_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "library_format_id_format_id_fk": { + "name": "library_format_id_format_id_fk", + "tableFrom": "library", + "tableTo": "format", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "format_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "library_label_id_labels_id_fk": { + "name": "library_label_id_labels_id_fk", + "tableFrom": "library", + "tableTo": "labels", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.library_identity": { + "name": "library_identity", + "schema": "wxyc_schema", + "columns": { + "library_id": { + "name": "library_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "discogs_master_id": { + "name": "discogs_master_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discogs_release_id": { + "name": "discogs_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_release_group_mbid": { + "name": "musicbrainz_release_group_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_release_mbid": { + "name": "musicbrainz_release_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_recording_mbid": { + "name": "musicbrainz_recording_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "wikidata_qid": { + "name": "wikidata_qid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_id": { + "name": "spotify_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_id": { + "name": "apple_music_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "agreement_sources": { + "name": "agreement_sources", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "distinct_unresolved_sources": { + "name": "distinct_unresolved_sources", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "(\n (CASE WHEN \"discogs_master_id\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"discogs_release_id\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"musicbrainz_release_group_mbid\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"musicbrainz_release_mbid\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"musicbrainz_recording_mbid\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"wikidata_qid\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"spotify_id\" IS NULL THEN 1 ELSE 0 END)\n + (CASE WHEN \"apple_music_id\" IS NULL THEN 1 ELSE 0 END)\n )", + "type": "stored" + } + } + }, + "indexes": { + "library_identity_audit_idx": { + "name": "library_identity_audit_idx", + "columns": [ + { + "expression": "confidence", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"distinct_unresolved_sources\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "library_identity_library_id_library_id_fk": { + "name": "library_identity_library_id_library_id_fk", + "tableFrom": "library_identity", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "library_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "library_identity_confidence_range": { + "name": "library_identity_confidence_range", + "value": "\"wxyc_schema\".\"library_identity\".\"confidence\" BETWEEN 0 AND 1" + } + }, + "isRLSEnabled": false + }, + "wxyc_schema.library_identity_history": { + "name": "library_identity_history", + "schema": "wxyc_schema", + "columns": { + "history_id": { + "name": "history_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "library_id": { + "name": "library_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "discogs_master_id": { + "name": "discogs_master_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discogs_release_id": { + "name": "discogs_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_release_group_mbid": { + "name": "musicbrainz_release_group_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_release_mbid": { + "name": "musicbrainz_release_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_recording_mbid": { + "name": "musicbrainz_recording_mbid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "wikidata_qid": { + "name": "wikidata_qid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spotify_id": { + "name": "spotify_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apple_music_id": { + "name": "apple_music_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "agreement_sources": { + "name": "agreement_sources", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "superseded_reason": { + "name": "superseded_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_category": { + "name": "reason_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.library_identity_source": { + "name": "library_identity_source", + "schema": "wxyc_schema", + "columns": { + "library_id": { + "name": "library_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "boost_sources": { + "name": "boost_sources", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "library_identity_source_library_id_library_id_fk": { + "name": "library_identity_source_library_id_library_id_fk", + "tableFrom": "library_identity_source", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "library_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "library_identity_source_library_id_source_pk": { + "name": "library_identity_source_library_id_source_pk", + "columns": [ + "library_id", + "source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "library_identity_source_confidence_range": { + "name": "library_identity_source_confidence_range", + "value": "\"wxyc_schema\".\"library_identity_source\".\"confidence\" BETWEEN 0 AND 1" + } + }, + "isRLSEnabled": false + }, + "public.auth_member": { + "name": "auth_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auth_member_org_user_key": { + "name": "auth_member_org_user_key", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_member_organization_id_auth_organization_id_fk": { + "name": "auth_member_organization_id_auth_organization_id_fk", + "tableFrom": "auth_member", + "tableTo": "auth_organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_member_user_id_auth_user_id_fk": { + "name": "auth_member_user_id_auth_user_id_fk", + "tableFrom": "auth_member", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_organization": { + "name": "auth_organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_organization_slug_key": { + "name": "auth_organization_slug_key", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.reviews": { + "name": "reviews", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "review": { + "name": "review", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "author": { + "name": "author", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reviews_album_id_library_id_fk": { + "name": "reviews_album_id_library_id_fk", + "tableFrom": "reviews", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reviews_album_id_unique": { + "name": "reviews_album_id_unique", + "nullsNotDistinct": false, + "columns": [ + "album_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.rotation": { + "name": "rotation", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_rotation_id": { + "name": "legacy_rotation_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_library_release_id": { + "name": "legacy_library_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rotation_bin": { + "name": "rotation_bin", + "type": "freq_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kill_date": { + "name": "kill_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "album_title": { + "name": "album_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "record_label": { + "name": "record_label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "discogs_release_id": { + "name": "discogs_release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discogs_release_id_source": { + "name": "discogs_release_id_source", + "type": "discogs_release_id_source_enum", + "typeSchema": "wxyc_schema", + "primaryKey": false, + "notNull": true, + "default": "'tubafrenzy_paste'" + }, + "tracklist_lookup_attempted_at": { + "name": "tracklist_lookup_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lml_identity_id": { + "name": "lml_identity_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "album_id_idx": { + "name": "album_id_idx", + "columns": [ + { + "expression": "album_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rotation_legacy_rotation_id_idx": { + "name": "rotation_legacy_rotation_id_idx", + "columns": [ + { + "expression": "legacy_rotation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rotation_album_id_library_id_fk": { + "name": "rotation_album_id_library_id_fk", + "tableFrom": "rotation", + "tableTo": "library", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "album_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.schedule": { + "name": "schedule", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "day": { + "name": "day", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "time", + "primaryKey": false, + "notNull": true + }, + "show_duration": { + "name": "show_duration", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "specialty_id": { + "name": "specialty_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assigned_dj_id": { + "name": "assigned_dj_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "assigned_dj_id2": { + "name": "assigned_dj_id2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_specialty_id_specialty_shows_id_fk": { + "name": "schedule_specialty_id_specialty_shows_id_fk", + "tableFrom": "schedule", + "tableTo": "specialty_shows", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "specialty_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_assigned_dj_id_auth_user_id_fk": { + "name": "schedule_assigned_dj_id_auth_user_id_fk", + "tableFrom": "schedule", + "tableTo": "auth_user", + "columnsFrom": [ + "assigned_dj_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "schedule_assigned_dj_id2_auth_user_id_fk": { + "name": "schedule_assigned_dj_id2_auth_user_id_fk", + "tableFrom": "schedule", + "tableTo": "auth_user", + "columnsFrom": [ + "assigned_dj_id2" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_session_token_key": { + "name": "auth_session_token_key", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.shift_covers": { + "name": "shift_covers", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shift_timestamp": { + "name": "shift_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cover_dj_id": { + "name": "cover_dj_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "covered": { + "name": "covered", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "shift_covers_schedule_id_schedule_id_fk": { + "name": "shift_covers_schedule_id_schedule_id_fk", + "tableFrom": "shift_covers", + "tableTo": "schedule", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shift_covers_cover_dj_id_auth_user_id_fk": { + "name": "shift_covers_cover_dj_id_auth_user_id_fk", + "tableFrom": "shift_covers", + "tableTo": "auth_user", + "columnsFrom": [ + "cover_dj_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.show_djs": { + "name": "show_djs", + "schema": "wxyc_schema", + "columns": { + "show_id": { + "name": "show_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dj_id": { + "name": "dj_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + } + }, + "indexes": { + "show_djs_show_id_dj_id_unique": { + "name": "show_djs_show_id_dj_id_unique", + "columns": [ + { + "expression": "show_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dj_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "show_djs_show_id_shows_id_fk": { + "name": "show_djs_show_id_shows_id_fk", + "tableFrom": "show_djs", + "tableTo": "shows", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "show_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "show_djs_dj_id_auth_user_id_fk": { + "name": "show_djs_dj_id_auth_user_id_fk", + "tableFrom": "show_djs", + "tableTo": "auth_user", + "columnsFrom": [ + "dj_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.shows": { + "name": "shows", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "primary_dj_id": { + "name": "primary_dj_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "specialty_id": { + "name": "specialty_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_show_id": { + "name": "legacy_show_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "legacy_dj_name": { + "name": "legacy_dj_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "legacy_dj_id": { + "name": "legacy_dj_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dj_name_override": { + "name": "dj_name_override", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "show_name": { + "name": "show_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shows_legacy_show_id_idx": { + "name": "shows_legacy_show_id_idx", + "columns": [ + { + "expression": "legacy_show_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shows_primary_dj_id_auth_user_id_fk": { + "name": "shows_primary_dj_id_auth_user_id_fk", + "tableFrom": "shows", + "tableTo": "auth_user", + "columnsFrom": [ + "primary_dj_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shows_specialty_id_specialty_shows_id_fk": { + "name": "shows_specialty_id_specialty_shows_id_fk", + "tableFrom": "shows", + "tableTo": "specialty_shows", + "schemaTo": "wxyc_schema", + "columnsFrom": [ + "specialty_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.specialty_shows": { + "name": "specialty_shows", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "specialty_name": { + "name": "specialty_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "add_date": { + "name": "add_date", + "type": "date", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "real_name": { + "name": "real_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "dj_name": { + "name": "dj_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "app_skin": { + "name": "app_skin", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'modern-light'" + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "has_completed_onboarding": { + "name": "has_completed_onboarding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "auth_user_email_key": { + "name": "auth_user_email_key", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_user_username_key": { + "name": "auth_user_username_key", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_activity": { + "name": "user_activity", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_auth_user_id_fk": { + "name": "user_activity_user_id_auth_user_id_fk", + "tableFrom": "user_activity", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "wxyc_schema.venues": { + "name": "venues", + "schema": "wxyc_schema", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_modified": { + "name": "last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "venues_slug_idx": { + "name": "venues_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "wxyc_schema.concert_source_enum": { + "name": "concert_source_enum", + "schema": "wxyc_schema", + "values": [ + "rhp_scrape" + ] + }, + "wxyc_schema.concert_status_enum": { + "name": "concert_status_enum", + "schema": "wxyc_schema", + "values": [ + "on_sale", + "sold_out", + "cancelled", + "rescheduled" + ] + }, + "wxyc_schema.discogs_release_id_source_enum": { + "name": "discogs_release_id_source_enum", + "schema": "wxyc_schema", + "values": [ + "tubafrenzy_paste", + "lml_offline_backfill", + "discogs_direct_backfill", + "library_identity" + ] + }, + "wxyc_schema.flowsheet_entry_type": { + "name": "flowsheet_entry_type", + "schema": "wxyc_schema", + "values": [ + "track", + "show_start", + "show_end", + "dj_join", + "dj_leave", + "talkset", + "breakpoint", + "message" + ] + }, + "public.freq_enum": { + "name": "freq_enum", + "schema": "public", + "values": [ + "S", + "L", + "M", + "H", + "N" + ] + }, + "wxyc_schema.metadata_status_enum": { + "name": "metadata_status_enum", + "schema": "wxyc_schema", + "values": [ + "pending", + "enriching", + "enriched_match", + "enriched_no_match", + "failed_no_retry" + ] + } + }, + "schemas": { + "wxyc_schema": "wxyc_schema" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "wxyc_schema.library_artist_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code_letters": { + "name": "code_letters", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "artist_genre_code": { + "name": "artist_genre_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code_number": { + "name": "code_number", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "alphabetical_name": { + "name": "alphabetical_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "album_title": { + "name": "album_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "format_name": { + "name": "format_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "genre_name": { + "name": "genre_name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "rotation_bin": { + "name": "rotation_bin", + "type": "freq_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "add_date": { + "name": "add_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "label_id": { + "name": "label_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "on_streaming": { + "name": "on_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "album_artist": { + "name": "album_artist", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "plays": { + "name": "plays", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "artwork_url": { + "name": "artwork_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "discogs_artist_id": { + "name": "discogs_artist_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "musicbrainz_artist_id": { + "name": "musicbrainz_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "wikidata_qid": { + "name": "wikidata_qid", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "spotify_artist_id": { + "name": "spotify_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "apple_music_artist_id": { + "name": "apple_music_artist_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "bandcamp_id": { + "name": "bandcamp_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "artist_id": { + "name": "artist_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"wxyc_schema\".\"library\".\"id\", \"wxyc_schema\".\"artists\".\"code_letters\", \"wxyc_schema\".\"genre_artist_crossreference\".\"artist_genre_code\", \"wxyc_schema\".\"library\".\"code_number\", \"wxyc_schema\".\"artists\".\"artist_name\", \"wxyc_schema\".\"artists\".\"alphabetical_name\", \"wxyc_schema\".\"library\".\"album_title\", \"wxyc_schema\".\"format\".\"format_name\", \"wxyc_schema\".\"genres\".\"genre_name\", \"wxyc_schema\".\"rotation\".\"rotation_bin\", \"wxyc_schema\".\"library\".\"add_date\", \"wxyc_schema\".\"library\".\"label\", \"wxyc_schema\".\"library\".\"label_id\", \"wxyc_schema\".\"library\".\"on_streaming\", \"wxyc_schema\".\"library\".\"album_artist\", \"wxyc_schema\".\"library\".\"plays\", \"wxyc_schema\".\"library\".\"artwork_url\", \"wxyc_schema\".\"artists\".\"discogs_artist_id\", \"wxyc_schema\".\"artists\".\"musicbrainz_artist_id\", \"wxyc_schema\".\"artists\".\"wikidata_qid\", \"wxyc_schema\".\"artists\".\"spotify_artist_id\", \"wxyc_schema\".\"artists\".\"apple_music_artist_id\", \"wxyc_schema\".\"artists\".\"bandcamp_id\", \"wxyc_schema\".\"library\".\"artist_id\" from \"wxyc_schema\".\"library\" inner join \"wxyc_schema\".\"artists\" on \"wxyc_schema\".\"artists\".\"id\" = \"wxyc_schema\".\"library\".\"artist_id\" inner join \"wxyc_schema\".\"format\" on \"wxyc_schema\".\"format\".\"id\" = \"wxyc_schema\".\"library\".\"format_id\" inner join \"wxyc_schema\".\"genres\" on \"wxyc_schema\".\"genres\".\"id\" = \"wxyc_schema\".\"library\".\"genre_id\" inner join \"wxyc_schema\".\"genre_artist_crossreference\" on (\"wxyc_schema\".\"genre_artist_crossreference\".\"artist_id\" = \"wxyc_schema\".\"library\".\"artist_id\" and \"wxyc_schema\".\"genre_artist_crossreference\".\"genre_id\" = \"wxyc_schema\".\"library\".\"genre_id\") left join \"wxyc_schema\".\"rotation\" on \"wxyc_schema\".\"rotation\".\"album_id\" = \"wxyc_schema\".\"library\".\"id\" AND (\"wxyc_schema\".\"rotation\".\"kill_date\" > CURRENT_DATE OR \"wxyc_schema\".\"rotation\".\"kill_date\" IS NULL)", + "name": "library_artist_view", + "schema": "wxyc_schema", + "isExisting": false, + "materialized": false + }, + "wxyc_schema.rotation_library_view": { + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "label_id": { + "name": "label_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rotation_bin": { + "name": "rotation_bin", + "type": "freq_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "album_title": { + "name": "album_title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "artist_name": { + "name": "artist_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "alphabetical_name": { + "name": "alphabetical_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "kill_date": { + "name": "kill_date", + "type": "date", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"wxyc_schema\".\"library\".\"id\", \"wxyc_schema\".\"rotation\".\"id\", \"wxyc_schema\".\"library\".\"label\", \"wxyc_schema\".\"library\".\"label_id\", \"wxyc_schema\".\"rotation\".\"rotation_bin\", \"wxyc_schema\".\"library\".\"album_title\", \"wxyc_schema\".\"artists\".\"artist_name\", \"wxyc_schema\".\"artists\".\"alphabetical_name\", \"wxyc_schema\".\"rotation\".\"kill_date\" from \"wxyc_schema\".\"library\" inner join \"wxyc_schema\".\"rotation\" on \"wxyc_schema\".\"library\".\"id\" = \"wxyc_schema\".\"rotation\".\"album_id\" inner join \"wxyc_schema\".\"artists\" on \"wxyc_schema\".\"artists\".\"id\" = \"wxyc_schema\".\"library\".\"artist_id\"", + "name": "rotation_library_view", + "schema": "wxyc_schema", + "isExisting": false, + "materialized": false + }, + "wxyc_schema.album_plays": { + "columns": { + "album_id": { + "name": "album_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "plays": { + "name": "plays", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "name": "album_plays", + "schema": "wxyc_schema", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/shared/database/src/migrations/meta/_journal.json b/shared/database/src/migrations/meta/_journal.json index db4aa77f..da3bd209 100644 --- a/shared/database/src/migrations/meta/_journal.json +++ b/shared/database/src/migrations/meta/_journal.json @@ -666,6 +666,13 @@ "when": 1781468384341, "tag": "0096_cdc_oversized_fallback", "breakpoints": true + }, + { + "idx": 97, + "version": "7", + "when": 1781468384342, + "tag": "0097_fix-fk-on-delete-flowsheet-rotation-reviews", + "breakpoints": true } ] } diff --git a/shared/database/src/migrations/meta/applied-hashes.json b/shared/database/src/migrations/meta/applied-hashes.json index 7816c2eb..0dec8c74 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -91,7 +91,5 @@ "0091_venues-and-concerts": "e738de9bdb0ab762c982a6e31f664f88705bc2b7336bbd57a15efe3b250e6e87", "0092_normalize-artist-name": "57bf7f03733591917917c5a19b9b67d8527a64ccce224cdc3f1102f4a9cca368", "0093_concerts-first-scraped-at": "bbeee146c92ab63fa70e9f2c537d5a06a13f80a5f52c23cb5a1bed1432057421", - "0094_rotation-lml-identity-id": "10134a18381f7daad56ca2d8cae80723832be3158cbe6dd01aae2f8e6afcaaa0", - "0095_drop-stale-trigram-indexes": "aba5b26a66102d427e1abd6520aa1f7ef14b776eaaa87ea8b99c3b97e79d24c7", - "0096_cdc_oversized_fallback": "5615c7a6aa7514b139340ef6c8ba86a548ac6e7b6f9f0c39241bea2a62a1274b" + "0094_fix-fk-on-delete-flowsheet-rotation-reviews": "35e9df0f17f40dbb348b0a4e437bc3d44c72260a2eeeef9572046f3eef713e47" } diff --git a/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js new file mode 100644 index 00000000..33227913 --- /dev/null +++ b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js @@ -0,0 +1,130 @@ +/** + * FK ON DELETE behaviour on flowsheet / rotation / reviews + * (WXYC/Backend-Service#1126, migration 0094). + * + * Five FK constraints drifted between the Drizzle schema source (SET NULL + * for the three flowsheet FKs, CASCADE for rotation.album_id and + * reviews.album_id) and the actual migration history (ON DELETE NO ACTION + * via 0000_rare_prima.sql, recreated unchanged by 0016_nervous_hydra.sql). + * Because the latest snapshot already records the schema-source values, + * `drizzle-kit generate` produced no fix migration — the drift was + * invisible to the normal authoring loop. Migration 0094 patches it. + * + * This spec is the live regression test: it asserts the current + * `pg_constraint.confdeltype` for each of the five FKs (mirroring the + * issue body's reproduction query) AND exercises the actual ON DELETE + * behaviour via a parent-row DELETE under transaction rollback (so the + * shape fixture loaded by globalSetup is not perturbed). + * + * If a future schema change re-introduces the drift, this spec fails + * before the migration is ever attempted against prod. + */ + +const postgres = require('postgres'); + +const SCHEMA = process.env.WXYC_SCHEMA_NAME || 'wxyc_schema'; + +function makeSql() { + return postgres({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || process.env.CI_DB_PORT || '5433', 10), + database: process.env.DB_NAME || 'wxyc_db', + user: process.env.DB_USERNAME || 'test-user', + password: process.env.DB_PASSWORD || 'test-pw', + onnotice: () => {}, + max: 2, + }); +} + +// Map from pg_constraint.confdeltype's single-char encoding to the SQL +// keyword we expect, for readable assertion failure messages. +const CONFDELTYPE_LABEL = { + a: 'NO ACTION', + r: 'RESTRICT', + c: 'CASCADE', + n: 'SET NULL', + d: 'SET DEFAULT', +}; + +describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0094)', () => { + let sql; + + beforeAll(() => { + sql = makeSql(); + }); + + afterAll(async () => { + if (sql) await sql.end({ timeout: 5 }); + }); + + test('pg_constraint.confdeltype matches the schema-source declarations', async () => { + // The five FKs the migration repairs, with the action declared in + // shared/database/src/schema.ts. + const expectations = [ + { name: 'flowsheet_show_id_shows_id_fk', expected: 'n' }, // SET NULL + { name: 'flowsheet_album_id_library_id_fk', expected: 'n' }, // SET NULL + { name: 'flowsheet_rotation_id_rotation_id_fk', expected: 'n' }, // SET NULL + { name: 'rotation_album_id_library_id_fk', expected: 'c' }, // CASCADE + { name: 'reviews_album_id_library_id_fk', expected: 'c' }, // CASCADE + ]; + + // Constraint namespace is the per-worker schema (WXYC_SCHEMA_NAME). We + // filter by both the constraint name and the namespace so a stale + // constraint in `wxyc_schema` doesn't bleed into a worker schema's + // assertions. + const rows = await sql` + SELECT c.conname, c.confdeltype + FROM pg_constraint c + JOIN pg_namespace n ON n.oid = c.connamespace + WHERE n.nspname = ${SCHEMA} + AND c.conname = ANY (${sql.array(expectations.map((e) => e.name))}) + `; + const observed = new Map(rows.map((r) => [r.conname, r.confdeltype])); + + for (const { name, expected } of expectations) { + const actual = observed.get(name); + expect({ + constraint: name, + actual: `${actual ?? '(missing)'} (${CONFDELTYPE_LABEL[actual] ?? 'unknown'})`, + expected: `${expected} (${CONFDELTYPE_LABEL[expected]})`, + }).toEqual({ + constraint: name, + actual: `${expected} (${CONFDELTYPE_LABEL[expected]})`, + expected: `${expected} (${CONFDELTYPE_LABEL[expected]})`, + }); + } + }); + + test('DELETE on a library row sets flowsheet.album_id to NULL (was NO ACTION)', async () => { + // Run inside a transaction we abort, so the fixture stays intact for + // any spec that runs after this one in the same Jest worker. + await sql + .begin(async (tx) => { + // Insert a library row + a flowsheet entry referencing it. Use a + // synthetic id well above the shape-fixture range (7000-7099) and + // above the `bigserial`/`serial` PK floor for stability. + const [lib] = await tx.unsafe( + `INSERT INTO "${SCHEMA}".library (artist_name, album_title) + VALUES ('FK Test Artist', 'FK Test Album') + RETURNING id` + ); + const [fls] = await tx.unsafe( + `INSERT INTO "${SCHEMA}".flowsheet (album_id, entry_type, message) + VALUES (${lib.id}, 'track', 'fk-test') + RETURNING id` + ); + + await tx.unsafe(`DELETE FROM "${SCHEMA}".library WHERE id = ${lib.id}`); + + const after = await tx.unsafe(`SELECT album_id FROM "${SCHEMA}".flowsheet WHERE id = ${fls.id}`); + expect(after.length).toBe(1); + expect(after[0].album_id).toBeNull(); + + // Roll back so neither row survives the test. + throw new Error('intentional rollback'); + }) + .catch((err) => { + if (err.message !== 'intentional rollback') throw err; + }); + }); +}); From 1c8000d8ca620bb39592efa60dc0d999c377ffde Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 13 Jun 2026 23:11:41 -0700 Subject: [PATCH 2/3] =?UTF-8?q?fix(migrations):=20address=20#1411=20review?= =?UTF-8?q?=20=E2=80=94=20non-blocking=20FK=20validation=20+=20complete=20?= =?UTF-8?q?integration=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply five review findings on the #1126 FK ON DELETE drift fix: 1. Migration 0094 now uses ADD CONSTRAINT ... NOT VALID instead of a bare ADD CONSTRAINT. The bare form would have taken AccessExclusiveLock on flowsheet (~857k rows) for the full validation-scan duration, blocking on-air DJ writes during deploy. NOT VALID makes the ADD metadata-only and instant. Drizzle's migrator wraps the whole migration in one transaction (drizzle-orm/pg-core/dialect.js:60), so an in-migration VALIDATE would defeat the lock benefit — VALIDATE is documented as an out-of-band operator step in the header instead. For these five constraints the validation is effectively a no-op anyway: the existing NO ACTION FK already kept the reference relation consistent, and only the ON DELETE action is changing (forward-looking). 2. Integration spec now exercises all three ON DELETE actions in the fix (flowsheet SET NULL, rotation CASCADE, reviews CASCADE) instead of only the flowsheet SET NULL path. The previous spec ran two tests: a static pg_constraint.confdeltype assertion plus a single behavioural test. The behavioural test silently never ran because its library INSERT omitted four NOT NULL columns (artist_id, genre_id, format_id, code_number) and its flowsheet INSERT omitted play_order — so the spec was a false green for the actual cascade/SET-NULL behaviour. 3. Spec now uses the shared getTestDb() pool + withRollback helper from tests/utils/db.js instead of opening its own postgres() connection and rolling back via a hand-written intentional-error idiom. Matches the convention used by neighbouring specs (album-metadata-upsert, enrichment-worker-claim, library-identity-backfill). 4. Inserts now use the seed_db.sql baseline rows (artist 1, genre 11, format 1) to satisfy library's NOT NULL FK columns rather than threading FK resolution through the test. Sequence-assigned ids land at 7200+ (shape fixture sets library_id_seq to 7199), safely above the explicit-id fixture range. Local CI: lint, format:check, typecheck, test:unit all pass. Migration dry-run not available locally (Docker daemon not running); will be covered by CI's migrate-dryrun job since the change touches db-init paths. --- ...k-on-delete-flowsheet-rotation-reviews.sql | 72 ++++++++- .../src/migrations/meta/applied-hashes.json | 5 +- ...-delete-flowsheet-rotation-reviews.spec.js | 138 +++++++++++------- 3 files changed, 156 insertions(+), 59 deletions(-) diff --git a/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql index 3c4d6fc5..903eee80 100644 --- a/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql +++ b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql @@ -17,27 +17,85 @@ -- -- This migration follows the pattern in `0048_fix-fk-on-delete-set-null.sql` -- (the predecessor that patched the analogous drift for schedule / --- shift_covers / shows.primary_dj_id; see #433). The five constraints below --- were missed by 0048. +-- shift_covers / shows.primary_dj_id; see #433) but uses `ADD CONSTRAINT +-- ... NOT VALID` rather than a bare `ADD CONSTRAINT` to avoid blocking +-- writes on flowsheet (~857k prod rows) during the deploy. The five +-- constraints below were missed by 0048. +-- +-- ## Lock behaviour: why NOT VALID (and why VALIDATE runs out-of-band) +-- +-- A bare `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY ...` takes an +-- `AccessExclusiveLock` AND runs a full-table validation scan that holds +-- the lock for the entire scan — blocking every concurrent INSERT/UPDATE/ +-- DELETE on the table for the deploy's duration. On an on-air station with +-- active DJs writing flowsheet rows in real time, that is a user-visible +-- outage window. +-- +-- `ADD CONSTRAINT ... NOT VALID` skips the validation scan: it takes +-- `AccessExclusiveLock` for a metadata-only change and releases it +-- instantly. New writes are enforced against the new FK shape immediately; +-- only retroactive validation of pre-existing rows is deferred. +-- +-- The companion `ALTER TABLE ... VALIDATE CONSTRAINT` runs the scan under +-- the lighter `ShareUpdateExclusiveLock`, which allows concurrent SELECT, +-- INSERT, UPDATE, DELETE. **But this benefit only materializes if VALIDATE +-- runs in its OWN transaction** — Drizzle's migrator +-- (`drizzle-orm/pg-core/dialect.js:60`) wraps the entire migration in one +-- `session.transaction()`, so a VALIDATE statement inside the migration +-- file would run under the AccessExclusiveLock that the preceding DROP / +-- ADD already acquired, defeating the point. We therefore omit VALIDATE +-- here and document it as the post-deploy operator step below. +-- +-- For these five constraints the validation is effectively a no-op anyway: +-- the existing `NO ACTION` FK has already kept the reference relation +-- consistent (every flowsheet.album_id either points at a live library.id +-- or is NULL). Changing only the `ON DELETE` action does not introduce +-- any new data invariant on existing rows — the action governs future +-- parent-row DELETEs. VALIDATE still has to scan because PostgreSQL +-- tracks the `convalidated` flag per constraint; until VALIDATE runs the +-- constraint is recorded as "trusted for new writes but not proven for +-- old rows." +-- +-- ## Post-deploy operator step +-- +-- After this migration deploys, an operator runs the following five +-- statements (each in its own implicit transaction — do NOT wrap them in +-- BEGIN/COMMIT) during a low-write window to clear the unvalidated state. +-- Skipping this step is harmless for correctness; it only leaves the +-- constraints with `convalidated = false` until the next operator runs +-- it. A bare `ANALYZE` is not needed (no row mutations). +-- +-- ALTER TABLE "wxyc_schema"."flowsheet" VALIDATE CONSTRAINT "flowsheet_show_id_shows_id_fk"; +-- ALTER TABLE "wxyc_schema"."flowsheet" VALIDATE CONSTRAINT "flowsheet_album_id_library_id_fk"; +-- ALTER TABLE "wxyc_schema"."flowsheet" VALIDATE CONSTRAINT "flowsheet_rotation_id_rotation_id_fk"; +-- ALTER TABLE "wxyc_schema"."rotation" VALIDATE CONSTRAINT "rotation_album_id_library_id_fk"; +-- ALTER TABLE "wxyc_schema"."reviews" VALIDATE CONSTRAINT "reviews_album_id_library_id_fk"; +-- +-- We DROP+ADD rather than `ALTER CONSTRAINT` because PostgreSQL has no +-- syntax to change `ON DELETE` action in place — you must drop and recreate. +-- The DROP itself is metadata-only and instant. -- -- See WXYC/Backend-Service#1126 for the full drift table and reproduction. +-- See PostgreSQL docs: +-- https://www.postgresql.org/docs/current/sql-altertable.html (NOT VALID) +-- https://www.postgresql.org/docs/current/explicit-locking.html (lock modes) -- flowsheet.show_id → shows.id : NO ACTION → SET NULL ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_show_id_shows_id_fk"; -ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_show_id_shows_id_fk" FOREIGN KEY ("show_id") REFERENCES "wxyc_schema"."shows"("id") ON DELETE SET NULL ON UPDATE NO ACTION; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_show_id_shows_id_fk" FOREIGN KEY ("show_id") REFERENCES "wxyc_schema"."shows"("id") ON DELETE SET NULL ON UPDATE NO ACTION NOT VALID; -- flowsheet.album_id → library.id : NO ACTION → SET NULL ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_album_id_library_id_fk"; -ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE SET NULL ON UPDATE NO ACTION; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE SET NULL ON UPDATE NO ACTION NOT VALID; -- flowsheet.rotation_id → rotation.id : NO ACTION → SET NULL ALTER TABLE "wxyc_schema"."flowsheet" DROP CONSTRAINT "flowsheet_rotation_id_rotation_id_fk"; -ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_rotation_id_rotation_id_fk" FOREIGN KEY ("rotation_id") REFERENCES "wxyc_schema"."rotation"("id") ON DELETE SET NULL ON UPDATE NO ACTION; +ALTER TABLE "wxyc_schema"."flowsheet" ADD CONSTRAINT "flowsheet_rotation_id_rotation_id_fk" FOREIGN KEY ("rotation_id") REFERENCES "wxyc_schema"."rotation"("id") ON DELETE SET NULL ON UPDATE NO ACTION NOT VALID; -- rotation.album_id → library.id : NO ACTION → CASCADE ALTER TABLE "wxyc_schema"."rotation" DROP CONSTRAINT "rotation_album_id_library_id_fk"; -ALTER TABLE "wxyc_schema"."rotation" ADD CONSTRAINT "rotation_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION; +ALTER TABLE "wxyc_schema"."rotation" ADD CONSTRAINT "rotation_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION NOT VALID; -- reviews.album_id → library.id : NO ACTION → CASCADE ALTER TABLE "wxyc_schema"."reviews" DROP CONSTRAINT "reviews_album_id_library_id_fk"; -ALTER TABLE "wxyc_schema"."reviews" ADD CONSTRAINT "reviews_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION; +ALTER TABLE "wxyc_schema"."reviews" ADD CONSTRAINT "reviews_album_id_library_id_fk" FOREIGN KEY ("album_id") REFERENCES "wxyc_schema"."library"("id") ON DELETE CASCADE ON UPDATE NO ACTION NOT VALID; diff --git a/shared/database/src/migrations/meta/applied-hashes.json b/shared/database/src/migrations/meta/applied-hashes.json index 0dec8c74..8c3bd4d8 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -91,5 +91,8 @@ "0091_venues-and-concerts": "e738de9bdb0ab762c982a6e31f664f88705bc2b7336bbd57a15efe3b250e6e87", "0092_normalize-artist-name": "57bf7f03733591917917c5a19b9b67d8527a64ccce224cdc3f1102f4a9cca368", "0093_concerts-first-scraped-at": "bbeee146c92ab63fa70e9f2c537d5a06a13f80a5f52c23cb5a1bed1432057421", - "0094_fix-fk-on-delete-flowsheet-rotation-reviews": "35e9df0f17f40dbb348b0a4e437bc3d44c72260a2eeeef9572046f3eef713e47" + "0094_rotation-lml-identity-id": "10134a18381f7daad56ca2d8cae80723832be3158cbe6dd01aae2f8e6afcaaa0", + "0095_drop-stale-trigram-indexes": "aba5b26a66102d427e1abd6520aa1f7ef14b776eaaa87ea8b99c3b97e79d24c7", + "0096_cdc_oversized_fallback": "5615c7a6aa7514b139340ef6c8ba86a548ac6e7b6f9f0c39241bea2a62a1274b", + "0097_fix-fk-on-delete-flowsheet-rotation-reviews": "558e6ce5fa9ae4589a988f15ea3e8fd798c430a94c20c5dd480b4ab8290ebaf0" } diff --git a/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js index 33227913..abad2a3c 100644 --- a/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js +++ b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js @@ -12,30 +12,19 @@ * * This spec is the live regression test: it asserts the current * `pg_constraint.confdeltype` for each of the five FKs (mirroring the - * issue body's reproduction query) AND exercises the actual ON DELETE - * behaviour via a parent-row DELETE under transaction rollback (so the - * shape fixture loaded by globalSetup is not perturbed). + * issue body's reproduction query) AND exercises each of the three + * actual ON DELETE behaviours (flowsheet SET NULL, rotation CASCADE, + * reviews CASCADE) via a parent-row DELETE under transaction rollback + * (so the shape fixture loaded by globalSetup is not perturbed). * * If a future schema change re-introduces the drift, this spec fails * before the migration is ever attempted against prod. */ -const postgres = require('postgres'); +const { getTestDb, withRollback } = require('../utils/db'); const SCHEMA = process.env.WXYC_SCHEMA_NAME || 'wxyc_schema'; -function makeSql() { - return postgres({ - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || process.env.CI_DB_PORT || '5433', 10), - database: process.env.DB_NAME || 'wxyc_db', - user: process.env.DB_USERNAME || 'test-user', - password: process.env.DB_PASSWORD || 'test-pw', - onnotice: () => {}, - max: 2, - }); -} - // Map from pg_constraint.confdeltype's single-char encoding to the SQL // keyword we expect, for readable assertion failure messages. const CONFDELTYPE_LABEL = { @@ -46,15 +35,22 @@ const CONFDELTYPE_LABEL = { d: 'SET DEFAULT', }; +// Seeded baseline (dev_env/seed_db.sql): artists 1-3, genres 1-15 +// (rock=11), formats include cd=1 / vinyl=2. The shape fixture +// (tests/fixtures/shape.sql) advances the library/rotation/flowsheet +// sequences to 7199 so any serial-assigned id below lands at 7200+, +// safely above the explicit-id fixture range (7000-7099). Each +// behavioural test wraps its INSERT/DELETE in a withRollback transaction +// so the fixture stays intact for downstream specs. +const SEED_ARTIST_ID = 1; +const SEED_GENRE_ID = 11; +const SEED_FORMAT_ID = 1; + describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0094)', () => { let sql; beforeAll(() => { - sql = makeSql(); - }); - - afterAll(async () => { - if (sql) await sql.end({ timeout: 5 }); + sql = getTestDb(); }); test('pg_constraint.confdeltype matches the schema-source declarations', async () => { @@ -96,35 +92,75 @@ describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0094) }); test('DELETE on a library row sets flowsheet.album_id to NULL (was NO ACTION)', async () => { - // Run inside a transaction we abort, so the fixture stays intact for - // any spec that runs after this one in the same Jest worker. - await sql - .begin(async (tx) => { - // Insert a library row + a flowsheet entry referencing it. Use a - // synthetic id well above the shape-fixture range (7000-7099) and - // above the `bigserial`/`serial` PK floor for stability. - const [lib] = await tx.unsafe( - `INSERT INTO "${SCHEMA}".library (artist_name, album_title) - VALUES ('FK Test Artist', 'FK Test Album') - RETURNING id` - ); - const [fls] = await tx.unsafe( - `INSERT INTO "${SCHEMA}".flowsheet (album_id, entry_type, message) - VALUES (${lib.id}, 'track', 'fk-test') - RETURNING id` - ); - - await tx.unsafe(`DELETE FROM "${SCHEMA}".library WHERE id = ${lib.id}`); - - const after = await tx.unsafe(`SELECT album_id FROM "${SCHEMA}".flowsheet WHERE id = ${fls.id}`); - expect(after.length).toBe(1); - expect(after[0].album_id).toBeNull(); - - // Roll back so neither row survives the test. - throw new Error('intentional rollback'); - }) - .catch((err) => { - if (err.message !== 'intentional rollback') throw err; - }); + await withRollback(async (tx) => { + // library: artist_id, genre_id, format_id, album_title, code_number + // are all NOT NULL (see shared/database/src/schema.ts:344). + // Let `serial` auto-assign id from the post-fixture sequence floor + // (7200+); the explicit-id fixture range stops at 7099. + const [lib] = await tx` + INSERT INTO ${tx(SCHEMA)}.library (artist_id, genre_id, format_id, album_title, code_number, artist_name) + VALUES (${SEED_ARTIST_ID}, ${SEED_GENRE_ID}, ${SEED_FORMAT_ID}, + 'FK Test Album SET NULL', 9001, 'FK Test Artist') + RETURNING id + `; + // flowsheet: entry_type defaults to 'track'; play_order is NOT NULL + // with no default (schema.ts:679). + const [fls] = await tx` + INSERT INTO ${tx(SCHEMA)}.flowsheet (album_id, entry_type, play_order, message) + VALUES (${lib.id}, 'track', 1, 'fk-test-set-null') + RETURNING id + `; + + await tx`DELETE FROM ${tx(SCHEMA)}.library WHERE id = ${lib.id}`; + + const after = await tx`SELECT album_id FROM ${tx(SCHEMA)}.flowsheet WHERE id = ${fls.id}`; + expect(after).toHaveLength(1); + expect(after[0].album_id).toBeNull(); + }); + }); + + test('DELETE on a library row cascades to rotation rows (was NO ACTION)', async () => { + await withRollback(async (tx) => { + const [lib] = await tx` + INSERT INTO ${tx(SCHEMA)}.library (artist_id, genre_id, format_id, album_title, code_number, artist_name) + VALUES (${SEED_ARTIST_ID}, ${SEED_GENRE_ID}, ${SEED_FORMAT_ID}, + 'FK Test Album CASCADE rotation', 9002, 'FK Test Artist') + RETURNING id + `; + // rotation: album_id is nullable but referenced; rotation_bin is + // NOT NULL (schema.ts:558). + const [rot] = await tx` + INSERT INTO ${tx(SCHEMA)}.rotation (album_id, rotation_bin) + VALUES (${lib.id}, 'L') + RETURNING id + `; + + await tx`DELETE FROM ${tx(SCHEMA)}.library WHERE id = ${lib.id}`; + + const after = await tx`SELECT id FROM ${tx(SCHEMA)}.rotation WHERE id = ${rot.id}`; + expect(after).toHaveLength(0); + }); + }); + + test('DELETE on a library row cascades to reviews rows (was NO ACTION)', async () => { + await withRollback(async (tx) => { + const [lib] = await tx` + INSERT INTO ${tx(SCHEMA)}.library (artist_id, genre_id, format_id, album_title, code_number, artist_name) + VALUES (${SEED_ARTIST_ID}, ${SEED_GENRE_ID}, ${SEED_FORMAT_ID}, + 'FK Test Album CASCADE reviews', 9003, 'FK Test Artist') + RETURNING id + `; + // reviews: album_id is NOT NULL + UNIQUE (schema.ts:1075). + const [rev] = await tx` + INSERT INTO ${tx(SCHEMA)}.reviews (album_id, review, author) + VALUES (${lib.id}, 'fk-test-cascade-reviews', 'fk-test') + RETURNING id + `; + + await tx`DELETE FROM ${tx(SCHEMA)}.library WHERE id = ${lib.id}`; + + const after = await tx`SELECT id FROM ${tx(SCHEMA)}.reviews WHERE id = ${rev.id}`; + expect(after).toHaveLength(0); + }); }); }); From b55f2fcba512d06407964de4513b01ead76cba63 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Sat, 13 Jun 2026 23:21:38 -0700 Subject: [PATCH 3/3] fix(spec): use postgres-js native array binding in ANY(...) clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static-assertion test in fk-on-delete-flowsheet-rotation-reviews.spec.js was failing CI with `op ANY/ALL (array) requires array on right side` because `sql.array(arr)` (without a type argument) does not bind the value as a Postgres text[] in postgres-js. The canonical idiom in this codebase (flowsheet-etl-setwhere.spec.js:61, album-metadata-upsert.spec.js:159, enrichment-worker-sweep.spec.js:63) is `ANY(${arr})` with a bare JS array — postgres-js infers the array type from the values. This test was failing on the previous CI run too (pre-review-fix); this commit clears the inherited bug. --- ...97_fix-fk-on-delete-flowsheet-rotation-reviews.sql | 4 ++-- .../database/src/migrations/meta/applied-hashes.json | 2 +- .../fk-on-delete-flowsheet-rotation-reviews.spec.js | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql index 903eee80..8d9688ee 100644 --- a/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql +++ b/shared/database/src/migrations/0097_fix-fk-on-delete-flowsheet-rotation-reviews.sql @@ -5,13 +5,13 @@ -- old NO ACTION constraint enforced exactly that) -- @no-precondition-needed: ON DELETE behaviour change is forward-looking; it -- governs future DELETEs on the parent table, not the present FK shape. --- 0094 — Fix FK ON DELETE drift on flowsheet / rotation / reviews. +-- 0097 — Fix FK ON DELETE drift on flowsheet / rotation / reviews. -- -- Five FK constraints were created with ON DELETE NO ACTION in -- `0000_rare_prima.sql` and recreated unchanged by `0016_nervous_hydra.sql`, -- but the Drizzle schema source declares them as SET NULL (flowsheet) and -- CASCADE (rotation, reviews). The most-recent snapshot --- (`meta/0093_snapshot.json`) records the schema-source values, masking the +-- (`meta/0096_snapshot.json`) records the schema-source values, masking the -- drift from `drizzle-kit generate` — no subsequent migration patched the -- production DB to match, so new environments diverge from prod. -- diff --git a/shared/database/src/migrations/meta/applied-hashes.json b/shared/database/src/migrations/meta/applied-hashes.json index 8c3bd4d8..276f6d47 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -94,5 +94,5 @@ "0094_rotation-lml-identity-id": "10134a18381f7daad56ca2d8cae80723832be3158cbe6dd01aae2f8e6afcaaa0", "0095_drop-stale-trigram-indexes": "aba5b26a66102d427e1abd6520aa1f7ef14b776eaaa87ea8b99c3b97e79d24c7", "0096_cdc_oversized_fallback": "5615c7a6aa7514b139340ef6c8ba86a548ac6e7b6f9f0c39241bea2a62a1274b", - "0097_fix-fk-on-delete-flowsheet-rotation-reviews": "558e6ce5fa9ae4589a988f15ea3e8fd798c430a94c20c5dd480b4ab8290ebaf0" + "0097_fix-fk-on-delete-flowsheet-rotation-reviews": "f13d8eed11d7da774cbdf92abe12cd45440ac7bff594226364c387bd378e4224" } diff --git a/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js index abad2a3c..903ce814 100644 --- a/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js +++ b/tests/integration/fk-on-delete-flowsheet-rotation-reviews.spec.js @@ -1,6 +1,6 @@ /** * FK ON DELETE behaviour on flowsheet / rotation / reviews - * (WXYC/Backend-Service#1126, migration 0094). + * (WXYC/Backend-Service#1126, migration 0097). * * Five FK constraints drifted between the Drizzle schema source (SET NULL * for the three flowsheet FKs, CASCADE for rotation.album_id and @@ -46,7 +46,7 @@ const SEED_ARTIST_ID = 1; const SEED_GENRE_ID = 11; const SEED_FORMAT_ID = 1; -describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0094)', () => { +describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0097)', () => { let sql; beforeAll(() => { @@ -67,13 +67,16 @@ describe('FK ON DELETE on flowsheet / rotation / reviews (#1126, migration 0094) // Constraint namespace is the per-worker schema (WXYC_SCHEMA_NAME). We // filter by both the constraint name and the namespace so a stale // constraint in `wxyc_schema` doesn't bleed into a worker schema's - // assertions. + // assertions. postgres-js binds a JS array as a Postgres text[] in the + // `ANY(${arr})` position (see flowsheet-etl-setwhere.spec.js:61, + // album-metadata-upsert.spec.js:159 for the canonical idiom). + const constraintNames = expectations.map((e) => e.name); const rows = await sql` SELECT c.conname, c.confdeltype FROM pg_constraint c JOIN pg_namespace n ON n.oid = c.connamespace WHERE n.nspname = ${SCHEMA} - AND c.conname = ANY (${sql.array(expectations.map((e) => e.name))}) + AND c.conname = ANY(${constraintNames}) `; const observed = new Map(rows.map((r) => [r.conname, r.confdeltype]));