@runa-ai/runa-cli 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{chunk-Z7A4BEWF.js → chunk-3JO6YP3T.js} +1 -1
  2. package/dist/chunk-6E2DRXIL.js +452 -0
  3. package/dist/{chunk-PMXE5XOJ.js → chunk-GHQH6UC5.js} +1 -1
  4. package/dist/{chunk-LCK2LGVR.js → chunk-PAWNJA3N.js} +1 -1
  5. package/dist/{chunk-FWMGC5FP.js → chunk-RB2ZUS76.js} +249 -12
  6. package/dist/{chunk-CKRLVEIO.js → chunk-ZYT7OQJB.js} +16 -11
  7. package/dist/{ci-Z4525QW6.js → ci-ZK3LKYFX.js} +305 -429
  8. package/dist/{cli-Q2XIQDRS.js → cli-ZY5VRIJA.js} +13 -13
  9. package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -2
  10. package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +1 -1
  11. package/dist/commands/ci/machine/contract.d.ts +6 -1
  12. package/dist/commands/ci/machine/guards.d.ts +16 -0
  13. package/dist/commands/ci/machine/machine.d.ts +11 -3
  14. package/dist/commands/db/apply/actors/seed-actors.d.ts +1 -0
  15. package/dist/commands/db/apply/contract.d.ts +23 -0
  16. package/dist/commands/db/apply/helpers/fresh-db-handler.d.ts +2 -1
  17. package/dist/commands/db/apply/helpers/hazard-handler.d.ts +19 -8
  18. package/dist/commands/db/apply/helpers/index.d.ts +2 -1
  19. package/dist/commands/db/apply/helpers/no-change-plan.d.ts +2 -0
  20. package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +11 -0
  21. package/dist/commands/db/apply/machine.d.ts +52 -1
  22. package/dist/commands/db/utils/boundary-policy/types.d.ts +2 -0
  23. package/dist/commands/db/utils/duplicate-function-ownership.d.ts +35 -0
  24. package/dist/commands/db/utils/plan-size-guard.d.ts +16 -0
  25. package/dist/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.d.ts +4 -0
  26. package/dist/constants/versions.d.ts +1 -1
  27. package/dist/{db-BPQ2TEQM.js → db-EPI2DQYN.js} +1203 -410
  28. package/dist/{dev-MLRKIP7F.js → dev-GB5ERUVR.js} +1 -1
  29. package/dist/{env-WNHJVLOT.js → env-WP74UUMO.js} +1 -1
  30. package/dist/{hotfix-Z5EGVSMH.js → hotfix-TOSGTVCW.js} +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/{init-S2ATHLJ6.js → init-35JLDFHI.js} +1 -1
  33. package/dist/{risk-detector-VO5HJR4R.js → risk-detector-S7XQF4I2.js} +1 -1
  34. package/dist/{risk-detector-core-7WZJZ5ZI.js → risk-detector-core-TGFKWHRS.js} +1 -1
  35. package/dist/{risk-detector-plpgsql-ULV7NLDB.js → risk-detector-plpgsql-O32TUR34.js} +103 -5
  36. package/dist/{upgrade-BDUWBRT5.js → upgrade-7L4JIE4K.js} +1 -1
  37. package/dist/{vuln-check-66RXX3TO.js → vuln-check-G6I4YYDC.js} +1 -1
  38. package/dist/{vuln-checker-FFOGOJPT.js → vuln-checker-CT2AYPIS.js} +1 -1
  39. package/dist/{watch-ITYW57SL.js → watch-AL4LCBRM.js} +1 -1
  40. package/package.json +3 -3
  41. package/dist/chunk-4XHZQRRK.js +0 -215
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
3
  import { detectDatabaseStack, getStackPaths } from './chunk-CCKG5R4Y.js';
4
- import { categorizeRisks, detectSchemaRisks } from './chunk-LCK2LGVR.js';
4
+ import { categorizeRisks, detectSchemaRisks } from './chunk-PAWNJA3N.js';
5
5
  import './chunk-ZZOXM6Q4.js';
6
6
  import { createError } from './chunk-JQXOVCOP.js';
7
- import { resolveDatabaseUrl, resolveDatabaseTarget, tryResolveDatabaseUrl } from './chunk-CKRLVEIO.js';
8
- export { resolveDatabaseUrl, tryResolveDatabaseUrl } from './chunk-CKRLVEIO.js';
9
- import { detectAppSchemas, normalizeDatabaseUrlForDdl, formatSchemasForSql } from './chunk-4XHZQRRK.js';
7
+ import { resolveDatabaseUrl, resolveDatabaseTarget, tryResolveDatabaseUrl } from './chunk-ZYT7OQJB.js';
8
+ export { resolveDatabaseUrl, tryResolveDatabaseUrl } from './chunk-ZYT7OQJB.js';
9
+ import { detectAppSchemas, getIdempotentProtectedTables, getIdempotentProtectedObjects, parseExpectedPartitions, queryActualPartitions, detectPartitionDrift, formatPartitionWarnings, normalizeDatabaseUrlForDdl, filterFalsePositiveHazards, formatSchemasForSql, PARTITION_OF_REGEX, extractQualifiedName, resolveIdempotentDir, readIdempotentSqlFiles, getIdempotentRoles } from './chunk-6E2DRXIL.js';
10
10
  import './chunk-UHDAYPHH.js';
11
11
  import { splitPlpgsqlStatementsWithOffsets, extractExecuteExpressions, extractStaticSqlFromExpression } from './chunk-Y5ANTCKE.js';
12
12
  import { loadEnvFiles } from './chunk-WPMR7RQ4.js';
@@ -15,7 +15,7 @@ import { validateUserFilePath, filterSafePaths, resolveSafePath } from './chunk-
15
15
  import { runMachine } from './chunk-QDF7QXBL.js';
16
16
  import './chunk-XVNDDHAF.js';
17
17
  import { extractSchemaTablesAndEnums, fetchDbTablesAndEnums, extractTablesFromIdempotentSql, diffSchema, generateTablesManifest, writeEnvLocalBridge, removeEnvLocalBridge } from './chunk-OBYZDT2E.js';
18
- import { parsePostgresUrl, buildPsqlEnv, buildPsqlArgs, psqlSyncQuery, blankDollarQuotedBodies, stripSqlComments, psqlSyncBatch, psqlSyncFile, psqlExec, psqlQuery } from './chunk-A6A7JIRD.js';
18
+ import { parsePostgresUrl, buildPsqlEnv, buildPsqlArgs, psqlSyncQuery, psqlSyncBatch, psqlSyncFile, psqlExec, psqlQuery, blankDollarQuotedBodies, stripSqlComments } from './chunk-A6A7JIRD.js';
19
19
  import { redactSecrets } from './chunk-II7VYQEM.js';
20
20
  import { init_local_supabase, buildLocalDatabaseUrl, init_constants, detectLocalSupabasePorts, DATABASE_DEFAULTS, SEED_DEFAULTS, SCRIPT_LOCATIONS } from './chunk-QSEF4T3Y.js';
21
21
  export { DATABASE_DEFAULTS, SCRIPT_LOCATIONS, SEED_DEFAULTS } from './chunk-QSEF4T3Y.js';
@@ -31,8 +31,8 @@ import { CommandOutcomeSchema, createCLILogger, CLIError, dbGenerateDiagram, DbD
31
31
  import { fromPromise, setup, assign, createActor } from 'xstate';
32
32
  import { z } from 'zod';
33
33
  import { spawn, spawnSync, execFileSync } from 'child_process';
34
- import { existsSync, mkdirSync, copyFileSync, readdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync, lstatSync, unlinkSync, statSync } from 'fs';
35
- import path15, { join, dirname, resolve, basename, relative } from 'path';
34
+ import path15, { join, dirname, resolve, relative } from 'path';
35
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync, unlinkSync, lstatSync, statSync } from 'fs';
36
36
  import crypto, { randomBytes, randomUUID } from 'crypto';
37
37
  import { tmpdir } from 'os';
38
38
  import { writeFile, appendFile, readFile, stat, realpath } from 'fs/promises';
@@ -118,6 +118,17 @@ var DbApplyMetricsSchema = z.object({
118
118
  /** Number of retry attempts for lock_timeout */
119
119
  retryAttempts: z.number().optional()
120
120
  });
