-- CF-3124: add tenant discriminator + tenant_profile to SmartTranslate DB -- Safe to run multiple times; migrations wrapped in IF NOT EXISTS. -- Rollback at bottom of file (commented out). BEGIN; CREATE EXTENSION IF NOT EXISTS vector; -- translation_memory: tenant column ALTER TABLE translation_memory ADD COLUMN IF NOT EXISTS tenant VARCHAR(64); CREATE INDEX IF NOT EXISTS ix_tm_tenant_lang ON translation_memory (tenant, source_lang, target_lang); -- glossary_terms: tenant column ALTER TABLE glossary_terms ADD COLUMN IF NOT EXISTS tenant VARCHAR(64); CREATE INDEX IF NOT EXISTS ix_glossary_tenant_src ON glossary_terms (tenant, LOWER(source_term), target_lang); -- Per-tenant uniqueness. The existing uq_glossary_term_ci_global (WHERE customer_id -- IS NULL) would collide across tenants for NULL-customer rows — redefine to exclude -- tenant'd rows, and add a parallel partial unique index for tenant'd rows. DROP INDEX IF EXISTS uq_glossary_term_ci_global; CREATE UNIQUE INDEX IF NOT EXISTS uq_glossary_term_ci_global ON glossary_terms (LOWER(source_term), target_lang, COALESCE(context_hint, '')) WHERE customer_id IS NULL AND tenant IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS uq_glossary_term_tenant ON glossary_terms (tenant, LOWER(source_term), target_lang, COALESCE(context_hint, '')) WHERE tenant IS NOT NULL; -- tenant_profile: new table CREATE TABLE IF NOT EXISTS tenant_profile ( tenant VARCHAR(64) PRIMARY KEY, tone VARCHAR(32), -- professional / friendly / technical / marketing / editorial formality VARCHAR(16), -- formal / informal / neutral industry VARCHAR(64), base_rules TEXT, -- free-form system prompt fragment forbidden_terms TEXT[] DEFAULT '{}', -- e.g. {'AdGuardHome','Hetzner'} for vpn-marketing preferred_author_voice TEXT, -- e.g. "wir/nous (author voice ok, never Sie/du/vous)" direct_address_allowed BOOLEAN DEFAULT TRUE, default_source_lang VARCHAR(8), default_target_langs VARCHAR(8)[] DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- Seed known tenants (idempotent upsert on tenant PK) INSERT INTO tenant_profile (tenant, tone, formality, industry, base_rules, forbidden_terms, preferred_author_voice, direct_address_allowed, default_source_lang, default_target_langs) VALUES ('ifk', 'editorial', 'neutral', 'kids-education', 'Neutral, parent-facing tone. Refer to "Eltern", "das Kind", "les parents", "parents" rather than addressing the reader. Author voice "wir" / "nous" is OK. Do not append sales/CTA blocks; inline mentions only.', '{}'::text[], 'wir/nous (author voice only)', false, 'en', ARRAY['de','fr']), ('vpn-marketing', 'marketing', 'neutral', 'consumer-vpn', 'Consumer privacy/VPN marketing. Describe blocklists qualitatively (curated HaGeZi/OISD/Cloudflare-top-10k allowlist, ~15-30k DE-optimized entries). Use "Cloud-Server in Deutschland" instead of naming the hoster.', ARRAY['AdGuardHome','Hetzner'], NULL, true, 'en', ARRAY['de','fr']), ('clicksports.de', 'professional', 'formal', 'hosting-msp', 'B2B hosting/MSP tone. Always "Service" never "Support". Update-safe WHMCS context; avoid internal ticket jargon in customer-facing strings.', ARRAY['Support'], NULL, true, 'de', ARRAY['en']), ('etoro', 'professional', 'neutral', 'retail-trading', 'Retail trading/copy-trading tone. Factual, compliance-aware. No exaggerated performance claims. Use official instrument names.', '{}'::text[], NULL, true, 'en', ARRAY['de','fr','es','it']), ('matrixhost', 'friendly', 'neutral', 'matrix-hosting', 'Matrix chat hosting SaaS tone. Technical-but-approachable; family-first angle.', '{}'::text[], NULL, true, 'en', ARRAY['de']), ('agiliton', 'professional', 'formal', 'msp', 'Agiliton corporate voice. Short sentences, German engineering precision.', '{}'::text[], NULL, true, 'de', ARRAY['en']) ON CONFLICT (tenant) DO NOTHING; COMMIT; -- ROLLBACK (run manually if needed): -- BEGIN; -- DROP TABLE IF EXISTS tenant_profile; -- DROP INDEX IF EXISTS ix_glossary_tenant_src; -- DROP INDEX IF EXISTS ix_tm_tenant_lang; -- ALTER TABLE glossary_terms DROP COLUMN IF EXISTS tenant; -- ALTER TABLE translation_memory DROP COLUMN IF EXISTS tenant; -- COMMIT;