@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.
- package/dist/{chunk-Z7A4BEWF.js → chunk-3JO6YP3T.js} +1 -1
- package/dist/chunk-6E2DRXIL.js +452 -0
- package/dist/{chunk-PMXE5XOJ.js → chunk-GHQH6UC5.js} +1 -1
- package/dist/{chunk-LCK2LGVR.js → chunk-PAWNJA3N.js} +1 -1
- package/dist/{chunk-FWMGC5FP.js → chunk-RB2ZUS76.js} +249 -12
- package/dist/{chunk-CKRLVEIO.js → chunk-ZYT7OQJB.js} +16 -11
- package/dist/{ci-Z4525QW6.js → ci-ZK3LKYFX.js} +305 -429
- package/dist/{cli-Q2XIQDRS.js → cli-ZY5VRIJA.js} +13 -13
- package/dist/commands/ci/commands/ci-resolvers.d.ts +1 -2
- package/dist/commands/ci/machine/actors/setup/pr-common.d.ts +1 -1
- package/dist/commands/ci/machine/contract.d.ts +6 -1
- package/dist/commands/ci/machine/guards.d.ts +16 -0
- package/dist/commands/ci/machine/machine.d.ts +11 -3
- package/dist/commands/db/apply/actors/seed-actors.d.ts +1 -0
- package/dist/commands/db/apply/contract.d.ts +23 -0
- package/dist/commands/db/apply/helpers/fresh-db-handler.d.ts +2 -1
- package/dist/commands/db/apply/helpers/hazard-handler.d.ts +19 -8
- package/dist/commands/db/apply/helpers/index.d.ts +2 -1
- package/dist/commands/db/apply/helpers/no-change-plan.d.ts +2 -0
- package/dist/commands/db/apply/helpers/plan-check-filter.d.ts +11 -0
- package/dist/commands/db/apply/machine.d.ts +52 -1
- package/dist/commands/db/utils/boundary-policy/types.d.ts +2 -0
- package/dist/commands/db/utils/duplicate-function-ownership.d.ts +35 -0
- package/dist/commands/db/utils/plan-size-guard.d.ts +16 -0
- package/dist/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.d.ts +4 -0
- package/dist/constants/versions.d.ts +1 -1
- package/dist/{db-BPQ2TEQM.js → db-EPI2DQYN.js} +1203 -410
- package/dist/{dev-MLRKIP7F.js → dev-GB5ERUVR.js} +1 -1
- package/dist/{env-WNHJVLOT.js → env-WP74UUMO.js} +1 -1
- package/dist/{hotfix-Z5EGVSMH.js → hotfix-TOSGTVCW.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/{init-S2ATHLJ6.js → init-35JLDFHI.js} +1 -1
- package/dist/{risk-detector-VO5HJR4R.js → risk-detector-S7XQF4I2.js} +1 -1
- package/dist/{risk-detector-core-7WZJZ5ZI.js → risk-detector-core-TGFKWHRS.js} +1 -1
- package/dist/{risk-detector-plpgsql-ULV7NLDB.js → risk-detector-plpgsql-O32TUR34.js} +103 -5
- package/dist/{upgrade-BDUWBRT5.js → upgrade-7L4JIE4K.js} +1 -1
- package/dist/{vuln-check-66RXX3TO.js → vuln-check-G6I4YYDC.js} +1 -1
- package/dist/{vuln-checker-FFOGOJPT.js → vuln-checker-CT2AYPIS.js} +1 -1
- package/dist/{watch-ITYW57SL.js → watch-AL4LCBRM.js} +1 -1
- package/package.json +3 -3
- 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-
|
|
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-
|
|
8
|
-
export { resolveDatabaseUrl, tryResolveDatabaseUrl } from './chunk-
|
|
9
|
-
import { detectAppSchemas, normalizeDatabaseUrlForDdl, formatSchemasForSql } from './chunk-
|
|
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,
|
|
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
|
|
35
|
-
import
|
|
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/
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
1408
|
-
|
|
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
|
-
`
|
|
1259
|
+
`Plan summary: ${filterInfo.planSummary.rawStatements} raw statement(s), ${filterInfo.planSummary.effectiveStatements} structural, ${filterInfo.planSummary.noiseStatements} collapsed noise`
|
|
1411
1260
|
);
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5948
|
-
const
|
|
5949
|
-
|
|
5950
|
-
const
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
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
|
|
6457
|
+
return definitions;
|
|
5965
6458
|
}
|
|
5966
|
-
function
|
|
5967
|
-
const
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
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
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
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
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
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
|
-
|
|
6524
|
+
const hasAmbiguousMatch = declarativeDefinition.signature === null || idempotentMatches.some((candidate) => candidate.signature === null);
|
|
6525
|
+
if (!hasAmbiguousMatch) {
|
|
6003
6526
|
continue;
|
|
6004
6527
|
}
|
|
6005
|
-
|
|
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
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
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
|
-
|
|
6027
|
-
|
|
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
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6583
|
+
return reasons;
|
|
6584
|
+
}
|
|
6585
|
+
function assessPlanSize(summary) {
|
|
6586
|
+
if (!summary) {
|
|
6587
|
+
return { severity: "ok", reasons: [] };
|
|
6037
6588
|
}
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6589
|
+
const blockerReasons = buildThresholdReasons(summary, PLAN_SIZE_BLOCKER_THRESHOLD);
|
|
6590
|
+
if (blockerReasons.length > 0) {
|
|
6591
|
+
return { severity: "blocker", reasons: blockerReasons };
|
|
6041
6592
|
}
|
|
6042
|
-
const
|
|
6043
|
-
|
|
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
|
|
6677
|
-
|
|
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
|
|
6697
|
-
lineEnd: scopedLineRange
|
|
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 (
|
|
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
|
-
|
|
7581
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
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
|
|
16515
|
-
const
|
|
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
|
}
|