UNPKG

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