rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
230 lines • 10.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpstreamSelectQueryFinder = void 0;
const SelectQuery_1 = require("../models/SelectQuery");
const Clause_1 = require("../models/Clause");
const InsertQuery_1 = require("../models/InsertQuery");
const UpdateQuery_1 = require("../models/UpdateQuery");
const DeleteQuery_1 = require("../models/DeleteQuery");
const MergeQuery_1 = require("../models/MergeQuery");
const ValueComponent_1 = require("../models/ValueComponent");
const SelectableColumnCollector_1 = require("./SelectableColumnCollector");
const CTECollector_1 = require("./CTECollector");
/**
* UpstreamSelectQueryFinder searches upstream queries for the specified columns.
* If a query (including its upstream CTEs or subqueries) contains all columns,
* it returns the highest such SelectQuery. Otherwise, it searches downstream.
*
* For BinarySelectQuery (UNION/INTERSECT/EXCEPT), this finder processes each branch
* independently, as SelectableColumnCollector is designed for SimpleSelectQuery only.
* This approach ensures accurate column detection within individual SELECT branches
* while maintaining compatibility with compound query structures.
*/
class UpstreamSelectQueryFinder {
constructor(tableColumnResolver, options) {
this.options = options || {};
this.tableColumnResolver = tableColumnResolver;
// Pass the tableColumnResolver instead of options to fix type mismatch.
this.columnCollector = new SelectableColumnCollector_1.SelectableColumnCollector(this.tableColumnResolver, false, // includeWildCard
SelectableColumnCollector_1.DuplicateDetectionMode.FullName, // Use FullName to preserve JOIN table columns
{ upstream: true } // Enable upstream collection for qualified name resolution
);
}
/**
* Finds the highest SelectQuery containing all specified columns.
* @param query The root SelectQuery to search.
* @param columnNames A column name or array of column names to check for.
* @returns An array of SelectQuery objects, or an empty array if not found.
*/
find(query, columnNames) {
// Normalize columnNames to array
const namesArray = typeof columnNames === 'string' ? [columnNames] : columnNames;
// Use CTECollector to collect CTEs from the root query only once and reuse
const cteCollector = new CTECollector_1.CTECollector();
const ctes = cteCollector.collect(query);
const cteMap = new Map();
for (const cte of ctes) {
cteMap.set(cte.getSourceAliasName(), cte);
}
return this.findUpstream(query, namesArray, cteMap);
}
handleTableSource(src, columnNames, cteMap) {
// Handles the logic for TableSource in findUpstream
const cte = cteMap.get(src.table.name);
if (cte) {
// Remove the current CTE name from the map to prevent infinite recursion
const nextCteMap = new Map(cteMap);
nextCteMap.delete(src.table.name);
if (!this.isSelectQuery(cte.query)) {
return null;
}
const result = this.findUpstream(cte.query, columnNames, nextCteMap);
if (result.length === 0) {
return null;
}
return result;
}
return null;
}
handleSubQuerySource(src, columnNames, cteMap) {
// Handles the logic for SubQuerySource in findUpstream
const result = this.findUpstream(src.query, columnNames, cteMap);
if (result.length === 0) {
return null;
}
return result;
}
/**
* Processes all source branches in a FROM clause and checks if all upstream queries contain the specified columns.
* Returns a flat array of SelectQuery if all branches are valid, otherwise null.
*/
processFromClauseBranches(fromClause, columnNames, cteMap) {
const sources = fromClause.getSources();
if (sources.length === 0)
return null;
let allBranchResults = [];
let allBranchesOk = true;
let validBranchCount = 0; // Count only filterable branches
for (const sourceExpr of sources) {
const src = sourceExpr.datasource;
let branchResult = null;
if (src instanceof Clause_1.TableSource) {
branchResult = this.handleTableSource(src, columnNames, cteMap);
validBranchCount++;
}
else if (src instanceof Clause_1.SubQuerySource) {
branchResult = this.handleSubQuerySource(src, columnNames, cteMap);
validBranchCount++;
}
else if (src instanceof SelectQuery_1.ValuesQuery) {
// Skip ValuesQuery: not filterable, do not count as a valid branch
continue;
}
else {
allBranchesOk = false;
break;
}
// If the branch result is null,
// it means it didn't find the required columns in this branch
if (branchResult === null) {
allBranchesOk = false;
break;
}
allBranchResults.push(branchResult);
}
// Check if all valid (filterable) branches are valid and contain the required columns
if (allBranchesOk && allBranchResults.length === validBranchCount) {
return allBranchResults.flat();
}
return null;
}
findUpstream(query, columnNames, cteMap) {
if (query instanceof SelectQuery_1.SimpleSelectQuery) {
// First, try to find upstream queries from FROM clause
const fromClause = query.fromClause;
if (fromClause) {
const branchResult = this.processFromClauseBranches(fromClause, columnNames, cteMap);
if (branchResult && branchResult.length > 0) {
return branchResult;
}
}
// If no upstream queries found, check if current query contains all columns
const columns = this.columnCollector.collect(query).map(col => col.name);
// Collect columns defined in CTEs as well
const cteColumns = this.collectCTEColumns(query, cteMap);
const allColumns = [...columns, ...cteColumns];
const normalize = (s) => this.options.ignoreCaseAndUnderscore ? s.toLowerCase().replace(/_/g, '') : s;
// Normalize both the columns and the required names for comparison.
const hasAll = columnNames.every(name => allColumns.some(col => normalize(col) === normalize(name)));
if (hasAll) {
return [query];
}
return [];
}
else if (query instanceof SelectQuery_1.BinarySelectQuery) {
// Process BinarySelectQuery by decomposing into individual branches.
// SelectableColumnCollector is designed for SimpleSelectQuery only,
// so we handle UNION/INTERSECT/EXCEPT by processing left and right branches separately.
const left = this.findUpstream(query.left, columnNames, cteMap);
const right = this.findUpstream(query.right, columnNames, cteMap);
return [...left, ...right];
}
return [];
}
/**
* Collects columns defined in CTEs
*/
collectCTEColumns(query, cteMap) {
const cteColumns = [];
// If WITH clause exists, collect columns defined in CTEs
if (query.withClause) {
for (const cte of query.withClause.tables) {
// Collect columns from CTE query
const columns = this.collectColumnsFromCteQuery(cte.query);
cteColumns.push(...columns);
}
}
return cteColumns;
}
/**
* Recursively collects columns from SelectQuery
*/
collectColumnsFromCteQuery(query) {
if (!this.isSelectQuery(query)) {
return this.collectColumnsFromReturning(query);
}
return this.collectColumnsFromSelectQuery(query);
}
collectColumnsFromSelectQuery(query) {
if (query instanceof SelectQuery_1.SimpleSelectQuery) {
try {
return this.columnCollector.collect(query).map(col => col.name);
}
catch (error) {
// Return empty array if SelectableColumnCollector fails
console.warn('Failed to collect columns from SimpleSelectQuery:', error);
return [];
}
}
else if (query instanceof SelectQuery_1.BinarySelectQuery) {
// For BinarySelectQuery (UNION etc.), get column names from the left query
// In UNION statements, left and right must have matching column count/types, so left side is sufficient
return this.collectColumnsFromSelectQuery(query.left);
}
return [];
}
collectColumnsFromReturning(query) {
if (query instanceof InsertQuery_1.InsertQuery || query instanceof UpdateQuery_1.UpdateQuery || query instanceof DeleteQuery_1.DeleteQuery || query instanceof MergeQuery_1.MergeQuery) {
return this.extractReturningColumns(query.returningClause);
}
return [];
}
extractReturningColumns(returningClause) {
var _a, _b;
if (!returningClause) {
return [];
}
const columns = [];
for (const item of returningClause.items) {
const name = (_b = (_a = item.identifier) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : this.extractColumnName(item);
if (name) {
columns.push(name);
}
}
return columns;
}
extractColumnName(item) {
if (item.identifier) {
return item.identifier.name;
}
if (item.value instanceof ValueComponent_1.ColumnReference) {
return item.value.column.name;
}
return null;
}
isSelectQuery(query) {
return '__selectQueryType' in query && query.__selectQueryType === 'SelectQuery';
}
}
exports.UpstreamSelectQueryFinder = UpstreamSelectQueryFinder;
//# sourceMappingURL=UpstreamSelectQueryFinder.js.map