UNPKG

rawsql-ts

Version:

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

516 lines 22 kB
import { JoinOnClause, SubQuerySource, TableSource } from "../models/Clause"; import { SimpleSelectQuery } from "../models/SelectQuery"; import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, InlineQuery, JsonPredicateExpression, TupleExpression, TypeValue, UnaryExpression, ValueList } from "../models/ValueComponent"; import { SelectQueryParser } from "../parsers/SelectQueryParser"; import { formatSqlComponent } from "./SqlComponentFormatter"; import { SelectOutputCollector } from "./SelectOutputCollector"; import { cloneColumnReference, cloneValueComponent, collectTopLevelAndTerms, columnReferenceText, identifiersEqual, isBinaryOperator, normalizeIdentifier, sameColumnReference, unwrapParens } from "./PredicateExpressionUtils"; export class PredicateReachabilityAnalyzer { analyze(input, options = {}) { const warnings = []; const errors = []; let query = null; try { if (typeof input === "string") { query = SelectQueryParser.parse(input); } else if (options.cloneInput === false) { query = input; } else { query = SelectQueryParser.parse(formatSqlComponent(input, options)); } } catch (error) { errors.push({ code: "PARSE_FAILED", message: "Predicate reachability analysis could not parse the input SQL.", detail: error instanceof Error ? error.message : String(error) }); return this.buildResult(null, [], warnings, errors); } if (!(query instanceof SimpleSelectQuery)) { warnings.push({ code: "UNSUPPORTED_ROOT_QUERY", message: "Predicate reachability analysis currently supports only SimpleSelectQuery roots." }); return this.buildResult(query, [], warnings, errors); } const predicates = this.analyzeScope(query, query, "scope:root", options); return this.buildResult(query, predicates, warnings, errors); } analyzeScope(contextRoot, query, scopeId, options) { if (!query.whereClause) { return []; } return collectTopLevelAndTerms(query.whereClause.condition).map(term => { const references = this.collectOuterColumnReferences(query, term); const reaches = [{ scopeId, relation: "origin", mode: "rewrite_safe", predicateSql: formatSqlComponent(term, options) }]; const blocked = []; const direct = this.resolveDirectOutputReach(contextRoot, query, references); if (direct) { const rebased = this.rebasePredicate(term, direct.targetColumns, options); reaches.push({ scopeId: direct.scopeId, relation: "direct_output", mode: "rewrite_safe", predicateSql: formatSqlComponent(rebased, options) }); } const joinReachability = this.resolveJoinEquivalenceReach(contextRoot, query, references, term, options); reaches.push(...joinReachability.reaches); blocked.push(...joinReachability.blocked); return { predicateSql: formatSqlComponent(term, options), originScopeId: scopeId, columnReferences: references.map(columnReferenceText), reaches, blocked, probeTargets: this.buildProbeTargets(reaches) }; }); } buildProbeTargets(reaches) { return reaches.map(reach => (Object.assign(Object.assign(Object.assign({ scopeId: reach.scopeId, relation: reach.relation, mode: reach.mode, predicateSql: reach.predicateSql }, (reach.targetPredicateSql ? { targetPredicateSql: reach.targetPredicateSql } : {})), { target: this.describeScopeTarget(reach.scopeId), probeKinds: ["count", "sample"] }), (reach.mode === "debug_only" ? { caution: "This target is inferred through a debug-only relationship and is not a safe SQL rewrite target." } : {})))); } describeScopeTarget(scopeId) { const [kind, ...rest] = scopeId.split(":"); const name = rest.join(":") || scopeId; switch (kind) { case "cte": return { kind: "cte", name }; case "table": return { kind: "table", name }; case "subquery": return { kind: "subquery", name }; case "source": return { kind: "source", name }; default: return { kind: "scope", name }; } } resolveDirectOutputReach(contextRoot, query, references) { if (references.length === 0 || !query.fromClause) { return null; } const bindings = []; for (const reference of references) { const binding = this.resolveSourceBinding(query, reference); if (!binding) { return null; } if (!bindings.some(item => item.source === binding.source)) { bindings.push(binding); } } if (bindings.length !== 1) { return null; } const binding = bindings[0]; const target = this.resolveSourceQuery(contextRoot, binding); if (!target) { return null; } const targetColumns = []; for (const reference of references) { const output = this.resolveDirectOutputColumn(contextRoot, target.query, reference.column.name); if (!output || !(output.value instanceof ColumnReference)) { return null; } targetColumns.push({ sourceColumn: reference, targetColumn: output.value }); } return { query: target.query, scopeId: target.scopeId, targetColumns }; } resolveJoinEquivalenceReach(contextRoot, query, references, predicate, options) { var _a; const reaches = []; const blocked = []; if (!query.fromClause || references.length === 0) { return { reaches, blocked }; } const bindings = this.getSourceBindings(query.fromClause); for (const join of (_a = query.fromClause.joins) !== null && _a !== void 0 ? _a : []) { if (!(join.condition instanceof JoinOnClause)) { continue; } const equalities = this.collectJoinEqualities(join.condition.condition); for (const equality of equalities) { for (const reference of references) { const equivalent = sameColumnReference(reference, equality.left) ? equality.right : sameColumnReference(reference, equality.right) ? equality.left : null; if (!equivalent) { continue; } const referenceBinding = this.resolveSourceBindingFromList(bindings, reference); if (!referenceBinding) { continue; } const targetBinding = this.resolveSourceBindingFromList(bindings, equivalent); if (!targetBinding) { continue; } const via = [formatSqlComponent(equality.expression, options)]; const rebased = this.rebasePredicate(predicate, [{ sourceColumn: reference, targetColumn: equivalent }], options); const scopeId = this.scopeIdForBinding(contextRoot, targetBinding); const joinEquivalenceAllowed = this.isInnerJoin(join) || this.isLeftJoinPreservedToNullableReach(join, referenceBinding, targetBinding); if (!joinEquivalenceAllowed) { blocked.push({ scopeId, relation: "join_equivalence", code: "OUTER_JOIN_EQUIVALENCE_UNSUPPORTED", reason: "JOIN equivalence debug reachability is reported for INNER JOIN predicates and LEFT JOIN preserved-side predicates only.", via }); continue; } const targetPredicate = this.rebasePredicateToSourceQuery(contextRoot, targetBinding, equivalent, rebased, options); reaches.push(Object.assign(Object.assign({ scopeId, relation: "join_equivalence", mode: "debug_only", predicateSql: formatSqlComponent(rebased, options) }, (targetPredicate ? { targetPredicateSql: formatSqlComponent(targetPredicate, options) } : {})), { via })); } } } return { reaches, blocked }; } collectJoinEqualities(expression) { const candidate = unwrapParens(expression); if (isBinaryOperator(candidate, "and")) { return [ ...this.collectJoinEqualities(candidate.left), ...this.collectJoinEqualities(candidate.right) ]; } if (candidate instanceof BinaryExpression && candidate.operator.value.trim() === "=" && unwrapParens(candidate.left) instanceof ColumnReference && unwrapParens(candidate.right) instanceof ColumnReference) { return [{ expression: candidate, left: unwrapParens(candidate.left), right: unwrapParens(candidate.right) }]; } return []; } resolveSourceQuery(contextRoot, binding) { const source = binding.source.datasource; if (source instanceof SubQuerySource && source.query instanceof SimpleSelectQuery) { return { query: source.query, scopeId: `subquery:${binding.alias}` }; } if (!(source instanceof TableSource)) { return null; } const cte = this.findCte(contextRoot, source.table.name); if (!cte || !(cte.query instanceof SimpleSelectQuery)) { return null; } return { query: cte.query, scopeId: `cte:${cte.getSourceAliasName()}` }; } resolveDirectOutputColumn(contextRoot, query, columnName) { const matches = this.collectSelectOutputs(contextRoot, query) .filter(item => identifiersEqual(item.name, columnName)); return matches.length === 1 ? matches[0] : null; } collectSelectOutputs(root, query) { var _a, _b, _c, _d; const commonTables = [ ...((_b = (_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []), ...((_d = (_c = root.withClause) === null || _c === void 0 ? void 0 : _c.tables) !== null && _d !== void 0 ? _d : []) ]; const collector = new SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null); return collector.collect(query); } findCte(root, name) { var _a, _b; const normalized = normalizeIdentifier(name); const matches = ((_b = (_a = root.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : []) .filter(table => normalizeIdentifier(table.getSourceAliasName()) === normalized); return matches.length === 1 ? matches[0] : null; } resolveSourceBinding(query, column) { if (!query.fromClause) { return null; } return this.resolveSourceBindingFromList(this.getSourceBindings(query.fromClause), column); } resolveSourceBindingFromList(bindings, column) { const namespace = column.getNamespace(); if (namespace) { const matches = bindings.filter(binding => identifiersEqual(binding.alias, namespace)); return matches.length === 1 ? matches[0] : null; } return bindings.length === 1 ? bindings[0] : null; } getSourceBindings(fromClause) { var _a, _b, _c; const bindings = [{ source: fromClause.source, alias: (_a = fromClause.source.getAliasName()) !== null && _a !== void 0 ? _a : "", join: null, isPrimary: true }]; for (const join of (_b = fromClause.joins) !== null && _b !== void 0 ? _b : []) { bindings.push({ source: join.source, alias: (_c = join.source.getAliasName()) !== null && _c !== void 0 ? _c : "", join, isPrimary: false }); } return bindings; } scopeIdForBinding(contextRoot, binding) { const source = binding.source.datasource; if (source instanceof SubQuerySource) { return `subquery:${binding.alias}`; } if (source instanceof TableSource) { if (contextRoot && this.findCte(contextRoot, source.table.name)) { return `cte:${source.table.name}`; } return `table:${source.getSourceName()}`; } return `source:${binding.alias || "unknown"}`; } isInnerJoin(join) { const joinType = join.joinType.value.trim().toLowerCase(); return joinType === "join" || joinType === "inner join"; } isLeftJoinPreservedToNullableReach(join, referenceBinding, targetBinding) { const joinType = join.joinType.value.trim().toLowerCase(); return (joinType === "left join" || joinType === "left outer join") && targetBinding.source === join.source && referenceBinding.source !== join.source; } rebasePredicateToSourceQuery(contextRoot, targetBinding, equivalentColumn, predicate, options) { const target = this.resolveSourceQuery(contextRoot, targetBinding); if (!target) { return null; } const output = this.resolveDirectOutputColumn(contextRoot, target.query, equivalentColumn.column.name); if (!output || !(output.value instanceof ColumnReference)) { return null; } return this.rebasePredicate(predicate, [{ sourceColumn: equivalentColumn, targetColumn: output.value }], options); } rebasePredicate(expression, targetColumns, options) { const cloned = cloneValueComponent(expression, options); const visit = (value) => { const candidate = unwrapParens(value); if (candidate instanceof ColumnReference) { const target = targetColumns.find(item => sameColumnReference(candidate, item.sourceColumn)); if (target) { candidate.qualifiedName = cloneColumnReference(target.targetColumn).qualifiedName; } return; } if (candidate instanceof BinaryExpression) { visit(candidate.left); visit(candidate.right); return; } if (candidate instanceof UnaryExpression) { visit(candidate.expression); return; } if (candidate instanceof InlineQuery) { return; } if (candidate instanceof FunctionCall) { if (candidate.argument) { visit(candidate.argument); } if (candidate.filterCondition) { visit(candidate.filterCondition); } return; } if (candidate instanceof CastExpression) { visit(candidate.input); return; } if (candidate instanceof CaseExpression) { if (candidate.condition) { visit(candidate.condition); } for (const pair of candidate.switchCase.cases) { visit(pair.key); visit(pair.value); } if (candidate.switchCase.elseValue) { visit(candidate.switchCase.elseValue); } return; } if (candidate instanceof BetweenExpression) { visit(candidate.expression); visit(candidate.lower); visit(candidate.upper); return; } if (candidate instanceof JsonPredicateExpression) { visit(candidate.expression); return; } if (candidate instanceof ArrayExpression) { visit(candidate.expression); return; } if (candidate instanceof ArrayQueryExpression) { return; } if (candidate instanceof ValueList) { candidate.values.forEach(visit); return; } if (candidate instanceof TupleExpression) { candidate.values.forEach(visit); return; } if (candidate instanceof TypeValue && candidate.argument) { visit(candidate.argument); } }; visit(cloned); return cloned; } collectOuterColumnReferences(query, expression) { var _a, _b; const references = []; const sourceAliases = new Set(((_b = (_a = query.fromClause) === null || _a === void 0 ? void 0 : _a.getSources()) !== null && _b !== void 0 ? _b : []) .map(source => source.getAliasName()) .filter((alias) => alias !== null) .map(normalizeIdentifier)); const collect = (reference) => { if (!references.some(existing => sameColumnReference(existing, reference))) { references.push(reference); } }; const visit = (value) => { const candidate = unwrapParens(value); if (candidate instanceof ColumnReference) { const namespace = normalizeIdentifier(candidate.getNamespace()); if (!namespace || sourceAliases.has(namespace)) { collect(candidate); } return; } if (candidate instanceof BinaryExpression) { visit(candidate.left); visit(candidate.right); return; } if (candidate instanceof UnaryExpression) { visit(candidate.expression); return; } if (candidate instanceof InlineQuery) { return; } if (candidate instanceof FunctionCall) { if (candidate.argument) { visit(candidate.argument); } if (candidate.filterCondition) { visit(candidate.filterCondition); } return; } if (candidate instanceof CastExpression) { visit(candidate.input); return; } if (candidate instanceof CaseExpression) { if (candidate.condition) { visit(candidate.condition); } for (const pair of candidate.switchCase.cases) { visit(pair.key); visit(pair.value); } if (candidate.switchCase.elseValue) { visit(candidate.switchCase.elseValue); } return; } if (candidate instanceof BetweenExpression) { visit(candidate.expression); visit(candidate.lower); visit(candidate.upper); return; } if (candidate instanceof JsonPredicateExpression) { visit(candidate.expression); return; } if (candidate instanceof ArrayExpression) { visit(candidate.expression); return; } if (candidate instanceof ArrayQueryExpression) { return; } if (candidate instanceof ValueList) { candidate.values.forEach(visit); return; } if (candidate instanceof TupleExpression) { candidate.values.forEach(visit); return; } if (candidate instanceof TypeValue && candidate.argument) { visit(candidate.argument); } }; visit(expression); return references; } buildResult(query, predicates, warnings, errors) { // API output shape review: this diagnostic API reports structured reachability only and never emits rewritten SQL. return { ok: errors.length === 0, query, predicates, warnings, errors, safety: { mode: "debug_only", sqlRewritten: false } }; } } export const analyzePredicateReachability = (input, options = {}) => { return new PredicateReachabilityAnalyzer().analyze(input, options); }; //# sourceMappingURL=PredicateReachabilityAnalyzer.js.map