From 48827820e86eb6292fcf9a4439e7fe3ef26c263e Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Mon, 11 May 2026 09:41:12 -0700 Subject: [PATCH 1/3] migrations(0076): deploy wxyc_identity_match_* plpgsql functions (closes #805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendors the canonical artifacts from WXYC/wxyc-etl@v0.4.0 (`data/`) under `vendor/wxyc-etl/` and ships them as migration 0076. The migration sets up the `wxyc_unaccent` text-search dictionary, then inlines the canonical four-function SQL byte-for-byte (drizzle-kit applies plain SQL in a single transaction, so `\i` isn't an option). SHA-pinned in `wxyc-etl-pin.txt`. Both `db` (dev profile) and `ci-db` (ci profile) in `dev_env/docker-compose.yml` now mount the rules + version files into `/usr/local/share/postgresql/tsearch_data/` so the dictionary creates cleanly on first migrate. The integration spec at `tests/integration/wxyc-identity-match-functions.spec.js` exercises three layers: pin SHA freshness, migration-vs-canonical byte-equality, and a small canonical-artist smoke + idempotence on the live PG. Column flip on `library_identity*` is deliberately out of scope here — gated on the E2-BS step-2 backfill PR (#663) per the ticket and the wiki §3.3.0 row 6. This migration ships the function definitions so the backfill window has them available. The journal entry uses `when = previous + 1ms` per the hand-edit recipe in `docs/migrations.md`. Snapshot 0076 mirrors 0075's table/enum/etc state with new id/prevId UUIDs since no schema-level changes accompany the function deploy. --- dev_env/docker-compose.yml | 9 + docs/migrations.md | 4 + .../0076_wxyc-identity-match-functions.sql | 327 ++ .../src/migrations/meta/0076_snapshot.json | 3877 +++++++++++++++++ .../src/migrations/meta/_journal.json | 9 +- .../src/migrations/meta/applied-hashes.json | 3 +- .../wxyc-identity-match-functions.spec.js | 127 + .../wxyc_identity_match_functions.sql | 302 ++ vendor/wxyc-etl/wxyc_unaccent.rules | 433 ++ vendor/wxyc-etl/wxyc_unaccent.version | 1 + wxyc-etl-pin.txt | 18 + 11 files changed, 5108 insertions(+), 2 deletions(-) create mode 100644 shared/database/src/migrations/0076_wxyc-identity-match-functions.sql create mode 100644 shared/database/src/migrations/meta/0076_snapshot.json create mode 100644 tests/integration/wxyc-identity-match-functions.spec.js create mode 100644 vendor/wxyc-etl/wxyc_identity_match_functions.sql create mode 100644 vendor/wxyc-etl/wxyc_unaccent.rules create mode 100644 vendor/wxyc-etl/wxyc_unaccent.version create mode 100644 wxyc-etl-pin.txt diff --git a/dev_env/docker-compose.yml b/dev_env/docker-compose.yml index ee351e51..d6660c06 100644 --- a/dev_env/docker-compose.yml +++ b/dev_env/docker-compose.yml @@ -12,6 +12,11 @@ services: - '${DB_PORT:-5432}:5432' volumes: - pg-data:/var/lib/postgresql/data + # Required by migration 0076: the wxyc_unaccent text-search dictionary + # loads its rules from $SHAREDIR/tsearch_data/. Vendored from + # WXYC/wxyc-etl@v0.4.0; SHA-pinned in wxyc-etl-pin.txt. + - ../vendor/wxyc-etl/wxyc_unaccent.rules:/usr/local/share/postgresql/tsearch_data/wxyc_unaccent.rules:ro + - ../vendor/wxyc-etl/wxyc_unaccent.version:/usr/local/share/postgresql/tsearch_data/wxyc_unaccent.version:ro healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${DB_USERNAME} -d ${DB_NAME} || pg_isready'] interval: 2s @@ -56,6 +61,10 @@ services: - '${CI_DB_PORT:-5433}:5432' volumes: - ci-pg-data:/var/lib/postgresql/data + # Mirror of the `db` mounts so migration 0076 finds the wxyc_unaccent + # rules file inside the CI container too. + - ../vendor/wxyc-etl/wxyc_unaccent.rules:/usr/local/share/postgresql/tsearch_data/wxyc_unaccent.rules:ro + - ../vendor/wxyc-etl/wxyc_unaccent.version:/usr/local/share/postgresql/tsearch_data/wxyc_unaccent.version:ro healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${DB_USERNAME} -d ${DB_NAME} || pg_isready'] interval: 2s diff --git a/docs/migrations.md b/docs/migrations.md index dbefcfa7..cc856ce2 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -81,6 +81,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS ... The same shape covers `NOT NULL` (count `WHERE col IS NULL`), `CHECK` (count rows that violate the predicate), and `FK` (count orphans via `LEFT JOIN ... WHERE referenced.id IS NULL`). The pattern is the same prevention this codebase already uses on the 0053 + `jobs/flowsheet-dj-name-backfill/` + 0054 chain — generalize it to any constraint-adding migration. Some constraints are provably safe (e.g. a `UNIQUE` index on a freshly-added nullable column, or `NOT NULL` paired with a `DEFAULT`); when no real precondition exists, document the reasoning with a `-- @no-precondition-needed: ` comment so the linter (`scripts/validate-migrations.mjs` Check 8) suppresses its warning. The PR-bot data-shape report (companion #703) catches violations at PR time; the precondition guard is the last line of defense at apply time. + + +**`wxyc_identity_match_*` function vendoring (cross-cache-identity §3.3.5).** The four plpgsql functions (`wxyc_identity_match_artist`, `_title`, `_with_punctuation`, `_with_disambiguator_strip`) are deployed by migration `0076_wxyc-identity-match-functions.sql`. The canonical bodies live in `WXYC/wxyc-etl@v0.4.0` (`data/`) and are vendored byte-for-byte into this repo at `vendor/wxyc-etl/` with SHA pins in `wxyc-etl-pin.txt`. The migration is a generated wrapper: extension + text-search dictionary setup followed by the canonical SQL inlined verbatim (drizzle-kit applies plain SQL files in a single transaction, so `\i` isn't available). The dictionary loads its rules from `$SHAREDIR/tsearch_data/wxyc_unaccent.rules` — `dev_env/docker-compose.yml` mounts both `wxyc_unaccent.rules` and `.version` into that path on `db` and `ci-db`. Refresh procedure: re-vendor from a new wxyc-etl tag, bump the SHAs + version in `wxyc-etl-pin.txt`, regenerate a follow-on migration with the new content, rerun `tests/integration/wxyc-identity-match-functions.spec.js`. Column-side flip on `library_identity*` is deliberately downstream (gated on the E2-BS step-2 backfill PR #663) — this rule only governs the function-deploy half. + **Cross-cache-identity precondition guards (cross-epic, project-scoped).** The precondition-guard pattern above (0053 → `jobs/flowsheet-dj-name-backfill/` → 0054, scoped within a single migration chain) extends to **cross-epic** prerequisite chains for the cross-cache-identity project. The substrate migration `0NNN_library_identity_substrate.sql` (filed under WXYC/Backend-Service#663; PR number to be backfilled here when the substrate PR opens) ships its gate-check at `scripts/check-library-identity-gate.sql`. Any migration in any epic that FK-references `library_identity` / `library_identity_source` / `library_identity_history`, or adds a `NOT NULL` / `UNIQUE` / `CHECK` constraint to those tables, must include a `DO $$ ... RAISE EXCEPTION ... END $$;` block that calls or inlines the gate-check's `truly_unresolved_rows < 1000` predicate. Same mechanism as 0053/0054, scoped across epics rather than within one chain. Plan reference: `WXYC/wiki/plans/library-hook-canonicalization-plan.md` §3.2.3. CI enforcement is the `Migration guards` job in `.github/workflows/test.yml`, which runs `scripts/check-precondition-guards.sh` (ships with the substrate PR); a migration that legitimately doesn't need the guard opts out with a `-- precondition-guard: not-required (rationale)` first line. diff --git a/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql b/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql new file mode 100644 index 00000000..657762cb --- /dev/null +++ b/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql @@ -0,0 +1,327 @@ +-- Cross-cache-identity match form (wiki §3.3.5). +-- +-- Deploys the four `wxyc_identity_match_*` plpgsql functions onto the Backend's +-- `wxyc_schema` so cross-cache identity-form expressions can join cleanly across +-- the four caches. Vendored byte-for-byte from WXYC/wxyc-etl@v0.4.0 (`data/`, +-- mirrored here at `vendor/wxyc-etl/`); SHA pin lives at `wxyc-etl-pin.txt`. +-- The sqlx-cli-style wrapper prelude (extension + text-search dictionary) is +-- prepended because drizzle-kit applies plain SQL files in a single transaction, +-- and `\i` isn't available. A test in +-- `apps/backend/tests/integration/db/wxycIdentityMatchFunctions.test.ts` +-- asserts the four entry points return the expected normalization on a small +-- WXYC-canonical artist set. +-- +-- Backend's column flip on `library_identity*` is downstream of this migration +-- — gated on the E2-BS step-2 backfill PR (#663). This file just ships the +-- function definitions so the backfill window has them available. + +CREATE EXTENSION IF NOT EXISTS unaccent; + +DROP TEXT SEARCH DICTIONARY IF EXISTS wxyc_unaccent; +CREATE TEXT SEARCH DICTIONARY wxyc_unaccent ( + TEMPLATE = unaccent, + RULES = 'wxyc_unaccent' +); + +-- Canonical SQL implementation of the cross-cache-identity match form. +-- +-- Vendored verbatim into every cache repo (discogs-etl, musicbrainz-cache, +-- wikidata-cache) and Backend-Service. The four function bodies must produce +-- byte-identical output to the corresponding Rust entry points in +-- `wxyc_etl::text::identity`: +-- +-- wxyc_identity_match_artist <-> to_identity_match_form +-- wxyc_identity_match_title <-> to_identity_match_form_title +-- wxyc_identity_match_with_punctuation <-> to_identity_match_form_with_punctuation +-- wxyc_identity_match_with_disambiguator_strip +-- <-> to_identity_match_form_with_disambiguator_strip +-- +-- Parity is asserted by `wxyc-etl/tests/postgres_parity_test.rs` against the +-- 252-row fixture in `wxyc-etl/tests/fixtures/identity_normalization_cases.csv`. +-- +-- Required Postgres version: 16+ (Unicode property classes, `normalize()`, +-- stable regex behavior). Required extension: `unaccent` configured with the +-- `wxyc_unaccent` text-search dictionary installed from +-- `data/wxyc_unaccent.rules`. +-- +-- Vendoring contract: each consumer carries `wxyc-etl-pin.txt` recording the +-- SHA-256 of `data/wxyc_unaccent.rules` and the version header read from the +-- file's first comment line. Mismatch fails CI. See +-- `wxyc-etl/docs/postgres-analog-vendoring.md`. + +DO $$ +BEGIN + IF current_setting('server_version_num')::int < 160000 THEN + RAISE EXCEPTION 'wxyc identity-match functions require Postgres 16+; got %', + current_setting('server_version'); + END IF; +END $$; + +-- The wxyc_unaccent dictionary must be created before this file loads. +-- Consumer migrations do: +-- CREATE EXTENSION IF NOT EXISTS unaccent; +-- CREATE TEXT SEARCH DICTIONARY wxyc_unaccent ( +-- TEMPLATE = unaccent, RULES = 'wxyc_unaccent' +-- ); +-- followed by the rules-file SHA verification block (see vendoring docs). + +-- --------------------------------------------------------------------------- +-- Base match-form pipeline. +-- +-- Mirror of `wxyc_etl::text::to_match_form` after the storage-form pass +-- (no mojibake repair — callers responsible for storing pre-cleaned bytes). +-- Pipeline: +-- normalize NFKC -> lower -> wxyc_unaccent dictionary -> strip-Cf-except-ZWJ +-- -> collapse-ASCII-space + trim. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_match_form(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; + zwj text := chr(8205); -- U+200D + cf_pattern text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := normalize(s, NFKC); + r := lower(r); + r := unaccent('wxyc_unaccent', r); + -- Strip Cf (format) characters except U+200D ZWJ (emoji integrity), matching + -- `strip_cf_except_zwj` in the Rust pipeline. Postgres regex has no + -- `\p{Cf}` and no char-class subtraction; build the class from explicit + -- BMP Cf codepoints split around ZWJ. Supplementary-plane Cf (U+E0001 etc.) + -- is rare in music-catalog data and intentionally not handled here. + cf_pattern := + '[' + || chr(173) -- U+00AD soft hyphen + || chr(1564) -- U+061C ALM + || chr(1757) -- U+06DD ARABIC END OF AYAH + || chr(1807) -- U+070F SYRIAC ABBREV MARK + || chr(2274) -- U+08E2 ARABIC DISPUTED END OF AYAH + || chr(6158) -- U+180E MONG VOWEL SEP + || chr(8203) || '-' || chr(8204) -- U+200B-U+200C (200D ZWJ skipped) + || chr(8206) || '-' || chr(8207) -- U+200E-U+200F + || chr(8234) || '-' || chr(8238) -- U+202A-U+202E + || chr(8288) || '-' || chr(8303) -- U+2060-U+206F + || chr(65279) -- U+FEFF BOM + || chr(65529) || '-' || chr(65531) -- U+FFF9-U+FFFB + || ']'; + -- ZWJ is excluded from the class above, so no placeholder swap needed. + r := regexp_replace(r, cf_pattern, '', 'g'); + -- Collapse runs of ASCII space + trim. Other whitespace (TAB etc.) preserved. + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: strip a single trailing (...) or [...] group. +-- +-- Mirror of `strip_trailing_parens` in `wxyc_etl::text::identity`. Returns +-- input unchanged when: no trailing close-bracket, brackets unbalanced, +-- or the matching open is at position 0 (would reduce stem to empty). +-- One pass only. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_strip_trailing_parens(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + trimmed text; + open_chr char; + close_chr char; + ch char; + depth int := 0; + open_idx int := -1; + i int; + stem text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + trimmed := regexp_replace(s, ' +$', ''); + IF length(trimmed) = 0 THEN RETURN s; END IF; + ch := right(trimmed, 1); + IF ch = ')' THEN + open_chr := '('; close_chr := ')'; + ELSIF ch = ']' THEN + open_chr := '['; close_chr := ']'; + ELSE + RETURN s; + END IF; + -- Scan right-to-left for the matching open. + FOR i IN REVERSE length(trimmed)..1 LOOP + ch := substr(trimmed, i, 1); + IF ch = close_chr THEN + depth := depth + 1; + ELSIF ch = open_chr THEN + depth := depth - 1; + IF depth = 0 THEN + open_idx := i; + EXIT; + END IF; + END IF; + END LOOP; + IF open_idx < 0 OR open_idx = 1 THEN + -- Unbalanced or full-string brackets — preserve. + RETURN s; + END IF; + stem := substr(trimmed, 1, open_idx - 1); + stem := regexp_replace(stem, ' +$', ''); + RETURN stem; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: drop a leading article or trailing comma-form article. +-- +-- Mirror of `drop_articles` in `wxyc_etl::text::identity`. At most one +-- match is consumed. The leading form requires the article followed by +-- ASCII space (`the `, `a `, `an `); `theater` does not match. The comma +-- form requires `, the` / `, a` / `, an` at end-of-string with a +-- non-empty stem; `Beatles, the Best Of` does not match. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_drop_articles(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + art text; + stripped text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + FOREACH art IN ARRAY ARRAY['the ', 'a ', 'an '] LOOP + IF starts_with(s, art) THEN + RETURN substr(s, length(art) + 1); + END IF; + END LOOP; + FOREACH art IN ARRAY ARRAY[', the', ', a', ', an'] LOOP + -- Suffix check via `right()` rather than `LIKE '%' || art` so a future + -- article containing `%` or `_` doesn't trigger wildcard semantics. + IF length(s) >= length(art) AND right(s, length(art)) = art THEN + stripped := substr(s, 1, length(s) - length(art)); + IF length(stripped) > 0 THEN + RETURN stripped; + END IF; + END IF; + END LOOP; + RETURN s; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: identity baseline (steps 4 + 5). +-- +-- Mirror of `identity_baseline` in `wxyc_etl::text::identity`. The shared +-- body of artist + title entry points. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_baseline(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_match_form(s); + r := wxyc_strip_trailing_parens(r); + r := wxyc_drop_articles(r); + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: artist identity match. +-- Mirror of `wxyc_etl::text::to_identity_match_form`. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_artist(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +BEGIN + RETURN wxyc_identity_baseline(s); +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: title identity match. +-- Mirror of `wxyc_etl::text::to_identity_match_form_title`. Same body as +-- artist today; separate function so callers type-distinguish at the call +-- site and a future step-6 promotion does not silently change titles that +-- would not benefit (`Side A/2` etc.). +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_title(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +BEGIN + RETURN wxyc_identity_baseline(s); +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: identity match + opt-in punctuation collapse (step 6). +-- Mirror of `wxyc_etl::text::to_identity_match_form_with_punctuation`. +-- Each run of one-or-more non-letter, non-number, non-whitespace codepoints +-- becomes a single ASCII space; result is re-collapsed and re-trimmed. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_with_punctuation(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_match_form(s); + r := wxyc_strip_trailing_parens(r); + r := wxyc_drop_articles(r); + -- Step 6: replace each run of non-{Letter,Number,Whitespace} with one space. + -- Postgres regex doesn't support `\p{L}` directly, but POSIX `[:alpha:]` / + -- `[:digit:]` / `[:space:]` are locale-aware (en_US.UTF-8 collation = + -- full Unicode coverage). + r := regexp_replace(r, '[^[:alpha:][:digit:][:space:]]+', ' ', 'g'); + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: identity match + opt-in `/N` disambiguator strip (step 8). +-- Mirror of `wxyc_etl::text::to_identity_match_form_with_disambiguator_strip`. +-- +-- Artists only. The leading whitespace before `/` is REQUIRED (`John Smith /1` +-- strips; `Track 1/12` does not — matches Rust's `\s+/\d+$` not `\s*`). +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_with_disambiguator_strip(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_identity_baseline(s); + r := regexp_replace(r, ' +/\d+$', ''); + RETURN r; +END +$$; diff --git a/shared/database/src/migrations/meta/0076_snapshot.json b/shared/database/src/migrations/meta/0076_snapshot.json new file mode 100644 index 00000000..6af97707 --- /dev/null +++ b/shared/database/src/migrations/meta/0076_snapshot.json @@ -0,0 +1,3877 @@ +{ + "id": "56df8872-7985-41d2-a475-9f6210336551", + "prevId": "621d7e4d-23e4-4073-a60b-7e5c91b0fcaa", + "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 + }, + "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.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.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.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 + }, + "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()" + }, + "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 + }, + "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_dj_name_trgm_idx": { + "name": "flowsheet_dj_name_trgm_idx", + "columns": [ + { + "expression": "\"dj_name\" 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_artwork_lookup_idx": { + "name": "flowsheet_artwork_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\".\"artwork_url\" 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_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": {} + } + }, + "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.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 + } + }, + "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 + }, + "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": {} + }, + "shows_legacy_dj_name_trgm_idx": { + "name": "shows_legacy_dj_name_trgm_idx", + "columns": [ + { + "expression": "\"legacy_dj_name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "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": {} + }, + "auth_user_dj_name_trgm_idx": { + "name": "auth_user_dj_name_trgm_idx", + "columns": [ + { + "expression": "\"dj_name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "auth_user_name_trgm_idx": { + "name": "auth_user_name_trgm_idx", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "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 + }, + "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.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" + ] + } + }, + "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 + } + }, + "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\" 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 6673bac1..4a006882 100644 --- a/shared/database/src/migrations/meta/_journal.json +++ b/shared/database/src/migrations/meta/_journal.json @@ -519,6 +519,13 @@ "when": 1779856000007, "tag": "0075_library-identity-substrate", "breakpoints": true + }, + { + "idx": 76, + "version": "7", + "when": 1779856000008, + "tag": "0076_wxyc-identity-match-functions", + "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 61641865..8a1f36a2 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -72,5 +72,6 @@ "0072_drop-rotation-active-album-bin-uniq": "0c2960c694f7ab4eec5ea9280064b2535c38736bb8714f80007690d673d6b791", "0073_flowsheet-play-order-idx": "c825132efd611b3d0d4f75488a7774121e4b343bbde4564aca16218403a92cdf", "0074_flowsheet-metadata-attempt-pending-covering-idx": "663b37f4c3d17f741a9d70dc9e4534e0f4941058785fec4faa98d01a6673e9e6", - "0075_library-identity-substrate": "49b9e4e2dcee9de2b57720efe29b40a9513bf74a7e397c24b0df8e927187cb6d" + "0075_library-identity-substrate": "49b9e4e2dcee9de2b57720efe29b40a9513bf74a7e397c24b0df8e927187cb6d", + "0076_wxyc-identity-match-functions": "2f855d791e99d8eb90778e7d68a159a87231061c7febea4e2cefe849e9d8c178" } diff --git a/tests/integration/wxyc-identity-match-functions.spec.js b/tests/integration/wxyc-identity-match-functions.spec.js new file mode 100644 index 00000000..247d17b7 --- /dev/null +++ b/tests/integration/wxyc-identity-match-functions.spec.js @@ -0,0 +1,127 @@ +/** + * Sanity check for the four wxyc_identity_match_* plpgsql functions + * deployed by migration 0076. + * + * Three layers: + * + * 1. SHA pin freshness — `wxyc-etl-pin.txt` hashes match the vendored + * bytes at `vendor/wxyc-etl/`. Drift means re-vendor. + * 2. Migration freshness — `shared/database/src/migrations/0076_*.sql` + * ends with `vendor/wxyc-etl/wxyc_identity_match_functions.sql` + * byte-for-byte. Prefix is the wrapper (CREATE EXTENSION + dictionary). + * 3. PG smoke — each function returns the expected normalization for a + * small WXYC-canonical artist set. Not the full 252-row fixture + * (that lives in wxyc-etl); just enough to catch a deploy that + * shipped functions with different bodies than expected. + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const postgres = require('postgres'); + +const REPO_ROOT = path.join(__dirname, '..', '..'); +const PIN_PATH = path.join(REPO_ROOT, 'wxyc-etl-pin.txt'); +const VENDOR_RULES = path.join(REPO_ROOT, 'vendor', 'wxyc-etl', 'wxyc_unaccent.rules'); +const VENDOR_FUNCTIONS = path.join(REPO_ROOT, 'vendor', 'wxyc-etl', 'wxyc_identity_match_functions.sql'); +const MIGRATION_PATH = path.join( + REPO_ROOT, + 'shared', + 'database', + 'src', + 'migrations', + '0076_wxyc-identity-match-functions.sql', +); + +function sha256(filePath) { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); +} + +function parsePin() { + const text = fs.readFileSync(PIN_PATH, 'utf8'); + const m = {}; + for (const line of text.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq < 0) continue; + m[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim(); + } + return m; +} + +function makeSql() { + return postgres({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || process.env.CI_DB_PORT || '5433', 10), + database: process.env.DB_NAME || 'wxyc_db', + user: process.env.DB_USERNAME || 'test-user', + password: process.env.DB_PASSWORD || 'test-pw', + onnotice: () => {}, + max: 2, + }); +} + +describe('wxyc_identity_match_* deploy (#805)', () => { + test('pin SHAs match vendored files', () => { + const pin = parsePin(); + expect(sha256(VENDOR_RULES)).toBe(pin.unaccent_rules_sha256); + expect(sha256(VENDOR_FUNCTIONS)).toBe(pin.functions_sql_sha256); + }); + + test('migration 0076 inlines vendored functions SQL byte-for-byte', () => { + const migration = fs.readFileSync(MIGRATION_PATH, 'utf8'); + const canonical = fs.readFileSync(VENDOR_FUNCTIONS, 'utf8'); + const firstLine = canonical.split('\n')[0]; + const idx = migration.indexOf(firstLine); + expect(idx).toBeGreaterThan(-1); + expect(migration.slice(idx)).toBe(canonical); + }); + + describe('PG-deployed functions', () => { + let sql; + + beforeAll(() => { + sql = makeSql(); + }); + + afterAll(async () => { + if (sql) await sql.end(); + }); + + test.each([ + // Canonical WXYC artists exercising the article-drop + paren-strip paths. + // Pulled from wxycExampleArtists in @wxyc/shared/test-utils — small set + // so a body-shape regression surfaces without dragging in the full + // 252-row fixture (that's wxyc-etl's job). + ['wxyc_identity_match_artist', 'Stereolab', 'stereolab'], + ['wxyc_identity_match_artist', 'Cat Power', 'cat power'], + ['wxyc_identity_match_artist', 'Juana Molina (Live)', 'juana molina'], + ['wxyc_identity_match_artist', 'The Microphones', 'microphones'], + ['wxyc_identity_match_artist', 'Hermanos Gutiérrez', 'hermanos gutierrez'], + ['wxyc_identity_match_title', 'In a Sentimental Mood (Live)', 'in a sentimental mood'], + ['wxyc_identity_match_title', 'Call Your Name', 'call your name'], + ['wxyc_identity_match_with_punctuation', 'Godspeed You! Black Emperor', 'godspeed you black emperor'], + ['wxyc_identity_match_with_punctuation', '10,000 Maniacs', '10 000 maniacs'], + ['wxyc_identity_match_with_disambiguator_strip', 'Stereolab /1', 'stereolab'], + ['wxyc_identity_match_with_disambiguator_strip', 'Track 1/12', 'track 1/12'], + ])('%s(%s) → %s', async (fnName, input, expected) => { + const rows = await sql.unsafe(`SELECT ${fnName}($1::text) AS out`, [input]); + expect(rows[0].out).toBe(expected); + }); + + test('functions are idempotent', async () => { + const probe = ' The Foo Fighters (1995) '; + for (const fnName of [ + 'wxyc_identity_match_artist', + 'wxyc_identity_match_title', + 'wxyc_identity_match_with_punctuation', + 'wxyc_identity_match_with_disambiguator_strip', + ]) { + const once = (await sql.unsafe(`SELECT ${fnName}($1::text) AS out`, [probe]))[0].out; + const twice = (await sql.unsafe(`SELECT ${fnName}($1::text) AS out`, [once]))[0].out; + expect(twice).toBe(once); + } + }); + }); +}); diff --git a/vendor/wxyc-etl/wxyc_identity_match_functions.sql b/vendor/wxyc-etl/wxyc_identity_match_functions.sql new file mode 100644 index 00000000..97744685 --- /dev/null +++ b/vendor/wxyc-etl/wxyc_identity_match_functions.sql @@ -0,0 +1,302 @@ +-- Canonical SQL implementation of the cross-cache-identity match form. +-- +-- Vendored verbatim into every cache repo (discogs-etl, musicbrainz-cache, +-- wikidata-cache) and Backend-Service. The four function bodies must produce +-- byte-identical output to the corresponding Rust entry points in +-- `wxyc_etl::text::identity`: +-- +-- wxyc_identity_match_artist <-> to_identity_match_form +-- wxyc_identity_match_title <-> to_identity_match_form_title +-- wxyc_identity_match_with_punctuation <-> to_identity_match_form_with_punctuation +-- wxyc_identity_match_with_disambiguator_strip +-- <-> to_identity_match_form_with_disambiguator_strip +-- +-- Parity is asserted by `wxyc-etl/tests/postgres_parity_test.rs` against the +-- 252-row fixture in `wxyc-etl/tests/fixtures/identity_normalization_cases.csv`. +-- +-- Required Postgres version: 16+ (Unicode property classes, `normalize()`, +-- stable regex behavior). Required extension: `unaccent` configured with the +-- `wxyc_unaccent` text-search dictionary installed from +-- `data/wxyc_unaccent.rules`. +-- +-- Vendoring contract: each consumer carries `wxyc-etl-pin.txt` recording the +-- SHA-256 of `data/wxyc_unaccent.rules` and the version header read from the +-- file's first comment line. Mismatch fails CI. See +-- `wxyc-etl/docs/postgres-analog-vendoring.md`. + +DO $$ +BEGIN + IF current_setting('server_version_num')::int < 160000 THEN + RAISE EXCEPTION 'wxyc identity-match functions require Postgres 16+; got %', + current_setting('server_version'); + END IF; +END $$; + +-- The wxyc_unaccent dictionary must be created before this file loads. +-- Consumer migrations do: +-- CREATE EXTENSION IF NOT EXISTS unaccent; +-- CREATE TEXT SEARCH DICTIONARY wxyc_unaccent ( +-- TEMPLATE = unaccent, RULES = 'wxyc_unaccent' +-- ); +-- followed by the rules-file SHA verification block (see vendoring docs). + +-- --------------------------------------------------------------------------- +-- Base match-form pipeline. +-- +-- Mirror of `wxyc_etl::text::to_match_form` after the storage-form pass +-- (no mojibake repair — callers responsible for storing pre-cleaned bytes). +-- Pipeline: +-- normalize NFKC -> lower -> wxyc_unaccent dictionary -> strip-Cf-except-ZWJ +-- -> collapse-ASCII-space + trim. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_match_form(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; + zwj text := chr(8205); -- U+200D + cf_pattern text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := normalize(s, NFKC); + r := lower(r); + r := unaccent('wxyc_unaccent', r); + -- Strip Cf (format) characters except U+200D ZWJ (emoji integrity), matching + -- `strip_cf_except_zwj` in the Rust pipeline. Postgres regex has no + -- `\p{Cf}` and no char-class subtraction; build the class from explicit + -- BMP Cf codepoints split around ZWJ. Supplementary-plane Cf (U+E0001 etc.) + -- is rare in music-catalog data and intentionally not handled here. + cf_pattern := + '[' + || chr(173) -- U+00AD soft hyphen + || chr(1564) -- U+061C ALM + || chr(1757) -- U+06DD ARABIC END OF AYAH + || chr(1807) -- U+070F SYRIAC ABBREV MARK + || chr(2274) -- U+08E2 ARABIC DISPUTED END OF AYAH + || chr(6158) -- U+180E MONG VOWEL SEP + || chr(8203) || '-' || chr(8204) -- U+200B-U+200C (200D ZWJ skipped) + || chr(8206) || '-' || chr(8207) -- U+200E-U+200F + || chr(8234) || '-' || chr(8238) -- U+202A-U+202E + || chr(8288) || '-' || chr(8303) -- U+2060-U+206F + || chr(65279) -- U+FEFF BOM + || chr(65529) || '-' || chr(65531) -- U+FFF9-U+FFFB + || ']'; + -- ZWJ is excluded from the class above, so no placeholder swap needed. + r := regexp_replace(r, cf_pattern, '', 'g'); + -- Collapse runs of ASCII space + trim. Other whitespace (TAB etc.) preserved. + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: strip a single trailing (...) or [...] group. +-- +-- Mirror of `strip_trailing_parens` in `wxyc_etl::text::identity`. Returns +-- input unchanged when: no trailing close-bracket, brackets unbalanced, +-- or the matching open is at position 0 (would reduce stem to empty). +-- One pass only. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_strip_trailing_parens(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + trimmed text; + open_chr char; + close_chr char; + ch char; + depth int := 0; + open_idx int := -1; + i int; + stem text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + trimmed := regexp_replace(s, ' +$', ''); + IF length(trimmed) = 0 THEN RETURN s; END IF; + ch := right(trimmed, 1); + IF ch = ')' THEN + open_chr := '('; close_chr := ')'; + ELSIF ch = ']' THEN + open_chr := '['; close_chr := ']'; + ELSE + RETURN s; + END IF; + -- Scan right-to-left for the matching open. + FOR i IN REVERSE length(trimmed)..1 LOOP + ch := substr(trimmed, i, 1); + IF ch = close_chr THEN + depth := depth + 1; + ELSIF ch = open_chr THEN + depth := depth - 1; + IF depth = 0 THEN + open_idx := i; + EXIT; + END IF; + END IF; + END LOOP; + IF open_idx < 0 OR open_idx = 1 THEN + -- Unbalanced or full-string brackets — preserve. + RETURN s; + END IF; + stem := substr(trimmed, 1, open_idx - 1); + stem := regexp_replace(stem, ' +$', ''); + RETURN stem; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: drop a leading article or trailing comma-form article. +-- +-- Mirror of `drop_articles` in `wxyc_etl::text::identity`. At most one +-- match is consumed. The leading form requires the article followed by +-- ASCII space (`the `, `a `, `an `); `theater` does not match. The comma +-- form requires `, the` / `, a` / `, an` at end-of-string with a +-- non-empty stem; `Beatles, the Best Of` does not match. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_drop_articles(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + art text; + stripped text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + FOREACH art IN ARRAY ARRAY['the ', 'a ', 'an '] LOOP + IF starts_with(s, art) THEN + RETURN substr(s, length(art) + 1); + END IF; + END LOOP; + FOREACH art IN ARRAY ARRAY[', the', ', a', ', an'] LOOP + -- Suffix check via `right()` rather than `LIKE '%' || art` so a future + -- article containing `%` or `_` doesn't trigger wildcard semantics. + IF length(s) >= length(art) AND right(s, length(art)) = art THEN + stripped := substr(s, 1, length(s) - length(art)); + IF length(stripped) > 0 THEN + RETURN stripped; + END IF; + END IF; + END LOOP; + RETURN s; +END +$$; + +-- --------------------------------------------------------------------------- +-- Helper: identity baseline (steps 4 + 5). +-- +-- Mirror of `identity_baseline` in `wxyc_etl::text::identity`. The shared +-- body of artist + title entry points. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_baseline(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_match_form(s); + r := wxyc_strip_trailing_parens(r); + r := wxyc_drop_articles(r); + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: artist identity match. +-- Mirror of `wxyc_etl::text::to_identity_match_form`. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_artist(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +BEGIN + RETURN wxyc_identity_baseline(s); +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: title identity match. +-- Mirror of `wxyc_etl::text::to_identity_match_form_title`. Same body as +-- artist today; separate function so callers type-distinguish at the call +-- site and a future step-6 promotion does not silently change titles that +-- would not benefit (`Side A/2` etc.). +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_title(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +BEGIN + RETURN wxyc_identity_baseline(s); +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: identity match + opt-in punctuation collapse (step 6). +-- Mirror of `wxyc_etl::text::to_identity_match_form_with_punctuation`. +-- Each run of one-or-more non-letter, non-number, non-whitespace codepoints +-- becomes a single ASCII space; result is re-collapsed and re-trimmed. +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_with_punctuation(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_match_form(s); + r := wxyc_strip_trailing_parens(r); + r := wxyc_drop_articles(r); + -- Step 6: replace each run of non-{Letter,Number,Whitespace} with one space. + -- Postgres regex doesn't support `\p{L}` directly, but POSIX `[:alpha:]` / + -- `[:digit:]` / `[:space:]` are locale-aware (en_US.UTF-8 collation = + -- full Unicode coverage). + r := regexp_replace(r, '[^[:alpha:][:digit:][:space:]]+', ' ', 'g'); + r := regexp_replace(r, ' +', ' ', 'g'); + r := regexp_replace(r, '^ | $', '', 'g'); + RETURN r; +END +$$; + +-- --------------------------------------------------------------------------- +-- Public entry point: identity match + opt-in `/N` disambiguator strip (step 8). +-- Mirror of `wxyc_etl::text::to_identity_match_form_with_disambiguator_strip`. +-- +-- Artists only. The leading whitespace before `/` is REQUIRED (`John Smith /1` +-- strips; `Track 1/12` does not — matches Rust's `\s+/\d+$` not `\s*`). +-- --------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION wxyc_identity_match_with_disambiguator_strip(s text) + RETURNS text + LANGUAGE plpgsql + IMMUTABLE + PARALLEL SAFE +AS $$ +DECLARE + r text; +BEGIN + IF s IS NULL THEN RETURN NULL; END IF; + r := wxyc_identity_baseline(s); + r := regexp_replace(r, ' +/\d+$', ''); + RETURN r; +END +$$; diff --git a/vendor/wxyc-etl/wxyc_unaccent.rules b/vendor/wxyc-etl/wxyc_unaccent.rules new file mode 100644 index 00000000..a3035308 --- /dev/null +++ b/vendor/wxyc-etl/wxyc_unaccent.rules @@ -0,0 +1,433 @@ +ª a +² 2 +³ 3 +µ μ +¹ 1 +º o +¼ 1⁄4 +½ 1⁄2 +¾ 3⁄4 +à a +á a +â a +ã a +ä a +å a +æ ae +ç c +è e +é e +ê e +ë e +ì i +í i +î i +ï i +ñ n +ò o +ó o +ô o +õ o +ö o +ù u +ú u +û u +ü u +ý y +ÿ y +ā a +ă a +ą a +ć c +ĉ c +ċ c +č c +ď d +ē e +ĕ e +ė e +ę e +ě e +ĝ g +ğ g +ġ g +ģ g +ĥ h +ĩ i +ī i +ĭ i +į i +ij ij +ĵ j +ķ k +ĺ l +ļ l +ľ l +ŀ l· +ń n +ņ n +ň n +ʼn ʼn +ō o +ŏ o +ő o +œ oe +ŕ r +ŗ r +ř r +ś s +ŝ s +ş s +š s +ţ t +ť t +ũ u +ū u +ŭ u +ů u +ű u +ų u +ŵ w +ŷ y +ź z +ż z +ž z +ſ s +ơ o +ư u +dž dz +lj lj +nj nj +ǎ a +ǐ i +ǒ o +ǔ u +ǖ u +ǘ u +ǚ u +ǜ u +ǟ a +ǡ a +ǣ ae +ǧ g +ǩ k +ǫ o +ǭ o +ǰ j +dz dz +ǵ g +ǹ n +ǻ a +ǽ ae +ǿ ø +ȁ a +ȃ a +ȅ e +ȇ e +ȉ i +ȋ i +ȍ o +ȏ o +ȑ r +ȓ r +ȕ u +ȗ u +ș s +ț t +ȟ h +ȧ a +ȩ e +ȫ o +ȭ o +ȯ o +ȱ o +ȳ y +ʹ ʹ +; ; +· · +ΐ ι +ά α +έ ε +ή η +ί ι +ΰ υ +ς σ +ϊ ι +ϋ υ +ό ο +ύ υ +ώ ω +ϐ β +ϑ θ +ϒ υ +ϓ υ +ϔ υ +ϕ φ +ϖ π +ϰ κ +ϱ ρ +ϲ σ +ϵ ε +ḁ a +ḃ b +ḅ b +ḇ b +ḉ c +ḋ d +ḍ d +ḏ d +ḑ d +ḓ d +ḕ e +ḗ e +ḙ e +ḛ e +ḝ e +ḟ f +ḡ g +ḣ h +ḥ h +ḧ h +ḩ h +ḫ h +ḭ i +ḯ i +ḱ k +ḳ k +ḵ k +ḷ l +ḹ l +ḻ l +ḽ l +ḿ m +ṁ m +ṃ m +ṅ n +ṇ n +ṉ n +ṋ n +ṍ o +ṏ o +ṑ o +ṓ o +ṕ p +ṗ p +ṙ r +ṛ r +ṝ r +ṟ r +ṡ s +ṣ s +ṥ s +ṧ s +ṩ s +ṫ t +ṭ t +ṯ t +ṱ t +ṳ u +ṵ u +ṷ u +ṹ u +ṻ u +ṽ v +ṿ v +ẁ w +ẃ w +ẅ w +ẇ w +ẉ w +ẋ x +ẍ x +ẏ y +ẑ z +ẓ z +ẕ z +ẖ h +ẗ t +ẘ w +ẙ y +ẚ aʾ +ẛ s +ạ a +ả a +ấ a +ầ a +ẩ a +ẫ a +ậ a +ắ a +ằ a +ẳ a +ẵ a +ặ a +ẹ e +ẻ e +ẽ e +ế e +ề e +ể e +ễ e +ệ e +ỉ i +ị i +ọ o +ỏ o +ố o +ồ o +ổ o +ỗ o +ộ o +ớ o +ờ o +ở o +ỡ o +ợ o +ụ u +ủ u +ứ u +ừ u +ử u +ữ u +ự u +ỳ y +ỵ y +ỷ y +ỹ y +ἀ α +ἁ α +ἂ α +ἃ α +ἄ α +ἅ α +ἆ α +ἇ α +ἐ ε +ἑ ε +ἒ ε +ἓ ε +ἔ ε +ἕ ε +ἠ η +ἡ η +ἢ η +ἣ η +ἤ η +ἥ η +ἦ η +ἧ η +ἰ ι +ἱ ι +ἲ ι +ἳ ι +ἴ ι +ἵ ι +ἶ ι +ἷ ι +ὀ ο +ὁ ο +ὂ ο +ὃ ο +ὄ ο +ὅ ο +ὐ υ +ὑ υ +ὒ υ +ὓ υ +ὔ υ +ὕ υ +ὖ υ +ὗ υ +ὠ ω +ὡ ω +ὢ ω +ὣ ω +ὤ ω +ὥ ω +ὦ ω +ὧ ω +ὰ α +ά α +ὲ ε +έ ε +ὴ η +ή η +ὶ ι +ί ι +ὸ ο +ό ο +ὺ υ +ύ υ +ὼ ω +ώ ω +ᾀ α +ᾁ α +ᾂ α +ᾃ α +ᾄ α +ᾅ α +ᾆ α +ᾇ α +ᾐ η +ᾑ η +ᾒ η +ᾓ η +ᾔ η +ᾕ η +ᾖ η +ᾗ η +ᾠ ω +ᾡ ω +ᾢ ω +ᾣ ω +ᾤ ω +ᾥ ω +ᾦ ω +ᾧ ω +ᾰ α +ᾱ α +ᾲ α +ᾳ α +ᾴ α +ᾶ α +ᾷ α +ι ι +ῂ η +ῃ η +ῄ η +ῆ η +ῇ η +ῐ ι +ῑ ι +ῒ ι +ΐ ι +ῖ ι +ῗ ι +ῠ υ +ῡ υ +ῢ υ +ΰ υ +ῤ ρ +ῥ ρ +ῦ υ +ῧ υ +` ` +ῲ ω +ῳ ω +ῴ ω +ῶ ω +ῷ ω +ⱼ j +ⱽ v +ꝰ ꝯ +꟱ s +ꟲ c +ꟳ f +ꟴ q +ꟸ ħ +ꟹ oe +ꭜ ꜧ +ꭝ ꬷ +ꭞ ɫ +ꭟ ꭒ +ꭩ ʍ diff --git a/vendor/wxyc-etl/wxyc_unaccent.version b/vendor/wxyc-etl/wxyc_unaccent.version new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/vendor/wxyc-etl/wxyc_unaccent.version @@ -0,0 +1 @@ +0.1.0 diff --git a/wxyc-etl-pin.txt b/wxyc-etl-pin.txt new file mode 100644 index 00000000..9501e0da --- /dev/null +++ b/wxyc-etl-pin.txt @@ -0,0 +1,18 @@ +# wxyc-etl postgres-analog vendor pin. +# +# Pins the canonical bytes from WXYC/wxyc-etl@v0.4.0 (`data/`), vendored here +# under `vendor/wxyc-etl/`. Migration 0076 is a generated wrapper: setup +# prelude + the canonical SQL inlined byte-for-byte (drizzle-kit applies plain +# SQL files in a single transaction, so `\i` isn't available). +# +# Verify with: +# shasum -a 256 vendor/wxyc-etl/wxyc_unaccent.rules vendor/wxyc-etl/wxyc_identity_match_functions.sql +# Refresh procedure: re-vendor from a new wxyc-etl tag, bump the SHAs + +# version below, regenerate migration 0077 (or higher) with the new content, +# rerun `tests/integration/db/wxycIdentityMatchFunctions.test.ts`. +# See WXYC/wxyc-etl/docs/postgres-analog-vendoring.md. + +unaccent_rules_version = 0.1.0 +unaccent_rules_sha256 = fc51eceb722904fa0d80734d4b2f4bbcffb0fcfecc133450f774210a067b9d26 +functions_sql_sha256 = 4beb7db2b44d479a4878a386e4d1b626d26fd3738e34371b23abe1ab4ccdc21a +wxyc_etl_version = 0.4.0 From 41b018f9f72b29d11361ccdc7031ff901b667ff9 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Mon, 11 May 2026 09:49:39 -0700 Subject: [PATCH 2/3] fixup(805): prettier format on integration spec --- tests/integration/wxyc-identity-match-functions.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/wxyc-identity-match-functions.spec.js b/tests/integration/wxyc-identity-match-functions.spec.js index 247d17b7..8f1ae1a0 100644 --- a/tests/integration/wxyc-identity-match-functions.spec.js +++ b/tests/integration/wxyc-identity-match-functions.spec.js @@ -30,7 +30,7 @@ const MIGRATION_PATH = path.join( 'database', 'src', 'migrations', - '0076_wxyc-identity-match-functions.sql', + '0076_wxyc-identity-match-functions.sql' ); function sha256(filePath) { From a6525f911e63f8d21d08e6fbdd3a201d28f31799 Mon Sep 17 00:00:00 2001 From: Jake Bromberg Date: Mon, 11 May 2026 10:01:07 -0700 Subject: [PATCH 3/3] fixup(805): add precondition-guard opt-out comment + refreeze hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI 'Migration guards' check greps for any reference to library_identity* in migration SQL; my body comment mentioning the downstream #663 column flip tripped it. Add the documented opt-out comment as the first line of the migration. Refreeze applied-hashes.json with the new SHA since the migration body changed. Does NOT address the second dry-run failure (RDS managed PG can't load custom tsearch_data files) — that's tracked separately as a deployment-pattern concern. --- .../src/migrations/0076_wxyc-identity-match-functions.sql | 1 + shared/database/src/migrations/meta/applied-hashes.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql b/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql index 657762cb..87f51c6b 100644 --- a/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql +++ b/shared/database/src/migrations/0076_wxyc-identity-match-functions.sql @@ -1,3 +1,4 @@ +-- precondition-guard: not-required (function-deploy-only; no DDL touches library_identity* tables; the body-comment mention is a forward-reference to BS#663, not a column edit) -- Cross-cache-identity match form (wiki §3.3.5). -- -- Deploys the four `wxyc_identity_match_*` plpgsql functions onto the Backend's diff --git a/shared/database/src/migrations/meta/applied-hashes.json b/shared/database/src/migrations/meta/applied-hashes.json index 8a1f36a2..4f5777ed 100644 --- a/shared/database/src/migrations/meta/applied-hashes.json +++ b/shared/database/src/migrations/meta/applied-hashes.json @@ -73,5 +73,5 @@ "0073_flowsheet-play-order-idx": "c825132efd611b3d0d4f75488a7774121e4b343bbde4564aca16218403a92cdf", "0074_flowsheet-metadata-attempt-pending-covering-idx": "663b37f4c3d17f741a9d70dc9e4534e0f4941058785fec4faa98d01a6673e9e6", "0075_library-identity-substrate": "49b9e4e2dcee9de2b57720efe29b40a9513bf74a7e397c24b0df8e927187cb6d", - "0076_wxyc-identity-match-functions": "2f855d791e99d8eb90778e7d68a159a87231061c7febea4e2cefe849e9d8c178" + "0076_wxyc-identity-match-functions": "5ac0d907774ef688b1f6840721c01237b7881cdde09829f7fd45f1ac5bf2e5ed" }