diff --git a/apps/backend/services/cdc/dispatcher.ts b/apps/backend/services/cdc/dispatcher.ts index 4dca02fc..ada22e63 100644 --- a/apps/backend/services/cdc/dispatcher.ts +++ b/apps/backend/services/cdc/dispatcher.ts @@ -9,23 +9,98 @@ * * Wire format and `onCdcEvent` API are unchanged — both still come from * `@wxyc/database` and remain the cross-consumer contract. + * + * BS#1120 fallback-channel sinks: `cdc_oversized` and `cdc_error` notifications + * (emitted by migration 0094 when the primary `cdc` payload would have been + * dropped) are wired here to `Sentry.captureMessage` so AC #3's "emit a metric + * Sentry can alert on" is satisfied. Subscribing only — the cdc-listener owns + * the LISTEN; this module owns the Sentry signal. + */ + +import * as Sentry from '@sentry/node'; +import { onCdcErrorEvent, onCdcOversizedEvent, startCdcListener, stopCdcListener } from '@wxyc/database'; + +/** + * Stable Sentry fingerprints for the BS#1120 fallback channels. Each channel + * gets a single issue group so alert thresholds count notifications, not + * per-table churn. (The `table` is on `tags` for breakdown queries.) + */ +const OVERSIZED_FINGERPRINT = ['cdc-oversized-payload']; +const ERROR_FINGERPRINT = ['cdc-trigger-exception']; + +let fallbackSinksRegistered = false; + +/** + * Wires the BS#1120 fallback channels to Sentry. Idempotent: a second call is + * a no-op so a stray `startCdcDispatcher()` (e.g. dev hot-reload) doesn't + * stack duplicate captures. Exported so tests can drive the wiring without + * coupling to module-init order. `__resetCdcFallbackSinksForTests` lets the + * test harness drop the latch between cases. */ +export function registerCdcFallbackSinks(): void { + if (fallbackSinksRegistered) return; + fallbackSinksRegistered = true; -import { startCdcListener, stopCdcListener } from '@wxyc/database'; + onCdcOversizedEvent((event) => { + Sentry.captureMessage('cdc.oversized_payload', { + level: 'warning', + tags: { + subsystem: 'cdc', + table: event.table, + action: event.action, + reason: event.reason, + }, + extra: { + schema: event.schema, + primary_key: event.primary_key, + payload_bytes: event.payload_bytes, + timestamp: event.timestamp, + }, + fingerprint: OVERSIZED_FINGERPRINT, + }); + }); + + onCdcErrorEvent((event) => { + Sentry.captureMessage('cdc.trigger_exception', { + level: 'error', + tags: { + subsystem: 'cdc', + table: event.table, + action: event.action, + reason: event.reason, + sqlstate: event.sqlstate, + }, + extra: { + schema: event.schema, + sqlerrm: event.sqlerrm, + timestamp: event.timestamp, + }, + fingerprint: ERROR_FINGERPRINT, + }); + }); +} /** * Starts the per-process CDC LISTEN connection. Idempotent at the listener - * layer (`startCdcListener` warns and returns on a second call). Call once - * at startup, before any consumer that registers via `onCdcEvent`. + * layer (`startCdcListener` warns and returns on a second call) and at the + * fallback-sink layer (`registerCdcFallbackSinks` no-ops on the second call + * via its module-level latch). Call once at startup, before any consumer that + * registers via `onCdcEvent`. */ export async function startCdcDispatcher(): Promise { + registerCdcFallbackSinks(); await startCdcListener(); } /** * Stops the per-process CDC LISTEN connection and clears registered * callbacks. Safe to call unconditionally during shutdown. + * + * Drops the fallback-sink latch so a subsequent `startCdcDispatcher()` (test + * harness, future hot-reload) re-wires the captures against the freshly + * cleared `@wxyc/database` callback arrays. */ export async function shutdownCdcDispatcher(): Promise { await stopCdcListener(); + fallbackSinksRegistered = false; } diff --git a/apps/enrichment-worker/worker.ts b/apps/enrichment-worker/worker.ts index 7e5f4957..5340e438 100644 --- a/apps/enrichment-worker/worker.ts +++ b/apps/enrichment-worker/worker.ts @@ -18,7 +18,9 @@ import { closeDatabaseConnection, enableLivenessProbe, onCdcConnectionStateChange, + onCdcErrorEvent, onCdcEvent, + onCdcOversizedEvent, startCdcListener, stopCdcListener, } from '@wxyc/database'; @@ -214,6 +216,51 @@ const main = async (): Promise => { }); onCdcEvent(makeEnrichmentHandler()); + + // BS#1120: wire the migration-0094 fallback channels to Sentry so a dropped + // primary `cdc` payload (oversized row) or an unexpected trigger exception + // produces a metric the alert can fire on. Both the worker and the + // backend's CDC dispatcher subscribe; they're independent LISTEN + // connections, so each process needs its own sink. + onCdcOversizedEvent((event) => { + Sentry.captureMessage('cdc.oversized_payload', { + level: 'warning', + tags: { + subsystem: 'cdc', + consumer: 'enrichment-worker', + table: event.table, + action: event.action, + reason: event.reason, + }, + extra: { + schema: event.schema, + primary_key: event.primary_key, + payload_bytes: event.payload_bytes, + timestamp: event.timestamp, + }, + fingerprint: ['cdc-oversized-payload'], + }); + }); + onCdcErrorEvent((event) => { + Sentry.captureMessage('cdc.trigger_exception', { + level: 'error', + tags: { + subsystem: 'cdc', + consumer: 'enrichment-worker', + table: event.table, + action: event.action, + reason: event.reason, + sqlstate: event.sqlstate, + }, + extra: { + schema: event.schema, + sqlerrm: event.sqlerrm, + timestamp: event.timestamp, + }, + fingerprint: ['cdc-trigger-exception'], + }); + }); + await startCdcListener(); // Belt-and-suspenders: the onlisten hook should have already flipped this // to true; re-assert in case a future cdc-listener change skips dispatch. diff --git a/shared/database/src/cdc-listener.ts b/shared/database/src/cdc-listener.ts index 028157a2..11c7f9b6 100644 --- a/shared/database/src/cdc-listener.ts +++ b/shared/database/src/cdc-listener.ts @@ -10,6 +10,15 @@ * worker's /healthcheck within ~one probe-cycle + echo-timeout. Postgres-js's * `onlisten` callback (third arg of `listen()`) also dispatches connected=true * on the initial subscribe and on every auto-reconnect. + * + * Oversized + error visibility (BS#1120): migration 0094 routes payloads that + * exceed Postgres's 8000-byte `pg_notify` cap to a `cdc_oversized` channel and + * unexpected trigger exceptions to a `cdc_error` channel. Both have distinct + * payload shapes from the main `cdc` channel — see `CdcOversizedEvent` and + * `CdcErrorEvent` — and consumers register via `onCdcOversizedEvent` / + * `onCdcErrorEvent`. Wiring a Sentry sink in the process entry point (the + * dispatcher in the backend, `worker.ts` in the enrichment worker) gives the + * BS#1120 AC #3 metric the alert hook can drive off. */ import postgres from 'postgres'; @@ -22,15 +31,58 @@ export interface CdcEvent { timestamp: number; } +/** + * Payload shape emitted by `pg_notify('cdc_oversized', ...)` in migration 0094 + * when the would-be `cdc` payload exceeds the 7800-byte safety threshold. + * + * The originating mutation still committed — only the live notification was + * dropped. Consumers that need the row's new state must refetch it (by + * `primary_key` when present, otherwise by a source-of-truth scan). + */ +export interface CdcOversizedEvent { + table: string; + schema: string; + action: 'INSERT' | 'UPDATE' | 'DELETE'; + /** `data->>'id'` from the row, when the table has an `id` column. Null otherwise. */ + primary_key: string | null; + /** `octet_length(payload::text)` of the would-be `cdc` payload, in bytes. */ + payload_bytes: number; + timestamp: number; + reason: 'payload_too_large'; +} + +/** + * Payload shape emitted by `pg_notify('cdc_error', ...)` in migration 0094 when + * the trigger body raised an unexpected exception. Paired with a `RAISE + * WARNING` so PG logs still record the failure for forensics. + */ +export interface CdcErrorEvent { + table: string; + schema: string; + action: 'INSERT' | 'UPDATE' | 'DELETE'; + /** SQLSTATE of the underlying PL/pgSQL exception. */ + sqlstate: string; + /** SQLERRM of the underlying PL/pgSQL exception. */ + sqlerrm: string; + timestamp: number; + reason: 'trigger_exception'; +} + export type CdcEventCallback = (event: CdcEvent) => void; export type CdcConnectionStateCallback = (connected: boolean) => void; +export type CdcOversizedEventCallback = (event: CdcOversizedEvent) => void; +export type CdcErrorEventCallback = (event: CdcErrorEvent) => void; const CDC_CHANNEL = 'cdc'; const HEALTH_CHANNEL = 'cdc_health'; +const CDC_OVERSIZED_CHANNEL = 'cdc_oversized'; +const CDC_ERROR_CHANNEL = 'cdc_error'; let listenConnection: ReturnType | null = null; let callbacks: CdcEventCallback[] = []; let stateCallbacks: CdcConnectionStateCallback[] = []; +let oversizedCallbacks: CdcOversizedEventCallback[] = []; +let errorCallbacks: CdcErrorEventCallback[] = []; let livenessTimer: ReturnType | null = null; let outstandingProbeToken: string | null = null; @@ -45,6 +97,40 @@ export function onCdcEvent(callback: CdcEventCallback): void { callbacks.push(callback); } +/** + * Registers a callback to receive `cdc_oversized` events (BS#1120). + * + * Fired when a row's would-be `cdc` payload would exceed Postgres's 8000-byte + * `pg_notify` cap (migration 0094 cuts over at 7800 bytes to leave wire + * headroom). The originating mutation already committed; the live notification + * was dropped. Sinks are typically: + * + * - Sentry signal so an alert can fire on AC #3 (see `dispatcher.ts`, + * `worker.ts` for the wiring). + * - A refetch path keyed off `primary_key` (when non-null) for downstream + * consumers (SSE, enrichment, reconciliation) that need the row state. + * + * Multiple callbacks can be registered; all are invoked for each event. + */ +export function onCdcOversizedEvent(callback: CdcOversizedEventCallback): void { + oversizedCallbacks.push(callback); +} + +/** + * Registers a callback to receive `cdc_error` events (BS#1120). + * + * Fired when the `cdc_notify()` trigger body raised an unexpected exception + * (anything other than the oversized branch, which has its own channel). The + * trigger also emits `RAISE WARNING` so PG logs still record it for forensics; + * this callback is the listener-side visibility path so the failure isn't + * confined to PG logs the application servers don't tail. + * + * Multiple callbacks can be registered; all are invoked for each event. + */ +export function onCdcErrorEvent(callback: CdcErrorEventCallback): void { + errorCallbacks.push(callback); +} + /** * Registers a callback fired on CDC connection-state transitions. * @@ -118,7 +204,42 @@ export async function startCdcListener(): Promise { } ); - console.log('[cdc-listener] Listening on channel:', CDC_CHANNEL); + // BS#1120: fallback channels carry oversized/error notifications when the + // primary `cdc` payload would have been dropped. Subscribed alongside `cdc` + // so a single LISTEN connection covers all three. State callback intentionally + // omitted — the `cdc` re-LISTEN above already covers reconnect signaling, + // and these channels reuse the same socket. + await listenConnection.listen(CDC_OVERSIZED_CHANNEL, (payload: string) => { + try { + const event = JSON.parse(payload) as CdcOversizedEvent; + for (const cb of oversizedCallbacks) { + try { + cb(event); + } catch (err) { + console.error('[cdc-listener] Oversized callback error:', err); + } + } + } catch (err) { + console.error('[cdc-listener] Failed to parse cdc_oversized payload:', err); + } + }); + + await listenConnection.listen(CDC_ERROR_CHANNEL, (payload: string) => { + try { + const event = JSON.parse(payload) as CdcErrorEvent; + for (const cb of errorCallbacks) { + try { + cb(event); + } catch (err) { + console.error('[cdc-listener] Error callback error:', err); + } + } + } catch (err) { + console.error('[cdc-listener] Failed to parse cdc_error payload:', err); + } + }); + + console.log('[cdc-listener] Listening on channels:', CDC_CHANNEL, CDC_OVERSIZED_CHANNEL, CDC_ERROR_CHANNEL); } export interface LivenessProbeOptions { @@ -227,6 +348,8 @@ export async function stopCdcListener(): Promise { listenConnection = null; callbacks = []; stateCallbacks = []; + oversizedCallbacks = []; + errorCallbacks = []; console.log('[cdc-listener] Stopped'); } } diff --git a/shared/database/src/migrations/0096_cdc_oversized_fallback.sql b/shared/database/src/migrations/0096_cdc_oversized_fallback.sql new file mode 100644 index 00000000..796add06 --- /dev/null +++ b/shared/database/src/migrations/0096_cdc_oversized_fallback.sql @@ -0,0 +1,133 @@ +-- Make CDC trigger visibility safe: detect oversized payloads before pg_notify +-- and emit a minimal fallback notification instead of silently dropping the event, +-- and surface any other trigger failure through a dedicated error channel. +-- +-- Background: WXYC/Backend-Service#1120 +-- PostgreSQL's pg_notify enforces an 8000-byte payload limit. The previous +-- cdc_notify() (migration 0046) wrapped the call in a broad `EXCEPTION WHEN +-- OTHERS ... RAISE WARNING ... RETURN NULL` block. Because cdc_notify is wired +-- as an AFTER trigger, the returned NULL is ignored by Postgres — the +-- originating INSERT/UPDATE commits, but the notification never fires. The +-- only operator signal was a PG-log WARNING line that the application servers +-- don't tail. Risk hot-spot: the enrichment worker's flowsheet UPDATE writes +-- artist_bio (free text) plus seven streaming-URL columns in a single row; +-- `to_jsonb(NEW)` can plausibly cross 8000 bytes, dropping the `liveFs:update` +-- event that dj-site SSE clients depend on. +-- +-- Notification channels emitted by the updated function: +-- +-- cdc — primary channel, unchanged shape: +-- { table, schema, action, data, timestamp } +-- Consumers: cdc-listener.ts, cdc-websocket.ts, +-- metadata-broadcast, enrichment-worker. +-- +-- cdc_oversized — fired in place of `cdc` when the primary payload would +-- exceed the 7800-byte safety threshold (200 bytes of +-- headroom below the 8000-byte pg_notify cap to cover +-- JSON escaping in extremely rare edge cases). Carries: +-- { table, schema, action, primary_key, payload_bytes, +-- timestamp, reason: 'payload_too_large' } +-- `primary_key` is best-effort: `data->>'id'` if the row +-- has an `id` column, otherwise NULL. Consumers must +-- refetch the row from the source of truth (e.g. REST +-- by primary key, or a full table scan). New channel — +-- see cdc-listener.ts subscription update. +-- +-- cdc_error — fired when the trigger body raised an unexpected +-- exception (anything other than the oversized branch). +-- Carries: +-- { table, schema, action, sqlstate, sqlerrm, timestamp, +-- reason: 'trigger_exception' } +-- Backed by a `RAISE WARNING` so the PG log still records +-- the failure for forensics. New channel. +-- +-- Trigger contract: still AFTER INSERT/UPDATE/DELETE on the same 22+ tables +-- as 0046. AFTER triggers' return value is ignored by Postgres, but to keep +-- the function's behavior easy to reason about (and safe if a future +-- maintainer rewires it as BEFORE), we return NEW for INSERT/UPDATE and OLD +-- for DELETE. + +-- @no-precondition-needed: trigger-function replacement is idempotent; no +-- data-shape invariant required. + +CREATE OR REPLACE FUNCTION cdc_notify() RETURNS trigger AS $$ +DECLARE + payload jsonb; + payload_text text; + row_data jsonb; + pk_value text; + return_row record; +BEGIN + IF TG_OP = 'DELETE' THEN + row_data := to_jsonb(OLD); + return_row := OLD; + ELSE + row_data := to_jsonb(NEW); + return_row := NEW; + END IF; + + BEGIN + payload := jsonb_build_object( + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA, + 'action', TG_OP, + 'data', row_data, + 'timestamp', (extract(epoch from clock_timestamp()) * 1000)::bigint + ); + payload_text := payload::text; + + -- pg_notify hard-caps each payload at 8000 bytes. We check below 7800 + -- to leave headroom: the LISTEN-side JSON re-encode is identity, but + -- pathological Unicode in the row could grow the wire form by a few + -- bytes during transit. + IF octet_length(payload_text) > 7800 THEN + pk_value := row_data->>'id'; + PERFORM pg_notify( + 'cdc_oversized', + jsonb_build_object( + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA, + 'action', TG_OP, + 'primary_key', pk_value, + 'payload_bytes', octet_length(payload_text), + 'timestamp', (extract(epoch from clock_timestamp()) * 1000)::bigint, + 'reason', 'payload_too_large' + )::text + ); + RAISE WARNING 'cdc_notify oversized payload: table=% action=% bytes=% pk=%', + TG_TABLE_NAME, TG_OP, octet_length(payload_text), COALESCE(pk_value, ''); + ELSE + PERFORM pg_notify('cdc', payload_text); + END IF; + EXCEPTION WHEN OTHERS THEN + -- Last-resort visibility path. The previous behavior silently swallowed + -- the exception here and returned NULL. We keep returning the row (the + -- originating mutation must not roll back on a notify failure) but emit + -- a dedicated cdc_error notification *and* a WARNING so the failure is + -- visible to the listener fan-out, not just the PG log. + RAISE WARNING 'cdc_notify failed: table=% action=% sqlstate=% sqlerrm=%', + TG_TABLE_NAME, TG_OP, SQLSTATE, SQLERRM; + BEGIN + PERFORM pg_notify( + 'cdc_error', + jsonb_build_object( + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA, + 'action', TG_OP, + 'sqlstate', SQLSTATE, + 'sqlerrm', SQLERRM, + 'timestamp', (extract(epoch from clock_timestamp()) * 1000)::bigint, + 'reason', 'trigger_exception' + )::text + ); + EXCEPTION WHEN OTHERS THEN + -- pg_notify on cdc_error itself failed (e.g. its own payload too + -- large, which would be extraordinary given the fixed shape). + -- Nothing else to do; the outer WARNING above is the last signal. + NULL; + END; + END; + + RETURN return_row; +END; +$$ LANGUAGE plpgsql; diff --git a/shared/database/src/migrations/meta/0096_snapshot.json b/shared/database/src/migrations/meta/0096_snapshot.json new file mode 100644 index 00000000..5e43c0c6 --- /dev/null +++ b/shared/database/src/migrations/meta/0096_snapshot.json @@ -0,0 +1,4608 @@ +{ + "id": "c234d50f-46dc-4d1d-8e48-3687f1cedaaa", + "prevId": "a6af8b03-b9f0-468c-8ae8-80ffc614a65a", + "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 15b7edcc..db4aa77f 100644 --- a/shared/database/src/migrations/meta/_journal.json +++ b/shared/database/src/migrations/meta/_journal.json @@ -659,6 +659,13 @@ "when": 1781468384340, "tag": "0095_drop-stale-trigram-indexes", "breakpoints": true + }, + { + "idx": 96, + "version": "7", + "when": 1781468384341, + "tag": "0096_cdc_oversized_fallback", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/shared/database/src/migrations/meta/applied-hashes.json b/shared/database/src/migrations/meta/applied-hashes.json index 5bf3880c..7816c2eb 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -92,5 +92,6 @@ "0092_normalize-artist-name": "57bf7f03733591917917c5a19b9b67d8527a64ccce224cdc3f1102f4a9cca368", "0093_concerts-first-scraped-at": "bbeee146c92ab63fa70e9f2c537d5a06a13f80a5f52c23cb5a1bed1432057421", "0094_rotation-lml-identity-id": "10134a18381f7daad56ca2d8cae80723832be3158cbe6dd01aae2f8e6afcaaa0", - "0095_drop-stale-trigram-indexes": "aba5b26a66102d427e1abd6520aa1f7ef14b776eaaa87ea8b99c3b97e79d24c7" + "0095_drop-stale-trigram-indexes": "aba5b26a66102d427e1abd6520aa1f7ef14b776eaaa87ea8b99c3b97e79d24c7", + "0096_cdc_oversized_fallback": "5615c7a6aa7514b139340ef6c8ba86a548ac6e7b6f9f0c39241bea2a62a1274b" } diff --git a/tests/integration/cdc-oversized-fallback.spec.js b/tests/integration/cdc-oversized-fallback.spec.js new file mode 100644 index 00000000..567dc733 --- /dev/null +++ b/tests/integration/cdc-oversized-fallback.spec.js @@ -0,0 +1,190 @@ +/** + * Integration test for the CDC oversized-payload + error-branch fallback + * paths (WXYC/Backend-Service#1120). + * + * Background: migration 0046 wrapped `pg_notify('cdc', payload::text)` in a + * broad `EXCEPTION WHEN OTHERS ... RAISE WARNING ... RETURN NULL` block. + * Since cdc_notify is an AFTER trigger, the returned NULL is ignored — the + * mutation commits, but the CDC event is silently dropped when the payload + * exceeds Postgres's 8000-byte pg_notify cap. The 2026-06-13 migration 0094 + * detects oversized payloads up-front and emits a minimal-payload + * `cdc_oversized` notification carrying (table, schema, action, primary_key) + * instead, and routes unexpected exceptions through a dedicated `cdc_error` + * channel. + * + * This spec exercises both new channels against real Postgres: + * + * 1. Normal-size INSERT → cdc fires, cdc_oversized is silent. + * 2. INSERT whose to_jsonb(NEW) crosses the 7800-byte threshold → + * cdc_oversized fires with reason='payload_too_large' and a primary_key + * string; cdc is silent for that row. + * 3. The originating mutation commits regardless (visibility, not data + * safety, is the contract). + * + * Pure SQL (babel-jest runner, no TS import) — same pattern as + * flowsheet-etl-cdc-delivery.spec.js. + */ + +const postgres = require('postgres'); +const { getTestDb } = require('../utils/db'); + +const SCHEMA = process.env.WXYC_SCHEMA_NAME || 'wxyc_schema'; + +async function waitForEvent(events, predicate, timeoutMs = 2000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const match = events.find(predicate); + if (match) return match; + await new Promise((r) => setTimeout(r, 25)); + } + return null; +} + +describe('CDC oversized + error fallback channels (real PG)', () => { + let sql; + let listenConn; + const cdcEvents = []; + const oversizedEvents = []; + const errorEvents = []; + const insertedIds = []; + + beforeAll(async () => { + sql = getTestDb(); + listenConn = 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', + username: process.env.DB_USERNAME || 'test-user', + password: process.env.DB_PASSWORD || 'test-pw', + onnotice: () => {}, + }); + await listenConn.listen('cdc', (payload) => { + try { + cdcEvents.push(JSON.parse(payload)); + } catch { + /* ignore */ + } + }); + await listenConn.listen('cdc_oversized', (payload) => { + try { + oversizedEvents.push(JSON.parse(payload)); + } catch { + /* ignore */ + } + }); + await listenConn.listen('cdc_error', (payload) => { + try { + errorEvents.push(JSON.parse(payload)); + } catch { + /* ignore */ + } + }); + }); + + afterAll(async () => { + if (insertedIds.length > 0) { + await sql`DELETE FROM ${sql(SCHEMA)}.flowsheet WHERE id = ANY(${insertedIds})`; + } + if (listenConn) await listenConn.end(); + }); + + beforeEach(() => { + cdcEvents.length = 0; + oversizedEvents.length = 0; + errorEvents.length = 0; + }); + + test('normal-size flowsheet INSERT fires `cdc`, not `cdc_oversized`', async () => { + const rows = await sql` + INSERT INTO ${sql(SCHEMA)}.flowsheet + (entry_type, artist_name, album_title, track_title, play_order, request_flag, segue) + VALUES + ('track', '#1120 normal artist', '#1120 album', '#1120 track', 89901, false, false) + RETURNING id + `; + const id = rows[0].id; + insertedIds.push(id); + + const event = await waitForEvent(cdcEvents, (e) => e.data?.id === id); + expect(event).not.toBeNull(); + expect(event.action).toBe('INSERT'); + + // No oversized fallback should have fired for this row. + const oversized = await waitForEvent(oversizedEvents, (e) => Number(e.primary_key) === id, 150); + expect(oversized).toBeNull(); + }); + + test('oversized flowsheet INSERT fires `cdc_oversized` fallback, not `cdc`', async () => { + // Build a ~10 KB artist_bio. With the rest of the row, to_jsonb(NEW) is + // guaranteed to cross the 7800-byte safety threshold. + const bigBio = 'x'.repeat(10000); + const rows = await sql` + INSERT INTO ${sql(SCHEMA)}.flowsheet + (entry_type, artist_name, album_title, track_title, artist_bio, + play_order, request_flag, segue) + VALUES + ('track', '#1120 oversized', '#1120 big album', '#1120 big track', ${bigBio}, + 89902, false, false) + RETURNING id + `; + const id = rows[0].id; + insertedIds.push(id); + + const fallback = await waitForEvent(oversizedEvents, (e) => Number(e.primary_key) === id); + expect(fallback).not.toBeNull(); + expect(fallback.table).toBe('flowsheet'); + expect(fallback.schema).toBe(SCHEMA); + expect(fallback.action).toBe('INSERT'); + expect(fallback.reason).toBe('payload_too_large'); + expect(typeof fallback.payload_bytes).toBe('number'); + expect(fallback.payload_bytes).toBeGreaterThan(7800); + expect(typeof fallback.timestamp).toBe('number'); + + // The full `cdc` channel must NOT have received this row. + const fullEvent = await waitForEvent(cdcEvents, (e) => e.data?.id === id, 200); + expect(fullEvent).toBeNull(); + }); + + test('oversized INSERT still commits the row (visibility-only failure mode)', async () => { + const bigBio = 'y'.repeat(10000); + const rows = await sql` + INSERT INTO ${sql(SCHEMA)}.flowsheet + (entry_type, artist_name, album_title, track_title, artist_bio, + play_order, request_flag, segue) + VALUES + ('track', '#1120 commit-check', '#1120 album', '#1120 track', ${bigBio}, + 89903, false, false) + RETURNING id + `; + const id = rows[0].id; + insertedIds.push(id); + + const persisted = await sql` + SELECT id, artist_name FROM ${sql(SCHEMA)}.flowsheet WHERE id = ${id} + `; + expect(persisted.length).toBe(1); + expect(persisted[0].artist_name).toBe('#1120 commit-check'); + + // And the oversized fallback fired (sanity). + const fallback = await waitForEvent(oversizedEvents, (e) => Number(e.primary_key) === id); + expect(fallback).not.toBeNull(); + }); + + test('cdc_notify() function is the post-0094 shape (declares cdc_oversized + cdc_error)', async () => { + // Pin: the deployed function body references both new channels by name. + // Catches accidental rollbacks of the migration without exercising the + // full overflow path. Cheap last-line-of-defense. + const result = await sql` + SELECT pg_get_functiondef(p.oid) AS body + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE p.proname = 'cdc_notify' AND n.nspname IN ('public', ${SCHEMA}) + LIMIT 1 + `; + expect(result.length).toBe(1); + const body = result[0].body; + expect(body).toMatch(/cdc_oversized/); + expect(body).toMatch(/cdc_error/); + expect(body).toMatch(/payload_too_large/); + }); +}); diff --git a/tests/unit/apps/backend/services/cdc/cdc-websocket.test.ts b/tests/unit/apps/backend/services/cdc/cdc-websocket.test.ts index 7667e5a8..99764f9a 100644 --- a/tests/unit/apps/backend/services/cdc/cdc-websocket.test.ts +++ b/tests/unit/apps/backend/services/cdc/cdc-websocket.test.ts @@ -36,6 +36,8 @@ jest.mock('@wxyc/database', () => ({ onCdcEvent: jest.fn(), + onCdcOversizedEvent: jest.fn(), + onCdcErrorEvent: jest.fn(), startCdcListener: jest.fn().mockResolvedValue(undefined), stopCdcListener: jest.fn().mockResolvedValue(undefined), })); @@ -69,7 +71,7 @@ jest.mock('ws', () => { }); import type { Server as HttpServer } from 'http'; -import { onCdcEvent, startCdcListener, stopCdcListener } from '@wxyc/database'; +import { onCdcErrorEvent, onCdcEvent, onCdcOversizedEvent, startCdcListener, stopCdcListener } from '@wxyc/database'; import { WebSocketServer } from 'ws'; import { setupCdcWebSocket, shutdownCdcWebSocket } from '../../../../../../apps/backend/services/cdc/cdc-websocket'; import { startCdcDispatcher, shutdownCdcDispatcher } from '../../../../../../apps/backend/services/cdc/dispatcher'; @@ -222,6 +224,124 @@ describe('setupCdcWebSocket (BS#1187)', () => { }); }); +describe('startCdcDispatcher BS#1120 fallback sinks', () => { + // BS#1120 / AC #3: migration 0094 emits to `cdc_oversized` and `cdc_error` + // when the primary `cdc` payload would have been dropped (oversized) or the + // trigger body raised. The dispatcher's job is to bridge those into Sentry + // captureMessage calls so the alert hook can fire. Pins: + // 1. Both channel subscriptions are wired on startCdcDispatcher. + // 2. An oversized event produces a Sentry.captureMessage('cdc.oversized_payload', ...). + // 3. A cdc_error event produces a Sentry.captureMessage('cdc.trigger_exception', ...). + // 4. Double-start does not double-wire (idempotency latch). + // 5. shutdownCdcDispatcher drops the latch so a re-start re-wires. + + beforeEach(async () => { + jest.clearAllMocks(); + // Drop any latch left over from a prior describe's startCdcDispatcher call. + await shutdownCdcDispatcher(); + jest.clearAllMocks(); + }); + + afterEach(async () => { + await shutdownCdcDispatcher(); + }); + + it('subscribes to both cdc_oversized and cdc_error on dispatcher start', async () => { + await startCdcDispatcher(); + expect(onCdcOversizedEvent).toHaveBeenCalledTimes(1); + expect(onCdcErrorEvent).toHaveBeenCalledTimes(1); + }); + + it('captureMessages cdc.oversized_payload with table + action + reason tags when an oversized event arrives', async () => { + await startCdcDispatcher(); + // The dispatcher registered a callback via onCdcOversizedEvent — pull it + // out of the mock and invoke it directly. + const oversizedCb = (onCdcOversizedEvent as jest.Mock).mock.calls[0][0] as (e: unknown) => void; + + oversizedCb({ + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'UPDATE', + primary_key: '42', + payload_bytes: 8501, + timestamp: 1_700_000_000_000, + reason: 'payload_too_large', + }); + + expect(captureMessageMock).toHaveBeenCalledTimes(1); + expect(captureMessageMock).toHaveBeenCalledWith( + 'cdc.oversized_payload', + expect.objectContaining({ + level: 'warning', + tags: expect.objectContaining({ + subsystem: 'cdc', + table: 'flowsheet', + action: 'UPDATE', + reason: 'payload_too_large', + }), + extra: expect.objectContaining({ + schema: 'wxyc_schema', + primary_key: '42', + payload_bytes: 8501, + }), + fingerprint: ['cdc-oversized-payload'], + }) + ); + }); + + it('captureMessages cdc.trigger_exception with sqlstate tag when a cdc_error event arrives', async () => { + await startCdcDispatcher(); + const errorCb = (onCdcErrorEvent as jest.Mock).mock.calls[0][0] as (e: unknown) => void; + + errorCb({ + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'INSERT', + sqlstate: '22023', + sqlerrm: 'invalid_parameter_value', + timestamp: 1_700_000_000_000, + reason: 'trigger_exception', + }); + + expect(captureMessageMock).toHaveBeenCalledTimes(1); + expect(captureMessageMock).toHaveBeenCalledWith( + 'cdc.trigger_exception', + expect.objectContaining({ + level: 'error', + tags: expect.objectContaining({ + subsystem: 'cdc', + table: 'flowsheet', + action: 'INSERT', + reason: 'trigger_exception', + sqlstate: '22023', + }), + extra: expect.objectContaining({ + schema: 'wxyc_schema', + sqlerrm: 'invalid_parameter_value', + }), + fingerprint: ['cdc-trigger-exception'], + }) + ); + }); + + it('does not re-wire fallback sinks on a second startCdcDispatcher call (idempotency)', async () => { + await startCdcDispatcher(); + await startCdcDispatcher(); + // Without the latch, each call would register a fresh sink → 2 captures + // per inbound event. With the latch, exactly one. + expect(onCdcOversizedEvent).toHaveBeenCalledTimes(1); + expect(onCdcErrorEvent).toHaveBeenCalledTimes(1); + }); + + it('shutdownCdcDispatcher drops the latch so a subsequent start re-wires', async () => { + await startCdcDispatcher(); + await shutdownCdcDispatcher(); + await startCdcDispatcher(); + expect(onCdcOversizedEvent).toHaveBeenCalledTimes(2); + expect(onCdcErrorEvent).toHaveBeenCalledTimes(2); + }); +}); + describe('shutdownCdcWebSocket (BS#1187)', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/tests/unit/shared/database/cdc-listener.test.ts b/tests/unit/shared/database/cdc-listener.test.ts index e9fcaaed..52849d86 100644 --- a/tests/unit/shared/database/cdc-listener.test.ts +++ b/tests/unit/shared/database/cdc-listener.test.ts @@ -38,6 +38,8 @@ interface MockHandles { cdcOnNotify?: (payload: string) => void; cdcOnListen?: () => void; healthOnNotify?: (payload: string) => void; + cdcOversizedOnNotify?: (payload: string) => void; + cdcErrorOnNotify?: (payload: string) => void; } function makeMockSql(): MockHandles { @@ -50,6 +52,10 @@ function makeMockSql(): MockHandles { if (onlisten) onlisten(); } else if (channel === 'cdc_health') { handles.healthOnNotify = onnotify; + } else if (channel === 'cdc_oversized') { + handles.cdcOversizedOnNotify = onnotify; + } else if (channel === 'cdc_error') { + handles.cdcErrorOnNotify = onnotify; } return Promise.resolve({ unlisten: jest.fn(() => Promise.resolve()) }); }), @@ -143,9 +149,10 @@ describe('cdc-listener liveness (BS#1014)', () => { it('registers a LISTEN on cdc_health and starts an interval timer', async () => { await cdc.startCdcListener(); await cdc.enableLivenessProbe({ probeIntervalMs: 5000, echoTimeoutMs: 12_000 }); - // Two LISTENs total: cdc + cdc_health + // Four LISTENs total: cdc + cdc_oversized + cdc_error (from startCdcListener, + // post-BS#1120) + cdc_health (added by enableLivenessProbe). const channels = handles.sql.listen.mock.calls.map((c) => c[0]); - expect(channels).toEqual(['cdc', 'cdc_health']); + expect(channels).toEqual(['cdc', 'cdc_oversized', 'cdc_error', 'cdc_health']); }); it('no-ops on second call (warns once)', async () => { @@ -332,3 +339,219 @@ describe('cdc-listener liveness (BS#1014)', () => { }); }); }); + +describe('cdc-listener fallback channels (BS#1120)', () => { + let cdc: CdcListenerModule; + let handles: MockHandles; + + beforeEach(async () => { + jest.resetModules(); + handles = makeMockSql(); + postgresFactory.mockReset(); + postgresFactory.mockReturnValue(handles.sql); + cdc = await import('../../../../shared/database/src/cdc-listener'); + }); + + afterEach(async () => { + await cdc.stopCdcListener(); + }); + + describe('subscription', () => { + it('subscribes to cdc, cdc_oversized, and cdc_error on startCdcListener', async () => { + await cdc.startCdcListener(); + const channels = handles.sql.listen.mock.calls.map((c) => c[0]); + expect(channels).toEqual(['cdc', 'cdc_oversized', 'cdc_error']); + }); + }); + + describe('onCdcOversizedEvent', () => { + it('dispatches a parsed CdcOversizedEvent when cdc_oversized fires', async () => { + const cb = jest.fn(); + cdc.onCdcOversizedEvent(cb); + await cdc.startCdcListener(); + + const payload = { + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'UPDATE', + primary_key: '42', + payload_bytes: 8500, + timestamp: 1_700_000_000_000, + reason: 'payload_too_large', + }; + handles.cdcOversizedOnNotify?.(JSON.stringify(payload)); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(payload); + }); + + it('invokes all registered callbacks on a single notification', async () => { + const a = jest.fn(); + const b = jest.fn(); + cdc.onCdcOversizedEvent(a); + cdc.onCdcOversizedEvent(b); + await cdc.startCdcListener(); + + handles.cdcOversizedOnNotify?.( + JSON.stringify({ + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'INSERT', + primary_key: null, + payload_bytes: 9001, + timestamp: 1_700_000_000_000, + reason: 'payload_too_large', + }) + ); + + expect(a).toHaveBeenCalledTimes(1); + expect(b).toHaveBeenCalledTimes(1); + }); + + it('isolates a throwing callback so siblings still run', async () => { + const consoleErr = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const bad = jest.fn(() => { + throw new Error('boom'); + }); + const good = jest.fn(); + cdc.onCdcOversizedEvent(bad); + cdc.onCdcOversizedEvent(good); + await cdc.startCdcListener(); + + handles.cdcOversizedOnNotify?.( + JSON.stringify({ + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'UPDATE', + primary_key: '1', + payload_bytes: 8000, + timestamp: 1_700_000_000_000, + reason: 'payload_too_large', + }) + ); + + expect(bad).toHaveBeenCalled(); + expect(good).toHaveBeenCalled(); + expect(consoleErr).toHaveBeenCalledWith('[cdc-listener] Oversized callback error:', expect.any(Error)); + consoleErr.mockRestore(); + }); + + it('logs and does not throw on a malformed payload', async () => { + const consoleErr = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const cb = jest.fn(); + cdc.onCdcOversizedEvent(cb); + await cdc.startCdcListener(); + + expect(() => handles.cdcOversizedOnNotify?.('not-json')).not.toThrow(); + expect(cb).not.toHaveBeenCalled(); + expect(consoleErr).toHaveBeenCalledWith( + '[cdc-listener] Failed to parse cdc_oversized payload:', + expect.any(Error) + ); + consoleErr.mockRestore(); + }); + }); + + describe('onCdcErrorEvent', () => { + it('dispatches a parsed CdcErrorEvent when cdc_error fires', async () => { + const cb = jest.fn(); + cdc.onCdcErrorEvent(cb); + await cdc.startCdcListener(); + + const payload = { + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'INSERT', + sqlstate: '22023', + sqlerrm: 'invalid_parameter_value', + timestamp: 1_700_000_000_000, + reason: 'trigger_exception', + }; + handles.cdcErrorOnNotify?.(JSON.stringify(payload)); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(payload); + }); + + it('isolates a throwing callback so siblings still run', async () => { + const consoleErr = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const bad = jest.fn(() => { + throw new Error('boom'); + }); + const good = jest.fn(); + cdc.onCdcErrorEvent(bad); + cdc.onCdcErrorEvent(good); + await cdc.startCdcListener(); + + handles.cdcErrorOnNotify?.( + JSON.stringify({ + table: 'flowsheet', + schema: 'wxyc_schema', + action: 'UPDATE', + sqlstate: 'XX000', + sqlerrm: 'internal_error', + timestamp: 1_700_000_000_000, + reason: 'trigger_exception', + }) + ); + + expect(bad).toHaveBeenCalled(); + expect(good).toHaveBeenCalled(); + expect(consoleErr).toHaveBeenCalledWith('[cdc-listener] Error callback error:', expect.any(Error)); + consoleErr.mockRestore(); + }); + + it('logs and does not throw on a malformed payload', async () => { + const consoleErr = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const cb = jest.fn(); + cdc.onCdcErrorEvent(cb); + await cdc.startCdcListener(); + + expect(() => handles.cdcErrorOnNotify?.('not-json')).not.toThrow(); + expect(cb).not.toHaveBeenCalled(); + expect(consoleErr).toHaveBeenCalledWith('[cdc-listener] Failed to parse cdc_error payload:', expect.any(Error)); + consoleErr.mockRestore(); + }); + }); + + describe('stopCdcListener', () => { + it('clears oversized + error callback arrays so a re-start does not double-fire', async () => { + const oversized = jest.fn(); + const errored = jest.fn(); + cdc.onCdcOversizedEvent(oversized); + cdc.onCdcErrorEvent(errored); + await cdc.startCdcListener(); + await cdc.stopCdcListener(); + + // Fresh start should see empty callback arrays — no callbacks fire. + handles = makeMockSql(); + postgresFactory.mockReturnValue(handles.sql); + await cdc.startCdcListener(); + handles.cdcOversizedOnNotify?.( + JSON.stringify({ + table: 't', + schema: 's', + action: 'INSERT', + primary_key: null, + payload_bytes: 9000, + timestamp: 0, + reason: 'payload_too_large', + }) + ); + handles.cdcErrorOnNotify?.( + JSON.stringify({ + table: 't', + schema: 's', + action: 'INSERT', + sqlstate: 'XX000', + sqlerrm: 'x', + timestamp: 0, + reason: 'trigger_exception', + }) + ); + + expect(oversized).not.toHaveBeenCalled(); + expect(errored).not.toHaveBeenCalled(); + }); + }); +});