rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
510 lines • 28 kB
JavaScript
import { SimpleSelectQuery } from "../models/SelectQuery";
import { BinarySelectQuery } from "../models/BinarySelectQuery";
import { SelectableColumnCollector } from "./SelectableColumnCollector";
import { BinaryExpression, FunctionCall, ParameterExpression, ParenExpression, ValueList } from "../models/ValueComponent";
import { UpstreamSelectQueryFinder } from "./UpstreamSelectQueryFinder";
import { SelectQueryParser } from "../parsers/SelectQueryParser";
/**
* SqlParamInjector injects state parameters into a SelectQuery model,
* creating WHERE conditions and setting parameter values.
*/
export class SqlParamInjector {
constructor(optionsOrResolver, options) {
// Type-check to decide which argument was provided
if (typeof optionsOrResolver === 'function') {
this.tableColumnResolver = optionsOrResolver;
this.options = options || {};
}
else {
this.tableColumnResolver = undefined;
this.options = optionsOrResolver || {};
}
}
/**
* Injects parameters as WHERE conditions into the given query model.
* @param query The SelectQuery to modify
* @param state A record of parameter names and values
* @returns The modified SelectQuery
* @throws Error when all parameters are undefined and allowAllUndefined is not set to true
*/
inject(query, state) {
// Convert string query to SimpleSelectQuery using SelectQueryParser if needed
if (typeof query === 'string') {
query = SelectQueryParser.parse(query);
}
// Pass tableColumnResolver to finder and collector
const finder = new UpstreamSelectQueryFinder(this.tableColumnResolver, this.options);
const collector = new SelectableColumnCollector(this.tableColumnResolver);
// Normalization is handled locally below.
const normalize = (s) => this.options.ignoreCaseAndUnderscore ? s.toLowerCase().replace(/_/g, '') : s;
const allowedOps = ['min', 'max', 'like', 'ilike', 'in', 'any', '=', '<', '>', '!=', '<>', '<=', '>=', 'or', 'and', 'column'];
// Check if all parameters are undefined
const stateValues = Object.values(state);
const hasParameters = stateValues.length > 0;
const allUndefined = hasParameters && stateValues.every(value => value === undefined);
if (allUndefined && !this.options.allowAllUndefined) {
throw new Error('All parameters are undefined. This would result in fetching all records. Use allowAllUndefined: true option to explicitly allow this behavior.');
}
for (const [name, stateValue] of Object.entries(state)) {
// skip undefined values
if (stateValue === undefined)
continue;
this.processStateParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators);
}
function injectAndConditions(q, baseName, andConditions, normalize, availableColumns, collector) {
// For AND conditions, we process each condition and add them all with AND logic
for (let i = 0; i < andConditions.length; i++) {
const andCondition = andConditions[i];
const columnName = andCondition.column || baseName;
// Find the target column
const entry = availableColumns.find(item => normalize(item.name) === normalize(columnName));
if (!entry) {
throw new Error(`Column '${columnName}' not found in query for AND condition`);
}
const columnRef = entry.value;
// Process each operator in the AND condition
if ('=' in andCondition && andCondition['='] !== undefined) {
const paramName = `${baseName}_and_${i}_eq`;
const paramExpr = new ParameterExpression(paramName, andCondition['=']);
q.appendWhere(new BinaryExpression(columnRef, "=", paramExpr));
}
if ('min' in andCondition && andCondition.min !== undefined) {
const paramName = `${baseName}_and_${i}_min`;
const paramExpr = new ParameterExpression(paramName, andCondition.min);
q.appendWhere(new BinaryExpression(columnRef, ">=", paramExpr));
}
if ('max' in andCondition && andCondition.max !== undefined) {
const paramName = `${baseName}_and_${i}_max`;
const paramExpr = new ParameterExpression(paramName, andCondition.max);
q.appendWhere(new BinaryExpression(columnRef, "<=", paramExpr));
}
if ('like' in andCondition && andCondition.like !== undefined) {
const paramName = `${baseName}_and_${i}_like`;
const paramExpr = new ParameterExpression(paramName, andCondition.like);
q.appendWhere(new BinaryExpression(columnRef, "like", paramExpr));
}
if ('ilike' in andCondition && andCondition.ilike !== undefined) {
const paramName = `${baseName}_and_${i}_ilike`;
const paramExpr = new ParameterExpression(paramName, andCondition.ilike);
q.appendWhere(new BinaryExpression(columnRef, "ilike", paramExpr));
}
if ('in' in andCondition && andCondition.in !== undefined) {
const arr = andCondition.in;
const prms = arr.map((v, j) => new ParameterExpression(`${baseName}_and_${i}_in_${j}`, v));
q.appendWhere(new BinaryExpression(columnRef, "in", new ParenExpression(new ValueList(prms))));
}
if ('any' in andCondition && andCondition.any !== undefined) {
const paramName = `${baseName}_and_${i}_any`;
const paramExpr = new ParameterExpression(paramName, andCondition.any);
q.appendWhere(new BinaryExpression(columnRef, "=", new FunctionCall(null, "any", paramExpr, null)));
}
if ('<' in andCondition && andCondition['<'] !== undefined) {
const paramName = `${baseName}_and_${i}_lt`;
const paramExpr = new ParameterExpression(paramName, andCondition['<']);
q.appendWhere(new BinaryExpression(columnRef, "<", paramExpr));
}
if ('>' in andCondition && andCondition['>'] !== undefined) {
const paramName = `${baseName}_and_${i}_gt`;
const paramExpr = new ParameterExpression(paramName, andCondition['>']);
q.appendWhere(new BinaryExpression(columnRef, ">", paramExpr));
}
if ('!=' in andCondition && andCondition['!='] !== undefined) {
const paramName = `${baseName}_and_${i}_neq`;
const paramExpr = new ParameterExpression(paramName, andCondition['!=']);
q.appendWhere(new BinaryExpression(columnRef, "!=", paramExpr));
}
if ('<>' in andCondition && andCondition['<>'] !== undefined) {
const paramName = `${baseName}_and_${i}_ne`;
const paramExpr = new ParameterExpression(paramName, andCondition['<>']);
q.appendWhere(new BinaryExpression(columnRef, "<>", paramExpr));
}
if ('<=' in andCondition && andCondition['<='] !== undefined) {
const paramName = `${baseName}_and_${i}_le`;
const paramExpr = new ParameterExpression(paramName, andCondition['<=']);
q.appendWhere(new BinaryExpression(columnRef, "<=", paramExpr));
}
if ('>=' in andCondition && andCondition['>='] !== undefined) {
const paramName = `${baseName}_and_${i}_ge`;
const paramExpr = new ParameterExpression(paramName, andCondition['>=']);
q.appendWhere(new BinaryExpression(columnRef, ">=", paramExpr));
}
}
}
function injectOrConditions(q, baseName, orConditions, normalize, availableColumns, collector) {
const orExpressions = [];
for (let i = 0; i < orConditions.length; i++) {
const orCondition = orConditions[i];
const columnName = orCondition.column || baseName;
// Find the target column
const entry = availableColumns.find(item => normalize(item.name) === normalize(columnName));
if (!entry) {
throw new Error(`Column '${columnName}' not found in query for OR condition`);
}
const columnRef = entry.value;
// Create conditions for this OR branch
const branchConditions = [];
// Process each operator in the OR condition
if ('=' in orCondition && orCondition['='] !== undefined) {
const paramName = `${baseName}_or_${i}_eq`;
const paramExpr = new ParameterExpression(paramName, orCondition['=']);
branchConditions.push(new BinaryExpression(columnRef, "=", paramExpr));
}
if ('min' in orCondition && orCondition.min !== undefined) {
const paramName = `${baseName}_or_${i}_min`;
const paramExpr = new ParameterExpression(paramName, orCondition.min);
branchConditions.push(new BinaryExpression(columnRef, ">=", paramExpr));
}
if ('max' in orCondition && orCondition.max !== undefined) {
const paramName = `${baseName}_or_${i}_max`;
const paramExpr = new ParameterExpression(paramName, orCondition.max);
branchConditions.push(new BinaryExpression(columnRef, "<=", paramExpr));
}
if ('like' in orCondition && orCondition.like !== undefined) {
const paramName = `${baseName}_or_${i}_like`;
const paramExpr = new ParameterExpression(paramName, orCondition.like);
branchConditions.push(new BinaryExpression(columnRef, "like", paramExpr));
}
if ('ilike' in orCondition && orCondition.ilike !== undefined) {
const paramName = `${baseName}_or_${i}_ilike`;
const paramExpr = new ParameterExpression(paramName, orCondition.ilike);
branchConditions.push(new BinaryExpression(columnRef, "ilike", paramExpr));
}
if ('in' in orCondition && orCondition.in !== undefined) {
const arr = orCondition.in;
const prms = arr.map((v, j) => new ParameterExpression(`${baseName}_or_${i}_in_${j}`, v));
branchConditions.push(new BinaryExpression(columnRef, "in", new ParenExpression(new ValueList(prms))));
}
if ('any' in orCondition && orCondition.any !== undefined) {
const paramName = `${baseName}_or_${i}_any`;
const paramExpr = new ParameterExpression(paramName, orCondition.any);
branchConditions.push(new BinaryExpression(columnRef, "=", new FunctionCall(null, "any", paramExpr, null)));
}
if ('<' in orCondition && orCondition['<'] !== undefined) {
const paramName = `${baseName}_or_${i}_lt`;
const paramExpr = new ParameterExpression(paramName, orCondition['<']);
branchConditions.push(new BinaryExpression(columnRef, "<", paramExpr));
}
if ('>' in orCondition && orCondition['>'] !== undefined) {
const paramName = `${baseName}_or_${i}_gt`;
const paramExpr = new ParameterExpression(paramName, orCondition['>']);
branchConditions.push(new BinaryExpression(columnRef, ">", paramExpr));
}
if ('!=' in orCondition && orCondition['!='] !== undefined) {
const paramName = `${baseName}_or_${i}_neq`;
const paramExpr = new ParameterExpression(paramName, orCondition['!=']);
branchConditions.push(new BinaryExpression(columnRef, "!=", paramExpr));
}
if ('<>' in orCondition && orCondition['<>'] !== undefined) {
const paramName = `${baseName}_or_${i}_ne`;
const paramExpr = new ParameterExpression(paramName, orCondition['<>']);
branchConditions.push(new BinaryExpression(columnRef, "<>", paramExpr));
}
if ('<=' in orCondition && orCondition['<='] !== undefined) {
const paramName = `${baseName}_or_${i}_le`;
const paramExpr = new ParameterExpression(paramName, orCondition['<=']);
branchConditions.push(new BinaryExpression(columnRef, "<=", paramExpr));
}
if ('>=' in orCondition && orCondition['>='] !== undefined) {
const paramName = `${baseName}_or_${i}_ge`;
const paramExpr = new ParameterExpression(paramName, orCondition['>=']);
branchConditions.push(new BinaryExpression(columnRef, ">=", paramExpr));
}
// Combine branch conditions with AND if there are multiple
if (branchConditions.length > 0) {
let branchExpr = branchConditions[0];
for (let j = 1; j < branchConditions.length; j++) {
branchExpr = new BinaryExpression(branchExpr, "and", branchConditions[j]);
}
// Wrap in parentheses if multiple conditions within the OR branch
if (branchConditions.length > 1) {
orExpressions.push(new ParenExpression(branchExpr));
}
else {
orExpressions.push(branchExpr);
}
}
}
// Combine OR expressions
if (orExpressions.length > 0) {
let finalOrExpr = orExpressions[0];
for (let i = 1; i < orExpressions.length; i++) {
finalOrExpr = new BinaryExpression(finalOrExpr, "or", orExpressions[i]);
}
// Wrap in parentheses and append to WHERE clause
q.appendWhere(new ParenExpression(finalOrExpr));
}
}
function validateOperators(stateValue, allowedOps, name) {
Object.keys(stateValue).forEach(op => {
if (!allowedOps.includes(op)) {
throw new Error(`Unsupported operator '${op}' for state key '${name}'`);
}
});
}
function injectSimpleCondition(q, columnRef, name, stateValue) {
const paramExpr = new ParameterExpression(name, stateValue);
q.appendWhere(new BinaryExpression(columnRef, "=", paramExpr));
}
function injectComplexConditions(q, columnRef, name, stateValue) {
const conditions = [];
if ('=' in stateValue) {
const paramEq = new ParameterExpression(name, stateValue['=']);
conditions.push(new BinaryExpression(columnRef, "=", paramEq));
}
if ('min' in stateValue) {
const paramMin = new ParameterExpression(name + "_min", stateValue.min);
conditions.push(new BinaryExpression(columnRef, ">=", paramMin));
}
if ('max' in stateValue) {
const paramMax = new ParameterExpression(name + "_max", stateValue.max);
conditions.push(new BinaryExpression(columnRef, "<=", paramMax));
}
if ('like' in stateValue) {
const paramLike = new ParameterExpression(name + "_like", stateValue.like);
conditions.push(new BinaryExpression(columnRef, "like", paramLike));
}
if ('ilike' in stateValue) {
const paramIlike = new ParameterExpression(name + "_ilike", stateValue.ilike);
conditions.push(new BinaryExpression(columnRef, "ilike", paramIlike));
}
if ('in' in stateValue) {
const arr = stateValue['in'];
const prms = arr.map((v, i) => new ParameterExpression(`${name}_in_${i}`, v));
conditions.push(new BinaryExpression(columnRef, "in", new ParenExpression(new ValueList(prms))));
}
if ('any' in stateValue) {
const paramAny = new ParameterExpression(name + "_any", stateValue.any);
conditions.push(new BinaryExpression(columnRef, "=", new FunctionCall(null, "any", paramAny, null)));
}
if ('<' in stateValue) {
const paramLT = new ParameterExpression(name + "_lt", stateValue['<']);
conditions.push(new BinaryExpression(columnRef, "<", paramLT));
}
if ('>' in stateValue) {
const paramGT = new ParameterExpression(name + "_gt", stateValue['>']);
conditions.push(new BinaryExpression(columnRef, ">", paramGT));
}
if ('!=' in stateValue) {
const paramNEQ = new ParameterExpression(name + "_neq", stateValue['!=']);
conditions.push(new BinaryExpression(columnRef, "!=", paramNEQ));
}
if ('<>' in stateValue) {
const paramNE = new ParameterExpression(name + "_ne", stateValue['<>']);
conditions.push(new BinaryExpression(columnRef, "<>", paramNE));
}
if ('<=' in stateValue) {
const paramLE = new ParameterExpression(name + "_le", stateValue['<=']);
conditions.push(new BinaryExpression(columnRef, "<=", paramLE));
}
if ('>=' in stateValue) {
const paramGE = new ParameterExpression(name + "_ge", stateValue['>=']);
conditions.push(new BinaryExpression(columnRef, ">=", paramGE));
}
// Combine conditions with AND and wrap in parentheses if multiple conditions for clarity
if (conditions.length === 1) {
// Single condition - no parentheses needed
q.appendWhere(conditions[0]);
}
else if (conditions.length > 1) {
// Multiple conditions - combine with AND and wrap in parentheses for clarity
let combinedExpr = conditions[0];
for (let i = 1; i < conditions.length; i++) {
combinedExpr = new BinaryExpression(combinedExpr, "and", conditions[i]);
}
q.appendWhere(new ParenExpression(combinedExpr));
}
}
return query;
}
/**
* Type guard for OR conditions
*/
isOrCondition(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) && 'or' in value;
}
/**
* Type guard for AND conditions
*/
isAndCondition(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) && 'and' in value;
}
/**
* Type guard for explicit column mapping without OR
*/
isExplicitColumnMapping(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) &&
'column' in value && !('or' in value);
}
/**
* Type guard for objects that need operator validation
*/
isValidatableObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype;
}
/**
* Type guard for column mapping presence
*/
hasColumnMapping(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value) && 'column' in value;
}
/**
* Type guard for simple values (non-object conditions)
*/
isSimpleValue(value) {
return value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date;
}
/**
* Processes a single state parameter
*/
processStateParameter(name, stateValue, query, finder, collector, normalize, allowedOps, injectOrConditions, injectAndConditions, injectSimpleCondition, injectComplexConditions, validateOperators) {
// Handle OR conditions specially - they don't need the main column to exist
if (this.isOrCondition(stateValue)) {
const orConditions = stateValue.or;
if (orConditions && orConditions.length > 0) {
const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, orConditions);
const allColumns = this.getAllAvailableColumns(targetQuery, collector);
injectOrConditions(targetQuery, name, orConditions, normalize, allColumns, collector);
return;
}
}
// Handle AND conditions specially - they don't need the main column to exist
if (this.isAndCondition(stateValue)) {
const andConditions = stateValue.and;
if (andConditions && andConditions.length > 0) {
const targetQuery = this.findTargetQueryForLogicalCondition(finder, query, name, andConditions);
const allColumns = this.getAllAvailableColumns(targetQuery, collector);
injectAndConditions(targetQuery, name, andConditions, normalize, allColumns, collector);
return;
}
}
// Handle explicit column mapping without OR
if (this.isExplicitColumnMapping(stateValue)) {
const explicitColumnName = stateValue.column;
if (explicitColumnName) {
const queries = finder.find(query, explicitColumnName);
if (queries.length === 0) {
throw new Error(`Explicit column '${explicitColumnName}' not found in query`);
}
for (const q of queries) {
const allColumns = this.getAllAvailableColumns(q, collector);
const entry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName));
if (!entry) {
throw new Error(`Explicit column '${explicitColumnName}' not found in query`);
}
// if object, validate its keys
if (this.isValidatableObject(stateValue)) {
validateOperators(stateValue, allowedOps, name);
}
injectComplexConditions(q, entry.value, name, stateValue);
}
return;
}
}
// Handle regular column conditions
this.processRegularColumnCondition(name, stateValue, query, finder, collector, normalize, allowedOps, injectSimpleCondition, injectComplexConditions, validateOperators);
}
/**
* Processes regular column conditions (non-logical, non-explicit)
*/
processRegularColumnCondition(name, stateValue, query, finder, collector, normalize, allowedOps, injectSimpleCondition, injectComplexConditions, validateOperators) {
const queries = finder.find(query, name);
if (queries.length === 0) {
// Ignore non-existent columns if option is enabled
if (this.options.ignoreNonExistentColumns) {
return;
}
throw new Error(`Column '${name}' not found in query`);
}
for (const q of queries) {
const allColumns = this.getAllAvailableColumns(q, collector);
const entry = allColumns.find(item => normalize(item.name) === normalize(name));
if (!entry) {
throw new Error(`Column '${name}' not found in query`);
}
const columnRef = entry.value;
// if object, validate its keys
if (this.isValidatableObject(stateValue)) {
validateOperators(stateValue, allowedOps, name);
}
// Handle explicit column mapping
let targetColumn = columnRef;
let targetColumnName = name;
if (this.hasColumnMapping(stateValue)) {
const explicitColumnName = stateValue.column;
if (explicitColumnName) {
const explicitEntry = allColumns.find(item => normalize(item.name) === normalize(explicitColumnName));
if (explicitEntry) {
targetColumn = explicitEntry.value;
targetColumnName = explicitColumnName;
}
}
}
if (this.isSimpleValue(stateValue)) {
injectSimpleCondition(q, targetColumn, targetColumnName, stateValue);
}
else {
injectComplexConditions(q, targetColumn, targetColumnName, stateValue);
}
}
}
/**
* Finds target query for logical conditions (AND/OR)
*/
findTargetQueryForLogicalCondition(finder, query, baseName, conditions) {
const referencedColumns = conditions
.map(cond => cond.column || baseName)
.filter((col, index, arr) => arr.indexOf(col) === index); // unique columns
for (const colName of referencedColumns) {
const queries = finder.find(query, colName);
if (queries.length > 0) {
return queries[0];
}
}
const conditionType = conditions === conditions.or ? 'OR' : 'AND';
throw new Error(`None of the ${conditionType} condition columns [${referencedColumns.join(', ')}] found in query`);
}
/**
* Collects all available columns from a query including CTE columns
*/
getAllAvailableColumns(query, collector) {
const columns = collector.collect(query);
const cteColumns = this.collectCTEColumns(query);
return [...columns, ...cteColumns];
}
/**
* Collects column names and references from CTE definitions
*/
collectCTEColumns(query) {
const cteColumns = [];
if (query.withClause) {
for (const cte of query.withClause.tables) {
try {
const columns = this.collectColumnsFromSelectQuery(cte.query);
cteColumns.push(...columns);
}
catch (error) {
// Log error but continue processing other CTEs
console.warn(`Failed to collect columns from CTE '${cte.getSourceAliasName()}':`, error);
}
}
}
return cteColumns;
}
/**
* Recursively collects columns from any SelectQuery type
*/
collectColumnsFromSelectQuery(query) {
if (query instanceof SimpleSelectQuery) {
const collector = new SelectableColumnCollector(this.tableColumnResolver);
return collector.collect(query);
}
else if (query instanceof BinarySelectQuery) {
// For UNION/INTERSECT/EXCEPT, columns from left side are representative
// since both sides must have matching column structure
return this.collectColumnsFromSelectQuery(query.left);
}
return [];
}
}
//# sourceMappingURL=SqlParamInjector.js.map