UNPKG

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
"use strict"; 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