@runa-ai/runa-cli 0.7.3 → 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 (31) hide show
  1. package/dist/chunk-6E2DRXIL.js +452 -0
  2. package/dist/{chunk-AO554K3G.js → chunk-GHQH6UC5.js} +1 -1
  3. package/dist/{chunk-FWMGC5FP.js → chunk-RB2ZUS76.js} +249 -12
  4. package/dist/{chunk-CKRLVEIO.js → chunk-ZYT7OQJB.js} +16 -11
  5. package/dist/{ci-Z4525QW6.js → ci-ZK3LKYFX.js} +305 -429
  6. package/dist/{cli-SVXOSMW6.js → cli-ZY5VRIJA.js} +8 -8
  7. package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -2
  8. package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +1 -1
  9. package/dist/commands/ci/machine/contract.d.ts +6 -1
  10. package/dist/commands/ci/machine/guards.d.ts +16 -0
  11. package/dist/commands/ci/machine/machine.d.ts +11 -3
  12. package/dist/commands/db/apply/actors/seed-actors.d.ts +1 -0
  13. package/dist/commands/db/apply/contract.d.ts +23 -0
  14. package/dist/commands/db/apply/helpers/fresh-db-handler.d.ts +2 -1
  15. package/dist/commands/db/apply/helpers/hazard-handler.d.ts +19 -8
  16. package/dist/commands/db/apply/helpers/index.d.ts +2 -1
  17. package/dist/commands/db/apply/helpers/no-change-plan.d.ts +2 -0
  18. package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +11 -0
  19. package/dist/commands/db/apply/machine.d.ts +52 -1
  20. package/dist/commands/db/utils/duplicate-function-ownership.d.ts +35 -0
  21. package/dist/commands/db/utils/plan-size-guard.d.ts +16 -0
  22. package/dist/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.d.ts +4 -0
  23. package/dist/{db-S4V4ETDR.js → db-EPI2DQYN.js} +1025 -306
  24. package/dist/{dev-MLRKIP7F.js → dev-GB5ERUVR.js} +1 -1
  25. package/dist/{env-WNHJVLOT.js → env-WP74UUMO.js} +1 -1
  26. package/dist/{hotfix-Z5EGVSMH.js → hotfix-TOSGTVCW.js} +1 -1
  27. package/dist/index.js +3 -3
  28. package/dist/{vuln-check-D575VXIQ.js → vuln-check-G6I4YYDC.js} +1 -1
  29. package/dist/{vuln-checker-QV6XODTJ.js → vuln-checker-CT2AYPIS.js} +1 -1
  30. package/package.json +3 -3
  31. package/dist/chunk-4XHZQRRK.js +0 -215
