From 93d54d7783a4afbfb338a8f757f1834649e39de6 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 08:43:23 +0200 Subject: [PATCH] feat(db): add vocabulary, term, and authority tables Co-Authored-By: Claude Sonnet 4.6 --- .../0002_vocabularies_authorities.sql | 34 +++++++++++++++++++ crates/db/tests/migrate.rs | 23 +++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 crates/db/migrations/0002_vocabularies_authorities.sql diff --git a/crates/db/migrations/0002_vocabularies_authorities.sql b/crates/db/migrations/0002_vocabularies_authorities.sql new file mode 100644 index 0000000..668de69 --- /dev/null +++ b/crates/db/migrations/0002_vocabularies_authorities.sql @@ -0,0 +1,34 @@ +-- Controlled vocabularies (term sources) and their terms. +CREATE TABLE vocabulary ( + id UUID PRIMARY KEY, + key TEXT NOT NULL UNIQUE -- e.g. 'material', 'object_name' +); + +CREATE TABLE term ( + id UUID PRIMARY KEY, + vocabulary_id UUID NOT NULL REFERENCES vocabulary (id) ON DELETE CASCADE, + external_uri TEXT -- e.g. Getty AAT / KulturNav / Wikidata URI +); +CREATE INDEX term_vocabulary_idx ON term (vocabulary_id); + +CREATE TABLE term_label ( + term_id UUID NOT NULL REFERENCES term (id) ON DELETE CASCADE, + lang TEXT NOT NULL, -- BCP-47, e.g. 'sv', 'en' + label TEXT NOT NULL, + PRIMARY KEY (term_id, lang) +); + +-- Authority records: person / organisation / place. Store once, link many. +CREATE TABLE authority ( + id UUID PRIMARY KEY, + kind TEXT NOT NULL CHECK (kind IN ('person', 'organisation', 'place')), + external_uri TEXT +); +CREATE INDEX authority_kind_idx ON authority (kind); + +CREATE TABLE authority_label ( + authority_id UUID NOT NULL REFERENCES authority (id) ON DELETE CASCADE, + lang TEXT NOT NULL, + label TEXT NOT NULL, + PRIMARY KEY (authority_id, lang) +); diff --git a/crates/db/tests/migrate.rs b/crates/db/tests/migrate.rs index c1f4d47..74d3227 100644 --- a/crates/db/tests/migrate.rs +++ b/crates/db/tests/migrate.rs @@ -18,3 +18,26 @@ async fn migrate_is_idempotent_and_creates_audit_log(pool: PgPool) { .unwrap(); assert_eq!(regclass.as_deref(), Some("audit_log")); } + +#[sqlx::test] +async fn migrate_creates_vocabulary_and_authority_tables(pool: PgPool) { + let db = Db::from_pool(pool); + for table in [ + "vocabulary", + "term", + "term_label", + "authority", + "authority_label", + ] { + let regclass: Option = + sqlx::query_scalar(&format!("SELECT to_regclass('public.{table}')::text")) + .fetch_one(db.pool()) + .await + .unwrap(); + assert_eq!( + regclass.as_deref(), + Some(table), + "table {table} should exist" + ); + } +}