rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
537 lines • 23.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzePredicateReachability = exports.PredicateReachabilityAnalyzer = 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 SqlComponentFormatter_1 = require("./SqlComponentFormatter");
const SelectOutputCollector_1 = require("./SelectOutputCollector");
const PredicateExpressionUtils_1 = require("./PredicateExpressionUtils");
class PredicateReachabilityAnalyzer {
analyze(input, options = {}) {
const warnings = [];
const errors = [];
let query = null;
try {
if (typeof input === "string") {
query = SelectQueryParser_1.SelectQueryParser.parse(input);
}
else if (options.cloneInput === false) {
query = input;
}
else {
query = SelectQueryParser_1.SelectQueryParser.parse((0, SqlComponentFormatter_1.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 SelectQuery_1.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 (0, PredicateExpressionUtils_1.collectTopLevelAndTerms)(query.whereClause.condition).map(term => {
const references = this.collectOuterColumnReferences(query, term);
const reaches = [{
scopeId,
relation: "origin",
mode: "rewrite_safe",
predicateSql: (0, SqlComponentFormatter_1.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: (0, SqlComponentFormatter_1.formatSqlComponent)(rebased, options)
});
}
const joinReachability = this.resolveJoinEquivalenceReach(contextRoot, query, references, term, options);
reaches.push(...joinReachability.reaches);
blocked.push(...joinReachability.blocked);
return {
predicateSql: (0, SqlComponentFormatter_1.formatSqlComponent)(term, options),
originScopeId: scopeId,
columnReferences: references.map(PredicateExpressionUtils_1.columnReferenceText),
reaches,
blocked,
probeTargets: this.buildProbeTargets(reaches)
};
});
}
buildProbeTargets(reaches) {
return reaches.map(reach => ({
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 ValueComponent_1.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 Clause_1.JoinOnClause)) {
continue;
}
const equalities = this.collectJoinEqualities(join.condition.condition);
for (const equality of equalities) {
for (const reference of references) {
const equivalent = (0, PredicateExpressionUtils_1.sameColumnReference)(reference, equality.left)
? equality.right
: (0, PredicateExpressionUtils_1.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 = [(0, SqlComponentFormatter_1.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({
scopeId,
relation: "join_equivalence",
mode: "debug_only",
predicateSql: (0, SqlComponentFormatter_1.formatSqlComponent)(rebased, options),
...(targetPredicate ? { targetPredicateSql: (0, SqlComponentFormatter_1.formatSqlComponent)(targetPredicate, options) } : {}),
via
});
}
}
}
return { reaches, blocked };
}
collectJoinEqualities(expression) {
const candidate = (0, PredicateExpressionUtils_1.unwrapParens)(expression);
if ((0, PredicateExpressionUtils_1.isBinaryOperator)(candidate, "and")) {
return [
...this.collectJoinEqualities(candidate.left),
...this.collectJoinEqualities(candidate.right)
];
}
if (candidate instanceof ValueComponent_1.BinaryExpression
&& candidate.operator.value.trim() === "="
&& (0, PredicateExpressionUtils_1.unwrapParens)(candidate.left) instanceof ValueComponent_1.ColumnReference
&& (0, PredicateExpressionUtils_1.unwrapParens)(candidate.right) instanceof ValueComponent_1.ColumnReference) {
return [{
expression: candidate,
left: (0, PredicateExpressionUtils_1.unwrapParens)(candidate.left),
right: (0, PredicateExpressionUtils_1.unwrapParens)(candidate.right)
}];
}
return [];
}
resolveSourceQuery(contextRoot, binding) {
const source = binding.source.datasource;
if (source instanceof Clause_1.SubQuerySource && source.query instanceof SelectQuery_1.SimpleSelectQuery) {
return {
query: source.query,
scopeId: `subquery:${binding.alias}`
};
}
if (!(source instanceof Clause_1.TableSource)) {
return null;
}
const cte = this.findCte(contextRoot, source.table.name);
if (!cte || !(cte.query instanceof SelectQuery_1.SimpleSelectQuery)) {
return null;
}
return {
query: cte.query,
scopeId: `cte:${cte.getSourceAliasName()}`
};
}
resolveDirectOutputColumn(contextRoot, query, columnName) {
const matches = this.collectSelectOutputs(contextRoot, query)
.filter(item => (0, PredicateExpressionUtils_1.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_1.SelectOutputCollector(null, commonTables.length > 0 ? commonTables : null);
return collector.collect(query);
}
findCte(root, name) {
var _a, _b;
const normalized = (0, PredicateExpressionUtils_1.normalizeIdentifier)(name);
const matches = ((_b = (_a = root.withClause) === null || _a === void 0 ? void 0 : _a.tables) !== null && _b !== void 0 ? _b : [])
.filter(table => (0, PredicateExpressionUtils_1.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 => (0, PredicateExpressionUtils_1.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 Clause_1.SubQuerySource) {
return `subquery:${binding.alias}`;
}
if (source instanceof Clause_1.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 ValueComponent_1.ColumnReference)) {
return null;
}
return this.rebasePredicate(predicate, [{
sourceColumn: equivalentColumn,
targetColumn: output.value
}], options);
}
rebasePredicate(expression, targetColumns, options) {
const cloned = (0, PredicateExpressionUtils_1.cloneValueComponent)(expression, options);
const visit = (value) => {
const candidate = (0, PredicateExpressionUtils_1.unwrapParens)(value);
if (candidate instanceof ValueComponent_1.ColumnReference) {
const target = targetColumns.find(item => (0, PredicateExpressionUtils_1.sameColumnReference)(candidate, item.sourceColumn));
if (target) {
candidate.qualifiedName = (0, PredicateExpressionUtils_1.cloneColumnReference)(target.targetColumn).qualifiedName;
}
return;
}
if (candidate instanceof ValueComponent_1.BinaryExpression) {
visit(candidate.left);
visit(candidate.right);
return;
}
if (candidate instanceof ValueComponent_1.UnaryExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.InlineQuery) {
return;
}
if (candidate instanceof ValueComponent_1.FunctionCall) {
if (candidate.argument) {
visit(candidate.argument);
}
if (candidate.filterCondition) {
visit(candidate.filterCondition);
}
return;
}
if (candidate instanceof ValueComponent_1.CastExpression) {
visit(candidate.input);
return;
}
if (candidate instanceof ValueComponent_1.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 ValueComponent_1.BetweenExpression) {
visit(candidate.expression);
visit(candidate.lower);
visit(candidate.upper);
return;
}
if (candidate instanceof ValueComponent_1.JsonPredicateExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.ArrayExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.ArrayQueryExpression) {
return;
}
if (candidate instanceof ValueComponent_1.ValueList) {
candidate.values.forEach(visit);
return;
}
if (candidate instanceof ValueComponent_1.TupleExpression) {
candidate.values.forEach(visit);
return;
}
if (candidate instanceof ValueComponent_1.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(PredicateExpressionUtils_1.normalizeIdentifier));
const collect = (reference) => {
if (!references.some(existing => (0, PredicateExpressionUtils_1.sameColumnReference)(existing, reference))) {
references.push(reference);
}
};
const visit = (value) => {
const candidate = (0, PredicateExpressionUtils_1.unwrapParens)(value);
if (candidate instanceof ValueComponent_1.ColumnReference) {
const namespace = (0, PredicateExpressionUtils_1.normalizeIdentifier)(candidate.getNamespace());
if (!namespace || sourceAliases.has(namespace)) {
collect(candidate);
}
return;
}
if (candidate instanceof ValueComponent_1.BinaryExpression) {
visit(candidate.left);
visit(candidate.right);
return;
}
if (candidate instanceof ValueComponent_1.UnaryExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.InlineQuery) {
return;
}
if (candidate instanceof ValueComponent_1.FunctionCall) {
if (candidate.argument) {
visit(candidate.argument);
}
if (candidate.filterCondition) {
visit(candidate.filterCondition);
}
return;
}
if (candidate instanceof ValueComponent_1.CastExpression) {
visit(candidate.input);
return;
}
if (candidate instanceof ValueComponent_1.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 ValueComponent_1.BetweenExpression) {
visit(candidate.expression);
visit(candidate.lower);
visit(candidate.upper);
return;
}
if (candidate instanceof ValueComponent_1.JsonPredicateExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.ArrayExpression) {
visit(candidate.expression);
return;
}
if (candidate instanceof ValueComponent_1.ArrayQueryExpression) {
return;
}
if (candidate instanceof ValueComponent_1.ValueList) {
candidate.values.forEach(visit);
return;
}
if (candidate instanceof ValueComponent_1.TupleExpression) {
candidate.values.forEach(visit);
return;
}
if (candidate instanceof ValueComponent_1.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
}
};
}
}
exports.PredicateReachabilityAnalyzer = PredicateReachabilityAnalyzer;
const analyzePredicateReachability = (input, options = {}) => {
return new PredicateReachabilityAnalyzer().analyze(input, options);
};
exports.analyzePredicateReachability = analyzePredicateReachability;
//# sourceMappingURL=PredicateReachabilityAnalyzer.js.map