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; """); } }