@@ -4,9 +4,9 @@ import { detectDatabaseStack, getStackPaths } from './chunk-CCKG5R4Y.js';
4
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)");
@@ -6045,6 +5922,685 @@ var previewIdempotentSchemas = fromPromise(async ({ input: { input, targetDir }
6045
5922
 
6046
5923
  // src/commands/db/apply/actors/pg-schema-diff-actors.ts
6047
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;
6413
+ }
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(", "))})`;
6422
+ }
6423
+ function extractFunctionOwnershipDefinition(statement, file, line, layer) {
6424
+ const regex = createFunctionDefinitionRegex();
6425
+ const match = regex.exec(statement);
6426
+ if (!match) {
6427
+ return null;
6428
+ }
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
+ };
6441
+ }
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);
6454
+ }
6455
+ }
6456
+ }
6457
+ return definitions;
6458
+ }
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);
6465
+ }
6466
+ return grouped;
6467
+ }
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;
6501
+ }
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
+ }
6523
+ }
6524
+ const hasAmbiguousMatch = declarativeDefinition.signature === null || idempotentMatches.some((candidate) => candidate.signature === null);
6525
+ if (!hasAmbiguousMatch) {
6526
+ continue;
6527
+ }
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
+ }
6540
+ }
6541
+ return {
6542
+ contractNote: DUPLICATE_FUNCTION_OWNERSHIP_NOTE,
6543
+ findings,
6544
+ definitions: {
6545
+ declarative: declarativeDefinitions,
6546
+ idempotent: idempotentDefinitions
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
+ );
6579
+ }
6580
+ if (summary.rawStatements >= threshold.rawStatements) {
6581
+ reasons.push(`raw statements ${summary.rawStatements} >= ${threshold.rawStatements}`);
6582
+ }
6583
+ return reasons;
6584
+ }
6585
+ function assessPlanSize(summary) {
6586
+ if (!summary) {
6587
+ return { severity: "ok", reasons: [] };
6588
+ }
6589
+ const blockerReasons = buildThresholdReasons(summary, PLAN_SIZE_BLOCKER_THRESHOLD);
6590
+ if (blockerReasons.length > 0) {
6591
+ return { severity: "blocker", reasons: blockerReasons };
6592
+ }
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
+ }
6602
+
6603
+ // src/commands/db/apply/actors/pg-schema-diff-actors.ts
6048
6604
  init_local_supabase();
6049
6605
 
6050
6606
  // src/commands/db/utils/declarative-dependency-warning-governance.ts
@@ -7511,6 +8067,25 @@ function assertDeclarativeDependencyBoundary(targetDir, input) {
7511
8067
  throw new Error(buildDeclarativeDependencyWarningFailureLines(warningReview).join("\n"));
7512
8068
  }
7513
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
+ }
7514
8089
  function createCombinedSchemaBundle(schemaFiles, verbose) {
7515
8090
  const tmpDir = mkdtempSync(join(tmpdir(), "runa_pg_schema_diff_"));
7516
8091
  const combinedSchemaPath = join(tmpDir, "desired_schema.sql");
@@ -7541,7 +8116,7 @@ async function createShadowDbForRun(dbUrl, shadowExtensions, verbose) {
7541
8116
  return shadowDb;
7542
8117
  }
7543
8118
  function buildNoChangesResult(planOutput) {
7544
- if (!planOutput.trim() || planOutput.includes("No changes")) {
8119
+ if (isNoChangePlanOutput(planOutput)) {
7545
8120
  logger15.success("No schema changes detected");
7546
8121
  return { sql: "", hazards: [], applied: true };
7547
8122
  }
@@ -7596,20 +8171,43 @@ function runPreApplyDataCompatibility(dbUrl, planOutput, input) {
7596
8171
  }
7597
8172
  return 0;
7598
8173
  }
7599
- function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir) {
8174
+ function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir, options) {
7600
8175
  if (!input.check) {
7601
8176
  return null;
7602
8177
  }
7603
8178
  const plan = parsePlanOutput(planOutput);
7604
- const { filteredPlan, removedDropStatements, removedAuthzStatements } = filterCheckModePlanStatements(plan, protectedTables, protectedObjects, schemasDir);
7605
- displayCheckModeResults(planOutput, {
7606
- 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,
7607
8183
  removedDropStatements,
7608
- 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
+ }
7609
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
+ }
7610
8207
  return {
7611
8208
  sql: planOutput,
7612
8209
  filteredPlanSql: filteredPlan.rawSql,
8210
+ planSummary,
7613
8211
  hazards,
7614
8212
  applied: false,
7615
8213
  dataViolations: dataViolationCount > 0 ? dataViolationCount : void 0
@@ -7658,6 +8256,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7658
8256
  logger15.info("No declarative schemas found");
7659
8257
  return { sql: "", hazards: [], applied: false };
7660
8258
  }
8259
+ warnDuplicateFunctionOwnership(targetDir);
7661
8260
  assertDeclarativeDependencyBoundary(targetDir, input);
7662
8261
  const dbUrl = getDbUrl(input);
7663
8262
  const configState = loadPgSchemaDiffConfigState(targetDir, input.verbose);
@@ -7739,13 +8338,14 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7739
8338
  }
7740
8339
  )
7741
8340
  );
8341
+ const rawPlanStatementCount = parsePlanOutput(planOutput).statements.length;
7742
8342
  let effectivePlanOutput = planOutput;
8343
+ let suppressedPlan = {
8344
+ planOutput,
8345
+ suppressedStatements: [],
8346
+ warnings: []
8347
+ };
7743
8348
  if (!input.compareOnly) {
7744
- let suppressedPlan = {
7745
- planOutput,
7746
- suppressedStatements: [],
7747
- warnings: []
7748
- };
7749
8349
  try {
7750
8350
  suppressedPlan = await withProgress(
7751
8351
  "reviewing plan false positives",
@@ -7765,7 +8365,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7765
8365
  }
7766
8366
  effectivePlanOutput = suppressedPlan.planOutput;
7767
8367
  }
7768
- const noChangesResult = buildNoChangesResult(effectivePlanOutput);
8368
+ const noChangesResult = rawPlanStatementCount === 0 || isNoChangePlanOutput(effectivePlanOutput) ? buildNoChangesResult(effectivePlanOutput) : null;
7769
8369
  if (noChangesResult) return noChangesResult;
7770
8370
  const { hazards } = await withProgress(
7771
8371
  "extracting migration hazards",
@@ -7792,7 +8392,11 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
7792
8392
  protectedTables,
7793
8393
  protectedObjects,
7794
8394
  dataViolationCount,
7795
- schemasDir
8395
+ schemasDir,
8396
+ {
8397
+ rawStatementCount: rawPlanStatementCount,
8398
+ suppressedFunctionStatements: !input.compareOnly ? suppressedPlan.suppressedStatements : void 0
8399
+ }
7796
8400
  );
7797
8401
  if (checkModeResult) return checkModeResult;
7798
8402
  backupProtectedTablesForProduction(dbUrl, protectedTables, input);
@@ -8003,15 +8607,16 @@ function runSeeds(input, targetDir, dbUrl) {
8003
8607
  async function generateTablesManifestSafely(targetDir, dbUrl) {
8004
8608
  try {
8005
8609
  await generateTablesManifest(targetDir, { databaseUrl: dbUrl });
8610
+ return void 0;
8006
8611
  } catch (error) {
8007
- logger15.warn(`Failed to generate tables manifest: ${error}`);
8612
+ return `Failed to generate tables manifest: ${error}`;
8008
8613
  }
8009
8614
  }
8010
8615
  var applySeeds = fromPromise(async ({ input: { input, targetDir } }) => {
8011
8616
  const dbUrl = getDbUrl(input);
8012
8617
  const seedsApplied = runSeeds(input, targetDir, dbUrl);
8013
- await generateTablesManifestSafely(targetDir, dbUrl);
8014
- return { applied: seedsApplied };
8618
+ const manifestWarning = await generateTablesManifestSafely(targetDir, dbUrl);
8619
+ return { applied: seedsApplied, warnings: manifestWarning ? [manifestWarning] : [] };
8015
8620
  });
8016
8621
 
8017
8622
  // src/commands/db/apply/machine.ts
@@ -8068,7 +8673,7 @@ var e2eMeta = {
8068
8673
  "expect(log).toContain('pg-schema-diff')",
8069
8674
  "expect(ctx.schemaChangesApplied).toBeDefined()"
8070
8675
  ],
8071
- nextStates: ["applyingIdempotentPost", "done", "failed"]
8676
+ nextStates: ["applyingIdempotentPost", "validatingPartitions", "done", "failed"]
8072
8677
  },
8073
8678
  applyingIdempotentPost: {
8074
8679
  description: "Apply idempotent schemas (2nd pass: dependent tables, RLS)",
@@ -8159,6 +8764,9 @@ function buildDbApplyMetrics(context, endTime) {
8159
8764
  retryAttempts: toOptionalPositive(context.retryAttempts)
8160
8765
  };
8161
8766
  }
8767
+ function createMachineWarning(code, message, phase) {
8768
+ return { code, message, phase };
8769
+ }
8162
8770
  function createDbApplyOutput(context, endTime) {
8163
8771
  const metrics = buildDbApplyMetrics(context, endTime);
8164
8772
  const totalIdempotentApplied = context.idempotentPreApplied + context.idempotentPostApplied;
@@ -8189,6 +8797,7 @@ function createDbApplyOutput(context, endTime) {
8189
8797
  dataViolations: toOptionalPositive(context.dataViolations),
8190
8798
  planSql: context.planSql ?? void 0,
8191
8799
  filteredPlanSql: context.filteredPlanSql ?? void 0,
8800
+ planSummary: context.planSummary ?? void 0,
8192
8801
  checkOnly: toOptionalTrue(context.input.check === true),
8193
8802
  partitionWarnings: toOptionalArray(context.partitionWarnings),
8194
8803
  idempotentFiles: toOptionalArray(context.idempotentFiles),
@@ -8201,7 +8810,7 @@ function createDbApplyOutput(context, endTime) {
8201
8810
  endedAt: new Date(endTime).toISOString(),
8202
8811
  durationMs: endTime - context.startTime,
8203
8812
  phases,
8204
- warnings: [],
8813
+ warnings: context.nonCriticalWarnings,
8205
8814
  errors: phases.map((phase) => phase.error).filter((value) => value != null),
8206
8815
  summary: buildCommandOutcomeSummary(phases)
8207
8816
  }
@@ -8210,6 +8819,23 @@ function createDbApplyOutput(context, endTime) {
8210
8819
  var dbApplyMachine = setup({
8211
8820
  types: {},
8212
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
+ }),
8213
8839
  releaseAdvisoryLockOnFailure: ({ context }) => {
8214
8840
  if (context.lockAcquired) {
8215
8841
  try {
@@ -8251,7 +8877,9 @@ var dbApplyMachine = setup({
8251
8877
  error: null,
8252
8878
  planSql: null,
8253
8879
  filteredPlanSql: null,
8880
+ planSummary: null,
8254
8881
  ssotWarning: null,
8882
+ nonCriticalWarnings: [],
8255
8883
  // Idempotent preview (check mode)
8256
8884
  idempotentFiles: [],
8257
8885
  idempotentRisks: null,
@@ -8360,30 +8988,18 @@ var dbApplyMachine = setup({
8360
8988
  {
8361
8989
  guard: ({ context }) => context.input.check === true,
8362
8990
  target: "done",
8363
- actions: assign({
8364
- schemaChangesApplied: ({ event }) => event.output.applied,
8365
- dataViolations: ({ event }) => event.output.dataViolations ?? 0,
8366
- hazards: ({ event }) => event.output.hazards,
8367
- planSql: ({ event }) => event.output.sql ?? null,
8368
- filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
8369
- ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
8370
- pgSchemaDiffEndTime: () => Date.now(),
8371
- retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
8372
- })
8991
+ actions: "assignPgSchemaDiffResult"
8373
8992
  },
8374
- // 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
8375
9000
  {
8376
9001
  target: "applyingIdempotentPost",
8377
- actions: assign({
8378
- schemaChangesApplied: ({ event }) => event.output.applied,
8379
- dataViolations: ({ event }) => event.output.dataViolations ?? 0,
8380
- hazards: ({ event }) => event.output.hazards,
8381
- planSql: ({ event }) => event.output.sql ?? null,
8382
- filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
8383
- ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
8384
- pgSchemaDiffEndTime: () => Date.now(),
8385
- retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
8386
- })
9002
+ actions: "assignPgSchemaDiffResult"
8387
9003
  }
8388
9004
  ],
8389
9005
  onError: {
@@ -8471,6 +9087,12 @@ var dbApplyMachine = setup({
8471
9087
  target: "done",
8472
9088
  actions: assign({
8473
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
+ ],
8474
9096
  seedEndTime: () => Date.now()
8475
9097
  })
8476
9098
  },
@@ -8674,7 +9296,7 @@ function createWarning(code, message, phase) {
8674
9296
  return { code, message, phase };
8675
9297
  }
8676
9298
  function buildDbApplyOutcome(params) {
8677
- const warnings = [];
9299
+ const warnings = [...params.result.outcome.warnings];
8678
9300
  const phases = [...params.phases];
8679
9301
  if (params.result.partitionWarnings && params.result.partitionWarnings.length > 0) {
8680
9302
  const warningList = params.result.partitionWarnings.map(
@@ -8700,9 +9322,20 @@ function buildDbApplyOutcome(params) {
8700
9322
  );
8701
9323
  warnings.push(seedWarning);
8702
9324
  seedPhase.status = "warning";
8703
- seedPhase.warningCount = 1;
8704
- 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;
8705
9333
  }
9334
+ if (target.status === "passed") {
9335
+ target.status = "warning";
9336
+ }
9337
+ target.warningCount = (target.warningCount ?? 0) + 1;
9338
+ target.warnings = [...target.warnings ?? [], warning];
8706
9339
  }
8707
9340
  const errors = phases.map((phase) => phase.error).filter((value) => value != null);
8708
9341
  return {
@@ -8757,6 +9390,7 @@ function describeSeedStatus(env, _noSeed, seedFlag) {
8757
9390
  async function runDbApply(env, options) {
8758
9391
  const logger17 = createCLILogger("db:apply");
8759
9392
  const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
9393
+ loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
8760
9394
  const noSeed = resolveNoSeed(resolvedEnv, options.seed);
8761
9395
  const compareOnly = options.check === true && options.compareOnly === true;
8762
9396
  if (resolvedEnv === "production" && !noSeed) {
@@ -8861,6 +9495,11 @@ function printCheckSummary(logger17, result) {
8861
9495
  logger17.info(` Result: ${result.outcome.exitMode}`);
8862
9496
  logger17.info(` \u2713 Idempotent schemas: ${result.idempotentSchemasApplied} files`);
8863
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
+ }
8864
9503
  if (result.hazards.length > 0) logger17.info(` \u26A0 Hazards: ${result.hazards.join(", ")}`);
8865
9504
  logger17.info(" \u2139 No changes were applied (check mode)");
8866
9505
  }
@@ -8887,6 +9526,12 @@ function printApplySummary(logger17, result) {
8887
9526
  logger17.info(` \u26A0 Warnings: ${result.outcome.summary.warnings}`);
8888
9527
  }
8889
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
+ }
8890
9535
  }
8891
9536
  function printSummary(logger17, result) {
8892
9537
  if (result.error) {
@@ -8919,6 +9564,7 @@ var applyCommand = new Command("apply").description("Apply schema changes to any
8919
9564
  ).option("--fresh-db-check-sql <sql>", "Custom SQL to check if DB is fresh").action(async (env, options) => {
8920
9565
  const logger17 = createCLILogger("db:apply");
8921
9566
  const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
9567
+ loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
8922
9568
  try {
8923
9569
  const noSeed = resolveNoSeed(env, options.seed);
8924
9570
  const compareOnly = options.check === true && options.compareOnly === true;
@@ -10870,6 +11516,32 @@ async function runDeclarativeDependencyCheck(result, logger17, step, strictOptio
10870
11516
  }
10871
11517
  }
10872
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
+
10873
11545
  // src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
10874
11546
  init_esm_shims();
10875
11547
 
@@ -12561,6 +13233,7 @@ async function runPreflightChecks(env, strict = false) {
12561
13233
  await runSqlSchemaRiskCheck(result, logger17, step);
12562
13234
  await runSchemaBoundaryPlacementCheck(result, logger17, step, strict);
12563
13235
  await runIdempotentSchemaRiskCheck(result, logger17, step, strict);
13236
+ await runDuplicateFunctionOwnershipCheck(result, logger17, step);
12564
13237
  await runDeclarativeDependencyCheck(result, logger17, step, strict);
12565
13238
  await runDomainNamingCheck(result, logger17, step);
12566
13239
  logSummary(result, logger17);
@@ -16585,9 +17258,20 @@ async function collectLocalPrecheckBundle(strict) {
16585
17258
  const adjustedIdempotentRisks = applyStrictModeToReport(idempotentRisks, strict);
16586
17259
  const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
16587
17260
  const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
16588
- const hasLocalBlockers = hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
16589
- 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);
16590
17273
  const localBlockers = [
17274
+ ...duplicateOwnershipBlockers,
16591
17275
  ...adjustedPlacementRisks.blockers,
16592
17276
  ...adjustedDeclarativeRisks.blockers,
16593
17277
  ...adjustedIdempotentRisks.blockers,
@@ -16602,6 +17286,7 @@ async function collectLocalPrecheckBundle(strict) {
16602
17286
  adjustedIdempotentRisks,
16603
17287
  adjustedPlacementRisks,
16604
17288
  adjustedExtensionRisks,
17289
+ duplicateOwnershipBlockers,
16605
17290
  hasLocalBlockers,
16606
17291
  hasLocalFindings,
16607
17292
  localBlockers
@@ -16616,6 +17301,12 @@ ${heading}`);
16616
17301
  }
