@runa-ai/runa-cli 0.10.0 → 0.10.2
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-Y5ANTCKE.js → chunk-EZ46JIEO.js} +5 -2
- package/dist/{chunk-ZWDWFMOX.js → chunk-HWR5NUUZ.js} +24 -3
- package/dist/{chunk-OXQISY3J.js → chunk-IR7SA2ME.js} +1 -1
- package/dist/{chunk-QDOR3GTD.js → chunk-LCJNIHZY.js} +82 -14
- package/dist/{chunk-JQXOVCOP.js → chunk-NIS77243.js} +8 -5
- package/dist/{chunk-URWDB7YL.js → chunk-O3M7A73M.js} +58 -2
- package/dist/{chunk-PAWNJA3N.js → chunk-XFXGFUAM.js} +1 -1
- package/dist/{chunk-IEKYTCYA.js → chunk-YTQS2O4H.js} +59 -0
- package/dist/{ci-FLTJ2UXB.js → ci-6XYG7XNX.js} +5 -5
- package/dist/{cli-THEA6T7N.js → cli-2XL3VESS.js} +14 -14
- package/dist/commands/build/contract.d.ts +2 -2
- package/dist/commands/build/machine.d.ts +6 -6
- package/dist/commands/ci/commands/ci-prod-types.d.ts +1 -1
- package/dist/commands/ci/machine/contract.d.ts +10 -10
- package/dist/commands/ci/machine/machine.d.ts +3 -3
- package/dist/commands/ci/utils/ci-summary.d.ts +3 -3
- package/dist/commands/db/apply/contract.d.ts +1 -1
- package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +6 -0
- package/dist/commands/db/apply/helpers/planner-artifact.d.ts +1 -1
- package/dist/commands/db/commands/db-preview-profile.d.ts +1 -1
- package/dist/commands/db/commands/db-sync/production-precheck.d.ts +0 -8
- package/dist/commands/db/preflight/contract.d.ts +1 -1
- package/dist/commands/db/sync/contract.d.ts +5 -5
- package/dist/commands/db/sync/machine.d.ts +2 -2
- package/dist/commands/db/sync/schema-guardrail-graph-guidance.d.ts +18 -1
- package/dist/commands/db/sync/schema-guardrail-graph-metadata.d.ts +1 -7
- 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/duplicate-function-ownership-allowlist.d.ts +13 -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/commands/upgrade.d.ts +36 -0
- package/dist/constants/versions.d.ts +9 -0
- package/dist/{db-IDKQ44VX.js → db-4AGPISOW.js} +1560 -1006
- 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/{risk-detector-S7XQF4I2.js → risk-detector-GDDLISVE.js} +1 -1
- package/dist/{risk-detector-core-TGFKWHRS.js → risk-detector-core-YI3M6INI.js} +1 -1
- package/dist/{risk-detector-plpgsql-O32TUR34.js → risk-detector-plpgsql-4GWEQXUG.js} +1 -1
- package/dist/{template-check-VNNQQXCX.js → template-check-D35F2GDP.js} +4 -0
- package/dist/{upgrade-QZKEI3NJ.js → upgrade-X7P6WRD5.js} +190 -20
- 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-JRPMUHLF.js → vuln-check-LMDYYJUE.js} +1 -1
- package/dist/{vuln-checker-Q7LSHUHJ.js → vuln-checker-NHXLNZRM.js} +1 -1
- package/dist/{watch-RFVCEQLH.js → watch-4RHXVCQ3.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
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { detectDatabaseStack, getStackPaths } from './chunk-MILCC3B6.js';
|
|
4
|
-
import { categorizeRisks, detectSchemaRisks } from './chunk-
|
|
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';
|
|
4
|
+
import { categorizeRisks, detectSchemaRisks } from './chunk-XFXGFUAM.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-LCJNIHZY.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
|
-
import './chunk-
|
|
11
|
+
import './chunk-EZ46JIEO.js';
|
|
13
12
|
import { loadEnvFiles } from './chunk-IWVXI5O4.js';
|
|
14
|
-
import './chunk-
|
|
13
|
+
import './chunk-IR7SA2ME.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
|
}
|
|
@@ -1832,6 +1848,10 @@ async function collectSchemaRisks(sqlDir, sqlFiles) {
|
|
|
1832
1848
|
}
|
|
1833
1849
|
return allRisks;
|
|
1834
1850
|
}
|
|
1851
|
+
function filterAllowlistedSchemaRisks(risks) {
|
|
1852
|
+
const policy = getBoundaryPolicy();
|
|
1853
|
+
return risks.filter((risk) => !findDeclarativeRiskAllowlistMatch(risk, policy));
|
|
1854
|
+
}
|
|
1835
1855
|
function summarizeRisks(risks) {
|
|
1836
1856
|
const summaries = /* @__PURE__ */ new Map();
|
|
1837
1857
|
for (const risk of risks) {
|
|
@@ -1885,10 +1905,54 @@ function reportMediumRisks(result, logger4, mediumRisks) {
|
|
|
1885
1905
|
logger4.info(` \u2022 ${summary}`);
|
|
1886
1906
|
}
|
|
1887
1907
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1908
|
+
var SHOW_LOW_RISKS = process.env.RUNA_DB_SHOW_LOW_RISKS === "1";
|
|
1909
|
+
function reportLowRisks(logger4, lowRisks) {
|
|
1910
|
+
if (lowRisks.length === 0) return;
|
|
1911
|
+
if (SHOW_LOW_RISKS) {
|
|
1912
|
+
logger4.info(` Found ${lowRisks.length} LOW risk suggestion(s):`);
|
|
1913
|
+
const summaries = summarizeRisks(lowRisks);
|
|
1914
|
+
for (const summary of summaries) {
|
|
1915
|
+
logger4.info(` ${summary.replace("[MEDIUM]", "[LOW]")}`);
|
|
1916
|
+
}
|
|
1917
|
+
} else {
|
|
1918
|
+
logger4.info(
|
|
1919
|
+
` Found ${lowRisks.length} LOW risk suggestion(s) (informational; set RUNA_DB_SHOW_LOW_RISKS=1 to see details)`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function writeRiskJson(allRisks, categorized) {
|
|
1924
|
+
if (!SHOW_LOW_RISKS) return;
|
|
1925
|
+
const outputDir = path12.join(process.cwd(), ".runa", "tmp");
|
|
1926
|
+
const outputPath = path12.join(outputDir, "schema-risks.json");
|
|
1927
|
+
try {
|
|
1928
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1929
|
+
const json = JSON.stringify(
|
|
1930
|
+
{
|
|
1931
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1932
|
+
summary: {
|
|
1933
|
+
high: categorized.high.length,
|
|
1934
|
+
medium: categorized.medium.length,
|
|
1935
|
+
low: categorized.low.length,
|
|
1936
|
+
total: allRisks.length
|
|
1937
|
+
},
|
|
1938
|
+
risks: allRisks.map((risk) => ({
|
|
1939
|
+
level: risk.level,
|
|
1940
|
+
file: risk.file,
|
|
1941
|
+
line: risk.line,
|
|
1942
|
+
description: risk.description,
|
|
1943
|
+
mitigation: risk.mitigation,
|
|
1944
|
+
reasonCode: risk.reasonCode
|
|
1945
|
+
}))
|
|
1946
|
+
},
|
|
1947
|
+
null,
|
|
1948
|
+
2
|
|
1949
|
+
);
|
|
1950
|
+
writeFileSync(outputPath, json, "utf-8");
|
|
1951
|
+
} catch {
|
|
1891
1952
|
}
|
|
1953
|
+
}
|
|
1954
|
+
function reportRiskGuidance(logger4, highRiskCount, lowRisks) {
|
|
1955
|
+
reportLowRisks(logger4, lowRisks);
|
|
1892
1956
|
if (highRiskCount > 0) {
|
|
1893
1957
|
logger4.info("");
|
|
1894
1958
|
logger4.info(" AGENTS.md requires Supabase Auth Schema Independence:");
|
|
@@ -1906,7 +1970,7 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
|
|
|
1906
1970
|
const sqlFiles = getDeclarativeSqlFiles(sqlDir, logger4);
|
|
1907
1971
|
if (!sqlFiles) return;
|
|
1908
1972
|
try {
|
|
1909
|
-
const allRisks = await collectSchemaRisks(sqlDir, sqlFiles);
|
|
1973
|
+
const allRisks = filterAllowlistedSchemaRisks(await collectSchemaRisks(sqlDir, sqlFiles));
|
|
1910
1974
|
if (allRisks.length === 0) {
|
|
1911
1975
|
logger4.success(`Scanned ${sqlFiles.length} SQL file(s) - no violations`);
|
|
1912
1976
|
return;
|
|
@@ -1914,7 +1978,8 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
|
|
|
1914
1978
|
const categorized = categorizeRisks(applySyncPreflightRiskPolicy(allRisks));
|
|
1915
1979
|
reportHighRisks(result, logger4, categorized.high);
|
|
1916
1980
|
reportMediumRisks(result, logger4, categorized.medium);
|
|
1917
|
-
reportRiskGuidance(logger4, categorized.high.length, categorized.low
|
|
1981
|
+
reportRiskGuidance(logger4, categorized.high.length, categorized.low);
|
|
1982
|
+
writeRiskJson(allRisks, categorized);
|
|
1918
1983
|
} catch (error) {
|
|
1919
1984
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1920
1985
|
logger4.warn(`SQL schema risk check skipped: ${message}`);
|
|
@@ -1971,9 +2036,10 @@ async function runOrphanCheck(env, dbPackagePath, result, logger4, step) {
|
|
|
1971
2036
|
} catch {
|
|
1972
2037
|
}
|
|
1973
2038
|
const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
|
|
1974
|
-
|
|
2039
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
|
|
2040
|
+
if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
|
|
1975
2041
|
excludeFromOrphanDetection = [
|
|
1976
|
-
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
|
|
2042
|
+
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
|
|
1977
2043
|
];
|
|
1978
2044
|
}
|
|
1979
2045
|
const diff = diffSchema({
|
|
@@ -2591,664 +2657,991 @@ async function runDeclarativeDependencyCheck(result, logger4, step, strictOption
|
|
|
2591
2657
|
|
|
2592
2658
|
// src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
|
|
2593
2659
|
init_esm_shims();
|
|
2594
|
-
async function runDuplicateFunctionOwnershipCheck(result, logger4, step) {
|
|
2595
|
-
logger4.step("Checking duplicate function ownership", step.next());
|
|
2596
|
-
const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
|
|
2597
|
-
if (analysis.findings.length === 0) {
|
|
2598
|
-
logger4.success("No duplicate declarative/idempotent function ownership detected");
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
result.passed = false;
|
|
2602
|
-
result.errors.push(`Found ${analysis.findings.length} duplicate function ownership finding(s)`);
|
|
2603
|
-
logger4.error(`Found ${analysis.findings.length} duplicate function ownership finding(s):`);
|
|
2604
|
-
logger4.info(` ${analysis.contractNote}`);
|
|
2605
|
-
for (const finding of analysis.findings) {
|
|
2606
|
-
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
2607
|
-
logger4.info(` [ERROR] ${formatted.summary}`);
|
|
2608
|
-
for (const location of formatted.declarativeLocations) {
|
|
2609
|
-
logger4.info(` declarative: ${location}`);
|
|
2610
|
-
}
|
|
2611
|
-
for (const location of formatted.idempotentLocations) {
|
|
2612
|
-
logger4.info(` idempotent: ${location}`);
|
|
2613
|
-
}
|
|
2614
|
-
logger4.info(` ${formatted.suggestion}`);
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
// src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
|
|
2619
|
-
init_esm_shims();
|
|
2620
2660
|
|
|
2621
|
-
// src/commands/db/
|
|
2661
|
+
// src/commands/db/utils/duplicate-function-ownership-allowlist.ts
|
|
2622
2662
|
init_esm_shims();
|
|
2623
|
-
var SHOW_ALLOWLIST_REPORT = process.env.RUNA_DB_PRECHECK_ALLOWLIST_REPORT === "1";
|
|
2624
|
-
var DIRECTORY_PLACEMENT_WARNING_PREFIX = " [misplacement] ";
|
|
2625
|
-
function applyStrictModeToReport(report, strict) {
|
|
2626
|
-
if (!strict) return report;
|
|
2627
|
-
return {
|
|
2628
|
-
blockers: [...report.blockers, ...report.warnings],
|
|
2629
|
-
warnings: []
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
function formatAllowlistReason({
|
|
2633
|
-
label,
|
|
2634
|
-
ruleId,
|
|
2635
|
-
reason,
|
|
2636
|
-
rule
|
|
2637
|
-
}) {
|
|
2638
|
-
const meta = rule ? formatAllowlistMetadata(rule) : "";
|
|
2639
|
-
return meta ? `[allowlist:${label}] ${ruleId}: ${reason} (${meta})` : `[allowlist:${label}] ${ruleId}: ${reason}`;
|
|
2640
|
-
}
|
|
2641
2663
|
|
|
2642
|
-
// src/commands/db/
|
|
2664
|
+
// src/commands/db/sync/schema-guardrail-config.ts
|
|
2643
2665
|
init_esm_shims();
|
|
2644
2666
|
|
|
2645
|
-
// src/commands/db/
|
|
2667
|
+
// src/commands/db/sync/schema-guardrail-config-test-support.ts
|
|
2646
2668
|
init_esm_shims();
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
const raw = process.env[name];
|
|
2651
|
-
if (!raw) return fallback;
|
|
2652
|
-
const value = Number.parseInt(raw, 10);
|
|
2653
|
-
if (!Number.isFinite(value) || Number.isNaN(value)) return fallback;
|
|
2654
|
-
const normalized = Math.trunc(value);
|
|
2655
|
-
if (normalized < min) return min;
|
|
2656
|
-
if (normalized > max) return max;
|
|
2657
|
-
return normalized;
|
|
2658
|
-
}
|
|
2659
|
-
var RUNA_SCHEMA_PRECHECK_MAX_FILES = parseIntEnv(
|
|
2660
|
-
"RUNA_SCHEMA_PRECHECK_MAX_FILES",
|
|
2661
|
-
3e3,
|
|
2662
|
-
100,
|
|
2663
|
-
2e4
|
|
2664
|
-
);
|
|
2665
|
-
var RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES = parseIntEnv(
|
|
2666
|
-
"RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES",
|
|
2667
|
-
512 * ONE_MB,
|
|
2668
|
-
32 * ONE_MB,
|
|
2669
|
-
20 * ONE_GB
|
|
2670
|
-
);
|
|
2671
|
-
var RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES = parseIntEnv(
|
|
2672
|
-
"RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES",
|
|
2673
|
-
64 * ONE_MB,
|
|
2674
|
-
1 * ONE_MB,
|
|
2675
|
-
512 * ONE_MB
|
|
2676
|
-
);
|
|
2677
|
-
var RUNA_PLAN_PRECHECK_MAX_BYTES = parseIntEnv(
|
|
2678
|
-
"RUNA_PLAN_PRECHECK_MAX_BYTES",
|
|
2679
|
-
128 * ONE_MB,
|
|
2680
|
-
8 * ONE_MB,
|
|
2681
|
-
4 * ONE_GB
|
|
2682
|
-
);
|
|
2683
|
-
var RUNA_PLAN_PRECHECK_MAX_STATEMENTS = parseIntEnv(
|
|
2684
|
-
"RUNA_PLAN_PRECHECK_MAX_STATEMENTS",
|
|
2685
|
-
4e3,
|
|
2686
|
-
100,
|
|
2687
|
-
5e4
|
|
2688
|
-
);
|
|
2689
|
-
function formatBytes2(value) {
|
|
2690
|
-
if (value >= ONE_MB * 1024) return `${(value / (ONE_MB * 1024)).toFixed(1)} GB`;
|
|
2691
|
-
if (value >= ONE_MB) return `${(value / ONE_MB).toFixed(1)} MB`;
|
|
2692
|
-
return `${(value / 1024).toFixed(1)} KB`;
|
|
2693
|
-
}
|
|
2694
|
-
function buildBudgetExceededReason(context, files, bytes, maxFiles, maxBytes) {
|
|
2695
|
-
return `${context}: budget exceeded (files=${files}/${maxFiles}, bytes=${formatBytes2(bytes)}/${formatBytes2(maxBytes)}). Stop and review with stricter scoping or increase RUNA_SCHEMA_PRECHECK_MAX_* settings only after approval.`;
|
|
2696
|
-
}
|
|
2697
|
-
function createSchemaPrecheckBudgetState() {
|
|
2698
|
-
return {
|
|
2699
|
-
scannedFiles: 0,
|
|
2700
|
-
scannedBytes: 0,
|
|
2701
|
-
maxFiles: RUNA_SCHEMA_PRECHECK_MAX_FILES,
|
|
2702
|
-
maxBytes: RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES,
|
|
2703
|
-
maxFileBytes: RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES
|
|
2704
|
-
};
|
|
2669
|
+
function extractFirstStringMatch(content, fieldName) {
|
|
2670
|
+
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*['"]([^'"]+)['"]`));
|
|
2671
|
+
return match?.[1];
|
|
2705
2672
|
}
|
|
2706
|
-
function
|
|
2707
|
-
const
|
|
2708
|
-
if (
|
|
2709
|
-
return
|
|
2710
|
-
state.maxFileBytes
|
|
2711
|
-
)}`;
|
|
2712
|
-
}
|
|
2713
|
-
if (state.scannedFiles + 1 > state.maxFiles) {
|
|
2714
|
-
return `Schema file scan budget exceeds ${state.maxFiles} files at ${path12.basename(filePath)}.`;
|
|
2673
|
+
function extractFirstNumberMatch(content, fieldName) {
|
|
2674
|
+
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`));
|
|
2675
|
+
if (!match?.[1]) {
|
|
2676
|
+
return void 0;
|
|
2715
2677
|
}
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
|
|
2678
|
+
const parsed = Number(match[1]);
|
|
2679
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2680
|
+
}
|
|
2681
|
+
function extractStringArrayMatch(content, fieldName) {
|
|
2682
|
+
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
2683
|
+
if (!match?.[1]) {
|
|
2684
|
+
return [];
|
|
2719
2685
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2686
|
+
return Array.from(match[1].matchAll(/['"]([^'"]+)['"]/g), (entry) => entry[1] ?? "").filter(
|
|
2687
|
+
(value) => value.length > 0
|
|
2688
|
+
);
|
|
2723
2689
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
continue;
|
|
2740
|
-
}
|
|
2741
|
-
if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
2742
|
-
yield fullPath;
|
|
2743
|
-
}
|
|
2690
|
+
function extractNamedObjectBlock(content, fieldName) {
|
|
2691
|
+
const nameMatch = new RegExp(`${fieldName}\\s*:\\s*\\{`).exec(content);
|
|
2692
|
+
if (!nameMatch) {
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
const startIndex = nameMatch.index + nameMatch[0].length - 1;
|
|
2696
|
+
let depth = 0;
|
|
2697
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
2698
|
+
const current = content[index];
|
|
2699
|
+
if (current === "{") {
|
|
2700
|
+
depth += 1;
|
|
2701
|
+
} else if (current === "}") {
|
|
2702
|
+
depth -= 1;
|
|
2703
|
+
if (depth === 0) {
|
|
2704
|
+
return content.slice(startIndex + 1, index);
|
|
2744
2705
|
}
|
|
2745
|
-
} catch {
|
|
2746
2706
|
}
|
|
2747
2707
|
}
|
|
2708
|
+
return null;
|
|
2748
2709
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
// src/commands/db/commands/db-sync/sql-parser.ts
|
|
2754
|
-
init_esm_shims();
|
|
2755
|
-
function isWordChar(char) {
|
|
2756
|
-
const code = char.charCodeAt(0);
|
|
2757
|
-
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_";
|
|
2758
|
-
}
|
|
2759
|
-
function isWhitespaceChar(char) {
|
|
2760
|
-
return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
2761
|
-
}
|
|
2762
|
-
function parseWordAt(statement, start) {
|
|
2763
|
-
if (start < 0 || start >= statement.length) return null;
|
|
2764
|
-
const first = statement[start];
|
|
2765
|
-
if (!first || !isWordChar(first)) return null;
|
|
2766
|
-
let end = start + 1;
|
|
2767
|
-
while (end < statement.length && isWordChar(statement[end] ?? "")) {
|
|
2768
|
-
end += 1;
|
|
2769
|
-
}
|
|
2770
|
-
return { value: statement.slice(start, end), start, end };
|
|
2771
|
-
}
|
|
2772
|
-
function skipWhitespace(statement, start) {
|
|
2773
|
-
let index = start;
|
|
2774
|
-
while (index < statement.length && isWhitespaceChar(statement[index] ?? "")) {
|
|
2775
|
-
index += 1;
|
|
2710
|
+
function extractAllowedDuplicateFunctions(content, normalizers) {
|
|
2711
|
+
const match = content.match(/allowedDuplicateFunctions\s*:\s*\[([\s\S]*?)\]/);
|
|
2712
|
+
if (!match?.[1]) {
|
|
2713
|
+
return [];
|
|
2776
2714
|
}
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2715
|
+
const entries = [];
|
|
2716
|
+
for (const objectMatch of match[1].matchAll(/\{([\s\S]*?)\}/g)) {
|
|
2717
|
+
const objectBody = objectMatch[1] ?? "";
|
|
2718
|
+
const qualifiedName = extractFirstStringMatch(objectBody, "qualifiedName");
|
|
2719
|
+
const signature = extractFirstStringMatch(objectBody, "signature");
|
|
2720
|
+
const reason = extractFirstStringMatch(objectBody, "reason");
|
|
2721
|
+
if (!qualifiedName || !signature || !reason) {
|
|
2722
|
+
continue;
|
|
2723
|
+
}
|
|
2724
|
+
entries.push({
|
|
2725
|
+
qualifiedName: normalizers.normalizeFunctionQualifiedName(qualifiedName),
|
|
2726
|
+
signature: normalizers.normalizeAllowlistSignature(signature),
|
|
2727
|
+
reason,
|
|
2728
|
+
declarativeFile: extractFirstStringMatch(objectBody, "declarativeFile"),
|
|
2729
|
+
idempotentFile: extractFirstStringMatch(objectBody, "idempotentFile"),
|
|
2730
|
+
expectedBodyHash: extractFirstStringMatch(objectBody, "expectedBodyHash")
|
|
2731
|
+
});
|
|
2783
2732
|
}
|
|
2784
|
-
return
|
|
2733
|
+
return entries;
|
|
2785
2734
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2735
|
+
function tryLoadSchemaGuardrailConfigFromText(params) {
|
|
2736
|
+
const configPath = findRunaConfig(params.targetDir);
|
|
2737
|
+
if (!configPath || !existsSync(configPath)) {
|
|
2738
|
+
return null;
|
|
2790
2739
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
while (index < statement.length) {
|
|
2796
|
-
const afterWhitespace = skipWhitespace(statement, index);
|
|
2797
|
-
if (afterWhitespace !== index) {
|
|
2798
|
-
index = afterWhitespace;
|
|
2799
|
-
continue;
|
|
2800
|
-
}
|
|
2801
|
-
if (statement[index] === "-" && statement[index + 1] === "-") {
|
|
2802
|
-
index = skipLineComment(statement, index + 2);
|
|
2803
|
-
continue;
|
|
2804
|
-
}
|
|
2805
|
-
if (statement[index] === "/" && statement[index + 1] === "*") {
|
|
2806
|
-
index = skipBlockComment(statement, index + 2);
|
|
2807
|
-
continue;
|
|
2808
|
-
}
|
|
2809
|
-
break;
|
|
2810
|
-
}
|
|
2811
|
-
return index;
|
|
2812
|
-
}
|
|
2813
|
-
function getPreviousWord(statement, start) {
|
|
2814
|
-
let index = start - 1;
|
|
2815
|
-
while (index >= 0 && isWhitespaceChar(statement[index] ?? "")) index -= 1;
|
|
2816
|
-
if (index < 0 || !isWordChar(statement[index] ?? "")) return "";
|
|
2817
|
-
const end = index;
|
|
2818
|
-
while (index >= 0 && isWordChar(statement[index] ?? "")) index -= 1;
|
|
2819
|
-
return statement.slice(index + 1, end + 1).toUpperCase();
|
|
2820
|
-
}
|
|
2821
|
-
function parseDollarQuotedLiteral(statement, start) {
|
|
2822
|
-
if (statement[start] !== "$") return null;
|
|
2823
|
-
let tagEnd = start + 1;
|
|
2824
|
-
if (tagEnd >= statement.length) return null;
|
|
2825
|
-
if (statement[tagEnd] === "$") {
|
|
2826
|
-
const delimiter2 = "$$";
|
|
2827
|
-
const bodyStart2 = tagEnd + 1;
|
|
2828
|
-
const bodyEnd2 = statement.indexOf(delimiter2, bodyStart2);
|
|
2829
|
-
if (bodyEnd2 < 0) return null;
|
|
2740
|
+
try {
|
|
2741
|
+
const content = readFileSync(configPath, "utf-8");
|
|
2742
|
+
const schemaGuardrailsBlock = extractNamedObjectBlock(content, "schemaGuardrails") ?? "";
|
|
2743
|
+
const pgSchemaDiffBlock = extractNamedObjectBlock(content, "pgSchemaDiff") ?? "";
|
|
2830
2744
|
return {
|
|
2831
|
-
|
|
2832
|
-
|
|
2745
|
+
...params.defaults,
|
|
2746
|
+
declarativeSqlDir: extractFirstStringMatch(schemaGuardrailsBlock, "declarativeSqlDir") ?? params.defaults.declarativeSqlDir,
|
|
2747
|
+
allowedDuplicateFunctions: extractAllowedDuplicateFunctions(
|
|
2748
|
+
schemaGuardrailsBlock,
|
|
2749
|
+
params.normalizers
|
|
2750
|
+
),
|
|
2751
|
+
generatedHeaderRewriteTargets: params.normalizers.normalizeFileList(
|
|
2752
|
+
extractStringArrayMatch(schemaGuardrailsBlock, "generatedHeaderRewriteTargets").map(
|
|
2753
|
+
(value) => params.normalizers.normalizePathForMatch(value)
|
|
2754
|
+
)
|
|
2755
|
+
),
|
|
2756
|
+
semanticWarnings: {
|
|
2757
|
+
threshold: extractFirstNumberMatch(schemaGuardrailsBlock, "threshold") ?? params.defaults.semanticWarnings.threshold,
|
|
2758
|
+
maxCandidates: extractFirstNumberMatch(schemaGuardrailsBlock, "maxCandidates") ?? params.defaults.semanticWarnings.maxCandidates,
|
|
2759
|
+
ignorePairs: new Set(
|
|
2760
|
+
extractStringArrayMatch(schemaGuardrailsBlock, "ignorePairs").map(
|
|
2761
|
+
(value) => params.normalizers.normalizeSuppressionPair(value)
|
|
2762
|
+
)
|
|
2763
|
+
)
|
|
2764
|
+
},
|
|
2765
|
+
tableHeaderMaxWidth: extractFirstNumberMatch(schemaGuardrailsBlock, "tableHeaderMaxWidth") ?? params.defaults.tableHeaderMaxWidth,
|
|
2766
|
+
excludeFromOrphanDetection: extractStringArrayMatch(
|
|
2767
|
+
pgSchemaDiffBlock,
|
|
2768
|
+
"excludeFromOrphanDetection"
|
|
2769
|
+
),
|
|
2770
|
+
idempotentSqlDir: extractFirstStringMatch(pgSchemaDiffBlock, "idempotentSqlDir") ?? params.defaults.idempotentSqlDir
|
|
2833
2771
|
};
|
|
2772
|
+
} catch {
|
|
2773
|
+
return null;
|
|
2834
2774
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// src/commands/db/sync/schema-guardrail-config.ts
|
|
2778
|
+
var DEFAULT_TABLE_HEADER_MAX_WIDTH = 160;
|
|
2779
|
+
var DEFAULT_SEMANTIC_WARNING_THRESHOLD = 0.55;
|
|
2780
|
+
var DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES = 3;
|
|
2781
|
+
var GENERIC_SIMILARITY_COLUMNS = /* @__PURE__ */ new Set([
|
|
2782
|
+
"id",
|
|
2783
|
+
"created_at",
|
|
2784
|
+
"updated_at",
|
|
2785
|
+
"deleted_at",
|
|
2786
|
+
"scope_id"
|
|
2787
|
+
]);
|
|
2788
|
+
function normalizePathForMatch(filePath) {
|
|
2789
|
+
return filePath.replaceAll("\\", "/");
|
|
2790
|
+
}
|
|
2791
|
+
function normalizeFunctionQualifiedName(value) {
|
|
2792
|
+
return value.trim().toLowerCase();
|
|
2793
|
+
}
|
|
2794
|
+
function normalizeAllowlistSignature(value) {
|
|
2795
|
+
return value.replace(/\s+/g, " ").trim();
|
|
2796
|
+
}
|
|
2797
|
+
function normalizeSuppressionPair(value) {
|
|
2798
|
+
return value.split("::").map((part) => part.trim().toLowerCase()).filter((part) => part.length > 0).sort((left, right) => left.localeCompare(right)).join("::");
|
|
2799
|
+
}
|
|
2800
|
+
function normalizeFileList(files) {
|
|
2801
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
2802
|
+
}
|
|
2803
|
+
function isSchemaGuardrailTextFallbackAllowed() {
|
|
2804
|
+
return Boolean(process.env.VITEST);
|
|
2805
|
+
}
|
|
2806
|
+
function createDefaultSchemaGuardrailConfig() {
|
|
2846
2807
|
return {
|
|
2847
|
-
|
|
2848
|
-
|
|
2808
|
+
declarativeSqlDir: "supabase/schemas/declarative",
|
|
2809
|
+
allowedDuplicateFunctions: [],
|
|
2810
|
+
generatedHeaderRewriteTargets: [],
|
|
2811
|
+
semanticWarnings: {
|
|
2812
|
+
threshold: DEFAULT_SEMANTIC_WARNING_THRESHOLD,
|
|
2813
|
+
maxCandidates: DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES,
|
|
2814
|
+
ignorePairs: /* @__PURE__ */ new Set()
|
|
2815
|
+
},
|
|
2816
|
+
tableHeaderMaxWidth: DEFAULT_TABLE_HEADER_MAX_WIDTH,
|
|
2817
|
+
excludeFromOrphanDetection: [],
|
|
2818
|
+
idempotentSqlDir: "supabase/schemas/idempotent"
|
|
2849
2819
|
};
|
|
2850
2820
|
}
|
|
2851
|
-
function
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2821
|
+
function loadAllowedDuplicatesFromSchemaOwnership(targetDir) {
|
|
2822
|
+
const candidates = [
|
|
2823
|
+
join(targetDir, "supabase", "schemas", "schema-ownership.json"),
|
|
2824
|
+
join(targetDir, "schema-ownership.json")
|
|
2825
|
+
];
|
|
2826
|
+
for (const filePath of candidates) {
|
|
2827
|
+
if (!existsSync(filePath)) continue;
|
|
2828
|
+
try {
|
|
2829
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
2830
|
+
const allowedDuplicates = raw?.rules?.allowed_duplicates;
|
|
2831
|
+
if (!Array.isArray(allowedDuplicates)) continue;
|
|
2832
|
+
return allowedDuplicates.filter(
|
|
2833
|
+
(entry) => typeof entry === "object" && entry !== null && "qualifiedName" in entry && typeof entry.qualifiedName === "string"
|
|
2834
|
+
).map((entry) => ({
|
|
2835
|
+
qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
|
|
2836
|
+
signature: normalizeAllowlistSignature(entry.signature ?? ""),
|
|
2837
|
+
reason: entry.reason ?? "Loaded from schema-ownership.json"
|
|
2838
|
+
}));
|
|
2839
|
+
} catch {
|
|
2870
2840
|
}
|
|
2871
|
-
body += char;
|
|
2872
|
-
index += 1;
|
|
2873
2841
|
}
|
|
2874
|
-
return
|
|
2842
|
+
return [];
|
|
2875
2843
|
}
|
|
2876
|
-
function
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2844
|
+
function loadSchemaGuardrailConfig(targetDir) {
|
|
2845
|
+
const defaults = createDefaultSchemaGuardrailConfig();
|
|
2846
|
+
try {
|
|
2847
|
+
const config = loadRunaConfig(targetDir);
|
|
2848
|
+
const databaseConfig = config.database ?? {};
|
|
2849
|
+
return {
|
|
2850
|
+
declarativeSqlDir: databaseConfig.schemaGuardrails?.declarativeSqlDir ?? defaults.declarativeSqlDir,
|
|
2851
|
+
allowedDuplicateFunctions: [
|
|
2852
|
+
...databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
|
|
2853
|
+
...entry,
|
|
2854
|
+
qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
|
|
2855
|
+
signature: normalizeAllowlistSignature(entry.signature),
|
|
2856
|
+
declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
|
|
2857
|
+
idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
|
|
2858
|
+
})) ?? [],
|
|
2859
|
+
// Fallback: merge schema-ownership.json allowed_duplicates if present
|
|
2860
|
+
...loadAllowedDuplicatesFromSchemaOwnership(targetDir)
|
|
2861
|
+
],
|
|
2862
|
+
generatedHeaderRewriteTargets: normalizeFileList(
|
|
2863
|
+
(databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
|
|
2864
|
+
(value) => normalizePathForMatch(value)
|
|
2865
|
+
)
|
|
2866
|
+
),
|
|
2867
|
+
semanticWarnings: {
|
|
2868
|
+
threshold: databaseConfig.schemaGuardrails?.semanticWarnings?.threshold ?? defaults.semanticWarnings.threshold,
|
|
2869
|
+
maxCandidates: databaseConfig.schemaGuardrails?.semanticWarnings?.maxCandidates ?? defaults.semanticWarnings.maxCandidates,
|
|
2870
|
+
ignorePairs: new Set(
|
|
2871
|
+
(databaseConfig.schemaGuardrails?.semanticWarnings?.ignorePairs ?? []).map(
|
|
2872
|
+
(value) => normalizeSuppressionPair(value)
|
|
2873
|
+
)
|
|
2874
|
+
)
|
|
2875
|
+
},
|
|
2876
|
+
tableHeaderMaxWidth: databaseConfig.schemaGuardrails?.tableHeaderMaxWidth ?? defaults.tableHeaderMaxWidth,
|
|
2877
|
+
excludeFromOrphanDetection: databaseConfig.pgSchemaDiff?.excludeFromOrphanDetection ?? [],
|
|
2878
|
+
idempotentSqlDir: databaseConfig.pgSchemaDiff?.idempotentSqlDir ?? defaults.idempotentSqlDir
|
|
2879
|
+
};
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
if (isSchemaGuardrailTextFallbackAllowed()) {
|
|
2882
|
+
return tryLoadSchemaGuardrailConfigFromText({
|
|
2883
|
+
targetDir,
|
|
2884
|
+
defaults,
|
|
2885
|
+
normalizers: {
|
|
2886
|
+
normalizeAllowlistSignature,
|
|
2887
|
+
normalizeFileList,
|
|
2888
|
+
normalizeFunctionQualifiedName,
|
|
2889
|
+
normalizePathForMatch,
|
|
2890
|
+
normalizeSuppressionPair
|
|
2891
|
+
}
|
|
2892
|
+
}) ?? defaults;
|
|
2885
2893
|
}
|
|
2886
|
-
|
|
2887
|
-
}
|
|
2888
|
-
const parsedDollar = parseDollarQuotedLiteral(statement, index);
|
|
2889
|
-
if (parsedDollar) {
|
|
2890
|
-
if (parsedDollar.body.trim().length > 0) fragments.push(parsedDollar.body);
|
|
2891
|
-
return parsedDollar.next;
|
|
2894
|
+
throw error;
|
|
2892
2895
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// src/commands/db/utils/duplicate-function-ownership-allowlist.ts
|
|
2899
|
+
function matchesDuplicateFunctionSignature(params) {
|
|
2900
|
+
return normalizeFunctionQualifiedName(params.entry.qualifiedName) === normalizeFunctionQualifiedName(params.finding.qualifiedName) && normalizeAllowlistSignature(params.entry.signature) === normalizeAllowlistSignature(params.finding.signature ?? "");
|
|
2901
|
+
}
|
|
2902
|
+
function matchesOptionalFilePath(params) {
|
|
2903
|
+
if (!params.configuredPath) {
|
|
2904
|
+
return true;
|
|
2900
2905
|
}
|
|
2901
|
-
return
|
|
2906
|
+
return params.definitionFiles.some(
|
|
2907
|
+
(filePath) => normalizePathForMatch(filePath) === params.configuredPath
|
|
2908
|
+
);
|
|
2902
2909
|
}
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2910
|
+
function matchesExpectedBodyHash(params) {
|
|
2911
|
+
if (!params.entry.expectedBodyHash || !params.bodyHashes) {
|
|
2912
|
+
return true;
|
|
2913
|
+
}
|
|
2914
|
+
const definitions = [
|
|
2915
|
+
...params.finding.declarativeDefinitions,
|
|
2916
|
+
...params.finding.idempotentDefinitions
|
|
2917
|
+
];
|
|
2918
|
+
if (definitions.length === 0) {
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
return definitions.every((definition) => {
|
|
2922
|
+
const key = `${definition.layer}:${definition.file}:${definition.line}`;
|
|
2923
|
+
return params.bodyHashes?.get(key) === params.entry.expectedBodyHash;
|
|
2924
|
+
});
|
|
2908
2925
|
}
|
|
2909
|
-
function
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
if (!
|
|
2914
|
-
|
|
2915
|
-
fragments.push(candidate);
|
|
2926
|
+
function isAllowlistedDuplicateFunction(params) {
|
|
2927
|
+
const { finding, allowlist, bodyHashes } = params;
|
|
2928
|
+
if (!finding.signature) return false;
|
|
2929
|
+
return allowlist.some((entry) => {
|
|
2930
|
+
if (!matchesDuplicateFunctionSignature({ entry, finding })) {
|
|
2931
|
+
return false;
|
|
2916
2932
|
}
|
|
2917
|
-
if (
|
|
2918
|
-
|
|
2933
|
+
if (!matchesOptionalFilePath({
|
|
2934
|
+
configuredPath: entry.declarativeFile,
|
|
2935
|
+
definitionFiles: finding.declarativeDefinitions.map((definition) => definition.file)
|
|
2936
|
+
})) {
|
|
2937
|
+
return false;
|
|
2919
2938
|
}
|
|
2920
|
-
|
|
2939
|
+
if (!matchesOptionalFilePath({
|
|
2940
|
+
configuredPath: entry.idempotentFile,
|
|
2941
|
+
definitionFiles: finding.idempotentDefinitions.map((definition) => definition.file)
|
|
2942
|
+
})) {
|
|
2943
|
+
return false;
|
|
2944
|
+
}
|
|
2945
|
+
return matchesExpectedBodyHash({ entry, finding, bodyHashes });
|
|
2946
|
+
});
|
|
2921
2947
|
}
|
|
2922
|
-
function
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2948
|
+
function filterAllowlistedDuplicateFunctions(params) {
|
|
2949
|
+
return params.findings.filter(
|
|
2950
|
+
(finding) => !isAllowlistedDuplicateFunction({
|
|
2951
|
+
finding,
|
|
2952
|
+
allowlist: params.allowlist,
|
|
2953
|
+
bodyHashes: params.bodyHashes
|
|
2954
|
+
})
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
|
|
2959
|
+
async function runDuplicateFunctionOwnershipCheck(result, logger4, step) {
|
|
2960
|
+
logger4.step("Checking duplicate function ownership", step.next());
|
|
2961
|
+
const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
|
|
2962
|
+
let findings = analysis.findings;
|
|
2963
|
+
try {
|
|
2964
|
+
const allowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
|
|
2965
|
+
findings = filterAllowlistedDuplicateFunctions({
|
|
2966
|
+
findings,
|
|
2967
|
+
allowlist
|
|
2968
|
+
});
|
|
2969
|
+
} catch {
|
|
2935
2970
|
}
|
|
2936
|
-
if (
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2971
|
+
if (findings.length === 0) {
|
|
2972
|
+
logger4.success("No duplicate declarative/idempotent function ownership detected");
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
result.passed = false;
|
|
2976
|
+
result.errors.push(`Found ${findings.length} duplicate function ownership finding(s)`);
|
|
2977
|
+
logger4.error(`Found ${findings.length} duplicate function ownership finding(s):`);
|
|
2978
|
+
logger4.info(` ${analysis.contractNote}`);
|
|
2979
|
+
for (const finding of findings) {
|
|
2980
|
+
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
2981
|
+
logger4.info(` [ERROR] ${formatted.summary}`);
|
|
2982
|
+
for (const location of formatted.declarativeLocations) {
|
|
2983
|
+
logger4.info(` declarative: ${location}`);
|
|
2940
2984
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2985
|
+
for (const location of formatted.idempotentLocations) {
|
|
2986
|
+
logger4.info(` idempotent: ${location}`);
|
|
2943
2987
|
}
|
|
2944
|
-
|
|
2945
|
-
return true;
|
|
2988
|
+
logger4.info(` ${formatted.suggestion}`);
|
|
2946
2989
|
}
|
|
2947
|
-
return false;
|
|
2948
2990
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
return
|
|
2991
|
+
|
|
2992
|
+
// src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
|
|
2993
|
+
init_esm_shims();
|
|
2994
|
+
|
|
2995
|
+
// src/commands/db/commands/db-sync/precheck-helpers.ts
|
|
2996
|
+
init_esm_shims();
|
|
2997
|
+
var SHOW_ALLOWLIST_REPORT = process.env.RUNA_DB_PRECHECK_ALLOWLIST_REPORT === "1";
|
|
2998
|
+
var DIRECTORY_PLACEMENT_WARNING_PREFIX = " [misplacement] ";
|
|
2999
|
+
function applyStrictModeToReport(report, strict) {
|
|
3000
|
+
if (!strict) return report;
|
|
3001
|
+
return {
|
|
3002
|
+
blockers: [...report.blockers, ...report.warnings],
|
|
3003
|
+
warnings: []
|
|
3004
|
+
};
|
|
2959
3005
|
}
|
|
2960
|
-
function
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
state.index += 1;
|
|
2969
|
-
return true;
|
|
2970
|
-
}
|
|
2971
|
-
return false;
|
|
3006
|
+
function formatAllowlistReason({
|
|
3007
|
+
label,
|
|
3008
|
+
ruleId,
|
|
3009
|
+
reason,
|
|
3010
|
+
rule
|
|
3011
|
+
}) {
|
|
3012
|
+
const meta = rule ? formatAllowlistMetadata(rule) : "";
|
|
3013
|
+
return meta ? `[allowlist:${label}] ${ruleId}: ${reason} (${meta})` : `[allowlist:${label}] ${ruleId}: ${reason}`;
|
|
2972
3014
|
}
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3015
|
+
|
|
3016
|
+
// src/commands/db/commands/db-sync/directory-placement-check.ts
|
|
3017
|
+
init_esm_shims();
|
|
3018
|
+
|
|
3019
|
+
// src/commands/db/utils/schema-precheck-budget.ts
|
|
3020
|
+
init_esm_shims();
|
|
3021
|
+
var ONE_MB = 1024 * 1024;
|
|
3022
|
+
var ONE_GB = 1024 * ONE_MB;
|
|
3023
|
+
function parseIntEnv(name, fallback, min, max) {
|
|
3024
|
+
const raw = process.env[name];
|
|
3025
|
+
if (!raw) return fallback;
|
|
3026
|
+
const value = Number.parseInt(raw, 10);
|
|
3027
|
+
if (!Number.isFinite(value) || Number.isNaN(value)) return fallback;
|
|
3028
|
+
const normalized = Math.trunc(value);
|
|
3029
|
+
if (normalized < min) return min;
|
|
3030
|
+
if (normalized > max) return max;
|
|
3031
|
+
return normalized;
|
|
2983
3032
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3033
|
+
var RUNA_SCHEMA_PRECHECK_MAX_FILES = parseIntEnv(
|
|
3034
|
+
"RUNA_SCHEMA_PRECHECK_MAX_FILES",
|
|
3035
|
+
3e3,
|
|
3036
|
+
100,
|
|
3037
|
+
2e4
|
|
3038
|
+
);
|
|
3039
|
+
var RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES = parseIntEnv(
|
|
3040
|
+
"RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES",
|
|
3041
|
+
512 * ONE_MB,
|
|
3042
|
+
32 * ONE_MB,
|
|
3043
|
+
20 * ONE_GB
|
|
3044
|
+
);
|
|
3045
|
+
var RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES = parseIntEnv(
|
|
3046
|
+
"RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES",
|
|
3047
|
+
64 * ONE_MB,
|
|
3048
|
+
1 * ONE_MB,
|
|
3049
|
+
512 * ONE_MB
|
|
3050
|
+
);
|
|
3051
|
+
var RUNA_PLAN_PRECHECK_MAX_BYTES = parseIntEnv(
|
|
3052
|
+
"RUNA_PLAN_PRECHECK_MAX_BYTES",
|
|
3053
|
+
128 * ONE_MB,
|
|
3054
|
+
8 * ONE_MB,
|
|
3055
|
+
4 * ONE_GB
|
|
3056
|
+
);
|
|
3057
|
+
var RUNA_PLAN_PRECHECK_MAX_STATEMENTS = parseIntEnv(
|
|
3058
|
+
"RUNA_PLAN_PRECHECK_MAX_STATEMENTS",
|
|
3059
|
+
4e3,
|
|
3060
|
+
100,
|
|
3061
|
+
5e4
|
|
3062
|
+
);
|
|
3063
|
+
function formatBytes2(value) {
|
|
3064
|
+
if (value >= ONE_MB * 1024) return `${(value / (ONE_MB * 1024)).toFixed(1)} GB`;
|
|
3065
|
+
if (value >= ONE_MB) return `${(value / ONE_MB).toFixed(1)} MB`;
|
|
3066
|
+
return `${(value / 1024).toFixed(1)} KB`;
|
|
2995
3067
|
}
|
|
2996
|
-
function
|
|
2997
|
-
|
|
2998
|
-
const executeCapture = [];
|
|
2999
|
-
const parsed = parsePotentialEmbeddedSqlLiteral(statement, cursor, executeCapture);
|
|
3000
|
-
if (parsed <= cursor || executeCapture.length === 0) return null;
|
|
3001
|
-
return { fragment: executeCapture[0] ?? "", nextIndex: parsed };
|
|
3068
|
+
function buildBudgetExceededReason(context, files, bytes, maxFiles, maxBytes) {
|
|
3069
|
+
return `${context}: budget exceeded (files=${files}/${maxFiles}, bytes=${formatBytes2(bytes)}/${formatBytes2(maxBytes)}). Stop and review with stricter scoping or increase RUNA_SCHEMA_PRECHECK_MAX_* settings only after approval.`;
|
|
3002
3070
|
}
|
|
3003
|
-
function
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3071
|
+
function createSchemaPrecheckBudgetState() {
|
|
3072
|
+
return {
|
|
3073
|
+
scannedFiles: 0,
|
|
3074
|
+
scannedBytes: 0,
|
|
3075
|
+
maxFiles: RUNA_SCHEMA_PRECHECK_MAX_FILES,
|
|
3076
|
+
maxBytes: RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES,
|
|
3077
|
+
maxFileBytes: RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function shouldAbortSchemaPrecheckForBudget(state, filePath) {
|
|
3081
|
+
const size = statSync(filePath).size;
|
|
3082
|
+
if (size > state.maxFileBytes) {
|
|
3083
|
+
return `Unscanned schema file ${filePath}: ${formatBytes2(size)} exceeds per-file limit ${formatBytes2(
|
|
3084
|
+
state.maxFileBytes
|
|
3085
|
+
)}`;
|
|
3015
3086
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
addFragment(executeLiteral.fragment);
|
|
3019
|
-
state.index = executeLiteral.nextIndex;
|
|
3020
|
-
return true;
|
|
3087
|
+
if (state.scannedFiles + 1 > state.maxFiles) {
|
|
3088
|
+
return `Schema file scan budget exceeds ${state.maxFiles} files at ${path12.basename(filePath)}.`;
|
|
3021
3089
|
}
|
|
3022
|
-
state.
|
|
3023
|
-
|
|
3024
|
-
}
|
|
3025
|
-
function collectEmbeddableSqlFragments(statement, fragments, depth, maxDepth, seen) {
|
|
3026
|
-
if (!statement || statement.trim().length === 0) return;
|
|
3027
|
-
if (depth > maxDepth) return;
|
|
3028
|
-
const normalized = statement.toUpperCase();
|
|
3029
|
-
const state = {
|
|
3030
|
-
index: 0,
|
|
3031
|
-
inSingleQuote: false,
|
|
3032
|
-
inDoubleQuote: false
|
|
3033
|
-
};
|
|
3034
|
-
const addFragment = createFragmentCollector(fragments, depth, maxDepth, seen);
|
|
3035
|
-
while (state.index < normalized.length) {
|
|
3036
|
-
if (tryAdvanceWithinQuotedText(normalized, state)) continue;
|
|
3037
|
-
if (tryAdvanceComment(normalized, state)) continue;
|
|
3038
|
-
if (tryEnterQuotedText(normalized, state)) continue;
|
|
3039
|
-
if (tryConsumeDollarQuotedFragment(statement, state, addFragment)) continue;
|
|
3040
|
-
if (tryConsumeWordToken(statement, normalized, state, addFragment)) continue;
|
|
3041
|
-
state.index += 1;
|
|
3090
|
+
const projectedBytes = state.scannedBytes + size;
|
|
3091
|
+
if (projectedBytes > state.maxBytes) {
|
|
3092
|
+
return `Schema scan budget exceeds total size limit ${formatBytes2(state.maxBytes)}.`;
|
|
3042
3093
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
collectEmbeddableSqlFragments(
|
|
3047
|
-
statement,
|
|
3048
|
-
fragments,
|
|
3049
|
-
0,
|
|
3050
|
-
EMBEDDABLE_SQL_MAX_RECURSION_DEPTH,
|
|
3051
|
-
/* @__PURE__ */ new Set()
|
|
3052
|
-
);
|
|
3053
|
-
return dedupeAndSort(fragments);
|
|
3054
|
-
}
|
|
3055
|
-
function normalizeSqlForPlacementCheck(content) {
|
|
3056
|
-
return blankDollarQuotedBodies(stripSqlComments(content)).toUpperCase();
|
|
3057
|
-
}
|
|
3058
|
-
function isPartitionOfCreateTable(statement) {
|
|
3059
|
-
return /\bPARTITION\s+OF\b/.test(statement);
|
|
3060
|
-
}
|
|
3061
|
-
function dedupeAndSort(lines) {
|
|
3062
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3063
|
-
return [...lines].sort((a, b) => a.localeCompare(b)).filter((line) => {
|
|
3064
|
-
if (seen.has(line)) return false;
|
|
3065
|
-
seen.add(line);
|
|
3066
|
-
return true;
|
|
3067
|
-
});
|
|
3094
|
+
state.scannedBytes = projectedBytes;
|
|
3095
|
+
state.scannedFiles += 1;
|
|
3096
|
+
return null;
|
|
3068
3097
|
}
|
|
3069
3098
|
|
|
3070
|
-
// src/commands/db/
|
|
3099
|
+
// src/commands/db/utils/sql-file-collector.ts
|
|
3071
3100
|
init_esm_shims();
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
function getDirectoryRiskWeight(level) {
|
|
3086
|
-
return DIRECTORY_RISK_WEIGHT[level];
|
|
3101
|
+
var IGNORED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
3102
|
+
"compat",
|
|
3103
|
+
"archive",
|
|
3104
|
+
"legacy",
|
|
3105
|
+
"deprecated",
|
|
3106
|
+
"backup",
|
|
3107
|
+
"node_modules",
|
|
3108
|
+
".git",
|
|
3109
|
+
"dist",
|
|
3110
|
+
"build"
|
|
3111
|
+
]);
|
|
3112
|
+
function shouldSkipDirectory(name) {
|
|
3113
|
+
return IGNORED_DIRECTORY_NAMES.has(name.toLowerCase());
|
|
3087
3114
|
}
|
|
3088
|
-
function
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3115
|
+
function scanDirectory(dir, queue, sqlFiles) {
|
|
3116
|
+
try {
|
|
3117
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
3118
|
+
for (const entry of entries) {
|
|
3119
|
+
const fullPath = path12.join(dir, entry.name);
|
|
3120
|
+
if (entry.isDirectory()) {
|
|
3121
|
+
if (!shouldSkipDirectory(entry.name)) queue.push(fullPath);
|
|
3122
|
+
} else if (entry.isFile() && entry.name.endsWith(".sql")) {
|
|
3123
|
+
sqlFiles.push(fullPath);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
} catch {
|
|
3092
3127
|
}
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3128
|
+
}
|
|
3129
|
+
function* collectSqlFilesRecursively(baseDir) {
|
|
3130
|
+
const queue = [baseDir];
|
|
3131
|
+
const sqlFiles = [];
|
|
3132
|
+
let index = 0;
|
|
3133
|
+
while (index < queue.length) {
|
|
3134
|
+
const currentDir = queue[index++];
|
|
3135
|
+
if (!currentDir) continue;
|
|
3136
|
+
scanDirectory(currentDir, queue, sqlFiles);
|
|
3137
|
+
}
|
|
3138
|
+
for (const file of sqlFiles) {
|
|
3139
|
+
yield file;
|
|
3097
3140
|
}
|
|
3098
|
-
return left.message.localeCompare(right.message);
|
|
3099
3141
|
}
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
return byPriority[0];
|
|
3142
|
+
|
|
3143
|
+
// src/commands/db/commands/db-sync/boundary-classifier.ts
|
|
3144
|
+
init_esm_shims();
|
|
3145
|
+
|
|
3146
|
+
// src/commands/db/commands/db-sync/sql-parser.ts
|
|
3147
|
+
init_esm_shims();
|
|
3148
|
+
function isWordChar(char) {
|
|
3149
|
+
const code = char.charCodeAt(0);
|
|
3150
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_";
|
|
3110
3151
|
}
|
|
3111
|
-
function
|
|
3112
|
-
return
|
|
3152
|
+
function isWhitespaceChar(char) {
|
|
3153
|
+
return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
3113
3154
|
}
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
}
|
|
3155
|
+
function parseWordAt(statement, start) {
|
|
3156
|
+
if (start < 0 || start >= statement.length) return null;
|
|
3157
|
+
const first = statement[start];
|
|
3158
|
+
if (!first || !isWordChar(first)) return null;
|
|
3159
|
+
let end = start + 1;
|
|
3160
|
+
while (end < statement.length && isWordChar(statement[end] ?? "")) {
|
|
3161
|
+
end += 1;
|
|
3122
3162
|
}
|
|
3123
|
-
return
|
|
3124
|
-
const priorityDelta = getDirectoryRiskWeight(right.level) - getDirectoryRiskWeight(left.level);
|
|
3125
|
-
if (priorityDelta !== 0) return priorityDelta;
|
|
3126
|
-
const levelDelta = DIRECTORY_RISK_ORDER.indexOf(right.level) - DIRECTORY_RISK_ORDER.indexOf(left.level);
|
|
3127
|
-
if (levelDelta !== 0) return levelDelta;
|
|
3128
|
-
const leftLine = left.line ?? 0;
|
|
3129
|
-
const rightLine = right.line ?? 0;
|
|
3130
|
-
if (leftLine !== rightLine) return leftLine - rightLine;
|
|
3131
|
-
return left.message.localeCompare(right.message);
|
|
3132
|
-
});
|
|
3163
|
+
return { value: statement.slice(start, end), start, end };
|
|
3133
3164
|
}
|
|
3134
|
-
function
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
for (const item of items) {
|
|
3139
|
-
logger4.info(` \u2022 ${item}`);
|
|
3165
|
+
function skipWhitespace(statement, start) {
|
|
3166
|
+
let index = start;
|
|
3167
|
+
while (index < statement.length && isWhitespaceChar(statement[index] ?? "")) {
|
|
3168
|
+
index += 1;
|
|
3140
3169
|
}
|
|
3170
|
+
return index;
|
|
3141
3171
|
}
|
|
3142
|
-
function
|
|
3143
|
-
|
|
3172
|
+
function skipLineComment(statement, start) {
|
|
3173
|
+
let index = start;
|
|
3174
|
+
while (index < statement.length && statement[index] !== "\n") {
|
|
3175
|
+
index += 1;
|
|
3176
|
+
}
|
|
3177
|
+
return index;
|
|
3144
3178
|
}
|
|
3145
|
-
function
|
|
3146
|
-
|
|
3147
|
-
|
|
3179
|
+
function skipBlockComment(statement, start) {
|
|
3180
|
+
let index = start;
|
|
3181
|
+
while (index + 1 < statement.length && !(statement[index] === "*" && statement[index + 1] === "/")) {
|
|
3182
|
+
index += 1;
|
|
3183
|
+
}
|
|
3184
|
+
return index + 1 < statement.length ? index + 2 : index;
|
|
3148
3185
|
}
|
|
3149
|
-
function
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
const
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
if (separator <= 0) {
|
|
3156
|
-
const existing2 = grouped.get(withoutPrefix);
|
|
3157
|
-
if (!existing2) {
|
|
3158
|
-
grouped.set(withoutPrefix, { message: withoutPrefix, count: 1, samples: [] });
|
|
3159
|
-
} else {
|
|
3160
|
-
existing2.count += 1;
|
|
3161
|
-
}
|
|
3186
|
+
function skipWhitespaceAndComments(statement, start) {
|
|
3187
|
+
let index = Math.max(0, start);
|
|
3188
|
+
while (index < statement.length) {
|
|
3189
|
+
const afterWhitespace = skipWhitespace(statement, index);
|
|
3190
|
+
if (afterWhitespace !== index) {
|
|
3191
|
+
index = afterWhitespace;
|
|
3162
3192
|
continue;
|
|
3163
3193
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
const existing = grouped.get(message);
|
|
3167
|
-
if (!existing) {
|
|
3168
|
-
grouped.set(message, { message, count: 1, samples: source ? [source] : [] });
|
|
3194
|
+
if (statement[index] === "-" && statement[index + 1] === "-") {
|
|
3195
|
+
index = skipLineComment(statement, index + 2);
|
|
3169
3196
|
continue;
|
|
3170
3197
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
return [...grouped.values()].sort((left, right) => {
|
|
3177
|
-
if (right.count !== left.count) return right.count - left.count;
|
|
3178
|
-
return left.message.localeCompare(right.message);
|
|
3179
|
-
});
|
|
3180
|
-
}
|
|
3181
|
-
function printCompactSummary(logger4, title, rawItems, topLimit) {
|
|
3182
|
-
if (rawItems.length === 0) {
|
|
3183
|
-
return;
|
|
3184
|
-
}
|
|
3185
|
-
const findingSummary = buildCompactFindingSummary(rawItems);
|
|
3186
|
-
const top = findingSummary.slice(0, Math.max(1, topLimit));
|
|
3187
|
-
logger4.warn(`
|
|
3188
|
-
${title} (total ${rawItems.length} item(s), ${top.length} reason pattern(s))`);
|
|
3189
|
-
for (const finding of top) {
|
|
3190
|
-
logger4.info(` - ${finding.count}x ${finding.message}`);
|
|
3191
|
-
if (finding.samples.length > 0) {
|
|
3192
|
-
logger4.info(` samples:`);
|
|
3193
|
-
for (const sample of finding.samples.slice(0, 2)) {
|
|
3194
|
-
logger4.info(` - ${sample}`);
|
|
3195
|
-
}
|
|
3198
|
+
if (statement[index] === "/" && statement[index + 1] === "*") {
|
|
3199
|
+
index = skipBlockComment(statement, index + 2);
|
|
3200
|
+
continue;
|
|
3196
3201
|
}
|
|
3202
|
+
break;
|
|
3197
3203
|
}
|
|
3198
|
-
|
|
3199
|
-
if (extra > 0) {
|
|
3200
|
-
logger4.info(` ... and ${extra} more reason patterns`);
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
// src/commands/db/commands/db-sync/boundary-classifier.ts
|
|
3205
|
-
function isBoundaryRelevantDdlStatement(statement) {
|
|
3206
|
-
return /^\s*(?:CREATE|ALTER|DROP|RENAME|TRUNCATE|COMMENT)\b/i.test(statement);
|
|
3204
|
+
return index;
|
|
3207
3205
|
}
|
|
3208
|
-
function
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
if (
|
|
3212
|
-
|
|
3206
|
+
function getPreviousWord(statement, start) {
|
|
3207
|
+
let index = start - 1;
|
|
3208
|
+
while (index >= 0 && isWhitespaceChar(statement[index] ?? "")) index -= 1;
|
|
3209
|
+
if (index < 0 || !isWordChar(statement[index] ?? "")) return "";
|
|
3210
|
+
const end = index;
|
|
3211
|
+
while (index >= 0 && isWordChar(statement[index] ?? "")) index -= 1;
|
|
3212
|
+
return statement.slice(index + 1, end + 1).toUpperCase();
|
|
3213
3213
|
}
|
|
3214
|
-
function
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
if (
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
};
|
|
3224
|
-
}
|
|
3225
|
-
if (!inDeclarative && !inIdempotent) {
|
|
3226
|
-
return {
|
|
3227
|
-
file,
|
|
3228
|
-
level: "medium",
|
|
3229
|
-
message: `Unrecognized declarative object "${object}" detected; review placement (runtime/setup candidate)`
|
|
3230
|
-
};
|
|
3231
|
-
}
|
|
3232
|
-
return null;
|
|
3233
|
-
}
|
|
3234
|
-
if (inDeclarative && !inIdempotent) {
|
|
3214
|
+
function parseDollarQuotedLiteral(statement, start) {
|
|
3215
|
+
if (statement[start] !== "$") return null;
|
|
3216
|
+
let tagEnd = start + 1;
|
|
3217
|
+
if (tagEnd >= statement.length) return null;
|
|
3218
|
+
if (statement[tagEnd] === "$") {
|
|
3219
|
+
const delimiter2 = "$$";
|
|
3220
|
+
const bodyStart2 = tagEnd + 1;
|
|
3221
|
+
const bodyEnd2 = statement.indexOf(delimiter2, bodyStart2);
|
|
3222
|
+
if (bodyEnd2 < 0) return null;
|
|
3235
3223
|
return {
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
message: `Object "${object}" is declarative-preferred and should be in supabase/schemas/declarative`
|
|
3224
|
+
body: statement.slice(bodyStart2, bodyEnd2),
|
|
3225
|
+
next: bodyEnd2 + delimiter2.length
|
|
3239
3226
|
};
|
|
3240
3227
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
message: `Unrecognized idempotent object "${object}" detected; review placement (likely declarative SSOT object)`
|
|
3246
|
-
};
|
|
3228
|
+
const firstChar = statement[tagEnd];
|
|
3229
|
+
if (!firstChar || !/[A-Za-z_]/.test(firstChar)) return null;
|
|
3230
|
+
while (tagEnd < statement.length && /[A-Za-z0-9_]/.test(statement[tagEnd] ?? "")) {
|
|
3231
|
+
tagEnd += 1;
|
|
3247
3232
|
}
|
|
3248
|
-
return null;
|
|
3233
|
+
if (statement[tagEnd] !== "$") return null;
|
|
3234
|
+
const tag = statement.slice(start + 1, tagEnd);
|
|
3235
|
+
const delimiter = `$${tag}$`;
|
|
3236
|
+
const bodyStart = tagEnd + 1;
|
|
3237
|
+
const bodyEnd = statement.indexOf(delimiter, bodyStart);
|
|
3238
|
+
if (bodyEnd < 0) return null;
|
|
3239
|
+
return {
|
|
3240
|
+
body: statement.slice(bodyStart, bodyEnd),
|
|
3241
|
+
next: bodyEnd + delimiter.length
|
|
3242
|
+
};
|
|
3249
3243
|
}
|
|
3250
|
-
function
|
|
3251
|
-
|
|
3244
|
+
function parseSingleQuotedLiteral(statement, start) {
|
|
3245
|
+
if (statement[start] !== "'") return null;
|
|
3246
|
+
let index = start + 1;
|
|
3247
|
+
let body = "";
|
|
3248
|
+
while (index < statement.length) {
|
|
3249
|
+
const char = statement[index];
|
|
3250
|
+
const next = statement[index + 1] ?? "";
|
|
3251
|
+
if (char === "'") {
|
|
3252
|
+
if (next === "'") {
|
|
3253
|
+
body += "'";
|
|
3254
|
+
index += 2;
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
return { body, next: index + 1 };
|
|
3258
|
+
}
|
|
3259
|
+
if (char === "\\" && index + 1 < statement.length) {
|
|
3260
|
+
body += next;
|
|
3261
|
+
index += 2;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
body += char;
|
|
3265
|
+
index += 1;
|
|
3266
|
+
}
|
|
3267
|
+
return null;
|
|
3268
|
+
}
|
|
3269
|
+
function parsePotentialEmbeddedSqlLiteral(statement, start, fragments) {
|
|
3270
|
+
const index = skipWhitespaceAndComments(statement, start);
|
|
3271
|
+
if (index >= statement.length) return index;
|
|
3272
|
+
const maybePrefix = (statement[index] ?? "").toUpperCase();
|
|
3273
|
+
if (maybePrefix === "E" && statement[index + 1] === "'") {
|
|
3274
|
+
const parsed = parseSingleQuotedLiteral(statement, index + 1);
|
|
3275
|
+
if (parsed && parsed.body.trim().length > 0) {
|
|
3276
|
+
fragments.push(parsed.body);
|
|
3277
|
+
return parsed.next;
|
|
3278
|
+
}
|
|
3279
|
+
return index + 1;
|
|
3280
|
+
}
|
|
3281
|
+
const parsedDollar = parseDollarQuotedLiteral(statement, index);
|
|
3282
|
+
if (parsedDollar) {
|
|
3283
|
+
if (parsedDollar.body.trim().length > 0) fragments.push(parsedDollar.body);
|
|
3284
|
+
return parsedDollar.next;
|
|
3285
|
+
}
|
|
3286
|
+
if (statement[index] === "'") {
|
|
3287
|
+
const parsed = parseSingleQuotedLiteral(statement, index);
|
|
3288
|
+
if (parsed && parsed.body.trim().length > 0) {
|
|
3289
|
+
fragments.push(parsed.body.replace(/\\'/g, "'"));
|
|
3290
|
+
return parsed.next;
|
|
3291
|
+
}
|
|
3292
|
+
return index + 1;
|
|
3293
|
+
}
|
|
3294
|
+
return index + 1;
|
|
3295
|
+
}
|
|
3296
|
+
var EMBEDDABLE_SQL_HINT_WORDS = /* @__PURE__ */ new Set(["DO", "AS", "EXECUTE", "FORMAT"]);
|
|
3297
|
+
var EMBEDDABLE_SQL_MAX_RECURSION_DEPTH = 3;
|
|
3298
|
+
var PLPGSQL_RECURSIVE_PATTERN = /\b(?:DO|EXECUTE|FORMAT)\b/i;
|
|
3299
|
+
function shouldRecurseIntoEmbeddableFragment(statement) {
|
|
3300
|
+
return PLPGSQL_RECURSIVE_PATTERN.test(statement);
|
|
3301
|
+
}
|
|
3302
|
+
function createFragmentCollector(fragments, depth, maxDepth, seen) {
|
|
3303
|
+
return (fragment) => {
|
|
3304
|
+
const candidate = fragment.trim();
|
|
3305
|
+
if (!candidate) return;
|
|
3306
|
+
if (!seen.has(candidate)) {
|
|
3307
|
+
seen.add(candidate);
|
|
3308
|
+
fragments.push(candidate);
|
|
3309
|
+
}
|
|
3310
|
+
if (depth < maxDepth && shouldRecurseIntoEmbeddableFragment(candidate)) {
|
|
3311
|
+
collectEmbeddableSqlFragments(candidate, fragments, depth + 1, maxDepth, seen);
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
function tryAdvanceWithinQuotedText(normalized, state) {
|
|
3316
|
+
const char = normalized[state.index];
|
|
3317
|
+
if (!char) return false;
|
|
3318
|
+
if (state.inSingleQuote) {
|
|
3319
|
+
if (char === "'" && normalized[state.index + 1] === "'") {
|
|
3320
|
+
state.index += 2;
|
|
3321
|
+
return true;
|
|
3322
|
+
}
|
|
3323
|
+
if (char === "'") {
|
|
3324
|
+
state.inSingleQuote = false;
|
|
3325
|
+
}
|
|
3326
|
+
state.index += 1;
|
|
3327
|
+
return true;
|
|
3328
|
+
}
|
|
3329
|
+
if (state.inDoubleQuote) {
|
|
3330
|
+
if (char === '"' && normalized[state.index + 1] === '"') {
|
|
3331
|
+
state.index += 2;
|
|
3332
|
+
return true;
|
|
3333
|
+
}
|
|
3334
|
+
if (char === '"') {
|
|
3335
|
+
state.inDoubleQuote = false;
|
|
3336
|
+
}
|
|
3337
|
+
state.index += 1;
|
|
3338
|
+
return true;
|
|
3339
|
+
}
|
|
3340
|
+
return false;
|
|
3341
|
+
}
|
|
3342
|
+
function tryAdvanceComment(normalized, state) {
|
|
3343
|
+
if (normalized[state.index] === "-" && normalized[state.index + 1] === "-") {
|
|
3344
|
+
state.index = skipLineComment(normalized, state.index + 2);
|
|
3345
|
+
return true;
|
|
3346
|
+
}
|
|
3347
|
+
if (normalized[state.index] === "/" && normalized[state.index + 1] === "*") {
|
|
3348
|
+
state.index = skipBlockComment(normalized, state.index + 2);
|
|
3349
|
+
return true;
|
|
3350
|
+
}
|
|
3351
|
+
return false;
|
|
3352
|
+
}
|
|
3353
|
+
function tryEnterQuotedText(normalized, state) {
|
|
3354
|
+
if (normalized[state.index] === "'") {
|
|
3355
|
+
state.inSingleQuote = true;
|
|
3356
|
+
state.index += 1;
|
|
3357
|
+
return true;
|
|
3358
|
+
}
|
|
3359
|
+
if (normalized[state.index] === '"') {
|
|
3360
|
+
state.inDoubleQuote = true;
|
|
3361
|
+
state.index += 1;
|
|
3362
|
+
return true;
|
|
3363
|
+
}
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
function tryConsumeDollarQuotedFragment(statement, state, addFragment) {
|
|
3367
|
+
if (statement[state.index] !== "$") return false;
|
|
3368
|
+
const parsedDollar = parseDollarQuotedLiteral(statement, state.index);
|
|
3369
|
+
if (!parsedDollar) return false;
|
|
3370
|
+
const previousWord = getPreviousWord(statement, state.index);
|
|
3371
|
+
if (previousWord && EMBEDDABLE_SQL_HINT_WORDS.has(previousWord)) {
|
|
3372
|
+
addFragment(parsedDollar.body);
|
|
3373
|
+
}
|
|
3374
|
+
state.index = parsedDollar.next;
|
|
3375
|
+
return true;
|
|
3376
|
+
}
|
|
3377
|
+
function tryExtractExecuteFormatLiteral(statement, normalized, tokenEnd) {
|
|
3378
|
+
let cursor = skipWhitespaceAndComments(statement, tokenEnd);
|
|
3379
|
+
const maybeFormat = parseWordAt(normalized, cursor);
|
|
3380
|
+
if (!maybeFormat || maybeFormat.value !== "FORMAT") return null;
|
|
3381
|
+
cursor = skipWhitespaceAndComments(statement, maybeFormat.end);
|
|
3382
|
+
if (statement[cursor] !== "(") return null;
|
|
3383
|
+
const afterParen = skipWhitespaceAndComments(statement, cursor + 1);
|
|
3384
|
+
const formatCapture = [];
|
|
3385
|
+
const parsed = parsePotentialEmbeddedSqlLiteral(statement, afterParen, formatCapture);
|
|
3386
|
+
if (parsed <= afterParen || formatCapture.length === 0) return null;
|
|
3387
|
+
return { fragment: formatCapture[0] ?? "", nextIndex: parsed };
|
|
3388
|
+
}
|
|
3389
|
+
function tryExtractExecuteLiteral(statement, tokenEnd) {
|
|
3390
|
+
const cursor = skipWhitespaceAndComments(statement, tokenEnd);
|
|
3391
|
+
const executeCapture = [];
|
|
3392
|
+
const parsed = parsePotentialEmbeddedSqlLiteral(statement, cursor, executeCapture);
|
|
3393
|
+
if (parsed <= cursor || executeCapture.length === 0) return null;
|
|
3394
|
+
return { fragment: executeCapture[0] ?? "", nextIndex: parsed };
|
|
3395
|
+
}
|
|
3396
|
+
function tryConsumeWordToken(statement, normalized, state, addFragment) {
|
|
3397
|
+
const token = parseWordAt(normalized, state.index);
|
|
3398
|
+
if (!token) return false;
|
|
3399
|
+
if (token.value !== "EXECUTE") {
|
|
3400
|
+
state.index = token.end;
|
|
3401
|
+
return true;
|
|
3402
|
+
}
|
|
3403
|
+
const formatLiteral = tryExtractExecuteFormatLiteral(statement, normalized, token.end);
|
|
3404
|
+
if (formatLiteral) {
|
|
3405
|
+
addFragment(formatLiteral.fragment);
|
|
3406
|
+
state.index = formatLiteral.nextIndex;
|
|
3407
|
+
return true;
|
|
3408
|
+
}
|
|
3409
|
+
const executeLiteral = tryExtractExecuteLiteral(statement, token.end);
|
|
3410
|
+
if (executeLiteral) {
|
|
3411
|
+
addFragment(executeLiteral.fragment);
|
|
3412
|
+
state.index = executeLiteral.nextIndex;
|
|
3413
|
+
return true;
|
|
3414
|
+
}
|
|
3415
|
+
state.index = token.end;
|
|
3416
|
+
return true;
|
|
3417
|
+
}
|
|
3418
|
+
function collectEmbeddableSqlFragments(statement, fragments, depth, maxDepth, seen) {
|
|
3419
|
+
if (!statement || statement.trim().length === 0) return;
|
|
3420
|
+
if (depth > maxDepth) return;
|
|
3421
|
+
const normalized = statement.toUpperCase();
|
|
3422
|
+
const state = {
|
|
3423
|
+
index: 0,
|
|
3424
|
+
inSingleQuote: false,
|
|
3425
|
+
inDoubleQuote: false
|
|
3426
|
+
};
|
|
3427
|
+
const addFragment = createFragmentCollector(fragments, depth, maxDepth, seen);
|
|
3428
|
+
while (state.index < normalized.length) {
|
|
3429
|
+
if (tryAdvanceWithinQuotedText(normalized, state)) continue;
|
|
3430
|
+
if (tryAdvanceComment(normalized, state)) continue;
|
|
3431
|
+
if (tryEnterQuotedText(normalized, state)) continue;
|
|
3432
|
+
if (tryConsumeDollarQuotedFragment(statement, state, addFragment)) continue;
|
|
3433
|
+
if (tryConsumeWordToken(statement, normalized, state, addFragment)) continue;
|
|
3434
|
+
state.index += 1;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
function extractEmbeddableSqlFragments(statement) {
|
|
3438
|
+
const fragments = [];
|
|
3439
|
+
collectEmbeddableSqlFragments(
|
|
3440
|
+
statement,
|
|
3441
|
+
fragments,
|
|
3442
|
+
0,
|
|
3443
|
+
EMBEDDABLE_SQL_MAX_RECURSION_DEPTH,
|
|
3444
|
+
/* @__PURE__ */ new Set()
|
|
3445
|
+
);
|
|
3446
|
+
return dedupeAndSort(fragments);
|
|
3447
|
+
}
|
|
3448
|
+
function normalizeSqlForPlacementCheck(content) {
|
|
3449
|
+
return blankDollarQuotedBodies(stripSqlComments(content)).toUpperCase();
|
|
3450
|
+
}
|
|
3451
|
+
function isPartitionOfCreateTable(statement) {
|
|
3452
|
+
return /\bPARTITION\s+OF\b/.test(statement);
|
|
3453
|
+
}
|
|
3454
|
+
function dedupeAndSort(lines) {
|
|
3455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3456
|
+
return [...lines].sort((a, b) => a.localeCompare(b)).filter((line) => {
|
|
3457
|
+
if (seen.has(line)) return false;
|
|
3458
|
+
seen.add(line);
|
|
3459
|
+
return true;
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// src/commands/db/commands/db-sync/risk-reporter.ts
|
|
3464
|
+
init_esm_shims();
|
|
3465
|
+
|
|
3466
|
+
// src/commands/db/commands/db-sync/types.ts
|
|
3467
|
+
init_esm_shims();
|
|
3468
|
+
var DIRECTORY_RISK_ORDER = ["high", "medium", "low"];
|
|
3469
|
+
var PLAN_BOUNDARY_CONTEXT_FILE = "pg-schema-diff plan.sql";
|
|
3470
|
+
var GRANT_STATEMENT_RULE_TEXT = "Grant/REVOKE statements are usually idempotent/bootstrap ACL setup and should be treated as such";
|
|
3471
|
+
|
|
3472
|
+
// src/commands/db/commands/db-sync/risk-reporter.ts
|
|
3473
|
+
var DIRECTORY_RISK_WEIGHT = {
|
|
3474
|
+
high: 3,
|
|
3475
|
+
medium: 2,
|
|
3476
|
+
low: 1
|
|
3477
|
+
};
|
|
3478
|
+
function getDirectoryRiskWeight(level) {
|
|
3479
|
+
return DIRECTORY_RISK_WEIGHT[level];
|
|
3480
|
+
}
|
|
3481
|
+
function compareDirectoryRiskPriority(left, right) {
|
|
3482
|
+
const weightDelta = getDirectoryRiskWeight(left.level) - getDirectoryRiskWeight(right.level);
|
|
3483
|
+
if (weightDelta !== 0) {
|
|
3484
|
+
return weightDelta;
|
|
3485
|
+
}
|
|
3486
|
+
const leftLine = left.line ?? 0;
|
|
3487
|
+
const rightLine = right.line ?? 0;
|
|
3488
|
+
if (leftLine !== rightLine) {
|
|
3489
|
+
return leftLine - rightLine;
|
|
3490
|
+
}
|
|
3491
|
+
return left.message.localeCompare(right.message);
|
|
3492
|
+
}
|
|
3493
|
+
function selectHighestPriorityRisk(entries) {
|
|
3494
|
+
if (entries.length === 0) return void 0;
|
|
3495
|
+
const byPriority = [...entries].sort((left, right) => {
|
|
3496
|
+
const priorityDelta = compareDirectoryRiskPriority(right, left);
|
|
3497
|
+
if (priorityDelta !== 0) return priorityDelta;
|
|
3498
|
+
const leftMessage = left.message.toLowerCase();
|
|
3499
|
+
const rightMessage = right.message.toLowerCase();
|
|
3500
|
+
return leftMessage.localeCompare(rightMessage);
|
|
3501
|
+
});
|
|
3502
|
+
return byPriority[0];
|
|
3503
|
+
}
|
|
3504
|
+
function buildDirectoryRiskKey(entry) {
|
|
3505
|
+
return `${entry.level}:${entry.file}:${entry.line ?? 0}:${entry.message}`;
|
|
3506
|
+
}
|
|
3507
|
+
function dedupeDirectoryRisksBySeverity(entries) {
|
|
3508
|
+
const byLocationAndMessage = /* @__PURE__ */ new Map();
|
|
3509
|
+
for (const entry of entries) {
|
|
3510
|
+
const key = `${entry.file}:${entry.line ?? 0}:${entry.message}`;
|
|
3511
|
+
const existing = byLocationAndMessage.get(key);
|
|
3512
|
+
if (!existing || getDirectoryRiskWeight(entry.level) > getDirectoryRiskWeight(existing.level)) {
|
|
3513
|
+
byLocationAndMessage.set(key, entry);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
return [...byLocationAndMessage.values()].sort((left, right) => {
|
|
3517
|
+
const priorityDelta = getDirectoryRiskWeight(right.level) - getDirectoryRiskWeight(left.level);
|
|
3518
|
+
if (priorityDelta !== 0) return priorityDelta;
|
|
3519
|
+
const levelDelta = DIRECTORY_RISK_ORDER.indexOf(right.level) - DIRECTORY_RISK_ORDER.indexOf(left.level);
|
|
3520
|
+
if (levelDelta !== 0) return levelDelta;
|
|
3521
|
+
const leftLine = left.line ?? 0;
|
|
3522
|
+
const rightLine = right.line ?? 0;
|
|
3523
|
+
if (leftLine !== rightLine) return leftLine - rightLine;
|
|
3524
|
+
return left.message.localeCompare(right.message);
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
function printList(title, items, logger4) {
|
|
3528
|
+
if (items.length === 0) return;
|
|
3529
|
+
logger4.warn(`
|
|
3530
|
+
${title} (${items.length}):`);
|
|
3531
|
+
for (const item of items) {
|
|
3532
|
+
logger4.info(` \u2022 ${item}`);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
function stripCommonPrefix(filePath) {
|
|
3536
|
+
return filePath.startsWith(process.cwd()) ? filePath.replace(`${process.cwd()}/`, "") : filePath;
|
|
3537
|
+
}
|
|
3538
|
+
function formatDeclarativeRiskMessage(risk) {
|
|
3539
|
+
const line = risk.line ? `:${risk.line}` : "";
|
|
3540
|
+
return `${stripCommonPrefix(risk.file)}${line} ${risk.description}${risk.mitigation ? ` | Fix: ${risk.mitigation}` : ""}`;
|
|
3541
|
+
}
|
|
3542
|
+
function buildCompactFindingSummary(rawItems) {
|
|
3543
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3544
|
+
for (const item of rawItems) {
|
|
3545
|
+
const withoutPrefix = item.replace(/^\s*\[[^\]]+\]\s*/, "");
|
|
3546
|
+
const delimiter = ": ";
|
|
3547
|
+
const separator = withoutPrefix.indexOf(delimiter);
|
|
3548
|
+
if (separator <= 0) {
|
|
3549
|
+
const existing2 = grouped.get(withoutPrefix);
|
|
3550
|
+
if (!existing2) {
|
|
3551
|
+
grouped.set(withoutPrefix, { message: withoutPrefix, count: 1, samples: [] });
|
|
3552
|
+
} else {
|
|
3553
|
+
existing2.count += 1;
|
|
3554
|
+
}
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
const source = withoutPrefix.slice(0, separator).trim();
|
|
3558
|
+
const message = withoutPrefix.slice(separator + delimiter.length).trim();
|
|
3559
|
+
const existing = grouped.get(message);
|
|
3560
|
+
if (!existing) {
|
|
3561
|
+
grouped.set(message, { message, count: 1, samples: source ? [source] : [] });
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
existing.count += 1;
|
|
3565
|
+
if (source && !existing.samples.includes(source)) {
|
|
3566
|
+
existing.samples.push(source);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
return [...grouped.values()].sort((left, right) => {
|
|
3570
|
+
if (right.count !== left.count) return right.count - left.count;
|
|
3571
|
+
return left.message.localeCompare(right.message);
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
function printCompactSummary(logger4, title, rawItems, topLimit) {
|
|
3575
|
+
if (rawItems.length === 0) {
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
const findingSummary = buildCompactFindingSummary(rawItems);
|
|
3579
|
+
const top = findingSummary.slice(0, Math.max(1, topLimit));
|
|
3580
|
+
logger4.warn(`
|
|
3581
|
+
${title} (total ${rawItems.length} item(s), ${top.length} reason pattern(s))`);
|
|
3582
|
+
for (const finding of top) {
|
|
3583
|
+
logger4.info(` - ${finding.count}x ${finding.message}`);
|
|
3584
|
+
if (finding.samples.length > 0) {
|
|
3585
|
+
logger4.info(` samples:`);
|
|
3586
|
+
for (const sample of finding.samples.slice(0, 2)) {
|
|
3587
|
+
logger4.info(` - ${sample}`);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
const extra = findingSummary.length - top.length;
|
|
3592
|
+
if (extra > 0) {
|
|
3593
|
+
logger4.info(` ... and ${extra} more reason patterns`);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/commands/db/commands/db-sync/boundary-classifier.ts
|
|
3598
|
+
function isBoundaryRelevantDdlStatement(statement) {
|
|
3599
|
+
return /^\s*(?:CREATE|ALTER|DROP|RENAME|TRUNCATE|COMMENT)\b/i.test(statement);
|
|
3600
|
+
}
|
|
3601
|
+
function isPlanBoundaryAmbiguous(statement) {
|
|
3602
|
+
const trimmed = statement.trim();
|
|
3603
|
+
if (!trimmed) return false;
|
|
3604
|
+
if (/^\s*--/.test(trimmed)) return false;
|
|
3605
|
+
return isBoundaryRelevantDdlStatement(trimmed);
|
|
3606
|
+
}
|
|
3607
|
+
function classifyUnknownObjectBoundary(file, object, fileType, policy) {
|
|
3608
|
+
const inDeclarative = policy.declarativePreferredObjects.has(object);
|
|
3609
|
+
const inIdempotent = policy.idempotentPreferredObjects.has(object);
|
|
3610
|
+
if (fileType === "declarative") {
|
|
3611
|
+
if (inIdempotent && !inDeclarative) {
|
|
3612
|
+
return {
|
|
3613
|
+
file,
|
|
3614
|
+
level: "high",
|
|
3615
|
+
message: `Object "${object}" is idempotent-preferred and should be in supabase/schemas/idempotent`
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
if (!inDeclarative && !inIdempotent) {
|
|
3619
|
+
return {
|
|
3620
|
+
file,
|
|
3621
|
+
level: "medium",
|
|
3622
|
+
message: `Unrecognized declarative object "${object}" detected; review placement (runtime/setup candidate)`
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
return null;
|
|
3626
|
+
}
|
|
3627
|
+
if (inDeclarative && !inIdempotent) {
|
|
3628
|
+
return {
|
|
3629
|
+
file,
|
|
3630
|
+
level: "high",
|
|
3631
|
+
message: `Object "${object}" is declarative-preferred and should be in supabase/schemas/declarative`
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
if (!inDeclarative && !inIdempotent) {
|
|
3635
|
+
return {
|
|
3636
|
+
file,
|
|
3637
|
+
level: "high",
|
|
3638
|
+
message: `Unrecognized idempotent object "${object}" detected; review placement (likely declarative SSOT object)`
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
return null;
|
|
3642
|
+
}
|
|
3643
|
+
function matchesRiskRule(rule, statement) {
|
|
3644
|
+
return typeof rule.pattern === "function" ? rule.pattern(statement) : rule.pattern.test(statement);
|
|
3252
3645
|
}
|
|
3253
3646
|
function collectRuleBasedCandidates(params) {
|
|
3254
3647
|
const candidates = [];
|
|
@@ -3557,12 +3950,18 @@ function classifyIdempotentMisplacementRisk(file, content, boundaryPolicy) {
|
|
|
3557
3950
|
skipUnknown: (statement, unknownObject) => isPartitionOfCreateTable(statement) && unknownObject === "TABLE"
|
|
3558
3951
|
});
|
|
3559
3952
|
}
|
|
3953
|
+
var DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS = [
|
|
3954
|
+
"Function DDL should be in declarative",
|
|
3955
|
+
"Trigger DDL should be in declarative"
|
|
3956
|
+
];
|
|
3957
|
+
var DROP_IF_EXISTS_CLEANUP = /^\s*DROP\s+(?:FUNCTION|TRIGGER|VIEW|INDEX|POLICY|TYPE|SEQUENCE)\s+IF\s+EXISTS\b/i;
|
|
3560
3958
|
function classifyFileMisplacementRisks(params) {
|
|
3561
3959
|
const risks = [];
|
|
3562
3960
|
const relative2 = path12.relative(process.cwd(), params.file);
|
|
3563
3961
|
const normalized = normalizeSqlForPlacementCheck(params.content);
|
|
3564
3962
|
const statements = splitSqlStatements(normalized);
|
|
3565
3963
|
const seenMessages = /* @__PURE__ */ new Set();
|
|
3964
|
+
const hasAllowDynamicSqlAnnotation = params.fileType === "idempotent" && ALLOW_DYNAMIC_SQL_ANNOTATION.test(params.content);
|
|
3566
3965
|
for (const { statement, line } of statements) {
|
|
3567
3966
|
const candidates = collectRuleBasedCandidates({
|
|
3568
3967
|
statement,
|
|
@@ -3584,6 +3983,14 @@ function classifyFileMisplacementRisks(params) {
|
|
|
3584
3983
|
}
|
|
3585
3984
|
const best = selectHighestPriorityRisk(candidates);
|
|
3586
3985
|
if (!best) continue;
|
|
3986
|
+
if (params.fileType === "idempotent") {
|
|
3987
|
+
if (DROP_IF_EXISTS_CLEANUP.test(statement)) {
|
|
3988
|
+
best.level = "low";
|
|
3989
|
+
}
|
|
3990
|
+
if (hasAllowDynamicSqlAnnotation && DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS.some((p) => best.message.includes(p))) {
|
|
3991
|
+
best.level = "low";
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3587
3994
|
const key = buildDirectoryRiskKey(best);
|
|
3588
3995
|
if (seenMessages.has(key)) continue;
|
|
3589
3996
|
seenMessages.add(key);
|
|
@@ -3789,7 +4196,7 @@ init_esm_shims();
|
|
|
3789
4196
|
var riskDetectorLoader = null;
|
|
3790
4197
|
function loadRiskDetectorModule() {
|
|
3791
4198
|
if (!riskDetectorLoader) {
|
|
3792
|
-
riskDetectorLoader = import('./risk-detector-
|
|
4199
|
+
riskDetectorLoader = import('./risk-detector-GDDLISVE.js').then((module) => ({
|
|
3793
4200
|
detectSchemaRisks: module.detectSchemaRisks
|
|
3794
4201
|
})).catch((error) => {
|
|
3795
4202
|
riskDetectorLoader = null;
|
|
@@ -4388,9 +4795,10 @@ var reconcile = fromPromise(
|
|
|
4388
4795
|
} catch {
|
|
4389
4796
|
}
|
|
4390
4797
|
const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
|
|
4391
|
-
|
|
4798
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
|
|
4799
|
+
if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
|
|
4392
4800
|
excludeFromOrphanDetection = [
|
|
4393
|
-
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
|
|
4801
|
+
.../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
|
|
4394
4802
|
];
|
|
4395
4803
|
}
|
|
4396
4804
|
const diff = diffSchema({
|
|
@@ -4883,357 +5291,150 @@ var dbSyncMachine = setup({
|
|
|
4883
5291
|
meta: { e2e: e2eMeta.preflight },
|
|
4884
5292
|
invoke: {
|
|
4885
5293
|
src: "preflight",
|
|
4886
|
-
input: ({ context }) => ({ ctx: assertStepCtx(context) }),
|
|
4887
|
-
onDone: [
|
|
4888
|
-
{
|
|
4889
|
-
guard: ({ event }) => event.output.passed === false,
|
|
4890
|
-
target: "failed",
|
|
4891
|
-
actions: assign({
|
|
4892
|
-
error: ({ event }) => event.output.error ?? "Preflight failed"
|
|
4893
|
-
})
|
|
4894
|
-
},
|
|
4895
|
-
{
|
|
4896
|
-
target: "snapshot",
|
|
4897
|
-
actions: assign({ preflightPassed: true })
|
|
4898
|
-
}
|
|
4899
|
-
],
|
|
4900
|
-
onError: {
|
|
4901
|
-
target: "failed",
|
|
4902
|
-
actions: assign({
|
|
4903
|
-
error: ({ event }) => event.error instanceof Error ? event.error.message : "Preflight failed"
|
|
4904
|
-
})
|
|
4905
|
-
}
|
|
4906
|
-
}
|
|
4907
|
-
},
|
|
4908
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4909
|
-
// Step 4: Snapshot (optional)
|
|
4910
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4911
|
-
snapshot: {
|
|
4912
|
-
meta: { e2e: e2eMeta.snapshot },
|
|
4913
|
-
invoke: {
|
|
4914
|
-
src: "snapshot",
|
|
4915
|
-
input: ({ context }) => ({
|
|
4916
|
-
ctx: assertStepCtx(context),
|
|
4917
|
-
autoSnapshot: context.stepCtx?.autoSnapshot ?? false
|
|
4918
|
-
}),
|
|
4919
|
-
onDone: {
|
|
4920
|
-
target: "sync",
|
|
4921
|
-
actions: assign({
|
|
4922
|
-
snapshotCreated: ({ event }) => event.output.created
|
|
4923
|
-
})
|
|
4924
|
-
},
|
|
4925
|
-
onError: {
|
|
4926
|
-
// Non-critical, continue to sync
|
|
4927
|
-
target: "sync",
|
|
4928
|
-
actions: assign({ snapshotCreated: false })
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
4931
|
-
},
|
|
4932
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4933
|
-
// Step 5: Sync Schema
|
|
4934
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4935
|
-
sync: {
|
|
4936
|
-
meta: { e2e: e2eMeta.sync },
|
|
4937
|
-
invoke: {
|
|
4938
|
-
src: "syncSchema",
|
|
4939
|
-
input: ({ context }) => ({ ctx: assertStepCtx(context) }),
|
|
4940
|
-
onDone: [
|
|
4941
|
-
{
|
|
4942
|
-
guard: ({ event }) => event.output.applied === false && event.output.error != null,
|
|
4943
|
-
target: "report",
|
|
4944
|
-
actions: assign({
|
|
4945
|
-
applied: false,
|
|
4946
|
-
applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
|
|
4947
|
-
stepsCompleted: ({ event }) => event.output.stepsCompleted,
|
|
4948
|
-
error: ({ event }) => event.output.error ?? "Sync failed"
|
|
4949
|
-
})
|
|
4950
|
-
},
|
|
4951
|
-
{
|
|
4952
|
-
target: "report",
|
|
4953
|
-
actions: assign({
|
|
4954
|
-
applied: ({ event }) => event.output.applied,
|
|
4955
|
-
applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
|
|
4956
|
-
stepsCompleted: ({ event }) => event.output.stepsCompleted
|
|
4957
|
-
})
|
|
4958
|
-
}
|
|
4959
|
-
],
|
|
4960
|
-
onError: {
|
|
4961
|
-
target: "report",
|
|
4962
|
-
actions: assign({
|
|
4963
|
-
applied: false,
|
|
4964
|
-
applyCommitted: false,
|
|
4965
|
-
error: ({ event }) => event.error instanceof Error ? event.error.message : "Sync failed"
|
|
4966
|
-
})
|
|
4967
|
-
}
|
|
4968
|
-
}
|
|
4969
|
-
},
|
|
4970
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4971
|
-
// Step 6: Write Report
|
|
4972
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
4973
|
-
report: {
|
|
4974
|
-
meta: { e2e: e2eMeta.report },
|
|
4975
|
-
invoke: {
|
|
4976
|
-
src: "writeReport",
|
|
4977
|
-
input: ({ context }) => ({
|
|
4978
|
-
ctx: assertStepCtx(context),
|
|
4979
|
-
startTime: context.startTime,
|
|
4980
|
-
stepsCompleted: context.stepsCompleted,
|
|
4981
|
-
success: context.error == null,
|
|
4982
|
-
error: context.error ?? void 0
|
|
4983
|
-
}),
|
|
5294
|
+
input: ({ context }) => ({ ctx: assertStepCtx(context) }),
|
|
4984
5295
|
onDone: [
|
|
4985
5296
|
{
|
|
4986
|
-
guard: ({
|
|
5297
|
+
guard: ({ event }) => event.output.passed === false,
|
|
4987
5298
|
target: "failed",
|
|
4988
|
-
actions: assign({
|
|
5299
|
+
actions: assign({
|
|
5300
|
+
error: ({ event }) => event.output.error ?? "Preflight failed"
|
|
5301
|
+
})
|
|
4989
5302
|
},
|
|
4990
5303
|
{
|
|
4991
|
-
target: "
|
|
4992
|
-
actions: assign({
|
|
5304
|
+
target: "snapshot",
|
|
5305
|
+
actions: assign({ preflightPassed: true })
|
|
4993
5306
|
}
|
|
4994
5307
|
],
|
|
4995
5308
|
onError: {
|
|
4996
5309
|
target: "failed",
|
|
4997
|
-
actions: assign({
|
|
5310
|
+
actions: assign({
|
|
5311
|
+
error: ({ event }) => event.error instanceof Error ? event.error.message : "Preflight failed"
|
|
5312
|
+
})
|
|
4998
5313
|
}
|
|
4999
5314
|
}
|
|
5000
5315
|
},
|
|
5001
5316
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5002
|
-
//
|
|
5317
|
+
// Step 4: Snapshot (optional)
|
|
5003
5318
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5004
|
-
|
|
5005
|
-
meta: { e2e: e2eMeta.
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
}
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
return snapshot2.context.error;
|
|
5023
|
-
}
|
|
5024
|
-
|
|
5025
|
-
// src/commands/db/sync/guardrail-orchestrator.ts
|
|
5026
|
-
init_esm_shims();
|
|
5027
|
-
|
|
5028
|
-
// src/commands/db/sync/schema-guardrail.ts
|
|
5029
|
-
init_esm_shims();
|
|
5030
|
-
|
|
5031
|
-
// src/commands/db/sync/schema-guardrail-config.ts
|
|
5032
|
-
init_esm_shims();
|
|
5033
|
-
|
|
5034
|
-
// src/commands/db/sync/schema-guardrail-config-test-support.ts
|
|
5035
|
-
init_esm_shims();
|
|
5036
|
-
function extractFirstStringMatch(content, fieldName) {
|
|
5037
|
-
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*['"]([^'"]+)['"]`));
|
|
5038
|
-
return match?.[1];
|
|
5039
|
-
}
|
|
5040
|
-
function extractFirstNumberMatch(content, fieldName) {
|
|
5041
|
-
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`));
|
|
5042
|
-
if (!match?.[1]) {
|
|
5043
|
-
return void 0;
|
|
5044
|
-
}
|
|
5045
|
-
const parsed = Number(match[1]);
|
|
5046
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
5047
|
-
}
|
|
5048
|
-
function extractStringArrayMatch(content, fieldName) {
|
|
5049
|
-
const match = content.match(new RegExp(`${fieldName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
5050
|
-
if (!match?.[1]) {
|
|
5051
|
-
return [];
|
|
5052
|
-
}
|
|
5053
|
-
return Array.from(match[1].matchAll(/['"]([^'"]+)['"]/g), (entry) => entry[1] ?? "").filter(
|
|
5054
|
-
(value) => value.length > 0
|
|
5055
|
-
);
|
|
5056
|
-
}
|
|
5057
|
-
function extractNamedObjectBlock(content, fieldName) {
|
|
5058
|
-
const nameMatch = new RegExp(`${fieldName}\\s*:\\s*\\{`).exec(content);
|
|
5059
|
-
if (!nameMatch) {
|
|
5060
|
-
return null;
|
|
5061
|
-
}
|
|
5062
|
-
const startIndex = nameMatch.index + nameMatch[0].length - 1;
|
|
5063
|
-
let depth = 0;
|
|
5064
|
-
for (let index = startIndex; index < content.length; index += 1) {
|
|
5065
|
-
const current = content[index];
|
|
5066
|
-
if (current === "{") {
|
|
5067
|
-
depth += 1;
|
|
5068
|
-
} else if (current === "}") {
|
|
5069
|
-
depth -= 1;
|
|
5070
|
-
if (depth === 0) {
|
|
5071
|
-
return content.slice(startIndex + 1, index);
|
|
5072
|
-
}
|
|
5073
|
-
}
|
|
5074
|
-
}
|
|
5075
|
-
return null;
|
|
5076
|
-
}
|
|
5077
|
-
function extractAllowedDuplicateFunctions(content, normalizers) {
|
|
5078
|
-
const match = content.match(/allowedDuplicateFunctions\s*:\s*\[([\s\S]*?)\]/);
|
|
5079
|
-
if (!match?.[1]) {
|
|
5080
|
-
return [];
|
|
5081
|
-
}
|
|
5082
|
-
const entries = [];
|
|
5083
|
-
for (const objectMatch of match[1].matchAll(/\{([\s\S]*?)\}/g)) {
|
|
5084
|
-
const objectBody = objectMatch[1] ?? "";
|
|
5085
|
-
const qualifiedName = extractFirstStringMatch(objectBody, "qualifiedName");
|
|
5086
|
-
const signature = extractFirstStringMatch(objectBody, "signature");
|
|
5087
|
-
const reason = extractFirstStringMatch(objectBody, "reason");
|
|
5088
|
-
if (!qualifiedName || !signature || !reason) {
|
|
5089
|
-
continue;
|
|
5090
|
-
}
|
|
5091
|
-
entries.push({
|
|
5092
|
-
qualifiedName: normalizers.normalizeFunctionQualifiedName(qualifiedName),
|
|
5093
|
-
signature: normalizers.normalizeAllowlistSignature(signature),
|
|
5094
|
-
reason,
|
|
5095
|
-
declarativeFile: extractFirstStringMatch(objectBody, "declarativeFile"),
|
|
5096
|
-
idempotentFile: extractFirstStringMatch(objectBody, "idempotentFile"),
|
|
5097
|
-
expectedBodyHash: extractFirstStringMatch(objectBody, "expectedBodyHash")
|
|
5098
|
-
});
|
|
5099
|
-
}
|
|
5100
|
-
return entries;
|
|
5101
|
-
}
|
|
5102
|
-
function tryLoadSchemaGuardrailConfigFromText(params) {
|
|
5103
|
-
const configPath = findRunaConfig(params.targetDir);
|
|
5104
|
-
if (!configPath || !existsSync(configPath)) {
|
|
5105
|
-
return null;
|
|
5106
|
-
}
|
|
5107
|
-
try {
|
|
5108
|
-
const content = readFileSync(configPath, "utf-8");
|
|
5109
|
-
const schemaGuardrailsBlock = extractNamedObjectBlock(content, "schemaGuardrails") ?? "";
|
|
5110
|
-
const pgSchemaDiffBlock = extractNamedObjectBlock(content, "pgSchemaDiff") ?? "";
|
|
5111
|
-
return {
|
|
5112
|
-
...params.defaults,
|
|
5113
|
-
declarativeSqlDir: extractFirstStringMatch(schemaGuardrailsBlock, "declarativeSqlDir") ?? params.defaults.declarativeSqlDir,
|
|
5114
|
-
allowedDuplicateFunctions: extractAllowedDuplicateFunctions(
|
|
5115
|
-
schemaGuardrailsBlock,
|
|
5116
|
-
params.normalizers
|
|
5117
|
-
),
|
|
5118
|
-
generatedHeaderRewriteTargets: params.normalizers.normalizeFileList(
|
|
5119
|
-
extractStringArrayMatch(schemaGuardrailsBlock, "generatedHeaderRewriteTargets").map(
|
|
5120
|
-
(value) => params.normalizers.normalizePathForMatch(value)
|
|
5121
|
-
)
|
|
5122
|
-
),
|
|
5123
|
-
semanticWarnings: {
|
|
5124
|
-
threshold: extractFirstNumberMatch(schemaGuardrailsBlock, "threshold") ?? params.defaults.semanticWarnings.threshold,
|
|
5125
|
-
maxCandidates: extractFirstNumberMatch(schemaGuardrailsBlock, "maxCandidates") ?? params.defaults.semanticWarnings.maxCandidates,
|
|
5126
|
-
ignorePairs: new Set(
|
|
5127
|
-
extractStringArrayMatch(schemaGuardrailsBlock, "ignorePairs").map(
|
|
5128
|
-
(value) => params.normalizers.normalizeSuppressionPair(value)
|
|
5129
|
-
)
|
|
5130
|
-
)
|
|
5131
|
-
},
|
|
5132
|
-
tableHeaderMaxWidth: extractFirstNumberMatch(schemaGuardrailsBlock, "tableHeaderMaxWidth") ?? params.defaults.tableHeaderMaxWidth,
|
|
5133
|
-
excludeFromOrphanDetection: extractStringArrayMatch(
|
|
5134
|
-
pgSchemaDiffBlock,
|
|
5135
|
-
"excludeFromOrphanDetection"
|
|
5136
|
-
),
|
|
5137
|
-
idempotentSqlDir: extractFirstStringMatch(pgSchemaDiffBlock, "idempotentSqlDir") ?? params.defaults.idempotentSqlDir
|
|
5138
|
-
};
|
|
5139
|
-
} catch {
|
|
5140
|
-
return null;
|
|
5141
|
-
}
|
|
5142
|
-
}
|
|
5143
|
-
|
|
5144
|
-
// src/commands/db/sync/schema-guardrail-config.ts
|
|
5145
|
-
var DEFAULT_TABLE_HEADER_MAX_WIDTH = 160;
|
|
5146
|
-
var DEFAULT_SEMANTIC_WARNING_THRESHOLD = 0.55;
|
|
5147
|
-
var DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES = 3;
|
|
5148
|
-
var GENERIC_SIMILARITY_COLUMNS = /* @__PURE__ */ new Set([
|
|
5149
|
-
"id",
|
|
5150
|
-
"created_at",
|
|
5151
|
-
"updated_at",
|
|
5152
|
-
"deleted_at",
|
|
5153
|
-
"scope_id"
|
|
5154
|
-
]);
|
|
5155
|
-
function normalizePathForMatch(filePath) {
|
|
5156
|
-
return filePath.replaceAll("\\", "/");
|
|
5157
|
-
}
|
|
5158
|
-
function normalizeFunctionQualifiedName(value) {
|
|
5159
|
-
return value.trim().toLowerCase();
|
|
5160
|
-
}
|
|
5161
|
-
function normalizeAllowlistSignature(value) {
|
|
5162
|
-
return value.replace(/\s+/g, " ").trim();
|
|
5163
|
-
}
|
|
5164
|
-
function normalizeSuppressionPair(value) {
|
|
5165
|
-
return value.split("::").map((part) => part.trim().toLowerCase()).filter((part) => part.length > 0).sort((left, right) => left.localeCompare(right)).join("::");
|
|
5166
|
-
}
|
|
5167
|
-
function normalizeFileList(files) {
|
|
5168
|
-
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
5169
|
-
}
|
|
5170
|
-
function isSchemaGuardrailTextFallbackAllowed() {
|
|
5171
|
-
return Boolean(process.env.VITEST);
|
|
5172
|
-
}
|
|
5173
|
-
function createDefaultSchemaGuardrailConfig() {
|
|
5174
|
-
return {
|
|
5175
|
-
declarativeSqlDir: "supabase/schemas/declarative",
|
|
5176
|
-
allowedDuplicateFunctions: [],
|
|
5177
|
-
generatedHeaderRewriteTargets: [],
|
|
5178
|
-
semanticWarnings: {
|
|
5179
|
-
threshold: DEFAULT_SEMANTIC_WARNING_THRESHOLD,
|
|
5180
|
-
maxCandidates: DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES,
|
|
5181
|
-
ignorePairs: /* @__PURE__ */ new Set()
|
|
5182
|
-
},
|
|
5183
|
-
tableHeaderMaxWidth: DEFAULT_TABLE_HEADER_MAX_WIDTH,
|
|
5184
|
-
excludeFromOrphanDetection: [],
|
|
5185
|
-
idempotentSqlDir: "supabase/schemas/idempotent"
|
|
5186
|
-
};
|
|
5187
|
-
}
|
|
5188
|
-
function loadSchemaGuardrailConfig(targetDir) {
|
|
5189
|
-
const defaults = createDefaultSchemaGuardrailConfig();
|
|
5190
|
-
try {
|
|
5191
|
-
const config = loadRunaConfig(targetDir);
|
|
5192
|
-
const databaseConfig = config.database ?? {};
|
|
5193
|
-
return {
|
|
5194
|
-
declarativeSqlDir: databaseConfig.schemaGuardrails?.declarativeSqlDir ?? defaults.declarativeSqlDir,
|
|
5195
|
-
allowedDuplicateFunctions: databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
|
|
5196
|
-
...entry,
|
|
5197
|
-
qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
|
|
5198
|
-
signature: normalizeAllowlistSignature(entry.signature),
|
|
5199
|
-
declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
|
|
5200
|
-
idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
|
|
5201
|
-
})) ?? [],
|
|
5202
|
-
generatedHeaderRewriteTargets: normalizeFileList(
|
|
5203
|
-
(databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
|
|
5204
|
-
(value) => normalizePathForMatch(value)
|
|
5205
|
-
)
|
|
5206
|
-
),
|
|
5207
|
-
semanticWarnings: {
|
|
5208
|
-
threshold: databaseConfig.schemaGuardrails?.semanticWarnings?.threshold ?? defaults.semanticWarnings.threshold,
|
|
5209
|
-
maxCandidates: databaseConfig.schemaGuardrails?.semanticWarnings?.maxCandidates ?? defaults.semanticWarnings.maxCandidates,
|
|
5210
|
-
ignorePairs: new Set(
|
|
5211
|
-
(databaseConfig.schemaGuardrails?.semanticWarnings?.ignorePairs ?? []).map(
|
|
5212
|
-
(value) => normalizeSuppressionPair(value)
|
|
5213
|
-
)
|
|
5214
|
-
)
|
|
5215
|
-
},
|
|
5216
|
-
tableHeaderMaxWidth: databaseConfig.schemaGuardrails?.tableHeaderMaxWidth ?? defaults.tableHeaderMaxWidth,
|
|
5217
|
-
excludeFromOrphanDetection: databaseConfig.pgSchemaDiff?.excludeFromOrphanDetection ?? [],
|
|
5218
|
-
idempotentSqlDir: databaseConfig.pgSchemaDiff?.idempotentSqlDir ?? defaults.idempotentSqlDir
|
|
5219
|
-
};
|
|
5220
|
-
} catch (error) {
|
|
5221
|
-
if (isSchemaGuardrailTextFallbackAllowed()) {
|
|
5222
|
-
return tryLoadSchemaGuardrailConfigFromText({
|
|
5223
|
-
targetDir,
|
|
5224
|
-
defaults,
|
|
5225
|
-
normalizers: {
|
|
5226
|
-
normalizeAllowlistSignature,
|
|
5227
|
-
normalizeFileList,
|
|
5228
|
-
normalizeFunctionQualifiedName,
|
|
5229
|
-
normalizePathForMatch,
|
|
5230
|
-
normalizeSuppressionPair
|
|
5319
|
+
snapshot: {
|
|
5320
|
+
meta: { e2e: e2eMeta.snapshot },
|
|
5321
|
+
invoke: {
|
|
5322
|
+
src: "snapshot",
|
|
5323
|
+
input: ({ context }) => ({
|
|
5324
|
+
ctx: assertStepCtx(context),
|
|
5325
|
+
autoSnapshot: context.stepCtx?.autoSnapshot ?? false
|
|
5326
|
+
}),
|
|
5327
|
+
onDone: {
|
|
5328
|
+
target: "sync",
|
|
5329
|
+
actions: assign({
|
|
5330
|
+
snapshotCreated: ({ event }) => event.output.created
|
|
5331
|
+
})
|
|
5332
|
+
},
|
|
5333
|
+
onError: {
|
|
5334
|
+
// Non-critical, continue to sync
|
|
5335
|
+
target: "sync",
|
|
5336
|
+
actions: assign({ snapshotCreated: false })
|
|
5231
5337
|
}
|
|
5232
|
-
}
|
|
5338
|
+
}
|
|
5339
|
+
},
|
|
5340
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5341
|
+
// Step 5: Sync Schema
|
|
5342
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5343
|
+
sync: {
|
|
5344
|
+
meta: { e2e: e2eMeta.sync },
|
|
5345
|
+
invoke: {
|
|
5346
|
+
src: "syncSchema",
|
|
5347
|
+
input: ({ context }) => ({ ctx: assertStepCtx(context) }),
|
|
5348
|
+
onDone: [
|
|
5349
|
+
{
|
|
5350
|
+
guard: ({ event }) => event.output.applied === false && event.output.error != null,
|
|
5351
|
+
target: "report",
|
|
5352
|
+
actions: assign({
|
|
5353
|
+
applied: false,
|
|
5354
|
+
applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
|
|
5355
|
+
stepsCompleted: ({ event }) => event.output.stepsCompleted,
|
|
5356
|
+
error: ({ event }) => event.output.error ?? "Sync failed"
|
|
5357
|
+
})
|
|
5358
|
+
},
|
|
5359
|
+
{
|
|
5360
|
+
target: "report",
|
|
5361
|
+
actions: assign({
|
|
5362
|
+
applied: ({ event }) => event.output.applied,
|
|
5363
|
+
applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
|
|
5364
|
+
stepsCompleted: ({ event }) => event.output.stepsCompleted
|
|
5365
|
+
})
|
|
5366
|
+
}
|
|
5367
|
+
],
|
|
5368
|
+
onError: {
|
|
5369
|
+
target: "report",
|
|
5370
|
+
actions: assign({
|
|
5371
|
+
applied: false,
|
|
5372
|
+
applyCommitted: false,
|
|
5373
|
+
error: ({ event }) => event.error instanceof Error ? event.error.message : "Sync failed"
|
|
5374
|
+
})
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
},
|
|
5378
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5379
|
+
// Step 6: Write Report
|
|
5380
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5381
|
+
report: {
|
|
5382
|
+
meta: { e2e: e2eMeta.report },
|
|
5383
|
+
invoke: {
|
|
5384
|
+
src: "writeReport",
|
|
5385
|
+
input: ({ context }) => ({
|
|
5386
|
+
ctx: assertStepCtx(context),
|
|
5387
|
+
startTime: context.startTime,
|
|
5388
|
+
stepsCompleted: context.stepsCompleted,
|
|
5389
|
+
success: context.error == null,
|
|
5390
|
+
error: context.error ?? void 0
|
|
5391
|
+
}),
|
|
5392
|
+
onDone: [
|
|
5393
|
+
{
|
|
5394
|
+
guard: ({ context }) => context.error != null,
|
|
5395
|
+
target: "failed",
|
|
5396
|
+
actions: assign({ reportWritten: true })
|
|
5397
|
+
},
|
|
5398
|
+
{
|
|
5399
|
+
target: "done",
|
|
5400
|
+
actions: assign({ reportWritten: true })
|
|
5401
|
+
}
|
|
5402
|
+
],
|
|
5403
|
+
onError: {
|
|
5404
|
+
target: "failed",
|
|
5405
|
+
actions: assign({ reportWritten: false })
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
},
|
|
5409
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5410
|
+
// Final States
|
|
5411
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
5412
|
+
done: {
|
|
5413
|
+
meta: { e2e: e2eMeta.done },
|
|
5414
|
+
type: "final"
|
|
5415
|
+
},
|
|
5416
|
+
failed: {
|
|
5417
|
+
meta: { e2e: e2eMeta.failed },
|
|
5418
|
+
type: "final"
|
|
5233
5419
|
}
|
|
5234
|
-
|
|
5235
|
-
}
|
|
5420
|
+
},
|
|
5421
|
+
output: ({ context }) => buildMachineOutput(context)
|
|
5422
|
+
});
|
|
5423
|
+
function getDbSyncStateName(snapshot2) {
|
|
5424
|
+
return snapshot2.value;
|
|
5425
|
+
}
|
|
5426
|
+
function isDbSyncComplete(snapshot2) {
|
|
5427
|
+
return snapshot2.status === "done";
|
|
5236
5428
|
}
|
|
5429
|
+
function getDbSyncError(snapshot2) {
|
|
5430
|
+
return snapshot2.context.error;
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5433
|
+
// src/commands/db/sync/guardrail-orchestrator.ts
|
|
5434
|
+
init_esm_shims();
|
|
5435
|
+
|
|
5436
|
+
// src/commands/db/sync/schema-guardrail.ts
|
|
5437
|
+
init_esm_shims();
|
|
5237
5438
|
|
|
5238
5439
|
// src/commands/db/sync/schema-guardrail-graph.ts
|
|
5239
5440
|
init_esm_shims();
|
|
@@ -5266,8 +5467,13 @@ function loadExtraTableFilters(targetDir) {
|
|
|
5266
5467
|
const idempotentManagedTables = new Set(
|
|
5267
5468
|
extractTablesFromIdempotentSql(idempotentDirectory, targetDir)
|
|
5268
5469
|
);
|
|
5470
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
|
|
5471
|
+
idempotentDirectory,
|
|
5472
|
+
targetDir
|
|
5473
|
+
);
|
|
5474
|
+
const allExclusions = [...config.excludeFromOrphanDetection, ...dynamicPatterns];
|
|
5269
5475
|
return {
|
|
5270
|
-
exclusionMatcher: buildTablePatternMatcher(
|
|
5476
|
+
exclusionMatcher: buildTablePatternMatcher(allExclusions),
|
|
5271
5477
|
idempotentManagedTables
|
|
5272
5478
|
};
|
|
5273
5479
|
}
|
|
@@ -5480,24 +5686,36 @@ function collectExecuteOccurrencesFromBody(body, startLine) {
|
|
|
5480
5686
|
}
|
|
5481
5687
|
return occurrences.sort((left, right) => left.line - right.line);
|
|
5482
5688
|
}
|
|
5689
|
+
var PARTITION_INFRA_PATTERNS = [
|
|
5690
|
+
/\bPARTITION\b/i,
|
|
5691
|
+
/\bATTACH\b/i,
|
|
5692
|
+
/\bDETACH\b/i,
|
|
5693
|
+
/\bCREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i,
|
|
5694
|
+
/\bdefault\s*partition\b/i,
|
|
5695
|
+
/\brange_partition\b/i
|
|
5696
|
+
];
|
|
5697
|
+
function isPartitionInfrastructure(body) {
|
|
5698
|
+
return PARTITION_INFRA_PATTERNS.some((pattern) => pattern.test(body));
|
|
5699
|
+
}
|
|
5483
5700
|
function createDynamicSqlBlockersForStatement(params) {
|
|
5484
5701
|
const body = extractFirstDollarBody(params.statement, params.line);
|
|
5485
5702
|
if (!body) {
|
|
5486
5703
|
return [];
|
|
5487
5704
|
}
|
|
5488
5705
|
const executeOccurrences = collectExecuteOccurrencesFromBody(body.body, body.startLine);
|
|
5706
|
+
const isInfra = isPartitionInfrastructure(body.body);
|
|
5489
5707
|
return executeOccurrences.map((occurrence) => ({
|
|
5490
|
-
kind: "dynamic-sql",
|
|
5708
|
+
kind: isInfra ? "dynamic-sql-infra" : "dynamic-sql",
|
|
5491
5709
|
sourceFile: params.sourceFile,
|
|
5492
5710
|
line: occurrence.line,
|
|
5493
5711
|
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."
|
|
5712
|
+
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
5713
|
}));
|
|
5496
5714
|
}
|
|
5497
5715
|
function buildDynamicSqlBlockers(params) {
|
|
5498
5716
|
const blockers = [];
|
|
5499
|
-
const
|
|
5500
|
-
|
|
5717
|
+
for (const file of params.sources.declarativeFiles) {
|
|
5718
|
+
if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
|
|
5501
5719
|
for (const parsed of splitSqlStatements(file.content)) {
|
|
5502
5720
|
blockers.push(
|
|
5503
5721
|
...createDynamicSqlBlockersForStatement({
|
|
@@ -5508,6 +5726,21 @@ function buildDynamicSqlBlockers(params) {
|
|
|
5508
5726
|
);
|
|
5509
5727
|
}
|
|
5510
5728
|
}
|
|
5729
|
+
for (const file of params.sources.idempotentFiles) {
|
|
5730
|
+
if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
|
|
5731
|
+
for (const parsed of splitSqlStatements(file.content)) {
|
|
5732
|
+
const stmtBlockers = createDynamicSqlBlockersForStatement({
|
|
5733
|
+
sourceFile: file.relativePath,
|
|
5734
|
+
statement: parsed.statement,
|
|
5735
|
+
line: parsed.line
|
|
5736
|
+
});
|
|
5737
|
+
for (const blocker of stmtBlockers) {
|
|
5738
|
+
blocker.kind = "dynamic-sql-infra";
|
|
5739
|
+
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)`;
|
|
5740
|
+
}
|
|
5741
|
+
blockers.push(...stmtBlockers);
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5511
5744
|
return stableSorted2(blockers, (value) => `${value.sourceFile}:${value.line ?? 0}:${value.kind}`);
|
|
5512
5745
|
}
|
|
5513
5746
|
function buildLocalBlindSpotBlockers(params) {
|
|
@@ -5522,7 +5755,7 @@ function buildLocalBlindSpotBlockers(params) {
|
|
|
5522
5755
|
}),
|
|
5523
5756
|
...buildExtensionPlacementBlockers({
|
|
5524
5757
|
sources: params.sources,
|
|
5525
|
-
requiredFile:
|
|
5758
|
+
requiredFile: detectExtensionFilePath()
|
|
5526
5759
|
})
|
|
5527
5760
|
];
|
|
5528
5761
|
return stableSorted2(
|
|
@@ -5805,37 +6038,6 @@ function buildFunctionBodyHashMap(files, layer) {
|
|
|
5805
6038
|
}
|
|
5806
6039
|
return hashes;
|
|
5807
6040
|
}
|
|
5808
|
-
function isAllowlistedDuplicateFunction(params) {
|
|
5809
|
-
const { finding, allowlist, bodyHashes } = params;
|
|
5810
|
-
if (!finding.signature) return false;
|
|
5811
|
-
return allowlist.some((entry) => {
|
|
5812
|
-
if (normalizeFunctionQualifiedName(entry.qualifiedName) !== normalizeFunctionQualifiedName(finding.qualifiedName)) {
|
|
5813
|
-
return false;
|
|
5814
|
-
}
|
|
5815
|
-
if (normalizeAllowlistSignature(entry.signature) !== normalizeAllowlistSignature(finding.signature ?? "")) {
|
|
5816
|
-
return false;
|
|
5817
|
-
}
|
|
5818
|
-
if (entry.declarativeFile && !finding.declarativeDefinitions.some(
|
|
5819
|
-
(definition) => normalizePathForMatch(definition.file) === entry.declarativeFile
|
|
5820
|
-
)) {
|
|
5821
|
-
return false;
|
|
5822
|
-
}
|
|
5823
|
-
if (entry.idempotentFile && !finding.idempotentDefinitions.some(
|
|
5824
|
-
(definition) => normalizePathForMatch(definition.file) === entry.idempotentFile
|
|
5825
|
-
)) {
|
|
5826
|
-
return false;
|
|
5827
|
-
}
|
|
5828
|
-
if (!entry.expectedBodyHash) {
|
|
5829
|
-
return true;
|
|
5830
|
-
}
|
|
5831
|
-
const definitions = [...finding.declarativeDefinitions, ...finding.idempotentDefinitions];
|
|
5832
|
-
if (definitions.length === 0) return false;
|
|
5833
|
-
return definitions.every((definition) => {
|
|
5834
|
-
const key = `${definition.layer}:${definition.file}:${definition.line}`;
|
|
5835
|
-
return bodyHashes.get(key) === entry.expectedBodyHash;
|
|
5836
|
-
});
|
|
5837
|
-
});
|
|
5838
|
-
}
|
|
5839
6041
|
function normalizeQualifiedObjectRef(schema, name, isFunctionCall) {
|
|
5840
6042
|
return `${schema}.${name}${isFunctionCall ? "()" : ""}`;
|
|
5841
6043
|
}
|
|
@@ -5917,6 +6119,19 @@ function buildManagedBoundaryMetadataByFile(files) {
|
|
|
5917
6119
|
}
|
|
5918
6120
|
|
|
5919
6121
|
// src/commands/db/sync/schema-guardrail-graph-nodes.ts
|
|
6122
|
+
function extractTriggerFunctionArgs(statement) {
|
|
6123
|
+
const execMatch = statement.match(
|
|
6124
|
+
/\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"[^"]+"|[A-Za-z_]\w*)\s*\.\s*)?(?:"[^"]+"|[A-Za-z_]\w*)\s*\(([^)]*)\)/i
|
|
6125
|
+
);
|
|
6126
|
+
if (!execMatch?.[1]) return [];
|
|
6127
|
+
const argsText = execMatch[1].trim();
|
|
6128
|
+
if (!argsText) return [];
|
|
6129
|
+
return argsText.split(",").map((arg) => arg.trim()).map((arg) => {
|
|
6130
|
+
const stringMatch = arg.match(/^'([^']*)'$/);
|
|
6131
|
+
if (stringMatch) return stringMatch[1];
|
|
6132
|
+
return null;
|
|
6133
|
+
}).filter((arg) => arg !== null);
|
|
6134
|
+
}
|
|
5920
6135
|
function parseCreateTriggerStatement(statement) {
|
|
5921
6136
|
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
6137
|
const match = statement.match(triggerRegex);
|
|
@@ -5933,13 +6148,15 @@ function parseCreateTriggerStatement(statement) {
|
|
|
5933
6148
|
const schema = (match[5] ?? match[6] ?? "public").toLowerCase();
|
|
5934
6149
|
const functionSchema = (match[9] ?? match[10] ?? "").toLowerCase();
|
|
5935
6150
|
const functionName = (match[11] ?? match[12] ?? "").toLowerCase();
|
|
6151
|
+
const functionArgs = extractTriggerFunctionArgs(statement);
|
|
5936
6152
|
return {
|
|
5937
6153
|
qualifiedTable: `${schema}.${table}`,
|
|
5938
6154
|
trigger: {
|
|
5939
6155
|
name: triggerName,
|
|
5940
6156
|
timing,
|
|
5941
6157
|
event,
|
|
5942
|
-
functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0
|
|
6158
|
+
functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0,
|
|
6159
|
+
functionArgs: functionArgs.length > 0 ? functionArgs : void 0
|
|
5943
6160
|
}
|
|
5944
6161
|
};
|
|
5945
6162
|
}
|
|
@@ -6902,6 +7119,114 @@ function buildBoundaryGuidanceWarnings(params) {
|
|
|
6902
7119
|
(value) => `${value.sourceFile}.${value.kind}.${value.suggestedDeclarativeFile ?? ""}.${value.suggestedIdempotentFile ?? ""}.${value.target}`
|
|
6903
7120
|
);
|
|
6904
7121
|
}
|
|
7122
|
+
function extractCaseWhenBranches(functionBody) {
|
|
7123
|
+
const branches = [];
|
|
7124
|
+
const whenPattern = /\bWHEN\s+'([^']+)'/gi;
|
|
7125
|
+
for (const match of functionBody.matchAll(whenPattern)) {
|
|
7126
|
+
if (match[1]) branches.push(match[1].toLowerCase());
|
|
7127
|
+
}
|
|
7128
|
+
return [...new Set(branches)];
|
|
7129
|
+
}
|
|
7130
|
+
function escapeForRegex(value) {
|
|
7131
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7132
|
+
}
|
|
7133
|
+
function findFunctionBody(qualifiedName, sources) {
|
|
7134
|
+
const rawName = qualifiedName.includes(".") ? qualifiedName.split(".")[1] : qualifiedName;
|
|
7135
|
+
if (!rawName) return null;
|
|
7136
|
+
const escaped = escapeForRegex(rawName);
|
|
7137
|
+
const pattern = new RegExp(
|
|
7138
|
+
`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\(`,
|
|
7139
|
+
"i"
|
|
7140
|
+
);
|
|
7141
|
+
for (const file of [...sources.declarativeFiles, ...sources.idempotentFiles]) {
|
|
7142
|
+
if (!pattern.test(file.content)) continue;
|
|
7143
|
+
const bodyMatch = file.content.match(
|
|
7144
|
+
new RegExp(
|
|
7145
|
+
`CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\([^)]*\\)[\\s\\S]*?\\$\\w*\\$([\\s\\S]*?)\\$\\w*\\$`,
|
|
7146
|
+
"i"
|
|
7147
|
+
)
|
|
7148
|
+
);
|
|
7149
|
+
if (bodyMatch?.[1]) return bodyMatch[1];
|
|
7150
|
+
}
|
|
7151
|
+
return null;
|
|
7152
|
+
}
|
|
7153
|
+
function buildTriggerDispatchGapWarnings(params) {
|
|
7154
|
+
const argsByFunction = collectTriggerArgsByFunction(params.tableNodes);
|
|
7155
|
+
return validateDispatchCoverage(argsByFunction, params.sources);
|
|
7156
|
+
}
|
|
7157
|
+
function collectTriggerArgsByFunction(tableNodes) {
|
|
7158
|
+
const argsByFunction = /* @__PURE__ */ new Map();
|
|
7159
|
+
for (const [, tableNode] of tableNodes) {
|
|
7160
|
+
for (const trigger of tableNode.triggers) {
|
|
7161
|
+
if (!trigger.functionName || !trigger.functionArgs?.length) continue;
|
|
7162
|
+
for (const arg of trigger.functionArgs) {
|
|
7163
|
+
const entries = argsByFunction.get(trigger.functionName) ?? [];
|
|
7164
|
+
entries.push({
|
|
7165
|
+
arg: arg.toLowerCase(),
|
|
7166
|
+
triggerName: trigger.name,
|
|
7167
|
+
table: tableNode.qualifiedName
|
|
7168
|
+
});
|
|
7169
|
+
argsByFunction.set(trigger.functionName, entries);
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
return argsByFunction;
|
|
7174
|
+
}
|
|
7175
|
+
function validateDispatchCoverage(argsByFunction, sources) {
|
|
7176
|
+
const warnings = [];
|
|
7177
|
+
for (const [functionName, triggerArgs] of argsByFunction) {
|
|
7178
|
+
const body = findFunctionBody(functionName, sources);
|
|
7179
|
+
if (!body || !/\bCASE\b/i.test(body)) continue;
|
|
7180
|
+
const coveredBranches = extractCaseWhenBranches(body);
|
|
7181
|
+
if (coveredBranches.length === 0) continue;
|
|
7182
|
+
for (const { arg, triggerName, table } of triggerArgs) {
|
|
7183
|
+
if (!coveredBranches.includes(arg)) {
|
|
7184
|
+
warnings.push({
|
|
7185
|
+
sourceFile: table,
|
|
7186
|
+
kind: "trigger_dispatch_gap",
|
|
7187
|
+
target: `${functionName}('${arg}')`,
|
|
7188
|
+
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.`
|
|
7189
|
+
});
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
|
|
7194
|
+
}
|
|
7195
|
+
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;
|
|
7196
|
+
function extractIndexNames(content) {
|
|
7197
|
+
const names = /* @__PURE__ */ new Set();
|
|
7198
|
+
CREATE_INDEX_PATTERN.lastIndex = 0;
|
|
7199
|
+
for (const match of content.matchAll(CREATE_INDEX_PATTERN)) {
|
|
7200
|
+
const name = (match[1] ?? match[2] ?? "").toLowerCase();
|
|
7201
|
+
if (name) names.add(name);
|
|
7202
|
+
}
|
|
7203
|
+
return names;
|
|
7204
|
+
}
|
|
7205
|
+
function buildCrossLayerDuplicateIndexWarnings(sources) {
|
|
7206
|
+
const declarativeIndexes = /* @__PURE__ */ new Map();
|
|
7207
|
+
for (const file of sources.declarativeFiles) {
|
|
7208
|
+
for (const name of extractIndexNames(file.content)) {
|
|
7209
|
+
declarativeIndexes.set(name, file.relativePath);
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
const warnings = [];
|
|
7213
|
+
for (const file of sources.idempotentFiles) {
|
|
7214
|
+
for (const name of extractIndexNames(file.content)) {
|
|
7215
|
+
const declarativeFile = declarativeIndexes.get(name);
|
|
7216
|
+
if (declarativeFile) {
|
|
7217
|
+
warnings.push({
|
|
7218
|
+
sourceFile: file.relativePath,
|
|
7219
|
+
kind: "trigger_function",
|
|
7220
|
+
// reuse existing kind for index cross-layer
|
|
7221
|
+
target: name,
|
|
7222
|
+
suggestedDeclarativeFile: declarativeFile,
|
|
7223
|
+
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.`
|
|
7224
|
+
});
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
|
|
7229
|
+
}
|
|
6905
7230
|
|
|
6906
7231
|
// src/commands/db/sync/schema-guardrail-graph.ts
|
|
6907
7232
|
function loadSqlSources(targetDir, config) {
|
|
@@ -7031,12 +7356,19 @@ async function buildStaticGraph(targetDir, config, sources) {
|
|
|
7031
7356
|
runtimeTables: loadRuntimeTablesManifest(targetDir),
|
|
7032
7357
|
config
|
|
7033
7358
|
});
|
|
7034
|
-
const boundaryGuidanceWarnings =
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7359
|
+
const boundaryGuidanceWarnings = [
|
|
7360
|
+
...buildBoundaryGuidanceWarnings({
|
|
7361
|
+
fileNodes,
|
|
7362
|
+
schemaNodes,
|
|
7363
|
+
functionClaims,
|
|
7364
|
+
ownerFileByTable
|
|
7365
|
+
}),
|
|
7366
|
+
...buildTriggerDispatchGapWarnings({
|
|
7367
|
+
tableNodes: tableNodesByName,
|
|
7368
|
+
sources
|
|
7369
|
+
}),
|
|
7370
|
+
...buildCrossLayerDuplicateIndexWarnings(sources)
|
|
7371
|
+
];
|
|
7040
7372
|
const localBlindSpotBlockers = buildLocalBlindSpotBlockers({
|
|
7041
7373
|
graph,
|
|
7042
7374
|
sources,
|
|
@@ -7149,7 +7481,8 @@ function createCheckModePhases(report) {
|
|
|
7149
7481
|
phase: "compare_generated_headers",
|
|
7150
7482
|
details: {
|
|
7151
7483
|
file: block.file,
|
|
7152
|
-
target: block.target
|
|
7484
|
+
target: block.target,
|
|
7485
|
+
repair: "Run `runa db sync` to auto-regenerate headers"
|
|
7153
7486
|
}
|
|
7154
7487
|
}))
|
|
7155
7488
|
})
|
|
@@ -7231,7 +7564,7 @@ function setFailure2(report, phase, code, message) {
|
|
|
7231
7564
|
function stableSorted5(values, map) {
|
|
7232
7565
|
return [...values].sort((a, b) => map(a).localeCompare(map(b)));
|
|
7233
7566
|
}
|
|
7234
|
-
function
|
|
7567
|
+
function normalizeFileList3(files) {
|
|
7235
7568
|
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
7236
7569
|
}
|
|
7237
7570
|
function renderList(values) {
|
|
@@ -7716,7 +8049,7 @@ function rewriteManagedHeaders(params) {
|
|
|
7716
8049
|
)
|
|
7717
8050
|
};
|
|
7718
8051
|
}
|
|
7719
|
-
params.report.headersRewritten =
|
|
8052
|
+
params.report.headersRewritten = normalizeFileList3(params.report.headersRewritten);
|
|
7720
8053
|
params.report.rewritesRetainedOnDisk = params.report.headersRewritten.length > 0;
|
|
7721
8054
|
params.report.staleBlocks = [];
|
|
7722
8055
|
return null;
|
|
@@ -8073,7 +8406,7 @@ async function runProductionDdlOrderCheck(params) {
|
|
|
8073
8406
|
}
|
|
8074
8407
|
params.logger.info("\u{1F50D} Checking production DDL ordering...");
|
|
8075
8408
|
try {
|
|
8076
|
-
const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-
|
|
8409
|
+
const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-JZO4GAQG.js');
|
|
8077
8410
|
const { planOutput } = executePgSchemaDiffPlan(
|
|
8078
8411
|
productionUrl,
|
|
8079
8412
|
schemasDir,
|
|
@@ -11416,6 +11749,62 @@ function formatImportImpactReport(report, changedSymbols) {
|
|
|
11416
11749
|
// src/commands/db/commands/db-sync/production-precheck.ts
|
|
11417
11750
|
init_esm_shims();
|
|
11418
11751
|
|
|
11752
|
+
// src/commands/db/utils/changed-files-detector.ts
|
|
11753
|
+
init_esm_shims();
|
|
11754
|
+
function detectDefaultBranch() {
|
|
11755
|
+
const result = spawnSync("git", ["rev-parse", "--verify", "main"], {
|
|
11756
|
+
timeout: 5e3,
|
|
11757
|
+
encoding: "utf-8"
|
|
11758
|
+
});
|
|
11759
|
+
return result.status === 0 ? "main" : "master";
|
|
11760
|
+
}
|
|
11761
|
+
function getChangedSqlFiles() {
|
|
11762
|
+
try {
|
|
11763
|
+
const defaultBranch = detectDefaultBranch();
|
|
11764
|
+
const mergeBase = spawnSync("git", ["merge-base", "HEAD", defaultBranch], {
|
|
11765
|
+
timeout: 5e3,
|
|
11766
|
+
encoding: "utf-8"
|
|
11767
|
+
});
|
|
11768
|
+
let diffOutput;
|
|
11769
|
+
if (mergeBase.status === 0 && mergeBase.stdout.trim()) {
|
|
11770
|
+
const diff = spawnSync("git", ["diff", "--name-only", mergeBase.stdout.trim(), "HEAD"], {
|
|
11771
|
+
timeout: 5e3,
|
|
11772
|
+
encoding: "utf-8"
|
|
11773
|
+
});
|
|
11774
|
+
diffOutput = diff.stdout ?? "";
|
|
11775
|
+
} else {
|
|
11776
|
+
const diff = spawnSync("git", ["diff", "--name-only", "HEAD"], {
|
|
11777
|
+
timeout: 5e3,
|
|
11778
|
+
encoding: "utf-8"
|
|
11779
|
+
});
|
|
11780
|
+
diffOutput = diff.stdout ?? "";
|
|
11781
|
+
}
|
|
11782
|
+
const uncommitted = spawnSync("git", ["diff", "--name-only"], {
|
|
11783
|
+
timeout: 5e3,
|
|
11784
|
+
encoding: "utf-8"
|
|
11785
|
+
});
|
|
11786
|
+
const staged = spawnSync("git", ["diff", "--name-only", "--cached"], {
|
|
11787
|
+
timeout: 5e3,
|
|
11788
|
+
encoding: "utf-8"
|
|
11789
|
+
});
|
|
11790
|
+
const allChanged = [
|
|
11791
|
+
...diffOutput.split("\n"),
|
|
11792
|
+
...(uncommitted.stdout ?? "").split("\n"),
|
|
11793
|
+
...(staged.stdout ?? "").split("\n")
|
|
11794
|
+
].map((f) => f.trim()).filter((f) => f.length > 0 && f.endsWith(".sql"));
|
|
11795
|
+
return [...new Set(allChanged)].sort();
|
|
11796
|
+
} catch {
|
|
11797
|
+
return [];
|
|
11798
|
+
}
|
|
11799
|
+
}
|
|
11800
|
+
function classifyBlockerScope(blockerMessage, changedFiles) {
|
|
11801
|
+
const fileMatch = blockerMessage.match(
|
|
11802
|
+
/(supabase\/schemas\/(?:declarative|idempotent)\/[^\s:]+\.sql)/
|
|
11803
|
+
);
|
|
11804
|
+
if (!fileMatch) return "baseline";
|
|
11805
|
+
return changedFiles.has(fileMatch[1]) ? "current-change" : "baseline";
|
|
11806
|
+
}
|
|
11807
|
+
|
|
11419
11808
|
// src/commands/db/commands/db-sync/plan-hazard-analyzer.ts
|
|
11420
11809
|
init_esm_shims();
|
|
11421
11810
|
function parseHazardType(hazard) {
|
|
@@ -11722,13 +12111,22 @@ async function collectLocalPrecheckBundle(strict) {
|
|
|
11722
12111
|
const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
|
|
11723
12112
|
const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
|
|
11724
12113
|
const duplicateOwnershipAnalysis = analyzeDuplicateFunctionOwnership(process.cwd());
|
|
11725
|
-
|
|
12114
|
+
let guardrailAllowlist = [];
|
|
12115
|
+
try {
|
|
12116
|
+
guardrailAllowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
|
|
12117
|
+
} catch {
|
|
12118
|
+
}
|
|
12119
|
+
const duplicateOwnershipBlockers = filterAllowlistedDuplicateFunctions({
|
|
12120
|
+
findings: duplicateOwnershipAnalysis.findings,
|
|
12121
|
+
allowlist: guardrailAllowlist
|
|
12122
|
+
}).map((finding) => {
|
|
11726
12123
|
const formatted = formatDuplicateFunctionOwnershipFinding(finding);
|
|
11727
12124
|
return [
|
|
11728
12125
|
formatted.summary,
|
|
11729
12126
|
`declarative=${formatted.declarativeLocations.join(", ")}`,
|
|
11730
12127
|
`idempotent=${formatted.idempotentLocations.join(", ")}`,
|
|
11731
|
-
formatted.suggestion
|
|
12128
|
+
formatted.suggestion,
|
|
12129
|
+
"Allowlist: runa.config.ts database.schemaGuardrails.allowedDuplicateFunctions"
|
|
11732
12130
|
].join(" | ");
|
|
11733
12131
|
});
|
|
11734
12132
|
const hasLocalBlockers = duplicateOwnershipBlockers.length > 0 || hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
|
|
@@ -11795,32 +12193,185 @@ function printLocalFindingCompactSummary(logger4, local, topLimit) {
|
|
|
11795
12193
|
topLimit
|
|
11796
12194
|
);
|
|
11797
12195
|
}
|
|
12196
|
+
var APPLY_BLOCKER_PATTERNS = [
|
|
12197
|
+
/Extension DDL/i,
|
|
12198
|
+
/DROP.*CONCURRENTLY/i,
|
|
12199
|
+
/DML.*declarative/i,
|
|
12200
|
+
/DO.*maintenance/i,
|
|
12201
|
+
/cross.*schema.*rls/i,
|
|
12202
|
+
/auth\.\*.*direct.*reference/i,
|
|
12203
|
+
/Duplicate function ownership/i
|
|
12204
|
+
];
|
|
12205
|
+
var INFRA_DYNAMIC_SQL_INDICATOR = /idempotent file/i;
|
|
12206
|
+
function isApplyBlocker(message) {
|
|
12207
|
+
if (/EXECUTE|dynamic.*sql/i.test(message)) {
|
|
12208
|
+
return !INFRA_DYNAMIC_SQL_INDICATOR.test(message);
|
|
12209
|
+
}
|
|
12210
|
+
return APPLY_BLOCKER_PATTERNS.some((pattern) => pattern.test(message));
|
|
12211
|
+
}
|
|
12212
|
+
function categorizeBlockers(blockers) {
|
|
12213
|
+
const applyBlockers = [];
|
|
12214
|
+
const architectureDebt = [];
|
|
12215
|
+
for (const blocker of blockers) {
|
|
12216
|
+
if (isApplyBlocker(blocker)) {
|
|
12217
|
+
applyBlockers.push(blocker);
|
|
12218
|
+
} else {
|
|
12219
|
+
architectureDebt.push(blocker);
|
|
12220
|
+
}
|
|
12221
|
+
}
|
|
12222
|
+
return { applyBlockers, architectureDebt };
|
|
12223
|
+
}
|
|
11798
12224
|
function printLocalFindingDetailedReport(logger4, local) {
|
|
11799
|
-
|
|
12225
|
+
logLocalWarningsSections(logger4, local);
|
|
12226
|
+
const allBlockers = collectAllBlockers(local);
|
|
12227
|
+
const { applyBlockers, architectureDebt } = categorizeBlockers(allBlockers);
|
|
12228
|
+
const changedFiles = new Set(getChangedSqlFiles());
|
|
12229
|
+
if (changedFiles.size > 0) {
|
|
12230
|
+
logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles);
|
|
12231
|
+
} else {
|
|
12232
|
+
logUnscopedBlockers(logger4, applyBlockers, architectureDebt);
|
|
12233
|
+
}
|
|
12234
|
+
}
|
|
12235
|
+
function logLocalWarningsSections(logger4, local) {
|
|
12236
|
+
logFindingSection(logger4, "Declarative risk warnings:", local.adjustedDeclarativeRisks.warnings);
|
|
12237
|
+
logFindingSection(logger4, "Idempotent risk warnings:", local.adjustedIdempotentRisks.warnings);
|
|
12238
|
+
logFindingSection(logger4, "Placement warnings:", local.adjustedPlacementRisks.warnings);
|
|
12239
|
+
logFindingSection(logger4, "Extension warnings:", local.adjustedExtensionRisks.warnings);
|
|
12240
|
+
}
|
|
12241
|
+
function collectAllBlockers(local) {
|
|
12242
|
+
return [
|
|
12243
|
+
...local.duplicateOwnershipBlockers,
|
|
12244
|
+
...local.adjustedPlacementRisks.blockers,
|
|
12245
|
+
...local.adjustedDeclarativeRisks.blockers,
|
|
12246
|
+
...local.adjustedIdempotentRisks.blockers,
|
|
12247
|
+
...local.adjustedExtensionRisks.blockers
|
|
12248
|
+
];
|
|
12249
|
+
}
|
|
12250
|
+
function logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles) {
|
|
12251
|
+
const current = applyBlockers.filter(
|
|
12252
|
+
(b) => classifyBlockerScope(b, changedFiles) === "current-change"
|
|
12253
|
+
);
|
|
12254
|
+
const baseline = applyBlockers.filter(
|
|
12255
|
+
(b) => classifyBlockerScope(b, changedFiles) === "baseline"
|
|
12256
|
+
);
|
|
12257
|
+
const currentDebt = architectureDebt.filter(
|
|
12258
|
+
(b) => classifyBlockerScope(b, changedFiles) === "current-change"
|
|
12259
|
+
);
|
|
12260
|
+
const baselineDebt = architectureDebt.filter(
|
|
12261
|
+
(b) => classifyBlockerScope(b, changedFiles) === "baseline"
|
|
12262
|
+
);
|
|
12263
|
+
logBlockerGroup(
|
|
11800
12264
|
logger4,
|
|
11801
|
-
"
|
|
11802
|
-
|
|
12265
|
+
"error",
|
|
12266
|
+
"[current change] Apply blockers",
|
|
12267
|
+
current,
|
|
12268
|
+
"fix before merging"
|
|
11803
12269
|
);
|
|
11804
|
-
|
|
12270
|
+
logBlockerGroup(logger4, "warn", "[current change] Architecture debt", currentDebt);
|
|
12271
|
+
logBlockerGroup(
|
|
11805
12272
|
logger4,
|
|
11806
|
-
"
|
|
11807
|
-
|
|
12273
|
+
"error",
|
|
12274
|
+
"[repo baseline] Apply blockers",
|
|
12275
|
+
baseline,
|
|
12276
|
+
"pre-existing issues"
|
|
11808
12277
|
);
|
|
11809
|
-
|
|
12278
|
+
logBlockerGroup(
|
|
11810
12279
|
logger4,
|
|
11811
|
-
"
|
|
11812
|
-
|
|
12280
|
+
"warn",
|
|
12281
|
+
"[repo baseline] Architecture debt",
|
|
12282
|
+
baselineDebt,
|
|
12283
|
+
"pre-existing"
|
|
11813
12284
|
);
|
|
11814
|
-
|
|
12285
|
+
logImpactSummary(logger4, {
|
|
12286
|
+
current: current.length + currentDebt.length,
|
|
12287
|
+
currentApply: current.length,
|
|
12288
|
+
currentDebt: currentDebt.length,
|
|
12289
|
+
baseline: baseline.length + baselineDebt.length,
|
|
12290
|
+
baselineApply: baseline.length,
|
|
12291
|
+
baselineDebt: baselineDebt.length,
|
|
12292
|
+
total: applyBlockers.length + architectureDebt.length
|
|
12293
|
+
});
|
|
12294
|
+
}
|
|
12295
|
+
var PRECHECK_COUNTS_PATH = join(".runa", "tmp", "precheck-counts.json");
|
|
12296
|
+
function loadPreviousPrecheckCounts() {
|
|
12297
|
+
try {
|
|
12298
|
+
const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
|
|
12299
|
+
if (!existsSync(fullPath)) return null;
|
|
12300
|
+
return JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
12301
|
+
} catch {
|
|
12302
|
+
return null;
|
|
12303
|
+
}
|
|
12304
|
+
}
|
|
12305
|
+
function savePrecheckCounts(counts) {
|
|
12306
|
+
try {
|
|
12307
|
+
const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
|
|
12308
|
+
const dir = join(fullPath, "..");
|
|
12309
|
+
mkdirSync(dir, { recursive: true });
|
|
12310
|
+
writeFileSync(fullPath, JSON.stringify(counts, null, 2), "utf-8");
|
|
12311
|
+
} catch {
|
|
12312
|
+
}
|
|
12313
|
+
}
|
|
12314
|
+
function logImpactSummary(logger4, counts) {
|
|
12315
|
+
logger4.info("");
|
|
12316
|
+
logger4.info("Impact summary:");
|
|
12317
|
+
if (counts.current === 0 && counts.total > 0) {
|
|
12318
|
+
logger4.info(" Current change: no new issues introduced");
|
|
12319
|
+
} else if (counts.current > 0) {
|
|
12320
|
+
logger4.info(
|
|
12321
|
+
` Regressions: ${counts.current} issue(s) from changed files (${counts.currentApply} apply, ${counts.currentDebt} debt)`
|
|
12322
|
+
);
|
|
12323
|
+
}
|
|
12324
|
+
if (counts.baseline > 0) {
|
|
12325
|
+
logger4.info(
|
|
12326
|
+
` Baseline debt: ${counts.baseline} pre-existing issue(s) (${counts.baselineApply} apply, ${counts.baselineDebt} debt)`
|
|
12327
|
+
);
|
|
12328
|
+
}
|
|
12329
|
+
if (counts.total === 0) {
|
|
12330
|
+
logger4.info(" All clear \u2014 no blockers or debt detected");
|
|
12331
|
+
}
|
|
12332
|
+
const previous = loadPreviousPrecheckCounts();
|
|
12333
|
+
if (previous) {
|
|
12334
|
+
const delta = counts.total - previous.total;
|
|
12335
|
+
if (delta < 0) {
|
|
12336
|
+
logger4.info(` Improvement: ${Math.abs(delta)} fewer issue(s) than previous run`);
|
|
12337
|
+
} else if (delta > 0) {
|
|
12338
|
+
logger4.info(` Regression: ${delta} more issue(s) than previous run`);
|
|
12339
|
+
} else {
|
|
12340
|
+
logger4.info(" No change from previous run");
|
|
12341
|
+
}
|
|
12342
|
+
}
|
|
12343
|
+
savePrecheckCounts({
|
|
12344
|
+
total: counts.total,
|
|
12345
|
+
apply: counts.currentApply + counts.baselineApply,
|
|
12346
|
+
debt: counts.currentDebt + counts.baselineDebt,
|
|
12347
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
12348
|
+
});
|
|
12349
|
+
}
|
|
12350
|
+
function logUnscopedBlockers(logger4, applyBlockers, architectureDebt) {
|
|
12351
|
+
logBlockerGroup(
|
|
11815
12352
|
logger4,
|
|
11816
|
-
"
|
|
11817
|
-
|
|
12353
|
+
"error",
|
|
12354
|
+
"Apply blockers",
|
|
12355
|
+
applyBlockers,
|
|
12356
|
+
"deployment will fail if not fixed"
|
|
11818
12357
|
);
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
12358
|
+
logBlockerGroup(
|
|
12359
|
+
logger4,
|
|
12360
|
+
"warn",
|
|
12361
|
+
"Architecture debt",
|
|
12362
|
+
architectureDebt,
|
|
12363
|
+
"works but should be addressed"
|
|
12364
|
+
);
|
|
12365
|
+
}
|
|
12366
|
+
function logBlockerGroup(logger4, level, title, items, subtitle) {
|
|
12367
|
+
if (items.length === 0) return;
|
|
12368
|
+
const suffix = subtitle ? ` \u2014 ${subtitle}` : "";
|
|
12369
|
+
const logFn = level === "error" ? logger4.error.bind(logger4) : logger4.warn.bind(logger4);
|
|
12370
|
+
logFn(`
|
|
12371
|
+
${title} (${items.length})${suffix}:`);
|
|
12372
|
+
for (const item of items) {
|
|
12373
|
+
logger4.info(` ${item}`);
|
|
12374
|
+
}
|
|
11824
12375
|
}
|
|
11825
12376
|
function logLocalFindings(logger4, local, summary) {
|
|
11826
12377
|
if (!local.hasLocalFindings) return;
|
|
@@ -12163,9 +12714,12 @@ async function runSyncAction(env, options) {
|
|
|
12163
12714
|
const { dbTables, dbEnums } = await fetchDbTablesAndEnums(databaseUrl);
|
|
12164
12715
|
const schemaDiffConfig = loadSchemaDiffConfig();
|
|
12165
12716
|
const idempotentTables = extractTablesFromIdempotentSql(schemaDiffConfig.idempotentSqlDir);
|
|
12717
|
+
const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
|
|
12718
|
+
schemaDiffConfig.idempotentSqlDir
|
|
12719
|
+
);
|
|
12166
12720
|
const excludeFromOrphanDetection = mergeExcludedTables(
|
|
12167
12721
|
schemaDiffConfig.excludeFromOrphanDetection,
|
|
12168
|
-
idempotentTables
|
|
12722
|
+
[...idempotentTables, ...dynamicPatterns]
|
|
12169
12723
|
);
|
|
12170
12724
|
const diff = diffSchema({
|
|
12171
12725
|
expectedTables,
|