@runa-ai/runa-cli 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-6E2DRXIL.js +452 -0
- package/dist/{chunk-AO554K3G.js → chunk-GHQH6UC5.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-SVXOSMW6.js → cli-ZY5VRIJA.js} +8 -8
- 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/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/{db-S4V4ETDR.js → db-EPI2DQYN.js} +1025 -306
- 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/{vuln-check-D575VXIQ.js → vuln-check-G6I4YYDC.js} +1 -1
- package/dist/{vuln-checker-QV6XODTJ.js → vuln-checker-CT2AYPIS.js} +1 -1
- package/package.json +3 -3
- package/dist/chunk-4XHZQRRK.js +0 -215
|
@@ -4,9 +4,9 @@ import { detectDatabaseStack, getStackPaths } from './chunk-CCKG5R4Y.js';
|
|
|
4
4
|
import { categorizeRisks, detectSchemaRisks } from './chunk-PAWNJA3N.js';
|
|
5
5
|
import './chunk-ZZOXM6Q4.js';
|
|
6
6
|
import { createError } from './chunk-JQXOVCOP.js';
|
|
7
|
-
import { resolveDatabaseUrl, resolveDatabaseTarget, tryResolveDatabaseUrl } from './chunk-
|
|
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)");
|
|
@@ -6045,6 +5922,685 @@ var previewIdempotentSchemas = fromPromise(async ({ input: { input, targetDir }
|
|
|
6045
5922
|
|
|
6046
5923
|
// src/commands/db/apply/actors/pg-schema-diff-actors.ts
|
|
6047
5924
|
init_esm_shims();
|
|
5925
|
+
|
|
5926
|
+
// src/commands/db/utils/duplicate-function-ownership.ts
|
|
5927
|
+
init_esm_shims();
|
|
5928
|
+
var DUPLICATE_FUNCTION_OWNERSHIP_NOTE = "Function ownership belongs to declarative SQL; idempotent 2nd pass must not redefine declarative-managed functions.";
|
|
5929
|
+
var TYPE_START_KEYWORDS = /* @__PURE__ */ new Set([
|
|
5930
|
+
"bigint",
|
|
5931
|
+
"bigserial",
|
|
5932
|
+
"bit",
|
|
5933
|
+
"boolean",
|
|
5934
|
+
"box",
|
|
5935
|
+
"bytea",
|
|
5936
|
+
"char",
|
|
5937
|
+
"character",
|
|
5938
|
+
"cidr",
|
|
5939
|
+
"circle",
|
|
5940
|
+
"date",
|
|
5941
|
+
"decimal",
|
|
5942
|
+
"double",
|
|
5943
|
+
"float",
|
|
5944
|
+
"float4",
|
|
5945
|
+
"float8",
|
|
5946
|
+
"inet",
|
|
5947
|
+
"int",
|
|
5948
|
+
"int2",
|
|
5949
|
+
"int4",
|
|
5950
|
+
"int8",
|
|
5951
|
+
"integer",
|
|
5952
|
+
"interval",
|
|
5953
|
+
"json",
|
|
5954
|
+
"jsonb",
|
|
5955
|
+
"line",
|
|
5956
|
+
"lseg",
|
|
5957
|
+
"macaddr",
|
|
5958
|
+
"money",
|
|
5959
|
+
"numeric",
|
|
5960
|
+
"path",
|
|
5961
|
+
"pg_lsn",
|
|
5962
|
+
"point",
|
|
5963
|
+
"polygon",
|
|
5964
|
+
"real",
|
|
5965
|
+
"record",
|
|
5966
|
+
"serial",
|
|
5967
|
+
"smallint",
|
|
5968
|
+
"smallserial",
|
|
5969
|
+
"text",
|
|
5970
|
+
"time",
|
|
5971
|
+
"timestamp",
|
|
5972
|
+
"timestamptz",
|
|
5973
|
+
"timetz",
|
|
5974
|
+
"tsquery",
|
|
5975
|
+
"tsvector",
|
|
5976
|
+
"txid_snapshot",
|
|
5977
|
+
"uuid",
|
|
5978
|
+
"varchar",
|
|
5979
|
+
"varying",
|
|
5980
|
+
"void",
|
|
5981
|
+
"xml"
|
|
5982
|
+
]);
|
|
5983
|
+
function createFunctionDefinitionRegex() {
|
|
5984
|
+
return new RegExp(FUNCTION_DEFINITION_RE.source, FUNCTION_DEFINITION_RE.flags);
|
|
5985
|
+
}
|
|
5986
|
+
function normalizeSignatureText(signature) {
|
|
5987
|
+
return signature.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").trim();
|
|
5988
|
+
}
|
|
5989
|
+
function splitTopLevelCommaSeparated(input) {
|
|
5990
|
+
const parts = [];
|
|
5991
|
+
let current = "";
|
|
5992
|
+
let depth = 0;
|
|
5993
|
+
let inSingleQuote = false;
|
|
5994
|
+
let inDoubleQuote = false;
|
|
5995
|
+
let activeDollarTag = null;
|
|
5996
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
5997
|
+
if (activeDollarTag) {
|
|
5998
|
+
if (input.startsWith(activeDollarTag, index)) {
|
|
5999
|
+
current += activeDollarTag;
|
|
6000
|
+
index += activeDollarTag.length - 1;
|
|
6001
|
+
activeDollarTag = null;
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
6004
|
+
current += input[index] ?? "";
|
|
6005
|
+
continue;
|
|
6006
|
+
}
|
|
6007
|
+
const char = input[index] ?? "";
|
|
6008
|
+
const next = input[index + 1] ?? "";
|
|
6009
|
+
if (inSingleQuote) {
|
|
6010
|
+
current += char;
|
|
6011
|
+
if (char === "'" && next === "'") {
|
|
6012
|
+
current += next;
|
|
6013
|
+
index += 1;
|
|
6014
|
+
continue;
|
|
6015
|
+
}
|
|
6016
|
+
if (char === "'") {
|
|
6017
|
+
inSingleQuote = false;
|
|
6018
|
+
}
|
|
6019
|
+
continue;
|
|
6020
|
+
}
|
|
6021
|
+
if (inDoubleQuote) {
|
|
6022
|
+
current += char;
|
|
6023
|
+
if (char === '"' && next === '"') {
|
|
6024
|
+
current += next;
|
|
6025
|
+
index += 1;
|
|
6026
|
+
continue;
|
|
6027
|
+
}
|
|
6028
|
+
if (char === '"') {
|
|
6029
|
+
inDoubleQuote = false;
|
|
6030
|
+
}
|
|
6031
|
+
continue;
|
|
6032
|
+
}
|
|
6033
|
+
if (char === "'") {
|
|
6034
|
+
inSingleQuote = true;
|
|
6035
|
+
current += char;
|
|
6036
|
+
continue;
|
|
6037
|
+
}
|
|
6038
|
+
if (char === '"') {
|
|
6039
|
+
inDoubleQuote = true;
|
|
6040
|
+
current += char;
|
|
6041
|
+
continue;
|
|
6042
|
+
}
|
|
6043
|
+
if (char === "$") {
|
|
6044
|
+
const dollarTag = tryMatchDollarTag(input, index);
|
|
6045
|
+
if (dollarTag) {
|
|
6046
|
+
activeDollarTag = dollarTag;
|
|
6047
|
+
current += dollarTag;
|
|
6048
|
+
index += dollarTag.length - 1;
|
|
6049
|
+
continue;
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
if (char === "(" || char === "[") {
|
|
6053
|
+
depth += 1;
|
|
6054
|
+
current += char;
|
|
6055
|
+
continue;
|
|
6056
|
+
}
|
|
6057
|
+
if (char === ")" || char === "]") {
|
|
6058
|
+
depth = Math.max(0, depth - 1);
|
|
6059
|
+
current += char;
|
|
6060
|
+
continue;
|
|
6061
|
+
}
|
|
6062
|
+
if (char === "," && depth === 0) {
|
|
6063
|
+
parts.push(current.trim());
|
|
6064
|
+
current = "";
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
current += char;
|
|
6068
|
+
}
|
|
6069
|
+
if (inSingleQuote || inDoubleQuote || activeDollarTag) {
|
|
6070
|
+
return null;
|
|
6071
|
+
}
|
|
6072
|
+
parts.push(current.trim());
|
|
6073
|
+
return parts.filter((part) => part.length > 0);
|
|
6074
|
+
}
|
|
6075
|
+
function splitTopLevelWhitespace(input) {
|
|
6076
|
+
const tokens = [];
|
|
6077
|
+
let current = "";
|
|
6078
|
+
let depth = 0;
|
|
6079
|
+
let inSingleQuote = false;
|
|
6080
|
+
let inDoubleQuote = false;
|
|
6081
|
+
let activeDollarTag = null;
|
|
6082
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
6083
|
+
if (activeDollarTag) {
|
|
6084
|
+
if (input.startsWith(activeDollarTag, index)) {
|
|
6085
|
+
current += activeDollarTag;
|
|
6086
|
+
index += activeDollarTag.length - 1;
|
|
6087
|
+
activeDollarTag = null;
|
|
6088
|
+
continue;
|
|
6089
|
+
}
|
|
6090
|
+
current += input[index] ?? "";
|
|
6091
|
+
continue;
|
|
6092
|
+
}
|
|
6093
|
+
const char = input[index] ?? "";
|
|
6094
|
+
const next = input[index + 1] ?? "";
|
|
6095
|
+
if (inSingleQuote) {
|
|
6096
|
+
current += char;
|
|
6097
|
+
if (char === "'" && next === "'") {
|
|
6098
|
+
current += next;
|
|
6099
|
+
index += 1;
|
|
6100
|
+
continue;
|
|
6101
|
+
}
|
|
6102
|
+
if (char === "'") {
|
|
6103
|
+
inSingleQuote = false;
|
|
6104
|
+
}
|
|
6105
|
+
continue;
|
|
6106
|
+
}
|
|
6107
|
+
if (inDoubleQuote) {
|
|
6108
|
+
current += char;
|
|
6109
|
+
if (char === '"' && next === '"') {
|
|
6110
|
+
current += next;
|
|
6111
|
+
index += 1;
|
|
6112
|
+
continue;
|
|
6113
|
+
}
|
|
6114
|
+
if (char === '"') {
|
|
6115
|
+
inDoubleQuote = false;
|
|
6116
|
+
}
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
6119
|
+
if (char === "'") {
|
|
6120
|
+
inSingleQuote = true;
|
|
6121
|
+
current += char;
|
|
6122
|
+
continue;
|
|
6123
|
+
}
|
|
6124
|
+
if (char === '"') {
|
|
6125
|
+
inDoubleQuote = true;
|
|
6126
|
+
current += char;
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
if (char === "$") {
|
|
6130
|
+
const dollarTag = tryMatchDollarTag(input, index);
|
|
6131
|
+
if (dollarTag) {
|
|
6132
|
+
activeDollarTag = dollarTag;
|
|
6133
|
+
current += dollarTag;
|
|
6134
|
+
index += dollarTag.length - 1;
|
|
6135
|
+
continue;
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
if (char === "(" || char === "[") {
|
|
6139
|
+
depth += 1;
|
|
6140
|
+
current += char;
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
if (char === ")" || char === "]") {
|
|
6144
|
+
depth = Math.max(0, depth - 1);
|
|
6145
|
+
current += char;
|
|
6146
|
+
continue;
|
|
6147
|
+
}
|
|
6148
|
+
if (/\s/.test(char) && depth === 0) {
|
|
6149
|
+
if (current.trim().length > 0) {
|
|
6150
|
+
tokens.push(current.trim());
|
|
6151
|
+
current = "";
|
|
6152
|
+
}
|
|
6153
|
+
continue;
|
|
6154
|
+
}
|
|
6155
|
+
current += char;
|
|
6156
|
+
}
|
|
6157
|
+
if (inSingleQuote || inDoubleQuote || activeDollarTag) {
|
|
6158
|
+
return null;
|
|
6159
|
+
}
|
|
6160
|
+
if (current.trim().length > 0) {
|
|
6161
|
+
tokens.push(current.trim());
|
|
6162
|
+
}
|
|
6163
|
+
return tokens;
|
|
6164
|
+
}
|
|
6165
|
+
function findTopLevelDefaultIndex(input) {
|
|
6166
|
+
let depth = 0;
|
|
6167
|
+
let inSingleQuote = false;
|
|
6168
|
+
let inDoubleQuote = false;
|
|
6169
|
+
let activeDollarTag = null;
|
|
6170
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
6171
|
+
if (activeDollarTag) {
|
|
6172
|
+
if (input.startsWith(activeDollarTag, index)) {
|
|
6173
|
+
index += activeDollarTag.length - 1;
|
|
6174
|
+
activeDollarTag = null;
|
|
6175
|
+
}
|
|
6176
|
+
continue;
|
|
6177
|
+
}
|
|
6178
|
+
const char = input[index] ?? "";
|
|
6179
|
+
const next = input[index + 1] ?? "";
|
|
6180
|
+
if (inSingleQuote) {
|
|
6181
|
+
if (char === "'" && next === "'") {
|
|
6182
|
+
index += 1;
|
|
6183
|
+
continue;
|
|
6184
|
+
}
|
|
6185
|
+
if (char === "'") {
|
|
6186
|
+
inSingleQuote = false;
|
|
6187
|
+
}
|
|
6188
|
+
continue;
|
|
6189
|
+
}
|
|
6190
|
+
if (inDoubleQuote) {
|
|
6191
|
+
if (char === '"' && next === '"') {
|
|
6192
|
+
index += 1;
|
|
6193
|
+
continue;
|
|
6194
|
+
}
|
|
6195
|
+
if (char === '"') {
|
|
6196
|
+
inDoubleQuote = false;
|
|
6197
|
+
}
|
|
6198
|
+
continue;
|
|
6199
|
+
}
|
|
6200
|
+
if (char === "'") {
|
|
6201
|
+
inSingleQuote = true;
|
|
6202
|
+
continue;
|
|
6203
|
+
}
|
|
6204
|
+
if (char === '"') {
|
|
6205
|
+
inDoubleQuote = true;
|
|
6206
|
+
continue;
|
|
6207
|
+
}
|
|
6208
|
+
if (char === "$") {
|
|
6209
|
+
const dollarTag = tryMatchDollarTag(input, index);
|
|
6210
|
+
if (dollarTag) {
|
|
6211
|
+
activeDollarTag = dollarTag;
|
|
6212
|
+
index += dollarTag.length - 1;
|
|
6213
|
+
continue;
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
if (char === "(" || char === "[") {
|
|
6217
|
+
depth += 1;
|
|
6218
|
+
continue;
|
|
6219
|
+
}
|
|
6220
|
+
if (char === ")" || char === "]") {
|
|
6221
|
+
depth = Math.max(0, depth - 1);
|
|
6222
|
+
continue;
|
|
6223
|
+
}
|
|
6224
|
+
if (depth === 0 && char === "=") {
|
|
6225
|
+
return index;
|
|
6226
|
+
}
|
|
6227
|
+
if (depth === 0 && input.slice(index).match(/^DEFAULT\b/i) && (index === 0 || /\s/.test(input[index - 1] ?? " "))) {
|
|
6228
|
+
return index;
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
return null;
|
|
6232
|
+
}
|
|
6233
|
+
function stripDefaultExpression(argument) {
|
|
6234
|
+
const defaultIndex = findTopLevelDefaultIndex(argument);
|
|
6235
|
+
if (defaultIndex == null) {
|
|
6236
|
+
return argument.trim();
|
|
6237
|
+
}
|
|
6238
|
+
return argument.slice(0, defaultIndex).trim();
|
|
6239
|
+
}
|
|
6240
|
+
function normalizeTypeTokenText(tokens) {
|
|
6241
|
+
const normalized = tokens.join(" ").replace(/\s+/g, " ").replace(/\s+\(/g, "(").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/\s+\[/g, "[").replace(/\[\s+/g, "[").replace(/\s+\]/g, "]").replace(/\s*,\s*/g, ", ").trim().toLowerCase();
|
|
6242
|
+
return canonicalizeTypeAlias(normalized);
|
|
6243
|
+
}
|
|
6244
|
+
function canonicalizeTypeAlias(typeText) {
|
|
6245
|
+
const aliasReplacements = [
|
|
6246
|
+
[/^int$/g, "integer"],
|
|
6247
|
+
[/\bint4\b/g, "integer"],
|
|
6248
|
+
[/\bint8\b/g, "bigint"],
|
|
6249
|
+
[/\bint2\b/g, "smallint"],
|
|
6250
|
+
[/\bfloat8\b/g, "double precision"],
|
|
6251
|
+
[/\bfloat4\b/g, "real"],
|
|
6252
|
+
[/\bdecimal\b/g, "numeric"],
|
|
6253
|
+
[/\bcharacter varying\b/g, "varchar"],
|
|
6254
|
+
[/\bcharacter\b/g, "char"],
|
|
6255
|
+
[/\btimestamp with time zone\b/g, "timestamptz"],
|
|
6256
|
+
[/\btimestamp without time zone\b/g, "timestamp"],
|
|
6257
|
+
[/\btime with time zone\b/g, "timetz"],
|
|
6258
|
+
[/\btime without time zone\b/g, "time"]
|
|
6259
|
+
];
|
|
6260
|
+
return aliasReplacements.reduce(
|
|
6261
|
+
(current, [pattern, replacement]) => current.replace(pattern, replacement),
|
|
6262
|
+
typeText
|
|
6263
|
+
);
|
|
6264
|
+
}
|
|
6265
|
+
function normalizeIdentifierToken(token) {
|
|
6266
|
+
return token.replace(/^"|"$/g, "").toLowerCase();
|
|
6267
|
+
}
|
|
6268
|
+
function looksLikeNamedArgumentToken(token) {
|
|
6269
|
+
return /^"[^"]+"$/.test(token) || /^[A-Za-z_][A-Za-z0-9_]*$/.test(token);
|
|
6270
|
+
}
|
|
6271
|
+
function looksLikeTypeTokens(tokens) {
|
|
6272
|
+
if (tokens.length === 0) {
|
|
6273
|
+
return false;
|
|
6274
|
+
}
|
|
6275
|
+
const first = normalizeIdentifierToken(tokens[0] ?? "");
|
|
6276
|
+
if (TYPE_START_KEYWORDS.has(first)) {
|
|
6277
|
+
return true;
|
|
6278
|
+
}
|
|
6279
|
+
if (first.includes(".") || first.includes("%type") || first.endsWith("[]") || tokens[0]?.includes("(") || tokens[0]?.includes("[")) {
|
|
6280
|
+
return true;
|
|
6281
|
+
}
|
|
6282
|
+
return tokens.length === 1;
|
|
6283
|
+
}
|
|
6284
|
+
function normalizeArgumentIdentity(argument) {
|
|
6285
|
+
const withoutDefault = stripDefaultExpression(argument);
|
|
6286
|
+
const tokens = splitTopLevelWhitespace(withoutDefault);
|
|
6287
|
+
if (!tokens || tokens.length === 0) {
|
|
6288
|
+
return null;
|
|
6289
|
+
}
|
|
6290
|
+
let index = 0;
|
|
6291
|
+
let mode = null;
|
|
6292
|
+
const firstToken = normalizeIdentifierToken(tokens[index] ?? "");
|
|
6293
|
+
const secondToken = normalizeIdentifierToken(tokens[index + 1] ?? "");
|
|
6294
|
+
if (firstToken === "in" && secondToken === "out") {
|
|
6295
|
+
mode = "inout";
|
|
6296
|
+
index += 2;
|
|
6297
|
+
} else if (firstToken === "in" || firstToken === "out" || firstToken === "inout") {
|
|
6298
|
+
mode = firstToken;
|
|
6299
|
+
index += 1;
|
|
6300
|
+
} else if (firstToken === "variadic") {
|
|
6301
|
+
mode = "variadic";
|
|
6302
|
+
index += 1;
|
|
6303
|
+
}
|
|
6304
|
+
let remaining = tokens.slice(index);
|
|
6305
|
+
if (remaining.length === 0) {
|
|
6306
|
+
return null;
|
|
6307
|
+
}
|
|
6308
|
+
if (mode === "out") {
|
|
6309
|
+
return null;
|
|
6310
|
+
}
|
|
6311
|
+
const shouldDropName = remaining.length > 1 && looksLikeNamedArgumentToken(remaining[0] ?? "") && !TYPE_START_KEYWORDS.has(normalizeIdentifierToken(remaining[0] ?? "")) && looksLikeTypeTokens(remaining.slice(1));
|
|
6312
|
+
if (shouldDropName) {
|
|
6313
|
+
remaining = remaining.slice(1);
|
|
6314
|
+
}
|
|
6315
|
+
if (remaining.length === 0) {
|
|
6316
|
+
return null;
|
|
6317
|
+
}
|
|
6318
|
+
const normalizedType = normalizeTypeTokenText(remaining);
|
|
6319
|
+
if (!normalizedType) {
|
|
6320
|
+
return null;
|
|
6321
|
+
}
|
|
6322
|
+
return mode === "variadic" ? `variadic ${normalizedType}` : normalizedType;
|
|
6323
|
+
}
|
|
6324
|
+
function tryMatchDollarTag(sql, index) {
|
|
6325
|
+
const slice = sql.slice(index);
|
|
6326
|
+
const match = slice.match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/);
|
|
6327
|
+
return match?.[0] ?? null;
|
|
6328
|
+
}
|
|
6329
|
+
function findMatchingParen(sql, openIndex) {
|
|
6330
|
+
let depth = 0;
|
|
6331
|
+
let index = openIndex;
|
|
6332
|
+
let inSingleQuote = false;
|
|
6333
|
+
let inDoubleQuote = false;
|
|
6334
|
+
let activeDollarTag = null;
|
|
6335
|
+
while (index < sql.length) {
|
|
6336
|
+
if (activeDollarTag) {
|
|
6337
|
+
if (sql.startsWith(activeDollarTag, index)) {
|
|
6338
|
+
index += activeDollarTag.length;
|
|
6339
|
+
activeDollarTag = null;
|
|
6340
|
+
continue;
|
|
6341
|
+
}
|
|
6342
|
+
index += 1;
|
|
6343
|
+
continue;
|
|
6344
|
+
}
|
|
6345
|
+
const char = sql[index] ?? "";
|
|
6346
|
+
const next = sql[index + 1] ?? "";
|
|
6347
|
+
if (inSingleQuote) {
|
|
6348
|
+
if (char === "'" && next === "'") {
|
|
6349
|
+
index += 2;
|
|
6350
|
+
continue;
|
|
6351
|
+
}
|
|
6352
|
+
if (char === "'") {
|
|
6353
|
+
inSingleQuote = false;
|
|
6354
|
+
}
|
|
6355
|
+
index += 1;
|
|
6356
|
+
continue;
|
|
6357
|
+
}
|
|
6358
|
+
if (inDoubleQuote) {
|
|
6359
|
+
if (char === '"' && next === '"') {
|
|
6360
|
+
index += 2;
|
|
6361
|
+
continue;
|
|
6362
|
+
}
|
|
6363
|
+
if (char === '"') {
|
|
6364
|
+
inDoubleQuote = false;
|
|
6365
|
+
}
|
|
6366
|
+
index += 1;
|
|
6367
|
+
continue;
|
|
6368
|
+
}
|
|
6369
|
+
if (char === "'") {
|
|
6370
|
+
inSingleQuote = true;
|
|
6371
|
+
index += 1;
|
|
6372
|
+
continue;
|
|
6373
|
+
}
|
|
6374
|
+
if (char === '"') {
|
|
6375
|
+
inDoubleQuote = true;
|
|
6376
|
+
index += 1;
|
|
6377
|
+
continue;
|
|
6378
|
+
}
|
|
6379
|
+
if (char === "$") {
|
|
6380
|
+
const dollarTag = tryMatchDollarTag(sql, index);
|
|
6381
|
+
if (dollarTag) {
|
|
6382
|
+
activeDollarTag = dollarTag;
|
|
6383
|
+
index += dollarTag.length;
|
|
6384
|
+
continue;
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
if (char === "(") {
|
|
6388
|
+
depth += 1;
|
|
6389
|
+
} else if (char === ")") {
|
|
6390
|
+
depth -= 1;
|
|
6391
|
+
if (depth === 0) {
|
|
6392
|
+
return index;
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
index += 1;
|
|
6396
|
+
}
|
|
6397
|
+
return null;
|
|
6398
|
+
}
|
|
6399
|
+
function extractFunctionSignature(statement, match) {
|
|
6400
|
+
const definitionEndIndex = (match.index ?? 0) + match[0].length;
|
|
6401
|
+
const openParenIndex = statement.indexOf("(", definitionEndIndex);
|
|
6402
|
+
if (openParenIndex < 0) {
|
|
6403
|
+
return null;
|
|
6404
|
+
}
|
|
6405
|
+
const closeParenIndex = findMatchingParen(statement, openParenIndex);
|
|
6406
|
+
if (closeParenIndex == null) {
|
|
6407
|
+
return null;
|
|
6408
|
+
}
|
|
6409
|
+
const rawSignature = statement.slice(openParenIndex + 1, closeParenIndex);
|
|
6410
|
+
const argumentsList = splitTopLevelCommaSeparated(rawSignature);
|
|
6411
|
+
if (!argumentsList) {
|
|
6412
|
+
return null;
|
|
6413
|
+
}
|
|
6414
|
+
const normalizedArguments = [];
|
|
6415
|
+
for (const argument of argumentsList) {
|
|
6416
|
+
const normalizedArgument = normalizeArgumentIdentity(argument);
|
|
6417
|
+
if (normalizedArgument) {
|
|
6418
|
+
normalizedArguments.push(normalizedArgument);
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
return `(${normalizeSignatureText(normalizedArguments.join(", "))})`;
|
|
6422
|
+
}
|
|
6423
|
+
function extractFunctionOwnershipDefinition(statement, file, line, layer) {
|
|
6424
|
+
const regex = createFunctionDefinitionRegex();
|
|
6425
|
+
const match = regex.exec(statement);
|
|
6426
|
+
if (!match) {
|
|
6427
|
+
return null;
|
|
6428
|
+
}
|
|
6429
|
+
const schema = (match[1] ?? match[2] ?? PUBLIC_SCHEMA).toLowerCase();
|
|
6430
|
+
const name = (match[3] ?? match[4] ?? "").toLowerCase();
|
|
6431
|
+
if (!name) {
|
|
6432
|
+
return null;
|
|
6433
|
+
}
|
|
6434
|
+
return {
|
|
6435
|
+
qualifiedName: `${schema}.${name}`,
|
|
6436
|
+
signature: extractFunctionSignature(statement, match),
|
|
6437
|
+
file,
|
|
6438
|
+
line,
|
|
6439
|
+
layer
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
function collectFunctionOwnershipDefinitions(targetDir, layer) {
|
|
6443
|
+
const definitions = [];
|
|
6444
|
+
for (const file of collectSqlFiles(targetDir, layer)) {
|
|
6445
|
+
for (const statement of splitSqlStatements(file.content)) {
|
|
6446
|
+
const definition = extractFunctionOwnershipDefinition(
|
|
6447
|
+
statement.statement,
|
|
6448
|
+
file.relativePath,
|
|
6449
|
+
statement.line,
|
|
6450
|
+
layer
|
|
6451
|
+
);
|
|
6452
|
+
if (definition) {
|
|
6453
|
+
definitions.push(definition);
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
return definitions;
|
|
6458
|
+
}
|
|
6459
|
+
function groupDefinitionsByQualifiedName(definitions) {
|
|
6460
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6461
|
+
for (const definition of definitions) {
|
|
6462
|
+
const existing = grouped.get(definition.qualifiedName) ?? [];
|
|
6463
|
+
existing.push(definition);
|
|
6464
|
+
grouped.set(definition.qualifiedName, existing);
|
|
6465
|
+
}
|
|
6466
|
+
return grouped;
|
|
6467
|
+
}
|
|
6468
|
+
function buildExactFinding(params) {
|
|
6469
|
+
return {
|
|
6470
|
+
code: "DUPLICATE_FUNCTION_OWNERSHIP",
|
|
6471
|
+
qualifiedName: params.qualifiedName,
|
|
6472
|
+
signature: params.signature,
|
|
6473
|
+
message: `Function ${params.qualifiedName}${params.signature} is defined in both declarative and idempotent SQL.`,
|
|
6474
|
+
suggestion: "Keep the canonical function definition in declarative SQL and remove the idempotent duplicate.",
|
|
6475
|
+
declarativeDefinitions: params.declarativeDefinitions,
|
|
6476
|
+
idempotentDefinitions: params.idempotentDefinitions
|
|
6477
|
+
};
|
|
6478
|
+
}
|
|
6479
|
+
function buildAmbiguousFinding(params) {
|
|
6480
|
+
return {
|
|
6481
|
+
code: "DUPLICATE_FUNCTION_OWNERSHIP_AMBIGUOUS",
|
|
6482
|
+
qualifiedName: params.qualifiedName,
|
|
6483
|
+
signature: null,
|
|
6484
|
+
message: `Function ${params.qualifiedName} appears in both declarative and idempotent SQL, and the signature could not be matched safely.`,
|
|
6485
|
+
suggestion: "Review both definitions manually and keep ownership in declarative SQL only.",
|
|
6486
|
+
declarativeDefinitions: params.declarativeDefinitions,
|
|
6487
|
+
idempotentDefinitions: params.idempotentDefinitions
|
|
6488
|
+
};
|
|
6489
|
+
}
|
|
6490
|
+
function analyzeDuplicateFunctionOwnership(targetDir) {
|
|
6491
|
+
const declarativeDefinitions = collectFunctionOwnershipDefinitions(targetDir, "declarative");
|
|
6492
|
+
const idempotentDefinitions = collectFunctionOwnershipDefinitions(targetDir, "idempotent");
|
|
6493
|
+
const idempotentByQualifiedName = groupDefinitionsByQualifiedName(idempotentDefinitions);
|
|
6494
|
+
const findings = [];
|
|
6495
|
+
const seenExact = /* @__PURE__ */ new Set();
|
|
6496
|
+
const seenAmbiguous = /* @__PURE__ */ new Set();
|
|
6497
|
+
for (const declarativeDefinition of declarativeDefinitions) {
|
|
6498
|
+
const idempotentMatches = idempotentByQualifiedName.get(declarativeDefinition.qualifiedName);
|
|
6499
|
+
if (!idempotentMatches || idempotentMatches.length === 0) {
|
|
6500
|
+
continue;
|
|
6501
|
+
}
|
|
6502
|
+
if (declarativeDefinition.signature) {
|
|
6503
|
+
const exactMatches = idempotentMatches.filter(
|
|
6504
|
+
(candidate) => candidate.signature !== null && candidate.signature === declarativeDefinition.signature
|
|
6505
|
+
);
|
|
6506
|
+
if (exactMatches.length > 0) {
|
|
6507
|
+
const exactKey = `${declarativeDefinition.qualifiedName}:${declarativeDefinition.signature}`;
|
|
6508
|
+
if (!seenExact.has(exactKey)) {
|
|
6509
|
+
seenExact.add(exactKey);
|
|
6510
|
+
findings.push(
|
|
6511
|
+
buildExactFinding({
|
|
6512
|
+
qualifiedName: declarativeDefinition.qualifiedName,
|
|
6513
|
+
signature: declarativeDefinition.signature,
|
|
6514
|
+
declarativeDefinitions: declarativeDefinitions.filter(
|
|
6515
|
+
(candidate) => candidate.qualifiedName === declarativeDefinition.qualifiedName && candidate.signature === declarativeDefinition.signature
|
|
6516
|
+
),
|
|
6517
|
+
idempotentDefinitions: exactMatches
|
|
6518
|
+
})
|
|
6519
|
+
);
|
|
6520
|
+
}
|
|
6521
|
+
continue;
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
const hasAmbiguousMatch = declarativeDefinition.signature === null || idempotentMatches.some((candidate) => candidate.signature === null);
|
|
6525
|
+
if (!hasAmbiguousMatch) {
|
|
6526
|
+
continue;
|
|
6527
|
+
}
|
|
6528
|
+
if (!seenAmbiguous.has(declarativeDefinition.qualifiedName)) {
|
|
6529
|
+
seenAmbiguous.add(declarativeDefinition.qualifiedName);
|
|
6530
|
+
findings.push(
|
|
6531
|
+
buildAmbiguousFinding({
|
|
6532
|
+
qualifiedName: declarativeDefinition.qualifiedName,
|
|
6533
|
+
declarativeDefinitions: declarativeDefinitions.filter(
|
|
6534
|
+
(candidate) => candidate.qualifiedName === declarativeDefinition.qualifiedName
|
|
6535
|
+
),
|
|
6536
|
+
idempotentDefinitions: idempotentMatches
|
|
6537
|
+
})
|
|
6538
|
+
);
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
return {
|
|
6542
|
+
contractNote: DUPLICATE_FUNCTION_OWNERSHIP_NOTE,
|
|
6543
|
+
findings,
|
|
6544
|
+
definitions: {
|
|
6545
|
+
declarative: declarativeDefinitions,
|
|
6546
|
+
idempotent: idempotentDefinitions
|
|
6547
|
+
}
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
function formatLocation(definition) {
|
|
6551
|
+
return `${definition.file}:${definition.line}`;
|
|
6552
|
+
}
|
|
6553
|
+
function formatDuplicateFunctionOwnershipFinding(finding) {
|
|
6554
|
+
const signatureSuffix = finding.signature ?? "(<manual review required>)";
|
|
6555
|
+
return {
|
|
6556
|
+
summary: `${finding.code}: ${finding.qualifiedName}${signatureSuffix}`,
|
|
6557
|
+
declarativeLocations: finding.declarativeDefinitions.map(formatLocation),
|
|
6558
|
+
idempotentLocations: finding.idempotentDefinitions.map(formatLocation),
|
|
6559
|
+
suggestion: finding.suggestion
|
|
6560
|
+
};
|
|
6561
|
+
}
|
|
6562
|
+
|
|
6563
|
+
// src/commands/db/utils/plan-size-guard.ts
|
|
6564
|
+
init_esm_shims();
|
|
6565
|
+
var PLAN_SIZE_WARNING_THRESHOLD = {
|
|
6566
|
+
effectiveStatements: 200,
|
|
6567
|
+
rawStatements: 500
|
|
6568
|
+
};
|
|
6569
|
+
var PLAN_SIZE_BLOCKER_THRESHOLD = {
|
|
6570
|
+
effectiveStatements: 500,
|
|
6571
|
+
rawStatements: 1e3
|
|
6572
|
+
};
|
|
6573
|
+
function buildThresholdReasons(summary, threshold) {
|
|
6574
|
+
const reasons = [];
|
|
6575
|
+
if (summary.effectiveStatements >= threshold.effectiveStatements) {
|
|
6576
|
+
reasons.push(
|
|
6577
|
+
`effective structural statements ${summary.effectiveStatements} >= ${threshold.effectiveStatements}`
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
6580
|
+
if (summary.rawStatements >= threshold.rawStatements) {
|
|
6581
|
+
reasons.push(`raw statements ${summary.rawStatements} >= ${threshold.rawStatements}`);
|
|
6582
|
+
}
|
|
6583
|
+
return reasons;
|
|
6584
|
+
}
|
|
6585
|
+
function assessPlanSize(summary) {
|
|
6586
|
+
if (!summary) {
|
|
6587
|
+
return { severity: "ok", reasons: [] };
|
|
6588
|
+
}
|
|
6589
|
+
const blockerReasons = buildThresholdReasons(summary, PLAN_SIZE_BLOCKER_THRESHOLD);
|
|
6590
|
+
if (blockerReasons.length > 0) {
|
|
6591
|
+
return { severity: "blocker", reasons: blockerReasons };
|
|
6592
|
+
}
|
|
6593
|
+
const warningReasons = buildThresholdReasons(summary, PLAN_SIZE_WARNING_THRESHOLD);
|
|
6594
|
+
if (warningReasons.length > 0) {
|
|
6595
|
+
return { severity: "warning", reasons: warningReasons };
|
|
6596
|
+
}
|
|
6597
|
+
return { severity: "ok", reasons: [] };
|
|
6598
|
+
}
|
|
6599
|
+
function formatPlanSizeSummary(summary) {
|
|
6600
|
+
return `${summary.rawStatements} raw statement(s), ${summary.effectiveStatements} structural, ${summary.noiseStatements} collapsed noise`;
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
// src/commands/db/apply/actors/pg-schema-diff-actors.ts
|
|
6048
6604
|
init_local_supabase();
|
|
6049
6605
|
|
|
6050
6606
|
// src/commands/db/utils/declarative-dependency-warning-governance.ts
|
|
@@ -7511,6 +8067,25 @@ function assertDeclarativeDependencyBoundary(targetDir, input) {
|
|
|
7511
8067
|
throw new Error(buildDeclarativeDependencyWarningFailureLines(warningReview).join("\n"));
|
|
7512
8068
|
}
|
|
7513
8069
|
}
|
|
8070
|
+
function warnDuplicateFunctionOwnership(targetDir) {
|
|
8071
|
+
const analysis = analyzeDuplicateFunctionOwnership(targetDir);
|
|
8072
|
+
if (analysis.findings.length === 0) {
|
|
8073
|
+
return;
|
|
8074
|
+
}
|
|
8075
|
+
logger15.warn(`Duplicate function ownership detected (${analysis.findings.length} finding(s))`);
|
|
8076
|
+
logger15.info(` ${analysis.contractNote}`);
|
|
8077
|
+
for (const finding of analysis.findings) {
|
|
8078
|
+
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
8079
|
+
logger15.info(` [WARN] ${formatted.summary}`);
|
|
8080
|
+
for (const location of formatted.declarativeLocations) {
|
|
8081
|
+
logger15.info(` declarative: ${location}`);
|
|
8082
|
+
}
|
|
8083
|
+
for (const location of formatted.idempotentLocations) {
|
|
8084
|
+
logger15.info(` idempotent: ${location}`);
|
|
8085
|
+
}
|
|
8086
|
+
logger15.info(` ${formatted.suggestion}`);
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
7514
8089
|
function createCombinedSchemaBundle(schemaFiles, verbose) {
|
|
7515
8090
|
const tmpDir = mkdtempSync(join(tmpdir(), "runa_pg_schema_diff_"));
|
|
7516
8091
|
const combinedSchemaPath = join(tmpDir, "desired_schema.sql");
|
|
@@ -7541,7 +8116,7 @@ async function createShadowDbForRun(dbUrl, shadowExtensions, verbose) {
|
|
|
7541
8116
|
return shadowDb;
|
|
7542
8117
|
}
|
|
7543
8118
|
function buildNoChangesResult(planOutput) {
|
|
7544
|
-
if (
|
|
8119
|
+
if (isNoChangePlanOutput(planOutput)) {
|
|
7545
8120
|
logger15.success("No schema changes detected");
|
|
7546
8121
|
return { sql: "", hazards: [], applied: true };
|
|
7547
8122
|
}
|
|
@@ -7596,20 +8171,43 @@ function runPreApplyDataCompatibility(dbUrl, planOutput, input) {
|
|
|
7596
8171
|
}
|
|
7597
8172
|
return 0;
|
|
7598
8173
|
}
|
|
7599
|
-
function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir) {
|
|
8174
|
+
function buildCheckModeResult(input, planOutput, hazards, protectedTables, protectedObjects, dataViolationCount, schemasDir, options) {
|
|
7600
8175
|
if (!input.check) {
|
|
7601
8176
|
return null;
|
|
7602
8177
|
}
|
|
7603
8178
|
const plan = parsePlanOutput(planOutput);
|
|
7604
|
-
const { filteredPlan, removedDropStatements, removedAuthzStatements } = filterCheckModePlanStatements(plan, protectedTables, protectedObjects, schemasDir);
|
|
7605
|
-
|
|
7606
|
-
|
|
8179
|
+
const { filteredPlan, removedDropStatements, removedAuthzStatements, removedRlsStatements } = filterCheckModePlanStatements(plan, protectedTables, protectedObjects, schemasDir);
|
|
8180
|
+
const planSummary = buildCheckModePlanSummary({
|
|
8181
|
+
rawStatementCount: options?.rawStatementCount ?? plan.statements.length,
|
|
8182
|
+
filteredPlan,
|
|
7607
8183
|
removedDropStatements,
|
|
7608
|
-
removedAuthzStatements
|
|
8184
|
+
removedAuthzStatements,
|
|
8185
|
+
removedRlsStatements,
|
|
8186
|
+
suppressedFunctionStatements: options?.suppressedFunctionStatements
|
|
8187
|
+
});
|
|
8188
|
+
displayCheckModeResults(planOutput, {
|
|
8189
|
+
verbose: input.verbose,
|
|
8190
|
+
filterInfo: {
|
|
8191
|
+
filteredPlanSql: filteredPlan.rawSql,
|
|
8192
|
+
removedDropStatements,
|
|
8193
|
+
removedAuthzStatements,
|
|
8194
|
+
removedRlsStatements,
|
|
8195
|
+
suppressedFunctionStatements: options?.suppressedFunctionStatements,
|
|
8196
|
+
planSummary
|
|
8197
|
+
}
|
|
7609
8198
|
});
|
|
8199
|
+
const planSizeAssessment = assessPlanSize(planSummary);
|
|
8200
|
+
if (planSizeAssessment.severity !== "ok") {
|
|
8201
|
+
logger15.warn(`Large plan warning: ${formatPlanSizeSummary(planSummary)}`);
|
|
8202
|
+
for (const reason of planSizeAssessment.reasons) {
|
|
8203
|
+
logger15.info(` \u2022 ${reason}`);
|
|
8204
|
+
}
|
|
8205
|
+
logger15.info(" Small production instances may see elevated load during apply.");
|
|
8206
|
+
}
|
|
7610
8207
|
return {
|
|
7611
8208
|
sql: planOutput,
|
|
7612
8209
|
filteredPlanSql: filteredPlan.rawSql,
|
|
8210
|
+
planSummary,
|
|
7613
8211
|
hazards,
|
|
7614
8212
|
applied: false,
|
|
7615
8213
|
dataViolations: dataViolationCount > 0 ? dataViolationCount : void 0
|
|
@@ -7658,6 +8256,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
|
|
|
7658
8256
|
logger15.info("No declarative schemas found");
|
|
7659
8257
|
return { sql: "", hazards: [], applied: false };
|
|
7660
8258
|
}
|
|
8259
|
+
warnDuplicateFunctionOwnership(targetDir);
|
|
7661
8260
|
assertDeclarativeDependencyBoundary(targetDir, input);
|
|
7662
8261
|
const dbUrl = getDbUrl(input);
|
|
7663
8262
|
const configState = loadPgSchemaDiffConfigState(targetDir, input.verbose);
|
|
@@ -7739,13 +8338,14 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
|
|
|
7739
8338
|
}
|
|
7740
8339
|
)
|
|
7741
8340
|
);
|
|
8341
|
+
const rawPlanStatementCount = parsePlanOutput(planOutput).statements.length;
|
|
7742
8342
|
let effectivePlanOutput = planOutput;
|
|
8343
|
+
let suppressedPlan = {
|
|
8344
|
+
planOutput,
|
|
8345
|
+
suppressedStatements: [],
|
|
8346
|
+
warnings: []
|
|
8347
|
+
};
|
|
7743
8348
|
if (!input.compareOnly) {
|
|
7744
|
-
let suppressedPlan = {
|
|
7745
|
-
planOutput,
|
|
7746
|
-
suppressedStatements: [],
|
|
7747
|
-
warnings: []
|
|
7748
|
-
};
|
|
7749
8349
|
try {
|
|
7750
8350
|
suppressedPlan = await withProgress(
|
|
7751
8351
|
"reviewing plan false positives",
|
|
@@ -7765,7 +8365,7 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
|
|
|
7765
8365
|
}
|
|
7766
8366
|
effectivePlanOutput = suppressedPlan.planOutput;
|
|
7767
8367
|
}
|
|
7768
|
-
const noChangesResult = buildNoChangesResult(effectivePlanOutput);
|
|
8368
|
+
const noChangesResult = rawPlanStatementCount === 0 || isNoChangePlanOutput(effectivePlanOutput) ? buildNoChangesResult(effectivePlanOutput) : null;
|
|
7769
8369
|
if (noChangesResult) return noChangesResult;
|
|
7770
8370
|
const { hazards } = await withProgress(
|
|
7771
8371
|
"extracting migration hazards",
|
|
@@ -7792,7 +8392,11 @@ var applyPgSchemaDiff = fromPromise(async ({ input: { input, targetDir } }) => {
|
|
|
7792
8392
|
protectedTables,
|
|
7793
8393
|
protectedObjects,
|
|
7794
8394
|
dataViolationCount,
|
|
7795
|
-
schemasDir
|
|
8395
|
+
schemasDir,
|
|
8396
|
+
{
|
|
8397
|
+
rawStatementCount: rawPlanStatementCount,
|
|
8398
|
+
suppressedFunctionStatements: !input.compareOnly ? suppressedPlan.suppressedStatements : void 0
|
|
8399
|
+
}
|
|
7796
8400
|
);
|
|
7797
8401
|
if (checkModeResult) return checkModeResult;
|
|
7798
8402
|
backupProtectedTablesForProduction(dbUrl, protectedTables, input);
|
|
@@ -8003,15 +8607,16 @@ function runSeeds(input, targetDir, dbUrl) {
|
|
|
8003
8607
|
async function generateTablesManifestSafely(targetDir, dbUrl) {
|
|
8004
8608
|
try {
|
|
8005
8609
|
await generateTablesManifest(targetDir, { databaseUrl: dbUrl });
|
|
8610
|
+
return void 0;
|
|
8006
8611
|
} catch (error) {
|
|
8007
|
-
|
|
8612
|
+
return `Failed to generate tables manifest: ${error}`;
|
|
8008
8613
|
}
|
|
8009
8614
|
}
|
|
8010
8615
|
var applySeeds = fromPromise(async ({ input: { input, targetDir } }) => {
|
|
8011
8616
|
const dbUrl = getDbUrl(input);
|
|
8012
8617
|
const seedsApplied = runSeeds(input, targetDir, dbUrl);
|
|
8013
|
-
await generateTablesManifestSafely(targetDir, dbUrl);
|
|
8014
|
-
return { applied: seedsApplied };
|
|
8618
|
+
const manifestWarning = await generateTablesManifestSafely(targetDir, dbUrl);
|
|
8619
|
+
return { applied: seedsApplied, warnings: manifestWarning ? [manifestWarning] : [] };
|
|
8015
8620
|
});
|
|
8016
8621
|
|
|
8017
8622
|
// src/commands/db/apply/machine.ts
|
|
@@ -8068,7 +8673,7 @@ var e2eMeta = {
|
|
|
8068
8673
|
"expect(log).toContain('pg-schema-diff')",
|
|
8069
8674
|
"expect(ctx.schemaChangesApplied).toBeDefined()"
|
|
8070
8675
|
],
|
|
8071
|
-
nextStates: ["applyingIdempotentPost", "done", "failed"]
|
|
8676
|
+
nextStates: ["applyingIdempotentPost", "validatingPartitions", "done", "failed"]
|
|
8072
8677
|
},
|
|
8073
8678
|
applyingIdempotentPost: {
|
|
8074
8679
|
description: "Apply idempotent schemas (2nd pass: dependent tables, RLS)",
|
|
@@ -8159,6 +8764,9 @@ function buildDbApplyMetrics(context, endTime) {
|
|
|
8159
8764
|
retryAttempts: toOptionalPositive(context.retryAttempts)
|
|
8160
8765
|
};
|
|
8161
8766
|
}
|
|
8767
|
+
function createMachineWarning(code, message, phase) {
|
|
8768
|
+
return { code, message, phase };
|
|
8769
|
+
}
|
|
8162
8770
|
function createDbApplyOutput(context, endTime) {
|
|
8163
8771
|
const metrics = buildDbApplyMetrics(context, endTime);
|
|
8164
8772
|
const totalIdempotentApplied = context.idempotentPreApplied + context.idempotentPostApplied;
|
|
@@ -8189,6 +8797,7 @@ function createDbApplyOutput(context, endTime) {
|
|
|
8189
8797
|
dataViolations: toOptionalPositive(context.dataViolations),
|
|
8190
8798
|
planSql: context.planSql ?? void 0,
|
|
8191
8799
|
filteredPlanSql: context.filteredPlanSql ?? void 0,
|
|
8800
|
+
planSummary: context.planSummary ?? void 0,
|
|
8192
8801
|
checkOnly: toOptionalTrue(context.input.check === true),
|
|
8193
8802
|
partitionWarnings: toOptionalArray(context.partitionWarnings),
|
|
8194
8803
|
idempotentFiles: toOptionalArray(context.idempotentFiles),
|
|
@@ -8201,7 +8810,7 @@ function createDbApplyOutput(context, endTime) {
|
|
|
8201
8810
|
endedAt: new Date(endTime).toISOString(),
|
|
8202
8811
|
durationMs: endTime - context.startTime,
|
|
8203
8812
|
phases,
|
|
8204
|
-
warnings:
|
|
8813
|
+
warnings: context.nonCriticalWarnings,
|
|
8205
8814
|
errors: phases.map((phase) => phase.error).filter((value) => value != null),
|
|
8206
8815
|
summary: buildCommandOutcomeSummary(phases)
|
|
8207
8816
|
}
|
|
@@ -8210,6 +8819,23 @@ function createDbApplyOutput(context, endTime) {
|
|
|
8210
8819
|
var dbApplyMachine = setup({
|
|
8211
8820
|
types: {},
|
|
8212
8821
|
actions: {
|
|
8822
|
+
assignPgSchemaDiffResult: assign(({ event }) => {
|
|
8823
|
+
if (!("output" in event)) {
|
|
8824
|
+
return {};
|
|
8825
|
+
}
|
|
8826
|
+
const output = event.output;
|
|
8827
|
+
return {
|
|
8828
|
+
schemaChangesApplied: output.applied,
|
|
8829
|
+
dataViolations: output.dataViolations ?? 0,
|
|
8830
|
+
hazards: output.hazards,
|
|
8831
|
+
planSql: output.sql ?? null,
|
|
8832
|
+
filteredPlanSql: output.filteredPlanSql ?? null,
|
|
8833
|
+
planSummary: output.planSummary ?? null,
|
|
8834
|
+
ssotWarning: output.ssotWarning ?? null,
|
|
8835
|
+
pgSchemaDiffEndTime: Date.now(),
|
|
8836
|
+
retryAttempts: output.retryAttempts ?? 0
|
|
8837
|
+
};
|
|
8838
|
+
}),
|
|
8213
8839
|
releaseAdvisoryLockOnFailure: ({ context }) => {
|
|
8214
8840
|
if (context.lockAcquired) {
|
|
8215
8841
|
try {
|
|
@@ -8251,7 +8877,9 @@ var dbApplyMachine = setup({
|
|
|
8251
8877
|
error: null,
|
|
8252
8878
|
planSql: null,
|
|
8253
8879
|
filteredPlanSql: null,
|
|
8880
|
+
planSummary: null,
|
|
8254
8881
|
ssotWarning: null,
|
|
8882
|
+
nonCriticalWarnings: [],
|
|
8255
8883
|
// Idempotent preview (check mode)
|
|
8256
8884
|
idempotentFiles: [],
|
|
8257
8885
|
idempotentRisks: null,
|
|
@@ -8360,30 +8988,18 @@ var dbApplyMachine = setup({
|
|
|
8360
8988
|
{
|
|
8361
8989
|
guard: ({ context }) => context.input.check === true,
|
|
8362
8990
|
target: "done",
|
|
8363
|
-
actions:
|
|
8364
|
-
schemaChangesApplied: ({ event }) => event.output.applied,
|
|
8365
|
-
dataViolations: ({ event }) => event.output.dataViolations ?? 0,
|
|
8366
|
-
hazards: ({ event }) => event.output.hazards,
|
|
8367
|
-
planSql: ({ event }) => event.output.sql ?? null,
|
|
8368
|
-
filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
|
|
8369
|
-
ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
|
|
8370
|
-
pgSchemaDiffEndTime: () => Date.now(),
|
|
8371
|
-
retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
|
|
8372
|
-
})
|
|
8991
|
+
actions: "assignPgSchemaDiffResult"
|
|
8373
8992
|
},
|
|
8374
|
-
// Normal mode:
|
|
8993
|
+
// Normal mode: skip 2nd pass when no files were deferred in 1st pass
|
|
8994
|
+
{
|
|
8995
|
+
guard: ({ context }) => context.idempotentPreSkipped === 0,
|
|
8996
|
+
target: "validatingPartitions",
|
|
8997
|
+
actions: "assignPgSchemaDiffResult"
|
|
8998
|
+
},
|
|
8999
|
+
// Normal mode: run 2nd pass when deferred files exist
|
|
8375
9000
|
{
|
|
8376
9001
|
target: "applyingIdempotentPost",
|
|
8377
|
-
actions:
|
|
8378
|
-
schemaChangesApplied: ({ event }) => event.output.applied,
|
|
8379
|
-
dataViolations: ({ event }) => event.output.dataViolations ?? 0,
|
|
8380
|
-
hazards: ({ event }) => event.output.hazards,
|
|
8381
|
-
planSql: ({ event }) => event.output.sql ?? null,
|
|
8382
|
-
filteredPlanSql: ({ event }) => event.output.filteredPlanSql ?? null,
|
|
8383
|
-
ssotWarning: ({ event }) => event.output.ssotWarning ?? null,
|
|
8384
|
-
pgSchemaDiffEndTime: () => Date.now(),
|
|
8385
|
-
retryAttempts: ({ event }) => event.output.retryAttempts ?? 0
|
|
8386
|
-
})
|
|
9002
|
+
actions: "assignPgSchemaDiffResult"
|
|
8387
9003
|
}
|
|
8388
9004
|
],
|
|
8389
9005
|
onError: {
|
|
@@ -8471,6 +9087,12 @@ var dbApplyMachine = setup({
|
|
|
8471
9087
|
target: "done",
|
|
8472
9088
|
actions: assign({
|
|
8473
9089
|
seedsApplied: ({ event }) => event.output.applied,
|
|
9090
|
+
nonCriticalWarnings: ({ context, event }) => [
|
|
9091
|
+
...context.nonCriticalWarnings,
|
|
9092
|
+
...event.output.warnings.map(
|
|
9093
|
+
(warning) => createMachineWarning("TABLES_MANIFEST_WARNING", warning, "applyingSeeds")
|
|
9094
|
+
)
|
|
9095
|
+
],
|
|
8474
9096
|
seedEndTime: () => Date.now()
|
|
8475
9097
|
})
|
|
8476
9098
|
},
|
|
@@ -8674,7 +9296,7 @@ function createWarning(code, message, phase) {
|
|
|
8674
9296
|
return { code, message, phase };
|
|
8675
9297
|
}
|
|
8676
9298
|
function buildDbApplyOutcome(params) {
|
|
8677
|
-
const warnings = [];
|
|
9299
|
+
const warnings = [...params.result.outcome.warnings];
|
|
8678
9300
|
const phases = [...params.phases];
|
|
8679
9301
|
if (params.result.partitionWarnings && params.result.partitionWarnings.length > 0) {
|
|
8680
9302
|
const warningList = params.result.partitionWarnings.map(
|
|
@@ -8700,9 +9322,20 @@ function buildDbApplyOutcome(params) {
|
|
|
8700
9322
|
);
|
|
8701
9323
|
warnings.push(seedWarning);
|
|
8702
9324
|
seedPhase.status = "warning";
|
|
8703
|
-
seedPhase.warningCount = 1;
|
|
8704
|
-
seedPhase.warnings = [seedWarning];
|
|
9325
|
+
seedPhase.warningCount = (seedPhase.warningCount ?? 0) + 1;
|
|
9326
|
+
seedPhase.warnings = [...seedPhase.warnings ?? [], seedWarning];
|
|
9327
|
+
}
|
|
9328
|
+
}
|
|
9329
|
+
for (const warning of params.result.outcome.warnings) {
|
|
9330
|
+
const target = phases.find((phase) => phase.id === warning.phase);
|
|
9331
|
+
if (!target) {
|
|
9332
|
+
continue;
|
|
8705
9333
|
}
|
|
9334
|
+
if (target.status === "passed") {
|
|
9335
|
+
target.status = "warning";
|
|
9336
|
+
}
|
|
9337
|
+
target.warningCount = (target.warningCount ?? 0) + 1;
|
|
9338
|
+
target.warnings = [...target.warnings ?? [], warning];
|
|
8706
9339
|
}
|
|
8707
9340
|
const errors = phases.map((phase) => phase.error).filter((value) => value != null);
|
|
8708
9341
|
return {
|
|
@@ -8757,6 +9390,7 @@ function describeSeedStatus(env, _noSeed, seedFlag) {
|
|
|
8757
9390
|
async function runDbApply(env, options) {
|
|
8758
9391
|
const logger17 = createCLILogger("db:apply");
|
|
8759
9392
|
const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
|
|
9393
|
+
loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
|
|
8760
9394
|
const noSeed = resolveNoSeed(resolvedEnv, options.seed);
|
|
8761
9395
|
const compareOnly = options.check === true && options.compareOnly === true;
|
|
8762
9396
|
if (resolvedEnv === "production" && !noSeed) {
|
|
@@ -8861,6 +9495,11 @@ function printCheckSummary(logger17, result) {
|
|
|
8861
9495
|
logger17.info(` Result: ${result.outcome.exitMode}`);
|
|
8862
9496
|
logger17.info(` \u2713 Idempotent schemas: ${result.idempotentSchemasApplied} files`);
|
|
8863
9497
|
logger17.info(` \u2713 Schema changes: ${result.planSql ? "detected" : "none"}`);
|
|
9498
|
+
if (result.planSummary) {
|
|
9499
|
+
logger17.info(
|
|
9500
|
+
` \u2713 Plan summary: ${result.planSummary.rawStatements} raw / ${result.planSummary.effectiveStatements} structural / ${result.planSummary.noiseStatements} collapsed noise`
|
|
9501
|
+
);
|
|
9502
|
+
}
|
|
8864
9503
|
if (result.hazards.length > 0) logger17.info(` \u26A0 Hazards: ${result.hazards.join(", ")}`);
|
|
8865
9504
|
logger17.info(" \u2139 No changes were applied (check mode)");
|
|
8866
9505
|
}
|
|
@@ -8887,6 +9526,12 @@ function printApplySummary(logger17, result) {
|
|
|
8887
9526
|
logger17.info(` \u26A0 Warnings: ${result.outcome.summary.warnings}`);
|
|
8888
9527
|
}
|
|
8889
9528
|
if (result.metrics) logger17.info(buildMetricsString(result.metrics));
|
|
9529
|
+
if (result.outcome.warnings.length > 0) {
|
|
9530
|
+
logger17.warn("\nNon-critical warnings:");
|
|
9531
|
+
for (const warning of result.outcome.warnings) {
|
|
9532
|
+
logger17.info(` \u2022 [${warning.phase}] ${warning.message}`);
|
|
9533
|
+
}
|
|
9534
|
+
}
|
|
8890
9535
|
}
|
|
8891
9536
|
function printSummary(logger17, result) {
|
|
8892
9537
|
if (result.error) {
|
|
@@ -8919,6 +9564,7 @@ var applyCommand = new Command("apply").description("Apply schema changes to any
|
|
|
8919
9564
|
).option("--fresh-db-check-sql <sql>", "Custom SQL to check if DB is fresh").action(async (env, options) => {
|
|
8920
9565
|
const logger17 = createCLILogger("db:apply");
|
|
8921
9566
|
const resolvedEnv = env === "preview" ? "preview" : env === "production" ? "production" : "local";
|
|
9567
|
+
loadEnvFiles({ runaEnv: resolvedEnv, silent: true });
|
|
8922
9568
|
try {
|
|
8923
9569
|
const noSeed = resolveNoSeed(env, options.seed);
|
|
8924
9570
|
const compareOnly = options.check === true && options.compareOnly === true;
|
|
@@ -10870,6 +11516,32 @@ async function runDeclarativeDependencyCheck(result, logger17, step, strictOptio
|
|
|
10870
11516
|
}
|
|
10871
11517
|
}
|
|
10872
11518
|
|
|
11519
|
+
// src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
|
|
11520
|
+
init_esm_shims();
|
|
11521
|
+
async function runDuplicateFunctionOwnershipCheck(result, logger17, step) {
|
|
11522
|
+
logger17.step("Checking duplicate function ownership", step.next());
|
|
11523
|
+
const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
|
|
11524
|
+
if (analysis.findings.length === 0) {
|
|
11525
|
+
logger17.success("No duplicate declarative/idempotent function ownership detected");
|
|
11526
|
+
return;
|
|
11527
|
+
}
|
|
11528
|
+
result.passed = false;
|
|
11529
|
+
result.errors.push(`Found ${analysis.findings.length} duplicate function ownership finding(s)`);
|
|
11530
|
+
logger17.error(`Found ${analysis.findings.length} duplicate function ownership finding(s):`);
|
|
11531
|
+
logger17.info(` ${analysis.contractNote}`);
|
|
11532
|
+
for (const finding of analysis.findings) {
|
|
11533
|
+
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
11534
|
+
logger17.info(` [ERROR] ${formatted.summary}`);
|
|
11535
|
+
for (const location of formatted.declarativeLocations) {
|
|
11536
|
+
logger17.info(` declarative: ${location}`);
|
|
11537
|
+
}
|
|
11538
|
+
for (const location of formatted.idempotentLocations) {
|
|
11539
|
+
logger17.info(` idempotent: ${location}`);
|
|
11540
|
+
}
|
|
11541
|
+
logger17.info(` ${formatted.suggestion}`);
|
|
11542
|
+
}
|
|
11543
|
+
}
|
|
11544
|
+
|
|
10873
11545
|
// src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
|
|
10874
11546
|
init_esm_shims();
|
|
10875
11547
|
|
|
@@ -12561,6 +13233,7 @@ async function runPreflightChecks(env, strict = false) {
|
|
|
12561
13233
|
await runSqlSchemaRiskCheck(result, logger17, step);
|
|
12562
13234
|
await runSchemaBoundaryPlacementCheck(result, logger17, step, strict);
|
|
12563
13235
|
await runIdempotentSchemaRiskCheck(result, logger17, step, strict);
|
|
13236
|
+
await runDuplicateFunctionOwnershipCheck(result, logger17, step);
|
|
12564
13237
|
await runDeclarativeDependencyCheck(result, logger17, step, strict);
|
|
12565
13238
|
await runDomainNamingCheck(result, logger17, step);
|
|
12566
13239
|
logSummary(result, logger17);
|
|
@@ -16585,9 +17258,20 @@ async function collectLocalPrecheckBundle(strict) {
|
|
|
16585
17258
|
const adjustedIdempotentRisks = applyStrictModeToReport(idempotentRisks, strict);
|
|
16586
17259
|
const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
|
|
16587
17260
|
const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
|
|
16588
|
-
const
|
|
16589
|
-
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);
|
|
16590
17273
|
const localBlockers = [
|
|
17274
|
+
...duplicateOwnershipBlockers,
|
|
16591
17275
|
...adjustedPlacementRisks.blockers,
|
|
16592
17276
|
...adjustedDeclarativeRisks.blockers,
|
|
16593
17277
|
...adjustedIdempotentRisks.blockers,
|
|
@@ -16602,6 +17286,7 @@ async function collectLocalPrecheckBundle(strict) {
|
|
|
16602
17286
|
adjustedIdempotentRisks,
|
|
16603
17287
|
adjustedPlacementRisks,
|
|
16604
17288
|
adjustedExtensionRisks,
|
|
17289
|
+
duplicateOwnershipBlockers,
|
|
16605
17290
|
hasLocalBlockers,
|
|
16606
17291
|
hasLocalFindings,
|
|
16607
17292
|
localBlockers
|
|
@@ -16616,6 +17301,12 @@ ${heading}`);
|
|
|
16616
17301
|
}
|
|
16617
17302
|
}
|
|
16618
17303
|
function printLocalFindingCompactSummary(logger17, local, topLimit) {
|
|
17304
|
+
printCompactSummary(
|
|
17305
|
+
logger17,
|
|
17306
|
+
"Duplicate function ownership summary",
|
|
17307
|
+
local.duplicateOwnershipBlockers,
|
|
17308
|
+
topLimit
|
|
17309
|
+
);
|
|
16619
17310
|
printCompactSummary(
|
|
16620
17311
|
logger17,
|
|
16621
17312
|
"Declarative risk summary (declarative/*.sql)",
|
|
@@ -16642,6 +17333,11 @@ function printLocalFindingCompactSummary(logger17, local, topLimit) {
|
|
|
16642
17333
|
);
|
|
16643
17334
|
}
|
|
16644
17335
|
function printLocalFindingDetailedReport(logger17, local) {
|
|
17336
|
+
logFindingSection(
|
|
17337
|
+
logger17,
|
|
17338
|
+
"Duplicate function ownership blockers:",
|
|
17339
|
+
local.duplicateOwnershipBlockers
|
|
17340
|
+
);
|
|
16645
17341
|
logFindingSection(
|
|
16646
17342
|
logger17,
|
|
16647
17343
|
"Risk checks on supabase/schemas/declarative/*.sql:",
|
|
@@ -16797,6 +17493,28 @@ function assertNoProductionApplyRiskReasons(result) {
|
|
|
16797
17493
|
]
|
|
16798
17494
|
);
|
|
16799
17495
|
}
|
|
17496
|
+
function assertProductionPlanSizeBudget(logger17, result) {
|
|
17497
|
+
const assessment = assessPlanSize(result.planSummary);
|
|
17498
|
+
if (!result.planSummary || assessment.severity === "ok") {
|
|
17499
|
+
return;
|
|
17500
|
+
}
|
|
17501
|
+
if (assessment.severity === "warning") {
|
|
17502
|
+
logger17.warn(`Large production apply preview: ${formatPlanSizeSummary(result.planSummary)}`);
|
|
17503
|
+
for (const reason of assessment.reasons) {
|
|
17504
|
+
logger17.info(` ${reason}`);
|
|
17505
|
+
}
|
|
17506
|
+
return;
|
|
17507
|
+
}
|
|
17508
|
+
throw new CLIError(
|
|
17509
|
+
"Production apply preview exceeds plan size budget",
|
|
17510
|
+
"DB_PRODUCTION_APPLY_PLAN_SIZE",
|
|
17511
|
+
[
|
|
17512
|
+
`Plan summary: ${formatPlanSizeSummary(result.planSummary)}`,
|
|
17513
|
+
...assessment.reasons,
|
|
17514
|
+
"Reduce metadata churn or split the schema change before production apply"
|
|
17515
|
+
]
|
|
17516
|
+
);
|
|
17517
|
+
}
|
|
16800
17518
|
function collectProductionApplyRiskReasons(output) {
|
|
16801
17519
|
const reasons = [];
|
|
16802
17520
|
if (output.dataViolations && output.dataViolations > 0) {
|
|
@@ -16853,6 +17571,7 @@ async function runProductionApplyDryRunCheck(logger17, strict) {
|
|
|
16853
17571
|
placement: local.placementRisks.allowlist.length,
|
|
16854
17572
|
plan: plan.planBoundaryRisks.allowlist.length
|
|
16855
17573
|
});
|
|
17574
|
+
assertProductionPlanSizeBudget(logger17, plan.result);
|
|
16856
17575
|
assertNoPlanBoundaryBlockers(logger17, plan.adjustedPlanBoundaryRisks, strict);
|
|
16857
17576
|
assertNoProductionApplyRiskReasons(plan.result);
|
|
16858
17577
|
}
|