UNPKG

rawsql-ts

Version:

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

585 lines 28.5 kB
import { CommonTable, FromClause, SelectClause, SelectItem, SourceAliasExpression, SourceExpression, TableSource, WithClause } from '../models/Clause'; import { BinarySelectQuery, SimpleSelectQuery, ValuesQuery } from '../models/SelectQuery'; import { InsertQuerySelectValuesConverter } from './InsertQuerySelectValuesConverter'; import { ArrayExpression, ArrayIndexExpression, ArrayQueryExpression, ArraySliceExpression, BinaryExpression, BetweenExpression, CaseExpression, CaseKeyValuePair, CastExpression, ColumnReference, FunctionCall, IdentifierString, InlineQuery, LiteralValue, ParenExpression, RawString, SwitchCaseArgument, TupleExpression, TypeValue, UnaryExpression, ValueList, } from '../models/ValueComponent'; import { ValueParser } from '../parsers/ValueParser'; import { TableSourceCollector } from './TableSourceCollector'; import { FixtureCteBuilder } from './FixtureCteBuilder'; import { SelectQueryWithClauseHelper } from "../utils/SelectQueryWithClauseHelper"; import { rewriteValueComponentWithColumnResolver } from '../utils/ValueComponentRewriter'; import { tableNameVariants } from '../utils/TableNameUtils'; export class InsertResultSelectConverter { /** * Converts an INSERT ... SELECT/VALUES query into a SELECT that mirrors its RETURNING output * (or a count(*) when RETURNING is absent). */ static toSelectQuery(insertQuery, options) { var _a, _b; const preparedInsert = this.prepareInsertQuery(insertQuery); const sourceQuery = preparedInsert.selectQuery; if (!sourceQuery) { throw new Error('Cannot convert INSERT query without a data source.'); } const sourceWithClause = SelectQueryWithClauseHelper.detachWithClause(sourceQuery); const targetTableName = this.extractTargetTableName(preparedInsert.insertClause); const tableDefinition = this.resolveTableDefinition(targetTableName, options); const fixtureTables = (_a = options === null || options === void 0 ? void 0 : options.fixtureTables) !== null && _a !== void 0 ? _a : []; const fixtureMap = this.buildFixtureTableMap(fixtureTables); const missingStrategy = (_b = options === null || options === void 0 ? void 0 : options.missingFixtureStrategy) !== null && _b !== void 0 ? _b : this.DEFAULT_MISSING_FIXTURE_STRATEGY; const referencedTables = this.collectPhysicalTableReferences(sourceQuery, sourceWithClause); this.ensureFixtureCoverage(referencedTables, fixtureMap, missingStrategy); const filteredFixtures = this.filterFixtureTablesForReferences(fixtureTables, referencedTables); const insertColumnNames = this.resolveInsertColumns(preparedInsert.insertClause, sourceQuery, tableDefinition); const selectColumnCount = this.getSelectColumnCount(sourceQuery); if (insertColumnNames.length !== selectColumnCount) { throw new Error('Insert column count does not match SELECT output columns.'); } const columnMetadataMap = this.buildColumnMetadata(insertColumnNames, tableDefinition); this.assertRequiredColumns(columnMetadataMap, tableDefinition); this.applyColumnCasts(sourceQuery, insertColumnNames, columnMetadataMap); const fixtureCtes = this.buildFixtureCtes(filteredFixtures); const cteName = this.generateUniqueCteName(sourceWithClause, fixtureCtes); const cteAlias = new SourceAliasExpression(cteName, insertColumnNames); const insertedRowsCte = new CommonTable(sourceQuery, cteAlias, null); const withClause = this.buildWithClause(sourceWithClause, fixtureCtes, insertedRowsCte); if (!preparedInsert.returningClause) { return this.buildCountSelect(withClause, cteName); } const selectItems = this.buildReturningSelectItems(preparedInsert.returningClause, tableDefinition, insertColumnNames, columnMetadataMap, cteName); const fromExpr = new SourceExpression(new TableSource(null, cteName), null); const fromClause = new FromClause(fromExpr, null); return new SimpleSelectQuery({ withClause, selectClause: new SelectClause(selectItems), fromClause }); } static prepareInsertQuery(insertQuery) { // Values-based inserts need to be rewritten into INSERT ... SELECT before further processing. if (insertQuery.selectQuery instanceof ValuesQuery) { return InsertQuerySelectValuesConverter.toSelectUnion(insertQuery); } return insertQuery; } static extractTargetTableName(insertClause) { const datasource = insertClause.source.datasource; if (datasource instanceof TableSource) { return datasource.getSourceName(); } throw new Error('Insert target must be a table source for conversion.'); } static resolveTableDefinition(tableName, options) { // Prefer resolver results but fall back to the registry when the resolver cannot handle the table name. 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 normalizedMap = this.buildTableDefinitionMap(options.tableDefinitions); for (const variant of normalizedVariants) { const definition = normalizedMap.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(); if (!registry) { return map; } for (const definition of Object.values(registry)) { for (const variant of tableNameVariants(definition.name)) { map.set(variant, definition); } } return map; } static resolveInsertColumns(insertClause, selectQuery, tableDefinition) { if (insertClause.columns && insertClause.columns.length > 0) { return insertClause.columns.map((col) => col.name); } // When the column list is omitted we rely on the table definition to infer both names and order. if (!tableDefinition) { throw new Error('Cannot infer INSERT columns without a table definition.'); } const columnNames = tableDefinition.columns.map((col) => col.name); const expectedCount = this.getSelectColumnCount(selectQuery); if (columnNames.length !== expectedCount) { throw new Error('Table definition column count does not match the SELECT output when column list is omitted.'); } return columnNames; } static getSelectColumnCount(selectQuery) { const firstSimple = this.getFirstSimpleSelectQuery(selectQuery); return firstSimple.selectClause.items.length; } static getFirstSimpleSelectQuery(selectQuery) { if (selectQuery instanceof SimpleSelectQuery) { return selectQuery; } if (selectQuery instanceof BinarySelectQuery) { return this.getFirstSimpleSelectQuery(selectQuery.left); } throw new Error('Unsupported select query structure in insert conversion.'); } static buildColumnMetadata(insertColumns, tableDefinition) { // Capture both the provided columns and the broader table definition so we can resolve defaults later. const metadataMap = new Map(); const columnDefinitionMap = tableDefinition ? new Map(tableDefinition.columns.map((col) => [this.normalizeIdentifier(col.name), col])) : null; for (const columnName of insertColumns) { const normalized = this.normalizeIdentifier(columnName); const definition = columnDefinitionMap === null || columnDefinitionMap === void 0 ? void 0 : columnDefinitionMap.get(normalized); metadataMap.set(normalized, { name: columnName, normalized, provided: true, typeName: definition === null || definition === void 0 ? void 0 : definition.typeName, required: definition === null || definition === void 0 ? void 0 : definition.required, defaultValue: this.resolveDefaultValueExpression(definition) }); } if (columnDefinitionMap) { for (const [normalized, definition] of columnDefinitionMap.entries()) { if (!metadataMap.has(normalized)) { metadataMap.set(normalized, { name: definition.name, normalized, provided: false, typeName: definition.typeName, required: definition.required, defaultValue: this.resolveDefaultValueExpression(definition) }); } } } return metadataMap; } static assertRequiredColumns(metadataMap, tableDefinition) { var _a; if (!tableDefinition) { return; } // Ensure every NOT NULL column (without a default) was part of the insert so the transformation stays accurate. const requiredColumns = new Set(tableDefinition.columns .filter((col) => col.required) .map((col) => this.normalizeIdentifier(col.name))); for (const normalized of requiredColumns) { const metadata = metadataMap.get(normalized); if (metadata && metadata.provided) { continue; } if (metadata === null || metadata === void 0 ? void 0 : metadata.defaultValue) { continue; } const columnName = (_a = tableDefinition.columns.find((col) => this.normalizeIdentifier(col.name) === normalized)) === null || _a === void 0 ? void 0 : _a.name; if (columnName) { throw new Error(`Required column '${columnName}' is missing from INSERT, so conversion cannot proceed.`); } } } static buildReturningSelectItems(returning, tableDefinition, insertColumns, columnMetadataMap, cteName) { // Build SelectItems from the parsed RETURNING entries, expanding wildcard specs as needed. const selectItems = []; for (const item of returning.items) { if (this.isWildcardReturningItem(item)) { selectItems.push(...this.expandReturningWildcard(tableDefinition, insertColumns, columnMetadataMap, cteName)); continue; } selectItems.push(this.buildReturningSelectItem(item, columnMetadataMap, cteName)); } return selectItems; } static isWildcardReturningItem(item) { return (item.value instanceof ColumnReference && item.value.column.name === '*'); } static expandReturningWildcard(tableDefinition, insertColumns, columnMetadataMap, cteName) { // Expand RETURNING * into a concrete column list derived from metadata for accurate defaults. const columnNames = tableDefinition ? tableDefinition.columns.map((column) => column.name) : insertColumns.length > 0 ? insertColumns : null; if (!columnNames) { throw new Error('Cannot expand RETURNING * without table definition or column list.'); } return columnNames.map((columnName) => { const metadata = this.getColumnMetadata(columnMetadataMap, columnName); const expression = this.buildColumnExpression(metadata, cteName); return new SelectItem(expression, columnName); }); } static buildReturningSelectItem(item, columnMetadataMap, cteName) { // Rewrite the parsed expression tree so its column references point to the inserted rows. const expression = rewriteValueComponentWithColumnResolver(item.value, (column) => this.buildInsertColumnExpression(column, columnMetadataMap, cteName)); const alias = this.getReturningAlias(item); return new SelectItem(expression, alias); } static buildInsertColumnExpression(column, columnMetadataMap, cteName) { // Use metadata to decide whether the column expression comes from inserted data or a default. const columnName = this.extractColumnName(column); const metadata = this.getColumnMetadata(columnMetadataMap, columnName); return this.buildColumnExpression(metadata, cteName); } static extractColumnName(column) { const nameComponent = column.qualifiedName.name; if (nameComponent instanceof 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 ColumnReference) { return item.value.toString(); } return null; } static getColumnMetadata(metadataMap, columnName) { const normalized = this.normalizeIdentifier(columnName); const metadata = metadataMap.get(normalized); if (!metadata) { throw new Error(`Column '${columnName}' cannot be resolved for RETURNING output.`); } return metadata; } static buildColumnExpression(metadata, cteName) { // Choose either the inserted column reference or the configured default/null expression. let expression; if (metadata.provided) { expression = new ColumnReference(cteName, metadata.name); } else if (metadata.defaultValue) { expression = metadata.defaultValue; } else { expression = new LiteralValue(null); } return expression; } static buildTypeValue(typeName) { var _a, _b; // Split schema-qualified type names so namespaces become TypeValue namespaces. const parts = typeName.split('.'); const namePart = (_b = (_a = parts.pop()) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : typeName.trim(); const namespaces = parts.length > 0 ? parts.map((part) => part.trim()) : null; return new TypeValue(namespaces, new RawString(namePart)); } static collectPhysicalTableReferences(selectQuery, withClause) { const referencedTables = this.collectReferencedTables(selectQuery); const ignoredTables = this.collectCteNamesFromWithClause(withClause); const tablesToShadow = new Set(); // Retain only concrete tables that are not 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 buildFixtureCtes(fixtures) { if (!fixtures || fixtures.length === 0) { return []; } return FixtureCteBuilder.buildFixtures(fixtures); } static filterFixtureTablesForReferences(fixtures, referencedTables) { if (!fixtures.length || referencedTables.size === 0) { return []; } const filtered = []; // Keep fixtures only for the tables that the INSERT actually touches. 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 buildWithClause(original, fixtureCtes, insertedCte) { var _a, _b; // Preserve any existing CTEs while prefixing fixture-based definitions before the simulated inserted rows CTE. const originalTables = (_a = original === null || original === void 0 ? void 0 : original.tables) !== null && _a !== void 0 ? _a : []; const combinedTables = [...fixtureCtes, ...originalTables, insertedCte]; const withClause = new WithClause((_b = original === null || original === void 0 ? void 0 : original.recursive) !== null && _b !== void 0 ? _b : false, combinedTables); withClause.globalComments = (original === null || original === void 0 ? void 0 : original.globalComments) ? [...original.globalComments] : null; withClause.trailingComments = (original === null || original === void 0 ? void 0 : original.trailingComments) ? [...original.trailingComments] : null; return withClause; } static buildCountSelect(withClause, cteName) { // Build a simple tally query that counts the rows produced by the inserted rows CTE. const countItem = new SelectItem(new FunctionCall(null, 'count', new RawString('*'), null), 'count'); const selectClause = new SelectClause([countItem]); const fromExpr = new SourceExpression(new TableSource(null, cteName), null); const fromClause = new FromClause(fromExpr, null); return new SimpleSelectQuery({ withClause, selectClause, fromClause }); } static buildFixtureTableMap(fixtures) { const map = new Map(); if (!fixtures) { return map; } // Normalize table names so lookups are case-insensitive. for (const fixture of fixtures) { for (const variant of tableNameVariants(fixture.tableName)) { map.set(variant, fixture); } } return map; } static ensureFixtureCoverage(referencedTables, fixtureMap, strategy) { if (referencedTables.size === 0) { return; } // Identify any referenced table that lacks a fixture override. const missingTables = this.getMissingFixtureTables(referencedTables, fixtureMap); if (missingTables.length === 0) { return; } if (strategy === 'error') { throw new Error(`Insert SELECT refers to tables without fixture coverage: ${missingTables.join(', ')}.`); } // 'warn' and 'passthrough' intentionally allow the conversion to continue. } static collectReferencedTables(query) { // Scan every part of the query (including subqueries) so fixture coverage validates all referenced tables. const collector = new TableSourceCollector(false); const sources = collector.collect(query); const referenced = new Set(); for (const source of sources) { for (const variant of tableNameVariants(source.getSourceName())) { referenced.add(variant); } } return referenced; } static collectCteNamesFromWithClause(withClause) { const names = new Set(); if (!(withClause === null || withClause === void 0 ? void 0 : withClause.tables)) { return names; } // Normalize alias names before storing so they line up with referenced table normalization. for (const table of withClause.tables) { names.add(this.normalizeIdentifier(table.getSourceAliasName())); } return names; } static addCteNames(usedNames, tables) { if (!tables) { return; } for (const table of tables) { usedNames.add(this.normalizeIdentifier(table.getSourceAliasName())); } } static getMissingFixtureTables(referencedTables, fixtureMap) { // Compare normalized table names against the fixtures that were supplied. const missing = []; for (const table of referencedTables) { const covered = tableNameVariants(table).some((variant) => fixtureMap.has(variant)); if (!covered) { missing.push(table); } } return missing; } static generateUniqueCteName(withClause, fixtureCtes) { const usedNames = new Set(); this.addCteNames(usedNames, fixtureCtes); for (const name of this.collectCteNamesFromWithClause(withClause)) { usedNames.add(name); } let candidate = this.BASE_CTE_NAME; let suffix = 0; while (usedNames.has(this.normalizeIdentifier(candidate))) { suffix += 1; candidate = `${this.BASE_CTE_NAME}_${suffix}`; } return candidate; } static normalizeIdentifier(value) { return value.trim().toLowerCase(); } static parseDefaultValue(def) { try { return ValueParser.parse(def); } catch (error) { throw new Error(`Failed to parse default expression '${def}': ${error instanceof Error ? error.message : String(error)}`); } } static resolveDefaultValueExpression(definition) { if (!(definition === null || definition === void 0 ? void 0 : definition.defaultValue)) { return null; } const defaultValue = definition.defaultValue; if (typeof defaultValue === 'string') { const parsed = this.parseDefaultValue(defaultValue); if (this.referencesSequence(parsed)) { return this.parseDefaultValue('row_number() over ()'); } return parsed; } return defaultValue !== null && defaultValue !== void 0 ? defaultValue : null; } static referencesSequence(component) { if (component instanceof FunctionCall) { if (this.isSequenceFunction(component)) { return true; } if (component.argument && this.referencesSequence(component.argument)) { return true; } } if (component instanceof ValueList) { return component.values.some((value) => this.referencesSequence(value)); } if (component instanceof BinaryExpression) { return this.referencesSequence(component.left) || this.referencesSequence(component.right); } if (component instanceof UnaryExpression) { return this.referencesSequence(component.expression); } if (component instanceof CastExpression) { return this.referencesSequence(component.input); } if (component instanceof ParenExpression) { return this.referencesSequence(component.expression); } if (component instanceof InlineQuery) { return this.referencesSelect(component.selectQuery); } if (component instanceof ArrayExpression) { return this.referencesSequence(component.expression); } if (component instanceof ArrayQueryExpression) { return this.referencesSelect(component.query); } if (component instanceof BetweenExpression) { return (this.referencesSequence(component.expression) || this.referencesSequence(component.lower) || this.referencesSequence(component.upper)); } if (component instanceof ArraySliceExpression) { return (this.referencesSequence(component.array) || (component.startIndex ? this.referencesSequence(component.startIndex) : false) || (component.endIndex ? this.referencesSequence(component.endIndex) : false)); } if (component instanceof ArrayIndexExpression) { return this.referencesSequence(component.array) || this.referencesSequence(component.index); } if (component instanceof SwitchCaseArgument) { for (const pair of component.cases) { if (this.referencesSequence(pair.key) || this.referencesSequence(pair.value)) { return true; } } return component.elseValue ? this.referencesSequence(component.elseValue) : false; } if (component instanceof CaseExpression) { return ((component.condition ? this.referencesSequence(component.condition) : false) || this.referencesSequence(component.switchCase)); } if (component instanceof CaseKeyValuePair) { return (this.referencesSequence(component.key) || this.referencesSequence(component.value)); } if (component instanceof TupleExpression) { return component.values.some((value) => this.referencesSequence(value)); } return false; } static referencesSelect(query) { return false; } static isSequenceFunction(call) { const name = call.qualifiedName.name; const value = name instanceof RawString ? name.value : name.name; return value.toLowerCase() === 'nextval'; } static applyColumnCasts(selectQuery, insertColumns, metadataMap) { if (selectQuery instanceof SimpleSelectQuery) { this.applyColumnCastsToSimple(selectQuery, insertColumns, metadataMap); return; } if (selectQuery instanceof BinarySelectQuery) { this.applyColumnCasts(selectQuery.left, insertColumns, metadataMap); this.applyColumnCasts(selectQuery.right, insertColumns, metadataMap); return; } // ValuesQuery should have been converted to SELECT earlier. throw new Error('Unsupported select query structure for applying column casts.'); } static applyColumnCastsToSimple(simple, insertColumns, metadataMap) { var _a, _b; const items = simple.selectClause.items; for (let i = 0; i < items.length; i++) { const colName = insertColumns[i]; const metadata = metadataMap.get(this.normalizeIdentifier(colName)); if (!metadata || !metadata.typeName) { continue; } const identifier = (_b = (_a = items[i].identifier) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null; const casted = new CastExpression(items[i].value, this.buildTypeValue(metadata.typeName)); const newItem = new SelectItem(casted, identifier); newItem.comments = items[i].comments; newItem.positionedComments = items[i].positionedComments; simple.selectClause.items[i] = newItem; } } } InsertResultSelectConverter.BASE_CTE_NAME = '__inserted_rows'; InsertResultSelectConverter.DEFAULT_MISSING_FIXTURE_STRATEGY = 'error'; //# sourceMappingURL=InsertResultSelectConverter.js.map