UNPKG

rawsql-ts

Version:

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

235 lines 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresArrayEntityCteBuilder = void 0; const Clause_1 = require("../models/Clause"); const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); const SelectValueCollector_1 = require("./SelectValueCollector"); /** * PostgreSQL-specific builder for creating CTEs for array entities (array relationships). * This class handles the creation of CTEs that build JSON/JSONB arrays for child entities, * processing them from the deepest level up to ensure proper dependency ordering. * * Features: * - Depth-based CTE naming (cte_array_depth_N) * - Row compression using GROUP BY operations * - JSONB/JSON array aggregation * - Hierarchical processing of nested arrays * - Column exclusion to avoid duplication * * Why depth calculation is critical: * 1. Array entities can be nested at multiple levels. We must process the deepest * (most distant) arrays first to ensure their JSON representations are available * when building their parent arrays. * 2. Array entity processing is essentially a row compression operation using GROUP BY. * Unlike parent entities which use column compression, arrays require grouping * to aggregate multiple rows into JSON arrays. * * Example hierarchy: * Order (root, depth 0) * └─ Items (array, depth 1) * └─ Details (array, depth 2) * * Processing order: depth 2 → depth 1 → depth 0 */ class PostgresArrayEntityCteBuilder { /** * Build CTEs for all array entities in the correct dependency order * @param ctesSoFar Array of CTEs built so far (starts with the initial CTE) * @param aliasOfCteToBuildUpon Alias of the CTE from which the current array CTE will select * @param allEntities Map of all entities in the mapping * @param mapping The JSON mapping configuration * @returns Object containing the updated list of all CTEs and the alias of the last CTE created */ buildArrayEntityCtes(ctesSoFar, aliasOfCteToBuildUpon, allEntities, mapping) { let currentCtes = [...ctesSoFar]; let currentCteAlias = aliasOfCteToBuildUpon; // Collect and sort array entities by depth const sortedArrayInfos = this.collectAndSortArrayEntities(mapping, allEntities); if (sortedArrayInfos.length === 0) { return { updatedCtes: currentCtes, lastCteAlias: currentCteAlias }; } // Group array entities by depth level for batch processing const entitiesByDepth = this.groupEntitiesByDepth(sortedArrayInfos); // Process from deepest to shallowest (depth-first) const depths = Array.from(entitiesByDepth.keys()).sort((a, b) => b - a); for (const depth of depths) { const infos = entitiesByDepth.get(depth); // Build CTE for all entities at this depth const { cte, newCteAlias } = this.buildDepthCte(infos, currentCteAlias, currentCtes, depth, mapping); currentCtes.push(cte); currentCteAlias = newCteAlias; } return { updatedCtes: currentCtes, lastCteAlias: currentCteAlias }; } /** * Collect all array entities and calculate their depth from root. * * Depth calculation ensures proper processing order where deeper nested * arrays are processed first, making their aggregated data available * for parent array processing. * * @param mapping The JSON mapping configuration * @param allEntities Map of all entities in the mapping * @returns Array of array entity information with calculated depths, sorted deepest first */ collectAndSortArrayEntities(mapping, allEntities) { const arrayEntityInfos = []; // Helper function to calculate depth for an entity const getDepth = (entityId) => { const entity = allEntities.get(entityId); if (!entity || entity.isRoot) return 0; if (!entity.parentId) return 1; return 1 + getDepth(entity.parentId); }; // Collect all array-type nested entities mapping.nestedEntities.forEach(ne => { if (ne.relationshipType === "array") { const currentArrayEntity = allEntities.get(ne.id); const parentEntity = allEntities.get(ne.parentId); if (!currentArrayEntity || !parentEntity) { throw new Error(`Configuration error: Array entity '${ne.id}' or its parent '${ne.parentId}' not found.`); } // Determine the linking column from parent entity // This assumes the first column of the parent is a suitable key for linking. // More robust linking might require explicit configuration in the mapping. const parentSqlColumns = Object.values(parentEntity.columns); if (parentSqlColumns.length === 0) { throw new Error(`Configuration error: Parent entity '${parentEntity.name}' (ID: ${parentEntity.id}) must have at least one column defined to serve as a linking key for child array '${ne.name}'.`); } const parentIdColumnSqlName = parentSqlColumns[0]; arrayEntityInfos.push({ entity: currentArrayEntity, parentEntity: parentEntity, parentIdColumnSqlName: parentIdColumnSqlName, depth: getDepth(ne.id) }); } }); // Sort by depth, deepest arrays (higher depth number) processed first (bottom-up for arrays) arrayEntityInfos.sort((a, b) => b.depth - a.depth); return arrayEntityInfos; } /** * Group array entities by their depth level. * * Grouping by depth allows us to: * - Process all entities at the same level in a single CTE * - Optimize query performance by reducing the number of CTEs * - Maintain clear dependency ordering * * @param arrayInfos Array of array entity information with depths * @returns Map of depth level to entities at that depth */ groupEntitiesByDepth(arrayInfos) { const entitiesByDepth = new Map(); arrayInfos.forEach(info => { const depth = info.depth; if (!entitiesByDepth.has(depth)) { entitiesByDepth.set(depth, []); } entitiesByDepth.get(depth).push(info); }); return entitiesByDepth; } /** * Build a CTE that processes all array entities at a specific depth level. * * This method creates a single CTE that aggregates multiple array entities * at the same depth, using GROUP BY to compress rows into JSON arrays. * * @param infos Array entities at this depth level * @param currentCteAlias Alias of the CTE to build upon * @param currentCtes All CTEs built so far * @param depth Current depth level being processed * @param mapping JSON mapping configuration * @returns The new CTE and its alias */ buildDepthCte(infos, currentCteAlias, currentCtes, depth, mapping) { var _a; // Collect columns that will be compressed into arrays const arrayColumns = new Set(); infos.forEach(info => { Object.values(info.entity.columns).forEach(col => arrayColumns.add(col)); }); // Get columns from previous CTE const prevCte = (_a = currentCtes.find(c => c.aliasExpression.table.name === currentCteAlias)) === null || _a === void 0 ? void 0 : _a.query; if (!prevCte) { throw new Error(`CTE not found: ${currentCteAlias}`); } const prevSelects = new SelectValueCollector_1.SelectValueCollector(null, currentCtes).collect(prevCte); // Build SELECT items: columns that are NOT being compressed (for GROUP BY) const groupByItems = []; const selectItems = []; prevSelects.forEach(sv => { if (!arrayColumns.has(sv.name)) { selectItems.push(new Clause_1.SelectItem(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sv.name)), sv.name)); groupByItems.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sv.name))); } }); // Add JSON aggregation columns for each array entity at this depth for (const info of infos) { const agg = this.buildAggregationDetailsForArrayEntity(info.entity, mapping.nestedEntities, new Map(), // allEntities - not needed for array aggregation mapping.useJsonb); selectItems.push(new Clause_1.SelectItem(agg.jsonAgg, info.entity.propertyName)); } // Create the new CTE const cteAlias = `${PostgresArrayEntityCteBuilder.CTE_ARRAY_PREFIX}${depth}`; const cteSelect = new SimpleSelectQuery_1.SimpleSelectQuery({ selectClause: new Clause_1.SelectClause(selectItems), fromClause: new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(currentCteAlias)), null), null), groupByClause: groupByItems.length > 0 ? new Clause_1.GroupByClause(groupByItems) : null, }); const cte = new Clause_1.CommonTable(cteSelect, new Clause_1.SourceAliasExpression(cteAlias, null), null); return { cte, newCteAlias: cteAlias }; } /** * Build JSON aggregation function for an array entity. * * This method creates a jsonb_agg or json_agg function call that aggregates * the entity's columns into a JSON array. It also handles nested relationships * by including child entity properties in the JSON object. * * @param entity The array entity being processed * @param nestedEntities All nested entities from the mapping * @param allEntities Map of all entities (not used in current implementation) * @param useJsonb Whether to use JSONB functions * @returns Object containing the JSON aggregation function */ buildAggregationDetailsForArrayEntity(entity, nestedEntities, allEntities, useJsonb = false) { // Build JSON object for array elements const jsonBuildFunction = useJsonb ? "jsonb_build_object" : "json_build_object"; const args = []; // Add the entity's own columns Object.entries(entity.columns).forEach(([jsonKey, sqlColumn]) => { args.push(new ValueComponent_1.LiteralValue(jsonKey)); args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sqlColumn))); }); // Find and process child entities (both object and array types) const childEntities = nestedEntities.filter((ne) => ne.parentId === entity.id); childEntities.forEach((childEntity) => { args.push(new ValueComponent_1.LiteralValue(childEntity.propertyName)); if (childEntity.relationshipType === "object") { // For object relationships, use pre-computed JSON column const jsonColumnName = `${childEntity.name.toLowerCase()}_json`; args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(jsonColumnName))); } else if (childEntity.relationshipType === "array") { // For array relationships, use the column directly args.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(childEntity.propertyName))); } }); // Create JSON object const jsonObject = new ValueComponent_1.FunctionCall(null, new ValueComponent_1.RawString(jsonBuildFunction), new ValueComponent_1.ValueList(args), null); // Create JSON aggregation const jsonAggFunction = useJsonb ? "jsonb_agg" : "json_agg"; const jsonAgg = new ValueComponent_1.FunctionCall(null, new ValueComponent_1.RawString(jsonAggFunction), new ValueComponent_1.ValueList([jsonObject]), null); return { jsonAgg }; } } exports.PostgresArrayEntityCteBuilder = PostgresArrayEntityCteBuilder; // Constants for consistent naming conventions PostgresArrayEntityCteBuilder.CTE_ARRAY_PREFIX = 'cte_array_depth_'; //# sourceMappingURL=PostgresArrayEntityCteBuilder.js.map