rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
310 lines • 15.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MergeResultSelectConverter = void 0;
const Clause_1 = require("../models/Clause");
const MergeQuery_1 = require("../models/MergeQuery");
const ValueComponent_1 = require("../models/ValueComponent");
const SelectQuery_1 = require("../models/SelectQuery");
const FixtureCteBuilder_1 = require("./FixtureCteBuilder");
const TableSourceCollector_1 = require("./TableSourceCollector");
const SelectQueryWithClauseHelper_1 = require("../utils/SelectQueryWithClauseHelper");
class MergeResultSelectConverter {
/**
* Converts a MERGE query into a SELECT that counts or models the rows affected by each action.
*/
static toSelectQuery(mergeQuery, options) {
var _a, _b, _c;
// Build individual SELECTs for each WHEN clause so the row count can include every affected path.
const actionSelects = this.buildActionSelects(mergeQuery);
if (actionSelects.length === 0) {
throw new Error('MERGE query must include at least one action that affects rows.');
}
// Combine the individual action selects into one union so the COUNT(*) can inspect all of them.
const unionSource = this.combineSelects(actionSelects);
const derivedSource = new Clause_1.SourceExpression(new Clause_1.SubQuerySource(unionSource), new Clause_1.SourceAliasExpression('__merge_action_rows', null));
// Wrap the union in a derived table so the outer query can aggregate a single row count.
const finalSelect = new SelectQuery_1.SimpleSelectQuery({
selectClause: this.buildCountSelectClause(),
fromClause: new Clause_1.FromClause(derivedSource, null)
});
// Prepare fixture metadata before verifying coverage.
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 nativeWithClause = (_c = mergeQuery.withClause) !== null && _c !== void 0 ? _c : null;
const referencedTables = this.collectPhysicalTableReferences(unionSource, nativeWithClause);
const cteNames = this.collectCteNamesFromWithClause(nativeWithClause);
const targetName = this.normalizeIdentifier(this.extractTargetTableName(mergeQuery.target));
if (!cteNames.has(targetName)) {
referencedTables.add(targetName);
}
// Ensure every referenced physical table is backed by a fixture when required.
this.ensureFixtureCoverage(referencedTables, fixtureMap, missingStrategy);
// Merge fixture CTEs ahead of any original MERGE WITH clause definitions.
const filteredFixtures = this.filterFixtureTablesForReferences(fixtureTables, referencedTables);
const fixtureCtes = this.buildFixtureCtes(filteredFixtures);
const combinedWithClause = this.mergeWithClause(nativeWithClause, fixtureCtes);
SelectQueryWithClauseHelper_1.SelectQueryWithClauseHelper.setWithClause(finalSelect, combinedWithClause);
return finalSelect;
}
static buildActionSelects(mergeQuery) {
const selects = [];
// Translate each WHEN clause into a row-producing SELECT when it represents an actual change.
for (const clause of mergeQuery.whenClauses) {
const selectQuery = this.buildSelectForClause(mergeQuery, clause);
if (selectQuery) {
selects.push(selectQuery);
}
}
return selects;
}
static buildSelectForClause(mergeQuery, clause) {
switch (clause.matchType) {
case 'matched':
return this.buildMatchedSelect(mergeQuery, clause);
case 'not_matched':
case 'not_matched_by_target':
return this.buildNotMatchedSelect(mergeQuery, clause);
case 'not_matched_by_source':
return this.buildNotMatchedBySourceSelect(mergeQuery, clause);
default:
return null;
}
}
static buildMatchedSelect(mergeQuery, clause) {
const action = clause.action;
if (action instanceof MergeQuery_1.MergeDoNothingAction) {
return null;
}
if (!(action instanceof MergeQuery_1.MergeUpdateAction) && !(action instanceof MergeQuery_1.MergeDeleteAction)) {
return null;
}
// Match target rows with their source counterparts via the MERGE ON predicate.
const joinClause = new Clause_1.JoinClause('inner join', mergeQuery.source, new Clause_1.JoinOnClause(mergeQuery.onCondition), false);
// Apply any additional WHEN/WHERE filters tied to this action.
const combinedPredicate = this.combineConditions([
clause.condition,
this.buildActionWhereClause(action)
]);
const whereClause = combinedPredicate ? new Clause_1.WhereClause(combinedPredicate) : null;
return new SelectQuery_1.SimpleSelectQuery({
selectClause: this.buildLiteralSelectClause(),
fromClause: new Clause_1.FromClause(mergeQuery.target, [joinClause]),
whereClause
});
}
static buildNotMatchedSelect(mergeQuery, clause) {
if (!(clause.action instanceof MergeQuery_1.MergeInsertAction)) {
return null;
}
// Select source rows that lack any matching target record using NOT EXISTS semantics.
const notExistsExpression = this.buildNotExistsExpression(mergeQuery.target, mergeQuery.onCondition);
const combinedPredicate = this.combineConditions([notExistsExpression, clause.condition]);
const whereClause = combinedPredicate ? new Clause_1.WhereClause(combinedPredicate) : null;
return new SelectQuery_1.SimpleSelectQuery({
selectClause: this.buildLiteralSelectClause(),
fromClause: new Clause_1.FromClause(mergeQuery.source, null),
whereClause
});
}
static buildNotMatchedBySourceSelect(mergeQuery, clause) {
const action = clause.action;
if (!(action instanceof MergeQuery_1.MergeDeleteAction)) {
return null;
}
// Select target rows that are orphaned by the source to emulate delete actions.
const notExistsExpression = this.buildNotExistsExpression(mergeQuery.source, mergeQuery.onCondition);
const combinedPredicate = this.combineConditions([
notExistsExpression,
clause.condition,
this.buildActionWhereClause(action)
]);
const whereClause = combinedPredicate ? new Clause_1.WhereClause(combinedPredicate) : null;
return new SelectQuery_1.SimpleSelectQuery({
selectClause: this.buildLiteralSelectClause(),
fromClause: new Clause_1.FromClause(mergeQuery.target, null),
whereClause
});
}
static buildNotExistsExpression(sourceReference, predicate) {
// Build an EXISTS subquery that can be negated to detect missing matches.
const existsSelect = new SelectQuery_1.SimpleSelectQuery({
selectClause: this.buildLiteralSelectClause(),
fromClause: new Clause_1.FromClause(sourceReference, null),
whereClause: new Clause_1.WhereClause(predicate)
});
const existsExpression = new ValueComponent_1.UnaryExpression('exists', new ValueComponent_1.InlineQuery(existsSelect));
return new ValueComponent_1.UnaryExpression('not', existsExpression);
}
static buildActionWhereClause(action) {
var _a, _b;
return (_b = (_a = action.whereClause) === null || _a === void 0 ? void 0 : _a.condition) !== null && _b !== void 0 ? _b : null;
}
// Combine additional predicates into a single AND expression for filtering.
static combineConditions(predicates) {
const values = predicates.filter((predicate) => Boolean(predicate));
if (values.length === 0) {
return null;
}
return values.reduce((acc, value) => {
if (!acc) {
return value;
}
return new ValueComponent_1.BinaryExpression(acc, 'and', value);
}, null);
}
// Combine all action queries via UNION ALL so the count can see every simulated row.
static combineSelects(selects) {
if (selects.length === 1) {
return selects[0];
}
let combined = new SelectQuery_1.BinarySelectQuery(selects[0], 'union all', selects[1]);
for (let i = 2; i < selects.length; i++) {
combined = combined.unionAll(selects[i]);
}
return combined;
}
// Build the simple SELECT clause that yields one row per matched action.
static buildLiteralSelectClause() {
return new Clause_1.SelectClause([new Clause_1.SelectItem(new ValueComponent_1.LiteralValue(1))]);
}
// Summarize the merged action stream by counting every row that survived the union.
static buildCountSelectClause() {
const countFunction = new ValueComponent_1.FunctionCall(null, 'count', new ValueComponent_1.RawString('*'), null);
const selectItem = new Clause_1.SelectItem(countFunction, 'count');
return new Clause_1.SelectClause([selectItem]);
}
static buildFixtureCtes(fixtures) {
if (!fixtures || fixtures.length === 0) {
return [];
}
return FixtureCteBuilder_1.FixtureCteBuilder.buildFixtures(fixtures);
}
static collectPhysicalTableReferences(query, withClause) {
const referencedTables = this.collectReferencedTables(query);
const ignoredTables = this.collectCteNamesFromWithClause(withClause);
const tablesToShadow = new Set();
// Retain only tables that are not defined via WITH clauses so fixtures shadow physical sources.
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 = [];
// Keep fixtures only for tables that actually appear in the converted SELECT.
for (const fixture of fixtures) {
if (referencedTables.has(this.normalizeIdentifier(fixture.tableName))) {
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 extractTargetTableName(target) {
const datasource = target.datasource;
if (datasource instanceof Clause_1.TableSource) {
return datasource.getSourceName();
}
throw new Error('Merge target must be a table source for conversion.');
}
static buildFixtureTableMap(fixtures) {
const map = new Map();
for (const fixture of fixtures) {
map.set(this.normalizeIdentifier(fixture.tableName), fixture);
}
return map;
}
static ensureFixtureCoverage(referencedTables, fixtureMap, strategy) {
if (referencedTables.size === 0) {
return;
}
// Compare the referenced tables against the fixtures that were supplied.
const missingTables = this.getMissingFixtureTables(referencedTables, fixtureMap);
if (missingTables.length === 0) {
return;
}
if (strategy === 'error') {
throw new Error(`Merge SELECT refers to tables without fixture coverage: ${missingTables.join(', ')}.`);
}
}
// Use the collector to track every concrete table source referenced by the SELECT.
static collectReferencedTables(query) {
const collector = new TableSourceCollector_1.TableSourceCollector(false);
const sources = collector.collect(query);
const normalized = new Set();
for (const source of sources) {
normalized.add(this.normalizeIdentifier(source.getSourceName()));
}
return normalized;
}
// Track CTE aliases so those names are ignored when validating fixtures.
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) {
names.add(this.normalizeIdentifier(table.getSourceAliasName()));
}
return names;
}
// Return every referenced table that lacks an overriding fixture definition.
static getMissingFixtureTables(referencedTables, fixtureMap) {
const missing = [];
for (const table of referencedTables) {
if (!fixtureMap.has(table)) {
missing.push(table);
}
}
return missing;
}
// Prepend fixture CTEs ahead of the existing WITH clause so they shadow real tables.
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 Clause_1.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();
}
}
exports.MergeResultSelectConverter = MergeResultSelectConverter;
MergeResultSelectConverter.DEFAULT_MISSING_FIXTURE_STRATEGY = 'error';
//# sourceMappingURL=MergeResultSelectConverter.js.map