121
+ var DbApplyPlanSummarySchema = z.object({
122
+ rawStatements: z.number().int().nonnegative(),
123
+ effectiveStatements: z.number().int().nonnegative(),
124
+ noiseStatements: z.number().int().nonnegative(),
125
+ categories: z.object({
126
+ idempotentDrop: z.number().int().nonnegative(),
127
+ idempotentAuthz: z.number().int().nonnegative(),
128
+ idempotentRls: z.number().int().nonnegative(),
129
+ suppressedFunction: z.number().int().nonnegative()
130
+ }).strict()
131
+ }).strict();
121
132
  var DbApplyOutputSchema = z.object({
122
133
  success: z.boolean(),
123
134
  idempotentSchemasApplied: z.number(),
@@ -143,6 +154,12 @@ var DbApplyOutputSchema = z.object({
143
154
  * SQL that is actually shown as executable in check mode (after idempotent drop filtering)
144
155
  */
145
156
  filteredPlanSql: z.string().optional(),
157
+ /**
158
+ * Statement counts for check-mode / compare-only plan review.
159
+ * Separates structural statements from metadata noise and semantically
160
+ * suppressed function churn.
161
+ */
162
+ planSummary: DbApplyPlanSummarySchema.optional(),
146
163
  /**
147
164
  * Whether this was a check (dry-run) only
148
165
  */
@@ -819,249 +836,20 @@ function validatePlanForExecution(plan, allowedHazardTypes) {
819
836
  }
820
837
  }
821
838
 
822
- // src/commands/db/apply/helpers/plan-check-filter.ts
823
- init_esm_shims();
824
-
825
- // src/commands/db/apply/helpers/idempotent-object-registry.ts
826
- init_esm_shims();
827
-
828
- // src/commands/db/apply/helpers/partition-validator.ts
839
+ // src/commands/db/apply/helpers/no-change-plan.ts
829
840
  init_esm_shims();
830
- var QUALIFIED_NAME = '(?:(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\\s*\\.\\s*)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))';
831
- var PARTITION_OF_REGEX = new RegExp(
832
- `CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${QUALIFIED_NAME}\\s+PARTITION\\s+OF\\s+${QUALIFIED_NAME}`,
833
- "gi"
834
- );
835
- function extractQualifiedName(quotedSchema, unquotedSchema, quotedTable, unquotedTable) {
836
- const schema = quotedSchema || unquotedSchema || "";
837
- const table = quotedTable || unquotedTable || "";
838
- return schema ? `${schema}.${table}` : table;
839
- }
840
- function parseExpectedPartitions(idempotentDir) {
841
- if (!existsSync(idempotentDir)) return [];
842
- const files = readdirSync(idempotentDir).filter((f) => f.endsWith(".sql")).sort();
843
- const partitions = [];
844
- for (const file of files) {
845
- const fullPath = join(idempotentDir, file);
846
- try {
847
- if (lstatSync(fullPath).isSymbolicLink()) continue;
848
- } catch {
849
- continue;
850
- }
851
- const raw = readFileSync(fullPath, "utf-8");
852
- const cleaned = blankDollarQuotedBodies(stripSqlComments(raw));
853
- for (const match of cleaned.matchAll(PARTITION_OF_REGEX)) {
854
- const child = extractQualifiedName(match[1], match[2], match[3], match[4]);
855
- const parent = extractQualifiedName(match[5], match[6], match[7], match[8]);
856
- partitions.push({ child, parent, sourceFile: basename(file) });
857
- }
858
- }
859
- return partitions;
860
- }
861
- function isValidSchemaName(name) {
862
- return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
863
- }
864
- function queryActualPartitions(dbUrl, schemas) {
865
- if (schemas.length === 0) return /* @__PURE__ */ new Map();
866
- const validSchemas = schemas.filter(isValidSchemaName);
867
- if (validSchemas.length === 0) return /* @__PURE__ */ new Map();
868
- const schemaList = validSchemas.map((s) => `'${s}'`).join(", ");
869
- const sql = `
870
- SELECT
871
- child_ns.nspname || '.' || child.relname AS child_name,
872
- parent_ns.nspname || '.' || parent.relname AS parent_name
873
- FROM pg_inherits i
874
- JOIN pg_class child ON child.oid = i.inhrelid
875
- JOIN pg_namespace child_ns ON child_ns.oid = child.relnamespace
876
- JOIN pg_class parent ON parent.oid = i.inhparent
877
- JOIN pg_namespace parent_ns ON parent_ns.oid = parent.relnamespace
878
- WHERE child.relkind IN ('r', 'p')
879
- AND child_ns.nspname IN (${schemaList})
880
- `;
881
- const result = psqlSyncQuery({ databaseUrl: dbUrl, sql: sql.trim(), timeout: 1e4 });
882
- const map = /* @__PURE__ */ new Map();
883
- if (result.status !== 0) return map;
884
- const lines = (result.stdout || "").split("\n").filter((l) => l.trim());
885
- for (const line of lines) {
886
- const parts = line.split("|").map((p) => p.trim());
887
- if (parts.length === 2 && parts[0] && parts[1]) {
888
- map.set(parts[0], parts[1]);
889
- }
890
- }
891
- return map;
892
- }
893
- function detectPartitionDrift(expected, actual) {
894
- const missing = expected.filter((ep) => !actual.has(ep.child));
895
- return { missing };
896
- }
897
- function formatPartitionWarnings(drift) {
898
- return drift.missing.map(
899
- (ep) => `Missing partition: ${ep.child} (PARTITION OF ${ep.parent}, defined in ${ep.sourceFile})`
900
- );
901
- }
902
-
903
- // src/commands/db/apply/helpers/idempotent-object-registry.ts
904
- function resolveIdempotentDir(schemasDir) {
905
- return schemasDir ? join(schemasDir, "..", "idempotent") : join(process.cwd(), "supabase", "schemas", "idempotent");
906
- }
907
- function readIdempotentSqlFiles(idempotentDir) {
908
- if (!existsSync(idempotentDir)) return null;
909
- try {
910
- const files = readdirSync(idempotentDir).filter((f) => f.endsWith(".sql"));
911
- return files.map((file) => ({
912
- file,
913
- content: stripSqlComments(readFileSync(join(idempotentDir, file), "utf-8"))
914
- }));
915
- } catch {
916
- return null;
917
- }
918
- }
919
- function readIdempotentSqlContent(idempotentDir) {
920
- const files = readIdempotentSqlFiles(idempotentDir);
921
- if (!files) return null;
922
- return files.map((f) => f.content).join("\n");
923
- }
924
- function extractQualifiedName2(m, schemaIdx1, schemaIdx2, nameIdx1, nameIdx2) {
925
- const schema = (m[schemaIdx1] ?? m[schemaIdx2] ?? "").toLowerCase();
926
- const name = (m[nameIdx1] ?? m[nameIdx2] ?? "").toLowerCase();
927
- return schema ? `${schema}.${name}` : name;
928
- }
929
- var cachedIdempotentRoles = null;
930
- function getIdempotentRoles(schemasDir) {
931
- if (cachedIdempotentRoles !== null) {
932
- return cachedIdempotentRoles;
933
- }
934
- const roles = [];
935
- const idempotentDir = resolveIdempotentDir(schemasDir);
936
- const sqlFiles = readIdempotentSqlFiles(idempotentDir);
937
- if (!sqlFiles) {
938
- cachedIdempotentRoles = [];
939
- return [];
940
- }
941
- for (const { content } of sqlFiles) {
942
- const roleMatches = content.matchAll(/CREATE\s+ROLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/gi);
943
- for (const match of roleMatches) {
944
- if (match[1]) {
945
- roles.push(match[1].toLowerCase());
946
- }
947
- }
948
- const existsMatches = content.matchAll(/rolname\s*=\s*'(\w+)'/gi);
949
- for (const match of existsMatches) {
950
- if (match[1] && !roles.includes(match[1].toLowerCase())) {
951
- roles.push(match[1].toLowerCase());
952
- }
953
- }
954
- }
955
- cachedIdempotentRoles = [...new Set(roles)];
956
- return cachedIdempotentRoles;
957
- }
958
- function getIdempotentProtectedTables(schemasDir, configExclusions) {
959
- const tables = /* @__PURE__ */ new Set();
960
- const idempotentDir = resolveIdempotentDir(schemasDir);
961
- const sqlFiles = readIdempotentSqlFiles(idempotentDir);
962
- if (sqlFiles) {
963
- for (const { content } of sqlFiles) {
964
- const matches = content.matchAll(
965
- /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|(\w+))\.(?:"([^"]+)"|(\w+))/gi
966
- );
967
- for (const m of matches) {
968
- const schema = (m[1] ?? m[2]).toLowerCase();
969
- const table = (m[3] ?? m[4]).toLowerCase();
970
- tables.add(`${schema}.${table}`);
971
- }
972
- }
973
- }
974
- if (configExclusions) {
975
- for (const pattern of configExclusions) {
976
- tables.add(pattern.toLowerCase());
977
- }
978
- }
979
- return [...tables];
980
- }
981
- function getIdempotentProtectedObjects(schemasDir, configExclusions) {
982
- const tables = getIdempotentProtectedTables(schemasDir, configExclusions);
983
- const functions = /* @__PURE__ */ new Set();
984
- const triggers = /* @__PURE__ */ new Set();
985
- const views = /* @__PURE__ */ new Set();
986
- const types = /* @__PURE__ */ new Set();
987
- const sequences = /* @__PURE__ */ new Set();
988
- const idempotentDir = resolveIdempotentDir(schemasDir);
989
- const content = readIdempotentSqlContent(idempotentDir);
990
- if (!content) {
991
- return { tables, functions: [], triggers: [], views: [], types: [], sequences: [] };
992
- }
993
- const funcRe = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))\s*\(/gi;
994
- for (const m of content.matchAll(funcRe)) {
995
- functions.add(extractQualifiedName2(m, 1, 2, 3, 4));
996
- }
997
- const trigRe = /CREATE\s+(?:OR\s+REPLACE\s+)?TRIGGER\s+(?:"([^"]+)"|(\w+))\s+.*?\s+ON\s+(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))/gi;
998
- for (const m of content.matchAll(trigRe)) {
999
- const trigName = (m[1] ?? m[2] ?? "").toLowerCase();
1000
- const tblSchema = (m[3] ?? m[4] ?? "").toLowerCase();
1001
- const tblName = (m[5] ?? m[6] ?? "").toLowerCase();
1002
- const qualified = tblSchema ? `${tblSchema}.${trigName}` : trigName;
1003
- triggers.add(qualified);
1004
- if (tblSchema) {
1005
- triggers.add(`${tblSchema}.${trigName}_on_${tblName}`);
1006
- }
1007
- }
1008
- const viewRe = /CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))/gi;
1009
- for (const m of content.matchAll(viewRe)) {
1010
- views.add(extractQualifiedName2(m, 1, 2, 3, 4));
1011
- }
1012
- const typeRe = /CREATE\s+TYPE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))/gi;
1013
- for (const m of content.matchAll(typeRe)) {
1014
- types.add(extractQualifiedName2(m, 1, 2, 3, 4));
1015
- }
1016
- const seqRe = /CREATE\s+SEQUENCE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))/gi;
1017
- for (const m of content.matchAll(seqRe)) {
1018
- sequences.add(extractQualifiedName2(m, 1, 2, 3, 4));
1019
- }
1020
- return {
1021
- tables,
1022
- functions: [...functions],
1023
- triggers: [...triggers],
1024
- views: [...views],
1025
- types: [...types],
1026
- sequences: [...sequences]
1027
- };
1028
- }
1029
- function isIdempotentRoleHazard(hazard, schemasDir) {
1030
- if (hazard.type !== "AUTHZ_UPDATE") {
1031
- return false;
1032
- }
1033
- const idempotentRoles = getIdempotentRoles(schemasDir);
1034
- if (idempotentRoles.length === 0) {
1035
- return false;
1036
- }
1037
- const sql = hazard.causingSql?.trim().toLowerCase() || "";
1038
- const isGrantRevoke = /^\s*(?:grant|revoke)\b/i.test(sql);
1039
- if (!isGrantRevoke) {
1040
- return false;
1041
- }
1042
- for (const role of idempotentRoles) {
1043
- const escapedRole = role.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1044
- const roleRegex = new RegExp(`\\b${escapedRole}\\b`, "i");
1045
- if (roleRegex.test(sql)) {
1046
- return true;
1047
- }
841
+ function isNoChangePlanOutput(planOutput) {
842
+ if (!planOutput.trim()) {
843
+ return true;
1048
844
  }
1049
- return false;
1050
- }
1051
- function filterFalsePositiveHazards(hazards, schemasDir) {
1052
- const filtered = [];
1053
- const falsePositives = [];
1054
- for (const hazard of hazards) {
1055
- if (isIdempotentRoleHazard(hazard, schemasDir)) {
1056
- falsePositives.push(hazard);
1057
- } else {
1058
- filtered.push(hazard);
1059
- }
845
+ if (planOutput.includes("No changes")) {
846
+ return true;
1060
847
  }
1061
- return { filtered, falsePositives };
848
+ return parsePlanOutput(planOutput).totalStatements === 0;
1062
849
  }
1063
850
 
1064
851
  // src/commands/db/apply/helpers/plan-check-filter.ts
852
+ init_esm_shims();
1065
853
  var FUNCTION_AUTHZ_ROLES = /* @__PURE__ */ new Set(["authenticated", "public", "service_role"]);
1066
854
  var cachedManagedAuthz;
1067
855
  function stripWrappingQuotes(value) {
@@ -1114,6 +902,21 @@ function parseSystemSchemas(sqlFiles) {
1114
902
  }
1115
903
  return systemSchemas;
1116
904
  }
905
+ function parseManagedRlsTables(sqlFiles) {
906
+ const rlsManagedTables = /* @__PURE__ */ new Set();
907
+ const rlsRegex = /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))\s+(?:ENABLE|DISABLE|FORCE|NO\s+FORCE)\s+ROW\s+LEVEL\s+SECURITY\b/gi;
908
+ for (const { content } of sqlFiles) {
909
+ for (const match of content.matchAll(rlsRegex)) {
910
+ const schema = (match[1] ?? match[2] ?? "public").toLowerCase();
911
+ const name = (match[3] ?? match[4] ?? "").toLowerCase();
912
+ if (!name) {
913
+ continue;
914
+ }
915
+ rlsManagedTables.add(`${schema}.${name}`);
916
+ }
917
+ }
918
+ return rlsManagedTables;
919
+ }
1117
920
  function getManagedAuthzConfig(schemasDir) {
1118
921
  const idempotentDir = resolveIdempotentDir(schemasDir);
1119
922
  if (cachedManagedAuthz?.key === idempotentDir) {
@@ -1123,7 +926,8 @@ function getManagedAuthzConfig(schemasDir) {
1123
926
  const config = {
1124
927
  functionTargets: parseManagedFunctionTargets(sqlFiles),
1125
928
  idempotentRoles: new Set(getIdempotentRoles(schemasDir)),
1126
- systemSchemas: parseSystemSchemas(sqlFiles)
929
+ systemSchemas: parseSystemSchemas(sqlFiles),
930
+ rlsManagedTables: parseManagedRlsTables(sqlFiles)
1127
931
  };
1128
932
  cachedManagedAuthz = { key: idempotentDir, value: config };
1129
933
  return config;
@@ -1211,6 +1015,28 @@ function isIdempotentManagedAuthzStatement(sql, schemasDir) {
1211
1015
  }
1212
1016
  return isRoleManagedAuthz(target, roles, config);
1213
1017
  }
1018
+ function parseRlsTarget(sql) {
1019
+ const match = sql.match(
1020
+ /\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:(?:"([^"]+)"|(\w+))\.)?(?:"([^"]+)"|(\w+))\s+(?:ENABLE|DISABLE|FORCE|NO\s+FORCE)\s+ROW\s+LEVEL\s+SECURITY\b/i
1021
+ );
1022
+ if (!match) {
1023
+ return null;
1024
+ }
1025
+ const schema = (match[1] ?? match[2] ?? "public").toLowerCase();
1026
+ const name = (match[3] ?? match[4] ?? "").toLowerCase();
1027
+ return name ? `${schema}.${name}` : null;
1028
+ }
1029
+ function isIdempotentManagedRlsStatement(sql, schemasDir) {
1030
+ const normalized = stripLeadingSessionStatements(sql).trim();
1031
+ if (!/^ALTER\s+TABLE\b/i.test(normalized)) {
1032
+ return false;
1033
+ }
1034
+ const target = parseRlsTarget(normalized);
1035
+ if (!target) {
1036
+ return false;
1037
+ }
1038
+ return getManagedAuthzConfig(schemasDir).rlsManagedTables.has(target);
1039
+ }
1214
1040
  function rebuildPlan(statements, sourcePlan) {
1215
1041
  return {
1216
1042
  statements,
@@ -1226,18 +1052,39 @@ function filterCheckModePlanStatements(plan, protectedTables, protectedObjects,
1226
1052
  protectedObjects
1227
1053
  );
1228
1054
  const removedAuthzStatements = [];
1055
+ const removedRlsStatements = [];
1229
1056
  const keptStatements = [];
1230
1057
  for (const statement of dropFilterResult.filteredPlan.statements) {
1231
1058
  if (isIdempotentManagedAuthzStatement(statement.sql, schemasDir)) {
1232
1059
  removedAuthzStatements.push(statement);
1233
1060
  continue;
1234
1061
  }
1062
+ if (isIdempotentManagedRlsStatement(statement.sql, schemasDir)) {
1063
+ removedRlsStatements.push(statement);
1064
+ continue;
1065
+ }
1235
1066
  keptStatements.push(statement);
1236
1067
  }
1237
1068
  return {
1238
1069
  filteredPlan: rebuildPlan(keptStatements, dropFilterResult.filteredPlan),
1239
1070
  removedDropStatements: dropFilterResult.removedStatements,
1240
- removedAuthzStatements
1071
+ removedAuthzStatements,
1072
+ removedRlsStatements
1073
+ };
1074
+ }
1075
+ function buildCheckModePlanSummary(params) {
1076
+ const categories = {
1077
+ idempotentDrop: params.removedDropStatements.length,
1078
+ idempotentAuthz: params.removedAuthzStatements.length,
1079
+ idempotentRls: params.removedRlsStatements.length,
1080
+ suppressedFunction: params.suppressedFunctionStatements?.length ?? 0
1081
+ };
1082
+ const noiseStatements = categories.idempotentDrop + categories.idempotentAuthz + categories.idempotentRls + categories.suppressedFunction;
1083
+ return {
1084
+ rawStatements: params.rawStatementCount,
1085
+ effectiveStatements: params.filteredPlan.statements.length,
1086
+ noiseStatements,
1087
+ categories
1241
1088
  };
1242
1089
  }
1243
1090
 
@@ -1404,33 +1251,63 @@ function summarizePreviewStatement(sql) {
1404
1251
  });
1405
1252
  return meaningfulLine ?? lines[0] ?? normalizedSql.trim();
1406
1253
  }
1407
- function displayCheckModeResults(planOutput, filterInfo) {
1408
- if (filterInfo && filterInfo.removedDropStatements.length > 0) {
1254
+ function displayCheckModeResults(planOutput, options = {}) {
1255
+ const filterInfo = options.filterInfo;
1256
+ const verbose = options.verbose === true;
1257
+ if (filterInfo?.planSummary) {
1409
1258
  logger3.info(
1410
- `Filtered ${filterInfo.removedDropStatements.length} idempotent-managed DROP statement(s):`
1259
+ `Plan summary: ${filterInfo.planSummary.rawStatements} raw statement(s), ${filterInfo.planSummary.effectiveStatements} structural, ${filterInfo.planSummary.noiseStatements} collapsed noise`
1411
1260
  );
1412
- for (const stmt of filterInfo.removedDropStatements) {
1413
- logger3.info(` Skipped: ${stmt.sql.split("\n")[0]}`);
1261
+ const categoryLabels = [
1262
+ ["idempotent DROP", filterInfo.planSummary.categories.idempotentDrop],
1263
+ ["idempotent AUTHZ", filterInfo.planSummary.categories.idempotentAuthz],
1264
+ ["idempotent RLS", filterInfo.planSummary.categories.idempotentRls],
1265
+ ["semantic function suppression", filterInfo.planSummary.categories.suppressedFunction]
1266
+ ];
1267
+ const summary = categoryLabels.filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`).join(", ");
1268
+ if (summary) {
1269
+ logger3.info(`Collapsed noise: ${summary}`);
1414
1270
  }
1415
1271
  logger3.info("");
1416
1272
  }
1417
- if (filterInfo && filterInfo.removedAuthzStatements.length > 0) {
1418
- logger3.info(
1419
- `Filtered ${filterInfo.removedAuthzStatements.length} idempotent-managed AUTHZ statement(s):`
1420
- );
1421
- const previewStatements = filterInfo.removedAuthzStatements.slice(0, 10);
1422
- for (const stmt of previewStatements) {
1423
- logger3.info(` Skipped: ${summarizePreviewStatement(stmt.sql)}`);
1273
+ const verboseNoiseSections = [
1274
+ {
1275
+ title: "idempotent-managed DROP statement(s)",
1276
+ statements: filterInfo?.removedDropStatements ?? []
1277
+ },
1278
+ {
1279
+ title: "idempotent-managed AUTHZ statement(s)",
1280
+ statements: filterInfo?.removedAuthzStatements ?? []
1281
+ },
1282
+ {
1283
+ title: "idempotent-managed RLS statement(s)",
1284
+ statements: filterInfo?.removedRlsStatements ?? []
1285
+ },
1286
+ {
1287
+ title: "semantically equivalent function statement(s)",
1288
+ statements: filterInfo?.suppressedFunctionStatements ?? []
1424
1289
  }
1425
- const remainingStatements = filterInfo.removedAuthzStatements.length - previewStatements.length;
1426
- if (remainingStatements > 0) {
1427
- logger3.info(` ... (${remainingStatements} more idempotent-managed AUTHZ statement(s))`);
1290
+ ];
1291
+ if (verbose) {
1292
+ for (const section of verboseNoiseSections) {
1293
+ if (section.statements.length === 0) continue;
1294
+ logger3.info(`Filtered ${section.statements.length} ${section.title}:`);
1295
+ const previewStatements = section.statements.slice(0, 10);
1296
+ for (const stmt of previewStatements) {
1297
+ logger3.info(` Skipped: ${summarizePreviewStatement(stmt.sql)}`);
1298
+ }
1299
+ const remainingStatements = section.statements.length - previewStatements.length;
1300
+ if (remainingStatements > 0) {
1301
+ logger3.info(` ... (${remainingStatements} more statement(s))`);
1302
+ }
1303
+ logger3.info("");
1428
1304
  }
1429
- logger3.info("");
1430
1305
  }
1431
1306
  const displaySql = filterInfo?.filteredPlanSql ?? planOutput;
1432
1307
  if (filterInfo && displaySql === "-- No changes after filtering") {
1433
- logger3.success("Check mode: All changes are for idempotent-managed objects (nothing to apply)");
1308
+ logger3.success(
1309
+ "Check mode: Only idempotent-managed metadata noise or semantically equivalent function churn detected"
1310
+ );
1434
1311
  return;
1435
1312
  }
1436
1313
  logger3.success("Check mode: Schema changes detected (not applied)");
@@ -5929,122 +5806,801 @@ function incrementRiskSummary(summary, level) {
5929
5806
  summary.high += 1;
5930
5807
  return;
5931
5808
  }
5932
- if (level === "medium") {
5933
- summary.medium += 1;
5934
- return;
5809
+ if (level === "medium") {
5810
+ summary.medium += 1;
5811
+ return;
5812
+ }
5813
+ summary.low += 1;
5814
+ }
5815
+ function logIdempotentRiskSummary(summary) {
5816
+ if (summary.high > 0 || summary.medium > 0) {
5817
+ logger15.warn(
5818
+ `Idempotent risk summary: ${summary.high} HIGH, ${summary.medium} MEDIUM, ${summary.low} LOW`
5819
+ );
5820
+ return;
5821
+ }
5822
+ logger15.success(`Idempotent risk scan: no HIGH/MEDIUM risks detected`);
5823
+ }
5824
+ async function detectIdempotentRiskSummary(schemasDir, files, verbose) {
5825
+ const summary = emptyRiskSummary();
5826
+ try {
5827
+ const { detectSchemaRisks: detectSchemaRisks2 } = await import('./risk-detector-S7XQF4I2.js');
5828
+ for (const file of files) {
5829
+ const filePath = join(schemasDir, file);
5830
+ const risks = await detectSchemaRisks2(filePath);
5831
+ for (const risk of risks) {
5832
+ incrementRiskSummary(summary, risk.level);
5833
+ }
5834
+ }
5835
+ logIdempotentRiskSummary(summary);
5836
+ } catch {
5837
+ if (verbose) {
5838
+ logger15.debug("Could not load risk detector for idempotent preview");
5839
+ }
5840
+ }
5841
+ return summary;
5842
+ }
5843
+ function executeSqlFileWithTransactionStrategy(dbUrl, filePath, verbose) {
5844
+ const strategy = getTransactionStrategy(filePath);
5845
+ if (strategy === "skip") {
5846
+ if (verbose) logger15.debug(` Transaction: skip (incompatible statements detected)`);
5847
+ const result = psqlSyncFile({ databaseUrl: dbUrl, filePath, onErrorStop: true });
5848
+ return result;
5849
+ }
5850
+ const content = readFileSync(filePath, "utf-8");
5851
+ const wrapped = wrapInTransaction(content);
5852
+ const tempFile = join(tmpdir(), `runa-idempotent-${randomUUID()}.sql`);
5853
+ writeFileSync(tempFile, wrapped, "utf-8");
5854
+ try {
5855
+ if (verbose) logger15.debug(` Transaction: wrap (BEGIN/COMMIT)`);
5856
+ return psqlSyncFile({ databaseUrl: dbUrl, filePath: tempFile, onErrorStop: true });
5857
+ } finally {
5858
+ try {
5859
+ unlinkSync(tempFile);
5860
+ } catch {
5861
+ }
5862
+ }
5863
+ }
5864
+ async function applySingleIdempotentFile(dbUrl, schemasDir, file, verbose, pass) {
5865
+ const filePath = join(schemasDir, file);
5866
+ if (verbose) logger15.debug(`Applying ${file}...`);
5867
+ const maxAttempts = pass === "post" ? IDEMPOTENT_MAX_RETRIES : 1;
5868
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5869
+ await waitBeforeIdempotentRetry(file, attempt, maxAttempts);
5870
+ const result = executeSqlFileWithTransactionStrategy(dbUrl, filePath, verbose);
5871
+ writeVerboseSqlOutput(result, verbose);
5872
+ if (result.status === 0) {
5873
+ return true;
5874
+ }
5875
+ const stderr = result.stderr?.trim() ?? "";
5876
+ if (pass === "pre") {
5877
+ return handlePrePassFailure(file, stderr);
5878
+ }
5879
+ if (shouldRetryPostPass(file, stderr, attempt, maxAttempts)) {
5880
+ continue;
5881
+ }
5882
+ throwPostPassFailure(file, stderr);
5883
+ }
5884
+ return false;
5885
+ }
5886
+ var applyIdempotentSchemas = fromPromise(async ({ input: { input, targetDir, pass } }) => {
5887
+ if (pass === "pre") checkPasswordSecurity();
5888
+ const schemasDir = join(targetDir, "supabase/schemas/idempotent");
5889
+ const dbUrl = getDbUrl(input);
5890
+ let filesApplied = 0;
5891
+ let filesSkipped = 0;
5892
+ if (!existsSync(schemasDir)) {
5893
+ logger15.info("No idempotent schemas found");
5894
+ } else {
5895
+ const files = collectSortedSqlFiles(schemasDir);
5896
+ if (files.length > 0) {
5897
+ const result = await applyIdempotentFiles(dbUrl, schemasDir, files, input.verbose, pass);
5898
+ filesApplied = result.filesApplied;
5899
+ filesSkipped = result.filesSkipped;
5900
+ logApplySummary(pass, filesApplied, filesSkipped, files.length);
5901
+ }
5902
+ }
5903
+ const rolePasswordsSet = setRolePasswords(dbUrl, input.verbose);
5904
+ return { filesApplied, filesSkipped, rolePasswordsSet };
5905
+ });
5906
+ var previewIdempotentSchemas = fromPromise(async ({ input: { input, targetDir } }) => {
5907
+ const schemasDir = join(targetDir, "supabase/schemas/idempotent");
5908
+ if (!existsSync(schemasDir)) {
5909
+ return { files: [], riskSummary: emptyRiskSummary() };
5910
+ }
5911
+ const files = collectSortedSqlFiles(schemasDir);
5912
+ if (files.length === 0) {
5913
+ return { files: [], riskSummary: emptyRiskSummary() };
5914
+ }
5915
+ logger15.info(`Idempotent schemas (${files.length} file(s)):`);
5916
+ for (const file of files) {
5917
+ logger15.info(` ${file}`);
5918
+ }
5919
+ const riskSummary = await detectIdempotentRiskSummary(schemasDir, files, input.verbose);
5920
+ return { files, riskSummary };
5921
+ });
5922
+
5923
+ // src/commands/db/apply/actors/pg-schema-diff-actors.ts
5924
+ init_esm_shims();
5925
+
5926
+ // src/commands/db/utils/duplicate-function-ownership.ts
5927
+ init_esm_shims();
5928
+ var DUPLICATE_FUNCTION_OWNERSHIP_NOTE = "Function ownership belongs to declarative SQL; idempotent 2nd pass must not redefine declarative-managed functions.";
5929
+ var TYPE_START_KEYWORDS = /* @__PURE__ */ new Set([
5930
+ "bigint",
5931
+ "bigserial",
5932
+ "bit",
5933
+ "boolean",
5934
+ "box",
5935
+ "bytea",
5936
+ "char",
5937
+ "character",
5938
+ "cidr",
5939
+ "circle",
5940
+ "date",
5941
+ "decimal",
5942
+ "double",
5943
+ "float",
5944
+ "float4",
5945
+ "float8",
5946
+ "inet",
5947
+ "int",
5948
+ "int2",
5949
+ "int4",
5950
+ "int8",
5951
+ "integer",
5952
+ "interval",
5953
+ "json",
5954
+ "jsonb",
5955
+ "line",
5956
+ "lseg",
5957
+ "macaddr",
5958
+ "money",
5959
+ "numeric",
5960
+ "path",
5961
+ "pg_lsn",
5962
+ "point",
5963
+ "polygon",
5964
+ "real",
5965
+ "record",
5966
+ "serial",
5967
+ "smallint",
5968
+ "smallserial",
5969
+ "text",
5970
+ "time",
5971
+ "timestamp",
5972
+ "timestamptz",
5973
+ "timetz",
5974
+ "tsquery",
5975
+ "tsvector",
5976
+ "txid_snapshot",
5977
+ "uuid",
5978
+ "varchar",
5979
+ "varying",
5980
+ "void",
5981
+ "xml"
5982
+ ]);
5983
+ function createFunctionDefinitionRegex() {
5984
+ return new RegExp(FUNCTION_DEFINITION_RE.source, FUNCTION_DEFINITION_RE.flags);
5985
+ }
5986
+ function normalizeSignatureText(signature) {
5987
+ return signature.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim();
5988
+ }
5989
+ function splitTopLevelCommaSeparated(input) {
5990
+ const parts = [];
5991
+ let current = "";
5992
+ let depth = 0;
5993
+ let inSingleQuote = false;
5994
+ let inDoubleQuote = false;
5995
+ let activeDollarTag = null;
5996
+ for (let index = 0; index < input.length; index += 1) {
5997
+ if (activeDollarTag) {
5998
+ if (input.startsWith(activeDollarTag, index)) {
5999
+ current += activeDollarTag;
6000
+ index += activeDollarTag.length - 1;
6001
+ activeDollarTag = null;
6002
+ continue;
6003
+ }
6004
+ current += input[index] ?? "";
6005
+ continue;
6006
+ }
6007
+ const char = input[index] ?? "";
6008
+ const next = input[index + 1] ?? "";
6009
+ if (inSingleQuote) {
6010
+ current += char;
6011
+ if (char === "'" && next === "'") {
6012
+ current += next;
6013
+ index += 1;
6014
+ continue;
6015
+ }
6016
+ if (char === "'") {
6017
+ inSingleQuote = false;
6018
+ }
6019
+ continue;
6020
+ }
6021
+ if (inDoubleQuote) {
6022
+ current += char;
6023
+ if (char === '"' && next === '"') {
6024
+ current += next;
6025
+ index += 1;
6026
+ continue;
6027
+ }
6028
+ if (char === '"') {
6029
+ inDoubleQuote = false;
6030
+ }
6031
+ continue;
6032
+ }
6033
+ if (char === "'") {
6034
+ inSingleQuote = true;
6035
+ current += char;
6036
+ continue;
6037
+ }
6038
+ if (char === '"') {
6039
+ inDoubleQuote = true;
6040
+ current += char;
6041
+ continue;
6042
+ }
6043
+ if (char === "$") {
6044
+ const dollarTag = tryMatchDollarTag(input, index);
6045
+ if (dollarTag) {
6046
+ activeDollarTag = dollarTag;
6047
+ current += dollarTag;
6048
+ index += dollarTag.length - 1;
6049
+ continue;
6050
+ }
6051
+ }
6052
+ if (char === "(" || char === "[") {
6053
+ depth += 1;
6054
+ current += char;
6055
+ continue;
6056
+ }
6057
+ if (char === ")" || char === "]") {
6058
+ depth = Math.max(0, depth - 1);
6059
+ current += char;
6060
+ continue;
6061
+ }
6062
+ if (char === "," && depth === 0) {
6063
+ parts.push(current.trim());
6064
+ current = "";
6065
+ continue;
6066
+ }
6067
+ current += char;
6068
+ }
6069
+ if (inSingleQuote || inDoubleQuote || activeDollarTag) {
6070
+ return null;
6071
+ }
6072
+ parts.push(current.trim());
6073
+ return parts.filter((part) => part.length > 0);
6074
+ }
6075
+ function splitTopLevelWhitespace(input) {
6076
+ const tokens = [];
6077
+ let current = "";
6078
+ let depth = 0;
6079
+ let inSingleQuote = false;
6080
+ let inDoubleQuote = false;
6081
+ let activeDollarTag = null;
6082
+ for (let index = 0; index < input.length; index += 1) {
6083
+ if (activeDollarTag) {
6084
+ if (input.startsWith(activeDollarTag, index)) {
6085
+ current += activeDollarTag;
6086
+ index += activeDollarTag.length - 1;
6087
+ activeDollarTag = null;
6088
+ continue;
6089
+ }
6090
+ current += input[index] ?? "";
6091
+ continue;
6092
+ }
6093
+ const char = input[index] ?? "";
6094
+ const next = input[index + 1] ?? "";
6095
+ if (inSingleQuote) {
6096
+ current += char;
6097
+ if (char === "'" && next === "'") {
6098
+ current += next;
6099
+ index += 1;
6100
+ continue;
6101
+ }
6102
+ if (char === "'") {
6103
+ inSingleQuote = false;
6104
+ }
6105
+ continue;
6106
+ }
6107
+ if (inDoubleQuote) {
6108
+ current += char;
6109
+ if (char === '"' && next === '"') {
6110
+ current += next;
6111
+ index += 1;
6112
+ continue;
6113
+ }
6114
+ if (char === '"') {
6115
+ inDoubleQuote = false;
6116
+ }
6117
+ continue;
6118
+ }
6119
+ if (char === "'") {
6120
+ inSingleQuote = true;
6121
+ current += char;
6122
+ continue;
6123
+ }
6124
+ if (char === '"') {
6125
+ inDoubleQuote = true;
6126
+ current += char;
6127
+ continue;
6128
+ }
6129
+ if (char === "$") {
6130
+ const dollarTag = tryMatchDollarTag(input, index);
6131
+ if (dollarTag) {
6132
+ activeDollarTag = dollarTag;
6133
+ current += dollarTag;
6134
+ index += dollarTag.length - 1;
6135
+ continue;
6136
+ }
6137
+ }
6138
+ if (char === "(" || char === "[") {
6139
+ depth += 1;
6140
+ current += char;
6141
+ continue;
6142
+ }
6143
+ if (char === ")" || char === "]") {
6144
+ depth = Math.max(0, depth - 1);
6145
+ current += char;
6146
+ continue;
6147
+ }
6148
+ if (/\s/.test(char) && depth === 0) {
6149
+ if (current.trim().length > 0) {
6150
+ tokens.push(current.trim());
6151
+ current = "";
6152
+ }
6153
+ continue;
6154
+ }
6155
+ current += char;
6156
+ }
6157
+ if (inSingleQuote || inDoubleQuote || activeDollarTag) {
6158
+ return null;
6159
+ }
6160
+ if (current.trim().length > 0) {
6161
+ tokens.push(current.trim());
6162
+ }
6163
+ return tokens;
6164
+ }
6165
+ function findTopLevelDefaultIndex(input) {
6166
+ let depth = 0;
6167
+ let inSingleQuote = false;
6168
+ let inDoubleQuote = false;
6169
+ let activeDollarTag = null;
6170
+ for (let index = 0; index < input.length; index += 1) {
6171
+ if (activeDollarTag) {
6172
+ if (input.startsWith(activeDollarTag, index)) {
6173
+ index += activeDollarTag.length - 1;
6174
+ activeDollarTag = null;
6175
+ }
6176
+ continue;
6177
+ }
6178
+ const char = input[index] ?? "";
6179
+ const next = input[index + 1] ?? "";
6180
+ if (inSingleQuote) {
6181
+ if (char === "'" && next === "'") {
6182
+ index += 1;
6183
+ continue;
6184
+ }
6185
+ if (char === "'") {
6186
+ inSingleQuote = false;
6187
+ }
6188
+ continue;
6189
+ }
6190
+ if (inDoubleQuote) {
6191
+ if (char === '"' && next === '"') {
6192
+ index += 1;
6193
+ continue;
6194
+ }
6195
+ if (char === '"') {
6196
+ inDoubleQuote = false;
6197
+ }
6198
+ continue;
6199
+ }
6200
+ if (char === "'") {
6201
+ inSingleQuote = true;
6202
+ continue;
6203
+ }
6204
+ if (char === '"') {
6205
+ inDoubleQuote = true;
6206
+ continue;
6207
+ }
6208
+ if (char === "$") {
6209
+ const dollarTag = tryMatchDollarTag(input, index);
6210
+ if (dollarTag) {
6211
+ activeDollarTag = dollarTag;
6212
+ index += dollarTag.length - 1;
6213
+ continue;
6214
+ }
6215
+ }
6216
+ if (char === "(" || char === "[") {
6217
+ depth += 1;
6218
+ continue;
6219
+ }
6220
+ if (char === ")" || char === "]") {
6221
+ depth = Math.max(0, depth - 1);
6222
+ continue;
6223
+ }
6224
+ if (depth === 0 && char === "=") {
6225
+ return index;
6226
+ }
6227
+ if (depth === 0 && input.slice(index).match(/^DEFAULT\b/i) && (index === 0 || /\s/.test(input[index - 1] ?? " "))) {
6228
+ return index;
6229
+ }
6230
+ }
6231
+ return null;
6232
+ }
6233
+ function stripDefaultExpression(argument) {
6234
+ const defaultIndex = findTopLevelDefaultIndex(argument);
6235
+ if (defaultIndex == null) {
6236
+ return argument.trim();
6237
+ }
6238
+ return argument.slice(0, defaultIndex).trim();
6239
+ }
6240
+ function normalizeTypeTokenText(tokens) {
6241
+ const normalized = tokens.join(" ").replace(/\s+/g, " ").replace(/\s+\(/g, "(").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/\s+\[/g, "[").replace(/\[\s+/g, "[").replace(/\s+\]/g, "]").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
6242
+ return canonicalizeTypeAlias(normalized);
6243
+ }
6244
+ function canonicalizeTypeAlias(typeText) {
6245
+ const aliasReplacements = [
6246
+ [/^int$/g, "integer"],
6247
+ [/\bint4\b/g, "integer"],
6248
+ [/\bint8\b/g, "bigint"],
6249
+ [/\bint2\b/g, "smallint"],
6250
+ [/\bfloat8\b/g, "double precision"],
6251
+ [/\bfloat4\b/g, "real"],
6252
+ [/\bdecimal\b/g, "numeric"],
6253
+ [/\bcharacter varying\b/g, "varchar"],
6254
+ [/\bcharacter\b/g, "char"],
6255
+ [/\btimestamp with time zone\b/g, "timestamptz"],
6256
+ [/\btimestamp without time zone\b/g, "timestamp"],
6257
+ [/\btime with time zone\b/g, "timetz"],
6258
+ [/\btime without time zone\b/g, "time"]
6259
+ ];
6260
+ return aliasReplacements.reduce(
6261
+ (current, [pattern, replacement]) => current.replace(pattern, replacement),
6262
+ typeText
6263
+ );
6264
+ }
6265
+ function normalizeIdentifierToken(token) {
6266
+ return token.replace(/^"|"$/g, "").toLowerCase();
6267
+ }
6268
+ function looksLikeNamedArgumentToken(token) {
6269
+ return /^"[^"]+"$/.test(token) || /^[A-Za-z_][A-Za-z0-9_]*$/.test(token);
6270
+ }
6271
+ function looksLikeTypeTokens(tokens) {
6272
+ if (tokens.length === 0) {
6273
+ return false;
6274
+ }
6275
+ const first = normalizeIdentifierToken(tokens[0] ?? "");
6276
+ if (TYPE_START_KEYWORDS.has(first)) {
6277
+ return true;
6278
+ }
6279
+ if (first.includes(".") || first.includes("%type") || first.endsWith("[]") || tokens[0]?.includes("(") || tokens[0]?.includes("[")) {
6280
+ return true;
6281
+ }
6282
+ return tokens.length === 1;
6283
+ }
6284
+ function normalizeArgumentIdentity(argument) {
6285
+ const withoutDefault = stripDefaultExpression(argument);
6286
+ const tokens = splitTopLevelWhitespace(withoutDefault);
6287
+ if (!tokens || tokens.length === 0) {
6288
+ return null;
6289
+ }
6290
+ let index = 0;
6291
+ let mode = null;
6292
+ const firstToken = normalizeIdentifierToken(tokens[index] ?? "");
6293
+ const secondToken = normalizeIdentifierToken(tokens[index + 1] ?? "");
6294
+ if (firstToken === "in" && secondToken === "out") {
6295
+ mode = "inout";
6296
+ index += 2;
6297
+ } else if (firstToken === "in" || firstToken === "out" || firstToken === "inout") {
6298
+ mode = firstToken;
6299
+ index += 1;
6300
+ } else if (firstToken === "variadic") {
6301
+ mode = "variadic";
6302
+ index += 1;
6303
+ }
6304
+ let remaining = tokens.slice(index);
6305
+ if (remaining.length === 0) {
6306
+ return null;
6307
+ }
6308
+ if (mode === "out") {
6309
+ return null;
6310
+ }
6311
+ const shouldDropName = remaining.length > 1 && looksLikeNamedArgumentToken(remaining[0] ?? "") && !TYPE_START_KEYWORDS.has(normalizeIdentifierToken(remaining[0] ?? "")) && looksLikeTypeTokens(remaining.slice(1));
6312
+ if (shouldDropName) {
6313
+ remaining = remaining.slice(1);
6314
+ }
6315
+ if (remaining.length === 0) {
6316
+ return null;
6317
+ }
6318
+ const normalizedType = normalizeTypeTokenText(remaining);
6319
+ if (!normalizedType) {
6320
+ return null;
6321
+ }
6322
+ return mode === "variadic" ? `variadic ${normalizedType}` : normalizedType;
6323
+ }
6324
+ function tryMatchDollarTag(sql, index) {
6325
+ const slice = sql.slice(index);
6326
+ const match = slice.match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/);
6327
+ return match?.[0] ?? null;
6328
+ }
6329
+ function findMatchingParen(sql, openIndex) {
6330
+ let depth = 0;
6331
+ let index = openIndex;
6332
+ let inSingleQuote = false;
6333
+ let inDoubleQuote = false;
6334
+ let activeDollarTag = null;
6335
+ while (index < sql.length) {
6336
+ if (activeDollarTag) {
6337
+ if (sql.startsWith(activeDollarTag, index)) {
6338
+ index += activeDollarTag.length;
6339
+ activeDollarTag = null;
6340
+ continue;
6341
+ }
6342
+ index += 1;
6343
+ continue;
6344
+ }
6345
+ const char = sql[index] ?? "";
6346
+ const next = sql[index + 1] ?? "";
6347
+ if (inSingleQuote) {
6348
+ if (char === "'" && next === "'") {
6349
+ index += 2;
6350
+ continue;
6351
+ }
6352
+ if (char === "'") {
6353
+ inSingleQuote = false;
6354
+ }
6355
+ index += 1;
6356
+ continue;
6357
+ }
6358
+ if (inDoubleQuote) {
6359
+ if (char === '"' && next === '"') {
6360
+ index += 2;
6361
+ continue;
6362
+ }
6363
+ if (char === '"') {
6364
+ inDoubleQuote = false;
6365
+ }
6366
+ index += 1;
6367
+ continue;
6368
+ }
6369
+ if (char === "'") {
6370
+ inSingleQuote = true;
6371
+ index += 1;
6372
+ continue;
6373
+ }
6374
+ if (char === '"') {
6375
+ inDoubleQuote = true;
6376
+ index += 1;
6377
+ continue;
6378
+ }
6379
+ if (char === "$") {
6380
+ const dollarTag = tryMatchDollarTag(sql, index);
6381
+ if (dollarTag) {
6382
+ activeDollarTag = dollarTag;
6383
+ index += dollarTag.length;
6384
+ continue;
6385
+ }
6386
+ }
6387
+ if (char === "(") {
6388
+ depth += 1;
6389
+ } else if (char === ")") {
6390
+ depth -= 1;
6391
+ if (depth === 0) {
6392
+ return index;
6393
+ }
6394
+ }
6395
+ index += 1;
6396
+ }
6397
+ return null;
6398
+ }
6399
+ function extractFunctionSignature(statement, match) {
6400
+ const definitionEndIndex = (match.index ?? 0) + match[0].length;
6401
+ const openParenIndex = statement.indexOf("(", definitionEndIndex);
6402
+ if (openParenIndex < 0) {
6403
+ return null;
6404
+ }
6405
+ const closeParenIndex = findMatchingParen(statement, openParenIndex);
6406
+ if (closeParenIndex == null) {
6407
+ return null;
6408
+ }
6409
+ const rawSignature = statement.slice(openParenIndex + 1, closeParenIndex);
6410
+ const argumentsList = splitTopLevelCommaSeparated(rawSignature);
6411
+ if (!argumentsList) {
6412
+ return null;
5935
6413
  }
5936
- summary.low += 1;
6414
+ const normalizedArguments = [];
6415
+ for (const argument of argumentsList) {
6416
+ const normalizedArgument = normalizeArgumentIdentity(argument);
6417
+ if (normalizedArgument) {
6418
+ normalizedArguments.push(normalizedArgument);
6419
+ }
6420
+ }
6421
+ return `(${normalizeSignatureText(normalizedArguments.join(", "))})`;
5937
6422
  }
5938
- function logIdempotentRiskSummary(summary) {
5939
- if (summary.high > 0 || summary.medium > 0) {
5940
- logger15.warn(
5941
- `Idempotent risk summary: ${summary.high} HIGH, ${summary.medium} MEDIUM, ${summary.low} LOW`
5942
- );
5943
- return;
6423
+ function extractFunctionOwnershipDefinition(statement, file, line, layer) {
6424
+ const regex = createFunctionDefinitionRegex();
6425
+ const match = regex.exec(statement);
6426
+ if (!match) {
6427
+ return null;
5944
6428
  }
5945
- logger15.success(`Idempotent risk scan: no HIGH/MEDIUM risks detected`);
6429
+ const schema = (match[1] ?? match[2] ?? PUBLIC_SCHEMA).toLowerCase();
6430
+ const name = (match[3] ?? match[4] ?? "").toLowerCase();
6431
+ if (!name) {
6432
+ return null;
6433
+ }
6434
+ return {
6435
+ qualifiedName: `${schema}.${name}`,
6436
+ signature: extractFunctionSignature(statement, match),
6437
+ file,
6438
+ line,
6439
+ layer
6440
+ };
5946
6441
  }
5947
- async function detectIdempotentRiskSummary(schemasDir, files, verbose) {
5948
- const summary = emptyRiskSummary();
5949
- try {
5950
- const { detectSchemaRisks: detectSchemaRisks2 } = await import('./risk-detector-VO5HJR4R.js');
5951
- for (const file of files) {
5952
- const filePath = join(schemasDir, file);
5953
- const risks = await detectSchemaRisks2(filePath);
5954
- for (const risk of risks) {
5955
- incrementRiskSummary(summary, risk.level);
6442
+ function collectFunctionOwnershipDefinitions(targetDir, layer) {
6443
+ const definitions = [];
6444
+ for (const file of collectSqlFiles(targetDir, layer)) {
6445
+ for (const statement of splitSqlStatements(file.content)) {
6446
+ const definition = extractFunctionOwnershipDefinition(
6447
+ statement.statement,
6448
+ file.relativePath,
6449
+ statement.line,
6450
+ layer
6451
+ );
6452
+ if (definition) {
6453
+ definitions.push(definition);
5956
6454
  }
5957
6455
  }
5958
- logIdempotentRiskSummary(summary);
5959
- } catch {
5960
- if (verbose) {
5961
- logger15.debug("Could not load risk detector for idempotent preview");
5962
- }
5963
6456
  }
5964
- return summary;
6457
+ return definitions;
5965
6458
  }
5966
- function executeSqlFileWithTransactionStrategy(dbUrl, filePath, verbose) {
5967
- const strategy = getTransactionStrategy(filePath);
5968
- if (strategy === "skip") {
5969
- if (verbose) logger15.debug(` Transaction: skip (incompatible statements detected)`);
5970
- const result = psqlSyncFile({ databaseUrl: dbUrl, filePath, onErrorStop: true });
5971
- return result;
5972
- }
5973
- const content = readFileSync(filePath, "utf-8");
5974
- const wrapped = wrapInTransaction(content);
5975
- const tempFile = join(tmpdir(), `runa-idempotent-${randomUUID()}.sql`);
5976
- writeFileSync(tempFile, wrapped, "utf-8");
5977
- try {
5978
- if (verbose) logger15.debug(` Transaction: wrap (BEGIN/COMMIT)`);
5979
- return psqlSyncFile({ databaseUrl: dbUrl, filePath: tempFile, onErrorStop: true });
5980
- } finally {
5981
- try {
5982
- unlinkSync(tempFile);
5983
- } catch {
5984
- }
6459
+ function groupDefinitionsByQualifiedName(definitions) {
6460
+ const grouped = /* @__PURE__ */ new Map();
6461
+ for (const definition of definitions) {
6462
+ const existing = grouped.get(definition.qualifiedName) ?? [];
6463
+ existing.push(definition);
6464
+ grouped.set(definition.qualifiedName, existing);
5985
6465
  }
6466
+ return grouped;
5986
6467
  }
5987
- async function applySingleIdempotentFile(dbUrl, schemasDir, file, verbose, pass) {
5988
- const filePath = join(schemasDir, file);
5989
- if (verbose) logger15.debug(`Applying ${file}...`);
5990
- const maxAttempts = pass === "post" ? IDEMPOTENT_MAX_RETRIES : 1;
5991
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
5992
- await waitBeforeIdempotentRetry(file, attempt, maxAttempts);
5993
- const result = executeSqlFileWithTransactionStrategy(dbUrl, filePath, verbose);
5994
- writeVerboseSqlOutput(result, verbose);
5995
- if (result.status === 0) {
5996
- return true;
6468
+ function buildExactFinding(params) {
6469
+ return {
6470
+ code: "DUPLICATE_FUNCTION_OWNERSHIP",
6471
+ qualifiedName: params.qualifiedName,
6472
+ signature: params.signature,
6473
+ message: `Function ${params.qualifiedName}${params.signature} is defined in both declarative and idempotent SQL.`,
6474
+ suggestion: "Keep the canonical function definition in declarative SQL and remove the idempotent duplicate.",
6475
+ declarativeDefinitions: params.declarativeDefinitions,
6476
+ idempotentDefinitions: params.idempotentDefinitions
6477
+ };
6478
+ }
6479
+ function buildAmbiguousFinding(params) {
6480
+ return {
6481
+ code: "DUPLICATE_FUNCTION_OWNERSHIP_AMBIGUOUS",
6482
+ qualifiedName: params.qualifiedName,
6483
+ signature: null,
6484
+ message: `Function ${params.qualifiedName} appears in both declarative and idempotent SQL, and the signature could not be matched safely.`,
6485
+ suggestion: "Review both definitions manually and keep ownership in declarative SQL only.",
6486
+ declarativeDefinitions: params.declarativeDefinitions,
6487
+ idempotentDefinitions: params.idempotentDefinitions
6488
+ };
6489
+ }
6490
+ function analyzeDuplicateFunctionOwnership(targetDir) {
6491
+ const declarativeDefinitions = collectFunctionOwnershipDefinitions(targetDir, "declarative");
6492
+ const idempotentDefinitions = collectFunctionOwnershipDefinitions(targetDir, "idempotent");
6493
+ const idempotentByQualifiedName = groupDefinitionsByQualifiedName(idempotentDefinitions);
6494
+ const findings = [];
6495
+ const seenExact = /* @__PURE__ */ new Set();
6496
+ const seenAmbiguous = /* @__PURE__ */ new Set();
6497
+ for (const declarativeDefinition of declarativeDefinitions) {
6498
+ const idempotentMatches = idempotentByQualifiedName.get(declarativeDefinition.qualifiedName);
6499
+ if (!idempotentMatches || idempotentMatches.length === 0) {
6500
+ continue;
5997
6501
  }
5998
- const stderr = result.stderr?.trim() ?? "";
5999
- if (pass === "pre") {
6000
- return handlePrePassFailure(file, stderr);
6502
+ if (declarativeDefinition.signature) {
6503
+ const exactMatches = idempotentMatches.filter(
6504
+ (candidate) => candidate.signature !== null && candidate.signature === declarativeDefinition.signature
6505
+ );
6506
+ if (exactMatches.length > 0) {
6507
+ const exactKey = `${declarativeDefinition.qualifiedName}:${declarativeDefinition.signature}`;
6508
+ if (!seenExact.has(exactKey)) {
6509
+ seenExact.add(exactKey);
6510
+ findings.push(
6511
+ buildExactFinding({
6512
+ qualifiedName: declarativeDefinition.qualifiedName,
6513
+ signature: declarativeDefinition.signature,
6514
+ declarativeDefinitions: declarativeDefinitions.filter(
6515
+ (candidate) => candidate.qualifiedName === declarativeDefinition.qualifiedName && candidate.signature === declarativeDefinition.signature
6516
+ ),
6517
+ idempotentDefinitions: exactMatches
6518
+ })
6519
+ );
6520
+ }
6521
+ continue;
6522
+ }
6001
6523
  }
6002
- if (shouldRetryPostPass(file, stderr, attempt, maxAttempts)) {
6524
+ const hasAmbiguousMatch = declarativeDefinition.signature === null || idempotentMatches.some((candidate) => candidate.signature === null);
6525
+ if (!hasAmbiguousMatch) {
6003
6526
  continue;
6004
6527
  }
6005
- throwPostPassFailure(file, stderr);
6528
+ if (!seenAmbiguous.has(declarativeDefinition.qualifiedName)) {
6529
+ seenAmbiguous.add(declarativeDefinition.qualifiedName);
6530
+ findings.push(
6531
+ buildAmbiguousFinding({
6532
+ qualifiedName: declarativeDefinition.qualifiedName,
6533
+ declarativeDefinitions: declarativeDefinitions.filter(
6534
+ (candidate) => candidate.qualifiedName === declarativeDefinition.qualifiedName
6535
+ ),
6536
+ idempotentDefinitions: idempotentMatches
6537
+ })
6538
+ );
6539
+ }
6006
6540
  }
6007
- return false;
6008
- }
6009
- var applyIdempotentSchemas = fromPromise(async ({ input: { input, targetDir, pass } }) => {
6010
- if (pass === "pre") checkPasswordSecurity();
6011
- const schemasDir = join(targetDir, "supabase/schemas/idempotent");
6012
- const dbUrl = getDbUrl(input);
6013
- let filesApplied = 0;
6014
- let filesSkipped = 0;
6015
- if (!existsSync(schemasDir)) {
6016
- logger15.info("No idempotent schemas found");
6017
- } else {
6018
- const files = collectSortedSqlFiles(schemasDir);
6019
- if (files.length > 0) {
6020
- const result = await applyIdempotentFiles(dbUrl, schemasDir, files, input.verbose, pass);
6021
- filesApplied = result.filesApplied;
6022
- filesSkipped = result.filesSkipped;
6023
- logApplySummary(pass, filesApplied, filesSkipped, files.length);
6541
+ return {
6542
+ contractNote: DUPLICATE_FUNCTION_OWNERSHIP_NOTE,
6543
+ findings,
6544
+ definitions: {
6545
+ declarative: declarativeDefinitions,
6546
+ idempotent: idempotentDefinitions
6024
6547
  }
6548
+ };
6549
+ }
6550
+ function formatLocation(definition) {
6551
+ return `${definition.file}:${definition.line}`;
6552
+ }
6553
+ function formatDuplicateFunctionOwnershipFinding(finding) {
6554
+ const signatureSuffix = finding.signature ?? "(<manual review required>)";
6555
+ return {
6556
+ summary: `${finding.code}: ${finding.qualifiedName}${signatureSuffix}`,
6557
+ declarativeLocations: finding.declarativeDefinitions.map(formatLocation),
6558
+ idempotentLocations: finding.idempotentDefinitions.map(formatLocation),
6559
+ suggestion: finding.suggestion
6560
+ };
6561
+ }
6562
+
6563
+ // src/commands/db/utils/plan-size-guard.ts
6564
+ init_esm_shims();
6565
+ var PLAN_SIZE_WARNING_THRESHOLD = {
6566
+ effectiveStatements: 200,
6567
+ rawStatements: 500
6568
+ };
6569
+ var PLAN_SIZE_BLOCKER_THRESHOLD = {
6570
+ effectiveStatements: 500,
6571
+ rawStatements: 1e3
6572
+ };
6573
+ function buildThresholdReasons(summary, threshold) {
6574
+ const reasons = [];
6575
+ if (summary.effectiveStatements >= threshold.effectiveStatements) {
6576
+ reasons.push(
6577
+ `effective structural statements ${summary.effectiveStatements} >= ${threshold.effectiveStatements}`
6578
+ );
6025
6579
  }
6026
- const rolePasswordsSet = setRolePasswords(dbUrl, input.verbose);
6027
- return { filesApplied, filesSkipped, rolePasswordsSet };
6028
- });
6029
- var previewIdempotentSchemas = fromPromise(async ({ input: { input, targetDir } }) => {
6030
- const schemasDir = join(targetDir, "supabase/schemas/idempotent");
6031
- if (!existsSync(schemasDir)) {
6032
- return { files: [], riskSummary: emptyRiskSummary() };
6580
+ if (summary.rawStatements >= threshold.rawStatements) {
6581
+ reasons.push(`raw statements ${summary.rawStatements} >= ${threshold.rawStatements}`);
6033
6582
  }
6034
- const files = collectSortedSqlFiles(schemasDir);
6035
- if (files.length === 0) {
6036
- return { files: [], riskSummary: emptyRiskSummary() };
6583
+ return reasons;
6584
+ }
6585
+ function assessPlanSize(summary) {
6586
+ if (!summary) {
6587
+ return { severity: "ok", reasons: [] };
6037
6588
  }
6038
- logger15.info(`Idempotent schemas (${files.length} file(s)):`);
6039
- for (const file of files) {
6040
- logger15.info(` ${file}`);
6589
+ const blockerReasons = buildThresholdReasons(summary, PLAN_SIZE_BLOCKER_THRESHOLD);
6590
+ if (blockerReasons.length > 0) {
6591
+ return { severity: "blocker", reasons: blockerReasons };
6041
6592
  }
6042
- const riskSummary = await detectIdempotentRiskSummary(schemasDir, files, input.verbose);
6043
- return { files, riskSummary };
6044
- });
6593
+ const warningReasons = buildThresholdReasons(summary, PLAN_SIZE_WARNING_THRESHOLD);
6594
+ if (warningReasons.length > 0) {
6595
+ return { severity: "warning", reasons: warningReasons };
6596
+ }
6597
+ return { severity: "ok", reasons: [] };
6598
+ }
6599
+ function formatPlanSizeSummary(summary) {
6600
+ return `${summary.rawStatements} raw statement(s), ${summary.effectiveStatements} structural, ${summary.noiseStatements} collapsed noise`;
6601
+ }
6045
6602
 
6046
6603
  // src/commands/db/apply/actors/pg-schema-diff-actors.ts
6047
- init_esm_shims();
6048
6604
  init_local_supabase();
6049
6605
 
6050
6606
  // src/commands/db/utils/declarative-dependency-warning-governance.ts
@@ -6673,8 +7229,28 @@ function buildDirectoryPlacementRule(entry, index, seenIds, issueFormatter) {
6673
7229
  issueFormatter
6674
7230
  );
6675
7231
  reportDeprecatedLineUsage(id, line, lineStart, lineEnd, issueFormatter);
6676
- const scopedLineRange = toScopedLineRange(lineStart, lineEnd, id, section, issueFormatter);
6677
- if (!scopedLineRange) return null;
7232
+ const objectPatternStr = coerceOptionalString(
7233
+ rawRule.objectPattern,
7234
+ index,
7235
+ `directoryPlacementAllowlist[${index}].objectPattern`,
7236
+ issueFormatter
7237
+ );
7238
+ const compiledObjectPattern = compileOptionalRulePattern(
7239
+ objectPatternStr ?? null,
7240
+ issueFormatter
7241
+ );
7242
+ if (objectPatternStr && !compiledObjectPattern) return null;
7243
+ const hasObjectPattern = compiledObjectPattern !== void 0;
7244
+ let scopedLineRange;
7245
+ if (!hasObjectPattern) {
7246
+ scopedLineRange = toScopedLineRange(lineStart, lineEnd, id, section, issueFormatter);
7247
+ if (!scopedLineRange) return null;
7248
+ } else {
7249
+ if (lineStart !== void 0 && lineEnd !== void 0) {
7250
+ scopedLineRange = toScopedLineRange(lineStart, lineEnd, id, section, issueFormatter);
7251
+ if (!scopedLineRange) return null;
7252
+ }
7253
+ }
6678
7254
  const expiresAt = coerceExpiresAt(
6679
7255
  rawRule.expiresAt,
6680
7256
  id,
@@ -6692,9 +7268,10 @@ function buildDirectoryPlacementRule(entry, index, seenIds, issueFormatter) {
6692
7268
  id,
6693
7269
  filePattern: compiledPatterns[0],
6694
7270
  messagePattern: compiledPatterns[1],
7271
+ objectPattern: compiledObjectPattern,
6695
7272
  level,
6696
- lineStart: scopedLineRange.lineStart,
6697
- lineEnd: scopedLineRange.lineEnd,
7273
+ lineStart: scopedLineRange?.lineStart,
7274
+ lineEnd: scopedLineRange?.lineEnd,
6698
7275
  reason,
6699
7276
  owner,
6700
7277
  ticket,
@@ -7044,6 +7621,10 @@ function findDirectoryPlacementAllowlistMatch(issue, policy) {
7044
7621
  if (entry.level !== void 0 && entry.level !== issue.level) return false;
7045
7622
  if (!entry.filePattern.test(issue.file)) return false;
7046
7623
  if (!entry.messagePattern.test(issue.message)) return false;
7624
+ if (entry.objectPattern) {
7625
+ if (entry.objectPattern.test(issue.message)) return true;
7626
+ if (!hasExplicitLineScope(entry)) return false;
7627
+ }
7047
7628
  if (!hasExplicitLineScope(entry)) return false;
7048
7629
  return entryLineScopeMatches(issue.line, entry);
7049
7630
  });
@@ -7486,6 +8067,25 @@ function assertDeclarativeDependencyBoundary(targetDir, input) {
7486
8067
  throw new Error(buildDeclarativeDependencyWarningFailureLines(warningReview).join("\n"));
7487
8068
  }
7488
8069
  }
8070
+ function warnDuplicateFunctionOwnership(targetDir) {
8071
+ const analysis = analyzeDuplicateFunctionOwnership(targetDir);
8072
+ if (analysis.findings.length === 0) {
8073
+ return;
8074
+ }
8075
+ logger15.warn(`Duplicate function ownership detected (${analysis.findings.length} finding(s))`);
8076
+ logger15.info(` ${analysis.contractNote}`);
8077
+ for (const finding of analysis.findings) {
8078
+ const formatted = formatDuplicateFunctionOwnershipFinding(finding);
8079
+ logger15.info(` [WARN] ${formatted.summary}`);
8080
+ for (const location of formatted.declarativeLocations) {
8081
+ logger15.info(` declarative: ${location}`);
8082
+ }
8083
+ for (const location of formatted.idempotentLocations) {
8084
+ logger15.info(` idempotent: ${location}`);
8085
+ }
8086
+ logger15.info(` ${formatted.suggestion}`);
8087
+ }
8088
+ }
7489
8089
  function createCombinedSchemaBundle(schemaFiles, verbose) {
7490
8090
  const tmpDir = mkdtempSync(join(tmpdir(), "runa_pg_schema_diff_"));
7491
8091
  const combinedSchemaPath = join(tmpDir, "desired_schema.sql");
@@ -7516,7 +8116,7 @@ async function createShadowDbForRun(dbUrl, shadowExtensions, verbose) {
7516
8116
  return shadowDb;
7517
8117
  }
7518
8118
  function buildNoChangesResult(planOutput) {
7519
- if (!planOutput.trim() || planOutput.includes("No changes")) {
8119
+ if (isNoChangePlanOutput(planOutput)) {
7520
8120
  logger15.success("No schema changes detected");
7521
8121
  return { sql: "", hazards: [], applied: true };
7522
8122
  }
@@ -7571,20 +8171,43 @@ function runPreApplyDataCompatibility(dbUrl, planOutput, input) {
7571
8171
  }
7572
8172
  return 0;
7573
8173
  }
7574
- function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir) {
8174
+ function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir, options) {
7575
8175
  if (!input.check) {
7576
8176
  return null;
7577
8177
  }
7578
8178
  const plan = parsePlanOutput(planOutput);
7579
- const { filteredPlan, removedDropStatements, removedAuthzStatements } = filterCheckModePlanStatements(plan, protectedTables, protectedObjects, schemasDir);
7580
- displayCheckModeResults(planOutput, {
7581
- filteredPlanSql: filteredPlan.rawSql,
8179
+ const { filteredPlan, removedDropStatements, removedAuthzStatements, removedRlsStatements } = filterCheckModePlanStatements(plan, protectedTables, protectedObjects, schemasDir);
8180
+ const planSummary = buildCheckModePlanSummary({
8181
+ rawStatementCount: options?.rawStatementCount ?? plan.statements.length,
8182
+ filteredPlan,
7582
8183
  removedDropStatements,
7583
- removedAuthzStatements
8184
+ removedAuthzStatements,
8185
+ removedRlsStatements,
8186
+ suppressedFunctionStatements: options?.suppressedFunctionStatements
8187
+ });
8188
+ displayCheckModeResults(planOutput, {
8189
+ verbose: input.verbose,
8190
+ filterInfo: {
8191
+ filteredPlanSql: filteredPlan.rawSql,
8192
+ removedDropStatements,
8193
+ removedAuthzStatements,
8194
+ removedRlsStatements,
8195
+ suppressedFunctionStatements: options?.suppressedFunctionStatements,
8196
+ planSummary
8197
+ }
7584
8198
  });
8199
+ const planSizeAssessment = assessPlanSize(planSummary);
8200
+ if (planSizeAssessment.severity !== "ok") {
8201
+ logger15.warn(`Large plan warning: ${formatPlanSizeSummary(planSummary)}`);
8202
+ for (const reason of planSizeAssessment.reasons) {
8203
+ logger15.info(` \u2022 ${reason}`);
8204
+ }
8205
+ logger15.info(" Small production instances may see elevated load during apply.");
8206
+ }
7585
8207
  return {
7586
8208
  sql: planOutput,
7587
8209
  filteredPlanSql: filteredPlan.rawSql,
8210
+ planSummary,
7588
8211
  hazards,
7589
8212
  applied: false,
7590
8213
  dataViolations: dataViolationCount > 0 ? dataViolationCount : void 0
@@ -7633,6 +8256,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7633
8256
  logger15.info("No declarative schemas found");
7634
8257
  return { sql: "", hazards: [], applied: false };
7635
8258
  }
8259
+ warnDuplicateFunctionOwnership(targetDir);
7636
8260
  assertDeclarativeDependencyBoundary(targetDir, input);
7637
8261
  const dbUrl = getDbUrl(input);
7638
8262
  const configState = loadPgSchemaDiffConfigState(targetDir, input.verbose);
@@ -7714,13 +8338,14 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7714
8338
  }
7715
8339
  )
7716
8340
  );
8341
+ const rawPlanStatementCount = parsePlanOutput(planOutput).statements.length;
7717
8342
  let effectivePlanOutput = planOutput;
8343
+ let suppressedPlan = {
8344
+ planOutput,
8345
+ suppressedStatements: [],
8346
+ warnings: []
8347
+ };
7718
8348
  if (!input.compareOnly) {
7719
- let suppressedPlan = {
7720
- planOutput,
7721
- suppressedStatements: [],
7722
- warnings: []
7723
- };
7724
8349
  try {
7725
8350
  suppressedPlan = await withProgress(
7726
8351
  "reviewing plan false positives",
@@ -7740,7 +8365,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7740
8365
  }
7741
8366
  effectivePlanOutput = suppressedPlan.planOutput;
7742
8367
  }
7743
- const noChangesResult = buildNoChangesResult(effectivePlanOutput);
8368
+ const noChangesResult = rawPlanStatementCount === 0 || isNoChangePlanOutput(effectivePlanOutput) ? buildNoChangesResult(effectivePlanOutput) : null;
7744
8369
  if (noChangesResult) return noChangesResult;
7745
8370
  const { hazards } = await withProgress(
7746
8371
  "extracting migration hazards",
@@ -7767,7 +8392,11 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7767
8392
  protectedTables,
7768
8393
  protectedObjects,
7769
8394
  dataViolationCount,
7770
- schemasDir
8395
+ schemasDir,
8396
+ {
8397
+ rawStatementCount: rawPlanStatementCount,
8398
+ suppressedFunctionStatements: !input.compareOnly ? suppressedPlan.suppressedStatements : void 0
8399
+ }
7771
8400
  );
7772
8401
  if (checkModeResult) return checkModeResult;
7773
8402
  backupProtectedTablesForProduction(dbUrl, protectedTables, input);
@@ -7978,15 +8607,16 @@ function runSeeds(input, targetDir, dbUrl) {
7978
8607
  async function generateTablesManifestSafely(targetDir, dbUrl) {
7979
8608
  try {
7980
8609
  await generateTablesManifest(targetDir, { databaseUrl: dbUrl });
8610
+ return void 0;
7981
8611
  } catch (error) {
7982
- logger15.warn(`Failed to generate tables manifest: ${error}`);
8612
+ return `Failed to generate tables manifest: ${error}`;
7983
8613
  }
7984
8614
  }
7985
8615
  var applySeeds = fromPromise(async ({ input: { input, targetDir } }) => {
7986
8616
  const dbUrl = getDbUrl(input);
7987
8617
  const seedsApplied = runSeeds(input, targetDir, dbUrl);
7988
- await generateTablesManifestSafely(targetDir, dbUrl);
7989
- return { applied: seedsApplied };
8618
+ const manifestWarning = await generateTablesManifestSafely(targetDir, dbUrl);
8619
+ return { applied: seedsApplied, warnings: manifestWarning ? [manifestWarning] : [] };
7990
8620
  });
7991
8621
 
7992
8622
  // src/commands/db/apply/machine.ts
@@ -8043,7 +8673,7 @@ var e2eMeta = {
8043
8673
  "expect(log).toContain('pg-schema-diff')",
8044
8674
  "expect(ctx.schemaChangesApplied).toBeDefined()"
8045
8675
  ],
8046
- nextStates: ["applyingIdempotentPost", "done", "failed"]
8676
+ nextStates: ["applyingIdempotentPost", "validatingPartitions", "done", "failed"]
8047
8677
  },
8048
8678
  applyingIdempotentPost: {
8049
8679
  description: "Apply idempotent schemas (2nd pass: dependent tables, RLS)",
@@ -8134,6 +8764,9 @@ function buildDbApplyMetrics(context, endTime) {
8134
8764
  retryAttempts: toOptionalPositive(context.retryAttempts)
8135
8765
  };
8136
8766
  }
8767
+ function createMachineWarning(code, message, phase) {
8768
+ return { code, message, phase };
8769
+ }
8137
8770
  function createDbApplyOutput(context, endTime) {
8138
8771
  const metrics = buildDbApplyMetrics(context, endTime);
8139
8772
  const totalIdempotentApplied = context.idempotentPreApplied + context.idempotentPostApplied;
@@ -8164,6 +8797,7 @@ function createDbApplyOutput(context, endTime) {
8164
8797
  dataViolations: toOptionalPositive(context.dataViolations),
8165
8798
  planSql: context.planSql ?? void 0,
8166
8799
  filteredPlanSql: context.filteredPlanSql ?? void 0,
8800
+ planSummary: context.planSummary ?? void 0,
8167
8801
  checkOnly: toOptionalTrue(context.input.check === true),
8168
8802
  partitionWarnings: toOptionalArray(context.partitionWarnings),
8169
8803
  idempotentFiles: toOptionalArray(context.idempotentFiles),
@@ -8176,7 +8810,7 @@ function createDbApplyOutput(context, endTime) {
8176
8810
  endedAt: new Date(endTime).toISOString(),
8177
8811
  durationMs: endTime - context.startTime,
8178
8812
  phases,
8179
- warnings: [],
8813
+ warnings: context.nonCriticalWarnings,
8180
8814
  errors: phases.map((phase) => phase.error).filter((value) => value != null),
8181
8815
  summary: buildCommandOutcomeSummary(phases)
8182
8816
  }
@@ -8185,6 +8819,23 @@ function createDbApplyOutput(context, endTime) {
8185
8819
  var dbApplyMachine = setup({
8186
8820
  types: {},
8187
8821
  actions: {
8822
+ assignPgSchemaDiffResult: assign(({ event }) => {
8823
+ if (!("output" in event)) {
8824
+ return {};
8825
+ }
8826
+ const output = event.output;
8827
+ return {
8828
+ schemaChangesApplied: output.applied,
8829
+ dataViolations: output.dataViolations ?? 0,
8830
+ hazards: output.hazards,
8831
+ planSql: output.sql ?? null,
8832
+ filteredPlanSql: output.filteredPlanSql ?? null,
8833
+ planSummary: output.planSummary ?? null,
8834
+ ssotWarning: output.ssotWarning ?? null,
8835
+ pgSchemaDiffEndTime: Date.now(),
8836
+ retryAttempts: output.retryAttempts ?? 0
8837
+ };
8838
+ }),
8188
8839
  releaseAdvisoryLockOnFailure: ({ context }) => {
8189
8840
  if (context.lockAcquired) {
8190
8841
  try {
@@ -8226,7 +8877,9 @@ var dbApplyMachine = setup({
8226
8877
  error: null,
8227
8878
  planSql: null,
8228
8879
  filteredPlanSql: null,
8880
+ planSummary: null,
8229
8881
  ssotWarning: null,
8882
+ nonCriticalWarnings: [],
8230
8883
  // Idempotent preview (check mode)
8231
8884
  idempotentFiles: [],
8232
8885
  idempotentRisks: null,
@@ -8335,30 +8988,18 @@ var dbApplyMachine = setup({
8335
8988
  {
8336
8989
  guard: ({ context }) => context.input.check === true,
8337
8990
  target: "done",
8338
- actions: assign({
8339
- schemaChangesApplied: ({ event }) => event.output.applied,
8340
- dataViolations: ({ event }) => event.output.dataViolations ?? 0,
8341
- hazards: ({ event }) => event.output.hazards,
8342
- planSql: ({ event }) => event.output.sql ?? null,
8343
- filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
8344
- ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
8345
- pgSchemaDiffEndTime: () => Date.now(),
8346
- retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
8347
- })
8991
+ actions: "assignPgSchemaDiffResult"
8348
8992
  },
8349
- // Normal mode: continue to 2nd pass idempotent
8993
+ // Normal mode: skip 2nd pass when no files were deferred in 1st pass
8994
+ {
8995
+ guard: ({ context }) => context.idempotentPreSkipped === 0,
8996
+ target: "validatingPartitions",
8997
+ actions: "assignPgSchemaDiffResult"
8998
+ },
8999
+ // Normal mode: run 2nd pass when deferred files exist
8350
9000
  {
8351
9001
  target: "applyingIdempotentPost",
8352
- actions: assign({
8353
- schemaChangesApplied: ({ event }) => event.output.applied,
8354
- dataViolations: ({ event }) => event.output.dataViolations ?? 0,
8355
- hazards: ({ event }) => event.output.hazards,
8356
- planSql: ({ event }) => event.output.sql ?? null,
8357
- filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
8358
- ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
8359
- pgSchemaDiffEndTime: () => Date.now(),
8360
- retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
8361
- })
9002
+ actions: "assignPgSchemaDiffResult"
8362
9003
  }
8363
9004
  ],
8364
9005
  onError: {
@@ -8446,6 +9087,12 @@ var dbApplyMachine = setup({
8446
9087
  target: "done",
8447
9088
  actions: assign({
8448
9089
  seedsApplied: ({ event }) => event.output.applied,
9090
+ nonCriticalWarnings: ({ context, event }) => [
9091
+ ...context.nonCriticalWarnings,
9092
+ ...event.output.warnings.map(
9093
+ (warning) => createMachineWarning("TABLES_MANIFEST_WARNING", warning, "applyingSeeds")
9094
+ )
9095
+ ],
8449
9096
  seedEndTime: () => Date.now()
8450
9097
  })
8451
9098
  },
@@ -8649,7 +9296,7 @@ function createWarning(code, message, phase) {
8649
9296
  return { code, message, phase };
8650
9297
  }
8651
9298
  function buildDbApplyOutcome(params) {
8652
- const warnings = [];
9299
+ const warnings = [...params.result.outcome.warnings];
8653
9300
  const phases = [...params.phases];
8654
9301
  if (params.result.partitionWarnings && params.result.partitionWarnings.length > 0) {
8655
9302
  const warningList = params.result.partitionWarnings.map(
@@ -8675,9 +9322,20 @@ function buildDbApplyOutcome(params) {
8675
9322
  );
8676
9323
  warnings.push(seedWarning);
8677
9324
  seedPhase.status = "warning";
8678
- seedPhase.warningCount = 1;
8679
- seedPhase.warnings = [seedWarning];
9325
+ seedPhase.warningCount = (seedPhase.warningCount ?? 0) + 1;
9326
+ seedPhase.warnings = [...seedPhase.warnings ?? [], seedWarning];
9327
+ }
9328
+ }
9329
+ for (const warning of params.result.outcome.warnings) {
9330
+ const target = phases.find((phase) => phase.id === warning.phase);
9331
+ if (!target) {
9332
+ continue;
8680
9333
  }
9334
+ if (target.status === "passed") {
9335
+ target.status = "warning";
9336
+ }
9337
+ target.warningCount = (target.warningCount ?? 0) + 1;
9338
+ target.warnings = [...target.warnings ?? [], warning];
8681
9339
  }
8682
9340
  const errors = phases.map((phase) => phase.error).filter((value) => value != null);
8683
9341
  return {
@@ -8732,6 +9390,7 @@ function describeSeedStatus(env, _noSeed, seedFlag) {
8732
9390
  async function runDbApply(env, options) {
8733
9391
  const logger17 = createCLILogger("db:apply");
8734
9392
  const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
9393
+ loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
8735
9394
  const noSeed = resolveNoSeed(resolvedEnv, options.seed);
8736
9395
  const compareOnly = options.check === true && options.compareOnly === true;
8737
9396
  if (resolvedEnv === "production" && !noSeed) {
@@ -8836,6 +9495,11 @@ function printCheckSummary(logger17, result) {
8836
9495
  logger17.info(` Result: ${result.outcome.exitMode}`);
8837
9496
  logger17.info(` \u2713 Idempotent schemas: ${result.idempotentSchemasApplied} files`);
8838
9497
  logger17.info(` \u2713 Schema changes: ${result.planSql ? "detected" : "none"}`);
9498
+ if (result.planSummary) {
9499
+ logger17.info(
9500
+ ` \u2713 Plan summary: ${result.planSummary.rawStatements} raw / ${result.planSummary.effectiveStatements} structural / ${result.planSummary.noiseStatements} collapsed noise`
9501
+ );
9502
+ }
8839
9503
  if (result.hazards.length > 0) logger17.info(` \u26A0 Hazards: ${result.hazards.join(", ")}`);
8840
9504
  logger17.info(" \u2139 No changes were applied (check mode)");
8841
9505
  }
@@ -8862,6 +9526,12 @@ function printApplySummary(logger17, result) {
8862
9526
  logger17.info(` \u26A0 Warnings: ${result.outcome.summary.warnings}`);
8863
9527
  }
8864
9528
  if (result.metrics) logger17.info(buildMetricsString(result.metrics));
9529
+ if (result.outcome.warnings.length > 0) {
9530
+ logger17.warn("\nNon-critical warnings:");
9531
+ for (const warning of result.outcome.warnings) {
9532
+ logger17.info(` \u2022 [${warning.phase}] ${warning.message}`);
9533
+ }
9534
+ }
8865
9535
  }
8866
9536
  function printSummary(logger17, result) {
8867
9537
  if (result.error) {
@@ -8894,6 +9564,7 @@ var applyCommand = new Command("apply").description("Apply schema changes to any
8894
9564
  ).option("--fresh-db-check-sql <sql>", "Custom SQL to check if DB is fresh").action(async (env, options) => {
8895
9565
  const logger17 = createCLILogger("db:apply");
8896
9566
  const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
9567
+ loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
8897
9568
  try {
8898
9569
  const noSeed = resolveNoSeed(env, options.seed);
8899
9570
  const compareOnly = options.check === true && options.compareOnly === true;
@@ -10845,6 +11516,32 @@ async function runDeclarativeDependencyCheck(result, logger17, step, strictOptio
10845
11516
  }
10846
11517
  }
10847
11518
 
11519
+ // src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
11520
+ init_esm_shims();
11521
+ async function runDuplicateFunctionOwnershipCheck(result, logger17, step) {
11522
+ logger17.step("Checking duplicate function ownership", step.next());
11523
+ const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
11524
+ if (analysis.findings.length === 0) {
11525
+ logger17.success("No duplicate declarative/idempotent function ownership detected");
11526
+ return;
11527
+ }
11528
+ result.passed = false;
11529
+ result.errors.push(`Found ${analysis.findings.length} duplicate function ownership finding(s)`);
11530
+ logger17.error(`Found ${analysis.findings.length} duplicate function ownership finding(s):`);
11531
+ logger17.info(` ${analysis.contractNote}`);
11532
+ for (const finding of analysis.findings) {
11533
+ const formatted = formatDuplicateFunctionOwnershipFinding(finding);
11534
+ logger17.info(` [ERROR] ${formatted.summary}`);
11535
+ for (const location of formatted.declarativeLocations) {
11536
+ logger17.info(` declarative: ${location}`);
11537
+ }
11538
+ for (const location of formatted.idempotentLocations) {
11539
+ logger17.info(` idempotent: ${location}`);
11540
+ }
11541
+ logger17.info(` ${formatted.suggestion}`);
11542
+ }
11543
+ }
11544
+
10848
11545
  // src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
10849
11546
  init_esm_shims();
10850
11547
 
@@ -11996,7 +12693,9 @@ var IDEMPOTENT_DOWNGRADE_REASON_CODES = /* @__PURE__ */ new Set([
11996
12693
  "MEDIUM_RISK_DROP_SEQUENCE",
11997
12694
  "MEDIUM_RISK_DROP_INDEX",
11998
12695
  "MEDIUM_RISK_REVOKE",
11999
- "PLPGSQL_DO_BLOCK_DETECTED"
12696
+ "PLPGSQL_DO_BLOCK_DETECTED",
12697
+ "PLPGSQL_EXECUTE_BOUNDED_DISPATCH",
12698
+ "GUARDED_DDL_ADD_CONSTRAINT"
12000
12699
  ]);
12001
12700
  var IDEMPOTENT_MEDIUM_REASON_CODES = /* @__PURE__ */ new Set([
12002
12701
  "PLPGSQL_EXECUTE_UNRESOLVED",
@@ -12017,7 +12716,7 @@ init_esm_shims();
12017
12716
  var riskDetectorLoader = null;
12018
12717
  function loadRiskDetectorModule() {
12019
12718
  if (!riskDetectorLoader) {
12020
- riskDetectorLoader = import('./risk-detector-VO5HJR4R.js').then((module) => ({
12719
+ riskDetectorLoader = import('./risk-detector-S7XQF4I2.js').then((module) => ({
12021
12720
  detectSchemaRisks: module.detectSchemaRisks
12022
12721
  })).catch((error) => {
12023
12722
  riskDetectorLoader = null;
@@ -12130,6 +12829,7 @@ function formatCollectedIdempotentRisks(collected, allowlist) {
12130
12829
  }
12131
12830
  const high = collected.filter((r) => r.level === "high");
12132
12831
  const medium = collected.filter((r) => r.level === "medium");
12832
+ const low = collected.filter((r) => r.level === "low");
12133
12833
  const blockers = [];
12134
12834
  const warnings = [];
12135
12835
  for (const risk of high) {
@@ -12138,6 +12838,9 @@ function formatCollectedIdempotentRisks(collected, allowlist) {
12138
12838
  for (const risk of medium) {
12139
12839
  warnings.push(` [MEDIUM] ${formatDeclarativeRiskMessage(risk)}`);
12140
12840
  }
12841
+ for (const risk of low) {
12842
+ warnings.push(` [LOW] ${formatDeclarativeRiskMessage(risk)}`);
12843
+ }
12141
12844
  return {
12142
12845
  blockers: dedupeAndSort(blockers),
12143
12846
  warnings: dedupeAndSort(warnings),
@@ -12198,6 +12901,45 @@ async function processDeclarativeRiskFile(params) {
12198
12901
  });
12199
12902
  return { kind: "ok", detector: detectorResult.detector };
12200
12903
  }
12904
+ var CREATE_POLICY_NAME_PATTERN = /\bCREATE\s+(?:OR\s+REPLACE\s+)?POLICY\s+(?:"([^"]+)"|([A-Za-z_]\w*))/gi;
12905
+ var DROP_POLICY_NAME_LINE_PATTERN = /\bDROP\s+POLICY\s+IF\s+EXISTS\s+(?:"([^"]+)"|([A-Za-z_]\w*))/i;
12906
+ function extractCreatePolicyNames(content) {
12907
+ const names = /* @__PURE__ */ new Set();
12908
+ CREATE_POLICY_NAME_PATTERN.lastIndex = 0;
12909
+ let match = CREATE_POLICY_NAME_PATTERN.exec(content);
12910
+ while (match !== null) {
12911
+ const name = (match[1] ?? match[2] ?? "").toLowerCase();
12912
+ if (name) names.add(name);
12913
+ match = CREATE_POLICY_NAME_PATTERN.exec(content);
12914
+ }
12915
+ CREATE_POLICY_NAME_PATTERN.lastIndex = 0;
12916
+ return names;
12917
+ }
12918
+ function detectRecreatePairs(risks, absoluteFilePath) {
12919
+ const dropPolicyRisks = risks.filter((r) => r.reasonCode === "HIGH_RISK_DROP_POLICY");
12920
+ if (dropPolicyRisks.length === 0) return;
12921
+ let content;
12922
+ try {
12923
+ content = readFileSync(absoluteFilePath, "utf-8");
12924
+ } catch {
12925
+ return;
12926
+ }
12927
+ const createPolicyNames = extractCreatePolicyNames(content);
12928
+ if (createPolicyNames.size === 0) return;
12929
+ const lines = content.split("\n");
12930
+ for (const risk of dropPolicyRisks) {
12931
+ if (risk.line === void 0) continue;
12932
+ const startIdx = Math.max(0, risk.line - 2);
12933
+ const endIdx = Math.min(lines.length, risk.line + 1);
12934
+ const windowText = lines.slice(startIdx, endIdx).join(" ");
12935
+ const nameMatch = DROP_POLICY_NAME_LINE_PATTERN.exec(windowText);
12936
+ if (!nameMatch) continue;
12937
+ const dropName = (nameMatch[1] ?? nameMatch[2] ?? "").toLowerCase();
12938
+ if (dropName && createPolicyNames.has(dropName)) {
12939
+ risk.level = "low";
12940
+ }
12941
+ }
12942
+ }
12201
12943
  async function collectDeclarativeRiskReport() {
12202
12944
  const declarativeDir = path15.join(process.cwd(), "supabase", "schemas", "declarative");
12203
12945
  if (!existsSync(declarativeDir)) {
@@ -12266,9 +13008,13 @@ async function collectIdempotentRiskReport() {
12266
13008
  const risks = await detectSchemaRisks2(file);
12267
13009
  if (risks.length === 0) continue;
12268
13010
  const relPath = path15.relative(process.cwd(), file);
12269
- for (const risk of risks) {
12270
- const level = correctIdempotentRiskLevel(risk.level, risk.reasonCode);
12271
- const scopedRisk = { ...risk, level, file: relPath };
13011
+ const fileRisks = risks.map((risk) => ({
13012
+ ...risk,
13013
+ level: correctIdempotentRiskLevel(risk.level, risk.reasonCode),
13014
+ file: relPath
13015
+ }));
13016
+ detectRecreatePairs(fileRisks, file);
13017
+ for (const scopedRisk of fileRisks) {
12272
13018
  const matched = findDeclarativeRiskAllowlistMatch(scopedRisk, policy);
12273
13019
  if (matched) {
12274
13020
  if (SHOW_ALLOWLIST_REPORT2) {
@@ -12487,6 +13233,7 @@ async function runPreflightChecks(env, strict = false) {
12487
13233
  await runSqlSchemaRiskCheck(result, logger17, step);
12488
13234
  await runSchemaBoundaryPlacementCheck(result, logger17, step, strict);
12489
13235
  await runIdempotentSchemaRiskCheck(result, logger17, step, strict);
13236
+ await runDuplicateFunctionOwnershipCheck(result, logger17, step);
12490
13237
  await runDeclarativeDependencyCheck(result, logger17, step, strict);
12491
13238
  await runDomainNamingCheck(result, logger17, step);
12492
13239
  logSummary(result, logger17);
@@ -16511,9 +17258,20 @@ async function collectLocalPrecheckBundle(strict) {
16511
17258
  const adjustedIdempotentRisks = applyStrictModeToReport(idempotentRisks, strict);
16512
17259
  const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
16513
17260
  const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
16514
- const hasLocalBlockers = hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
16515
- const hasLocalFindings = hasReportFindings(adjustedDeclarativeRisks) || hasReportFindings(adjustedIdempotentRisks) || hasReportFindings(adjustedPlacementRisks) || hasReportFindings(adjustedExtensionRisks);
17261
+ const duplicateOwnershipAnalysis = analyzeDuplicateFunctionOwnership(process.cwd());
17262
+ const duplicateOwnershipBlockers = duplicateOwnershipAnalysis.findings.map((finding) => {
17263
+ const formatted = formatDuplicateFunctionOwnershipFinding(finding);
17264
+ return [
17265
+ formatted.summary,
17266
+ `declarative=${formatted.declarativeLocations.join(", ")}`,
17267
+ `idempotent=${formatted.idempotentLocations.join(", ")}`,
17268
+ formatted.suggestion
17269
+ ].join(" | ");
17270
+ });
17271
+ const hasLocalBlockers = duplicateOwnershipBlockers.length > 0 || hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
17272
+ const hasLocalFindings = duplicateOwnershipBlockers.length > 0 || hasReportFindings(adjustedDeclarativeRisks) || hasReportFindings(adjustedIdempotentRisks) || hasReportFindings(adjustedPlacementRisks) || hasReportFindings(adjustedExtensionRisks);
16516
17273
  const localBlockers = [
17274
+ ...duplicateOwnershipBlockers,
16517
17275
  ...adjustedPlacementRisks.blockers,
16518
17276
  ...adjustedDeclarativeRisks.blockers,
16519
17277
  ...adjustedIdempotentRisks.blockers,
@@ -16528,6 +17286,7 @@ async function collectLocalPrecheckBundle(strict) {
16528
17286
  adjustedIdempotentRisks,
16529
17287
  adjustedPlacementRisks,
16530
17288
  adjustedExtensionRisks,
17289
+ duplicateOwnershipBlockers,
16531
17290
  hasLocalBlockers,
16532
17291
  hasLocalFindings,
16533
17292
  localBlockers
@@ -16542,6 +17301,12 @@ ${heading}`);
16542
17301
  }
16543
17302
  }
16544
17303
  function printLocalFindingCompactSummary(logger17, local, topLimit) {
17304
+ printCompactSummary(
17305
+ logger17,
17306
+ "Duplicate function ownership summary",
17307
+ local.duplicateOwnershipBlockers,
17308
+ topLimit
17309
+ );
16545
17310
  printCompactSummary(
16546
17311
  logger17,
16547
17312
  "Declarative risk summary (declarative/*.sql)",
@@ -16568,6 +17333,11 @@ function printLocalFindingCompactSummary(logger17, local, topLimit) {
16568
17333
  );
16569
17334
  }
16570
17335
  function printLocalFindingDetailedReport(logger17, local) {
17336
+ logFindingSection(
17337
+ logger17,
17338
+ "Duplicate function ownership blockers:",
17339
+ local.duplicateOwnershipBlockers
17340
+ );
16571
17341
  logFindingSection(
16572
17342
  logger17,
16573
17343
  "Risk checks on supabase/schemas/declarative/*.sql:",
@@ -16723,6 +17493,28 @@ function assertNoProductionApplyRiskReasons(result) {
16723
17493
  ]
16724
17494
  );
16725
17495
  }
17496
+ function assertProductionPlanSizeBudget(logger17, result) {
17497
+ const assessment = assessPlanSize(result.planSummary);
17498
+ if (!result.planSummary || assessment.severity === "ok") {
17499
+ return;
17500
+ }
17501
+ if (assessment.severity === "warning") {
17502
+ logger17.warn(`Large production apply preview: ${formatPlanSizeSummary(result.planSummary)}`);
17503
+ for (const reason of assessment.reasons) {
17504
+ logger17.info(` ${reason}`);
17505
+ }
17506
+ return;
17507
+ }
17508
+ throw new CLIError(
17509
+ "Production apply preview exceeds plan size budget",
17510
+ "DB_PRODUCTION_APPLY_PLAN_SIZE",
17511
+ [
17512
+ `Plan summary: ${formatPlanSizeSummary(result.planSummary)}`,
17513
+ ...assessment.reasons,
17514
+ "Reduce metadata churn or split the schema change before production apply"
17515
+ ]
17516
+ );
17517
+ }
16726
17518
  function collectProductionApplyRiskReasons(output) {
16727
17519
  const reasons = [];
16728
17520
  if (output.dataViolations && output.dataViolations > 0) {
@@ -16779,6 +17571,7 @@ async function runProductionApplyDryRunCheck(logger17, strict) {
16779
17571
  placement: local.placementRisks.allowlist.length,
16780
17572
  plan: plan.planBoundaryRisks.allowlist.length
16781
17573
  });
17574
+ assertProductionPlanSizeBudget(logger17, plan.result);
16782
17575
  assertNoPlanBoundaryBlockers(logger17, plan.adjustedPlanBoundaryRisks, strict);
16783
17576
  assertNoProductionApplyRiskReasons(plan.result);
16784
17577
  }