UNPKG

rawsql-ts

Version:

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

359 lines 17.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UpdateResultSelectConverter = void 0; const Clause_1 = require("../models/Clause"); const SelectQuery_1 = require("../models/SelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); const TableSourceCollector_1 = require("./TableSourceCollector"); const FixtureCteBuilder_1 = require("./FixtureCteBuilder"); const SelectQueryWithClauseHelper_1 = require("../utils/SelectQueryWithClauseHelper"); const ValueComponentRewriter_1 = require("../utils/ValueComponentRewriter"); const TableNameUtils_1 = require("../utils/TableNameUtils"); class UpdateResultSelectConverter { /** * Converts an UPDATE with RETURNING (or a bare UPDATE) into a SELECT that mirrors its output rows. */ static toSelectQuery(updateQuery, options) { var _a, _b, _c, _d; const targetTableName = this.extractTargetTableName(updateQuery.updateClause); const tableDefinition = this.resolveTableDefinition(targetTableName, options); const targetAlias = updateQuery.updateClause.getSourceAliasName(); const fromClause = this.buildFromClause(updateQuery.updateClause.source, updateQuery.fromClause); const whereClause = (_a = updateQuery.whereClause) !== null && _a !== void 0 ? _a : null; // Decide whether RETURNING or a row count should drive the SELECT clause. const selectClause = updateQuery.returningClause ? this.buildReturningSelectClause(updateQuery.returningClause, updateQuery.setClause, targetAlias, tableDefinition) : this.buildCountSelectClause(); // Assemble the skeleton SELECT that mirrors the UPDATE's source and predicates. const selectQuery = new SelectQuery_1.SimpleSelectQuery({ withClause: (_b = updateQuery.withClause) !== null && _b !== void 0 ? _b : undefined, selectClause, fromClause, whereClause }); // Prepare fixture descriptors for the tables that will be touched by the SELECT. const fixtureTables = (_c = options === null || options === void 0 ? void 0 : options.fixtureTables) !== null && _c !== void 0 ? _c : []; const fixtureMap = this.buildFixtureTableMap(fixtureTables); const missingStrategy = (_d = options === null || options === void 0 ? void 0 : options.missingFixtureStrategy) !== null && _d !== void 0 ? _d : this.DEFAULT_MISSING_FIXTURE_STRATEGY; const originalWithClause = SelectQueryWithClauseHelper_1.SelectQueryWithClauseHelper.detachWithClause(selectQuery); const referencedTables = this.collectPhysicalTableReferences(selectQuery, originalWithClause); const cteNames = this.collectCteNamesFromWithClause(originalWithClause); const targetVariants = (0, TableNameUtils_1.tableNameVariants)(targetTableName); for (const variant of targetVariants) { if (!cteNames.has(variant)) { referencedTables.add(variant); } } // Ensure each referenced table is covered by a fixture (or allowed to skip it). this.ensureFixtureCoverage(referencedTables, fixtureMap, missingStrategy); const filteredFixtures = this.filterFixtureTablesForReferences(fixtureTables, referencedTables); // Turn the fixture definitions into CommonTable entries before reinjecting the WITH clause. const fixtureCtes = this.buildFixtureCtes(filteredFixtures); const recombinedWithClause = this.mergeWithClause(originalWithClause, fixtureCtes); // Reattach the combined WITH clause so fixture CTEs precede any existing definitions. SelectQueryWithClauseHelper_1.SelectQueryWithClauseHelper.setWithClause(selectQuery, recombinedWithClause); return selectQuery; } static buildReturningSelectClause(returning, setClause, targetAlias, tableDefinition) { const setExpressionMap = this.mapSetExpressions(setClause); const selectItems = this.buildReturningSelectItems(returning, setExpressionMap, targetAlias, tableDefinition); return new Clause_1.SelectClause(selectItems); } static buildReturningSelectItems(returning, setExpressions, targetAlias, tableDefinition) { // Convert each RETURNING item into a select entry, expanding wildcards up front. const selectItems = []; for (const item of returning.items) { if (this.isWildcardReturningItem(item)) { selectItems.push(...this.expandReturningWildcard(tableDefinition, setExpressions, targetAlias)); continue; } selectItems.push(this.buildUpdateReturningSelectItem(item, setExpressions, targetAlias, tableDefinition)); } return selectItems; } static isWildcardReturningItem(item) { return (item.value instanceof ValueComponent_1.ColumnReference && item.value.column.name === '*'); } static expandReturningWildcard(tableDefinition, setExpressions, targetAlias) { // Use metadata to expand RETURNING * so each column can honor SET overrides. if (!tableDefinition) { throw new Error('Cannot expand RETURNING * without table definition.'); } return tableDefinition.columns.map((column) => { const expression = this.buildUpdateColumnExpression(column.name, setExpressions, targetAlias, tableDefinition); return new Clause_1.SelectItem(expression, column.name); }); } static buildUpdateReturningSelectItem(item, setExpressions, targetAlias, tableDefinition) { // Rewrite the item expression so column references honor SET overrides. const expression = (0, ValueComponentRewriter_1.rewriteValueComponentWithColumnResolver)(item.value, (column) => this.buildUpdateColumnExpression(column, setExpressions, targetAlias, tableDefinition)); const alias = this.getReturningAlias(item); return new Clause_1.SelectItem(expression, alias); } static buildUpdateColumnExpression(columnOrName, setExpressions, targetAlias, tableDefinition) { const columnName = typeof columnOrName === 'string' ? columnOrName : this.getColumnReferenceName(columnOrName); const normalized = this.normalizeIdentifier(columnName); const overrideExpression = setExpressions.get(normalized); // Prefer the SET expression when the column is updated, otherwise preserve the target reference. if (overrideExpression) { return overrideExpression; } this.ensureColumnExists(columnName, tableDefinition); return new ValueComponent_1.ColumnReference(targetAlias, columnName); } static getColumnReferenceName(column) { const nameComponent = column.qualifiedName.name; if (nameComponent instanceof ValueComponent_1.IdentifierString) { return nameComponent.name; } return nameComponent.value; } static getReturningAlias(item) { var _a; if ((_a = item.identifier) === null || _a === void 0 ? void 0 : _a.name) { return item.identifier.name; } if (item.value instanceof ValueComponent_1.ColumnReference) { return item.value.toString(); } return null; } static buildCountSelectClause() { // Count rows when the UPDATE does not expose RETURNING output. const countFunction = new ValueComponent_1.FunctionCall(null, 'count', new ValueComponent_1.RawString('*'), null); const selectItem = new Clause_1.SelectItem(countFunction, 'count'); return new Clause_1.SelectClause([selectItem]); } static buildFromClause(targetSource, fromClause) { if (!fromClause) { return new Clause_1.FromClause(targetSource, null); } const joins = []; // Cross join any explicit FROM sources so their columns remain accessible. joins.push(new Clause_1.JoinClause('cross join', fromClause.source, null, false)); if (fromClause.joins) { joins.push(...fromClause.joins); } return new Clause_1.FromClause(targetSource, joins); } static mapSetExpressions(setClause) { // Normalize each column name so lookups are case-insensitive. const expressionMap = new Map(); for (const item of setClause.items) { const columnName = this.extractColumnName(item); expressionMap.set(this.normalizeIdentifier(columnName), item.value); } return expressionMap; } static ensureColumnExists(columnName, tableDefinition) { // Guard against referencing columns that do not exist when metadata is available. if (!tableDefinition) { return; } const normalized = this.normalizeIdentifier(columnName); const exists = tableDefinition.columns.some((column) => this.normalizeIdentifier(column.name) === normalized); if (!exists) { throw new Error(`Column '${columnName}' cannot be resolved for RETURNING output.`); } } static resolveTableDefinition(tableName, options) { // Prefer resolver callback results before falling back to the static registry. if (options === null || options === void 0 ? void 0 : options.tableDefinitionResolver) { const resolved = options.tableDefinitionResolver(tableName); if (resolved !== undefined) { return resolved; } } const normalizedVariants = new Set((0, TableNameUtils_1.tableNameVariants)(tableName)); if (options === null || options === void 0 ? void 0 : options.tableDefinitions) { const map = this.buildTableDefinitionMap(options.tableDefinitions); for (const variant of normalizedVariants) { const definition = map.get(variant); if (definition) { return definition; } } } if (options === null || options === void 0 ? void 0 : options.fixtureTables) { const fixture = options.fixtureTables.find((f) => (0, TableNameUtils_1.tableNameVariants)(f.tableName).some((variant) => normalizedVariants.has(variant))); if (fixture) { return this.convertFixtureToTableDefinition(fixture); } } return undefined; } static convertFixtureToTableDefinition(fixture) { return { name: fixture.tableName, columns: fixture.columns.map(col => { var _a; return ({ name: col.name, typeName: col.typeName, required: false, defaultValue: (_a = col.defaultValue) !== null && _a !== void 0 ? _a : null }); }) }; } static buildTableDefinitionMap(registry) { // Normalize registry keys so lookups ignore casing. const map = new Map(); for (const definition of Object.values(registry)) { for (const variant of (0, TableNameUtils_1.tableNameVariants)(definition.name)) { map.set(variant, definition); } } return map; } static extractTargetTableName(updateClause) { const datasource = updateClause.source.datasource; if (datasource instanceof Clause_1.TableSource) { return datasource.getSourceName(); } throw new Error('Update target must be a table source for conversion.'); } static extractColumnName(item) { const columnComponent = item.qualifiedName.name; if (columnComponent instanceof ValueComponent_1.RawString) { return columnComponent.value; } return columnComponent.name; } static buildFixtureCtes(fixtures) { if (!fixtures || fixtures.length === 0) { return []; } return FixtureCteBuilder_1.FixtureCteBuilder.buildFixtures(fixtures); } static collectPhysicalTableReferences(selectQuery, withClause) { const referencedTables = this.collectReferencedTables(selectQuery); const ignoredTables = this.collectCteNamesFromWithClause(withClause); const tablesToShadow = new Set(); // Record only concrete tables that are not already defined via CTE aliases. for (const table of referencedTables) { if (ignoredTables.has(table)) { continue; } tablesToShadow.add(table); } const cteReferencedTables = this.collectReferencedTablesFromWithClause(withClause); for (const table of cteReferencedTables) { if (ignoredTables.has(table)) { continue; } tablesToShadow.add(table); } return tablesToShadow; } static filterFixtureTablesForReferences(fixtures, referencedTables) { if (!fixtures.length || referencedTables.size === 0) { return []; } const filtered = []; for (const fixture of fixtures) { const fixtureVariants = (0, TableNameUtils_1.tableNameVariants)(fixture.tableName); if (fixtureVariants.some((variant) => referencedTables.has(variant))) { filtered.push(fixture); } } return filtered; } static collectReferencedTablesFromWithClause(withClause) { const tables = new Set(); if (!(withClause === null || withClause === void 0 ? void 0 : withClause.tables)) { return tables; } for (const cte of withClause.tables) { for (const table of this.collectReferencedTables(cte.query)) { tables.add(table); } } return tables; } static buildFixtureTableMap(fixtures) { // Normalize fixture table names to keep comparisons consistent. const map = new Map(); for (const fixture of fixtures) { for (const variant of (0, TableNameUtils_1.tableNameVariants)(fixture.tableName)) { map.set(variant, fixture); } } return map; } static ensureFixtureCoverage(referencedTables, fixtureMap, strategy) { if (referencedTables.size === 0) { return; } const missingTables = this.getMissingFixtureTables(referencedTables, fixtureMap); if (missingTables.length === 0) { return; } if (strategy === 'error') { throw new Error(`Update SELECT refers to tables without fixture coverage: ${missingTables.join(', ')}.`); } } static collectReferencedTables(query) { // Use the collector to track every TableSource referenced by the SELECT. const collector = new TableSourceCollector_1.TableSourceCollector(false); const sources = collector.collect(query); const normalized = new Set(); for (const source of sources) { for (const variant of (0, TableNameUtils_1.tableNameVariants)(source.getSourceName())) { normalized.add(variant); } } return normalized; } static collectCteNamesFromWithClause(withClause) { // Determine which table names come from CTE aliases so they can be ignored. const names = new Set(); if (!(withClause === null || withClause === void 0 ? void 0 : withClause.tables)) { return names; } for (const table of withClause.tables) { for (const variant of (0, TableNameUtils_1.tableNameVariants)(table.getSourceAliasName())) { names.add(variant); } } return names; } static getMissingFixtureTables(referencedTables, fixtureMap) { // Return every referenced table that lacks an overriding fixture definition. const missing = []; for (const table of referencedTables) { const covered = (0, TableNameUtils_1.tableNameVariants)(table).some((variant) => fixtureMap.has(variant)); if (!covered) { missing.push(table); } } return missing; } static mergeWithClause(original, fixtureCtes) { var _a; // Combine fixture CTEs ahead of any original definitions so they shadow physical tables. if (!fixtureCtes.length && !original) { return null; } const combinedTables = [...fixtureCtes]; if (original === null || original === void 0 ? void 0 : original.tables) { combinedTables.push(...original.tables); } if (!combinedTables.length) { return null; } const merged = new Clause_1.WithClause((_a = original === null || original === void 0 ? void 0 : original.recursive) !== null && _a !== void 0 ? _a : false, combinedTables); merged.globalComments = (original === null || original === void 0 ? void 0 : original.globalComments) ? [...original.globalComments] : null; merged.trailingComments = (original === null || original === void 0 ? void 0 : original.trailingComments) ? [...original.trailingComments] : null; return merged; } static normalizeIdentifier(value) { return value.trim().toLowerCase(); } } exports.UpdateResultSelectConverter = UpdateResultSelectConverter; UpdateResultSelectConverter.DEFAULT_MISSING_FIXTURE_STRATEGY = 'error'; //# sourceMappingURL=UpdateResultSelectConverter.js.map