UNPKG

rawsql-ts

Version:

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

287 lines 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresObjectEntityCteBuilder = void 0; const Clause_1 = require("../models/Clause"); const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); /** * PostgreSQL-specific builder for creating CTEs for object entities (object relationships). * This class handles the creation of CTEs that build JSON/JSONB objects for object entities, * processing them from the deepest level up to ensure proper dependency ordering. * * Features: * - Depth-based CTE naming (cte_object_depth_N) * - NULL handling for entity columns * - JSONB/JSON object construction * - Hierarchical processing of nested objects * * Why depth calculation is critical: * 1. Object entities can be nested at multiple levels. We must process the deepest * (most distant) objects first to ensure their JSON representations are available * when building their parent entities. * 2. Object entity processing is essentially a column compression operation. Entities * at the same depth level can be processed simultaneously since they don't depend * on each other. * * Example hierarchy: * Order (root, depth 0) * └─ Customer (depth 1) * └─ Address (depth 2) * └─ Shipping (depth 1) * └─ Carrier (depth 2) * * Processing order: depth 2 → depth 1 → depth 0 */ class PostgresObjectEntityCteBuilder { buildObjectEntityCtes(initialCte, allEntities, mapping) { const ctes = [initialCte]; let previousCteAlias = initialCte.aliasExpression.table.name; // Collect and sort object entities by depth const objectEntityInfos = this.collectAndSortObjectEntities(mapping, allEntities); // Group entities by depth const entitiesByDepth = this.groupEntitiesByDepth(objectEntityInfos); // Process each depth level, starting from the deepest const depths = Array.from(entitiesByDepth.keys()).sort((a, b) => b - a); for (const depth of depths) { const entitiesAtDepth = entitiesByDepth.get(depth); const cteAlias = `${PostgresObjectEntityCteBuilder.CTE_OBJECT_PREFIX}${depth}`; // Build CTE for all entities at this depth const cte = this.buildDepthCte(entitiesAtDepth, previousCteAlias, cteAlias, mapping, allEntities); ctes.push(cte); previousCteAlias = cteAlias; } return { ctes, lastCteAlias: previousCteAlias }; } /** * Collect all object entities and calculate their depth from root. * * Depth calculation is crucial because: * - It determines the processing order (deepest first) * - It ensures dependencies are resolved before an entity is processed * - It allows parallel processing of entities at the same depth level * * @param mapping The JSON mapping configuration * @param allEntities Map of all entities in the mapping * @returns Array of object entity information with calculated depths */ collectAndSortObjectEntities(mapping, allEntities) { const objectInfos = []; // Helper function to calculate actual object nesting depth for a given OBJECT entity const calculateActualObjectNestingDepth = (entityIdOfObject) => { const initialEntity = allEntities.get(entityIdOfObject); if (!initialEntity) { throw new Error(`Entity ${entityIdOfObject} not found for depth calculation.`); } // If the object itself is root, its depth is 0. (This function should ideally be called for nested entities, not the root itself as a "parent CTE" subject) if (initialEntity.isRoot) return 0; // If the object is not root and has no parentId, it's considered a top-level object, depth 1. if (!initialEntity.parentId) { return 1; } let currentParentIdInHierarchy = initialEntity.parentId; let calculatedObjectDepth = 0; const visitedInPath = new Set(); visitedInPath.add(entityIdOfObject); // Add the starting object itself to detect cycles while (currentParentIdInHierarchy) { if (visitedInPath.has(currentParentIdInHierarchy)) { throw new Error(`Circular dependency detected: ${currentParentIdInHierarchy} already visited in path for ${entityIdOfObject}`); } visitedInPath.add(currentParentIdInHierarchy); const parentEntityData = allEntities.get(currentParentIdInHierarchy); if (!parentEntityData) { throw new Error(`Parent entity ${currentParentIdInHierarchy} not found during depth calculation for ${entityIdOfObject}`); } let parentIsConsideredAnObjectForNesting = false; if (parentEntityData.isRoot) { parentIsConsideredAnObjectForNesting = true; // Root counts as an object ancestor } else { // For non-root parents, find their definition in nestedEntities to check their type const parentDefinition = mapping.nestedEntities.find(ne => ne.id === currentParentIdInHierarchy); if (parentDefinition) { if (parentDefinition.relationshipType === "object") { parentIsConsideredAnObjectForNesting = true; } // If parentDefinition.relationshipType === "array", it's not an object ancestor for depth counting } else { // This implies currentParentIdInHierarchy refers to an entity not defined as root or in nestedEntities // This should ideally not happen with a consistent mapping. throw new Error(`Parent entity ${currentParentIdInHierarchy} (ancestor of ${entityIdOfObject}) has no definition in mapping.nestedEntities and is not root.`); } } if (parentIsConsideredAnObjectForNesting) { calculatedObjectDepth++; } if (parentEntityData.isRoot) { break; // Stop when the root is processed as the highest object ancestor } currentParentIdInHierarchy = parentEntityData.parentId; // Move to the next ancestor } return calculatedObjectDepth; }; mapping.nestedEntities.forEach(nestedEntity => { if (nestedEntity.relationshipType === "object") { const entity = allEntities.get(nestedEntity.id); // Ensure we don't process the root entity itself as a "parent" CTE, // and that the entity actually exists. if (entity && !entity.isRoot) { objectInfos.push({ entity, depth: calculateActualObjectNestingDepth(nestedEntity.id) }); } } }); // The existing grouping and sorting by depth (b - a for descending) should still work correctly // as it processes deepest levels first, regardless of the absolute depth numbers. return objectInfos; } /** * Group 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 parentInfos Array of parent entity information with depths * @returns Map of depth level to entities at that depth */ groupEntitiesByDepth(objectInfos) { const entitiesByDepth = new Map(); objectInfos.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 entities at a specific depth level */ buildDepthCte(entitiesAtDepth, previousCteAlias, cteAlias, mapping, allEntities) { // Build SELECT items: * and JSON objects for all entities at this depth const selectItems = [ // Select all columns from previous CTE new Clause_1.SelectItem(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(PostgresObjectEntityCteBuilder.WILDCARD_COLUMN))) ]; // Process each entity at this depth for (const { entity } of entitiesAtDepth) { const jsonColumn = this.buildEntityJsonColumn(entity, mapping, allEntities); selectItems.push(jsonColumn); } // Create CTE that selects from previous CTE 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(previousCteAlias)), null), null) }); return new Clause_1.CommonTable(cteSelect, new Clause_1.SourceAliasExpression(cteAlias, null), null); } /** * Build JSON column for a single entity with NULL handling */ buildEntityJsonColumn(entity, mapping, allEntities) { // Build JSON object arguments and NULL checks const { jsonObjectArgs, nullChecks } = this.prepareEntityColumns(entity); // Add child object relationships this.addChildObjectRelationships(entity, jsonObjectArgs, mapping, allEntities); // Create JSON object const jsonObject = this.createJsonObject(jsonObjectArgs, mapping.useJsonb); // Build NULL condition and CASE expression const nullCondition = this.buildNullCondition(nullChecks); const caseExpr = this.createCaseExpression(nullCondition, jsonObject); // Add JSON object as named column const jsonColumnName = `${entity.name.toLowerCase()}${PostgresObjectEntityCteBuilder.JSON_COLUMN_SUFFIX}`; return new Clause_1.SelectItem(caseExpr, jsonColumnName); } /** * Prepare entity columns and NULL checks. * * This method extracts column data and creates NULL checks for each column. * The NULL checking is essential for handling outer joins correctly. * * In outer join scenarios, when there's no matching row in the joined table, * all columns from that table will be NULL. Instead of creating an empty object * with all NULL properties (e.g., {id: null, name: null, email: null}), * we want to represent the absence of the entity as NULL itself. * * This ensures cleaner JSON output where missing relationships are represented * as NULL rather than objects with all NULL fields. * * @param entity The entity whose columns are being processed * @returns Object containing arrays of JSON object arguments and NULL check conditions */ prepareEntityColumns(entity) { const jsonObjectArgs = []; const nullChecks = []; Object.entries(entity.columns).forEach(([jsonKey, sqlColumn]) => { jsonObjectArgs.push(new ValueComponent_1.LiteralValue(jsonKey)); jsonObjectArgs.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sqlColumn))); // Collect NULL checks for each column nullChecks.push(new ValueComponent_1.BinaryExpression(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(sqlColumn)), "is", new ValueComponent_1.LiteralValue(null))); }); return { jsonObjectArgs, nullChecks }; } /** * Add child object relationships to JSON object arguments. * * This method processes nested object-type entities that are direct children of the current entity. * For each child entity, it adds the property name and corresponding JSON column reference * to the arguments array that will be used to build the parent's JSON object. * * The child JSON columns are expected to already exist in the data source (created by deeper * level CTEs), as we process from the deepest level up to the root. * * Note: In this context, "child" refers to entities that have an object relationship (0..1) * with their parent. From a data perspective, these are typically entities referenced via * foreign keys, representing "parent" entities in traditional database terminology. * * @param entity The current entity being processed * @param jsonObjectArgs Array to which JSON object arguments will be added * @param mapping The JSON mapping configuration * @param allEntities Map of all entities in the mapping */ addChildObjectRelationships(entity, jsonObjectArgs, mapping, allEntities) { const childEntities = mapping.nestedEntities.filter(ne => ne.parentId === entity.id && ne.relationshipType === "object"); childEntities.forEach(childEntity => { const child = allEntities.get(childEntity.id); if (child) { jsonObjectArgs.push(new ValueComponent_1.LiteralValue(childEntity.propertyName)); const jsonColumnName = `${child.name.toLowerCase()}${PostgresObjectEntityCteBuilder.JSON_COLUMN_SUFFIX}`; jsonObjectArgs.push(new ValueComponent_1.ColumnReference(null, new ValueComponent_1.IdentifierString(jsonColumnName))); } }); } /** * Create JSON object function call */ createJsonObject(args, useJsonb = false) { const jsonBuildFunction = useJsonb ? "jsonb_build_object" : "json_build_object"; return new ValueComponent_1.FunctionCall(null, new ValueComponent_1.RawString(jsonBuildFunction), new ValueComponent_1.ValueList(args), null); } /** * Build NULL condition from NULL checks */ buildNullCondition(nullChecks) { return nullChecks.reduce((acc, check) => acc ? new ValueComponent_1.BinaryExpression(acc, "and", check) : check); } /** * Create CASE expression with NULL handling */ createCaseExpression(nullCondition, jsonObject) { return new ValueComponent_1.CaseExpression(null, new ValueComponent_1.SwitchCaseArgument([new ValueComponent_1.CaseKeyValuePair(nullCondition, new ValueComponent_1.LiteralValue(null))], jsonObject // ELSE return the JSON object )); } } exports.PostgresObjectEntityCteBuilder = PostgresObjectEntityCteBuilder; PostgresObjectEntityCteBuilder.JSON_COLUMN_SUFFIX = '_json'; PostgresObjectEntityCteBuilder.CTE_OBJECT_PREFIX = 'cte_object_depth_'; PostgresObjectEntityCteBuilder.WILDCARD_COLUMN = '*'; /** * Build CTEs for all object entities in the correct dependency order * @param initialCte The starting CTE containing all raw data * @param allEntities Map of all entities in the mapping * @param mapping The JSON mapping configuration * @returns Array of CTEs and the alias of the last CTE created */ //# sourceMappingURL=PostgresObjectEntityCteBuilder.js.map