UNPKG

rawsql-ts

Version:

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

275 lines 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DynamicQueryBuilder = void 0; const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); const SqlSortInjector_1 = require("./SqlSortInjector"); const SqlPaginationInjector_1 = require("./SqlPaginationInjector"); const QueryBuilder_1 = require("./QueryBuilder"); const SqlParameterBinder_1 = require("./SqlParameterBinder"); const ParameterDetector_1 = require("../utils/ParameterDetector"); const ValueComponent_1 = require("../models/ValueComponent"); const OptimizeUnusedLeftJoins_1 = require("./OptimizeUnusedLeftJoins"); const PruneOptionalConditionBranches_1 = require("./PruneOptionalConditionBranches"); /** * DynamicQueryBuilder combines SQL parsing with dynamic condition injection (filters, sorts, paging). * * Key behaviours verified in packages/core/tests/transformers/DynamicQueryBuilder.test.ts: * - Preserves the input SQL when no options are supplied. * - Applies filter, sort, and pagination in a deterministic order. * - Fails fast for removed SQL-result JSON shaping. */ class DynamicQueryBuilder { /** * Creates a new DynamicQueryBuilder instance. * Accepts either the legacy table resolver or an options object that can provide schema metadata. * * @param resolverOrOptions Optional resolver or configuration object */ constructor(resolverOrOptions) { if (typeof resolverOrOptions === "function") { this.tableColumnResolver = resolverOrOptions; } else if (resolverOrOptions) { this.tableColumnResolver = resolverOrOptions.tableColumnResolver; this.defaultSchemaInfo = resolverOrOptions.schemaInfo; } } /** * Builds a SelectQuery from SQL content with dynamic conditions. * This is a pure function that does not perform any I/O operations. * @param sqlContent Raw SQL string to parse and modify * @param options Dynamic conditions to apply (filter, sort, paging) * @returns Modified SelectQuery with all dynamic conditions applied * @example * ```typescript * const builder = new DynamicQueryBuilder(); * const query = builder.buildQuery( * 'SELECT id, name FROM users WHERE active = true', * { * filter: { status: 'premium' }, * sort: { created_at: { desc: true } }, * paging: { page: 2, pageSize: 10 } * } * ); * ``` */ buildQuery(sqlContent, options = {}) { var _a; const removedOptions = options; if ('serialize' in removedOptions || 'jsonb' in removedOptions) { throw new Error("DynamicQueryBuilder SQL-result JSON shaping has been removed. Keep SQL results as rows and use generated AOT mappers so the executed SQL remains debuggable."); } // Parse the base SQL let parsedQuery; try { parsedQuery = SelectQueryParser_1.SelectQueryParser.parse(sqlContent); } catch (error) { throw new Error(`Failed to parse SQL: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Apply dynamic modifications in the correct order let modifiedQuery = parsedQuery; // 1. Bind existing named parameters, but fail fast for legacy runtime filter assembly. if (options.filter && Object.keys(options.filter).length > 0) { const { hardcodedParams, dynamicFilters } = ParameterDetector_1.ParameterDetector.separateFilters(modifiedQuery, options.filter); // Bind hardcoded parameters if any exist if (Object.keys(hardcodedParams).length > 0) { const parameterBinder = new SqlParameterBinder_1.SqlParameterBinder({ requireAllParameters: false }); modifiedQuery = parameterBinder.bind(modifiedQuery, hardcodedParams); } const hasLegacyDynamicFilters = Object.keys(dynamicFilters).length > 0; if (hasLegacyDynamicFilters) { throw new Error("DynamicQueryBuilder no longer injects runtime filter predicates. Use `ashiba query optional add` to author optional filters, `ashiba query optional refresh` to refresh them, and `optionalConditionParameters` at runtime for pruning only."); } } // 2. Apply sorting second (after filtering to sort smaller dataset) if (options.sort && Object.keys(options.sort).length > 0) { const sortInjector = new SqlSortInjector_1.SqlSortInjector(this.tableColumnResolver); // Ensure we have a SimpleSelectQuery for the injector const simpleQuery = QueryBuilder_1.QueryBuilder.buildSimpleQuery(modifiedQuery); modifiedQuery = sortInjector.inject(simpleQuery, options.sort); } // 3. Apply pagination third (after filtering and sorting) if (options.paging) { const { page = 1, pageSize } = options.paging; if (pageSize !== undefined) { const paginationInjector = new SqlPaginationInjector_1.SqlPaginationInjector(); const paginationOptions = { page, pageSize }; // Ensure we have a SimpleSelectQuery for the injector const simpleQuery = QueryBuilder_1.QueryBuilder.buildSimpleQuery(modifiedQuery); modifiedQuery = paginationInjector.inject(simpleQuery, paginationOptions); } } // 4. Apply column projection filters before any optimizer passes. modifiedQuery = this.applyColumnFilters(modifiedQuery, options); // 5. Prune supported truthful optional branches before structural optimizers. const optionalConditionParameters = this.resolveOptionalConditionPruningParameters(options); if (Object.keys(optionalConditionParameters).length > 0) { modifiedQuery = (0, PruneOptionalConditionBranches_1.pruneOptionalConditionBranches)(modifiedQuery, optionalConditionParameters); } // 6. Remove unused LEFT JOINs when asked. const effectiveSchemaInfo = (_a = options.schemaInfo) !== null && _a !== void 0 ? _a : this.defaultSchemaInfo; if (options.removeUnusedLeftJoins && (effectiveSchemaInfo === null || effectiveSchemaInfo === void 0 ? void 0 : effectiveSchemaInfo.length)) { modifiedQuery = (0, OptimizeUnusedLeftJoins_1.optimizeUnusedLeftJoinsToFixedPoint)(modifiedQuery, effectiveSchemaInfo); } // 7. Remove unused CTEs when requested. if (options.removeUnusedCtes) { modifiedQuery = (0, OptimizeUnusedLeftJoins_1.optimizeUnusedCtesToFixedPoint)(modifiedQuery); } return modifiedQuery; } resolveOptionalConditionPruningParameters(options) { if (options.optionalConditionParameters) { return options.optionalConditionParameters; } if (!options.optionalConditionParameterStates) { return {}; } const legacyParameters = {}; // Preserve backward compatibility for the state-map API while the value-based API becomes the primary entry point. for (const [parameterName, state] of Object.entries(options.optionalConditionParameterStates)) { legacyParameters[parameterName] = state === 'absent' ? null : '__RAWSQL_OPTIONAL_CONDITION_PRESENT__'; } return legacyParameters; } applyColumnFilters(query, options) { const hasIncludeFilters = Array.isArray(options.includeColumns) && options.includeColumns.length > 0; const hasExcludeFilters = Array.isArray(options.excludeColumns) && options.excludeColumns.length > 0; if (!hasIncludeFilters && !hasExcludeFilters) { return query; } if (hasIncludeFilters && hasExcludeFilters) { throw new Error("includeColumns and excludeColumns cannot be used together."); } const simpleQuery = QueryBuilder_1.QueryBuilder.buildSimpleQuery(query); const metadata = simpleQuery.selectClause.items.map(item => { const name = this.getSelectItemName(item); return { item, normalized: name ? this.normalizeColumnIdentifier(name) : null }; }); const availableColumns = new Set(metadata .map(entry => entry.normalized) .filter((name) => name !== null)); const includeFilters = hasIncludeFilters ? this.normalizeColumnList(options.includeColumns) : null; const excludeFilters = hasExcludeFilters ? this.normalizeColumnList(options.excludeColumns) : null; const includeSet = includeFilters ? new Set(includeFilters.map(entry => entry.normalized)) : null; const excludeSet = excludeFilters ? new Set(excludeFilters.map(entry => entry.normalized)) : null; if (includeFilters) { const missing = includeFilters.filter(entry => !availableColumns.has(entry.normalized)); if (missing.length > 0) { throw new Error(`Column${missing.length === 1 ? "" : "s"} not found in SELECT clause: ${missing .map(entry => `'${entry.original}'`) .join(", ")}.`); } } if (excludeFilters) { const missing = excludeFilters.filter(entry => !availableColumns.has(entry.normalized)); if (missing.length > 0) { throw new Error(`Column${missing.length === 1 ? "" : "s"} not found in SELECT clause: ${missing .map(entry => `'${entry.original}'`) .join(", ")}.`); } } const filteredItems = metadata .filter(entry => { if (!entry.normalized) { return true; } if (includeSet) { return includeSet.has(entry.normalized); } if (excludeSet) { return !excludeSet.has(entry.normalized); } return true; }) .map(entry => entry.item); if (filteredItems.length === 0) { throw new Error("Column filtering removed every SELECT item."); } simpleQuery.selectClause.items = filteredItems; return simpleQuery; } normalizeColumnList(columns) { return columns.map(column => { if (typeof column !== "string") { throw new Error("Column filters must be strings."); } const trimmed = column.trim(); if (trimmed === "") { throw new Error("Column filters must not be empty."); } return { normalized: this.normalizeColumnIdentifier(trimmed), original: trimmed }; }); } normalizeColumnIdentifier(value) { return value.trim().toLowerCase(); } getSelectItemName(item) { if (item.identifier) { return item.identifier.name; } if (item.value instanceof ValueComponent_1.ColumnReference) { return item.value.column.name; } return null; } /** * Legacy helper for binding existing named parameters without adding new runtime predicates. * Dynamic WHERE-condition injection is no longer supported; use SSSQL scaffold/refresh instead. * * @param sqlContent Raw SQL string to parse and modify * @param filter Named parameters to bind when they already exist in the SQL * @returns Modified SelectQuery after binding existing named parameters */ buildFilteredQuery(sqlContent, filter) { return this.buildQuery(sqlContent, { filter }); } /** * Builds a SelectQuery with only sorting applied. * Convenience method for when you only need dynamic ORDER BY clauses. * * @param sqlContent Raw SQL string to parse and modify * @param sort Sort conditions to apply * @returns Modified SelectQuery with sort conditions applied */ buildSortedQuery(sqlContent, sort) { return this.buildQuery(sqlContent, { sort }); } /** * Builds a SelectQuery with only pagination applied. * Convenience method for when you only need LIMIT/OFFSET clauses. * * @param sqlContent Raw SQL string to parse and modify * @param paging Pagination options to apply * @returns Modified SelectQuery with pagination applied */ buildPaginatedQuery(sqlContent, paging) { return this.buildQuery(sqlContent, { paging }); } /** * Validates SQL content by attempting to parse it. * Useful for testing SQL validity without applying any modifications. * * @param sqlContent Raw SQL string to validate * @returns true if SQL is valid, throws error if invalid * @throws Error if SQL cannot be parsed */ validateSql(sqlContent) { try { SelectQueryParser_1.SelectQueryParser.parse(sqlContent); return true; } catch (error) { throw new Error(`Invalid SQL: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.DynamicQueryBuilder = DynamicQueryBuilder; //# sourceMappingURL=DynamicQueryBuilder.js.map