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
JavaScript
"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