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
JavaScript
"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",