rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
516 lines • 22 kB
JavaScript
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