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