UNPKG

rawsql-ts

Version:

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

298 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.injectExistsPredicates = injectExistsPredicates; const SelectQuery_1 = require("../models/SelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); const Clause_1 = require("../models/Clause"); const InsertQuery_1 = require("../models/InsertQuery"); const UpdateQuery_1 = require("../models/UpdateQuery"); const DeleteQuery_1 = require("../models/DeleteQuery"); const MergeQuery_1 = require("../models/MergeQuery"); const QueryBuilder_1 = require("./QueryBuilder"); const SelectableColumnCollector_1 = require("./SelectableColumnCollector"); const UpstreamSelectQueryFinder_1 = require("./UpstreamSelectQueryFinder"); const SqlFormatter_1 = require("./SqlFormatter"); const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); /** * Injects EXISTS/NOT EXISTS predicates into the provided SelectQuery. * Each instruction is evaluated independently so failures can be skipped * when `strict` is false. */ function injectExistsPredicates(query, instructions, options = {}) { if (instructions.length === 0) { return query; } const simpleQuery = QueryBuilder_1.QueryBuilder.buildSimpleQuery(query); const resolver = new ColumnReferenceResolver(options.tableColumnResolver); const formatter = new SqlFormatter_1.SqlFormatter(); const strictMode = !!options.strict; for (const instruction of instructions) { try { applyInstruction(simpleQuery, instruction, resolver, formatter); } catch (error) { if (strictMode) { throw error; } } } return simpleQuery; } function applyInstruction(query, instruction, resolver, formatter) { if (instruction.anchorColumns.length === 0) { throw new Error("EXISTS instruction requires at least one anchor column."); } const resolvedColumns = instruction.anchorColumns.map(column => { const columnRef = resolver.resolve(query, column); if (!columnRef) { throw new Error(`Unable to resolve anchor column '${column}'.`); } return columnRef; }); const formattedColumns = resolvedColumns.map(component => formatter.format(component).formattedSql); const placeholderSql = substitutePlaceholders(instruction.sql, formattedColumns); const normalizedSql = placeholderSql.trim(); enforceSqlConstraints(normalizedSql); const subquery = SelectQueryParser_1.SelectQueryParser.parse(normalizedSql); if (instruction.params) { bindSubqueryParameters(subquery, instruction.params); } const existsExpression = new ValueComponent_1.UnaryExpression("exists", new ValueComponent_1.InlineQuery(subquery)); const predicate = instruction.mode === "exists" ? existsExpression : new ValueComponent_1.UnaryExpression("not", existsExpression); query.appendWhere(predicate); } function substitutePlaceholders(sql, formattedColumns) { const usedIndexes = new Set(); const replaced = sql.replace(/\$c(\d+)/g, (_, indexDigits) => { const index = Number(indexDigits); if (!Number.isInteger(index)) { throw new Error(`Invalid placeholder '$c${indexDigits}' in EXISTS SQL.`); } if (index < 0 || index >= formattedColumns.length) { throw new Error(`Placeholder '$c${index}' references a missing anchor column.`); } usedIndexes.add(index); return formattedColumns[index]; }); for (let i = 0; i < formattedColumns.length; i++) { if (!usedIndexes.has(i)) { throw new Error(`Missing placeholder '$c${i}' for anchor column.`); } } return replaced; } function enforceSqlConstraints(sql) { if (!sql) { throw new Error("EXISTS SQL must not be empty."); } if (sql.includes(";")) { throw new Error("EXISTS SQL must not contain semicolons or multiple statements."); } if (/\blateral\b/i.test(sql)) { throw new Error("LATERAL is not supported in column-anchored EXISTS filters."); } } function bindSubqueryParameters(query, params) { for (const [name, value] of Object.entries(params)) { query.setParameter(name, value); } } class ColumnReferenceResolver { constructor(tableColumnResolver) { this.tableColumnResolver = tableColumnResolver; this.finder = new UpstreamSelectQueryFinder_1.UpstreamSelectQueryFinder(this.tableColumnResolver); this.collector = new SelectableColumnCollector_1.SelectableColumnCollector(this.tableColumnResolver, false, SelectableColumnCollector_1.DuplicateDetectionMode.FullName, { upstream: true }); } resolve(query, columnName) { var _a; const parsed = this.parseQualifiedColumnName(columnName); const searchColumn = (_a = parsed === null || parsed === void 0 ? void 0 : parsed.column) !== null && _a !== void 0 ? _a : columnName; const targetTable = parsed === null || parsed === void 0 ? void 0 : parsed.table; const candidateQueries = this.finder.find(query, searchColumn); for (const candidate of candidateQueries) { const columns = this.collectColumns(candidate); const match = this.findMatchingColumn(columns, searchColumn, targetTable, candidate); if (match) { return match.value; } } return null; } collectColumns(query) { const columnEntries = this.collector.collect(query); const cteColumns = this.collectCTEColumns(query); return [...columnEntries, ...cteColumns]; } findMatchingColumn(columns, searchColumn, targetTable, query) { const normalizedSearch = this.normalizeColumnName(searchColumn); for (const entry of columns) { const normalizedEntry = this.normalizeColumnName(entry.name); if (normalizedEntry !== normalizedSearch) continue; if (targetTable) { if (this.matchesTable(entry.value, targetTable, query)) { return entry; } continue; } return entry; } return null; } matchesTable(value, targetTable, query) { if (!(value instanceof ValueComponent_1.ColumnReference)) { return false; } const namespace = value.getNamespace(); if (!namespace) { return false; } const normalizedTarget = this.normalizeString(targetTable); const mapping = this.buildTableMapping(query); const aliasKey = namespace.toLowerCase(); const mappedRealTable = mapping.aliasToRealTable.get(aliasKey); if (mappedRealTable && this.normalizeString(mappedRealTable) === normalizedTarget) { return true; } if (this.normalizeString(namespace) === normalizedTarget) { return true; } const aliasFromTarget = mapping.realTableToAlias.get(normalizedTarget); if (aliasFromTarget && aliasFromTarget.toLowerCase() === aliasKey) { return true; } return false; } collectCTEColumns(query) { const results = []; if (!query.withClause) { return results; } for (const cte of query.withClause.tables) { try { const nestedColumns = this.collectColumnsFromCteQuery(cte.query); results.push(...nestedColumns); } catch { // Skip problematic CTEs to keep resolution best-effort. } } return results; } collectColumnsFromCteQuery(query) { if (!this.isSelectQuery(query)) { return this.collectColumnsFromReturning(query); } return this.collectColumnsFromSelectQuery(query); } collectColumnsFromSelectQuery(query) { if (query instanceof SelectQuery_1.SimpleSelectQuery) { return this.collector.collect(query); } if (query instanceof SelectQuery_1.BinarySelectQuery) { return this.collectColumnsFromSelectQuery(query.left); } return []; } collectColumnsFromReturning(query) { if (query instanceof InsertQuery_1.InsertQuery || query instanceof UpdateQuery_1.UpdateQuery || query instanceof DeleteQuery_1.DeleteQuery || query instanceof MergeQuery_1.MergeQuery) { return this.extractReturningColumns(query.returningClause); } return []; } extractReturningColumns(returningClause) { var _a, _b; if (!returningClause) { return []; } const columns = []; for (const item of returningClause.items) { const columnName = (_b = (_a = item.identifier) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.extractColumnName(item); if (columnName) { columns.push({ name: columnName, value: item.value }); } } return columns; } extractColumnName(item) { if (item.identifier) { return item.identifier.name; } if (item.value instanceof ValueComponent_1.ColumnReference) { return item.value.column.name; } return null; } buildTableMapping(query) { var _a, _b; const aliasToRealMap = new Map(); const realToAliasMap = new Map(); const collectFromClause = (fromClause) => { if (!fromClause) return; this.processSourceForMapping(fromClause.source, aliasToRealMap, realToAliasMap); if (fromClause.joins) { for (const join of fromClause.joins) { this.processSourceForMapping(join.source, aliasToRealMap, realToAliasMap); } } }; collectFromClause((_a = query.fromClause) !== null && _a !== void 0 ? _a : undefined); if (query.withClause) { for (const cte of query.withClause.tables) { const alias = (_b = cte.getSourceAliasName()) === null || _b === void 0 ? void 0 : _b.toLowerCase(); if (alias) { aliasToRealMap.set(alias, alias); realToAliasMap.set(alias, alias); } } } return { aliasToRealTable: aliasToRealMap, realTableToAlias: realToAliasMap }; } processSourceForMapping(source, aliasToReal, realToAlias) { var _a, _b; try { if (source.datasource instanceof Clause_1.TableSource) { const realName = source.datasource.getSourceName(); const aliasName = ((_b = (_a = source.aliasExpression) === null || _a === void 0 ? void 0 : _a.table) === null || _b === void 0 ? void 0 : _b.name) || realName; if (realName && aliasName) { aliasToReal.set(aliasName.toLowerCase(), realName); realToAlias.set(realName.toLowerCase(), aliasName); if (aliasName.toLowerCase() === realName.toLowerCase()) { aliasToReal.set(realName.toLowerCase(), realName); } } } } catch { // Ignore mapping issues while continuing best-effort column resolution. } } parseQualifiedColumnName(columnName) { const parts = columnName.split("."); if (parts.length === 2 && parts[0].trim() && parts[1].trim()) { return { table: parts[0].trim(), column: parts[1].trim() }; } return null; } normalizeColumnName(name) { var _a; const columnPart = name.includes(".") ? (_a = name.split(".").pop()) !== null && _a !== void 0 ? _a : name : name; return this.normalizeString(columnPart); } normalizeString(value) { return value.toLowerCase(); } isSelectQuery(query) { return "__selectQueryType" in query && query.__selectQueryType === "SelectQuery"; } } //# sourceMappingURL=ExistsPredicateInjector.js.map