UNPKG

rawsql-ts

Version:

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

410 lines 18.5 kB
import { FromClause, JoinClause, SelectClause, SelectItem, TableSource, WithClause } from '../models/Clause'; import { SimpleSelectQuery } from '../models/SelectQuery'; import { ColumnReference, FunctionCall, RawString } from '../models/ValueComponent'; import { TableSourceCollector } from './TableSourceCollector'; import { FixtureCteBuilder } from './FixtureCteBuilder'; import { SelectQueryWithClauseHelper } from '../utils/SelectQueryWithClauseHelper'; import { rewriteValueComponentWithColumnResolver } from '../utils/ValueComponentRewriter'; import { tableNameVariants } from '../utils/TableNameUtils'; import { FullNameParser } from '../parsers/FullNameParser'; export class DeleteResultSelectConverter { /** * Converts a DELETE (with optional RETURNING) into a SELECT that mirrors its output rows. */ static toSelectQuery(deleteQuery, options) { var _a, _b, _c, _d; const targetTableName = this.extractTargetTableName(deleteQuery.deleteClause); const tableDefinition = this.resolveTableDefinition(targetTableName, options); const targetAlias = deleteQuery.deleteClause.getSourceAliasName(); // Build the SELECT structure that mirrors the DELETE source/join semantics. const returningContext = deleteQuery.returningClause ? this.buildReturningContext(deleteQuery.deleteClause, deleteQuery.usingClause, targetAlias, tableDefinition, options) : null; const selectClause = deleteQuery.returningClause ? this.buildReturningSelectClause(deleteQuery.returningClause, returningContext) : this.buildCountSelectClause(); const fromClause = this.buildFromClause(deleteQuery.deleteClause, deleteQuery.usingClause); const whereClause = (_a = deleteQuery.whereClause) !== null && _a !== void 0 ? _a : null; const selectQuery = new SimpleSelectQuery({ withClause: (_b = deleteQuery.withClause) !== null && _b !== void 0 ? _b : undefined, selectClause, fromClause, whereClause }); // Ensure fixture coverage before weaving CTEs back into 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.detachWithClause(selectQuery); const referencedTables = this.collectPhysicalTableReferences(selectQuery, originalWithClause); const cteNames = this.collectCteNamesFromWithClause(originalWithClause); const targetVariants = tableNameVariants(targetTableName); for (const variant of targetVariants) { if (!cteNames.has(variant)) { referencedTables.add(variant); } } this.ensureFixtureCoverage(referencedTables, fixtureMap, missingStrategy); const filteredFixtures = this.filterFixtureTablesForReferences(fixtureTables, referencedTables); const fixtureCtes = this.buildFixtureCtes(filteredFixtures); const mergedWithClause = this.mergeWithClause(originalWithClause, fixtureCtes); SelectQueryWithClauseHelper.setWithClause(selectQuery, mergedWithClause); return selectQuery; } static buildReturningSelectClause(returning, context) { const selectItems = this.buildReturningSelectItems(returning, context); return new SelectClause(selectItems); } static buildReturningSelectItems(returning, context) { // Build SELECT entries from RETURNING items, expanding wildcards before expression rewriting. const selectItems = []; for (const item of returning.items) { if (this.isWildcardReturningItem(item)) { selectItems.push(...this.expandReturningWildcard(context)); continue; } selectItems.push(this.buildDeleteReturningSelectItem(item, context)); } return selectItems; } static isWildcardReturningItem(item) { return (item.value instanceof ColumnReference && item.value.column.name === '*'); } static expandReturningWildcard(context) { if (!context.targetDefinition) { throw new Error('Cannot expand RETURNING * without table definition.'); } return context.targetDefinition.columns.map((column) => { const expression = this.composeDeleteColumnReference({ namespaces: null, column: column.name }, context); return new SelectItem(expression, column.name); }); } static buildDeleteReturningSelectItem(item, context) { const expression = rewriteValueComponentWithColumnResolver(item.value, (column) => this.buildDeleteColumnReference(column, context)); const alias = this.getReturningAlias(item); return new SelectItem(expression, alias); } static buildDeleteColumnReference(column, context) { const parsed = this.parseReturningColumnName(column.toString()); return this.composeDeleteColumnReference(parsed, context); } static composeDeleteColumnReference(parsedColumn, context) { var _a; const tableContext = this.findTableContextForNamespaces(parsedColumn.namespaces, context); const definitionToValidate = (_a = tableContext === null || tableContext === void 0 ? void 0 : tableContext.tableDefinition) !== null && _a !== void 0 ? _a : (parsedColumn.namespaces ? undefined : context.targetDefinition); if (definitionToValidate) { this.ensureColumnExists(parsedColumn.column, definitionToValidate); } const columnNamespace = parsedColumn.namespaces && parsedColumn.namespaces.length > 0 ? [...parsedColumn.namespaces] : context.targetAlias ? [context.targetAlias] : null; return new ColumnReference(columnNamespace, parsedColumn.column); } 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 ColumnReference) { return item.value.toString(); } return null; } static buildCountSelectClause() { // Count rows when no RETURNING clause is present. const countFunction = new FunctionCall(null, 'count', new RawString('*'), null); const selectItem = new SelectItem(countFunction, 'count'); return new SelectClause([selectItem]); } static buildFromClause(deleteClause, usingClause) { var _a; if (!((_a = usingClause === null || usingClause === void 0 ? void 0 : usingClause.sources) === null || _a === void 0 ? void 0 : _a.length)) { return new FromClause(deleteClause.source, null); } // Cross join each USING source so their columns remain available for predicates. const joins = usingClause.sources.map((source) => new JoinClause('cross join', source, null, false)); return new FromClause(deleteClause.source, joins); } static buildReturningContext(deleteClause, usingClause, targetAlias, targetDefinition, options) { const { aliasMap, tableNameMap } = this.buildTableContexts(deleteClause, usingClause, options); return { aliasMap, tableNameMap, targetAlias, targetDefinition }; } static buildTableContexts(deleteClause, usingClause, options) { const aliasMap = new Map(); const tableNameMap = new Map(); // Capture alias and table name contexts to resolve metadata for both identifiers. const collectSource = (source) => { const alias = source.getAliasName(); if (!alias || !(source.datasource instanceof TableSource)) { return; } const normalizedAlias = this.normalizeIdentifier(alias); if (aliasMap.has(normalizedAlias)) { return; } const tableName = source.datasource.getSourceName(); const tableDefinition = this.resolveTableDefinition(tableName, options); const context = { alias, tableName, tableDefinition }; aliasMap.set(normalizedAlias, context); for (const variant of tableNameVariants(tableName)) { if (!tableNameMap.has(variant)) { tableNameMap.set(variant, context); } } }; collectSource(deleteClause.source); if (usingClause) { for (const source of usingClause.sources) { collectSource(source); } } return { aliasMap, tableNameMap }; } static parseReturningColumnName(columnName) { const trimmed = columnName.trim(); if (!trimmed) { throw new Error('Returning column name cannot be empty.'); } try { const parsed = FullNameParser.parse(trimmed); return { namespaces: parsed.namespaces, column: parsed.name.name }; } catch (_a) { const parts = trimmed.split('.').map((segment) => segment.trim()).filter((segment) => segment.length > 0); if (parts.length === 0) { return { namespaces: null, column: trimmed }; } const column = parts.pop(); return { namespaces: parts.length > 0 ? parts : null, column }; } } static findTableContextForNamespaces(namespaces, context) { if (!(namespaces === null || namespaces === void 0 ? void 0 : namespaces.length)) { return undefined; } for (let depth = namespaces.length; depth > 0; depth--) { const candidateParts = namespaces.slice(namespaces.length - depth); const identifier = candidateParts.join('.'); const normalized = this.normalizeIdentifier(identifier); const aliasContext = context.aliasMap.get(normalized); if (aliasContext) { return aliasContext; } const tableContext = context.tableNameMap.get(normalized); if (tableContext) { return tableContext; } } return undefined; } static ensureColumnExists(columnName, tableDefinition) { 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) { if (options === null || options === void 0 ? void 0 : options.tableDefinitionResolver) { const resolved = options.tableDefinitionResolver(tableName); if (resolved !== undefined) { return resolved; } } const normalizedVariants = new Set(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) => 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) { const map = new Map(); for (const definition of Object.values(registry)) { for (const variant of tableNameVariants(definition.name)) { map.set(variant, definition); } } return map; } static extractTargetTableName(deleteClause) { const datasource = deleteClause.source.datasource; if (datasource instanceof TableSource) { return datasource.getSourceName(); } throw new Error('Delete target must be a table source for conversion.'); } static buildFixtureCtes(fixtures) { if (!fixtures || fixtures.length === 0) { return []; } return FixtureCteBuilder.buildFixtures(fixtures); } static collectPhysicalTableReferences(selectQuery, withClause) { const referencedTables = this.collectReferencedTables(selectQuery); const ignoredTables = this.collectCteNamesFromWithClause(withClause); const tablesToShadow = new Set(); // Keep only concrete tables that are not defined via WITH clause 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 buildFixtureTableMap(fixtures) { const map = new Map(); for (const fixture of fixtures) { for (const variant of tableNameVariants(fixture.tableName)) { map.set(variant, fixture); } } return map; } static filterFixtureTablesForReferences(fixtures, referencedTables) { if (!fixtures.length || referencedTables.size === 0) { return []; } const filtered = []; for (const fixture of fixtures) { const fixtureVariants = 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 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(`Delete SELECT refers to tables without fixture coverage: ${missingTables.join(', ')}.`); } } static collectReferencedTables(query) { const collector = new TableSourceCollector(false); const sources = collector.collect(query); const normalized = new Set(); for (const source of sources) { for (const variant of tableNameVariants(source.getSourceName())) { normalized.add(variant); } } return normalized; } static collectCteNamesFromWithClause(withClause) { 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 tableNameVariants(table.getSourceAliasName())) { names.add(variant); } } return names; } static getMissingFixtureTables(referencedTables, fixtureMap) { const missing = []; for (const table of referencedTables) { const covered = tableNameVariants(table).some((variant) => fixtureMap.has(variant)); if (!covered) { missing.push(table); } } return missing; } static mergeWithClause(original, fixtureCtes) { var _a; 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 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(); } } DeleteResultSelectConverter.DEFAULT_MISSING_FIXTURE_STRATEGY = 'error'; //# sourceMappingURL=DeleteResultSelectConverter.js.map