rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
295 lines • 11.9 kB
JavaScript
import { BinarySelectQuery, SimpleSelectQuery } from "../models/SelectQuery";
import { ColumnReference, InlineQuery, UnaryExpression } from "../models/ValueComponent";
import { TableSource } from "../models/Clause";
import { InsertQuery } from "../models/InsertQuery";
import { UpdateQuery } from "../models/UpdateQuery";
import { DeleteQuery } from "../models/DeleteQuery";
import { MergeQuery } from "../models/MergeQuery";
import { QueryBuilder } from "./QueryBuilder";
import { SelectableColumnCollector, DuplicateDetectionMode } from "./SelectableColumnCollector";
import { UpstreamSelectQueryFinder } from "./UpstreamSelectQueryFinder";
import { SqlFormatter } from "./SqlFormatter";
import { SelectQueryParser } from "../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.
*/
export function injectExistsPredicates(query, instructions, options = {}) {
if (instructions.length === 0) {
return query;
}
const simpleQuery = QueryBuilder.buildSimpleQuery(query);
const resolver = new ColumnReferenceResolver(options.tableColumnResolver);
const formatter = new 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.parse(normalizedSql);
if (instruction.params) {
bindSubqueryParameters(subquery, instruction.params);
}
const existsExpression = new UnaryExpression("exists", new InlineQuery(subquery));
const predicate = instruction.mode === "exists"
? existsExpression
: new 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(this.tableColumnResolver);
this.collector = new SelectableColumnCollector(this.tableColumnResolver, false, 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 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 (_a) {
// 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 SimpleSelectQuery) {
return this.collector.collect(query);
}
if (query instanceof BinarySelectQuery) {
return this.collectColumnsFromSelectQuery(query.left);
}
return [];
}
collectColumnsFromReturning(query) {
if (query instanceof InsertQuery || query instanceof UpdateQuery || query instanceof DeleteQuery || query instanceof 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 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 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 (_c) {
// 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