UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

1,192 lines (1,191 loc) 71.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.refreshSssqlQuery = exports.scaffoldSssqlQuery = exports.SSSQLFilterBuilder = void 0; const Clause_1 = require("../models/Clause"); const SelectQuery_1 = require("../models/SelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); const SqlTokenizer_1 = require("../parsers/SqlTokenizer"); const Lexeme_1 = require("../models/Lexeme"); const UpstreamSelectQueryFinder_1 = require("./UpstreamSelectQueryFinder"); const ColumnReferenceCollector_1 = require("./ColumnReferenceCollector"); const SelectableColumnCollector_1 = require("./SelectableColumnCollector"); const ParameterCollector_1 = require("./ParameterCollector"); const CTECollector_1 = require("./CTECollector"); const PruneOptionalConditionBranches_1 = require("./PruneOptionalConditionBranches"); const SqlComponentFormatter_1 = require("./SqlComponentFormatter"); const SUPPORTED_SCALAR_OPERATORS = new Set(["=", "<>", "<", "<=", ">", ">=", "like", "ilike"]); const normalizeIdentifier = (value) => value.trim().toLowerCase(); const normalizeSql = (value) => value.replace(/\s+/g, " ").trim().toLowerCase(); const normalizeRewriteTokenType = (lexeme) => { if ((lexeme.type & Lexeme_1.TokenType.Command) !== 0) { return Lexeme_1.TokenType.Command; } if ((lexeme.type & Lexeme_1.TokenType.Identifier) !== 0) { return Lexeme_1.TokenType.Identifier; } return lexeme.type; }; const normalizeRewriteTokenValue = (lexeme) => { if ((lexeme.type & Lexeme_1.TokenType.Command) !== 0) { return lexeme.value.toLowerCase(); } return lexeme.value; }; const tokenizeForRewritePlan = (sql) => { return new SqlTokenizer_1.SqlTokenizer(sql).tokenize().map(lexeme => ({ type: normalizeRewriteTokenType(lexeme), value: normalizeRewriteTokenValue(lexeme) })); }; const tokenSequencesEqual = (left, right) => { if (left.length !== right.length) { return false; } return left.every((token, index) => { const other = right[index]; return other !== undefined && token.type === other.type && token.value === other.value; }); }; const collectCommentFragments = (sql) => { return new SqlTokenizer_1.SqlTokenizer(sql).tokenize().flatMap(lexeme => { if (lexeme.positionedComments) { return lexeme.positionedComments.flatMap(positioned => positioned.comments); } return lexeme.comments ? [...lexeme.comments] : []; }); }; const commentsPreservedInOrder = (before, after) => { let cursor = 0; for (const comment of before) { const foundAt = after.indexOf(comment, cursor); if (foundAt < 0) { return false; } cursor = foundAt + 1; } return true; }; const countCommandToken = (tokens, value) => { return tokens.filter(token => token.type === Lexeme_1.TokenType.Command && token.value === value).length; }; const applyRewriteEdits = (sql, edits) => { return [...edits] .sort((left, right) => right.start - left.start) .reduce((current, edit) => { return current.slice(0, edit.start) + edit.after + current.slice(edit.end); }, sql); }; const getStatementEndPosition = (sql) => { let end = sql.length; while (end > 0 && /\s/.test(sql[end - 1])) { end--; } if (end > 0 && sql[end - 1] === ";") { end--; while (end > 0 && /\s/.test(sql[end - 1])) { end--; } } return end; }; const clauseBoundaryCommands = new Set(["group by", "having", "order by", "limit", "offset", "fetch", "for"]); const isClauseBoundary = (lexeme) => { return (lexeme.type & Lexeme_1.TokenType.Command) !== 0 && clauseBoundaryCommands.has(lexeme.value.toLowerCase()); }; const findMinimalWhereInsertPosition = (sql) => { var _a, _b, _c, _d; const lexemes = new SqlTokenizer_1.SqlTokenizer(sql).tokenize(); const statementEnd = getStatementEndPosition(sql); const topLevelLexemes = []; let depth = 0; for (let index = 0; index < lexemes.length; index += 1) { const lexeme = lexemes[index]; if ((lexeme.type & Lexeme_1.TokenType.CloseParen) !== 0) { depth = Math.max(0, depth - 1); } if (depth === 0) { topLevelLexemes.push({ lexeme, index }); } if ((lexeme.type & Lexeme_1.TokenType.OpenParen) !== 0) { depth += 1; } } const where = topLevelLexemes.find(entry => (entry.lexeme.type & Lexeme_1.TokenType.Command) !== 0 && entry.lexeme.value.toLowerCase() === "where"); if (where) { const tail = topLevelLexemes .filter(entry => entry.index > where.index) .map(entry => entry.lexeme) .find(isClauseBoundary); return { position: (_b = (_a = tail === null || tail === void 0 ? void 0 : tail.position) === null || _a === void 0 ? void 0 : _a.startPosition) !== null && _b !== void 0 ? _b : statementEnd, hasWhere: true }; } const tail = topLevelLexemes.map(entry => entry.lexeme).find(isClauseBoundary); return { position: (_d = (_c = tail === null || tail === void 0 ? void 0 : tail.position) === null || _c === void 0 ? void 0 : _c.startPosition) !== null && _d !== void 0 ? _d : statementEnd, hasWhere: false }; }; const findMatchingParenEnd = (sql, start) => { let depth = 0; let quote = null; for (let index = start; index < sql.length; index++) { const char = sql[index]; if (quote) { if (char === quote) { if (quote === "'" && sql[index + 1] === "'") { index++; continue; } quote = null; } continue; } if (char === "'" || char === "\"") { quote = char; continue; } if (char === "(") { depth++; } if (char === ")") { depth--; if (depth === 0) { return index + 1; } } } return -1; }; // Fallback for #854: findOptionalBranchSpans, findBooleanOperatorBefore, and // findBooleanOperatorAfter recover minimal remove spans until the AST can expose // source positions for optional parenthesized OR/IS NULL branches reliably. // Keep regex-based rewriting limited to this fallback path. const findOptionalBranchSpans = (sql, parameterName) => { const spans = []; const parameterNeedle = `:${parameterName.toLowerCase()}`; const lowerSql = sql.toLowerCase(); for (let index = 0; index < sql.length; index++) { if (sql[index] !== "(") { continue; } const end = findMatchingParenEnd(sql, index); if (end < 0) { break; } const text = sql.slice(index, end); const normalized = lowerSql.slice(index, end); if (normalized.includes(parameterNeedle) && normalized.includes(" is null") && normalized.includes(" or ")) { spans.push({ start: index, end, text }); } index = end - 1; } return spans; }; const findBooleanOperatorBefore = (sql, start) => { const prefix = sql.slice(0, start); const match = /(\s+)(and|or)(\s*)$/i.exec(prefix); if (!match || match.index === undefined) { return null; } return { start: match.index, end: start, value: match[2].toLowerCase() }; }; const findBooleanOperatorAfter = (sql, end) => { const suffix = sql.slice(end); const match = /^(\s*)(and|or)(\s+)/i.exec(suffix); if (!match) { return null; } return { start: end, end: end + match[0].length, value: match[2].toLowerCase() }; }; const findWhereBefore = (sql, position) => { var _a, _b; const lexemes = new SqlTokenizer_1.SqlTokenizer(sql).tokenize(); let found = null; for (const lexeme of lexemes) { if (((_b = (_a = lexeme.position) === null || _a === void 0 ? void 0 : _a.startPosition) !== null && _b !== void 0 ? _b : 0) >= position) { break; } if ((lexeme.type & Lexeme_1.TokenType.Command) !== 0 && lexeme.value.toLowerCase() === "where") { found = lexeme; } } if (!(found === null || found === void 0 ? void 0 : found.position)) { return null; } return { start: found.position.startPosition, end: found.position.endPosition }; }; const findSourceColumnReferenceText = (sql, reference) => { const namespace = normalizeIdentifier(reference.getNamespace()); const column = normalizeIdentifier(reference.column.name); const lexemes = new SqlTokenizer_1.SqlTokenizer(sql).tokenize(); for (let index = 0; index < lexemes.length - 2; index++) { const first = lexemes[index]; const dot = lexemes[index + 1]; const last = lexemes[index + 2]; if ((first.type & Lexeme_1.TokenType.Identifier) === 0 || dot.value !== "." || (last.type & Lexeme_1.TokenType.Identifier) === 0) { continue; } if (normalizeIdentifier(first.value) !== namespace || normalizeIdentifier(last.value) !== column) { continue; } if (!first.position || !last.position) { continue; } return sql.slice(first.position.startPosition, last.position.endPosition); } return normalizeColumnReferenceText(reference); }; const normalizeColumnReferenceKey = (reference) => { return `${normalizeIdentifier(reference.getNamespace())}.${normalizeIdentifier(reference.column.name)}`; }; const normalizeColumnReferenceText = (reference) => { const namespace = reference.getNamespace(); return namespace ? `${namespace}.${reference.column.name}` : reference.column.name; }; const normalizeScalarOperator = (value) => { if (!value) { return "="; } const normalized = value.trim().toLowerCase(); if (normalized === "!=") { return "<>"; } if (SUPPORTED_SCALAR_OPERATORS.has(normalized)) { return normalized; } throw new Error(`Unsupported SSSQL operator '${value}'.`); }; const isExplicitEqualityScaffoldValue = (value) => { var _a; if (value === null || value === undefined) { return true; } if (Array.isArray(value)) { return false; } if (typeof value !== "object") { return true; } const entries = Object.entries(value).filter(([, entry]) => entry !== undefined); return entries.length === 1 && ((_a = entries[0]) === null || _a === void 0 ? void 0 : _a[0]) === "="; }; const parseQualifiedFilterName = (filterName) => { const segments = filterName.split("."); if (segments.length !== 2) { return null; } const [table, column] = segments.map(segment => segment.trim()); if (!table || !column) { return null; } return { table, column }; }; const makeParameterName = (filterName) => { return filterName .trim() .replace(/\./g, "_") .replace(/[^a-zA-Z0-9_]/g, "_"); }; const unwrapParens = (expression) => { let candidate = expression; while (candidate instanceof ValueComponent_1.ParenExpression) { candidate = candidate.expression; } return candidate; }; const isBinaryOperator = (expression, operator) => { return expression instanceof ValueComponent_1.BinaryExpression && expression.operator.value.trim().toLowerCase() === operator; }; const collectTopLevelAndTerms = (expression) => { const candidate = unwrapParens(expression); if (!isBinaryOperator(candidate, "and")) { return [expression]; } return [ ...collectTopLevelAndTerms(candidate.left), ...collectTopLevelAndTerms(candidate.right) ]; }; const collectTopLevelOrTerms = (expression) => { const candidate = unwrapParens(expression); if (!isBinaryOperator(candidate, "or")) { return [expression]; } return [ ...collectTopLevelOrTerms(candidate.left), ...collectTopLevelOrTerms(candidate.right) ]; }; const getGuardedParameterName = (expression) => { const candidate = unwrapParens(expression); if (!isBinaryOperator(candidate, "is")) { return null; } const left = unwrapOptionalGuardParameter(candidate.left); if (!(left instanceof ValueComponent_1.ParameterExpression)) { return null; } const right = unwrapParens(candidate.right); const isNull = (right instanceof ValueComponent_1.LiteralValue && right.value === null) || (right instanceof ValueComponent_1.RawString && right.value.trim().toLowerCase() === "null"); if (!isNull) { return null; } return left.name.value; }; const unwrapOptionalGuardParameter = (expression) => { let candidate = unwrapParens(expression); while (candidate instanceof ValueComponent_1.CastExpression) { candidate = unwrapParens(candidate.input); } if (candidate instanceof ValueComponent_1.FunctionCall && getFunctionCallName(candidate) === "cast" && candidate.argument) { const argument = unwrapParens(candidate.argument); if (isBinaryOperator(argument, "as")) { return unwrapOptionalGuardParameter(argument.left); } } return candidate; }; const getFunctionCallName = (expression) => { const name = expression.qualifiedName.name; return "value" in name ? name.value.toLowerCase() : name.name.toLowerCase(); }; const buildOptionalScalarBranch = (column, parameterName, operator) => { const guard = new ValueComponent_1.BinaryExpression(new ValueComponent_1.ParameterExpression(parameterName), "is", new ValueComponent_1.LiteralValue(null)); const predicate = new ValueComponent_1.BinaryExpression(new ValueComponent_1.ColumnReference(column.getNamespace() || null, column.column.name), operator, new ValueComponent_1.ParameterExpression(parameterName)); return new ValueComponent_1.ParenExpression(new ValueComponent_1.BinaryExpression(guard, "or", predicate)); }; const buildOptionalExistsBranch = (parameterName, subquery, kind) => { const guard = new ValueComponent_1.BinaryExpression(new ValueComponent_1.ParameterExpression(parameterName), "is", new ValueComponent_1.LiteralValue(null)); const existsExpression = new ValueComponent_1.UnaryExpression("exists", new ValueComponent_1.InlineQuery(subquery)); const predicate = kind === "exists" ? existsExpression : new ValueComponent_1.UnaryExpression("not", existsExpression); return new ValueComponent_1.ParenExpression(new ValueComponent_1.BinaryExpression(guard, "or", predicate)); }; const rebuildWhereWithoutTerm = (query, termToRemove) => { if (!query.whereClause) { return; } const terms = collectTopLevelAndTerms(query.whereClause.condition).filter(term => term !== termToRemove); if (terms.length === 0) { query.whereClause = null; return; } let rebuilt = terms[0]; for (let index = 1; index < terms.length; index += 1) { rebuilt = new ValueComponent_1.BinaryExpression(rebuilt, "and", terms[index]); } query.whereClause = new Clause_1.WhereClause(rebuilt); }; const enforceSubqueryConstraints = (sql) => { if (!sql.trim()) { throw new Error("SSSQL EXISTS/NOT EXISTS scaffold query must not be empty."); } if (sql.includes(";")) { throw new Error("SSSQL EXISTS/NOT EXISTS scaffold query must not contain semicolons or multiple statements."); } if (/\blateral\b/i.test(sql)) { throw new Error("LATERAL is not supported in SSSQL EXISTS/NOT EXISTS scaffold."); } }; const substituteAnchorPlaceholders = (sql, formattedColumns) => { const usedIndexes = new Set(); const replaced = sql.replace(/\$c(\d+)/g, (_, indexDigits) => { const index = Number(indexDigits); if (!Number.isInteger(index)) { throw new Error(`Invalid placeholder '$c${indexDigits}' in SSSQL scaffold query.`); } if (index < 0 || index >= formattedColumns.length) { throw new Error(`Placeholder '$c${index}' references a missing SSSQL scaffold anchor column.`); } usedIndexes.add(index); return formattedColumns[index]; }); if (formattedColumns.length === 0) { return replaced; } for (let index = 0; index < formattedColumns.length; index += 1) { if (!usedIndexes.has(index)) { throw new Error(`Missing placeholder '$c${index}' for SSSQL scaffold anchor column.`); } } return replaced; }; const getScalarBranchDetails = (expression, parameterName) => { const meaningfulTerms = collectTopLevelOrTerms(expression) .filter(term => getGuardedParameterName(term) !== parameterName); if (meaningfulTerms.length !== 1) { return null; } const predicate = unwrapParens(meaningfulTerms[0]); if (!(predicate instanceof ValueComponent_1.BinaryExpression)) { return null; } const left = unwrapParens(predicate.left); const right = unwrapParens(predicate.right); if (left instanceof ValueComponent_1.ColumnReference && right instanceof ValueComponent_1.ParameterExpression && right.name.value === parameterName) { try { return { operator: normalizeScalarOperator(predicate.operator.value), target: normalizeColumnReferenceText(left), column: left }; } catch { return null; } } if (right instanceof ValueComponent_1.ColumnReference && left instanceof ValueComponent_1.ParameterExpression && left.name.value === parameterName) { try { return { operator: normalizeScalarOperator(predicate.operator.value), target: normalizeColumnReferenceText(right), column: right }; } catch { return null; } } return null; }; const hasSelectQuery = (value) => { return typeof value === "object" && value !== null && "selectQuery" in value; }; const collectColumnReferencesDeep = (value) => { const references = []; const visited = new WeakSet(); const walk = (candidate) => { if (!candidate || typeof candidate !== "object") { return; } if (candidate instanceof ValueComponent_1.ColumnReference) { references.push(candidate); return; } if (visited.has(candidate)) { return; } visited.add(candidate); if (Array.isArray(candidate)) { for (const item of candidate) { walk(item); } return; } for (const child of Object.values(candidate)) { walk(child); } }; walk(value); return references; }; const getExistsBranchKind = (expression, parameterName) => { const meaningfulTerms = collectTopLevelOrTerms(expression) .filter(term => getGuardedParameterName(term) !== parameterName); if (meaningfulTerms.length !== 1) { return null; } const predicate = unwrapParens(meaningfulTerms[0]); const isInlineQueryValue = (value) => { return value instanceof ValueComponent_1.InlineQuery || hasSelectQuery(value); }; if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "exists") { return isInlineQueryValue(unwrapParens(predicate.expression)) ? "exists" : null; } if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "not exists") { return isInlineQueryValue(unwrapParens(predicate.expression)) ? "not-exists" : null; } if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "not" && unwrapParens(predicate.expression) instanceof ValueComponent_1.UnaryExpression) { const nested = unwrapParens(predicate.expression); if (nested.operator.value.trim().toLowerCase() === "exists" && isInlineQueryValue(unwrapParens(nested.expression))) { return "not-exists"; } } return null; }; const getExistsPredicateDetails = (expression, parameterName) => { const meaningfulTerms = collectTopLevelOrTerms(expression) .filter(term => getGuardedParameterName(term) !== parameterName); if (meaningfulTerms.length !== 1) { return null; } const predicate = unwrapParens(meaningfulTerms[0]); const isInlineQueryValue = (value) => { return value instanceof ValueComponent_1.InlineQuery || hasSelectQuery(value); }; if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "exists") { const candidate = unwrapParens(predicate.expression); if (isInlineQueryValue(candidate)) { return { kind: "exists", subquery: candidate.selectQuery }; } return null; } if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "not exists") { const candidate = unwrapParens(predicate.expression); if (isInlineQueryValue(candidate)) { return { kind: "not-exists", subquery: candidate.selectQuery }; } return null; } if (predicate instanceof ValueComponent_1.UnaryExpression && predicate.operator.value.trim().toLowerCase() === "not" && unwrapParens(predicate.expression) instanceof ValueComponent_1.UnaryExpression) { const nested = unwrapParens(predicate.expression); const candidate = unwrapParens(nested.expression); if (nested.operator.value.trim().toLowerCase() === "exists" && isInlineQueryValue(candidate)) { return { kind: "not-exists", subquery: candidate.selectQuery }; } } return null; }; const getBranchInfo = (branch) => { const scalar = getScalarBranchDetails(branch.expression, branch.parameterName); if (scalar) { return { parameterName: branch.parameterName, kind: "scalar", operator: scalar.operator, target: scalar.target, query: branch.query, expression: branch.expression, sql: (0, SqlComponentFormatter_1.formatSqlComponent)(branch.expression) }; } const existsKind = getExistsBranchKind(branch.expression, branch.parameterName); if (existsKind) { return { parameterName: branch.parameterName, kind: existsKind, query: branch.query, expression: branch.expression, sql: (0, SqlComponentFormatter_1.formatSqlComponent)(branch.expression) }; } return { parameterName: branch.parameterName, kind: "expression", query: branch.query, expression: branch.expression, sql: (0, SqlComponentFormatter_1.formatSqlComponent)(branch.expression) }; }; const isEquivalentScalarBranch = (branch, query, parameterName, operator, targetColumnText) => { return branch.query === query && branch.kind === "scalar" && branch.parameterName === parameterName && branch.operator === operator && branch.target !== undefined && normalizeIdentifier(branch.target) === normalizeIdentifier(targetColumnText); }; /** * Builds and refreshes truthful SSSQL optional filter branches. * Runtime callers should use pruning, not dynamic predicate injection. */ class SSSQLFilterBuilder { constructor(tableColumnResolver) { this.tableColumnResolver = tableColumnResolver; this.finder = new UpstreamSelectQueryFinder_1.UpstreamSelectQueryFinder(this.tableColumnResolver); } list(query) { const parsed = this.parseQuery(query); return (0, PruneOptionalConditionBranches_1.collectSupportedOptionalConditionBranches)(parsed).map(getBranchInfo); } planScaffold(query, filters) { if (typeof query === "string") { const entries = Object.entries(filters); if (entries.length === 1) { const [filterName, filterValue] = entries[0]; if (isExplicitEqualityScaffoldValue(filterValue)) { try { return this.planScalarInsert(query, { target: filterName, parameterName: makeParameterName(filterName), operator: "=" }); } catch { // Fall through to the conservative formatter-backed plan, which reports rewrite errors. } } } } return this.planRewrite(query, parsed => this.scaffold(parsed, filters)); } dryRunScaffold(query, filters) { return this.planScaffold(query, filters); } planScaffoldBranch(query, spec) { if (typeof query === "string" && (spec.kind === "exists" || spec.kind === "not-exists")) { try { return this.planExistsInsert(query, spec); } catch { // Fall through to the conservative formatter-backed plan, which reports rewrite errors. } } if (typeof query === "string" && spec.kind !== "exists" && spec.kind !== "not-exists") { try { return this.planScalarInsert(query, spec); } catch { // Fall through to the conservative formatter-backed plan, which reports rewrite errors. } } return this.planRewrite(query, parsed => this.scaffoldBranch(parsed, spec)); } dryRunScaffoldBranch(query, spec) { return this.planScaffoldBranch(query, spec); } planRefresh(query, filters) { if (typeof query === "string") { try { const parsed = SelectQueryParser_1.SelectQueryParser.parse(query); const result = this.refreshParsed(parsed, filters); if (!result.changed) { return this.buildPlanFromEdits(query, [], [], []); } } catch { // Fall through to the conservative formatter-backed plan, which reports rewrite errors. } } return this.planRewrite(query, parsed => this.refresh(parsed, filters)); } dryRunRefresh(query, filters) { return this.planRefresh(query, filters); } planRemove(query, spec) { if (typeof query === "string") { try { return this.planBranchRemoval(query, spec); } catch { // Fall through to the conservative formatter-backed plan, which reports rewrite errors. } } return this.planRewrite(query, parsed => this.remove(parsed, spec)); } dryRunRemove(query, spec) { return this.planRemove(query, spec); } planRemoveAll(query) { return this.planRewrite(query, parsed => this.removeAll(parsed)); } dryRunRemoveAll(query) { return this.planRemoveAll(query); } scaffold(query, filters) { const parsed = this.parseQuery(query); for (const [filterName, filterValue] of Object.entries(filters)) { if (!isExplicitEqualityScaffoldValue(filterValue)) { throw new Error(`SSSQL scaffold only supports equality filters in v1. Use structured scaffold or refresh for pre-authored branches: '${filterName}'.`); } this.scaffoldBranch(parsed, { target: filterName, parameterName: makeParameterName(filterName), operator: "=" }); } return parsed; } scaffoldBranch(query, spec) { const parsed = this.parseQuery(query); if (spec.kind === "exists" || spec.kind === "not-exists") { this.scaffoldExistsBranch(parsed, spec); return parsed; } this.scaffoldScalarBranch(parsed, spec); return parsed; } refresh(query, filters) { const parsed = this.parseQuery(query); this.refreshParsed(parsed, filters); return parsed; } refreshParsed(parsed, filters) { let changed = false; for (const [filterName, filterValue] of Object.entries(filters)) { let parameterName = filterName; let target = null; let matches = (0, PruneOptionalConditionBranches_1.collectSupportedOptionalConditionBranches)(parsed) .filter(branch => branch.parameterName === parameterName); if (matches.length === 0) { target = this.resolveTarget(parsed, filterName); parameterName = target.parameterName; matches = (0, PruneOptionalConditionBranches_1.collectSupportedOptionalConditionBranches)(parsed) .filter(branch => branch.parameterName === parameterName); } if (matches.length === 0) { if (!target) { target = this.resolveTarget(parsed, filterName); parameterName = target.parameterName; } if (!isExplicitEqualityScaffoldValue(filterValue)) { throw new Error(`No existing SSSQL branch was found for '${filterName}', and v1 scaffold only supports equality filters.`); } this.scaffoldScalarBranch(parsed, { target: filterName, parameterName: target.parameterName, operator: "=" }); changed = true; continue; } if (matches.length > 1) { throw new Error(`Multiple SSSQL branches matched parameter ':${parameterName}'. Refresh is ambiguous.`); } const [match] = matches; if (!match) { continue; } const scalarDetails = getScalarBranchDetails(match.expression, match.parameterName); if (scalarDetails && this.isNullableBranchColumn(match.query, scalarDetails.column)) { continue; } const correlatedPlan = this.buildCorrelatedRefreshPlan(parsed, match); if (correlatedPlan) { if (correlatedPlan.target.query === match.query) { continue; } this.rebaseMovedBranchByAlias(match.expression, correlatedPlan.sourceAlias, correlatedPlan.target.column); rebuildWhereWithoutTerm(match.query, match.expression); correlatedPlan.target.query.appendWhere(match.expression); changed = true; continue; } const scalarPlan = scalarDetails ? this.buildScalarRefreshPlan(parsed, match, scalarDetails) : null; if (scalarPlan) { if (scalarPlan.query === match.query) { continue; } this.rebaseMovedBranch(match.expression, match.query, scalarPlan.column); rebuildWhereWithoutTerm(match.query, match.expression); scalarPlan.query.appendWhere(match.expression); changed = true; continue; } if (!target) { target = this.tryResolveTarget(parsed, filterName); if (!target) { continue; } } if (match.query !== target.query) { this.rebaseMovedBranch(match.expression, match.query, target.column); rebuildWhereWithoutTerm(match.query, match.expression); target.query.appendWhere(match.expression); changed = true; } } return { query: parsed, changed }; } remove(query, spec) { const parsed = this.parseQuery(query); const matches = this.findMatchingBranchInfos(parsed, spec); if (matches.length === 0) { return parsed; } if (matches.length > 1) { throw new Error(`Multiple SSSQL branches matched parameter ':${spec.parameterName}'. Remove is ambiguous.`); } const [match] = matches; if (!match) { return parsed; } rebuildWhereWithoutTerm(match.query, match.expression); return parsed; } removeAll(query) { const parsed = this.parseQuery(query); const matches = this.list(parsed); for (const match of matches) { rebuildWhereWithoutTerm(match.query, match.expression); } return parsed; } parseQuery(query) { return typeof query === "string" ? SelectQueryParser_1.SelectQueryParser.parse(query) : query; } planScalarInsert(sourceSql, spec) { var _a; const parsed = SelectQueryParser_1.SelectQueryParser.parse(sourceSql); const target = this.resolveTarget(parsed, spec.target); if (target.query !== parsed) { return this.planRewrite(sourceSql, query => this.scaffoldBranch(query, spec)); } const parameterName = ((_a = spec.parameterName) === null || _a === void 0 ? void 0 : _a.trim()) || target.parameterName; const operator = normalizeScalarOperator(spec.operator); const targetColumnText = findSourceColumnReferenceText(sourceSql, target.column); const branchSql = `(:${parameterName} is null or ${targetColumnText} ${operator} :${parameterName})`; const normalizedBranch = normalizeSql(branchSql); const duplicate = this.list(parsed).find(existing => (existing.query === target.query && normalizeSql(existing.sql) === normalizedBranch) || isEquivalentScalarBranch(existing, target.query, parameterName, operator, targetColumnText)); if (duplicate) { return this.buildPlanFromEdits(sourceSql, [], [], []); } return this.buildMinimalInsertPlan(sourceSql, branchSql, { branchKind: "scalar", parameterName, column: targetColumnText }); } planExistsInsert(sourceSql, spec) { const parameterName = spec.parameterName.trim(); if (!parameterName) { throw new Error("SSSQL EXISTS/NOT EXISTS scaffold requires parameterName."); } if (spec.anchorColumns.length === 0) { throw new Error("SSSQL EXISTS/NOT EXISTS scaffold requires at least one anchorColumn."); } const parsed = SelectQueryParser_1.SelectQueryParser.parse(sourceSql); const anchorTargets = spec.anchorColumns.map(anchorColumn => this.resolveTarget(parsed, anchorColumn)); const targetQueries = [...new Set(anchorTargets.map(target => target.query))]; if (targetQueries.length !== 1) { throw new Error("SSSQL EXISTS/NOT EXISTS scaffold anchor columns must resolve within one query scope."); } const targetQuery = targetQueries[0]; if (targetQuery !== parsed) { return this.planRewrite(sourceSql, query => this.scaffoldBranch(query, spec)); } const sourceColumns = anchorTargets.map(target => findSourceColumnReferenceText(sourceSql, target.column)); const substitutedSql = substituteAnchorPlaceholders(spec.query, sourceColumns).trim(); enforceSubqueryConstraints(substitutedSql); const subquery = SelectQueryParser_1.SelectQueryParser.parse(substitutedSql); const parameterNames = new Set(ParameterCollector_1.ParameterCollector.collect(subquery).map(parameter => parameter.name.value)); if (parameterNames.size !== 1 || !parameterNames.has(parameterName)) { throw new Error(`SSSQL ${spec.kind.toUpperCase()} scaffold query must reference only parameter ':${parameterName}'.`); } const branchSql = `(:${parameterName} is null or ${spec.kind === "not-exists" ? "not exists" : "exists"} (${substitutedSql}))`; const duplicate = this.list(parsed).find(existing => existing.query === targetQuery && normalizeSql(existing.sql) === normalizeSql(branchSql)); if (duplicate) { return this.buildPlanFromEdits(sourceSql, [], [], []); } return this.buildMinimalInsertPlan(sourceSql, branchSql, { branchKind: spec.kind, parameterName, column: sourceColumns.join(", ") }); } buildMinimalInsertPlan(sourceSql, branchSql, target) { const insertPosition = findMinimalWhereInsertPosition(sourceSql); const needsLeadingSpace = insertPosition.position === 0 || !/\s/.test(sourceSql[insertPosition.position - 1]); const prefix = `${needsLeadingSpace ? " " : ""}${insertPosition.hasWhere ? "and" : "where"} `; const suffix = insertPosition.position < getStatementEndPosition(sourceSql) ? " " : ""; const branchLabel = target.branchKind === "scalar" ? "scalar" : target.branchKind; const edit = { start: insertPosition.position, end: insertPosition.position, before: "", after: `${prefix}${branchSql}${suffix}`, kind: "insert", reason: insertPosition.hasWhere ? `Append SSSQL ${branchLabel} branch to the existing WHERE clause.` : `Create a WHERE clause for the SSSQL ${branchLabel} branch.`, target }; const changedRegions = [{ kind: "target-branch", start: edit.start + prefix.length, end: edit.start + edit.after.length - suffix.length, message: `Inserted SSSQL ${branchLabel} optional branch.` }]; if (insertPosition.hasWhere) { changedRegions.unshift({ kind: "boolean-operator", start: edit.start, end: edit.start + prefix.length, message: `Inserted AND before the SSSQL ${branchLabel} branch.` }); } else { changedRegions.unshift({ kind: "where-keyword", start: edit.start, end: edit.start + prefix.length, message: `Inserted WHERE before the SSSQL ${branchLabel} branch.` }); } return this.buildPlanFromEdits(sourceSql, [edit], changedRegions, []); } planBranchRemoval(sourceSql, spec) { const parsed = SelectQueryParser_1.SelectQueryParser.parse(sourceSql); const matches = this.findMatchingBranchInfos(parsed, spec); if (matches.length > 1) { return this.buildPlanFromEdits(sourceSql, [], [], [], [{ code: "REWRITE_FAILED", message: "SSSQL remove planning found multiple matching branches.", detail: `Multiple SSSQL branches matched parameter ':${spec.parameterName}'. Remove is ambiguous.` }]); } if (matches.length === 0) { return this.buildPlanFromEdits(sourceSql, [], [], []); } const branchSpans = findOptionalBranchSpans(sourceSql, spec.parameterName); if (branchSpans.length > 1) { return this.planRewrite(sourceSql, query => this.remove(query, spec)); } const span = branchSpans[0]; if (!span) { return this.planRewrite(sourceSql, query => this.remove(query, spec)); } const beforeOperator = findBooleanOperatorBefore(sourceSql, span.start); const afterOperator = findBooleanOperatorAfter(sourceSql, span.end); const where = findWhereBefore(sourceSql, span.start); let start = span.start; let end = span.end; const changedRegions = [{ kind: "target-branch", start: span.start, end: span.end, message: "Removed SSSQL optional branch." }]; if (beforeOperator) { start = beforeOperator.start; changedRegions.unshift({ kind: "boolean-operator", start: beforeOperator.start, end: beforeOperator.end, message: `Removed adjacent ${beforeOperator.value.toUpperCase()} before the SSSQL branch.` }); } else if (afterOperator) { end = afterOperator.end; changedRegions.push({ kind: "boolean-operator", start: afterOperator.start, end: afterOperator.end, message: `Removed adjacent ${afterOperator.value.toUpperCase()} after the SSSQL branch.` }); } else if (where) { start = where.start; while (end < sourceSql.length && /\s/.test(sourceSql[end])) { end++; } changedRegions.unshift({ kind: "where-keyword", start: where.start, end: where.end, message: "Removed WHERE because the SSSQL branch was the only condition." }); } const edit = { start, end, before: sourceSql.slice(start, end), after: "", kind: "delete", reason: "Remove the targeted SSSQL optional branch from the source SQL.", target: { branchKind: matches[0].kind, parameterName: matches[0].parameterName, column: matches[0].target } }; return this.buildPlanFromEdits(sourceSql, [edit], changedRegions, []); } buildPlanFromEdits(sourceSql, edits, changedRegions, warnings, errors = []) { const plannedSql = errors.length === 0 ? applyRewriteEdits(sourceSql, edits) : undefined; const beforeTokens = tokenizeForRewritePlan(sourceSql); const beforeComments = collectCommentFragments(sourceSql); const afterTokens = plannedSql !== undefined ? tokenizeForRewritePlan(plannedSql) : []; const afterComments = plannedSql !== undefined ? collectCommentFragments(plannedSql) : []; const commentsPreserved = plannedSql !== undefined ? commentsPreservedInOrder(beforeComments, afterComments) : false; const changedOnlyTargetBranches = errors.length === 0 && changedRegions.every(region => region.kind === "target-branch" || region.kind === "where-keyword" || region.kind === "boolean-operator" || region.kind === "parentheses") && commentsPreserved; const planWarnings = [...warnings]; if (plannedSql !== undefined && applyRewriteEdits(sourceSql, edits) !== plannedSql) { errors = [...errors, { code: "APPLY_PLAN_MISMATCH", message: "Applying SSSQL rewrite plan edits did not reproduce the planned SQL." }]; } if (plannedSql !== undefined) { try { SelectQueryParser_1.SelectQueryParser.parse(plannedSql); } catch (error) { errors = [...errors, { code: "PARSE_AFTER_FAILED", message: "The SQL produced by SSSQL rewrite planning could not be parsed.", detail: error instanceof Error ? error.message : error }]; } } if (plannedSql !== undefined && !commentsPreserved) { planWarnings.push({ code: "COMMENTS_NOT_PRESERVED", message: "One or more input SQL comments are missing or reordered after the SSSQL rewrite." }); } return { ok: errors.length === 0, requiresFullReformat: false, edits, sql: plannedSql, safety: { tokenCountBefore: beforeTokens.length, tokenCountAfter: afterTokens.length, tokenSequencePreserved: plannedSql !== undefined ? tokenSequencesEqual(beforeTokens, afterTokens) : false, commentsPreserved, changedOnlyTargetBranches, changedRegions }, warnings: planWarnings, errors }; } planRewrite(query, rewrite) { const warnings = []; const errors = []; const sourceSql = typeof query === "string" ? query : (0, SqlComponentFormatter_1.formatSqlComponent)(query); if (typeof query !== "string") { warnings.push({ code: "SOURCE_SQL_UNAVAILABLE", message: "SSSQL rewrite planning received an AST, so the source SQL had to be formatter-generated before analysis." }); } let beforeTokens = []; let beforeComments = []; try { beforeTokens = tokenizeForRewritePlan(sourceSql); beforeComments = collectCommentFragments(sourceSql); } catch (error) { errors.push({ code: "TOKENIZE_BEFORE_FAILED", message: "Could not tokenize the input SQL before SSSQL rewrite planning.", detail: error instanceof Error ? error.message : error }); } let plannedSql; let afterTokens = []; let afterComments = []; if (errors.length === 0) { try { const parsed = SelectQueryParser_1.SelectQueryParser.parse(sourceSql); const rewritten = rewrite(parsed); plannedSql = (0, SqlComponentFormatter_1.formatSqlComponent)(rewritten); } catch (error) { errors.push({ code: "REWRITE_FAILED", message: "SSSQL rewrite planning could not produce a rewritten query.", detail: error instanceof Error ? error.message : error }); } } if (plannedSql !== undefined) { try { SelectQueryParser_1.SelectQueryParser.parse(plannedSql); } catch (error) { errors.push({ code: "PARSE_AFTER_FAILED", message: "The SQL produced by SSSQL rewrite planning could not be parsed.", detail: error instanceof Error ? error.message : error }); } try { afterTokens = tokenizeForRewritePlan(plannedSql); afterComments = collectCommentFragments(plannedSql); } catch (error) { errors.push({ code: "TOKENIZE_AFTER_FAILED", message: "Could not tokenize the SQL produced by SSSQL rewrite planning.", detail: error instanceof Error ? error.message : error }); } } const edits = plannedSql !== undefined && plannedSql !== sourceSql ? [{ start: 0, end: sourceSql.length, before: sourceSql, after: plannedSql, kind: "replace", reason: "Current SSSQL rewrite planning is backed by AST rewrite plus formatter output." }] : []; const changedRegions = edits.length > 0 ? [{ kind: "formatter-rewrite", start: 0, end: sourceSql.length, message: "The conservative SSSQL rewrite plan requires replacing formatter output for the full SQL text." }] : []; const requiresFullReformat = edits.length > 0; const tokenSequencePreserved = plannedSql !== undefined ? tokenSequencesEqual(beforeTokens, afterTokens) : false; const commentsPreserved = plannedSql !== undefined ? commentsPreservedInOrder(beforeComments, afterComments) : false; const changedOnlyTargetBranches = edits.length === 0; if (requiresFullReformat) { warnings.push({ code: "FULL_REFORMAT_REQUIRED", message: "The current SSSQL rewrite plan can only represent the change as a full SQL replacement." }); } if (plannedSql !== undefined && !tokenSequencePreserved) { warnings.push({ code: "TOKEN_SEQUENCE_CHANGED", message: "The SQL token sequence changes after the SSSQL rewrite. The conservative planner cannot prove that only target branches changed.", detail: { tokenCountBefore: beforeTokens.length, tokenCountAfter: afterTokens.length } }); } if (plannedSql !== undefined && !commentsPreserved) { warnings.push({ code: "COMMENTS_NOT_PRESERVED", message: "One or more input SQL comments are missing or reordered after the SSSQL rewrite." }); } if (plannedSql !== undefined && countCommandToken(afterTokens, "as") > countCommandToken(beforeTokens, "as")) { warnings.push({ code: "OPTIONAL_ALIAS_AS_ADDED",