rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
370 lines • 17.2 kB
JavaScript
import { SqlComponent } from "./SqlComponent";
import { HavingClause, JoinClause, JoinOnClause, SourceExpression, SubQuerySource, SourceAliasExpression, WhereClause, WithClause, CommonTable } from "./Clause";
import { BinaryExpression, ColumnReference } from "./ValueComponent";
import { ValueParser } from "../parsers/ValueParser";
import { CTENormalizer } from "../transformers/CTENormalizer";
import { SelectableColumnCollector } from "../transformers/SelectableColumnCollector";
import { SourceParser } from "../parsers/SourceParser";
import { SelectQueryParser } from "../parsers/SelectQueryParser";
import { Formatter } from "../transformers/Formatter";
import { UpstreamSelectQueryFinder } from "../transformers/UpstreamSelectQueryFinder";
import { QueryBuilder } from "../transformers/QueryBuilder";
import { ParameterHelper } from "../utils/ParameterHelper";
/**
* Represents a simple SELECT query in SQL.
*/
export class SimpleSelectQuery extends SqlComponent {
constructor(params) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
super();
this.withClause = (_a = params.withClause) !== null && _a !== void 0 ? _a : null;
this.selectClause = params.selectClause;
this.fromClause = (_b = params.fromClause) !== null && _b !== void 0 ? _b : null;
this.whereClause = (_c = params.whereClause) !== null && _c !== void 0 ? _c : null;
this.groupByClause = (_d = params.groupByClause) !== null && _d !== void 0 ? _d : null;
this.havingClause = (_e = params.havingClause) !== null && _e !== void 0 ? _e : null;
this.orderByClause = (_f = params.orderByClause) !== null && _f !== void 0 ? _f : null;
this.windowClause = (_g = params.windowClause) !== null && _g !== void 0 ? _g : null;
this.limitClause = (_h = params.limitClause) !== null && _h !== void 0 ? _h : null;
this.offsetClause = (_j = params.offsetClause) !== null && _j !== void 0 ? _j : null;
this.fetchClause = (_k = params.fetchClause) !== null && _k !== void 0 ? _k : null;
this.forClause = (_l = params.forClause) !== null && _l !== void 0 ? _l : null;
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using UNION as the operator.
*
* @param rightQuery The right side of the UNION
* @returns A new BinarySelectQuery representing "this UNION rightQuery"
*/
toUnion(rightQuery) {
return this.toBinaryQuery('union', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using UNION ALL as the operator.
*
* @param rightQuery The right side of the UNION ALL
* @returns A new BinarySelectQuery representing "this UNION ALL rightQuery"
*/
toUnionAll(rightQuery) {
return this.toBinaryQuery('union all', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using INTERSECT as the operator.
*
* @param rightQuery The right side of the INTERSECT
* @returns A new BinarySelectQuery representing "this INTERSECT rightQuery"
*/
toIntersect(rightQuery) {
return this.toBinaryQuery('intersect', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using INTERSECT ALL as the operator.
*
* @param rightQuery The right side of the INTERSECT ALL
* @returns A new BinarySelectQuery representing "this INTERSECT ALL rightQuery"
*/
toIntersectAll(rightQuery) {
return this.toBinaryQuery('intersect all', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using EXCEPT as the operator.
*
* @param rightQuery The right side of the EXCEPT
* @returns A new BinarySelectQuery representing "this EXCEPT rightQuery"
*/
toExcept(rightQuery) {
return this.toBinaryQuery('except', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using EXCEPT ALL as the operator.
*
* @param rightQuery The right side of the EXCEPT ALL
* @returns A new BinarySelectQuery representing "this EXCEPT ALL rightQuery"
*/
toExceptAll(rightQuery) {
return this.toBinaryQuery('except all', rightQuery);
}
/**
* Creates a new BinarySelectQuery with this query as the left side and the provided query as the right side,
* using the specified operator.
*
* @param operator SQL operator to use (e.g. 'union', 'union all', 'intersect', 'except')
* @param rightQuery The right side of the binary operation
* @returns A new BinarySelectQuery representing "this [operator] rightQuery"
*/
toBinaryQuery(operator, rightQuery) {
return QueryBuilder.buildBinaryQuery([this, rightQuery], operator);
}
/**
* Appends a new condition to the query's WHERE clause using AND logic.
* The condition is provided as a raw SQL string which is parsed internally.
*
* @param rawCondition Raw SQL string representing the condition (e.g. "status = 'active'")
*/
appendWhereRaw(rawCondition) {
const parsedCondition = ValueParser.parse(rawCondition);
this.appendWhere(parsedCondition);
}
/**
* Appends a new condition to the query's WHERE clause using AND logic.
* The condition is provided as a ValueComponent object.
*
* @param condition ValueComponent representing the condition
*/
appendWhere(condition) {
if (!this.whereClause) {
this.whereClause = new WhereClause(condition);
}
else {
this.whereClause.condition = new BinaryExpression(this.whereClause.condition, 'and', condition);
}
}
/**
* Appends a new condition to the query's HAVING clause using AND logic.
* The condition is provided as a raw SQL string which is parsed internally.
*
* @param rawCondition Raw SQL string representing the condition (e.g. "count(*) > 5")
*/
appendHavingRaw(rawCondition) {
const parsedCondition = ValueParser.parse(rawCondition);
this.appendHaving(parsedCondition);
}
/**
* Appends a new condition to the query's HAVING clause using AND logic.
* The condition is provided as a ValueComponent object.
*
* @param condition ValueComponent representing the condition
*/
appendHaving(condition) {
if (!this.havingClause) {
this.havingClause = new HavingClause(condition);
}
else {
this.havingClause.condition = new BinaryExpression(this.havingClause.condition, 'and', condition);
}
}
/**
* Appends an INNER JOIN clause to the query.
* @param joinSourceRawText The table source text to join (e.g., "my_table", "schema.my_table")
* @param alias The alias for the joined table
* @param columns The columns to use for the join condition (e.g. ["user_id"] or "user_id")
*/
innerJoinRaw(joinSourceRawText, alias, columns, resolver = null) {
this.joinSourceRaw('inner join', joinSourceRawText, alias, columns, resolver);
}
/**
* Appends a LEFT JOIN clause to the query.
* @param joinSourceRawText The table source text to join
* @param alias The alias for the joined table
* @param columns The columns to use for the join condition
*/
leftJoinRaw(joinSourceRawText, alias, columns, resolver = null) {
this.joinSourceRaw('left join', joinSourceRawText, alias, columns, resolver);
}
/**
* Appends a RIGHT JOIN clause to the query.
* @param joinSourceRawText The table source text to join
* @param alias The alias for the joined table
* @param columns The columns to use for the join condition
*/
rightJoinRaw(joinSourceRawText, alias, columns, resolver = null) {
this.joinSourceRaw('right join', joinSourceRawText, alias, columns, resolver);
}
/**
* Appends an INNER JOIN clause to the query using a SourceExpression.
* @param sourceExpr The source expression to join
* @param columns The columns to use for the join condition
*/
innerJoin(sourceExpr, columns, resolver = null) {
this.joinSource('inner join', sourceExpr, columns, resolver);
}
/**
* Appends a LEFT JOIN clause to the query using a SourceExpression.
* @param sourceExpr The source expression to join
* @param columns The columns to use for the join condition
*/
leftJoin(sourceExpr, columns, resolver = null) {
this.joinSource('left join', sourceExpr, columns, resolver);
}
/**
* Appends a RIGHT JOIN clause to the query using a SourceExpression.
* @param sourceExpr The source expression to join
* @param columns The columns to use for the join condition
*/
rightJoin(sourceExpr, columns, resolver = null) {
this.joinSource('right join', sourceExpr, columns, resolver);
}
/**
* Internal helper to append a JOIN clause.
* Parses the table source, finds the corresponding columns in the existing query context,
* and builds the JOIN condition.
* @param joinType Type of join (e.g., 'inner join', 'left join')
* @param joinSourceRawText Raw text for the table/source to join (e.g., "my_table", "schema.another_table")
* @param alias Alias for the table/source being joined
* @param columns Array or string of column names to join on
*/
joinSourceRaw(joinType, joinSourceRawText, alias, columns, resolver = null) {
const tableSource = SourceParser.parse(joinSourceRawText);
const sourceExpr = new SourceExpression(tableSource, new SourceAliasExpression(alias, null));
this.joinSource(joinType, sourceExpr, columns, resolver);
}
/**
* Internal helper to append a JOIN clause using a SourceExpression.
* @param joinType Type of join (e.g., 'inner join', 'left join')
* @param sourceExpr The source expression to join
* @param columns Array or string of column names to join on
*/
joinSource(joinType, sourceExpr, columns, resolver = null) {
if (!this.fromClause) {
throw new Error('A FROM clause is required to add a JOIN condition.');
}
// Always treat columns as array
const columnsArr = Array.isArray(columns) ? columns : [columns];
const collector = new SelectableColumnCollector(resolver);
const valueSets = collector.collect(this);
let joinCondition = null;
let count = 0;
const sourceAlias = sourceExpr.getAliasName();
if (!sourceAlias) {
throw new Error('An alias is required for the source expression to add a JOIN condition.');
}
for (const valueSet of valueSets) {
if (columnsArr.some(col => col == valueSet.name)) {
const expr = new BinaryExpression(valueSet.value, '=', new ColumnReference([sourceAlias], valueSet.name));
if (joinCondition) {
joinCondition = new BinaryExpression(joinCondition, 'and', expr);
}
else {
joinCondition = expr;
}
count++;
}
}
if (!joinCondition || count !== columnsArr.length) {
throw new Error(`Invalid JOIN condition. The specified columns were not found: ${columnsArr.join(', ')}`);
}
const joinOnClause = new JoinOnClause(joinCondition);
const joinClause = new JoinClause(joinType, sourceExpr, joinOnClause, false);
if (this.fromClause) {
if (this.fromClause.joins) {
this.fromClause.joins.push(joinClause);
}
else {
this.fromClause.joins = [joinClause];
}
}
CTENormalizer.normalize(this);
}
// Returns a SourceExpression wrapping this query as a subquery source.
// Alias is required for correct SQL generation and join logic.
toSource(alias) {
if (!alias || alias.trim() === "") {
throw new Error("Alias is required for toSource(). Please specify a non-empty alias name.");
}
return new SourceExpression(new SubQuerySource(this), new SourceAliasExpression(alias, null));
}
appendWith(commonTable) {
// Always treat as array for simplicity
const tables = Array.isArray(commonTable) ? commonTable : [commonTable];
if (!this.withClause) {
this.withClause = new WithClause(false, tables);
}
else {
this.withClause.tables.push(...tables);
}
CTENormalizer.normalize(this);
}
/**
* Appends a CommonTable (CTE) to the WITH clause from raw SQL text and alias.
* If alias is provided, it will be used as the CTE name.
*
* @param rawText Raw SQL string representing the CTE body (e.g. '(SELECT ...)')
* @param alias Optional alias for the CTE (e.g. 'cte_name')
*/
appendWithRaw(rawText, alias) {
const query = SelectQueryParser.parse(rawText);
const commonTable = new CommonTable(query, alias, null);
this.appendWith(commonTable);
}
/**
* Overrides a select item using a template literal function.
* The callback receives the SQL string of the original expression and must return a new SQL string.
* The result is parsed and set as the new select item value.
*
* Example usage:
* query.overrideSelectItemRaw("journal_date", expr => `greatest(${expr}, DATE '2025-01-01')`)
*
* @param columnName The name of the column to override
* @param fn Callback that receives the SQL string of the original expression and returns a new SQL string
*/
overrideSelectItemExpr(columnName, fn) {
const items = this.selectClause.items.filter(item => { var _a; return ((_a = item.identifier) === null || _a === void 0 ? void 0 : _a.name) === columnName; });
if (items.length === 0) {
throw new Error(`Column ${columnName} not found in the query`);
}
if (items.length > 1) {
throw new Error(`Duplicate column name ${columnName} found in the query`);
}
const item = items[0];
const formatter = new Formatter();
const exprSql = formatter.visit(item.value);
const newValue = fn(exprSql);
item.value = ValueParser.parse(newValue);
}
/**
* Appends a WHERE clause using the expression for the specified column.
* If `options.upstream` is true, applies to all upstream queries containing the column.
* If false or omitted, applies only to the current query.
*
* @param columnName The name of the column to target.
* @param exprBuilder Function that receives the column expression as a string and returns the WHERE condition string.
* @param options Optional settings. If `upstream` is true, applies to upstream queries.
*/
appendWhereExpr(columnName, exprBuilder, options) {
// If upstream option is true, find all upstream queries containing the column
if (options && options.upstream) {
// Use UpstreamSelectQueryFinder to find all relevant queries
// (Assume UpstreamSelectQueryFinder is imported)
const finder = new UpstreamSelectQueryFinder();
const queries = finder.find(this, [columnName]);
const collector = new SelectableColumnCollector();
const formatter = new Formatter();
for (const q of queries) {
const exprs = collector.collect(q).filter(item => item.name === columnName).map(item => item.value);
if (exprs.length !== 1) {
throw new Error(`Expected exactly one expression for column '${columnName}'`);
}
const exprStr = formatter.format(exprs[0]);
q.appendWhereRaw(exprBuilder(exprStr));
}
}
else {
// Only apply to the current query
const collector = new SelectableColumnCollector();
const formatter = new Formatter();
const exprs = collector.collect(this).filter(item => item.name === columnName).map(item => item.value);
if (exprs.length !== 1) {
throw new Error(`Expected exactly one expression for column '${columnName}'`);
}
const exprStr = formatter.format(exprs[0]);
this.appendWhereRaw(exprBuilder(exprStr));
}
}
/**
* Sets the value of a parameter by name in this query.
* @param name Parameter name
* @param value Value to set
*/
setParameter(name, value) {
ParameterHelper.set(this, name, value);
return this;
}
}
SimpleSelectQuery.kind = Symbol("SelectQuery");
//# sourceMappingURL=SimpleSelectQuery.js.map