16617
17302
  }
16618
17303
  function printLocalFindingCompactSummary(logger17, local, topLimit) {
17304
+ printCompactSummary(
17305
+ logger17,
17306
+ "Duplicate function ownership summary",
17307
+ local.duplicateOwnershipBlockers,
17308
+ topLimit
17309
+ );
16619
17310
  printCompactSummary(
16620
17311
  logger17,
16621
17312
  "Declarative risk summary (declarative/*.sql)",
@@ -16642,6 +17333,11 @@ function printLocalFindingCompactSummary(logger17, local, topLimit) {
16642
17333
  );
16643
17334
  }
16644
17335
  function printLocalFindingDetailedReport(logger17, local) {
17336
+ logFindingSection(
17337
+ logger17,
17338
+ "Duplicate function ownership blockers:",
17339
+ local.duplicateOwnershipBlockers
17340
+ );
16645
17341
  logFindingSection(
16646
17342
  logger17,
16647
17343
  "Risk checks on supabase/schemas/declarative/*.sql:",
@@ -16797,6 +17493,28 @@ function assertNoProductionApplyRiskReasons(result) {
16797
17493
  ]
16798
17494
  );
16799
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
+ }
16800
17518
  function collectProductionApplyRiskReasons(output) {
16801
17519
  const reasons = [];
16802
17520
  if (output.dataViolations && output.dataViolations > 0) {
@@ -16853,6 +17571,7 @@ async function runProductionApplyDryRunCheck(logger17, strict) {
16853
17571
  placement: local.placementRisks.allowlist.length,
16854
17572
  plan: plan.planBoundaryRisks.allowlist.length
16855
17573
  });
17574
+ assertProductionPlanSizeBudget(logger17, plan.result);
16856
17575
  assertNoPlanBoundaryBlockers(logger17, plan.adjustedPlanBoundaryRisks, strict);
16857
17576
  assertNoProductionApplyRiskReasons(plan.result);
16858
17577
  }