@runa-ai/runa-cli 0.9.0 → 0.10.1
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-ZWDWFMOX.js → chunk-HWR5NUUZ.js} +24 -3
- package/dist/{chunk-JQXOVCOP.js → chunk-NIS77243.js} +8 -5
- package/dist/{chunk-URWDB7YL.js → chunk-O3M7A73M.js} +58 -2
- package/dist/{chunk-YRNQEJQW.js → chunk-XRLIZKB2.js} +80 -12
- package/dist/{chunk-IEKYTCYA.js → chunk-YTQS2O4H.js} +59 -0
- package/dist/{chunk-GKBE7EIE.js → chunk-ZPE52NEK.js} +1 -1
- package/dist/{ci-S5KSBECX.js → ci-3HZWUQFN.js} +4 -4
- package/dist/{cli-TJZCAMB2.js → cli-RES5QRC2.js} +13 -13
- package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +6 -0
- package/dist/commands/db/commands/db-sync/production-precheck.d.ts +0 -8
- package/dist/commands/db/sync/schema-guardrail-graph-guidance.d.ts +18 -1
- package/dist/commands/db/sync/schema-guardrail-graph-nodes.d.ts +1 -1
- package/dist/commands/db/sync/schema-guardrail-graph-sql-helpers.d.ts +1 -1
- package/dist/commands/db/sync/schema-guardrail-types.d.ts +4 -2
- package/dist/commands/db/utils/changed-files-detector.d.ts +21 -0
- package/dist/commands/db/utils/schema-sync.d.ts +12 -0
- package/dist/commands/db/utils/sql-boundary-parser.d.ts +13 -0
- package/dist/commands/db/utils/sql-file-collector.d.ts +2 -0
- package/dist/constants/versions.d.ts +9 -0
- package/dist/{db-D2OLJDYW.js → db-PRGL7PBX.js} +587 -76
- package/dist/{dev-LGSMDFJN.js → dev-QR55VDNZ.js} +1 -1
- package/dist/{error-handler-YRQWRDEF.js → error-handler-XUQOP4TU.js} +1 -2
- package/dist/{hotfix-RJIAPLAM.js → hotfix-JYHDY2M6.js} +1 -2
- package/dist/index.js +4 -4
- package/dist/{init-2O6ODG5Z.js → init-4UAWYY75.js} +1 -1
- package/dist/{license-OB7GVJQ2.js → license-M6ODBV4X.js} +140 -154
- package/dist/pg-schema-diff-helpers-JZO4GAQG.js +7 -0
- package/dist/{template-check-BDFMT6ZO.js → template-check-VNNQQXCX.js} +10 -0
- package/dist/{upgrade-QZKEI3NJ.js → upgrade-LBO3Z3J7.js} +1 -1
- package/dist/utils/license/index.d.ts +15 -24
- package/dist/utils/license/types.d.ts +3 -4
- package/dist/utils/template-access.d.ts +20 -0
- package/dist/utils/template-fetcher.d.ts +10 -7
- package/dist/{vuln-check-5NUTETPW.js → vuln-check-5JJ2YAJW.js} +1 -1
- package/dist/{vuln-checker-UV342N66.js → vuln-checker-JF5234BL.js} +1 -1
- package/package.json +3 -3
- package/dist/chunk-ZZOXM6Q4.js +0 -8
- package/dist/pg-schema-diff-helpers-7377FS2D.js +0 -7
|
@@ -2,22 +2,21 @@
|
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { detectDatabaseStack, getStackPaths } from './chunk-MILCC3B6.js';
|
|
4
4
|
import { categorizeRisks, detectSchemaRisks } from './chunk-PAWNJA3N.js';
|
|
5
|
-
import { isExecaError, resolveDbPreviewEnvironment, buildDbPlanCommandLabel, runDbApply, buildDbApplyCliError, DbPlanOutputSchema, parseDbPreviewProfile, DEFAULT_DB_PREVIEW_PROFILE, getDbPreviewModeLabel, buildDbPreviewCommandLabel, isCompareOnlyPreviewProfile, DbApplyOutputSchema, applyCommand, getDbPreviewIdempotentSchemaCount, classifyDbSyncCommandFailure, getDbSyncFallbackSuggestions, detectAppSchemas, normalizeDatabaseUrlForDdl, analyzeDuplicateFunctionOwnership, formatDuplicateFunctionOwnershipFinding, reviewDeclarativeDependencyWarnings, logDeclarativeDependencyWarnings, buildDeclarativeDependencyWarningFailureLines, parsePlanOutput, validateDependencyOrder, getBoundaryPolicy, resolveProductionApplyStrictMode, findDeclarativeRiskAllowlistMatch, assertBoundaryPolicyUsable, assertBoundaryPolicyQualityGate, formatSchemasForSql, findDirectoryPlacementAllowlistMatch, formatAllowlistMetadata, assessPlanSize, formatPlanSizeSummary, extractFunctionOwnershipDefinition } from './chunk-
|
|
6
|
-
import './chunk-
|
|
7
|
-
import { createError } from './chunk-JQXOVCOP.js';
|
|
5
|
+
import { isExecaError, resolveDbPreviewEnvironment, buildDbPlanCommandLabel, runDbApply, buildDbApplyCliError, DbPlanOutputSchema, parseDbPreviewProfile, DEFAULT_DB_PREVIEW_PROFILE, getDbPreviewModeLabel, buildDbPreviewCommandLabel, isCompareOnlyPreviewProfile, DbApplyOutputSchema, applyCommand, getDbPreviewIdempotentSchemaCount, classifyDbSyncCommandFailure, getDbSyncFallbackSuggestions, detectAppSchemas, normalizeDatabaseUrlForDdl, analyzeDuplicateFunctionOwnership, formatDuplicateFunctionOwnershipFinding, reviewDeclarativeDependencyWarnings, logDeclarativeDependencyWarnings, buildDeclarativeDependencyWarningFailureLines, parsePlanOutput, validateDependencyOrder, getBoundaryPolicy, resolveProductionApplyStrictMode, findDeclarativeRiskAllowlistMatch, assertBoundaryPolicyUsable, assertBoundaryPolicyQualityGate, formatSchemasForSql, findDirectoryPlacementAllowlistMatch, formatAllowlistMetadata, assessPlanSize, formatPlanSizeSummary, extractFunctionOwnershipDefinition } from './chunk-XRLIZKB2.js';
|
|
6
|
+
import { createError } from './chunk-NIS77243.js';
|
|
8
7
|
import { resolveDatabaseUrl, resolveDatabaseTarget, tryResolveDatabaseUrl } from './chunk-WGRVAGSR.js';
|
|
9
8
|
export { resolveDatabaseUrl, tryResolveDatabaseUrl } from './chunk-WGRVAGSR.js';
|
|
10
|
-
import { analyzeDeclarativeDependencyContract, formatDeclarativeDependencyViolation, parseSqlFilename, collectSqlFiles, splitSqlStatements, extractFirstDollarBody, sanitizeExecutableCode, FUNCTION_DEFINITION_RE, blankQuotedStrings, sanitizeExecutableCodePreserveStrings, countNewlines, shouldReviewUnknownDeclarativeDdl, extractDdlObject, isNonSchemaOperation, isNonDdlMaintenanceStatement, shouldReviewUnknownIdempotentDdl } from './chunk-
|
|
9
|
+
import { analyzeDeclarativeDependencyContract, formatDeclarativeDependencyViolation, parseSqlFilename, collectSqlFiles, splitSqlStatements, ALLOW_DYNAMIC_SQL_ANNOTATION, extractFirstDollarBody, sanitizeExecutableCode, detectExtensionFilePath, FUNCTION_DEFINITION_RE, blankQuotedStrings, sanitizeExecutableCodePreserveStrings, countNewlines, shouldReviewUnknownDeclarativeDdl, extractDdlObject, isNonSchemaOperation, isNonDdlMaintenanceStatement, shouldReviewUnknownIdempotentDdl } from './chunk-HWR5NUUZ.js';
|
|
11
10
|
import './chunk-UHDAYPHH.js';
|
|
12
11
|
import './chunk-Y5ANTCKE.js';
|
|
13
12
|
import { loadEnvFiles } from './chunk-IWVXI5O4.js';
|
|
14
|
-
import './chunk-
|
|
13
|
+
import './chunk-ZPE52NEK.js';
|
|
15
14
|
import { diagnoseSupabaseStart } from './chunk-AAIE4F2U.js';
|
|
16
15
|
import { validateUserFilePath, filterSafePaths, resolveSafePath } from './chunk-B7C7CLW2.js';
|
|
17
16
|
import { runMachine } from './chunk-QDF7QXBL.js';
|
|
18
17
|
import './chunk-XVNDDHAF.js';
|
|
19
18
|
import { writeEnvLocalBridge, removeEnvLocalBridge } from './chunk-KUH3G522.js';
|
|
20
|
-
import { extractSchemaTablesAndEnums, fetchDbTablesAndEnums, extractTablesFromIdempotentSql, diffSchema, getSqlParserUtils, buildTablePatternMatcher } from './chunk-
|
|
19
|
+
import { extractSchemaTablesAndEnums, fetchDbTablesAndEnums, extractTablesFromIdempotentSql, extractDynamicTablePatternsFromIdempotentSql, diffSchema, getSqlParserUtils, buildTablePatternMatcher } from './chunk-O3M7A73M.js';
|
|
21
20
|
import { psqlExec, psqlQuery, blankDollarQuotedBodies, stripSqlComments, parsePostgresUrl, buildPsqlArgs, buildPsqlEnv } from './chunk-A6A7JIRD.js';
|
|
22
21
|
import { redactSecrets } from './chunk-II7VYQEM.js';
|
|
23
22
|
import { init_local_supabase, init_constants, detectLocalSupabasePorts, buildLocalDatabaseUrl, DATABASE_DEFAULTS, SEED_DEFAULTS, SCRIPT_LOCATIONS } from './chunk-QSEF4T3Y.js';
|
|
@@ -441,9 +440,10 @@ async function runCleanupAction(env, options) {
|
|
|
441
440
|
} catch {
|
|
442
441
|
}
|
|
443
442
|
const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
|
|
444
|
-
|
|
443
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
|
|
444
|
+
if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
|
|
445
445
|
excludeFromOrphanDetection = [
|
|
446
|
-
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
|
|
446
|
+
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
|
|
447
447
|
];
|
|
448
448
|
}
|
|
449
449
|
const diff = diffSchema({
|
|
@@ -1511,14 +1511,30 @@ function buildGuardrailConflictEntries(report, entries) {
|
|
|
1511
1511
|
}
|
|
1512
1512
|
function buildGuardrailHeaderEntries(report, entries) {
|
|
1513
1513
|
if (report.staleBlocks.length > 0) {
|
|
1514
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1515
|
+
for (const block of report.staleBlocks) {
|
|
1516
|
+
const kinds = byFile.get(block.file) ?? [];
|
|
1517
|
+
kinds.push(block.kind === "file-header" ? "file metadata" : `table: ${block.target}`);
|
|
1518
|
+
byFile.set(block.file, kinds);
|
|
1519
|
+
}
|
|
1514
1520
|
entries.push({
|
|
1515
1521
|
level: "warn",
|
|
1516
|
-
message: `Generated headers are stale
|
|
1522
|
+
message: `Generated headers are stale (${report.staleBlocks.length} block(s) in ${byFile.size} file(s)):`
|
|
1517
1523
|
});
|
|
1524
|
+
for (const [file, kinds] of byFile) {
|
|
1525
|
+
entries.push({
|
|
1526
|
+
level: "info",
|
|
1527
|
+
message: ` ${file}: ${kinds.join(", ")}`
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1518
1530
|
if (!report.failure) {
|
|
1519
1531
|
entries.push({
|
|
1520
1532
|
level: "info",
|
|
1521
|
-
message: "Run `runa db sync` to
|
|
1533
|
+
message: "Auto-fix: Run `runa db sync` to regenerate all headers automatically."
|
|
1534
|
+
});
|
|
1535
|
+
entries.push({
|
|
1536
|
+
level: "info",
|
|
1537
|
+
message: "Headers track: FK references, RLS policies, triggers, function ownership, and schema dependencies."
|
|
1522
1538
|
});
|
|
1523
1539
|
}
|
|
1524
1540
|
}
|
|
@@ -1885,10 +1901,54 @@ function reportMediumRisks(result, logger4, mediumRisks) {
|
|
|
1885
1901
|
logger4.info(` \u2022 ${summary}`);
|
|
1886
1902
|
}
|
|
1887
1903
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1904
|
+
var SHOW_LOW_RISKS = process.env.RUNA_DB_SHOW_LOW_RISKS === "1";
|
|
1905
|
+
function reportLowRisks(logger4, lowRisks) {
|
|
1906
|
+
if (lowRisks.length === 0) return;
|
|
1907
|
+
if (SHOW_LOW_RISKS) {
|
|
1908
|
+
logger4.info(` Found ${lowRisks.length} LOW risk suggestion(s):`);
|
|
1909
|
+
const summaries = summarizeRisks(lowRisks);
|
|
1910
|
+
for (const summary of summaries) {
|
|
1911
|
+
logger4.info(` ${summary.replace("[MEDIUM]", "[LOW]")}`);
|
|
1912
|
+
}
|
|
1913
|
+
} else {
|
|
1914
|
+
logger4.info(
|
|
1915
|
+
` Found ${lowRisks.length} LOW risk suggestion(s) (informational; set RUNA_DB_SHOW_LOW_RISKS=1 to see details)`
|
|
1916
|
+
);
|
|
1891
1917
|
}
|
|
1918
|
+
}
|
|
1919
|
+
function writeRiskJson(allRisks, categorized) {
|
|
1920
|
+
if (!SHOW_LOW_RISKS) return;
|
|
1921
|
+
const outputDir = path12.join(process.cwd(), ".runa", "tmp");
|
|
1922
|
+
const outputPath = path12.join(outputDir, "schema-risks.json");
|
|
1923
|
+
try {
|
|
1924
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1925
|
+
const json = JSON.stringify(
|
|
1926
|
+
{
|
|
1927
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1928
|
+
summary: {
|
|
1929
|
+
high: categorized.high.length,
|
|
1930
|
+
medium: categorized.medium.length,
|
|
1931
|
+
low: categorized.low.length,
|
|
1932
|
+
total: allRisks.length
|
|
1933
|
+
},
|
|
1934
|
+
risks: allRisks.map((risk) => ({
|
|
1935
|
+
level: risk.level,
|
|
1936
|
+
file: risk.file,
|
|
1937
|
+
line: risk.line,
|
|
1938
|
+
description: risk.description,
|
|
1939
|
+
mitigation: risk.mitigation,
|
|
1940
|
+
reasonCode: risk.reasonCode
|
|
1941
|
+
}))
|
|
1942
|
+
},
|
|
1943
|
+
null,
|
|
1944
|
+
2
|
|
1945
|
+
);
|
|
1946
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function reportRiskGuidance(logger4, highRiskCount, lowRisks) {
|
|
1951
|
+
reportLowRisks(logger4, lowRisks);
|
|
1892
1952
|
if (highRiskCount > 0) {
|
|
1893
1953
|
logger4.info("");
|
|
1894
1954
|
logger4.info(" AGENTS.md requires Supabase Auth Schema Independence:");
|
|
@@ -1914,7 +1974,8 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
|
|
|
1914
1974
|
const categorized = categorizeRisks(applySyncPreflightRiskPolicy(allRisks));
|
|
1915
1975
|
reportHighRisks(result, logger4, categorized.high);
|
|
1916
1976
|
reportMediumRisks(result, logger4, categorized.medium);
|
|
1917
|
-
reportRiskGuidance(logger4, categorized.high.length, categorized.low
|
|
1977
|
+
reportRiskGuidance(logger4, categorized.high.length, categorized.low);
|
|
1978
|
+
writeRiskJson(allRisks, categorized);
|
|
1918
1979
|
} catch (error) {
|
|
1919
1980
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1920
1981
|
logger4.warn(`SQL schema risk check skipped: ${message}`);
|
|
@@ -1971,9 +2032,10 @@ async function runOrphanCheck(env, dbPackagePath, result, logger4, step) {
|
|
|
1971
2032
|
} catch {
|
|
1972
2033
|
}
|
|
1973
2034
|
const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
|
|
1974
|
-
|
|
2035
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
|
|
2036
|
+
if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
|
|
1975
2037
|
excludeFromOrphanDetection = [
|
|
1976
|
-
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
|
|
2038
|
+
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
|
|
1977
2039
|
];
|
|
1978
2040
|
}
|
|
1979
2041
|
const diff = diffSchema({
|
|
@@ -2724,26 +2786,45 @@ function shouldAbortSchemaPrecheckForBudget(state, filePath) {
|
|
|
2724
2786
|
|
|
2725
2787
|
// src/commands/db/utils/sql-file-collector.ts
|
|
2726
2788
|
init_esm_shims();
|
|
2789
|
+
var IGNORED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
2790
|
+
"compat",
|
|
2791
|
+
"archive",
|
|
2792
|
+
"legacy",
|
|
2793
|
+
"deprecated",
|
|
2794
|
+
"backup",
|
|
2795
|
+
"node_modules",
|
|
2796
|
+
".git",
|
|
2797
|
+
"dist",
|
|
2798
|
+
"build"
|
|
2799
|
+
]);
|
|
2800
|
+
function shouldSkipDirectory(name) {
|
|
2801
|
+
return IGNORED_DIRECTORY_NAMES.has(name.toLowerCase());
|
|
2802
|
+
}
|
|
2803
|
+
function scanDirectory(dir, queue, sqlFiles) {
|
|
2804
|
+
try {
|
|
2805
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
2806
|
+
for (const entry of entries) {
|
|
2807
|
+
const fullPath = path12.join(dir, entry.name);
|
|
2808
|
+
if (entry.isDirectory()) {
|
|
2809
|
+
if (!shouldSkipDirectory(entry.name)) queue.push(fullPath);
|
|
2810
|
+
} else if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
2811
|
+
sqlFiles.push(fullPath);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
} catch {
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2727
2817
|
function* collectSqlFilesRecursively(baseDir) {
|
|
2728
2818
|
const queue = [baseDir];
|
|
2819
|
+
const sqlFiles = [];
|
|
2729
2820
|
let index = 0;
|
|
2730
2821
|
while (index < queue.length) {
|
|
2731
2822
|
const currentDir = queue[index++];
|
|
2732
2823
|
if (!currentDir) continue;
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
if (entry.isDirectory()) {
|
|
2738
|
-
queue.push(fullPath);
|
|
2739
|
-
continue;
|
|
2740
|
-
}
|
|
2741
|
-
if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
2742
|
-
yield fullPath;
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
} catch {
|
|
2746
|
-
}
|
|
2824
|
+
scanDirectory(currentDir, queue, sqlFiles);
|
|
2825
|
+
}
|
|
2826
|
+
for (const file of sqlFiles) {
|
|
2827
|
+
yield file;
|
|
2747
2828
|
}
|
|
2748
2829
|
}
|
|
2749
2830
|
|
|
@@ -3557,12 +3638,18 @@ function classifyIdempotentMisplacementRisk(file, content, boundaryPolicy) {
|
|
|
3557
3638
|
skipUnknown: (statement, unknownObject) => isPartitionOfCreateTable(statement) && unknownObject === "TABLE"
|
|
3558
3639
|
});
|
|
3559
3640
|
}
|
|
3641
|
+
var DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS = [
|
|
3642
|
+
"Function DDL should be in declarative",
|
|
3643
|
+
"Trigger DDL should be in declarative"
|
|
3644
|
+
];
|
|
3645
|
+
var DROP_IF_EXISTS_CLEANUP = /^\s*DROP\s+(?:FUNCTION|TRIGGER|VIEW|INDEX|POLICY|TYPE|SEQUENCE)\s+IF\s+EXISTS\b/i;
|
|
3560
3646
|
function classifyFileMisplacementRisks(params) {
|
|
3561
3647
|
const risks = [];
|
|
3562
3648
|
const relative2 = path12.relative(process.cwd(), params.file);
|
|
3563
3649
|
const normalized = normalizeSqlForPlacementCheck(params.content);
|
|
3564
3650
|
const statements = splitSqlStatements(normalized);
|
|
3565
3651
|
const seenMessages = /* @__PURE__ */ new Set();
|
|
3652
|
+
const hasAllowDynamicSqlAnnotation = params.fileType === "idempotent" && ALLOW_DYNAMIC_SQL_ANNOTATION.test(params.content);
|
|
3566
3653
|
for (const { statement, line } of statements) {
|
|
3567
3654
|
const candidates = collectRuleBasedCandidates({
|
|
3568
3655
|
statement,
|
|
@@ -3584,6 +3671,14 @@ function classifyFileMisplacementRisks(params) {
|
|
|
3584
3671
|
}
|
|
3585
3672
|
const best = selectHighestPriorityRisk(candidates);
|
|
3586
3673
|
if (!best) continue;
|
|
3674
|
+
if (params.fileType === "idempotent") {
|
|
3675
|
+
if (DROP_IF_EXISTS_CLEANUP.test(statement)) {
|
|
3676
|
+
best.level = "low";
|
|
3677
|
+
}
|
|
3678
|
+
if (hasAllowDynamicSqlAnnotation && DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS.some((p) => best.message.includes(p))) {
|
|
3679
|
+
best.level = "low";
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3587
3682
|
const key = buildDirectoryRiskKey(best);
|
|
3588
3683
|
if (seenMessages.has(key)) continue;
|
|
3589
3684
|
seenMessages.add(key);
|
|
@@ -4388,9 +4483,10 @@ var reconcile = fromPromise(
|
|
|
4388
4483
|
} catch {
|
|
4389
4484
|
}
|
|
4390
4485
|
const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
|
|
4391
|
-
|
|
4486
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
|
|
4487
|
+
if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
|
|
4392
4488
|
excludeFromOrphanDetection = [
|
|
4393
|
-
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
|
|
4489
|
+
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
|
|
4394
4490
|
];
|
|
4395
4491
|
}
|
|
4396
4492
|
const diff = diffSchema({
|
|
@@ -5185,6 +5281,29 @@ function createDefaultSchemaGuardrailConfig() {
|
|
|
5185
5281
|
idempotentSqlDir: "supabase/schemas/idempotent"
|
|
5186
5282
|
};
|
|
5187
5283
|
}
|
|
5284
|
+
function loadAllowedDuplicatesFromSchemaOwnership(targetDir) {
|
|
5285
|
+
const candidates = [
|
|
5286
|
+
join(targetDir, "supabase", "schemas", "schema-ownership.json"),
|
|
5287
|
+
join(targetDir, "schema-ownership.json")
|
|
5288
|
+
];
|
|
5289
|
+
for (const filePath of candidates) {
|
|
5290
|
+
if (!existsSync(filePath)) continue;
|
|
5291
|
+
try {
|
|
5292
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
5293
|
+
const allowedDuplicates = raw?.rules?.allowed_duplicates;
|
|
5294
|
+
if (!Array.isArray(allowedDuplicates)) continue;
|
|
5295
|
+
return allowedDuplicates.filter(
|
|
5296
|
+
(entry) => typeof entry === "object" && entry !== null && "qualifiedName" in entry && typeof entry.qualifiedName === "string"
|
|
5297
|
+
).map((entry) => ({
|
|
5298
|
+
qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
|
|
5299
|
+
signature: normalizeAllowlistSignature(entry.signature ?? ""),
|
|
5300
|
+
reason: entry.reason ?? "Loaded from schema-ownership.json"
|
|
5301
|
+
}));
|
|
5302
|
+
} catch {
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
return [];
|
|
5306
|
+
}
|
|
5188
5307
|
function loadSchemaGuardrailConfig(targetDir) {
|
|
5189
5308
|
const defaults = createDefaultSchemaGuardrailConfig();
|
|
5190
5309
|
try {
|
|
@@ -5192,13 +5311,17 @@ function loadSchemaGuardrailConfig(targetDir) {
|
|
|
5192
5311
|
const databaseConfig = config.database ?? {};
|
|
5193
5312
|
return {
|
|
5194
5313
|
declarativeSqlDir: databaseConfig.schemaGuardrails?.declarativeSqlDir ?? defaults.declarativeSqlDir,
|
|
5195
|
-
allowedDuplicateFunctions:
|
|
5196
|
-
...entry
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5314
|
+
allowedDuplicateFunctions: [
|
|
5315
|
+
...databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
|
|
5316
|
+
...entry,
|
|
5317
|
+
qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
|
|
5318
|
+
signature: normalizeAllowlistSignature(entry.signature),
|
|
5319
|
+
declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
|
|
5320
|
+
idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
|
|
5321
|
+
})) ?? [],
|
|
5322
|
+
// Fallback: merge schema-ownership.json allowed_duplicates if present
|
|
5323
|
+
...loadAllowedDuplicatesFromSchemaOwnership(targetDir)
|
|
5324
|
+
],
|
|
5202
5325
|
generatedHeaderRewriteTargets: normalizeFileList(
|
|
5203
5326
|
(databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
|
|
5204
5327
|
(value) => normalizePathForMatch(value)
|
|
@@ -5266,8 +5389,13 @@ function loadExtraTableFilters(targetDir) {
|
|
|
5266
5389
|
const idempotentManagedTables = new Set(
|
|
5267
5390
|
extractTablesFromIdempotentSql(idempotentDirectory, targetDir)
|
|
5268
5391
|
);
|
|
5392
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
|
|
5393
|
+
idempotentDirectory,
|
|
5394
|
+
targetDir
|
|
5395
|
+
);
|
|
5396
|
+
const allExclusions = [...config.excludeFromOrphanDetection, ...dynamicPatterns];
|
|
5269
5397
|
return {
|
|
5270
|
-
exclusionMatcher: buildTablePatternMatcher(
|
|
5398
|
+
exclusionMatcher: buildTablePatternMatcher(allExclusions),
|
|
5271
5399
|
idempotentManagedTables
|
|
5272
5400
|
};
|
|
5273
5401
|
}
|
|
@@ -5480,24 +5608,36 @@ function collectExecuteOccurrencesFromBody(body, startLine) {
|
|
|
5480
5608
|
}
|
|
5481
5609
|
return occurrences.sort((left, right) => left.line - right.line);
|
|
5482
5610
|
}
|
|
5611
|
+
var PARTITION_INFRA_PATTERNS = [
|
|
5612
|
+
/\bPARTITION\b/i,
|
|
5613
|
+
/\bATTACH\b/i,
|
|
5614
|
+
/\bDETACH\b/i,
|
|
5615
|
+
/\bCREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i,
|
|
5616
|
+
/\bdefault\s*partition\b/i,
|
|
5617
|
+
/\brange_partition\b/i
|
|
5618
|
+
];
|
|
5619
|
+
function isPartitionInfrastructure(body) {
|
|
5620
|
+
return PARTITION_INFRA_PATTERNS.some((pattern) => pattern.test(body));
|
|
5621
|
+
}
|
|
5483
5622
|
function createDynamicSqlBlockersForStatement(params) {
|
|
5484
5623
|
const body = extractFirstDollarBody(params.statement, params.line);
|
|
5485
5624
|
if (!body) {
|
|
5486
5625
|
return [];
|
|
5487
5626
|
}
|
|
5488
5627
|
const executeOccurrences = collectExecuteOccurrencesFromBody(body.body, body.startLine);
|
|
5628
|
+
const isInfra = isPartitionInfrastructure(body.body);
|
|
5489
5629
|
return executeOccurrences.map((occurrence) => ({
|
|
5490
|
-
kind: "dynamic-sql",
|
|
5630
|
+
kind: isInfra ? "dynamic-sql-infra" : "dynamic-sql",
|
|
5491
5631
|
sourceFile: params.sourceFile,
|
|
5492
5632
|
line: occurrence.line,
|
|
5493
5633
|
target: "EXECUTE",
|
|
5494
|
-
details: occurrence.hasStaticLiteral ? "EXECUTE in SQL bodies is blocked locally. Replace it with direct guarded calls or static SQL." : "Unresolved dynamic EXECUTE is blocked locally. Replace it with explicit static SQL or explicit allowlisted statements."
|
|
5634
|
+
details: isInfra ? "Dynamic SQL detected in partition/DDL infrastructure helper. Add `-- runa:allow-dynamic-sql reason: partition-helper` to suppress." : occurrence.hasStaticLiteral ? "EXECUTE in SQL bodies is blocked locally. Replace it with direct guarded calls or static SQL." : "Unresolved dynamic EXECUTE is blocked locally. Replace it with explicit static SQL or explicit allowlisted statements."
|
|
5495
5635
|
}));
|
|
5496
5636
|
}
|
|
5497
5637
|
function buildDynamicSqlBlockers(params) {
|
|
5498
5638
|
const blockers = [];
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5639
|
+
for (const file of params.sources.declarativeFiles) {
|
|
5640
|
+
if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
|
|
5501
5641
|
for (const parsed of splitSqlStatements(file.content)) {
|
|
5502
5642
|
blockers.push(
|
|
5503
5643
|
...createDynamicSqlBlockersForStatement({
|
|
@@ -5508,6 +5648,21 @@ function buildDynamicSqlBlockers(params) {
|
|
|
5508
5648
|
);
|
|
5509
5649
|
}
|
|
5510
5650
|
}
|
|
5651
|
+
for (const file of params.sources.idempotentFiles) {
|
|
5652
|
+
if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
|
|
5653
|
+
for (const parsed of splitSqlStatements(file.content)) {
|
|
5654
|
+
const stmtBlockers = createDynamicSqlBlockersForStatement({
|
|
5655
|
+
sourceFile: file.relativePath,
|
|
5656
|
+
statement: parsed.statement,
|
|
5657
|
+
line: parsed.line
|
|
5658
|
+
});
|
|
5659
|
+
for (const blocker of stmtBlockers) {
|
|
5660
|
+
blocker.kind = "dynamic-sql-infra";
|
|
5661
|
+
blocker.details = `${blocker.details} (idempotent file \u2014 add \`-- runa:allow-dynamic-sql reason: <role>\` if this is intentional infrastructure, e.g. partition-helper, runtime-infrastructure, compatibility-bootstrap)`;
|
|
5662
|
+
}
|
|
5663
|
+
blockers.push(...stmtBlockers);
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5511
5666
|
return stableSorted2(blockers, (value) => `${value.sourceFile}:${value.line ?? 0}:${value.kind}`);
|
|
5512
5667
|
}
|
|
5513
5668
|
function buildLocalBlindSpotBlockers(params) {
|
|
@@ -5522,7 +5677,7 @@ function buildLocalBlindSpotBlockers(params) {
|
|
|
5522
5677
|
}),
|
|
5523
5678
|
...buildExtensionPlacementBlockers({
|
|
5524
5679
|
sources: params.sources,
|
|
5525
|
-
requiredFile:
|
|
5680
|
+
requiredFile: detectExtensionFilePath()
|
|
5526
5681
|
})
|
|
5527
5682
|
];
|
|
5528
5683
|
return stableSorted2(
|
|
@@ -5917,6 +6072,19 @@ function buildManagedBoundaryMetadataByFile(files) {
|
|
|
5917
6072
|
}
|
|
5918
6073
|
|
|
5919
6074
|
// src/commands/db/sync/schema-guardrail-graph-nodes.ts
|
|
6075
|
+
function extractTriggerFunctionArgs(statement) {
|
|
6076
|
+
const execMatch = statement.match(
|
|
6077
|
+
/\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"[^"]+"|[A-Za-z_]\w*)\s*\.\s*)?(?:"[^"]+"|[A-Za-z_]\w*)\s*\(([^)]*)\)/i
|
|
6078
|
+
);
|
|
6079
|
+
if (!execMatch?.[1]) return [];
|
|
6080
|
+
const argsText = execMatch[1].trim();
|
|
6081
|
+
if (!argsText) return [];
|
|
6082
|
+
return argsText.split(",").map((arg) => arg.trim()).map((arg) => {
|
|
6083
|
+
const stringMatch = arg.match(/^'([^']*)'$/);
|
|
6084
|
+
if (stringMatch) return stringMatch[1];
|
|
6085
|
+
return null;
|
|
6086
|
+
}).filter((arg) => arg !== null);
|
|
6087
|
+
}
|
|
5920
6088
|
function parseCreateTriggerStatement(statement) {
|
|
5921
6089
|
const triggerRegex = /^\s*CREATE\s+TRIGGER\s+(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s+(BEFORE|AFTER|INSTEAD\s+OF)\s+([\s\S]+?)\s+ON\s+(?:(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*\.\s*)?(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))[\s\S]*?\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*\.\s*)?(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/i;
|
|
5922
6090
|
const match = statement.match(triggerRegex);
|
|
@@ -5933,13 +6101,15 @@ function parseCreateTriggerStatement(statement) {
|
|
|
5933
6101
|
const schema = (match[5] ?? match[6] ?? "public").toLowerCase();
|
|
5934
6102
|
const functionSchema = (match[9] ?? match[10] ?? "").toLowerCase();
|
|
5935
6103
|
const functionName = (match[11] ?? match[12] ?? "").toLowerCase();
|
|
6104
|
+
const functionArgs = extractTriggerFunctionArgs(statement);
|
|
5936
6105
|
return {
|
|
5937
6106
|
qualifiedTable: `${schema}.${table}`,
|
|
5938
6107
|
trigger: {
|
|
5939
6108
|
name: triggerName,
|
|
5940
6109
|
timing,
|
|
5941
6110
|
event,
|
|
5942
|
-
functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0
|
|
6111
|
+
functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0,
|
|
6112
|
+
functionArgs: functionArgs.length > 0 ? functionArgs : void 0
|
|
5943
6113
|
}
|
|
5944
6114
|
};
|
|
5945
6115
|
}
|
|
@@ -6902,6 +7072,114 @@ function buildBoundaryGuidanceWarnings(params) {
|
|
|
6902
7072
|
(value) => `${value.sourceFile}.${value.kind}.${value.suggestedDeclarativeFile ?? ""}.${value.suggestedIdempotentFile ?? ""}.${value.target}`
|
|
6903
7073
|
);
|
|
6904
7074
|
}
|
|
7075
|
+
function extractCaseWhenBranches(functionBody) {
|
|
7076
|
+
const branches = [];
|
|
7077
|
+
const whenPattern = /\bWHEN\s+'([^']+)'/gi;
|
|
7078
|
+
for (const match of functionBody.matchAll(whenPattern)) {
|
|
7079
|
+
if (match[1]) branches.push(match[1].toLowerCase());
|
|
7080
|
+
}
|
|
7081
|
+
return [...new Set(branches)];
|
|
7082
|
+
}
|
|
7083
|
+
function escapeForRegex(value) {
|
|
7084
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7085
|
+
}
|
|
7086
|
+
function findFunctionBody(qualifiedName, sources) {
|
|
7087
|
+
const rawName = qualifiedName.includes(".") ? qualifiedName.split(".")[1] : qualifiedName;
|
|
7088
|
+
if (!rawName) return null;
|
|
7089
|
+
const escaped = escapeForRegex(rawName);
|
|
7090
|
+
const pattern = new RegExp(
|
|
7091
|
+
`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\(`,
|
|
7092
|
+
"i"
|
|
7093
|
+
);
|
|
7094
|
+
for (const file of [...sources.declarativeFiles, ...sources.idempotentFiles]) {
|
|
7095
|
+
if (!pattern.test(file.content)) continue;
|
|
7096
|
+
const bodyMatch = file.content.match(
|
|
7097
|
+
new RegExp(
|
|
7098
|
+
`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\([^)]*\\)[\\s\\S]*?\\$\\w*\\$([\\s\\S]*?)\\$\\w*\\$`,
|
|
7099
|
+
"i"
|
|
7100
|
+
)
|
|
7101
|
+
);
|
|
7102
|
+
if (bodyMatch?.[1]) return bodyMatch[1];
|
|
7103
|
+
}
|
|
7104
|
+
return null;
|
|
7105
|
+
}
|
|
7106
|
+
function buildTriggerDispatchGapWarnings(params) {
|
|
7107
|
+
const argsByFunction = collectTriggerArgsByFunction(params.tableNodes);
|
|
7108
|
+
return validateDispatchCoverage(argsByFunction, params.sources);
|
|
7109
|
+
}
|
|
7110
|
+
function collectTriggerArgsByFunction(tableNodes) {
|
|
7111
|
+
const argsByFunction = /* @__PURE__ */ new Map();
|
|
7112
|
+
for (const [, tableNode] of tableNodes) {
|
|
7113
|
+
for (const trigger of tableNode.triggers) {
|
|
7114
|
+
if (!trigger.functionName || !trigger.functionArgs?.length) continue;
|
|
7115
|
+
for (const arg of trigger.functionArgs) {
|
|
7116
|
+
const entries = argsByFunction.get(trigger.functionName) ?? [];
|
|
7117
|
+
entries.push({
|
|
7118
|
+
arg: arg.toLowerCase(),
|
|
7119
|
+
triggerName: trigger.name,
|
|
7120
|
+
table: tableNode.qualifiedName
|
|
7121
|
+
});
|
|
7122
|
+
argsByFunction.set(trigger.functionName, entries);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
return argsByFunction;
|
|
7127
|
+
}
|
|
7128
|
+
function validateDispatchCoverage(argsByFunction, sources) {
|
|
7129
|
+
const warnings = [];
|
|
7130
|
+
for (const [functionName, triggerArgs] of argsByFunction) {
|
|
7131
|
+
const body = findFunctionBody(functionName, sources);
|
|
7132
|
+
if (!body || !/\bCASE\b/i.test(body)) continue;
|
|
7133
|
+
const coveredBranches = extractCaseWhenBranches(body);
|
|
7134
|
+
if (coveredBranches.length === 0) continue;
|
|
7135
|
+
for (const { arg, triggerName, table } of triggerArgs) {
|
|
7136
|
+
if (!coveredBranches.includes(arg)) {
|
|
7137
|
+
warnings.push({
|
|
7138
|
+
sourceFile: table,
|
|
7139
|
+
kind: "trigger_dispatch_gap",
|
|
7140
|
+
target: `${functionName}('${arg}')`,
|
|
7141
|
+
reason: `Trigger "${triggerName}" on ${table} passes '${arg}' to ${functionName}(), but the function's CASE statement does not handle this value. Add a WHEN '${arg}' branch to prevent runtime errors.`
|
|
7142
|
+
});
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
|
|
7147
|
+
}
|
|
7148
|
+
var CREATE_INDEX_PATTERN = /^\s*CREATE\s+(?:UNIQUE\s+)?(?:INDEX\s+)?(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([A-Za-z_]\w*))\s+ON\b/gim;
|
|
7149
|
+
function extractIndexNames(content) {
|
|
7150
|
+
const names = /* @__PURE__ */ new Set();
|
|
7151
|
+
CREATE_INDEX_PATTERN.lastIndex = 0;
|
|
7152
|
+
for (const match of content.matchAll(CREATE_INDEX_PATTERN)) {
|
|
7153
|
+
const name = (match[1] ?? match[2] ?? "").toLowerCase();
|
|
7154
|
+
if (name) names.add(name);
|
|
7155
|
+
}
|
|
7156
|
+
return names;
|
|
7157
|
+
}
|
|
7158
|
+
function buildCrossLayerDuplicateIndexWarnings(sources) {
|
|
7159
|
+
const declarativeIndexes = /* @__PURE__ */ new Map();
|
|
7160
|
+
for (const file of sources.declarativeFiles) {
|
|
7161
|
+
for (const name of extractIndexNames(file.content)) {
|
|
7162
|
+
declarativeIndexes.set(name, file.relativePath);
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
const warnings = [];
|
|
7166
|
+
for (const file of sources.idempotentFiles) {
|
|
7167
|
+
for (const name of extractIndexNames(file.content)) {
|
|
7168
|
+
const declarativeFile = declarativeIndexes.get(name);
|
|
7169
|
+
if (declarativeFile) {
|
|
7170
|
+
warnings.push({
|
|
7171
|
+
sourceFile: file.relativePath,
|
|
7172
|
+
kind: "trigger_function",
|
|
7173
|
+
// reuse existing kind for index cross-layer
|
|
7174
|
+
target: name,
|
|
7175
|
+
suggestedDeclarativeFile: declarativeFile,
|
|
7176
|
+
reason: `Index "${name}" is defined in both declarative (${declarativeFile}) and idempotent (${file.relativePath}). The idempotent copy is redundant \u2014 remove it or use a different index name.`
|
|
7177
|
+
});
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
|
|
7182
|
+
}
|
|
6905
7183
|
|
|
6906
7184
|
// src/commands/db/sync/schema-guardrail-graph.ts
|
|
6907
7185
|
function loadSqlSources(targetDir, config) {
|
|
@@ -7031,12 +7309,19 @@ async function buildStaticGraph(targetDir, config, sources) {
|
|
|
7031
7309
|
runtimeTables: loadRuntimeTablesManifest(targetDir),
|
|
7032
7310
|
config
|
|
7033
7311
|
});
|
|
7034
|
-
const boundaryGuidanceWarnings =
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7312
|
+
const boundaryGuidanceWarnings = [
|
|
7313
|
+
...buildBoundaryGuidanceWarnings({
|
|
7314
|
+
fileNodes,
|
|
7315
|
+
schemaNodes,
|
|
7316
|
+
functionClaims,
|
|
7317
|
+
ownerFileByTable
|
|
7318
|
+
}),
|
|
7319
|
+
...buildTriggerDispatchGapWarnings({
|
|
7320
|
+
tableNodes: tableNodesByName,
|
|
7321
|
+
sources
|
|
7322
|
+
}),
|
|
7323
|
+
...buildCrossLayerDuplicateIndexWarnings(sources)
|
|
7324
|
+
];
|
|
7040
7325
|
const localBlindSpotBlockers = buildLocalBlindSpotBlockers({
|
|
7041
7326
|
graph,
|
|
7042
7327
|
sources,
|
|
@@ -7149,7 +7434,8 @@ function createCheckModePhases(report) {
|
|
|
7149
7434
|
phase: "compare_generated_headers",
|
|
7150
7435
|
details: {
|
|
7151
7436
|
file: block.file,
|
|
7152
|
-
target: block.target
|
|
7437
|
+
target: block.target,
|
|
7438
|
+
repair: "Run `runa db sync` to auto-regenerate headers"
|
|
7153
7439
|
}
|
|
7154
7440
|
}))
|
|
7155
7441
|
})
|
|
@@ -7231,7 +7517,7 @@ function setFailure2(report, phase, code, message) {
|
|
|
7231
7517
|
function stableSorted5(values, map) {
|
|
7232
7518
|
return [...values].sort((a, b) => map(a).localeCompare(map(b)));
|
|
7233
7519
|
}
|
|
7234
|
-
function
|
|
7520
|
+
function normalizeFileList3(files) {
|
|
7235
7521
|
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
7236
7522
|
}
|
|
7237
7523
|
function renderList(values) {
|
|
@@ -7716,7 +8002,7 @@ function rewriteManagedHeaders(params) {
|
|
|
7716
8002
|
)
|
|
7717
8003
|
};
|
|
7718
8004
|
}
|
|
7719
|
-
params.report.headersRewritten =
|
|
8005
|
+
params.report.headersRewritten = normalizeFileList3(params.report.headersRewritten);
|
|
7720
8006
|
params.report.rewritesRetainedOnDisk = params.report.headersRewritten.length > 0;
|
|
7721
8007
|
params.report.staleBlocks = [];
|
|
7722
8008
|
return null;
|
|
@@ -8073,7 +8359,7 @@ async function runProductionDdlOrderCheck(params) {
|
|
|
8073
8359
|
}
|
|
8074
8360
|
params.logger.info("\u{1F50D} Checking production DDL ordering...");
|
|
8075
8361
|
try {
|
|
8076
|
-
const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-
|
|
8362
|
+
const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-JZO4GAQG.js');
|
|
8077
8363
|
const { planOutput } = executePgSchemaDiffPlan(
|
|
8078
8364
|
productionUrl,
|
|
8079
8365
|
schemasDir,
|
|
@@ -11416,6 +11702,62 @@ function formatImportImpactReport(report, changedSymbols) {
|
|
|
11416
11702
|
// src/commands/db/commands/db-sync/production-precheck.ts
|
|
11417
11703
|
init_esm_shims();
|
|
11418
11704
|
|
|
11705
|
+
// src/commands/db/utils/changed-files-detector.ts
|
|
11706
|
+
init_esm_shims();
|
|
11707
|
+
function detectDefaultBranch() {
|
|
11708
|
+
const result = spawnSync("git", ["rev-parse", "--verify", "main"], {
|
|
11709
|
+
timeout: 5e3,
|
|
11710
|
+
encoding: "utf-8"
|
|
11711
|
+
});
|
|
11712
|
+
return result.status === 0 ? "main" : "master";
|
|
11713
|
+
}
|
|
11714
|
+
function getChangedSqlFiles() {
|
|
11715
|
+
try {
|
|
11716
|
+
const defaultBranch = detectDefaultBranch();
|
|
11717
|
+
const mergeBase = spawnSync("git", ["merge-base", "HEAD", defaultBranch], {
|
|
11718
|
+
timeout: 5e3,
|
|
11719
|
+
encoding: "utf-8"
|
|
11720
|
+
});
|
|
11721
|
+
let diffOutput;
|
|
11722
|
+
if (mergeBase.status === 0 && mergeBase.stdout.trim()) {
|
|
11723
|
+
const diff = spawnSync("git", ["diff", "--name-only", mergeBase.stdout.trim(), "HEAD"], {
|
|
11724
|
+
timeout: 5e3,
|
|
11725
|
+
encoding: "utf-8"
|
|
11726
|
+
});
|
|
11727
|
+
diffOutput = diff.stdout ?? "";
|
|
11728
|
+
} else {
|
|
11729
|
+
const diff = spawnSync("git", ["diff", "--name-only", "HEAD"], {
|
|
11730
|
+
timeout: 5e3,
|
|
11731
|
+
encoding: "utf-8"
|
|
11732
|
+
});
|
|
11733
|
+
diffOutput = diff.stdout ?? "";
|
|
11734
|
+
}
|
|
11735
|
+
const uncommitted = spawnSync("git", ["diff", "--name-only"], {
|
|
11736
|
+
timeout: 5e3,
|
|
11737
|
+
encoding: "utf-8"
|
|
11738
|
+
});
|
|
11739
|
+
const staged = spawnSync("git", ["diff", "--name-only", "--cached"], {
|
|
11740
|
+
timeout: 5e3,
|
|
11741
|
+
encoding: "utf-8"
|
|
11742
|
+
});
|
|
11743
|
+
const allChanged = [
|
|
11744
|
+
...diffOutput.split("\n"),
|
|
11745
|
+
...(uncommitted.stdout ?? "").split("\n"),
|
|
11746
|
+
...(staged.stdout ?? "").split("\n")
|
|
11747
|
+
].map((f) => f.trim()).filter((f) => f.length > 0 && f.endsWith(".sql"));
|
|
11748
|
+
return [...new Set(allChanged)].sort();
|
|
11749
|
+
} catch {
|
|
11750
|
+
return [];
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
function classifyBlockerScope(blockerMessage, changedFiles) {
|
|
11754
|
+
const fileMatch = blockerMessage.match(
|
|
11755
|
+
/(supabase\/schemas\/(?:declarative|idempotent)\/[^\s:]+\.sql)/
|
|
11756
|
+
);
|
|
11757
|
+
if (!fileMatch) return "baseline";
|
|
11758
|
+
return changedFiles.has(fileMatch[1]) ? "current-change" : "baseline";
|
|
11759
|
+
}
|
|
11760
|
+
|
|
11419
11761
|
// src/commands/db/commands/db-sync/plan-hazard-analyzer.ts
|
|
11420
11762
|
init_esm_shims();
|
|
11421
11763
|
function parseHazardType(hazard) {
|
|
@@ -11722,13 +12064,26 @@ async function collectLocalPrecheckBundle(strict) {
|
|
|
11722
12064
|
const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
|
|
11723
12065
|
const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
|
|
11724
12066
|
const duplicateOwnershipAnalysis = analyzeDuplicateFunctionOwnership(process.cwd());
|
|
11725
|
-
|
|
12067
|
+
let guardrailAllowlist = [];
|
|
12068
|
+
try {
|
|
12069
|
+
guardrailAllowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
|
|
12070
|
+
} catch {
|
|
12071
|
+
}
|
|
12072
|
+
const duplicateOwnershipBlockers = duplicateOwnershipAnalysis.findings.filter(
|
|
12073
|
+
(finding) => !isAllowlistedDuplicateFunction({
|
|
12074
|
+
finding,
|
|
12075
|
+
allowlist: guardrailAllowlist,
|
|
12076
|
+
bodyHashes: /* @__PURE__ */ new Map()
|
|
12077
|
+
// Hash check skipped; name + signature matching still works
|
|
12078
|
+
})
|
|
12079
|
+
).map((finding) => {
|
|
11726
12080
|
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
11727
12081
|
return [
|
|
11728
12082
|
formatted.summary,
|
|
11729
12083
|
`declarative=${formatted.declarativeLocations.join(", ")}`,
|
|
11730
12084
|
`idempotent=${formatted.idempotentLocations.join(", ")}`,
|
|
11731
|
-
formatted.suggestion
|
|
12085
|
+
formatted.suggestion,
|
|
12086
|
+
"Allowlist: runa.config.ts database.schemaGuardrails.allowedDuplicateFunctions"
|
|
11732
12087
|
].join(" | ");
|
|
11733
12088
|
});
|
|
11734
12089
|
const hasLocalBlockers = duplicateOwnershipBlockers.length > 0 || hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
|
|
@@ -11795,32 +12150,185 @@ function printLocalFindingCompactSummary(logger4, local, topLimit) {
|
|
|
11795
12150
|
topLimit
|
|
11796
12151
|
);
|
|
11797
12152
|
}
|
|
12153
|
+
var APPLY_BLOCKER_PATTERNS = [
|
|
12154
|
+
/Extension DDL/i,
|
|
12155
|
+
/DROP.*CONCURRENTLY/i,
|
|
12156
|
+
/DML.*declarative/i,
|
|
12157
|
+
/DO.*maintenance/i,
|
|
12158
|
+
/cross.*schema.*rls/i,
|
|
12159
|
+
/auth\.\*.*direct.*reference/i,
|
|
12160
|
+
/Duplicate function ownership/i
|
|
12161
|
+
];
|
|
12162
|
+
var INFRA_DYNAMIC_SQL_INDICATOR = /idempotent file/i;
|
|
12163
|
+
function isApplyBlocker(message) {
|
|
12164
|
+
if (/EXECUTE|dynamic.*sql/i.test(message)) {
|
|
12165
|
+
return !INFRA_DYNAMIC_SQL_INDICATOR.test(message);
|
|
12166
|
+
}
|
|
12167
|
+
return APPLY_BLOCKER_PATTERNS.some((pattern) => pattern.test(message));
|
|
12168
|
+
}
|
|
12169
|
+
function categorizeBlockers(blockers) {
|
|
12170
|
+
const applyBlockers = [];
|
|
12171
|
+
const architectureDebt = [];
|
|
12172
|
+
for (const blocker of blockers) {
|
|
12173
|
+
if (isApplyBlocker(blocker)) {
|
|
12174
|
+
applyBlockers.push(blocker);
|
|
12175
|
+
} else {
|
|
12176
|
+
architectureDebt.push(blocker);
|
|
12177
|
+
}
|
|
12178
|
+
}
|
|
12179
|
+
return { applyBlockers, architectureDebt };
|
|
12180
|
+
}
|
|
11798
12181
|
function printLocalFindingDetailedReport(logger4, local) {
|
|
11799
|
-
|
|
12182
|
+
logLocalWarningsSections(logger4, local);
|
|
12183
|
+
const allBlockers = collectAllBlockers(local);
|
|
12184
|
+
const { applyBlockers, architectureDebt } = categorizeBlockers(allBlockers);
|
|
12185
|
+
const changedFiles = new Set(getChangedSqlFiles());
|
|
12186
|
+
if (changedFiles.size > 0) {
|
|
12187
|
+
logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles);
|
|
12188
|
+
} else {
|
|
12189
|
+
logUnscopedBlockers(logger4, applyBlockers, architectureDebt);
|
|
12190
|
+
}
|
|
12191
|
+
}
|
|
12192
|
+
function logLocalWarningsSections(logger4, local) {
|
|
12193
|
+
logFindingSection(logger4, "Declarative risk warnings:", local.adjustedDeclarativeRisks.warnings);
|
|
12194
|
+
logFindingSection(logger4, "Idempotent risk warnings:", local.adjustedIdempotentRisks.warnings);
|
|
12195
|
+
logFindingSection(logger4, "Placement warnings:", local.adjustedPlacementRisks.warnings);
|
|
12196
|
+
logFindingSection(logger4, "Extension warnings:", local.adjustedExtensionRisks.warnings);
|
|
12197
|
+
}
|
|
12198
|
+
function collectAllBlockers(local) {
|
|
12199
|
+
return [
|
|
12200
|
+
...local.duplicateOwnershipBlockers,
|
|
12201
|
+
...local.adjustedPlacementRisks.blockers,
|
|
12202
|
+
...local.adjustedDeclarativeRisks.blockers,
|
|
12203
|
+
...local.adjustedIdempotentRisks.blockers,
|
|
12204
|
+
...local.adjustedExtensionRisks.blockers
|
|
12205
|
+
];
|
|
12206
|
+
}
|
|
12207
|
+
function logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles) {
|
|
12208
|
+
const current = applyBlockers.filter(
|
|
12209
|
+
(b) => classifyBlockerScope(b, changedFiles) === "current-change"
|
|
12210
|
+
);
|
|
12211
|
+
const baseline = applyBlockers.filter(
|
|
12212
|
+
(b) => classifyBlockerScope(b, changedFiles) === "baseline"
|
|
12213
|
+
);
|
|
12214
|
+
const currentDebt = architectureDebt.filter(
|
|
12215
|
+
(b) => classifyBlockerScope(b, changedFiles) === "current-change"
|
|
12216
|
+
);
|
|
12217
|
+
const baselineDebt = architectureDebt.filter(
|
|
12218
|
+
(b) => classifyBlockerScope(b, changedFiles) === "baseline"
|
|
12219
|
+
);
|
|
12220
|
+
logBlockerGroup(
|
|
11800
12221
|
logger4,
|
|
11801
|
-
"
|
|
11802
|
-
|
|
12222
|
+
"error",
|
|
12223
|
+
"[current change] Apply blockers",
|
|
12224
|
+
current,
|
|
12225
|
+
"fix before merging"
|
|
11803
12226
|
);
|
|
11804
|
-
|
|
12227
|
+
logBlockerGroup(logger4, "warn", "[current change] Architecture debt", currentDebt);
|
|
12228
|
+
logBlockerGroup(
|
|
11805
12229
|
logger4,
|
|
11806
|
-
"
|
|
11807
|
-
|
|
12230
|
+
"error",
|
|
12231
|
+
"[repo baseline] Apply blockers",
|
|
12232
|
+
baseline,
|
|
12233
|
+
"pre-existing issues"
|
|
11808
12234
|
);
|
|
11809
|
-
|
|
12235
|
+
logBlockerGroup(
|
|
11810
12236
|
logger4,
|
|
11811
|
-
"
|
|
11812
|
-
|
|
12237
|
+
"warn",
|
|
12238
|
+
"[repo baseline] Architecture debt",
|
|
12239
|
+
baselineDebt,
|
|
12240
|
+
"pre-existing"
|
|
11813
12241
|
);
|
|
11814
|
-
|
|
12242
|
+
logImpactSummary(logger4, {
|
|
12243
|
+
current: current.length + currentDebt.length,
|
|
12244
|
+
currentApply: current.length,
|
|
12245
|
+
currentDebt: currentDebt.length,
|
|
12246
|
+
baseline: baseline.length + baselineDebt.length,
|
|
12247
|
+
baselineApply: baseline.length,
|
|
12248
|
+
baselineDebt: baselineDebt.length,
|
|
12249
|
+
total: applyBlockers.length + architectureDebt.length
|
|
12250
|
+
});
|
|
12251
|
+
}
|
|
12252
|
+
var PRECHECK_COUNTS_PATH = join(".runa", "tmp", "precheck-counts.json");
|
|
12253
|
+
function loadPreviousPrecheckCounts() {
|
|
12254
|
+
try {
|
|
12255
|
+
const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
|
|
12256
|
+
if (!existsSync(fullPath)) return null;
|
|
12257
|
+
return JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
12258
|
+
} catch {
|
|
12259
|
+
return null;
|
|
12260
|
+
}
|
|
12261
|
+
}
|
|
12262
|
+
function savePrecheckCounts(counts) {
|
|
12263
|
+
try {
|
|
12264
|
+
const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
|
|
12265
|
+
const dir = join(fullPath, "..");
|
|
12266
|
+
mkdirSync(dir, { recursive: true });
|
|
12267
|
+
writeFileSync(fullPath, JSON.stringify(counts, null, 2), "utf-8");
|
|
12268
|
+
} catch {
|
|
12269
|
+
}
|
|
12270
|
+
}
|
|
12271
|
+
function logImpactSummary(logger4, counts) {
|
|
12272
|
+
logger4.info("");
|
|
12273
|
+
logger4.info("Impact summary:");
|
|
12274
|
+
if (counts.current === 0 && counts.total > 0) {
|
|
12275
|
+
logger4.info(" Current change: no new issues introduced");
|
|
12276
|
+
} else if (counts.current > 0) {
|
|
12277
|
+
logger4.info(
|
|
12278
|
+
` Regressions: ${counts.current} issue(s) from changed files (${counts.currentApply} apply, ${counts.currentDebt} debt)`
|
|
12279
|
+
);
|
|
12280
|
+
}
|
|
12281
|
+
if (counts.baseline > 0) {
|
|
12282
|
+
logger4.info(
|
|
12283
|
+
` Baseline debt: ${counts.baseline} pre-existing issue(s) (${counts.baselineApply} apply, ${counts.baselineDebt} debt)`
|
|
12284
|
+
);
|
|
12285
|
+
}
|
|
12286
|
+
if (counts.total === 0) {
|
|
12287
|
+
logger4.info(" All clear \u2014 no blockers or debt detected");
|
|
12288
|
+
}
|
|
12289
|
+
const previous = loadPreviousPrecheckCounts();
|
|
12290
|
+
if (previous) {
|
|
12291
|
+
const delta = counts.total - previous.total;
|
|
12292
|
+
if (delta < 0) {
|
|
12293
|
+
logger4.info(` Improvement: ${Math.abs(delta)} fewer issue(s) than previous run`);
|
|
12294
|
+
} else if (delta > 0) {
|
|
12295
|
+
logger4.info(` Regression: ${delta} more issue(s) than previous run`);
|
|
12296
|
+
} else {
|
|
12297
|
+
logger4.info(" No change from previous run");
|
|
12298
|
+
}
|
|
12299
|
+
}
|
|
12300
|
+
savePrecheckCounts({
|
|
12301
|
+
total: counts.total,
|
|
12302
|
+
apply: counts.currentApply + counts.baselineApply,
|
|
12303
|
+
debt: counts.currentDebt + counts.baselineDebt,
|
|
12304
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
12305
|
+
});
|
|
12306
|
+
}
|
|
12307
|
+
function logUnscopedBlockers(logger4, applyBlockers, architectureDebt) {
|
|
12308
|
+
logBlockerGroup(
|
|
12309
|
+
logger4,
|
|
12310
|
+
"error",
|
|
12311
|
+
"Apply blockers",
|
|
12312
|
+
applyBlockers,
|
|
12313
|
+
"deployment will fail if not fixed"
|
|
12314
|
+
);
|
|
12315
|
+
logBlockerGroup(
|
|
11815
12316
|
logger4,
|
|
11816
|
-
"
|
|
11817
|
-
|
|
12317
|
+
"warn",
|
|
12318
|
+
"Architecture debt",
|
|
12319
|
+
architectureDebt,
|
|
12320
|
+
"works but should be addressed"
|
|
11818
12321
|
);
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
12322
|
+
}
|
|
12323
|
+
function logBlockerGroup(logger4, level, title, items, subtitle) {
|
|
12324
|
+
if (items.length === 0) return;
|
|
12325
|
+
const suffix = subtitle ? ` \u2014 ${subtitle}` : "";
|
|
12326
|
+
const logFn = level === "error" ? logger4.error.bind(logger4) : logger4.warn.bind(logger4);
|
|
12327
|
+
logFn(`
|
|
12328
|
+
${title} (${items.length})${suffix}:`);
|
|
12329
|
+
for (const item of items) {
|
|
12330
|
+
logger4.info(` ${item}`);
|
|
12331
|
+
}
|
|
11824
12332
|
}
|
|
11825
12333
|
function logLocalFindings(logger4, local, summary) {
|
|
11826
12334
|
if (!local.hasLocalFindings) return;
|
|
@@ -12163,9 +12671,12 @@ async function runSyncAction(env, options) {
|
|
|
12163
12671
|
const { dbTables, dbEnums } = await fetchDbTablesAndEnums(databaseUrl);
|
|
12164
12672
|
const schemaDiffConfig = loadSchemaDiffConfig();
|
|
12165
12673
|
const idempotentTables = extractTablesFromIdempotentSql(schemaDiffConfig.idempotentSqlDir);
|
|
12674
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
|
|
12675
|
+
schemaDiffConfig.idempotentSqlDir
|
|
12676
|
+
);
|
|
12166
12677
|
const excludeFromOrphanDetection = mergeExcludedTables(
|
|
12167
12678
|
schemaDiffConfig.excludeFromOrphanDetection,
|
|
12168
|
-
idempotentTables
|
|
12679
|
+
[...idempotentTables, ...dynamicPatterns]
|
|
12169
12680
|
);
|
|
12170
12681
|
const diff = diffSchema({
|
|
12171
12682
|
expectedTables,
|