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