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