rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
523 lines • 21.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.collectSupportedOptionalConditionBranchSpans = exports.collectSupportedOptionalConditionBranches = exports.pruneOptionalConditionBranches = void 0;
const Clause_1 = require("../models/Clause");
const Lexeme_1 = require("../models/Lexeme");
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 ValueParser_1 = require("../parsers/ValueParser");
const ParameterCollector_1 = require("./ParameterCollector");
const isBinaryOperator = (expression, operator) => {
return expression instanceof ValueComponent_1.BinaryExpression && expression.operator.value.trim().toLowerCase() === operator;
};
const unwrapSingleOuterParen = (expression) => {
let candidate = expression;
// Generated SQL often nests harmless wrapper parentheses, so peel them before shape matching.
while (candidate instanceof ValueComponent_1.ParenExpression) {
candidate = candidate.expression;
}
return candidate;
};
const unwrapOptionalGuardParameter = (expression) => {
let candidate = unwrapSingleOuterParen(expression);
while (candidate instanceof ValueComponent_1.CastExpression) {
candidate = unwrapSingleOuterParen(candidate.input);
}
if (candidate instanceof ValueComponent_1.FunctionCall && getFunctionCallName(candidate) === 'cast' && candidate.argument) {
const argument = unwrapSingleOuterParen(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 collectTopLevelAndTerms = (expression) => {
const candidate = unwrapSingleOuterParen(expression);
if (!isBinaryOperator(candidate, 'and')) {
return [expression];
}
return [
...collectTopLevelAndTerms(candidate.left),
...collectTopLevelAndTerms(candidate.right)
];
};
const collectTopLevelOrTerms = (expression) => {
const candidate = unwrapSingleOuterParen(expression);
if (!isBinaryOperator(candidate, 'or')) {
return [expression];
}
return [
...collectTopLevelOrTerms(candidate.left),
...collectTopLevelOrTerms(candidate.right)
];
};
const isNullLiteral = (expression) => {
return ((expression instanceof ValueComponent_1.LiteralValue && expression.value === null) ||
(expression instanceof ValueComponent_1.RawString && expression.value.trim().toLowerCase() === 'null'));
};
const isTrueSentinel = (expression) => {
const candidate = unwrapSingleOuterParen(expression);
if (candidate instanceof ValueComponent_1.LiteralValue) {
return candidate.value === true;
}
if (!isBinaryOperator(candidate, '=')) {
return false;
}
return (candidate.left instanceof ValueComponent_1.LiteralValue &&
candidate.right instanceof ValueComponent_1.LiteralValue &&
candidate.left.value === 1 &&
candidate.right.value === 1);
};
const getGuardedParameterName = (expression) => {
const candidate = unwrapSingleOuterParen(expression);
if (!isBinaryOperator(candidate, 'is')) {
return null;
}
const left = unwrapOptionalGuardParameter(candidate.left);
if (!(left instanceof ValueComponent_1.ParameterExpression) || !isNullLiteral(candidate.right)) {
return null;
}
return left.name.value;
};
const getUniqueParameterNames = (expression) => {
return new Set(ParameterCollector_1.ParameterCollector.collect(expression).map(parameter => parameter.name.value));
};
const isSupportedMeaningfulBranch = (expression, parameterName) => {
const candidate = unwrapSingleOuterParen(expression);
if (candidate instanceof ValueComponent_1.ParameterExpression) {
return false;
}
const parameterNames = getUniqueParameterNames(candidate);
if (parameterNames.size !== 1 || !parameterNames.has(parameterName)) {
return false;
}
// Keep the matcher conservative enough to avoid pruning tautologies or half-authored branches.
return !(candidate instanceof ValueComponent_1.LiteralValue || candidate instanceof ValueComponent_1.RawString);
};
const isExplicitPruningTarget = (pruningParameters, parameterName) => {
return Object.prototype.hasOwnProperty.call(pruningParameters, parameterName);
};
const isKnownAbsentTarget = (pruningParameters, parameterName) => {
if (!isExplicitPruningTarget(pruningParameters, parameterName)) {
return false;
}
const parameterValue = pruningParameters[parameterName];
return parameterValue === null || parameterValue === undefined;
};
const shouldPruneOptionalBranch = (expression, pruningParameters) => {
const branch = getSupportedOptionalConditionBranch(expression);
return branch !== null && isKnownAbsentTarget(pruningParameters, branch.parameterName);
};
const rebuildAndCondition = (terms) => {
if (terms.length === 0) {
return null;
}
let condition = terms[0];
for (let index = 1; index < terms.length; index += 1) {
condition = new ValueComponent_1.BinaryExpression(condition, 'and', terms[index]);
}
return condition;
};
const pruneSimpleQueryWhereClause = (query, pruningParameters) => {
if (!query.whereClause) {
return false;
}
const topLevelTerms = collectTopLevelAndTerms(query.whereClause.condition);
const retainedTerms = [];
let prunedAnyBranch = false;
// Only top-level WHERE ... AND ... terms are eligible for pruning in this MVP.
for (const term of topLevelTerms) {
if (shouldPruneOptionalBranch(term, pruningParameters)) {
prunedAnyBranch = true;
continue;
}
retainedTerms.push(term);
}
if (!prunedAnyBranch) {
return false;
}
// Cleanup stays intentionally conservative: only drop trivially-true sentinels after pruning.
const cleanedTerms = retainedTerms.filter(term => !isTrueSentinel(term));
const rebuiltCondition = rebuildAndCondition(cleanedTerms);
query.whereClause = rebuiltCondition ? new Clause_1.WhereClause(rebuiltCondition) : null;
return true;
};
const isSelectQueryNode = (value) => {
return value instanceof SelectQuery_1.SimpleSelectQuery || value instanceof SelectQuery_1.BinarySelectQuery;
};
const traverseNestedSelectQueries = (root, pruningParameters) => {
let changed = false;
const visited = new WeakSet();
const walk = (value) => {
if (!value || typeof value !== 'object') {
return;
}
if (visited.has(value)) {
return;
}
visited.add(value);
if (value !== root && isSelectQueryNode(value)) {
changed = traverseSelectQuery(value, pruningParameters) || changed;
return;
}
if (Array.isArray(value)) {
value.forEach(walk);
return;
}
for (const child of Object.values(value)) {
walk(child);
}
};
walk(root);
return changed;
};
const traverseSelectQuery = (query, pruningParameters) => {
if (query instanceof SelectQuery_1.SimpleSelectQuery) {
const selfChanged = pruneSimpleQueryWhereClause(query, pruningParameters);
const nestedChanged = traverseNestedSelectQueries(query, pruningParameters);
return selfChanged || nestedChanged;
}
if (query instanceof SelectQuery_1.BinarySelectQuery) {
const leftChanged = traverseSelectQuery(query.left, pruningParameters);
const rightChanged = traverseSelectQuery(query.right, pruningParameters);
return leftChanged || rightChanged;
}
return false;
};
const getSupportedOptionalConditionBranch = (expression) => {
const orTerms = collectTopLevelOrTerms(expression);
if (orTerms.length < 2) {
return null;
}
const guardTerms = orTerms
.map(term => ({ term, parameterName: getGuardedParameterName(term) }))
.filter((candidate) => candidate.parameterName !== null);
if (guardTerms.length !== 1) {
return null;
}
const [{ term: guardTerm, parameterName }] = guardTerms;
const meaningfulTerms = orTerms.filter(term => term !== guardTerm);
if (meaningfulTerms.length === 0) {
return null;
}
if (!meaningfulTerms.every(term => isSupportedMeaningfulBranch(term, parameterName))) {
return null;
}
return {
parameterName,
kind: 'expression'
};
};
const collectSupportedBranchesFromSimpleQuery = (query, branches) => {
if (!query.whereClause) {
return;
}
const topLevelTerms = collectTopLevelAndTerms(query.whereClause.condition);
for (const term of topLevelTerms) {
const branch = getSupportedOptionalConditionBranch(term);
if (!branch) {
continue;
}
branches.push({
query,
parameterName: branch.parameterName,
expression: term,
kind: branch.kind
});
}
};
const collectSupportedBranchesFromSelectQuery = (query, branches) => {
if (query instanceof SelectQuery_1.SimpleSelectQuery) {
collectSupportedBranchesFromSimpleQuery(query, branches);
traverseNestedSelectQueriesForCollection(query, branches);
return;
}
if (query instanceof SelectQuery_1.BinarySelectQuery) {
collectSupportedBranchesFromSelectQuery(query.left, branches);
collectSupportedBranchesFromSelectQuery(query.right, branches);
}
};
const traverseNestedSelectQueriesForCollection = (root, branches) => {
const visited = new WeakSet();
const walk = (value) => {
if (!value || typeof value !== 'object') {
return;
}
if (visited.has(value)) {
return;
}
visited.add(value);
if (value !== root && isSelectQueryNode(value)) {
collectSupportedBranchesFromSelectQuery(value, branches);
return;
}
if (Array.isArray(value)) {
value.forEach(walk);
return;
}
for (const child of Object.values(value)) {
walk(child);
}
};
walk(root);
};
/**
* Prunes supported optional WHERE branches when an explicitly targeted parameter is absent-equivalent.
* For the MVP, only `null` and `undefined` are treated as absent and unsupported shapes remain exact no-op.
*/
const pruneOptionalConditionBranches = (query, pruningParameters) => {
if (Object.keys(pruningParameters).length === 0) {
return query;
}
traverseSelectQuery(query, pruningParameters);
return query;
};
exports.pruneOptionalConditionBranches = pruneOptionalConditionBranches;
/**
* Collects supported top-level optional condition branches from the query graph.
* The returned branch expressions keep object identity so callers can move them without re-rendering.
*/
const collectSupportedOptionalConditionBranches = (query) => {
const branches = [];
collectSupportedBranchesFromSelectQuery(query, branches);
return branches;
};
exports.collectSupportedOptionalConditionBranches = collectSupportedOptionalConditionBranches;
/**
* Collects supported optional condition branches with source-text ranges.
*
* The AST collector remains the authority for whether a branch is supported. The range metadata
* is derived from tokenizer positions so development tools can generate runtime metadata without
* reparsing SQL in production.
*/
const collectSupportedOptionalConditionBranchSpans = (sql) => {
var _a;
const parsed = SelectQueryParser_1.SelectQueryParser.parse(sql);
const supportedBranches = (0, exports.collectSupportedOptionalConditionBranches)(parsed);
if (supportedBranches.length === 0) {
return [];
}
const candidates = collectOptionalConditionSpanCandidates(sql);
const remainingSupportedCounts = countSupportedBranchesByKey(supportedBranches);
assertUnambiguousCandidateCounts(candidates, remainingSupportedCounts);
const spans = [];
for (const candidate of candidates) {
const key = getSupportedBranchKey(candidate);
const remainingCount = (_a = remainingSupportedCounts.get(key)) !== null && _a !== void 0 ? _a : 0;
if (remainingCount <= 0) {
continue;
}
spans.push(candidate);
remainingSupportedCounts.set(key, remainingCount - 1);
}
assertNoMissingSupportedBranches(remainingSupportedCounts);
return spans;
};
exports.collectSupportedOptionalConditionBranchSpans = collectSupportedOptionalConditionBranchSpans;
const getSupportedBranchKey = (branch) => `${branch.kind}:${branch.parameterName}`;
const countSupportedBranchesByKey = (branches) => {
var _a;
const counts = new Map();
for (const branch of branches) {
const key = getSupportedBranchKey(branch);
counts.set(key, ((_a = counts.get(key)) !== null && _a !== void 0 ? _a : 0) + 1);
}
return counts;
};
const assertUnambiguousCandidateCounts = (candidates, supportedCounts) => {
var _a;
const candidateCounts = countSupportedBranchesByKey(candidates);
for (const [key, supportedCount] of supportedCounts) {
const candidateCount = (_a = candidateCounts.get(key)) !== null && _a !== void 0 ? _a : 0;
if (candidateCount < supportedCount) {
throw new Error(`Could not locate source range for supported optional condition branch '${key}'.`);
}
if (candidateCount > supportedCount) {
throw new Error(`Ambiguous source ranges for supported optional condition branch '${key}'.`);
}
}
};
const assertNoMissingSupportedBranches = (supportedCounts) => {
const missingKeys = [...supportedCounts.entries()]
.filter(([, count]) => count > 0)
.map(([key]) => key);
if (missingKeys.length > 0) {
throw new Error(`Could not locate source ranges for supported optional condition branches: ${missingKeys.join(', ')}.`);
}
};
const collectOptionalConditionSpanCandidates = (sql) => {
const lexemes = new SqlTokenizer_1.SqlTokenizer(sql).readLexemes();
const candidates = [];
const stack = [];
for (let index = 0; index < lexemes.length; index += 1) {
const lexeme = lexemes[index];
if (isOpenParen(lexeme)) {
stack.push(index);
continue;
}
if (!isCloseParen(lexeme)) {
continue;
}
const openParenIndex = stack.pop();
if (openParenIndex === undefined) {
continue;
}
const candidate = buildOptionalConditionSpanCandidate(sql, lexemes, openParenIndex, index);
if (candidate) {
candidates.push(candidate);
}
}
return candidates.sort((left, right) => left.sourceRange.start - right.sourceRange.start);
};
const buildOptionalConditionSpanCandidate = (sql, lexemes, openParenIndex, closeParenIndex) => {
const inside = lexemes.slice(openParenIndex + 1, closeParenIndex);
const orTermRanges = splitTopLevelTermsByKeyword(inside, 'or');
if (orTermRanges.length < 2) {
return null;
}
const guardTerms = orTermRanges
.map(range => ({ range, parameterName: getGuardedParameterNameFromLexemes(inside.slice(range.start, range.end)) }))
.filter((candidate) => candidate.parameterName !== null);
if (guardTerms.length !== 1) {
return null;
}
const [{ range: guardRange, parameterName }] = guardTerms;
const meaningfulTerms = orTermRanges.filter(range => range !== guardRange);
if (meaningfulTerms.length === 0) {
return null;
}
if (!meaningfulTerms.every(range => isSupportedMeaningfulBranchFromLexemes(inside.slice(range.start, range.end), parameterName))) {
return null;
}
const expandedRange = expandWrappingParenRange(lexemes, openParenIndex, closeParenIndex);
const sourceStart = requiredPosition(lexemes[expandedRange.openParenIndex]).startPosition;
const sourceEnd = requiredPosition(lexemes[expandedRange.closeParenIndex]).endPosition;
const removalRange = getRemovalRange(sql, lexemes, expandedRange.openParenIndex, expandedRange.closeParenIndex);
return {
parameterName,
kind: 'expression',
sourceRange: {
start: sourceStart,
end: sourceEnd,
text: sql.slice(sourceStart, sourceEnd)
},
removalRange,
openParenIndex: expandedRange.openParenIndex,
closeParenIndex: expandedRange.closeParenIndex
};
};
const expandWrappingParenRange = (lexemes, openParenIndex, closeParenIndex) => {
let expandedOpenParenIndex = openParenIndex;
let expandedCloseParenIndex = closeParenIndex;
while (isOpenParen(lexemes[expandedOpenParenIndex - 1]) &&
isCloseParen(lexemes[expandedCloseParenIndex + 1])) {
expandedOpenParenIndex -= 1;
expandedCloseParenIndex += 1;
}
return {
openParenIndex: expandedOpenParenIndex,
closeParenIndex: expandedCloseParenIndex
};
};
const splitTopLevelTermsByKeyword = (lexemes, keyword) => {
const ranges = [];
let depth = 0;
let start = 0;
for (let index = 0; index < lexemes.length; index += 1) {
const lexeme = lexemes[index];
if (isOpenParen(lexeme)) {
depth += 1;
continue;
}
if (isCloseParen(lexeme)) {
depth -= 1;
continue;
}
if (depth === 0 && isKeyword(lexeme, keyword)) {
ranges.push({ start, end: index });
start = index + 1;
}
}
ranges.push({ start, end: lexemes.length });
return ranges;
};
const getGuardedParameterNameFromLexemes = (lexemes) => {
var _a;
const compact = lexemes.filter(lexeme => !isWrappingParen(lexeme));
const isIndexKeyword = (index, keyword) => {
const lexeme = compact[index];
return lexeme !== undefined && isKeyword(lexeme, keyword);
};
if (compact.length === 3) {
if (!isParameter(compact[0]) || !isIndexKeyword(1, 'is') || !isIndexKeyword(2, 'null')) {
return null;
}
return normalizeParameterName(compact[0].value);
}
if (compact.length === 5 && isParameter(compact[0]) && ((_a = compact[1]) === null || _a === void 0 ? void 0 : _a.value) === '::' && isIndexKeyword(3, 'is') && isIndexKeyword(4, 'null')) {
return normalizeParameterName(compact[0].value);
}
if (compact.length >= 6 && isIndexKeyword(0, 'cast') && isParameter(compact[1]) && isIndexKeyword(2, 'as')) {
const isIndex = compact.findIndex((lexeme, index) => index > 2 && isKeyword(lexeme, 'is'));
if (isIndex >= 0 && isIndexKeyword(isIndex + 1, 'null')) {
return normalizeParameterName(compact[1].value);
}
}
return null;
};
const isSupportedMeaningfulBranchFromLexemes = (lexemes, parameterName) => {
try {
const parsed = ValueParser_1.ValueParser.parseFromLexeme(lexemes, 0);
if (parsed.newIndex !== lexemes.length) {
return false;
}
return isSupportedMeaningfulBranch(parsed.value, parameterName);
}
catch {
return false;
}
};
const getRemovalRange = (sql, lexemes, openParenIndex, closeParenIndex) => {
const previous = lexemes[openParenIndex - 1];
const next = lexemes[closeParenIndex + 1];
let start = requiredPosition(lexemes[openParenIndex]).startPosition;
let end = requiredPosition(lexemes[closeParenIndex]).endPosition;
if (previous && isKeyword(previous, 'and')) {
start = requiredPosition(previous).startPosition;
}
else if (next && isKeyword(next, 'and')) {
end = requiredPosition(next).endPosition;
}
else if (previous && isKeyword(previous, 'where')) {
start = requiredPosition(previous).startPosition;
}
return {
start,
end,
text: sql.slice(start, end)
};
};
const isParameter = (lexeme) => (lexeme.type & Lexeme_1.TokenType.Parameter) !== 0;
const isOpenParen = (lexeme) => (lexeme.type & Lexeme_1.TokenType.OpenParen) !== 0;
const isCloseParen = (lexeme) => (lexeme.type & Lexeme_1.TokenType.CloseParen) !== 0;
const isKeyword = (lexeme, keyword) => lexeme.value.toLowerCase() === keyword;
const isWrappingParen = (lexeme) => isOpenParen(lexeme) || isCloseParen(lexeme);
const normalizeParameterName = (value) => {
if (value.startsWith('${') && value.endsWith('}')) {
return value.slice(2, -1);
}
return value.replace(/^[:@$]/, '');
};
const requiredPosition = (lexeme) => {
if (!lexeme.position) {
throw new Error(`Lexeme '${lexeme.value}' is missing source position metadata.`);
}
return lexeme.position;
};
//# sourceMappingURL=PruneOptionalConditionBranches.js.map