rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
355 lines • 17 kB
JavaScript
import { FromClause, JoinClause, SelectClause, SelectItem, TableSource, WithClause } from '../models/Clause';
import { SimpleSelectQuery } from '../models/SelectQuery';
import { ColumnReference, FunctionCall, IdentifierString, 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';
export 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 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.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);
}
}
// 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.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 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 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 SelectItem(expression, column.name);
});
}
static buildUpdateReturningSelectItem(item, setExpressions, targetAlias, tableDefinition) {
// Rewrite the item expression so column references honor SET overrides.
const expression = rewriteValueComponentWithColumnResolver(item.value, (column) => this.buildUpdateColumnExpression(column, setExpressions, targetAlias, tableDefinition));
const alias = this.getReturningAlias(item);
return new 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 ColumnReference(targetAlias, columnName);
}
static getColumnReferenceName(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 buildCountSelectClause() {
// Count rows when the UPDATE does not expose RETURNING output.
const countFunction = new FunctionCall(null, 'count', new RawString('*'), null);
const selectItem = new SelectItem(countFunction, 'count');
return new SelectClause([selectItem]);
}
static buildFromClause(targetSource, fromClause) {
if (!fromClause) {
return new FromClause(targetSource, null);
}
const joins = [];
// Cross join any explicit FROM sources so their columns remain accessible.
joins.push(new JoinClause('cross join', fromClause.source, null, false));
if (fromClause.joins) {
joins.push(...fromClause.joins);
}
return new 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(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) {
// Normalize registry keys so lookups ignore casing.
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(updateClause) {
const datasource = updateClause.source.datasource;
if (datasource instanceof 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 RawString) {
return columnComponent.value;
}
return columnComponent.name;
}
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();
// 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 = 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 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(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) {
// 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 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 = 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 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();
}
}
UpdateResultSelectConverter.DEFAULT_MISSING_FIXTURE_STRATEGY = 'error';
//# sourceMappingURL=UpdateResultSelectConverter.js.map