UNPKG

rawsql-ts

Version:

[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

208 lines 10.3 kB
import { SetClause, SetClauseItem, FromClause, WhereClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, SubQuerySource, WithClause, UpdateClause, InsertClause } from '../models/Clause'; import { UpdateQuery } from '../models/UpdateQuery'; import { BinaryExpression, ColumnReference } from '../models/ValueComponent'; import { SelectValueCollector } from './SelectValueCollector'; import { BinarySelectQuery, SimpleSelectQuery, ValuesQuery } from "../models/SelectQuery"; import { CTECollector } from "./CTECollector"; import { CTENormalizer } from "./CTENormalizer"; import { CreateTableQuery } from "../models/CreateTableQuery"; import { InsertQuery } from "../models/InsertQuery"; import { CTEDisabler } from './CTEDisabler'; import { SourceExpressionParser } from '../parsers/SourceExpressionParser'; /** * QueryBuilder provides static methods to build or convert various SQL query objects. */ export 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) const wrap = (q) => q instanceof ValuesQuery ? QueryBuilder.buildSimpleQuery(q) : q; let result = new BinarySelectQuery(wrap(queries[0]), operator, wrap(queries[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 SimpleSelectQuery) { return query; } else if (query instanceof BinarySelectQuery) { return QueryBuilder.buildSimpleBinaryQuery(query); } else if (query instanceof ValuesQuery) { return QueryBuilder.buildSimpleValuesQuery(query); } throw new Error("Unsupported query type for buildSimpleQuery"); } static buildSimpleBinaryQuery(query) { // Create a subquery source from the binary query const subQuerySource = new SubQuerySource(query); // Create a source expression with alias const sourceExpr = new SourceExpression(subQuerySource, new SourceAliasExpression("bq", null)); // Create FROM clause with the source expression const fromClause = new FromClause(sourceExpr, null); // Create SELECT clause with * (all columns) const selectClause = QueryBuilder.createSelectAllClause(); // Create the final simple select query const q = new SimpleSelectQuery({ selectClause, fromClause }); return CTENormalizer.normalize(q); } /** * 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 SubQuerySource(query); const sourceExpr = new SourceExpression(subQuerySource, new SourceAliasExpression("vq", query.columnAliases)); // Create FROM clause with the source expression const fromClause = new FromClause(sourceExpr, null); // Create SELECT clause with all columns const selectItems = query.columnAliases.map(name => new SelectItem(new ColumnReference("vq", name), name)); const selectClause = new SelectClause(selectItems, null); // Create the final simple select query return new 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 ColumnReference(null, "*"); // Create a SelectItem with the column reference const selectItem = new SelectItem(columnRef, "*"); // Create and return a SelectClause with the item return new 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) { return new CreateTableQuery({ tableName, isTemporary, asSelectQuery: query }); } /** * Converts a SELECT query to an INSERT query (INSERT INTO ... SELECT ...) * @param selectQuery The SELECT query to use as the source * @param tableName The name of the table to insert into * @param columns Optional: array of column names. If omitted, columns are inferred from the selectQuery * @returns An InsertQuery instance */ static buildInsertQuery(selectQuery, tableName) { let cols; const count = selectQuery.selectClause.items.length; // Try to infer columns from the selectQuery const collector = new SelectValueCollector(); const items = collector.collect(selectQuery); cols = items.map(item => item.name); if (!cols.length || count !== cols.length) { throw new Error(`Columns cannot be inferred from the selectQuery. ` + `Make sure you are not using wildcards or unnamed columns.\n` + `Select clause column count: ${count}, ` + `Columns with valid names: ${cols.length}\n` + `Detected column names: [${cols.join(", ")}]`); } // Generate SourceExpression (supports only table name, does not support alias or schema) const sourceExpr = SourceExpressionParser.parse(tableName); return new InsertQuery({ insertClause: new InsertClause(sourceExpr, cols), selectQuery: selectQuery }); } /** * Builds an UPDATE query from a SELECT query, table name, and primary key(s). * @param selectQuery The SELECT query providing new values (must select all columns to update and PKs) * @param updateTableExprRaw The table name to update * @param primaryKeys The primary key column name(s) * @returns UpdateQuery instance */ static buildUpdateQuery(selectQuery, selectSourceName, updateTableExprRaw, primaryKeys) { const updateClause = new UpdateClause(SourceExpressionParser.parse(updateTableExprRaw)); const pkArray = Array.isArray(primaryKeys) ? primaryKeys : [primaryKeys]; const selectCollector = new SelectValueCollector(); const selectItems = selectCollector.collect(selectQuery); const cteCollector = new CTECollector(); const collectedCTEs = cteCollector.collect(selectQuery); const cteDisabler = new CTEDisabler(); cteDisabler.execute(selectQuery); for (const pk of pkArray) { if (!selectItems.some(item => item.name === pk)) { throw new Error(`Primary key column '${pk}' is not present in selectQuery select list.`); } } const updateSourceName = updateClause.getSourceAliasName(); if (!updateSourceName) { throw new Error(`Source expression does not have an alias. Please provide an alias for the source expression.`); } const setColumns = selectItems.filter(item => !pkArray.includes(item.name)); const setItems = setColumns.map(col => new SetClauseItem(col.name, new ColumnReference(updateSourceName, col.name))); const setClause = new SetClause(setItems); const from = new FromClause(selectQuery.toSource(selectSourceName), null); let where = null; for (const pk of pkArray) { const cond = new BinaryExpression(new ColumnReference(updateSourceName, pk), '=', new ColumnReference(selectSourceName, pk)); where = where ? new BinaryExpression(where, 'and', cond) : cond; } const whereClause = new WhereClause(where); const updateQuery = new UpdateQuery({ updateClause: updateClause, setClause: setClause, fromClause: from, whereClause: whereClause, withClause: collectedCTEs.length > 0 ? new WithClause(false, collectedCTEs) : undefined, }); return updateQuery; } } //# sourceMappingURL=QueryBuilder.js.map