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
JavaScript
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