UNPKG

rawsql-ts

Version:

High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

333 lines 12.1 kB
import { BinaryExpression, ColumnReference } from '../models/ValueComponent'; import { BinarySelectQuery, SimpleSelectQuery } from '../models/SelectQuery'; import { ColumnReferenceCollector } from './ColumnReferenceCollector'; import { CTETableReferenceCollector } from './CTETableReferenceCollector'; import { JoinOnClause, TableSource } from '../models/Clause'; const NAMESPACE_SEPARATOR = '|'; const normalizeIdentifier = (input) => { var _a; const value = (_a = input === null || input === void 0 ? void 0 : input.trim()) !== null && _a !== void 0 ? _a : ''; return value === '' ? '' : value.toLowerCase(); }; const normalizeColumnSetKey = (columns) => { return columns .map(column => normalizeIdentifier(column)) .filter(Boolean) .sort() .join(NAMESPACE_SEPARATOR); }; const buildSchemaMap = (schemaInfo) => { const map = new Map(); // Normalize table names and precompute reusable sets for column lookups. for (const table of schemaInfo) { const normalizedName = normalizeIdentifier(table.name); if (!normalizedName) { continue; } const columnSet = new Set(table.columns.map(normalizeIdentifier).filter(Boolean)); const uniqueSetKeys = new Set(); for (const uniqueKey of table.uniqueKeys) { const normalizedKey = normalizeColumnSetKey(uniqueKey); if (normalizedKey) { uniqueSetKeys.add(normalizedKey); } } if (columnSet.size === 0 && uniqueSetKeys.size === 0) { continue; } map.set(normalizedName, { columnSet, uniqueSetKeys }); } return map; }; const collectReferenceMetadata = (query) => { var _a, _b, _c; const collector = new ColumnReferenceCollector(); const namespaceCounts = new Map(); const unqualifiedColumns = new Set(); // Track every column reference in the query so we can detect aliases used outside their JOINs. for (const ref of collector.collect(query)) { const namespace = normalizeIdentifier(ref.getNamespace()); if (namespace) { namespaceCounts.set(namespace, ((_a = namespaceCounts.get(namespace)) !== null && _a !== void 0 ? _a : 0) + 1); } else { const column = normalizeIdentifier(ref.column.name); if (column) { unqualifiedColumns.add(column); } } } const joinConditionCounts = new Map(); if ((_b = query.fromClause) === null || _b === void 0 ? void 0 : _b.joins) { for (const join of query.fromClause.joins) { const counts = new Map(); if (join.condition && join.condition instanceof JoinOnClause) { const joinCollector = new ColumnReferenceCollector(); for (const ref of joinCollector.collect(join.condition.condition)) { const namespace = normalizeIdentifier(ref.getNamespace()); if (namespace) { counts.set(namespace, ((_c = counts.get(namespace)) !== null && _c !== void 0 ? _c : 0) + 1); } } } joinConditionCounts.set(join, counts); } } return { namespaceCounts, unqualifiedColumns, joinConditionCounts }; }; const isLeftJoin = (join) => { return join.joinType.value.toLowerCase().includes('left'); }; const getJoinIdentifiers = (join) => { const identifiers = new Set(); const alias = normalizeIdentifier(join.source.getAliasName()); if (alias) { identifiers.add(alias); } if (join.source.datasource instanceof TableSource) { const rawName = join.source.datasource.getSourceName(); if (rawName) { identifiers.add(normalizeIdentifier(rawName)); } const shortName = normalizeIdentifier(join.source.datasource.table.name); if (shortName) { identifiers.add(shortName); } } return [...identifiers]; }; const hasExternalReferences = (identifiers, metadata, join) => { var _a, _b, _c; const local = (_a = metadata.joinConditionCounts.get(join)) !== null && _a !== void 0 ? _a : new Map(); for (const identifier of identifiers) { const total = (_b = metadata.namespaceCounts.get(identifier)) !== null && _b !== void 0 ? _b : 0; const localCount = (_c = local.get(identifier)) !== null && _c !== void 0 ? _c : 0; if (total - localCount > 0) { return true; } } return false; }; const getJoinColumnInfo = (join, identifiers) => { if (!(join.condition instanceof JoinOnClause)) { return null; } const expression = join.condition.condition; if (!(expression instanceof BinaryExpression)) { return null; } const operatorValue = expression.operator.value.trim().toLowerCase(); if (operatorValue !== '=') { return null; } const resolveColumn = (component) => { return component instanceof ColumnReference ? component : null; }; const leftRef = resolveColumn(expression.left); const rightRef = resolveColumn(expression.right); if (!leftRef || !rightRef) { return null; } const normalizedLeftNamespace = normalizeIdentifier(leftRef.getNamespace()); const normalizedRightNamespace = normalizeIdentifier(rightRef.getNamespace()); if (identifiers.has(normalizedLeftNamespace)) { return normalizeIdentifier(leftRef.column.name); } if (identifiers.has(normalizedRightNamespace)) { return normalizeIdentifier(rightRef.column.name); } return null; }; const shouldRemoveJoin = (join, schemaMap, metadata) => { if (!isLeftJoin(join) || join.lateral) { return false; } if (!(join.source.datasource instanceof TableSource)) { return false; } const candidates = [ normalizeIdentifier(join.source.datasource.getSourceName()), normalizeIdentifier(join.source.datasource.table.name) ].filter(Boolean); let tableInfo; for (const candidate of candidates) { const info = schemaMap.get(candidate); if (info) { tableInfo = info; break; } } if (!tableInfo) { return false; } const identifiers = new Set(getJoinIdentifiers(join)); if (identifiers.size === 0) { return false; } if (hasExternalReferences([...identifiers], metadata, join)) { return false; } const joinColumn = getJoinColumnInfo(join, identifiers); if (!joinColumn) { return false; } if (metadata.unqualifiedColumns.has(joinColumn)) { return false; } if (tableInfo.columnSet.size > 0 && !tableInfo.columnSet.has(joinColumn)) { return false; } const uniqueKey = normalizeColumnSetKey([joinColumn]); if (!tableInfo.uniqueSetKeys.has(uniqueKey)) { return false; } return true; }; const optimizeSimpleQuery = (query, schemaMap) => { var _a, _b; if (!((_b = (_a = query.fromClause) === null || _a === void 0 ? void 0 : _a.joins) === null || _b === void 0 ? void 0 : _b.length)) { return false; } const metadata = collectReferenceMetadata(query); const retainedJoins = []; let removed = false; for (const join of query.fromClause.joins) { if (shouldRemoveJoin(join, schemaMap, metadata)) { removed = true; continue; } retainedJoins.push(join); } query.fromClause.joins = retainedJoins.length > 0 ? retainedJoins : null; return removed; }; const traverseSelectQuery = (query, schemaMap) => { if (query instanceof SimpleSelectQuery) { return optimizeSimpleQuery(query, schemaMap); } if (query instanceof BinarySelectQuery) { const leftChanged = traverseSelectQuery(query.left, schemaMap); const rightChanged = traverseSelectQuery(query.right, schemaMap); return leftChanged || rightChanged; } return false; }; const optimizeUnusedLeftJoinsOnce = (query, schemaMap) => { if (schemaMap.size === 0) { return false; } return traverseSelectQuery(query, schemaMap); }; /** * Removes LEFT JOIN clauses from the provided query when AST references prove the join target is unused and schema metadata certifies the join column is unique. */ export const optimizeUnusedLeftJoins = (query, schemaInfo) => { optimizeUnusedLeftJoinsOnce(query, buildSchemaMap(schemaInfo)); return query; }; /** * Applies the unused left join optimizer until no further joins can be trimmed, ensuring cascading removals stabilize. */ export const optimizeUnusedLeftJoinsToFixedPoint = (query, schemaInfo) => { const schemaMap = buildSchemaMap(schemaInfo); let changed = true; while (changed) { changed = optimizeUnusedLeftJoinsOnce(query, schemaMap); } return query; }; const collectTableSourceNames = (component) => { const collector = new CTETableReferenceCollector(); const names = new Set(); for (const source of collector.collect(component)) { const normalizedName = normalizeIdentifier(source.table.name); if (normalizedName) { names.add(normalizedName); } } return names; }; const isReferencedByOthers = (cteName, mainReferences, cteReferenceMap) => { if (mainReferences.has(cteName)) { return true; } for (const [otherName, references] of cteReferenceMap) { if (otherName === cteName) { continue; } if (references.has(cteName)) { return true; } } return false; }; const optimizeSimpleQueryCtes = (query) => { const withClause = query.withClause; if (!withClause || withClause.recursive || withClause.tables.length === 0) { return false; } const mainReferences = collectTableSourceNames(query); const cteReferenceMap = new Map(); for (const table of withClause.tables) { const normalizedName = normalizeIdentifier(table.aliasExpression.table.name); if (!normalizedName) { continue; } cteReferenceMap.set(normalizedName, collectTableSourceNames(table.query)); } const removableNames = []; for (const table of withClause.tables) { const normalizedName = normalizeIdentifier(table.aliasExpression.table.name); if (!normalizedName) { continue; } const body = table.query; if (!(body instanceof SimpleSelectQuery) && !(body instanceof BinarySelectQuery)) { continue; } if (isReferencedByOthers(normalizedName, mainReferences, cteReferenceMap)) { continue; } removableNames.push(normalizedName); } if (removableNames.length === 0) { return false; } // Use the public removal API so the internal CTE cache stays consistent. for (const name of removableNames) { query.removeCTE(name); } return true; }; const optimizeCtesInSelectQuery = (query) => { if (query instanceof SimpleSelectQuery) { return optimizeSimpleQueryCtes(query); } if (query instanceof BinarySelectQuery) { const leftChanged = optimizeCtesInSelectQuery(query.left); const rightChanged = optimizeCtesInSelectQuery(query.right); return leftChanged || rightChanged; } return false; }; const optimizeUnusedCtesOnce = (query) => { return optimizeCtesInSelectQuery(query); }; /** * Removes unused SELECT-only CTEs from the query when AST references confirm they are never consumed. */ export const optimizeUnusedCtes = (query) => { optimizeUnusedCtesOnce(query); return query; }; /** * Repeatedly prunes unused CTEs until a fixed point is reached so chained removals complete deterministically. */ export const optimizeUnusedCtesToFixedPoint = (query) => { let changed = true; while (changed) { changed = optimizeUnusedCtesOnce(query); } return query; }; //# sourceMappingURL=OptimizeUnusedLeftJoins.js.map