using Microsoft.EntityFrameworkCore.Migrations; namespace Tiku.Infrastructure.Persistence.Migrations; internal static class MigrationBuilderDatabaseObjectExtensions { public static void EnsureAuthorizationCacheInvalidationTriggers(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" INSERT INTO authorization_scope_versions (id, realm, tenant_id, version, created_at, updated_at) VALUES (gen_random_uuid(), 'platform', NULL, 1, now(), now()) ON CONFLICT DO NOTHING; INSERT INTO authorization_scope_versions (id, realm, tenant_id, version, created_at, updated_at) SELECT gen_random_uuid(), 'tenant', id, 1, now(), now() FROM tenants ON CONFLICT DO NOTHING; CREATE OR REPLACE FUNCTION tiku_bump_tenant_authorization_version() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE scope_tenant_id uuid; next_version bigint; BEGIN IF TG_OP = 'DELETE' THEN scope_tenant_id := OLD.tenant_id; ELSE scope_tenant_id := NEW.tenant_id; END IF; UPDATE authorization_scope_versions SET version = version + 1, updated_at = now() WHERE realm = 'tenant' AND tenant_id = scope_tenant_id RETURNING version INTO next_version; IF next_version IS NOT NULL THEN INSERT INTO authorization_cache_invalidations (id, target_type, tenant_id, realm, version, attempt_count, created_at, updated_at) VALUES (gen_random_uuid(), 'scope', scope_tenant_id, 'tenant', next_version, 0, now(), now()); END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION tiku_bump_platform_authorization_version() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE next_version bigint; BEGIN UPDATE authorization_scope_versions SET version = version + 1, updated_at = now() WHERE realm = 'platform' AND tenant_id IS NULL RETURNING version INTO next_version; INSERT INTO authorization_cache_invalidations (id, target_type, realm, version, attempt_count, created_at, updated_at) VALUES (gen_random_uuid(), 'scope', 'platform', next_version, 0, now(), now()); IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION tiku_bump_all_authorization_versions() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN UPDATE authorization_scope_versions SET version = version + 1, updated_at = now(); INSERT INTO authorization_cache_invalidations (id, target_type, tenant_id, realm, version, attempt_count, created_at, updated_at) SELECT gen_random_uuid(), 'scope', tenant_id, realm, version, 0, now(), now() FROM authorization_scope_versions; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_tenant_backend_roles_authorization_version ON tenant_backend_roles; DROP TRIGGER IF EXISTS trg_tenant_backend_role_permissions_authorization_version ON tenant_backend_role_permissions; DROP TRIGGER IF EXISTS trg_tenant_backend_user_roles_authorization_version ON tenant_backend_user_roles; DROP TRIGGER IF EXISTS trg_platform_backend_roles_authorization_version ON platform_backend_roles; DROP TRIGGER IF EXISTS trg_platform_backend_role_permissions_authorization_version ON platform_backend_role_permissions; DROP TRIGGER IF EXISTS trg_platform_backend_user_roles_authorization_version ON platform_backend_user_roles; DROP TRIGGER IF EXISTS trg_backend_permissions_authorization_version ON backend_permissions; CREATE TRIGGER trg_tenant_backend_roles_authorization_version AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_roles FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version(); CREATE TRIGGER trg_tenant_backend_role_permissions_authorization_version AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_role_permissions FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version(); CREATE TRIGGER trg_tenant_backend_user_roles_authorization_version AFTER INSERT OR UPDATE OR DELETE ON tenant_backend_user_roles FOR EACH ROW EXECUTE FUNCTION tiku_bump_tenant_authorization_version(); CREATE TRIGGER trg_platform_backend_roles_authorization_version AFTER INSERT OR UPDATE OR DELETE ON platform_backend_roles FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version(); CREATE TRIGGER trg_platform_backend_role_permissions_authorization_version AFTER INSERT OR UPDATE OR DELETE ON platform_backend_role_permissions FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version(); CREATE TRIGGER trg_platform_backend_user_roles_authorization_version AFTER INSERT OR UPDATE OR DELETE ON platform_backend_user_roles FOR EACH ROW EXECUTE FUNCTION tiku_bump_platform_authorization_version(); CREATE TRIGGER trg_backend_permissions_authorization_version AFTER INSERT OR UPDATE OR DELETE ON backend_permissions FOR EACH STATEMENT EXECUTE FUNCTION tiku_bump_all_authorization_versions(); """); } public static void DropAuthorizationCacheInvalidationTriggers(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" DROP FUNCTION IF EXISTS tiku_bump_tenant_authorization_version() CASCADE; DROP FUNCTION IF EXISTS tiku_bump_platform_authorization_version() CASCADE; DROP FUNCTION IF EXISTS tiku_bump_all_authorization_versions() CASCADE; """); } public static void EnsureQueryOptimizationIndexes(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" CREATE INDEX IF NOT EXISTS ix_scoreline_records_tenant_id_year_school_name_major_name_id ON scoreline_records (tenant_id, year DESC, school_name, major_name, id); CREATE INDEX IF NOT EXISTS ix_scoreline_records_field_values_jsonb_path ON scoreline_records USING gin (field_values jsonb_path_ops); CREATE INDEX IF NOT EXISTS ix_scoreline_records_school_name_trgm ON scoreline_records USING gin (school_name gin_trgm_ops) WHERE school_name IS NOT NULL; CREATE INDEX IF NOT EXISTS ix_scoreline_records_major_name_trgm ON scoreline_records USING gin (major_name gin_trgm_ops) WHERE major_name IS NOT NULL; CREATE INDEX IF NOT EXISTS ix_background_jobs_pending_due ON background_jobs (run_after, created_at, id) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS ix_background_jobs_processing_lease ON background_jobs (lock_expires_at, created_at, id) WHERE status = 'processing'; """); } public static void DropQueryOptimizationIndexes(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" DROP INDEX IF EXISTS ix_background_jobs_processing_lease; DROP INDEX IF EXISTS ix_background_jobs_pending_due; DROP INDEX IF EXISTS ix_scoreline_records_major_name_trgm; DROP INDEX IF EXISTS ix_scoreline_records_school_name_trgm; DROP INDEX IF EXISTS ix_scoreline_records_field_values_jsonb_path; DROP INDEX IF EXISTS ix_scoreline_records_tenant_id_year_school_name_major_name_id; """); } public static void EnsureContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" CREATE OR REPLACE FUNCTION tiku_guard_question_revision_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'question revisions are immutable'; END; $$; DROP TRIGGER IF EXISTS trg_question_revisions_immutable ON question_revisions; CREATE TRIGGER trg_question_revisions_immutable BEFORE UPDATE OR DELETE ON question_revisions FOR EACH ROW EXECUTE FUNCTION tiku_guard_question_revision_immutable(); CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_version_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.status = 'published' THEN RAISE EXCEPTION 'published product access manifest versions are immutable'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_product_manifest_versions_published_immutable ON product_access_manifest_versions; CREATE TRIGGER trg_product_manifest_versions_published_immutable BEFORE UPDATE OR DELETE ON product_access_manifest_versions FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_version_immutable(); CREATE OR REPLACE FUNCTION tiku_guard_published_manifest_child_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE parent_tenant_id uuid; parent_version_id uuid; parent_status text; BEGIN IF TG_OP = 'DELETE' THEN parent_tenant_id := OLD.tenant_id; parent_version_id := OLD.product_access_manifest_version_id; ELSE parent_tenant_id := NEW.tenant_id; parent_version_id := NEW.product_access_manifest_version_id; END IF; SELECT status INTO parent_status FROM product_access_manifest_versions WHERE tenant_id = parent_tenant_id AND id = parent_version_id; IF parent_status = 'published' THEN RAISE EXCEPTION 'children of a published product access manifest version are immutable'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_product_manifest_releases_published_immutable ON product_manifest_releases; DROP TRIGGER IF EXISTS trg_product_manifest_targets_published_immutable ON product_manifest_targets; DROP TRIGGER IF EXISTS trg_product_manifest_resources_published_immutable ON product_manifest_resources; CREATE TRIGGER trg_product_manifest_releases_published_immutable BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_releases FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); CREATE TRIGGER trg_product_manifest_targets_published_immutable BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_targets FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); CREATE TRIGGER trg_product_manifest_resources_published_immutable BEFORE INSERT OR UPDATE OR DELETE ON product_manifest_resources FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_manifest_child_immutable(); CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF OLD.status = 'published' THEN RAISE EXCEPTION 'published content releases are immutable'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_content_releases_published_immutable ON content_releases; CREATE TRIGGER trg_content_releases_published_immutable BEFORE UPDATE OR DELETE ON content_releases FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_immutable(); CREATE OR REPLACE FUNCTION tiku_guard_published_content_release_child_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE parent_tenant_id uuid; parent_release_id uuid; parent_status text; BEGIN IF TG_OP = 'DELETE' THEN parent_tenant_id := OLD.tenant_id; parent_release_id := OLD.content_release_id; ELSE parent_tenant_id := NEW.tenant_id; parent_release_id := NEW.content_release_id; END IF; SELECT status INTO parent_status FROM content_releases WHERE tenant_id = parent_tenant_id AND id = parent_release_id; IF parent_status = 'published' THEN RAISE EXCEPTION 'children of a published content release are immutable'; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$; DROP TRIGGER IF EXISTS trg_audience_segments_release_immutable ON audience_segments; DROP TRIGGER IF EXISTS trg_audience_segment_members_release_immutable ON audience_segment_members; DROP TRIGGER IF EXISTS trg_content_release_questions_release_immutable ON content_release_questions; CREATE TRIGGER trg_audience_segments_release_immutable BEFORE INSERT OR UPDATE OR DELETE ON audience_segments FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); CREATE TRIGGER trg_audience_segment_members_release_immutable BEFORE INSERT OR UPDATE OR DELETE ON audience_segment_members FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); CREATE TRIGGER trg_content_release_questions_release_immutable BEFORE INSERT OR UPDATE OR DELETE ON content_release_questions FOR EACH ROW EXECUTE FUNCTION tiku_guard_published_content_release_child_immutable(); """); } public static void DropContentV2ImmutabilityGuards(this MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" DROP FUNCTION IF EXISTS tiku_guard_published_content_release_child_immutable() CASCADE; DROP FUNCTION IF EXISTS tiku_guard_published_content_release_immutable() CASCADE; DROP FUNCTION IF EXISTS tiku_guard_published_manifest_child_immutable() CASCADE; DROP FUNCTION IF EXISTS tiku_guard_published_manifest_version_immutable() CASCADE; DROP FUNCTION IF EXISTS tiku_guard_question_revision_immutable() CASCADE; """); } }