{"version":3,"file":"joins.cjs","sources":["../../../src/query/joins.ts"],"sourcesContent":["import {\n  consolidate,\n  filter,\n  join as joinOperator,\n  map,\n} from \"@electric-sql/d2mini\"\nimport { evaluateConditionOnNamespacedRow } from \"./evaluators.js\"\nimport { extractJoinKey } from \"./extractors.js\"\nimport type { Query } from \"./index.js\"\nimport type { IStreamBuilder, JoinType } from \"@electric-sql/d2mini\"\nimport type {\n  KeyedStream,\n  NamespacedAndKeyedStream,\n  NamespacedRow,\n} from \"../types.js\"\n\n/**\n * Creates a processing pipeline for join clauses\n */\nexport function processJoinClause(\n  pipeline: NamespacedAndKeyedStream,\n  query: Query,\n  tables: Record<string, KeyedStream>,\n  mainTableAlias: string,\n  allInputs: Record<string, KeyedStream>\n) {\n  if (!query.join) return pipeline\n  const input = allInputs[query.from]\n\n  for (const joinClause of query.join) {\n    // Create a stream for the joined table\n    const joinedTableAlias = joinClause.as || joinClause.from\n\n    // Get the right join type for the operator\n    const joinType: JoinType =\n      joinClause.type === `cross` ? `inner` : joinClause.type\n\n    // The `in` is formatted as ['@mainKeyRef', '=', '@joinedKeyRef']\n    // Destructure the main key reference and the joined key references\n    const [mainKeyRef, , joinedKeyRefs] = joinClause.on\n\n    // We need to prepare the main pipeline and the joined pipeline\n    // to have the correct key format for joining\n    const mainPipeline = pipeline.pipe(\n      map(([currentKey, namespacedRow]) => {\n        // Extract the key from the ON condition left side for the main table\n        const mainRow = namespacedRow[mainTableAlias]!\n\n        // Extract the join key from the main row\n        const key = extractJoinKey(mainRow, mainKeyRef, mainTableAlias)\n\n        // Return [key, namespacedRow] as a KeyValue type\n        return [key, [currentKey, namespacedRow]] as [\n          unknown,\n          [string, typeof namespacedRow],\n        ]\n      })\n    )\n\n    // Get the joined table input from the inputs map\n    let joinedTableInput: KeyedStream\n\n    if (allInputs[joinClause.from]) {\n      // Use the provided input if available\n      joinedTableInput = allInputs[joinClause.from]!\n    } else {\n      // Create a new input if not provided\n      joinedTableInput =\n        input!.graph.newInput<[string, Record<string, unknown>]>()\n    }\n\n    tables[joinedTableAlias] = joinedTableInput\n\n    // Create a pipeline for the joined table\n    const joinedPipeline = joinedTableInput.pipe(\n      map(([currentKey, row]) => {\n        // Wrap the row in an object with the table alias as the key\n        const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }\n\n        // Extract the key from the ON condition right side for the joined table\n        const key = extractJoinKey(row, joinedKeyRefs, joinedTableAlias)\n\n        // Return [key, namespacedRow] as a KeyValue type\n        return [key, [currentKey, namespacedRow]] as [\n          string,\n          [string, typeof namespacedRow],\n        ]\n      })\n    )\n\n    // Apply join with appropriate typings based on join type\n    switch (joinType) {\n      case `inner`:\n        pipeline = mainPipeline.pipe(\n          joinOperator(joinedPipeline, `inner`),\n          consolidate(),\n          processJoinResults(mainTableAlias, joinedTableAlias, joinClause)\n        )\n        break\n      case `left`:\n        pipeline = mainPipeline.pipe(\n          joinOperator(joinedPipeline, `left`),\n          consolidate(),\n          processJoinResults(mainTableAlias, joinedTableAlias, joinClause)\n        )\n        break\n      case `right`:\n        pipeline = mainPipeline.pipe(\n          joinOperator(joinedPipeline, `right`),\n          consolidate(),\n          processJoinResults(mainTableAlias, joinedTableAlias, joinClause)\n        )\n        break\n      case `full`:\n        pipeline = mainPipeline.pipe(\n          joinOperator(joinedPipeline, `full`),\n          consolidate(),\n          processJoinResults(mainTableAlias, joinedTableAlias, joinClause)\n        )\n        break\n      default:\n        pipeline = mainPipeline.pipe(\n          joinOperator(joinedPipeline, `inner`),\n          consolidate(),\n          processJoinResults(mainTableAlias, joinedTableAlias, joinClause)\n        )\n    }\n  }\n  return pipeline\n}\n\n/**\n * Creates a processing pipeline for join results\n */\nexport function processJoinResults(\n  mainTableAlias: string,\n  joinedTableAlias: string,\n  joinClause: { on: any; type: string }\n) {\n  return function (\n    pipeline: IStreamBuilder<\n      [\n        key: string,\n        [\n          [string, NamespacedRow] | undefined,\n          [string, NamespacedRow] | undefined,\n        ],\n      ]\n    >\n  ): NamespacedAndKeyedStream {\n    return pipeline.pipe(\n      // Process the join result and handle nulls in the same step\n      map((result) => {\n        const [_key, [main, joined]] = result\n        const mainKey = main?.[0]\n        const mainNamespacedRow = main?.[1]\n        const joinedKey = joined?.[0]\n        const joinedNamespacedRow = joined?.[1]\n\n        // For inner joins, both sides should be non-null\n        if (joinClause.type === `inner` || joinClause.type === `cross`) {\n          if (!mainNamespacedRow || !joinedNamespacedRow) {\n            return undefined // Will be filtered out\n          }\n        }\n\n        // For left joins, the main row must be non-null\n        if (joinClause.type === `left` && !mainNamespacedRow) {\n          return undefined // Will be filtered out\n        }\n\n        // For right joins, the joined row must be non-null\n        if (joinClause.type === `right` && !joinedNamespacedRow) {\n          return undefined // Will be filtered out\n        }\n\n        // Merge the nested rows\n        const mergedNamespacedRow: NamespacedRow = {}\n\n        // Add main row data if it exists\n        if (mainNamespacedRow) {\n          Object.entries(mainNamespacedRow).forEach(\n            ([tableAlias, tableData]) => {\n              mergedNamespacedRow[tableAlias] = tableData\n            }\n          )\n        }\n\n        // If we have a joined row, add it to the merged result\n        if (joinedNamespacedRow) {\n          Object.entries(joinedNamespacedRow).forEach(\n            ([tableAlias, tableData]) => {\n              mergedNamespacedRow[tableAlias] = tableData\n            }\n          )\n        } else if (joinClause.type === `left` || joinClause.type === `full`) {\n          // For left or full joins, add the joined table with undefined data if missing\n          // mergedNamespacedRow[joinedTableAlias] = undefined\n        }\n\n        // For right or full joins, add the main table with undefined data if missing\n        if (\n          !mainNamespacedRow &&\n          (joinClause.type === `right` || joinClause.type === `full`)\n        ) {\n          // mergedNamespacedRow[mainTableAlias] = undefined\n        }\n\n        // New key\n        const newKey = `[${mainKey},${joinedKey}]`\n\n        return [newKey, mergedNamespacedRow] as [\n          string,\n          typeof mergedNamespacedRow,\n        ]\n      }),\n      // Filter out undefined results\n      filter((value) => value !== undefined),\n      // Process the ON condition\n      filter(([_key, namespacedRow]: [string, NamespacedRow]) => {\n        // If there's no ON condition, or it's a cross join, always return true\n        if (!joinClause.on || joinClause.type === `cross`) {\n          return true\n        }\n\n        // For LEFT JOIN, if the right side is null, we should include the row\n        if (\n          joinClause.type === `left` &&\n          namespacedRow[joinedTableAlias] === undefined\n        ) {\n          return true\n        }\n\n        // For RIGHT JOIN, if the left side is null, we should include the row\n        if (\n          joinClause.type === `right` &&\n          namespacedRow[mainTableAlias] === undefined\n        ) {\n          return true\n        }\n\n        // For FULL JOIN, if either side is null, we should include the row\n        if (\n          joinClause.type === `full` &&\n          (namespacedRow[mainTableAlias] === undefined ||\n            namespacedRow[joinedTableAlias] === undefined)\n        ) {\n          return true\n        }\n\n        return evaluateConditionOnNamespacedRow(\n          namespacedRow,\n          joinClause.on,\n          mainTableAlias,\n          joinedTableAlias\n        )\n      })\n    )\n  }\n}\n"],"names":["map","extractJoinKey","joinOperator","consolidate","filter","evaluateConditionOnNamespacedRow"],"mappings":"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"}