rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
633 lines • 32.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryBuilder = void 0;
const Clause_1 = require("../models/Clause");
const UpdateQuery_1 = require("../models/UpdateQuery");
const DeleteQuery_1 = require("../models/DeleteQuery");
const MergeQuery_1 = require("../models/MergeQuery");
const ValueComponent_1 = require("../models/ValueComponent");
const SelectValueCollector_1 = require("./SelectValueCollector");
const SelectQuery_1 = require("../models/SelectQuery");
const CTECollector_1 = require("./CTECollector");
const CTENormalizer_1 = require("./CTENormalizer");
const CreateTableQuery_1 = require("../models/CreateTableQuery");
const InsertQuery_1 = require("../models/InsertQuery");
const CTEDisabler_1 = require("./CTEDisabler");
const SourceExpressionParser_1 = require("../parsers/SourceExpressionParser");
const InsertQuerySelectValuesConverter_1 = require("./InsertQuerySelectValuesConverter");
/**
* QueryBuilder provides static methods to build or convert various SQL query objects.
*/
class QueryBuilder {
/**
* Builds a BinarySelectQuery by combining an array of SelectQuery using the specified operator.
* Throws if less than two queries are provided.
* @param queries Array of SelectQuery to combine
* @param operator SQL operator to use (e.g. 'union', 'union all', 'intersect', 'except')
* @returns BinarySelectQuery
*/
static buildBinaryQuery(queries, operator) {
if (!queries || queries.length === 0) {
throw new Error("No queries provided to combine.");
}
if (queries.length === 1) {
throw new Error("At least two queries are required to create a BinarySelectQuery.");
}
// Always create a new BinarySelectQuery instance (never mutate input)
// Note: ValuesQuery requires conversion to SimpleSelectQuery because it lacks SELECT clause structure
// BinarySelectQuery and SimpleSelectQuery can be used directly in binary operations
const wrap = (q) => q instanceof SelectQuery_1.ValuesQuery ? QueryBuilder.buildSimpleQuery(q) : q;
let result = new SelectQuery_1.BinarySelectQuery(wrap(queries[0]), operator, wrap(queries[1]));
CTENormalizer_1.CTENormalizer.normalize(result);
for (let i = 2; i < queries.length; i++) {
result.appendSelectQuery(operator, wrap(queries[i]));
}
return result;
}
constructor() {
// This class is not meant to be instantiated.
}
/**
* Converts a SELECT query to a standard SimpleSelectQuery form.
* @param query The query to convert
* @returns A SimpleSelectQuery
*/
static buildSimpleQuery(query) {
if (query instanceof SelectQuery_1.SimpleSelectQuery) {
return query;
}
else if (query instanceof SelectQuery_1.BinarySelectQuery) {
return QueryBuilder.buildSimpleBinaryQuery(query);
}
else if (query instanceof SelectQuery_1.ValuesQuery) {
return QueryBuilder.buildSimpleValuesQuery(query);
}
throw new Error("Unsupported query type for buildSimpleQuery");
}
static buildSimpleBinaryQuery(query) {
// Extract ORDER BY from the rightmost query in the binary tree and remove it
const extractedOrderBy = QueryBuilder.extractAndRemoveOrderByFromBinaryQuery(query);
// Create a subquery source from the binary query (now without ORDER BY)
const subQuerySource = new Clause_1.SubQuerySource(query);
// Create a source expression with alias
const sourceExpr = new Clause_1.SourceExpression(subQuerySource, new Clause_1.SourceAliasExpression("bq", null));
// Create FROM clause with the source expression
const fromClause = new Clause_1.FromClause(sourceExpr, null);
// Create SELECT clause with * (all columns)
const selectClause = QueryBuilder.createSelectAllClause();
// Create the final simple select query with extracted ORDER BY
const q = new SelectQuery_1.SimpleSelectQuery({
selectClause,
fromClause,
orderByClause: extractedOrderBy
});
return CTENormalizer_1.CTENormalizer.normalize(q);
}
/**
* Extracts ORDER BY clause from the rightmost query in a binary query tree and removes it.
* This clarifies the semantics by moving the ORDER BY from the ambiguous position
* in the UNION to the explicit outer SimpleSelectQuery level.
*
* NOTE: ORDER BY in UNION context applies to the entire result set, not individual subqueries.
* Therefore, table prefixes (e.g., "a.column") in ORDER BY are invalid SQL and would cause
* syntax errors. Valid ORDER BY clauses should only reference column names without prefixes
* or use positional notation (ORDER BY 1, 2). Since we only process valid SQL, the current
* implementation correctly handles legitimate cases without additional prefix processing.
*
* @param query BinarySelectQuery to process
* @returns Extracted OrderByClause or null if none found
*/
static extractAndRemoveOrderByFromBinaryQuery(query) {
return QueryBuilder.findAndRemoveRightmostOrderBy(query);
}
/**
* Recursively finds and removes ORDER BY from the rightmost query in a binary tree.
*
* @param query Current query being processed
* @returns Extracted OrderByClause or null
*/
static findAndRemoveRightmostOrderBy(query) {
if (query instanceof SelectQuery_1.BinarySelectQuery) {
// For binary queries, check right side first (rightmost takes precedence)
const rightOrderBy = QueryBuilder.findAndRemoveRightmostOrderBy(query.right);
if (rightOrderBy) {
return rightOrderBy;
}
// If no ORDER BY on right side, check left side
return QueryBuilder.findAndRemoveRightmostOrderBy(query.left);
}
else if (query instanceof SelectQuery_1.SimpleSelectQuery) {
// Extract ORDER BY from SimpleSelectQuery and remove it
const orderBy = query.orderByClause;
if (orderBy) {
query.orderByClause = null;
return orderBy;
}
}
return null;
}
/**
* Converts a ValuesQuery to a SimpleSelectQuery with sequentially numbered columns or user-specified columns
*
* @param query The VALUES query to convert
* @param columns Optional: column names
* @returns A SimpleSelectQuery
*/
static buildSimpleValuesQuery(query) {
// Figure out how many columns are in the VALUES clause
const columnCount = query.tuples.length > 0 ? query.tuples[0].values.length : 0;
if (query.tuples.length === 0) {
throw new Error("Empty VALUES clause cannot be converted to a SimpleSelectQuery");
}
if (!query.columnAliases) {
throw new Error("Column aliases are required to convert a VALUES clause to SimpleSelectQuery. Please specify column aliases.");
}
if (query.columnAliases.length !== columnCount) {
throw new Error(`The number of column aliases (${query.columnAliases.length}) does not match the number of columns in the first tuple (${columnCount}).`);
}
// Create a subquery source from the VALUES query
const subQuerySource = new Clause_1.SubQuerySource(query);
const sourceExpr = new Clause_1.SourceExpression(subQuerySource, new Clause_1.SourceAliasExpression("vq", query.columnAliases));
// Create FROM clause with the source expression
const fromClause = new Clause_1.FromClause(sourceExpr, null);
// Create SELECT clause with all columns
const selectItems = query.columnAliases.map(name => new Clause_1.SelectItem(new ValueComponent_1.ColumnReference("vq", name), name));
const selectClause = new Clause_1.SelectClause(selectItems, null);
// Create the final simple select query
return new SelectQuery_1.SimpleSelectQuery({
selectClause,
fromClause
});
}
/**
* Creates a SELECT clause with a single * (all columns) item
*
* @returns A SELECT clause with *
*/
static createSelectAllClause() {
// Create a column reference for *
const columnRef = new ValueComponent_1.ColumnReference(null, "*");
// Create a SelectItem with the column reference
const selectItem = new Clause_1.SelectItem(columnRef, "*");
// Create and return a SelectClause with the item
return new Clause_1.SelectClause([selectItem], null);
}
/**
* Converts a SELECT query to a CREATE TABLE query (CREATE [TEMPORARY] TABLE ... AS SELECT ...)
* @param query The SELECT query to use as the source
* @param tableName The name of the table to create
* @param isTemporary If true, creates a temporary table
* @returns A CreateTableQuery instance
*/
static buildCreateTableQuery(query, tableName, isTemporary = false, ifNotExists = false) {
return new CreateTableQuery_1.CreateTableQuery({
tableName,
isTemporary,
ifNotExists,
asSelectQuery: query
});
}
/**
* Converts a SELECT query to an INSERT query (INSERT INTO ... SELECT ...).
*/
static buildInsertQuery(selectQuery, targetOrOptions, explicitColumns) {
var _a;
// Derive normalized options while preserving the legacy signature for backward compatibility.
const options = QueryBuilder.normalizeInsertOptions(targetOrOptions, explicitColumns);
// Determine the final column order either from user-provided options or by inferring from the select list.
const columnNames = QueryBuilder.prepareInsertColumns(selectQuery, (_a = options.columns) !== null && _a !== void 0 ? _a : null);
// Promote WITH clauses to the INSERT statement so the SELECT body remains self-contained.
const withClause = QueryBuilder.extractWithClause(selectQuery);
const sourceExpr = SourceExpressionParser_1.SourceExpressionParser.parse(options.target);
return new InsertQuery_1.InsertQuery({
withClause: withClause !== null && withClause !== void 0 ? withClause : undefined,
insertClause: new Clause_1.InsertClause(sourceExpr, columnNames),
selectQuery
});
}
/**
* Converts an INSERT ... VALUES query into INSERT ... SELECT form using UNION ALL.
* @param insertQuery The VALUES-based InsertQuery to convert.
* @returns A new InsertQuery that selects rows instead of using VALUES.
*/
static convertInsertValuesToSelect(insertQuery) {
return InsertQuerySelectValuesConverter_1.InsertQuerySelectValuesConverter.toSelectUnion(insertQuery);
}
/**
* Converts an INSERT ... SELECT (optionally with UNION ALL) into INSERT ... VALUES form.
* @param insertQuery The SELECT-based InsertQuery to convert.
* @returns A new InsertQuery that uses VALUES tuples.
*/
static convertInsertSelectToValues(insertQuery) {
return InsertQuerySelectValuesConverter_1.InsertQuerySelectValuesConverter.toValues(insertQuery);
}
/**
* Builds an UPDATE query from a SELECT query and conversion options.
*/
static buildUpdateQuery(selectQuery, selectSourceOrOptions, updateTableExprRaw, primaryKeys) {
var _a;
// Normalize the function arguments into a single configuration object.
const options = QueryBuilder.normalizeUpdateOptions(selectSourceOrOptions, updateTableExprRaw, primaryKeys);
// Collect select-list metadata and align columns before mutating the query during WITH extraction.
const updateColumns = QueryBuilder.prepareUpdateColumns(selectQuery, options.primaryKeys, (_a = options.columns) !== null && _a !== void 0 ? _a : null);
const updateClause = new Clause_1.UpdateClause(SourceExpressionParser_1.SourceExpressionParser.parse(options.target));
const targetAlias = updateClause.getSourceAliasName();
if (!targetAlias) {
throw new Error(`Source expression does not have an alias. Please provide an alias for the source expression.`);
}
// Move CTE definitions to the UPDATE statement for cleaner SQL.
const withClause = QueryBuilder.extractWithClause(selectQuery);
const setItems = updateColumns.map(column => new Clause_1.SetClauseItem(column, QueryBuilder.toColumnReference(options.sourceAlias, column)));
if (setItems.length === 0) {
throw new Error(`No updatable columns found. Ensure the select list contains at least one column other than the specified primary keys.`);
}
const setClause = new Clause_1.SetClause(setItems);
const fromClause = new Clause_1.FromClause(selectQuery.toSource(options.sourceAlias), null);
const whereClause = new Clause_1.WhereClause(QueryBuilder.buildEqualityPredicate(targetAlias, options.sourceAlias, options.primaryKeys));
return new UpdateQuery_1.UpdateQuery({
updateClause,
setClause,
fromClause,
whereClause,
withClause: withClause !== null && withClause !== void 0 ? withClause : undefined
});
}
/**
* Builds a DELETE query that deletes the rows matched by the SELECT query output.
*/
static buildDeleteQuery(selectQuery, options) {
var _a;
// Normalise options to guarantee arrays and alias defaults.
const normalized = QueryBuilder.normalizeDeleteOptions(options);
const predicateColumns = QueryBuilder.prepareDeleteColumns(selectQuery, normalized.primaryKeys, (_a = normalized.columns) !== null && _a !== void 0 ? _a : null);
const deleteClause = new Clause_1.DeleteClause(SourceExpressionParser_1.SourceExpressionParser.parse(normalized.target));
const targetAlias = deleteClause.getSourceAliasName();
if (!targetAlias) {
throw new Error(`Source expression does not have an alias. Please provide an alias for the delete target.`);
}
const withClause = QueryBuilder.extractWithClause(selectQuery);
// Build correlated EXISTS predicate instead of Postgres-specific USING clause.
const predicate = QueryBuilder.buildEqualityPredicate(targetAlias, normalized.sourceAlias, predicateColumns);
const sourceExpression = selectQuery.toSource(normalized.sourceAlias);
const existsSelectClause = new Clause_1.SelectClause([new Clause_1.SelectItem(new ValueComponent_1.LiteralValue(1))]);
const existsSubquery = new SelectQuery_1.SimpleSelectQuery({
selectClause: existsSelectClause,
fromClause: new Clause_1.FromClause(sourceExpression, null),
whereClause: new Clause_1.WhereClause(predicate)
});
const whereClause = new Clause_1.WhereClause(new ValueComponent_1.UnaryExpression('exists', new ValueComponent_1.InlineQuery(existsSubquery)));
return new DeleteQuery_1.DeleteQuery({
deleteClause,
whereClause,
withClause: withClause !== null && withClause !== void 0 ? withClause : undefined
});
}
/**
* Builds a MERGE query (upsert) that coordinates actions based on row matches.
*/
static buildMergeQuery(selectQuery, options) {
var _a, _b, _c, _d, _e, _f, _g;
// Ensure the configuration is fully expanded before inspection.
const normalized = QueryBuilder.normalizeMergeOptions(options);
const mergeColumnPlan = QueryBuilder.prepareMergeColumns(selectQuery, normalized.primaryKeys, (_a = normalized.updateColumns) !== null && _a !== void 0 ? _a : null, (_b = normalized.insertColumns) !== null && _b !== void 0 ? _b : null, (_c = normalized.matchedAction) !== null && _c !== void 0 ? _c : 'update', (_d = normalized.notMatchedAction) !== null && _d !== void 0 ? _d : 'insert');
const targetExpression = SourceExpressionParser_1.SourceExpressionParser.parse(normalized.target);
const targetAlias = targetExpression.getAliasName();
if (!targetAlias) {
throw new Error(`Source expression does not have an alias. Please provide an alias for the merge target.`);
}
const withClause = QueryBuilder.extractWithClause(selectQuery);
const onCondition = QueryBuilder.buildEqualityPredicate(targetAlias, normalized.sourceAlias, normalized.primaryKeys);
const sourceExpression = selectQuery.toSource(normalized.sourceAlias);
const whenClauses = [];
const matchedAction = (_e = normalized.matchedAction) !== null && _e !== void 0 ? _e : 'update';
if (matchedAction === 'update') {
if (mergeColumnPlan.updateColumns.length === 0) {
throw new Error(`No columns available for MERGE update action. Provide updateColumns or ensure the select list includes non-key columns.`);
}
const setItems = mergeColumnPlan.updateColumns.map(column => new Clause_1.SetClauseItem(column, QueryBuilder.toColumnReference(normalized.sourceAlias, column)));
whenClauses.push(new MergeQuery_1.MergeWhenClause("matched", new MergeQuery_1.MergeUpdateAction(new Clause_1.SetClause(setItems))));
}
else if (matchedAction === 'delete') {
whenClauses.push(new MergeQuery_1.MergeWhenClause("matched", new MergeQuery_1.MergeDeleteAction()));
}
else if (matchedAction === 'doNothing') {
whenClauses.push(new MergeQuery_1.MergeWhenClause("matched", new MergeQuery_1.MergeDoNothingAction()));
}
const notMatchedAction = (_f = normalized.notMatchedAction) !== null && _f !== void 0 ? _f : 'insert';
if (notMatchedAction === 'insert') {
if (mergeColumnPlan.insertColumns.length === 0) {
throw new Error('Unable to infer MERGE insert columns. Provide insertColumns explicitly.');
}
const insertValues = new ValueComponent_1.ValueList(mergeColumnPlan.insertColumns.map(column => QueryBuilder.toColumnReference(normalized.sourceAlias, column)));
whenClauses.push(new MergeQuery_1.MergeWhenClause("not_matched", new MergeQuery_1.MergeInsertAction({
columns: mergeColumnPlan.insertColumns,
values: insertValues
})));
}
else if (notMatchedAction === 'doNothing') {
whenClauses.push(new MergeQuery_1.MergeWhenClause("not_matched", new MergeQuery_1.MergeDoNothingAction()));
}
const notMatchedBySourceAction = (_g = normalized.notMatchedBySourceAction) !== null && _g !== void 0 ? _g : 'doNothing';
if (notMatchedBySourceAction === 'delete') {
whenClauses.push(new MergeQuery_1.MergeWhenClause("not_matched_by_source", new MergeQuery_1.MergeDeleteAction()));
}
else if (notMatchedBySourceAction === 'doNothing') {
whenClauses.push(new MergeQuery_1.MergeWhenClause("not_matched_by_source", new MergeQuery_1.MergeDoNothingAction()));
}
if (whenClauses.length === 0) {
throw new Error(`At least one MERGE action must be generated. Adjust the merge conversion options.`);
}
return new MergeQuery_1.MergeQuery({
withClause: withClause !== null && withClause !== void 0 ? withClause : undefined,
target: targetExpression,
source: sourceExpression,
onCondition,
whenClauses
});
}
static normalizeInsertOptions(targetOrOptions, explicitColumns) {
if (typeof targetOrOptions === 'string') {
return {
target: targetOrOptions,
columns: explicitColumns
};
}
if (explicitColumns && explicitColumns.length > 0) {
return {
...targetOrOptions,
columns: explicitColumns
};
}
return { ...targetOrOptions };
}
static normalizeUpdateOptions(selectSourceOrOptions, updateTableExprRaw, primaryKeys) {
var _a;
if (typeof selectSourceOrOptions === 'string') {
if (!updateTableExprRaw) {
throw new Error('updateTableExprRaw is required when using the legacy buildUpdateQuery signature.');
}
if (primaryKeys === undefined) {
throw new Error('primaryKeys are required when using the legacy buildUpdateQuery signature.');
}
return {
target: updateTableExprRaw,
primaryKeys: QueryBuilder.normalizeColumnArray(primaryKeys),
sourceAlias: selectSourceOrOptions
};
}
return {
target: selectSourceOrOptions.target,
primaryKeys: QueryBuilder.normalizeColumnArray(selectSourceOrOptions.primaryKeys),
sourceAlias: (_a = selectSourceOrOptions.sourceAlias) !== null && _a !== void 0 ? _a : 'src',
columns: selectSourceOrOptions.columns
};
}
static normalizeDeleteOptions(options) {
var _a;
return {
...options,
primaryKeys: QueryBuilder.normalizeColumnArray(options.primaryKeys),
sourceAlias: (_a = options.sourceAlias) !== null && _a !== void 0 ? _a : 'src'
};
}
static normalizeMergeOptions(options) {
var _a;
return {
...options,
primaryKeys: QueryBuilder.normalizeColumnArray(options.primaryKeys),
sourceAlias: (_a = options.sourceAlias) !== null && _a !== void 0 ? _a : 'src'
};
}
static normalizeColumnArray(columns) {
const array = Array.isArray(columns) ? columns : [columns];
const normalized = array.map(col => col.trim()).filter(col => col.length > 0);
if (!normalized.length) {
throw new Error('At least one column must be specified.');
}
return normalized;
}
static collectSelectItems(selectQuery) {
const collector = new SelectValueCollector_1.SelectValueCollector();
return collector.collect(selectQuery);
}
static collectSelectColumnNames(selectQuery) {
const items = QueryBuilder.collectSelectItems(selectQuery);
const names = [];
for (const item of items) {
if (!item.name || item.name === '*') {
throw new Error(`Columns cannot be inferred from the selectQuery. ` +
`Make sure you are not using wildcards or unnamed columns.`);
}
if (!names.includes(item.name)) {
names.push(item.name);
}
}
if (!names.length) {
throw new Error('Unable to determine any column names from selectQuery.');
}
return names;
}
static ensurePrimaryKeys(selectColumns, primaryKeys) {
const available = new Set(selectColumns);
for (const pk of primaryKeys) {
if (!available.has(pk)) {
throw new Error(`Primary key column '${pk}' is not present in selectQuery select list.`);
}
}
}
static prepareInsertColumns(selectQuery, optionColumns) {
const selectColumns = QueryBuilder.collectSelectColumnNames(selectQuery);
if (optionColumns && optionColumns.length > 0) {
const normalized = QueryBuilder.normalizeColumnArray(optionColumns);
const uniqueNormalized = normalized.filter((name, idx) => normalized.indexOf(name) === idx);
const missing = uniqueNormalized.filter(name => !selectColumns.includes(name));
if (missing.length > 0) {
throw new Error(`Columns specified in conversion options were not found in selectQuery select list: [${missing.join(', ')}].`);
}
QueryBuilder.rebuildSelectClause(selectQuery, uniqueNormalized);
QueryBuilder.ensureSelectClauseSize(selectQuery, uniqueNormalized.length);
return uniqueNormalized;
}
QueryBuilder.ensureSelectClauseSize(selectQuery, selectColumns.length);
return selectColumns;
}
static prepareUpdateColumns(selectQuery, primaryKeys, explicitColumns) {
const selectColumns = QueryBuilder.collectSelectColumnNames(selectQuery);
QueryBuilder.ensurePrimaryKeys(selectColumns, primaryKeys);
const primaryKeySet = new Set(primaryKeys);
const updateCandidates = selectColumns.filter(name => !primaryKeySet.has(name));
let updateColumnsOrdered;
if (explicitColumns && explicitColumns.length > 0) {
const normalized = QueryBuilder.normalizeColumnArray(explicitColumns);
const uniqueNormalized = normalized.filter((name, idx) => normalized.indexOf(name) === idx);
const missing = uniqueNormalized.filter(name => primaryKeySet.has(name) || !updateCandidates.includes(name));
if (missing.length > 0) {
throw new Error(`Provided update columns were not found in selectQuery output or are primary keys: [${missing.join(', ')}].`);
}
updateColumnsOrdered = uniqueNormalized;
}
else {
updateColumnsOrdered = Array.from(new Set(updateCandidates));
}
const desiredOrder = Array.from(new Set([
...primaryKeys,
...updateColumnsOrdered
]));
QueryBuilder.rebuildSelectClause(selectQuery, desiredOrder);
QueryBuilder.ensureSelectClauseSize(selectQuery, desiredOrder.length);
return updateColumnsOrdered;
}
static prepareDeleteColumns(selectQuery, primaryKeys, explicitColumns) {
const selectColumns = QueryBuilder.collectSelectColumnNames(selectQuery);
QueryBuilder.ensurePrimaryKeys(selectColumns, primaryKeys);
const primaryKeySet = new Set(primaryKeys);
let matchColumns = [];
if (explicitColumns && explicitColumns.length > 0) {
const normalized = QueryBuilder.normalizeColumnArray(explicitColumns);
const preferred = new Set(normalized);
matchColumns = selectColumns.filter(name => preferred.has(name) && !primaryKeySet.has(name));
}
const requiredColumns = new Set();
primaryKeys.forEach(key => requiredColumns.add(key));
matchColumns.forEach(col => requiredColumns.add(col));
const desiredOrder = selectColumns.filter(name => requiredColumns.has(name));
QueryBuilder.rebuildSelectClause(selectQuery, desiredOrder);
QueryBuilder.ensureSelectClauseSize(selectQuery, desiredOrder.length);
return desiredOrder;
}
static prepareMergeColumns(selectQuery, primaryKeys, explicitUpdateColumns, explicitInsertColumns, matchedAction, notMatchedAction) {
const selectColumns = QueryBuilder.collectSelectColumnNames(selectQuery);
QueryBuilder.ensurePrimaryKeys(selectColumns, primaryKeys);
const primaryKeySet = new Set(primaryKeys);
let updateColumnsOrdered = [];
if (matchedAction === 'update') {
const candidates = selectColumns.filter(name => !primaryKeySet.has(name));
if (explicitUpdateColumns && explicitUpdateColumns.length > 0) {
const normalized = QueryBuilder.normalizeColumnArray(explicitUpdateColumns);
const uniqueNormalized = normalized.filter((name, idx) => normalized.indexOf(name) === idx);
const missing = uniqueNormalized.filter(name => primaryKeySet.has(name) || !candidates.includes(name));
if (missing.length > 0) {
throw new Error(`Provided update columns were not found in selectQuery output or are primary keys: [${missing.join(', ')}].`);
}
updateColumnsOrdered = uniqueNormalized;
}
else {
updateColumnsOrdered = Array.from(new Set(candidates));
}
}
let insertColumnsOrdered = [];
if (notMatchedAction === 'insert') {
if (explicitInsertColumns && explicitInsertColumns.length > 0) {
const normalized = QueryBuilder.normalizeColumnArray(explicitInsertColumns);
const uniqueNormalized = normalized.filter((name, idx) => normalized.indexOf(name) === idx);
const missing = uniqueNormalized.filter(name => !selectColumns.includes(name));
if (missing.length > 0) {
throw new Error(`Provided insert columns were not found in selectQuery output: [${missing.join(', ')}].`);
}
insertColumnsOrdered = uniqueNormalized;
}
else {
insertColumnsOrdered = Array.from(new Set(selectColumns));
}
}
const desiredOrder = Array.from(new Set([
...primaryKeys,
...updateColumnsOrdered,
...insertColumnsOrdered,
...selectColumns
])).filter(name => selectColumns.includes(name));
QueryBuilder.rebuildSelectClause(selectQuery, desiredOrder);
QueryBuilder.ensureSelectClauseSize(selectQuery, desiredOrder.length);
const finalUpdateColumns = matchedAction === 'update'
? updateColumnsOrdered
: [];
const finalInsertColumns = notMatchedAction === 'insert'
? insertColumnsOrdered
: [];
return {
updateColumns: finalUpdateColumns,
insertColumns: finalInsertColumns
};
}
static rebuildSelectClause(selectQuery, desiredColumns) {
const itemMap = new Map();
for (const item of selectQuery.selectClause.items) {
const name = QueryBuilder.getSelectItemName(item);
if (!name) {
continue;
}
if (!itemMap.has(name)) {
itemMap.set(name, item);
}
}
const rebuiltItems = [];
const seen = new Set();
for (const column of desiredColumns) {
if (seen.has(column)) {
continue;
}
const item = itemMap.get(column);
if (!item) {
throw new Error(`Column '${column}' not found in select clause.`);
}
rebuiltItems.push(item);
seen.add(column);
}
if (!rebuiltItems.length) {
throw new Error('Unable to rebuild select clause with the requested columns.');
}
selectQuery.selectClause.items = rebuiltItems;
}
static getSelectItemName(item) {
if (item.identifier) {
return item.identifier.name;
}
if (item.value instanceof ValueComponent_1.ColumnReference) {
return item.value.column.name;
}
return null;
}
static ensureSelectClauseSize(selectQuery, expected) {
if (selectQuery.selectClause.items.length !== expected) {
throw new Error(`Select clause column count (${selectQuery.selectClause.items.length}) does not match expected count (${expected}).`);
}
}
static extractWithClause(selectQuery) {
const cteCollector = new CTECollector_1.CTECollector();
const collected = cteCollector.collect(selectQuery);
if (collected.length === 0) {
return null;
}
const cteDisabler = new CTEDisabler_1.CTEDisabler();
cteDisabler.execute(selectQuery);
return new Clause_1.WithClause(false, collected);
}
static buildEqualityPredicate(leftAlias, rightAlias, columns) {
const uniqueColumns = QueryBuilder.mergeUniqueColumns(columns);
if (!uniqueColumns.length) {
throw new Error('At least one column is required to build a comparison predicate.');
}
let predicate = null;
for (const column of uniqueColumns) {
const comparison = new ValueComponent_1.BinaryExpression(QueryBuilder.toColumnReference(leftAlias, column), '=', QueryBuilder.toColumnReference(rightAlias, column));
predicate = predicate ? new ValueComponent_1.BinaryExpression(predicate, 'and', comparison) : comparison;
}
return predicate;
}
static toColumnReference(alias, column) {
return new ValueComponent_1.ColumnReference(alias, column);
}
static mergeUniqueColumns(columns) {
const seen = new Set();
const result = [];
for (const column of columns) {
if (!seen.has(column)) {
seen.add(column);
result.push(column);
}
}
return result;
}
}
exports.QueryBuilder = QueryBuilder;
//# sourceMappingURL=QueryBuilder.js.map