lakeql 0.2.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { Lake, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes, createParquetLake, parseSql, writePartitionedParquet, broadcastJoin } from './chunk-32PE6IK4.js';
2
- import { stableStringify } from './chunk-LWF5FKHB.js';
1
+ import { Lake, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes, createParquetLake, parseSql, writePartitionedParquet, broadcastJoin, crossJoin } from './chunk-AUL4DYT4.js';
2
+ import { stableStringify, collectExprColumns } from './chunk-LWF5FKHB.js';
3
3
  import { LakeqlError, jsonSafeValue, matches, evaluate } from './chunk-WGHR35QU.js';
4
4
 
5
5
  // ../core/dist/in-memory.js
@@ -984,7 +984,7 @@ async function* scanPlannedIcebergRows(options) {
984
984
  throwIfAborted(signal);
985
985
  const deletes = await decodedDeletesForFile(file, options, signal);
986
986
  throwIfAborted(signal);
987
- const data = await options.readDataFile(file);
987
+ const data = await options.readDataFile(file, deletes);
988
988
  throwIfAborted(signal);
989
989
  let rowOffset = 0;
990
990
  for await (const batch of rowBatches(data)) {
@@ -1876,15 +1876,51 @@ function projectedIds(fields, select) {
1876
1876
  function partitionMayMatch(expr, partition) {
1877
1877
  if (!expr)
1878
1878
  return true;
1879
+ const value = partitionEval(expr, partition);
1880
+ return value !== false;
1881
+ }
1882
+ function partitionEval(expr, partition) {
1883
+ switch (expr.kind) {
1884
+ case "literal":
1885
+ case "column":
1886
+ return "unknown";
1887
+ case "compare":
1888
+ case "in":
1889
+ case "between":
1890
+ case "null-check":
1891
+ case "like":
1892
+ case "call":
1893
+ case "arithmetic":
1894
+ case "case":
1895
+ return expressionIsPartitionOnly(expr, partition) ? matchesPartition(expr, partition) : "unknown";
1896
+ case "not": {
1897
+ const value = partitionEval(expr.operand, partition);
1898
+ return value === "unknown" ? "unknown" : !value;
1899
+ }
1900
+ case "logical": {
1901
+ const values = expr.operands.map((operand) => partitionEval(operand, partition));
1902
+ if (expr.op === "and") {
1903
+ if (values.some((value) => value === false))
1904
+ return false;
1905
+ return values.every((value) => value === true) ? true : "unknown";
1906
+ }
1907
+ if (values.some((value) => value === true))
1908
+ return true;
1909
+ return values.every((value) => value === false) ? false : "unknown";
1910
+ }
1911
+ }
1912
+ }
1913
+ function expressionIsPartitionOnly(expr, partition) {
1879
1914
  const columns = /* @__PURE__ */ new Set();
1880
1915
  collectColumns(expr, columns);
1881
- if (columns.size === 0 || [...columns].some((column) => !(column in partition)))
1882
- return true;
1916
+ return columns.size > 0 && [...columns].every((column) => column in partition);
1917
+ }
1918
+ function matchesPartition(expr, partition) {
1883
1919
  try {
1884
1920
  return matches(expr, partition);
1885
1921
  } catch (cause) {
1886
1922
  if (cause instanceof LakeqlError && cause.code === "LAKEQL_TYPE_ERROR")
1887
- return true;
1923
+ return "unknown";
1888
1924
  throw cause;
1889
1925
  }
1890
1926
  }
@@ -1982,13 +2018,14 @@ async function* scanBatches(plan, options = {}) {
1982
2018
  for await (const batch of scanPlannedIcebergRows({
1983
2019
  plan: plan.plan,
1984
2020
  ...options,
1985
- readDataFile: async (file) => projectIcebergParquetBatches(
2021
+ readDataFile: async (file, deletes) => projectIcebergParquetBatches(
1986
2022
  plan.store,
1987
2023
  plan.table,
1988
2024
  file.path,
1989
2025
  file.partition,
1990
2026
  file.snapshotId,
1991
2027
  plan.options,
2028
+ deletes,
1992
2029
  options
1993
2030
  ),
1994
2031
  readDeleteFile: async (deleteFile) => readIcebergParquetDeletes(plan.store, deleteFile)
@@ -2003,20 +2040,34 @@ async function* scanRows(plan, options = {}) {
2003
2040
  }
2004
2041
  }
2005
2042
  }
2006
- async function* projectIcebergParquetBatches(store, table, path, partition, snapshotId, planOptions, scanOptions) {
2043
+ async function* projectIcebergParquetBatches(store, table, path, partition, snapshotId, planOptions, deletes, scanOptions) {
2007
2044
  const readOptions = {};
2008
2045
  if (scanOptions.batchSize !== void 0) readOptions.batchSize = scanOptions.batchSize;
2046
+ const retainedColumns = retainedFilterColumns(deletes, planOptions);
2009
2047
  for await (const batch of readParquetObjectBatches(store, path, readOptions)) {
2010
2048
  yield {
2011
2049
  rowOffset: batch.rowOffset,
2012
2050
  rows: batch.rows.map((row) => {
2051
+ const sourceRow = { ...partition, ...row };
2013
2052
  const projectOptions = { snapshotId };
2014
2053
  if (planOptions.select !== void 0) projectOptions.select = planOptions.select;
2015
- return table.projectRow({ ...partition, ...row }, projectOptions);
2054
+ const projected = table.projectRow(sourceRow, projectOptions);
2055
+ for (const column of retainedColumns) {
2056
+ if (column in sourceRow && !(column in projected)) projected[column] = sourceRow[column];
2057
+ }
2058
+ return projected;
2016
2059
  })
2017
2060
  };
2018
2061
  }
2019
2062
  }
2063
+ function retainedFilterColumns(deletes, planOptions) {
2064
+ const columns = /* @__PURE__ */ new Set();
2065
+ if (planOptions.select === void 0) collectExprColumns(planOptions.where, columns);
2066
+ for (const deletion of deletes.equalityDeletes ?? []) {
2067
+ for (const column of deletion.columns) columns.add(column);
2068
+ }
2069
+ return columns;
2070
+ }
2020
2071
 
2021
2072
  // src/sql.ts
2022
2073
  var textEncoder = new TextEncoder();
@@ -2066,14 +2117,32 @@ var SqlQueryResult = class {
2066
2117
  };
2067
2118
  async function executeSql(lake, sql, options) {
2068
2119
  const ast = sqlAst(sql, options);
2120
+ validateSourceBindings(options);
2069
2121
  const bound = bindSources(ast, options);
2070
- const materialized = await materializeCteIfNeeded(lake.store, lake, bound);
2122
+ const pushed = pushdownIcebergPredicates(bound, options);
2123
+ const materializedIceberg = await materializeIcebergTablesIfNeeded(
2124
+ lake.store,
2125
+ pushed.ast,
2126
+ options,
2127
+ pushed
2128
+ );
2129
+ try {
2130
+ return await executeRowsFromAst(lake, materializedIceberg.ast, options);
2131
+ } finally {
2132
+ await materializedIceberg.cleanup();
2133
+ }
2134
+ }
2135
+ async function executeRowsFromAst(lake, ast, options) {
2136
+ const materialized = await materializeCteIfNeeded(lake.store, lake, ast, options);
2071
2137
  try {
2072
2138
  const resolved = await resolveScalarSubqueries(lake, materialized.ast);
2073
- if (resolved.subqueryJoin !== void 0)
2074
- return subqueryJoinRowsFromAst(lake, resolved, options);
2075
- if (resolved.join !== void 0) return joinRowsFromAst(lake, resolved, options);
2076
- if (hasAggregation(resolved)) return aggregateRowsFromAst(lake.path(resolved.source), resolved);
2139
+ if (sqlSubqueryJoins(resolved).length > 0)
2140
+ return await subqueryJoinRowsFromAst(lake, resolved, options);
2141
+ if (sqlJoins(resolved).length > 0) return await joinRowsFromAst(lake, resolved, options);
2142
+ if (hasAggregation(resolved))
2143
+ return await aggregateRowsFromAst(lake.path(resolved.source), resolved);
2144
+ if (hasCorrelatedScalarSubqueries(resolved))
2145
+ return await correlatedScalarRowsFromAst(lake, resolved);
2077
2146
  return await builderFromAst(lake.path(resolved.source), resolved).toArray();
2078
2147
  } finally {
2079
2148
  await materialized.cleanup();
@@ -2086,12 +2155,25 @@ function sqlAst(sql, options) {
2086
2155
  }
2087
2156
  function bindSources(ast, options) {
2088
2157
  const tables = options.tables ?? {};
2158
+ const icebergTables = options.icebergTables ?? {};
2089
2159
  const bindSource = (source) => {
2090
2160
  if (source === "input" && options.path !== void 0) return options.path;
2161
+ if (icebergTables[source] !== void 0) return source;
2091
2162
  return tables[source] ?? source;
2092
2163
  };
2093
2164
  return mapAstSources(ast, bindSource);
2094
2165
  }
2166
+ function validateSourceBindings(options) {
2167
+ const tables = options.tables ?? {};
2168
+ const icebergTables = options.icebergTables ?? {};
2169
+ for (const name of Object.keys(icebergTables)) {
2170
+ if (tables[name] === void 0) continue;
2171
+ throw new LakeqlError(
2172
+ "LAKEQL_VALIDATION_ERROR",
2173
+ `SQL source "${name}" is bound as both Parquet and Iceberg`
2174
+ );
2175
+ }
2176
+ }
2095
2177
  function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
2096
2178
  const out = { ...ast, source: bindSource(ast.source) };
2097
2179
  if (seen.has(ast)) {
@@ -2100,12 +2182,25 @@ function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
2100
2182
  }
2101
2183
  seen.add(ast);
2102
2184
  if (ast.join !== void 0) out.join = { ...ast.join, source: bindSource(ast.join.source) };
2103
- if (ast.subqueryJoin !== void 0) {
2185
+ if (ast.joins !== void 0) {
2186
+ out.joins = ast.joins.map((join) => ({ ...join, source: bindSource(join.source) }));
2187
+ const firstJoin = out.joins[0];
2188
+ if (firstJoin !== void 0) out.join = firstJoin;
2189
+ }
2190
+ if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
2104
2191
  out.subqueryJoin = {
2105
2192
  ...ast.subqueryJoin,
2106
2193
  source: bindSource(ast.subqueryJoin.source)
2107
2194
  };
2108
2195
  }
2196
+ if (ast.subqueryJoins !== void 0) {
2197
+ out.subqueryJoins = ast.subqueryJoins.map((join) => ({
2198
+ ...join,
2199
+ source: bindSource(join.source)
2200
+ }));
2201
+ const firstSubqueryJoin = out.subqueryJoins[0];
2202
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
2203
+ }
2109
2204
  if (ast.cte !== void 0) {
2110
2205
  out.cte = { ...ast.cte, query: mapAstSources(ast.cte.query, bindSource, seen) };
2111
2206
  }
@@ -2117,6 +2212,69 @@ function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
2117
2212
  ])
2118
2213
  );
2119
2214
  }
2215
+ if (ast.correlatedScalarSubqueries !== void 0) {
2216
+ out.correlatedScalarSubqueries = Object.fromEntries(
2217
+ Object.entries(ast.correlatedScalarSubqueries).map(([id, subquery]) => [
2218
+ id,
2219
+ { ...subquery, source: bindSource(subquery.source) }
2220
+ ])
2221
+ );
2222
+ }
2223
+ return out;
2224
+ }
2225
+ async function mapAstSourcesAsync(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
2226
+ const out = { ...ast, source: await bindSource(ast.source) };
2227
+ if (seen.has(ast)) {
2228
+ delete out.scalarSubqueries;
2229
+ return out;
2230
+ }
2231
+ seen.add(ast);
2232
+ if (ast.join !== void 0) {
2233
+ out.join = { ...ast.join, source: await bindSource(ast.join.source) };
2234
+ }
2235
+ if (ast.joins !== void 0) {
2236
+ out.joins = await Promise.all(
2237
+ ast.joins.map(async (join) => ({ ...join, source: await bindSource(join.source) }))
2238
+ );
2239
+ const firstJoin = out.joins[0];
2240
+ if (firstJoin !== void 0) out.join = firstJoin;
2241
+ }
2242
+ if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
2243
+ out.subqueryJoin = {
2244
+ ...ast.subqueryJoin,
2245
+ source: await bindSource(ast.subqueryJoin.source)
2246
+ };
2247
+ }
2248
+ if (ast.subqueryJoins !== void 0) {
2249
+ out.subqueryJoins = await Promise.all(
2250
+ ast.subqueryJoins.map(async (join) => ({ ...join, source: await bindSource(join.source) }))
2251
+ );
2252
+ const firstSubqueryJoin = out.subqueryJoins[0];
2253
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
2254
+ }
2255
+ if (ast.cte !== void 0) {
2256
+ out.cte = { ...ast.cte, query: await mapAstSourcesAsync(ast.cte.query, bindSource, seen) };
2257
+ }
2258
+ if (ast.scalarSubqueries !== void 0) {
2259
+ out.scalarSubqueries = Object.fromEntries(
2260
+ await Promise.all(
2261
+ Object.entries(ast.scalarSubqueries).map(async ([id, subquery]) => [
2262
+ id,
2263
+ { ...subquery, query: await mapAstSourcesAsync(subquery.query, bindSource, seen) }
2264
+ ])
2265
+ )
2266
+ );
2267
+ }
2268
+ if (ast.correlatedScalarSubqueries !== void 0) {
2269
+ out.correlatedScalarSubqueries = Object.fromEntries(
2270
+ await Promise.all(
2271
+ Object.entries(ast.correlatedScalarSubqueries).map(async ([id, subquery]) => [
2272
+ id,
2273
+ { ...subquery, source: await bindSource(subquery.source) }
2274
+ ])
2275
+ )
2276
+ );
2277
+ }
2120
2278
  return out;
2121
2279
  }
2122
2280
  function builderFromAst(builder, ast) {
@@ -2138,19 +2296,10 @@ function hasAggregation(ast) {
2138
2296
  function hasWindowSemantics(ast) {
2139
2297
  return ast.windows !== void 0 && Object.keys(ast.windows).length > 0 || ast.qualify !== void 0;
2140
2298
  }
2141
- async function materializeCteIfNeeded(store, lake, ast) {
2299
+ async function materializeCteIfNeeded(store, lake, ast, options) {
2142
2300
  if (ast.cte === void 0) return { ast, cleanup: async () => {
2143
2301
  } };
2144
- if (ast.source !== ast.cte.name) {
2145
- throw new LakeqlError(
2146
- "LAKEQL_SQL_UNSUPPORTED",
2147
- "CTEs are only supported as the outer FROM source"
2148
- );
2149
- }
2150
- if (ast.join !== void 0 || ast.subqueryJoin !== void 0) {
2151
- throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "CTEs inside JOINs are not supported yet");
2152
- }
2153
- const cteRows = hasAggregation(ast.cte.query) ? await aggregateRowsFromAst(lake.path(ast.cte.query.source), ast.cte.query) : await builderFromAst(lake.path(ast.cte.query.source), ast.cte.query).toArray();
2302
+ const cteRows = await executeRowsFromAst(lake, ast.cte.query, options);
2154
2303
  if (cteRows.length === 0) {
2155
2304
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Empty CTE results are not supported yet");
2156
2305
  }
@@ -2160,13 +2309,432 @@ async function materializeCteIfNeeded(store, lake, ast) {
2160
2309
  maxRowsPerFile: cteRows.length
2161
2310
  });
2162
2311
  const { cte: _cte, ...rest } = ast;
2312
+ const ctePath = `${prefix}/*.parquet`;
2163
2313
  return {
2164
- ast: { ...rest, source: `${prefix}/*.parquet` },
2314
+ ast: mapAstSources(rest, (source) => source === ast.cte?.name ? ctePath : source),
2165
2315
  cleanup: async () => {
2166
2316
  await Promise.all(written.files.map((file) => store.delete(file.path)));
2167
2317
  }
2168
2318
  };
2169
2319
  }
2320
+ async function materializeIcebergTablesIfNeeded(store, ast, options, pushed = emptyIcebergPushdownResult(ast)) {
2321
+ const icebergTables = options.icebergTables ?? {};
2322
+ if (Object.keys(icebergTables).length === 0 && pushed.sources.size === 0) {
2323
+ return { ast, cleanup: async () => {
2324
+ } };
2325
+ }
2326
+ const materialized = /* @__PURE__ */ new Map();
2327
+ const requiredColumns = collectSourceColumns(ast);
2328
+ const materializeSource = async (source) => {
2329
+ const icebergTable = pushed.sources.get(source) ?? icebergTables[source];
2330
+ if (icebergTable === void 0) return source;
2331
+ const cached = materialized.get(source);
2332
+ if (cached !== void 0) return cached.path;
2333
+ const table = await loadTable({
2334
+ format: "iceberg",
2335
+ store,
2336
+ metadataPath: icebergTable.metadataPath
2337
+ });
2338
+ if (table.format !== "iceberg") {
2339
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `SQL source "${source}" is not Iceberg`);
2340
+ }
2341
+ const planOptions = icebergPlanOptions(icebergTable);
2342
+ const sourceColumns = icebergRequiredColumns(requiredColumns, pushed.predicates, source);
2343
+ const selectedColumns = icebergSelectableColumns(table.table, planOptions, sourceColumns);
2344
+ if (selectedColumns !== void 0) planOptions.select = selectedColumns;
2345
+ const predicate = pushed.predicates.get(source);
2346
+ if (predicate !== void 0) planOptions.where = predicate;
2347
+ const plan = planFiles2(table, planOptions);
2348
+ const rows = [];
2349
+ for await (const row of scanRows(plan)) rows.push(row);
2350
+ if (rows.length === 0) {
2351
+ throw new LakeqlError(
2352
+ "LAKEQL_SQL_UNSUPPORTED",
2353
+ `Iceberg SQL source "${source}" produced no rows`
2354
+ );
2355
+ }
2356
+ const prefix = `__lakeql_sql_iceberg/${safeTempName(source)}/${nextSqlTempId()}`;
2357
+ const written = await writePartitionedParquet(store, prefix, {
2358
+ rows,
2359
+ maxRowsPerFile: icebergTable.maxRowsPerFile ?? rows.length
2360
+ });
2361
+ const result = {
2362
+ path: `${prefix}/*.parquet`,
2363
+ files: written.files.map((file) => file.path)
2364
+ };
2365
+ materialized.set(source, result);
2366
+ return result.path;
2367
+ };
2368
+ const mapped = await mapAstSourcesAsync(ast, materializeSource);
2369
+ return {
2370
+ ast: mapped,
2371
+ cleanup: async () => {
2372
+ await Promise.all(
2373
+ [...materialized.values()].flatMap((table) => table.files).map((path) => store.delete(path))
2374
+ );
2375
+ }
2376
+ };
2377
+ }
2378
+ function icebergRequiredColumns(requiredColumns, predicates, source) {
2379
+ const columns = new Set(requiredColumns.get(source) ?? []);
2380
+ const predicate = predicates.get(source);
2381
+ if (predicate !== void 0) collectExprColumns2(predicate, columns);
2382
+ return columns.size === 0 ? void 0 : columns;
2383
+ }
2384
+ function icebergSelectableColumns(table, planOptions, columns) {
2385
+ if (columns === void 0) return void 0;
2386
+ const snapshot = table.snapshot(planOptions);
2387
+ const fields = new Set(table.schema(snapshot["schema-id"]).map((field) => field.name));
2388
+ for (const column of columns) {
2389
+ if (!fields.has(column)) return void 0;
2390
+ }
2391
+ return [...columns];
2392
+ }
2393
+ function emptyIcebergPushdownResult(ast) {
2394
+ return { ast, predicates: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
2395
+ }
2396
+ function pushdownIcebergPredicates(ast, options) {
2397
+ const icebergTables = options.icebergTables ?? {};
2398
+ if (Object.keys(icebergTables).length === 0) return emptyIcebergPushdownResult(ast);
2399
+ const context = {
2400
+ icebergTables,
2401
+ predicates: /* @__PURE__ */ new Map(),
2402
+ sources: /* @__PURE__ */ new Map(),
2403
+ seen: /* @__PURE__ */ new WeakSet(),
2404
+ nextSourceId: 0
2405
+ };
2406
+ return {
2407
+ ast: pushdownIcebergPredicatesFromAst(ast, context),
2408
+ predicates: context.predicates,
2409
+ sources: context.sources
2410
+ };
2411
+ }
2412
+ function pushdownIcebergPredicatesFromAst(ast, context) {
2413
+ if (context.seen.has(ast)) return ast;
2414
+ context.seen.add(ast);
2415
+ const out = {
2416
+ ...ast,
2417
+ source: icebergOccurrenceSource(ast.source, context)
2418
+ };
2419
+ if (ast.join !== void 0) {
2420
+ out.join = { ...ast.join, source: icebergOccurrenceSource(ast.join.source, context) };
2421
+ }
2422
+ if (ast.joins !== void 0) {
2423
+ out.joins = ast.joins.map((join) => ({
2424
+ ...join,
2425
+ source: icebergOccurrenceSource(join.source, context)
2426
+ }));
2427
+ const firstJoin = out.joins[0];
2428
+ if (firstJoin !== void 0) out.join = firstJoin;
2429
+ }
2430
+ if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
2431
+ out.subqueryJoin = {
2432
+ ...ast.subqueryJoin,
2433
+ source: icebergOccurrenceSource(ast.subqueryJoin.source, context)
2434
+ };
2435
+ }
2436
+ if (ast.subqueryJoins !== void 0) {
2437
+ out.subqueryJoins = ast.subqueryJoins.map((join) => ({
2438
+ ...join,
2439
+ source: icebergOccurrenceSource(join.source, context)
2440
+ }));
2441
+ const firstSubqueryJoin = out.subqueryJoins[0];
2442
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
2443
+ }
2444
+ const aliases = sourceAliases(out);
2445
+ if (ast.where !== void 0) {
2446
+ const remaining = [];
2447
+ for (const predicate of splitAndPredicates(ast.where)) {
2448
+ const source = predicateSource(predicate, aliases);
2449
+ if (source !== void 0 && context.sources.has(source)) {
2450
+ addPushdownPredicate(
2451
+ context.predicates,
2452
+ source,
2453
+ stripExprSource(predicate, aliases, source)
2454
+ );
2455
+ }
2456
+ remaining.push(predicate);
2457
+ }
2458
+ const where = combineAndPredicates(remaining);
2459
+ if (where === void 0) delete out.where;
2460
+ else out.where = where;
2461
+ }
2462
+ const pushedSubqueryJoins = sqlSubqueryJoins(ast).map((join, index) => {
2463
+ const outJoin = sqlSubqueryJoins(out)[index];
2464
+ if (join.where === void 0 || outJoin === void 0) return outJoin ?? join;
2465
+ const source = outJoin.source;
2466
+ if (source !== void 0 && context.sources.has(source)) {
2467
+ const subqueryAliases = /* @__PURE__ */ new Map([[source, source]]);
2468
+ const remaining = [];
2469
+ for (const predicate of splitAndPredicates(join.where)) {
2470
+ const predicateTable = predicateSource(predicate, subqueryAliases);
2471
+ if (predicateTable === source) {
2472
+ addPushdownPredicate(
2473
+ context.predicates,
2474
+ source,
2475
+ stripExprSource(predicate, subqueryAliases, source)
2476
+ );
2477
+ }
2478
+ remaining.push(predicate);
2479
+ }
2480
+ const where = combineAndPredicates(remaining);
2481
+ const next = { ...outJoin };
2482
+ if (where === void 0) delete next.where;
2483
+ else next.where = where;
2484
+ return next;
2485
+ }
2486
+ return outJoin;
2487
+ });
2488
+ if (pushedSubqueryJoins.length > 0) {
2489
+ out.subqueryJoins = pushedSubqueryJoins;
2490
+ const firstSubqueryJoin = pushedSubqueryJoins[0];
2491
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
2492
+ }
2493
+ if (ast.cte !== void 0) {
2494
+ out.cte = {
2495
+ ...ast.cte,
2496
+ query: pushdownIcebergPredicatesFromAst(ast.cte.query, context)
2497
+ };
2498
+ }
2499
+ if (ast.scalarSubqueries !== void 0) {
2500
+ out.scalarSubqueries = Object.fromEntries(
2501
+ Object.entries(ast.scalarSubqueries).map(([id, subquery]) => [
2502
+ id,
2503
+ {
2504
+ ...subquery,
2505
+ query: pushdownIcebergPredicatesFromAst(subquery.query, context)
2506
+ }
2507
+ ])
2508
+ );
2509
+ }
2510
+ if (ast.correlatedScalarSubqueries !== void 0) {
2511
+ out.correlatedScalarSubqueries = Object.fromEntries(
2512
+ Object.entries(ast.correlatedScalarSubqueries).map(([id, subquery]) => [
2513
+ id,
2514
+ { ...subquery, source: icebergOccurrenceSource(subquery.source, context) }
2515
+ ])
2516
+ );
2517
+ }
2518
+ return out;
2519
+ }
2520
+ function icebergOccurrenceSource(source, context) {
2521
+ const table = context.icebergTables[source];
2522
+ if (table === void 0) return source;
2523
+ const occurrence = `__lakeql_sql_iceberg_source_${context.nextSourceId}_${safeTempName(source)}`;
2524
+ context.nextSourceId += 1;
2525
+ context.sources.set(occurrence, table);
2526
+ return occurrence;
2527
+ }
2528
+ function splitAndPredicates(expr) {
2529
+ if (expr.kind !== "logical" || expr.op !== "and") return [expr];
2530
+ return expr.operands.flatMap(splitAndPredicates);
2531
+ }
2532
+ function combineAndPredicates(predicates) {
2533
+ if (predicates.length === 0) return void 0;
2534
+ if (predicates.length === 1) return predicates[0];
2535
+ return { kind: "logical", op: "and", operands: [...predicates] };
2536
+ }
2537
+ function addPushdownPredicate(predicates, source, predicate) {
2538
+ const existing = predicates.get(source);
2539
+ predicates.set(
2540
+ source,
2541
+ existing === void 0 ? predicate : { kind: "logical", op: "and", operands: [existing, predicate] }
2542
+ );
2543
+ }
2544
+ function predicateSource(expr, aliases) {
2545
+ const columns = /* @__PURE__ */ new Set();
2546
+ collectExprColumns2(expr, columns);
2547
+ let source;
2548
+ for (const column of columns) {
2549
+ const columnSource = columnSourceName(column, aliases);
2550
+ if (columnSource === void 0) return void 0;
2551
+ if (source === void 0) source = columnSource;
2552
+ else if (source !== columnSource) return void 0;
2553
+ }
2554
+ return source;
2555
+ }
2556
+ function columnSourceName(column, aliases) {
2557
+ const dot = column.indexOf(".");
2558
+ if (dot > 0) return aliases.get(column.slice(0, dot));
2559
+ if (aliases.size !== 1) return void 0;
2560
+ return aliases.values().next().value;
2561
+ }
2562
+ function stripExprSource(expr, aliases, source) {
2563
+ switch (expr.kind) {
2564
+ case "column":
2565
+ return { ...expr, name: stripSourceColumn(expr.name, aliases, source) };
2566
+ case "compare":
2567
+ return {
2568
+ ...expr,
2569
+ left: stripExprSource(expr.left, aliases, source),
2570
+ right: stripExprSource(expr.right, aliases, source)
2571
+ };
2572
+ case "between":
2573
+ return {
2574
+ ...expr,
2575
+ target: stripExprSource(expr.target, aliases, source),
2576
+ low: stripExprSource(expr.low, aliases, source),
2577
+ high: stripExprSource(expr.high, aliases, source)
2578
+ };
2579
+ case "in":
2580
+ return {
2581
+ ...expr,
2582
+ target: stripExprSource(expr.target, aliases, source),
2583
+ values: expr.values.map((value) => stripExprSource(value, aliases, source))
2584
+ };
2585
+ case "logical":
2586
+ return {
2587
+ ...expr,
2588
+ operands: expr.operands.map((operand) => stripExprSource(operand, aliases, source))
2589
+ };
2590
+ case "not":
2591
+ return { ...expr, operand: stripExprSource(expr.operand, aliases, source) };
2592
+ case "null-check":
2593
+ return { ...expr, target: stripExprSource(expr.target, aliases, source) };
2594
+ case "like":
2595
+ return { ...expr, target: stripExprSource(expr.target, aliases, source) };
2596
+ case "call":
2597
+ return { ...expr, args: expr.args.map((arg) => stripExprSource(arg, aliases, source)) };
2598
+ case "arithmetic":
2599
+ return {
2600
+ ...expr,
2601
+ left: stripExprSource(expr.left, aliases, source),
2602
+ right: stripExprSource(expr.right, aliases, source)
2603
+ };
2604
+ case "case":
2605
+ return {
2606
+ ...expr,
2607
+ whens: expr.whens.map((branch) => ({
2608
+ when: stripExprSource(branch.when, aliases, source),
2609
+ value: stripExprSource(branch.value, aliases, source)
2610
+ })),
2611
+ ...expr.else === void 0 ? {} : { else: stripExprSource(expr.else, aliases, source) }
2612
+ };
2613
+ case "literal":
2614
+ return expr;
2615
+ }
2616
+ }
2617
+ function stripSourceColumn(column, aliases, source) {
2618
+ const dot = column.indexOf(".");
2619
+ if (dot <= 0) return column;
2620
+ const qualifier = column.slice(0, dot);
2621
+ return aliases.get(qualifier) === source ? column.slice(dot + 1) : column;
2622
+ }
2623
+ function collectSourceColumns(ast) {
2624
+ const columns = /* @__PURE__ */ new Map();
2625
+ collectSourceColumnsFromAst(ast, columns);
2626
+ return columns;
2627
+ }
2628
+ function collectSourceColumnsFromAst(ast, columns, seen = /* @__PURE__ */ new WeakSet()) {
2629
+ if (seen.has(ast)) return;
2630
+ seen.add(ast);
2631
+ const aliases = sourceAliases(ast);
2632
+ const referenced = /* @__PURE__ */ new Set();
2633
+ for (const select of ast.select ?? []) referenced.add(select);
2634
+ for (const expr of Object.values(ast.projections ?? {})) collectExprColumns2(expr, referenced);
2635
+ for (const aggregate of Object.values(ast.aggregates ?? {})) {
2636
+ if (aggregate.column !== void 0) referenced.add(aggregate.column);
2637
+ if (aggregate.expr !== void 0) collectExprColumns2(aggregate.expr, referenced);
2638
+ }
2639
+ for (const column of ast.groupBy ?? []) referenced.add(column);
2640
+ for (const term of ast.orderBy ?? []) referenced.add(term.column);
2641
+ if (ast.where !== void 0) collectExprColumns2(ast.where, referenced);
2642
+ if (ast.having !== void 0) collectExprColumns2(ast.having, referenced);
2643
+ if (ast.qualify !== void 0) collectExprColumns2(ast.qualify, referenced);
2644
+ for (const window of Object.values(ast.windows ?? {})) {
2645
+ for (const expr of window.args) collectExprColumns2(expr, referenced);
2646
+ for (const expr of window.over.partitionBy) collectExprColumns2(expr, referenced);
2647
+ for (const term of window.over.orderBy) collectExprColumns2(term.expr, referenced);
2648
+ if (window.over.frame?.start.offset !== void 0)
2649
+ collectExprColumns2(window.over.frame.start.offset, referenced);
2650
+ if (window.over.frame?.end.offset !== void 0)
2651
+ collectExprColumns2(window.over.frame.end.offset, referenced);
2652
+ if (window.filter !== void 0) collectExprColumns2(window.filter, referenced);
2653
+ }
2654
+ for (const join of sqlJoins(ast)) {
2655
+ for (const column of join.leftKey) referenced.add(column);
2656
+ for (const column of join.rightKey) referenced.add(column);
2657
+ if (join.predicate !== void 0) collectExprColumns2(join.predicate, referenced);
2658
+ }
2659
+ for (const subqueryJoin of sqlSubqueryJoins(ast)) {
2660
+ for (const column of subqueryJoin.leftKey) referenced.add(column);
2661
+ for (const column of subqueryJoin.rightKey) referenced.add(column);
2662
+ for (const column of subqueryJoin.groupBy ?? []) referenced.add(column);
2663
+ for (const aggregate of Object.values(subqueryJoin.aggregates ?? {})) {
2664
+ if (aggregate.column !== void 0) referenced.add(aggregate.column);
2665
+ if (aggregate.expr !== void 0) collectExprColumns2(aggregate.expr, referenced);
2666
+ }
2667
+ if (subqueryJoin.where !== void 0) collectExprColumns2(subqueryJoin.where, referenced);
2668
+ if (subqueryJoin.having !== void 0) collectExprColumns2(subqueryJoin.having, referenced);
2669
+ if (subqueryJoin.predicate !== void 0)
2670
+ collectExprColumns2(subqueryJoin.predicate, referenced);
2671
+ for (const term of subqueryJoin.orderBy ?? []) referenced.add(term.column);
2672
+ }
2673
+ for (const subquery of Object.values(ast.correlatedScalarSubqueries ?? {})) {
2674
+ for (const column of subquery.leftKey) referenced.add(column);
2675
+ for (const column of subquery.rightKey) addColumn(columns, subquery.source, column);
2676
+ for (const column of subquery.groupBy) addColumn(columns, subquery.source, column);
2677
+ for (const aggregate of Object.values(subquery.aggregates)) {
2678
+ if (aggregate.column !== void 0) addColumn(columns, subquery.source, aggregate.column);
2679
+ if (aggregate.expr !== void 0)
2680
+ collectExprColumnsForSource(aggregate.expr, columns, subquery.source);
2681
+ }
2682
+ if (subquery.where !== void 0)
2683
+ collectExprColumnsForSource(subquery.where, columns, subquery.source);
2684
+ if (subquery.having !== void 0)
2685
+ collectExprColumnsForSource(subquery.having, columns, subquery.source);
2686
+ }
2687
+ for (const column of referenced) addSourceColumn(columns, aliases, column);
2688
+ if (ast.cte !== void 0) collectSourceColumnsFromAst(ast.cte.query, columns, seen);
2689
+ for (const subquery of Object.values(ast.scalarSubqueries ?? {})) {
2690
+ collectSourceColumnsFromAst(subquery.query, columns, seen);
2691
+ }
2692
+ }
2693
+ function sourceAliases(ast) {
2694
+ const aliases = /* @__PURE__ */ new Map([[ast.source, ast.source]]);
2695
+ const joins = sqlJoins(ast);
2696
+ const firstJoin = joins[0];
2697
+ if (firstJoin !== void 0) aliases.set(firstJoin.leftAlias, ast.source);
2698
+ for (const join of joins) {
2699
+ aliases.set(join.source, join.source);
2700
+ aliases.set(join.alias, join.source);
2701
+ }
2702
+ return aliases;
2703
+ }
2704
+ function addSourceColumn(columns, aliases, column) {
2705
+ if (column === "*" || column.endsWith(".*")) return;
2706
+ const dot = column.indexOf(".");
2707
+ if (dot > 0) {
2708
+ const source2 = aliases.get(column.slice(0, dot));
2709
+ if (source2 === void 0) return;
2710
+ addColumn(columns, source2, column.slice(dot + 1));
2711
+ return;
2712
+ }
2713
+ if (aliases.size !== 1) return;
2714
+ const source = aliases.values().next().value;
2715
+ if (source !== void 0) addColumn(columns, source, column);
2716
+ }
2717
+ function collectExprColumnsForSource(expr, columns, source) {
2718
+ const referenced = /* @__PURE__ */ new Set();
2719
+ collectExprColumns2(expr, referenced);
2720
+ for (const column of referenced) addColumn(columns, source, column);
2721
+ }
2722
+ function addColumn(columns, source, column) {
2723
+ let sourceColumns = columns.get(source);
2724
+ if (sourceColumns === void 0) {
2725
+ sourceColumns = /* @__PURE__ */ new Set();
2726
+ columns.set(source, sourceColumns);
2727
+ }
2728
+ sourceColumns.add(column);
2729
+ }
2730
+ function icebergPlanOptions(table) {
2731
+ const options = {};
2732
+ if (table.snapshotId !== void 0) options.snapshotId = table.snapshotId;
2733
+ if (table.asOfTimestampMs !== void 0) options.asOfTimestampMs = table.asOfTimestampMs;
2734
+ if (table.ref !== void 0) options.ref = table.ref;
2735
+ if (table.readMode !== void 0) options.readMode = table.readMode;
2736
+ return options;
2737
+ }
2170
2738
  function nextSqlTempId() {
2171
2739
  const randomUuid = globalThis.crypto?.randomUUID?.();
2172
2740
  if (randomUuid !== void 0) return randomUuid;
@@ -2181,6 +2749,10 @@ async function resolveScalarSubqueries(lake, ast) {
2181
2749
  const values = /* @__PURE__ */ new Map();
2182
2750
  for (const [id, subquery] of Object.entries(ast.scalarSubqueries)) {
2183
2751
  const rows = hasAggregation(subquery.query) ? await aggregateRowsFromAst(lake.path(subquery.query.source), subquery.query) : await builderFromAst(lake.path(subquery.query.source), subquery.query).toArray();
2752
+ if (subquery.mode === "exists") {
2753
+ values.set(id, rows.length > 0);
2754
+ continue;
2755
+ }
2184
2756
  if (rows.length > 1) {
2185
2757
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
2186
2758
  }
@@ -2197,6 +2769,30 @@ async function resolveScalarSubqueries(lake, ast) {
2197
2769
  ])
2198
2770
  );
2199
2771
  }
2772
+ if (ast.joins !== void 0) {
2773
+ out.joins = ast.joins.map(
2774
+ (join) => join.predicate === void 0 ? join : { ...join, predicate: replaceScalarSubqueryExpr(join.predicate, values) }
2775
+ );
2776
+ const firstJoin = out.joins[0];
2777
+ if (firstJoin !== void 0) out.join = firstJoin;
2778
+ } else if (ast.join?.predicate !== void 0) {
2779
+ out.join = { ...ast.join, predicate: replaceScalarSubqueryExpr(ast.join.predicate, values) };
2780
+ }
2781
+ if (ast.subqueryJoins !== void 0) {
2782
+ out.subqueryJoins = ast.subqueryJoins.map((join) => ({
2783
+ ...join,
2784
+ ...join.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(join.where, values) },
2785
+ ...join.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(join.predicate, values) }
2786
+ }));
2787
+ const firstSubqueryJoin = out.subqueryJoins[0];
2788
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
2789
+ } else if (ast.subqueryJoin?.where !== void 0 || ast.subqueryJoin?.predicate !== void 0) {
2790
+ out.subqueryJoin = {
2791
+ ...ast.subqueryJoin,
2792
+ ...ast.subqueryJoin.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(ast.subqueryJoin.where, values) },
2793
+ ...ast.subqueryJoin.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(ast.subqueryJoin.predicate, values) }
2794
+ };
2795
+ }
2200
2796
  delete out.scalarSubqueries;
2201
2797
  return out;
2202
2798
  }
@@ -2262,221 +2858,402 @@ function replaceScalarSubqueryExpr(expr, values) {
2262
2858
  }
2263
2859
  }
2264
2860
  async function joinRowsFromAst(lake, ast, options) {
2265
- if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
2266
- const join = ast.join;
2861
+ const joins = sqlJoins(ast);
2862
+ const firstJoin = joins[0];
2863
+ if (firstJoin === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
2267
2864
  if (hasWindowSemantics(ast)) {
2268
2865
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over JOIN is not supported");
2269
2866
  }
2270
2867
  if (hasAggregation(ast)) {
2271
2868
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
2272
2869
  }
2273
- const leftAlias = ast.join.leftAlias;
2274
- const plan = planJoinSides(ast, leftAlias, join.alias);
2275
- let leftBuilder = lake.path(ast.source);
2276
- let rightBuilder = lake.path(join.source);
2277
- if (plan.leftWhere !== void 0) leftBuilder = leftBuilder.where(plan.leftWhere);
2278
- if (plan.rightWhere !== void 0) rightBuilder = rightBuilder.where(plan.rightWhere);
2279
- if (plan.leftColumns !== void 0) leftBuilder = leftBuilder.select(plan.leftColumns);
2280
- if (plan.rightColumns !== void 0) rightBuilder = rightBuilder.select(plan.rightColumns);
2281
- const leftRows = (await leftBuilder.toArray()).map((row) => qualifyRow(row, leftAlias));
2282
- const rightRows = (await rightBuilder.toArray()).map((row) => qualifyOnlyRow(row, join.alias));
2283
- let rows = await broadcastJoin(leftRows, rightRows, {
2284
- leftKey: join.leftKey,
2285
- rightKey: join.rightKey,
2286
- type: join.type,
2287
- rightPrefix: `${join.alias}.`,
2288
- maxRightRows: options.joinMaxRightRows ?? 1e5
2289
- });
2290
- if (join.type === "left") {
2291
- rows = fillLeftJoinNulls(rows, join.alias, leftJoinRightColumns(ast, join.alias, rightRows));
2870
+ const pushedFilters = pushdownJoinFilters(ast, joins);
2871
+ const readColumns = joinReadColumns(ast, joins);
2872
+ let leftRows = await rowsForJoinAlias(lake.path(ast.source), readColumns, firstJoin.leftAlias);
2873
+ const leftFilter = pushedFilters.filters.get(firstJoin.leftAlias);
2874
+ if (leftFilter !== void 0) leftRows = leftRows.filter((row) => matches(leftFilter, row));
2875
+ let rows = leftRows.map((row) => qualifyRow(row, firstJoin.leftAlias));
2876
+ for (const join of joins) {
2877
+ let rawRightRows = await rowsForJoinAlias(lake.path(join.source), readColumns, join.alias);
2878
+ const rightFilter = pushedFilters.filters.get(join.alias);
2879
+ if (rightFilter !== void 0)
2880
+ rawRightRows = rawRightRows.filter((row) => matches(rightFilter, row));
2881
+ const rightRows = rawRightRows.map((row) => qualifyOnlyRow(row, join.alias));
2882
+ if (join.predicate !== void 0) {
2883
+ rows = predicateJoinRows(rows, rightRows, ast, join, options);
2884
+ } else if (join.type === "right") {
2885
+ const leftRows2 = rows;
2886
+ rows = await broadcastJoin(rightRows, leftRows2, {
2887
+ leftKey: join.rightKey,
2888
+ rightKey: join.leftKey,
2889
+ type: "left",
2890
+ rightPrefix: `${join.leftAlias}.`,
2891
+ maxRightRows: options.joinMaxRightRows ?? 1e5
2892
+ });
2893
+ rows = fillRightJoinNulls(rows, ast, join.alias, leftRows2);
2894
+ } else if (join.type === "full") {
2895
+ const leftRows2 = rows;
2896
+ const leftJoined = await broadcastJoin(leftRows2, rightRows, {
2897
+ leftKey: join.leftKey,
2898
+ rightKey: join.rightKey,
2899
+ type: "left",
2900
+ rightPrefix: `${join.alias}.`,
2901
+ maxRightRows: options.joinMaxRightRows ?? 1e5
2902
+ });
2903
+ const unmatchedRight = await broadcastJoin(rightRows, leftRows2, {
2904
+ leftKey: join.rightKey,
2905
+ rightKey: join.leftKey,
2906
+ type: "anti",
2907
+ rightPrefix: `${join.leftAlias}.`,
2908
+ maxRightRows: options.joinMaxRightRows ?? 1e5
2909
+ });
2910
+ rows = [
2911
+ ...fillLeftJoinNulls(
2912
+ leftJoined,
2913
+ join.alias,
2914
+ leftJoinRightColumns(ast, join.alias, rightRows)
2915
+ ),
2916
+ ...fillRightJoinNulls(unmatchedRight, ast, join.alias, leftRows2)
2917
+ ];
2918
+ } else {
2919
+ rows = join.type === "cross" ? await crossJoin(rows, rightRows, {
2920
+ rightPrefix: `${join.alias}.`,
2921
+ maxRightRows: options.joinMaxRightRows ?? 1e5,
2922
+ maxOutputRows: options.joinMaxOutputRows ?? 1e5
2923
+ }) : await broadcastJoin(rows, rightRows, {
2924
+ leftKey: join.leftKey,
2925
+ rightKey: join.rightKey,
2926
+ type: join.type,
2927
+ rightPrefix: `${join.alias}.`,
2928
+ maxRightRows: options.joinMaxRightRows ?? 1e5
2929
+ });
2930
+ }
2931
+ if (join.type === "left") {
2932
+ rows = fillLeftJoinNulls(rows, join.alias, leftJoinRightColumns(ast, join.alias, rightRows));
2933
+ }
2292
2934
  }
2293
- if (plan.residualWhere !== void 0)
2294
- rows = rows.filter((row) => matches(plan.residualWhere, row));
2295
- if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
2296
- rows = projectRows(rows, ast);
2935
+ if (pushedFilters.where !== void 0)
2936
+ rows = rows.filter((row) => matches(pushedFilters.where, row));
2937
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
2938
+ rows = await projectRows(lake, rows, ast);
2297
2939
  if (ast.distinct === true) rows = distinctRows(rows);
2298
2940
  return offsetLimitRows(rows, ast);
2299
2941
  }
2300
- function planJoinSides(ast, leftAlias, rightAlias) {
2301
- if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
2302
- const leftPredicates = [];
2303
- const rightPredicates = [];
2304
- const residualPredicates = [];
2305
- for (const predicate of splitAndPredicate(ast.where)) {
2306
- const columns = exprColumns(predicate);
2307
- if (columns.length > 0 && columns.every((column) => isQualifiedBy(column, leftAlias))) {
2308
- leftPredicates.push(stripQualifiedExpr(predicate, leftAlias));
2309
- } else if (ast.join.type === "inner" && columns.length > 0 && columns.every((column) => isQualifiedBy(column, rightAlias))) {
2310
- rightPredicates.push(stripQualifiedExpr(predicate, rightAlias));
2311
- } else {
2312
- residualPredicates.push(predicate);
2942
+ function predicateJoinRows(leftRows, rightRows, ast, join, options) {
2943
+ if (join.predicate === void 0) return leftRows;
2944
+ const maxRightRows = options.joinMaxRightRows ?? 1e5;
2945
+ if (rightRows.length > maxRightRows) {
2946
+ throw new LakeqlError(
2947
+ "LAKEQL_BUDGET_EXCEEDED",
2948
+ `Predicate join exceeded maxRightRows (${rightRows.length} > ${maxRightRows})`,
2949
+ { metric: "maxRightRows", limit: maxRightRows, actual: rightRows.length }
2950
+ );
2951
+ }
2952
+ const maxOutputRows = options.joinMaxOutputRows ?? 1e5;
2953
+ const matchedLeft = /* @__PURE__ */ new Set();
2954
+ const matchedRight = /* @__PURE__ */ new Set();
2955
+ const out = [];
2956
+ let candidateRows = 0;
2957
+ for (const [leftIndex, leftRow] of leftRows.entries()) {
2958
+ for (const [rightIndex, rightRow] of rightRows.entries()) {
2959
+ candidateRows += 1;
2960
+ if (candidateRows > maxOutputRows) {
2961
+ throw new LakeqlError(
2962
+ "LAKEQL_BUDGET_EXCEEDED",
2963
+ `Predicate join exceeded maxOutputRows (${candidateRows} > ${maxOutputRows})`,
2964
+ { metric: "maxOutputRows", limit: maxOutputRows, actual: candidateRows }
2965
+ );
2966
+ }
2967
+ const joined = mergePredicateJoinRows(leftRow, rightRow, `${join.alias}.`);
2968
+ if (!matches(join.predicate, joined)) continue;
2969
+ matchedLeft.add(leftIndex);
2970
+ matchedRight.add(rightIndex);
2971
+ out.push(joined);
2972
+ }
2973
+ }
2974
+ if (join.type === "left" || join.type === "full") {
2975
+ const unmatchedLeft = leftRows.filter((_row, index) => !matchedLeft.has(index));
2976
+ out.push(
2977
+ ...fillLeftJoinNulls(
2978
+ unmatchedLeft,
2979
+ join.alias,
2980
+ leftJoinRightColumns(ast, join.alias, rightRows)
2981
+ )
2982
+ );
2983
+ }
2984
+ if (join.type === "right" || join.type === "full") {
2985
+ const unmatchedRight = rightRows.filter((_row, index) => !matchedRight.has(index));
2986
+ out.push(...fillRightJoinNulls(unmatchedRight, ast, join.alias, leftRows));
2987
+ }
2988
+ return out;
2989
+ }
2990
+ function mergePredicateJoinRows(left, right, rightPrefix) {
2991
+ const out = { ...left };
2992
+ for (const [key, value] of Object.entries(right)) {
2993
+ const outKey = key in out ? `${rightPrefix}${key}` : key;
2994
+ out[outKey] = value;
2995
+ }
2996
+ return out;
2997
+ }
2998
+ function sqlJoins(ast) {
2999
+ if (ast.joins !== void 0) return ast.joins;
3000
+ return ast.join === void 0 ? [] : [ast.join];
3001
+ }
3002
+ function sqlSubqueryJoins(ast) {
3003
+ if (ast.subqueryJoins !== void 0) return ast.subqueryJoins;
3004
+ return ast.subqueryJoin === void 0 ? [] : [ast.subqueryJoin];
3005
+ }
3006
+ function pushdownJoinFilters(ast, joins) {
3007
+ const filters = /* @__PURE__ */ new Map();
3008
+ if (ast.where === void 0) {
3009
+ return { filters, ...ast.where === void 0 ? {} : { where: ast.where } };
3010
+ }
3011
+ const removableAliases = nonNullableJoinAliases(joins);
3012
+ const aliases = /* @__PURE__ */ new Set();
3013
+ const firstJoin = joins[0];
3014
+ if (firstJoin !== void 0) aliases.add(firstJoin.leftAlias);
3015
+ for (const join of joins) aliases.add(join.alias);
3016
+ const remaining = [];
3017
+ for (const predicate of splitAndPredicates(ast.where)) {
3018
+ const alias = predicateJoinAlias(predicate, aliases);
3019
+ if (alias === void 0) {
3020
+ remaining.push(predicate);
3021
+ continue;
2313
3022
  }
3023
+ addJoinFilter(filters, alias, stripExprJoinAlias(predicate, alias));
3024
+ if (!removableAliases.has(alias)) remaining.push(predicate);
3025
+ }
3026
+ const where = combineAndPredicates(remaining);
3027
+ return { filters, ...where === void 0 ? {} : { where } };
3028
+ }
3029
+ function nonNullableJoinAliases(joins) {
3030
+ const aliases = /* @__PURE__ */ new Set();
3031
+ const firstJoin = joins[0];
3032
+ if (firstJoin === void 0) return aliases;
3033
+ aliases.add(firstJoin.leftAlias);
3034
+ for (const join of joins) {
3035
+ switch (join.type) {
3036
+ case "inner":
3037
+ case "cross":
3038
+ case "left":
3039
+ if (join.type !== "left") aliases.add(join.alias);
3040
+ break;
3041
+ case "right":
3042
+ aliases.clear();
3043
+ aliases.add(join.alias);
3044
+ break;
3045
+ case "full":
3046
+ aliases.clear();
3047
+ break;
3048
+ }
3049
+ }
3050
+ return aliases;
3051
+ }
3052
+ async function rowsForJoinAlias(builder, readColumns, alias) {
3053
+ const columns = readColumns?.get(alias);
3054
+ if (columns === void 0 || columns.size === 0) return await builder.toArray();
3055
+ return await builder.select([...columns]).toArray();
3056
+ }
3057
+ function joinReadColumns(ast, joins) {
3058
+ const aliases = /* @__PURE__ */ new Set();
3059
+ const firstJoin = joins[0];
3060
+ if (firstJoin !== void 0) aliases.add(firstJoin.leftAlias);
3061
+ for (const join of joins) aliases.add(join.alias);
3062
+ const columns = /* @__PURE__ */ new Map();
3063
+ for (const select of ast.select ?? []) {
3064
+ const { column } = selectColumn(select);
3065
+ if (!addJoinReadColumn(columns, aliases, column)) return void 0;
3066
+ }
3067
+ for (const expr of Object.values(ast.projections ?? {})) {
3068
+ if (!addJoinReadExpr(columns, aliases, expr)) return void 0;
3069
+ }
3070
+ if (ast.where !== void 0 && !addJoinReadExpr(columns, aliases, ast.where)) return void 0;
3071
+ for (const term of ast.orderBy ?? []) {
3072
+ if (!addJoinOrderByReadColumns(columns, aliases, term.column, ast)) return void 0;
2314
3073
  }
2315
- const plan = {};
2316
- const leftWhere = combineAndPredicate(leftPredicates);
2317
- const rightWhere = combineAndPredicate(rightPredicates);
2318
- const residualWhere = combineAndPredicate(residualPredicates);
2319
- if (leftWhere !== void 0) plan.leftWhere = leftWhere;
2320
- if (rightWhere !== void 0) plan.rightWhere = rightWhere;
2321
- if (residualWhere !== void 0) plan.residualWhere = residualWhere;
2322
- if (canProjectJoinSides(ast, leftAlias, rightAlias)) {
2323
- plan.leftColumns = joinSideColumns(ast, leftAlias, ast.join.leftKey);
2324
- plan.rightColumns = joinSideColumns(ast, rightAlias, ast.join.rightKey);
3074
+ for (const join of joins) {
3075
+ for (const column of join.leftKey) {
3076
+ if (!addJoinReadColumn(columns, aliases, column)) return void 0;
3077
+ }
3078
+ for (const column of join.rightKey) {
3079
+ if (!addJoinReadColumn(columns, aliases, column)) return void 0;
3080
+ }
3081
+ if (join.predicate !== void 0 && !addJoinReadExpr(columns, aliases, join.predicate)) {
3082
+ return void 0;
3083
+ }
2325
3084
  }
2326
- return plan;
3085
+ return columns;
2327
3086
  }
2328
- function splitAndPredicate(expr) {
2329
- if (expr === void 0) return [];
2330
- if (expr.kind === "logical" && expr.op === "and") return expr.operands.flatMap(splitAndPredicate);
2331
- return [expr];
3087
+ function addJoinOrderByReadColumns(columns, aliases, column, ast) {
3088
+ const projection = ast.projections?.[column];
3089
+ if (projection !== void 0) return addJoinReadExpr(columns, aliases, projection);
3090
+ const select = ast.select?.find((value) => selectColumn(value).alias === column);
3091
+ if (select !== void 0) return addJoinReadColumn(columns, aliases, selectColumn(select).column);
3092
+ return addJoinReadColumn(columns, aliases, column);
2332
3093
  }
2333
- function combineAndPredicate(predicates) {
2334
- if (predicates.length === 0) return void 0;
2335
- if (predicates.length === 1) return predicates[0];
2336
- return { kind: "logical", op: "and", operands: predicates };
3094
+ function addJoinReadExpr(columns, aliases, expr) {
3095
+ const referenced = /* @__PURE__ */ new Set();
3096
+ collectExprColumns2(expr, referenced);
3097
+ for (const column of referenced) {
3098
+ if (!addJoinReadColumn(columns, aliases, column)) return false;
3099
+ }
3100
+ return true;
2337
3101
  }
2338
- function isQualifiedBy(column, alias) {
2339
- return column.startsWith(`${alias}.`) && column.length > alias.length + 1;
3102
+ function addJoinReadColumn(columns, aliases, column) {
3103
+ if (column === "*" || column.endsWith(".*")) return false;
3104
+ const dot = column.indexOf(".");
3105
+ if (dot <= 0) return false;
3106
+ const alias = column.slice(0, dot);
3107
+ if (!aliases.has(alias)) return false;
3108
+ let aliasColumns = columns.get(alias);
3109
+ if (aliasColumns === void 0) {
3110
+ aliasColumns = /* @__PURE__ */ new Set();
3111
+ columns.set(alias, aliasColumns);
3112
+ }
3113
+ aliasColumns.add(column.slice(dot + 1));
3114
+ return true;
2340
3115
  }
2341
- function stripQualifiedColumn(column, alias) {
2342
- return isQualifiedBy(column, alias) ? column.slice(alias.length + 1) : column;
3116
+ function predicateJoinAlias(expr, aliases) {
3117
+ const columns = /* @__PURE__ */ new Set();
3118
+ collectExprColumns2(expr, columns);
3119
+ let alias;
3120
+ for (const column of columns) {
3121
+ const dot = column.indexOf(".");
3122
+ if (dot <= 0) return void 0;
3123
+ const columnAlias = column.slice(0, dot);
3124
+ if (!aliases.has(columnAlias)) return void 0;
3125
+ if (alias === void 0) alias = columnAlias;
3126
+ else if (alias !== columnAlias) return void 0;
3127
+ }
3128
+ return alias;
3129
+ }
3130
+ function addJoinFilter(filters, alias, predicate) {
3131
+ const existing = filters.get(alias);
3132
+ filters.set(
3133
+ alias,
3134
+ existing === void 0 ? predicate : { kind: "logical", op: "and", operands: [existing, predicate] }
3135
+ );
2343
3136
  }
2344
- function stripQualifiedExpr(expr, alias) {
3137
+ function stripExprJoinAlias(expr, alias) {
2345
3138
  switch (expr.kind) {
2346
3139
  case "column":
2347
3140
  return { ...expr, name: stripQualifiedColumn(expr.name, alias) };
2348
3141
  case "compare":
2349
3142
  return {
2350
3143
  ...expr,
2351
- left: stripQualifiedExpr(expr.left, alias),
2352
- right: stripQualifiedExpr(expr.right, alias)
3144
+ left: stripExprJoinAlias(expr.left, alias),
3145
+ right: stripExprJoinAlias(expr.right, alias)
2353
3146
  };
2354
3147
  case "between":
2355
3148
  return {
2356
3149
  ...expr,
2357
- target: stripQualifiedExpr(expr.target, alias),
2358
- low: stripQualifiedExpr(expr.low, alias),
2359
- high: stripQualifiedExpr(expr.high, alias)
3150
+ target: stripExprJoinAlias(expr.target, alias),
3151
+ low: stripExprJoinAlias(expr.low, alias),
3152
+ high: stripExprJoinAlias(expr.high, alias)
2360
3153
  };
2361
3154
  case "in":
2362
3155
  return {
2363
3156
  ...expr,
2364
- target: stripQualifiedExpr(expr.target, alias),
2365
- values: expr.values.map((value) => stripQualifiedExpr(value, alias))
3157
+ target: stripExprJoinAlias(expr.target, alias),
3158
+ values: expr.values.map((value) => stripExprJoinAlias(value, alias))
2366
3159
  };
2367
3160
  case "logical":
2368
3161
  return {
2369
3162
  ...expr,
2370
- operands: expr.operands.map((operand) => stripQualifiedExpr(operand, alias))
3163
+ operands: expr.operands.map((operand) => stripExprJoinAlias(operand, alias))
2371
3164
  };
2372
3165
  case "not":
2373
- return { ...expr, operand: stripQualifiedExpr(expr.operand, alias) };
3166
+ return { ...expr, operand: stripExprJoinAlias(expr.operand, alias) };
2374
3167
  case "null-check":
2375
- return { ...expr, target: stripQualifiedExpr(expr.target, alias) };
3168
+ return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
2376
3169
  case "like":
2377
- return { ...expr, target: stripQualifiedExpr(expr.target, alias) };
3170
+ return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
2378
3171
  case "call":
2379
- return { ...expr, args: expr.args.map((arg) => stripQualifiedExpr(arg, alias)) };
3172
+ return { ...expr, args: expr.args.map((arg) => stripExprJoinAlias(arg, alias)) };
2380
3173
  case "arithmetic":
2381
3174
  return {
2382
3175
  ...expr,
2383
- left: stripQualifiedExpr(expr.left, alias),
2384
- right: stripQualifiedExpr(expr.right, alias)
3176
+ left: stripExprJoinAlias(expr.left, alias),
3177
+ right: stripExprJoinAlias(expr.right, alias)
2385
3178
  };
2386
3179
  case "case":
2387
3180
  return {
2388
3181
  ...expr,
2389
3182
  whens: expr.whens.map((branch) => ({
2390
- when: stripQualifiedExpr(branch.when, alias),
2391
- value: stripQualifiedExpr(branch.value, alias)
3183
+ when: stripExprJoinAlias(branch.when, alias),
3184
+ value: stripExprJoinAlias(branch.value, alias)
2392
3185
  })),
2393
- ...expr.else === void 0 ? {} : { else: stripQualifiedExpr(expr.else, alias) }
3186
+ ...expr.else === void 0 ? {} : { else: stripExprJoinAlias(expr.else, alias) }
2394
3187
  };
2395
3188
  case "literal":
2396
3189
  return expr;
2397
3190
  }
2398
3191
  }
2399
- function exprColumns(expr) {
2400
- const columns = /* @__PURE__ */ new Set();
2401
- collectExprColumns(expr, columns);
2402
- return [...columns];
3192
+ function isQualifiedBy(column, alias) {
3193
+ return column.startsWith(`${alias}.`) && column.length > alias.length + 1;
3194
+ }
3195
+ function stripQualifiedColumn(column, alias) {
3196
+ return isQualifiedBy(column, alias) ? column.slice(alias.length + 1) : column;
2403
3197
  }
2404
- function collectExprColumns(expr, columns) {
3198
+ function collectExprColumns2(expr, columns) {
2405
3199
  switch (expr.kind) {
2406
3200
  case "column":
2407
3201
  columns.add(expr.name);
2408
3202
  return;
2409
3203
  case "compare":
2410
- collectExprColumns(expr.left, columns);
2411
- collectExprColumns(expr.right, columns);
3204
+ collectExprColumns2(expr.left, columns);
3205
+ collectExprColumns2(expr.right, columns);
2412
3206
  return;
2413
3207
  case "between":
2414
- collectExprColumns(expr.target, columns);
2415
- collectExprColumns(expr.low, columns);
2416
- collectExprColumns(expr.high, columns);
3208
+ collectExprColumns2(expr.target, columns);
3209
+ collectExprColumns2(expr.low, columns);
3210
+ collectExprColumns2(expr.high, columns);
2417
3211
  return;
2418
3212
  case "in":
2419
- collectExprColumns(expr.target, columns);
2420
- for (const value of expr.values) collectExprColumns(value, columns);
3213
+ collectExprColumns2(expr.target, columns);
3214
+ for (const value of expr.values) collectExprColumns2(value, columns);
2421
3215
  return;
2422
3216
  case "logical":
2423
- for (const operand of expr.operands) collectExprColumns(operand, columns);
3217
+ for (const operand of expr.operands) collectExprColumns2(operand, columns);
2424
3218
  return;
2425
3219
  case "not":
2426
- collectExprColumns(expr.operand, columns);
3220
+ collectExprColumns2(expr.operand, columns);
2427
3221
  return;
2428
3222
  case "null-check":
2429
- collectExprColumns(expr.target, columns);
3223
+ collectExprColumns2(expr.target, columns);
2430
3224
  return;
2431
3225
  case "like":
2432
- collectExprColumns(expr.target, columns);
3226
+ collectExprColumns2(expr.target, columns);
2433
3227
  return;
2434
3228
  case "call":
2435
- for (const arg of expr.args) collectExprColumns(arg, columns);
3229
+ for (const arg of expr.args) collectExprColumns2(arg, columns);
2436
3230
  return;
2437
3231
  case "arithmetic":
2438
- collectExprColumns(expr.left, columns);
2439
- collectExprColumns(expr.right, columns);
3232
+ collectExprColumns2(expr.left, columns);
3233
+ collectExprColumns2(expr.right, columns);
2440
3234
  return;
2441
3235
  case "case":
2442
3236
  for (const branch of expr.whens) {
2443
- collectExprColumns(branch.when, columns);
2444
- collectExprColumns(branch.value, columns);
3237
+ collectExprColumns2(branch.when, columns);
3238
+ collectExprColumns2(branch.value, columns);
2445
3239
  }
2446
- if (expr.else !== void 0) collectExprColumns(expr.else, columns);
3240
+ if (expr.else !== void 0) collectExprColumns2(expr.else, columns);
2447
3241
  return;
2448
3242
  case "literal":
2449
3243
  return;
2450
3244
  }
2451
3245
  }
2452
- function canProjectJoinSides(ast, leftAlias, rightAlias) {
2453
- if (ast.select?.includes("*") ?? false) return false;
2454
- return referencedJoinColumns(ast).every(
2455
- (column) => isQualifiedBy(column, leftAlias) || isQualifiedBy(column, rightAlias)
2456
- );
2457
- }
2458
3246
  function referencedJoinColumns(ast) {
2459
3247
  const columns = /* @__PURE__ */ new Set();
2460
3248
  for (const select of ast.select ?? []) {
2461
3249
  const { column } = selectColumn(select);
2462
3250
  columns.add(column);
2463
3251
  }
2464
- for (const expr of Object.values(ast.projections ?? {})) collectExprColumns(expr, columns);
2465
- if (ast.where !== void 0) collectExprColumns(ast.where, columns);
3252
+ for (const expr of Object.values(ast.projections ?? {})) collectExprColumns2(expr, columns);
3253
+ if (ast.where !== void 0) collectExprColumns2(ast.where, columns);
2466
3254
  for (const term of ast.orderBy ?? []) columns.add(term.column);
2467
3255
  return [...columns].filter((column) => column !== "*");
2468
3256
  }
2469
- function joinSideColumns(ast, alias, joinKey2) {
2470
- const columns = new Set(
2471
- (Array.isArray(joinKey2) ? joinKey2 : [joinKey2]).map(
2472
- (column) => stripQualifiedColumn(column, alias)
2473
- )
2474
- );
2475
- for (const column of referencedJoinColumns(ast)) {
2476
- if (isQualifiedBy(column, alias)) columns.add(stripQualifiedColumn(column, alias));
2477
- }
2478
- return [...columns];
2479
- }
2480
3257
  function leftJoinRightColumns(ast, rightAlias, rightRows) {
2481
3258
  const columns = /* @__PURE__ */ new Set();
2482
3259
  if (ast.select?.includes("*") ?? false) {
@@ -2505,33 +3282,213 @@ function fillLeftJoinNulls(rows, rightAlias, rightColumns) {
2505
3282
  return out ?? row;
2506
3283
  });
2507
3284
  }
3285
+ function fillRightJoinNulls(rows, ast, preservedAlias, leftRows) {
3286
+ const columns = rightJoinLeftColumns(ast, preservedAlias, leftRows);
3287
+ if (columns.length === 0) return rows;
3288
+ return rows.map((row) => {
3289
+ let out;
3290
+ for (const column of columns) {
3291
+ if (column in row) continue;
3292
+ out ??= { ...row };
3293
+ out[column] = null;
3294
+ }
3295
+ return out ?? row;
3296
+ });
3297
+ }
3298
+ function rightJoinLeftColumns(ast, preservedAlias, leftRows) {
3299
+ const columns = /* @__PURE__ */ new Set();
3300
+ if (ast.select?.includes("*") ?? false) {
3301
+ for (const row of leftRows) {
3302
+ for (const column of Object.keys(row)) {
3303
+ if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
3304
+ }
3305
+ }
3306
+ }
3307
+ for (const column of referencedJoinColumns(ast)) {
3308
+ if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
3309
+ }
3310
+ return [...columns];
3311
+ }
2508
3312
  async function subqueryJoinRowsFromAst(lake, ast, options) {
2509
- if (ast.subqueryJoin === void 0) {
3313
+ const joins = sqlSubqueryJoins(ast);
3314
+ if (joins.length === 0) {
2510
3315
  throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL IN subquery");
2511
3316
  }
2512
3317
  if (hasWindowSemantics(ast)) {
2513
3318
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over IN subquery is not supported");
2514
3319
  }
2515
3320
  if (hasAggregation(ast)) {
2516
- throw new LakeqlError(
2517
- "LAKEQL_SQL_UNSUPPORTED",
2518
- "Aggregate SQL over IN subquery is not supported yet"
2519
- );
3321
+ return await aggregateSubqueryJoinRowsFromAst(lake, ast, joins, options);
3322
+ }
3323
+ let rows = await lake.path(ast.source).toArray();
3324
+ for (const join of joins) {
3325
+ rows = await applySubqueryJoinRows(lake, rows, join, options);
3326
+ }
3327
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
3328
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
3329
+ rows = await projectRows(lake, rows, ast);
3330
+ if (ast.distinct === true) rows = distinctRows(rows);
3331
+ return offsetLimitRows(rows, ast);
3332
+ }
3333
+ async function aggregateSubqueryJoinRowsFromAst(lake, ast, joins, options) {
3334
+ let rows = await lake.path(ast.source).toArray();
3335
+ for (const join of joins) rows = await applySubqueryJoinRows(lake, rows, join, options);
3336
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
3337
+ const aggregateAst = subqueryJoinAggregateAst(ast);
3338
+ if (rows.length === 0) {
3339
+ if ((ast.groupBy?.length ?? 0) > 0) return [];
3340
+ return emptyAggregateRowsFromAst(aggregateAst);
3341
+ }
3342
+ const prefix = `__lakeql_sql_subquery/${safeTempName(ast.source)}/${nextSqlTempId()}`;
3343
+ const written = await writePartitionedParquet(lake.store, prefix, {
3344
+ rows,
3345
+ maxRowsPerFile: rows.length
3346
+ });
3347
+ try {
3348
+ return await aggregateRowsFromAst(lake.path(`${prefix}/*.parquet`), {
3349
+ ...aggregateAst,
3350
+ source: `${prefix}/*.parquet`
3351
+ });
3352
+ } finally {
3353
+ await Promise.all(written.files.map((file) => lake.store.delete(file.path)));
2520
3354
  }
2521
- const join = ast.subqueryJoin;
2522
- let rightRows = await lake.path(join.source).toArray();
2523
- if (join.where !== void 0) rightRows = rightRows.filter((row) => matches(join.where, row));
2524
- let rows = await broadcastJoin(await lake.path(ast.source).toArray(), rightRows, {
3355
+ }
3356
+ function subqueryJoinAggregateAst(ast) {
3357
+ const {
3358
+ subqueryJoin: _subqueryJoin,
3359
+ subqueryJoins: _subqueryJoins,
3360
+ where: _where,
3361
+ ...rest
3362
+ } = ast;
3363
+ return rest;
3364
+ }
3365
+ function emptyAggregateRowsFromAst(ast) {
3366
+ validateAggregateProjection(ast);
3367
+ const hiddenCountAlias = hiddenAggregateAlias(ast);
3368
+ const aggregateSpec = ast.aggregates === void 0 || Object.keys(ast.aggregates).length === 0 ? { [hiddenCountAlias]: { op: "count" } } : ast.aggregates;
3369
+ let rows = [emptyAggregateRow(aggregateSpec)];
3370
+ if (ast.having !== void 0) rows = rows.filter((row) => matches(ast.having, row));
3371
+ rows = projectAggregateRows(rows, ast, hiddenCountAlias);
3372
+ if (ast.distinct === true) rows = distinctRows(rows);
3373
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
3374
+ const offset = ast.offset ?? 0;
3375
+ if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
3376
+ else if (offset > 0) rows = rows.slice(offset);
3377
+ return rows;
3378
+ }
3379
+ function emptyAggregateRow(spec) {
3380
+ return Object.fromEntries(
3381
+ Object.entries(spec).map(([alias, aggregate]) => [alias, emptyAggregateValue(aggregate.op)])
3382
+ );
3383
+ }
3384
+ function emptyAggregateValue(op) {
3385
+ switch (op) {
3386
+ case "count":
3387
+ case "count_distinct":
3388
+ case "approx_count_distinct":
3389
+ return 0;
3390
+ case "sum":
3391
+ case "avg":
3392
+ case "var_samp":
3393
+ case "var_pop":
3394
+ case "stddev_samp":
3395
+ case "stddev_pop":
3396
+ case "median":
3397
+ case "quantile":
3398
+ case "min":
3399
+ case "max":
3400
+ case "mode":
3401
+ case "first":
3402
+ case "last":
3403
+ case "any":
3404
+ return null;
3405
+ }
3406
+ }
3407
+ async function applySubqueryJoinRows(lake, leftRows, join, options) {
3408
+ let rightRows = await subqueryJoinRightRows(lake, join);
3409
+ rightRows = offsetLimitSubqueryRows(rightRows, join);
3410
+ return join.predicate === void 0 ? await broadcastJoin(leftRows, rightRows, {
2525
3411
  leftKey: join.leftKey,
2526
3412
  rightKey: join.rightKey,
2527
3413
  type: join.type,
2528
3414
  maxRightRows: options.joinMaxRightRows ?? 1e5
2529
- });
2530
- if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
2531
- if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
2532
- rows = projectRows(rows, ast);
2533
- if (ast.distinct === true) rows = distinctRows(rows);
2534
- return offsetLimitRows(rows, ast);
3415
+ }) : predicateSubqueryJoinRows(leftRows, rightRows, join, options);
3416
+ }
3417
+ async function subqueryJoinRightRows(lake, join) {
3418
+ if (subqueryJoinHasAggregation(join)) {
3419
+ return await aggregateRowsFromAst(lake.path(join.source), {
3420
+ source: join.source,
3421
+ select: join.rightKey,
3422
+ ...join.where === void 0 ? {} : { where: join.where },
3423
+ ...join.groupBy === void 0 ? {} : { groupBy: join.groupBy },
3424
+ ...join.aggregates === void 0 ? {} : { aggregates: join.aggregates },
3425
+ ...join.hiddenAggregates === void 0 ? {} : { hiddenAggregates: join.hiddenAggregates },
3426
+ ...join.having === void 0 ? {} : { having: join.having },
3427
+ ...join.orderBy === void 0 ? {} : { orderBy: join.orderBy }
3428
+ });
3429
+ }
3430
+ let rows = await lake.path(join.source).toArray();
3431
+ if (join.where !== void 0) rows = rows.filter((row) => matches(join.where, row));
3432
+ if (join.orderBy !== void 0) rows = sortRows(rows, join.orderBy);
3433
+ return rows;
3434
+ }
3435
+ function subqueryJoinHasAggregation(join) {
3436
+ return join.aggregates !== void 0 || join.having !== void 0 || join.groupBy !== void 0 && join.groupBy.length > 0;
3437
+ }
3438
+ function predicateSubqueryJoinRows(leftRows, rightRows, join, options) {
3439
+ if (join.predicate === void 0) return leftRows;
3440
+ const maxRightRows = options.joinMaxRightRows ?? 1e5;
3441
+ if (rightRows.length > maxRightRows) {
3442
+ throw new LakeqlError(
3443
+ "LAKEQL_BUDGET_EXCEEDED",
3444
+ `Predicate subquery join exceeded maxRightRows (${rightRows.length} > ${maxRightRows})`,
3445
+ { metric: "maxRightRows", limit: maxRightRows, actual: rightRows.length }
3446
+ );
3447
+ }
3448
+ const maxOutputRows = options.joinMaxOutputRows ?? 1e5;
3449
+ const out = [];
3450
+ const leftAlias = join.leftAlias ?? "";
3451
+ const rightAlias = join.alias ?? join.source;
3452
+ let candidateRows = 0;
3453
+ for (const leftRow of leftRows) {
3454
+ let matched = false;
3455
+ for (const rightRow of rightRows) {
3456
+ candidateRows += 1;
3457
+ if (candidateRows > maxOutputRows) {
3458
+ throw new LakeqlError(
3459
+ "LAKEQL_BUDGET_EXCEEDED",
3460
+ `Predicate subquery join exceeded maxOutputRows (${candidateRows} > ${maxOutputRows})`,
3461
+ { metric: "maxOutputRows", limit: maxOutputRows, actual: candidateRows }
3462
+ );
3463
+ }
3464
+ if (!subqueryKeysMatch(leftRow, rightRow, join)) continue;
3465
+ const row = {
3466
+ ...leftRow,
3467
+ ...leftAlias.length === 0 ? {} : qualifyOnlyRow(leftRow, leftAlias),
3468
+ ...qualifyOnlyRow(rightRow, rightAlias)
3469
+ };
3470
+ if (!matches(join.predicate, row)) continue;
3471
+ matched = true;
3472
+ break;
3473
+ }
3474
+ if (join.type === "semi" && matched || join.type === "anti" && !matched) out.push(leftRow);
3475
+ }
3476
+ return out;
3477
+ }
3478
+ function subqueryKeysMatch(leftRow, rightRow, join) {
3479
+ for (const [index, leftKey] of join.leftKey.entries()) {
3480
+ const rightKey = join.rightKey[index];
3481
+ if (rightKey === void 0) return false;
3482
+ const left = leftRow[leftKey];
3483
+ const right = rightRow[rightKey];
3484
+ if (left === null || right === null || left !== right) return false;
3485
+ }
3486
+ return true;
3487
+ }
3488
+ function offsetLimitSubqueryRows(rows, subquery) {
3489
+ const offset = subquery.offset ?? 0;
3490
+ if (subquery.limit !== void 0) return rows.slice(offset, offset + subquery.limit);
3491
+ return offset > 0 ? rows.slice(offset) : rows;
2535
3492
  }
2536
3493
  function qualifyRow(row, alias) {
2537
3494
  const out = { ...row };
@@ -2541,7 +3498,19 @@ function qualifyRow(row, alias) {
2541
3498
  function qualifyOnlyRow(row, alias) {
2542
3499
  return Object.fromEntries(Object.entries(row).map(([key, value]) => [`${alias}.${key}`, value]));
2543
3500
  }
2544
- function projectRows(rows, ast) {
3501
+ async function correlatedScalarRowsFromAst(lake, ast) {
3502
+ let rows = await lake.path(ast.source).toArray();
3503
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
3504
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
3505
+ rows = await projectRows(lake, rows, ast);
3506
+ if (ast.distinct === true) rows = distinctRows(rows);
3507
+ return offsetLimitRows(rows, ast);
3508
+ }
3509
+ function hasCorrelatedScalarSubqueries(ast) {
3510
+ return ast.correlatedScalarSubqueries !== void 0 && Object.keys(ast.correlatedScalarSubqueries).length > 0;
3511
+ }
3512
+ async function projectRows(lake, rows, ast) {
3513
+ const lookups = await correlatedScalarLookups(lake, ast);
2545
3514
  const hasWildcardProjection = ast.select?.includes("*") ?? false;
2546
3515
  return rows.map((row) => {
2547
3516
  if (hasWildcardProjection && ast.projections === void 0) return row;
@@ -2555,11 +3524,123 @@ function projectRows(rows, ast) {
2555
3524
  }
2556
3525
  }
2557
3526
  for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
2558
- out[alias] = evaluate(expr, row);
3527
+ out[alias] = evaluate(resolveCorrelatedScalarExpr(expr, row, lookups), row);
2559
3528
  }
2560
3529
  return out;
2561
3530
  });
2562
3531
  }
3532
+ async function correlatedScalarLookups(lake, ast) {
3533
+ const lookups = /* @__PURE__ */ new Map();
3534
+ for (const [id, subquery] of Object.entries(ast.correlatedScalarSubqueries ?? {})) {
3535
+ const rightRows = await aggregateRowsFromAst(lake.path(subquery.source), {
3536
+ source: subquery.source,
3537
+ select: subquery.rightKey,
3538
+ groupBy: subquery.groupBy,
3539
+ aggregates: subquery.aggregates,
3540
+ ...subquery.hiddenAggregates === void 0 ? {} : { hiddenAggregates: subquery.hiddenAggregates },
3541
+ ...subquery.where === void 0 ? {} : { where: subquery.where },
3542
+ ...subquery.having === void 0 ? {} : { having: subquery.having }
3543
+ });
3544
+ lookups.set(id, {
3545
+ subquery,
3546
+ rows: new Map(rightRows.map((row) => [correlatedScalarRightKey(row, subquery), row]))
3547
+ });
3548
+ }
3549
+ return lookups;
3550
+ }
3551
+ function resolveCorrelatedScalarExpr(expr, row, lookups) {
3552
+ switch (expr.kind) {
3553
+ case "call":
3554
+ if (expr.fn === "__lakeql_correlated_scalar_subquery") {
3555
+ return {
3556
+ kind: "literal",
3557
+ value: correlatedScalarValue(expr, row, lookups)
3558
+ };
3559
+ }
3560
+ return {
3561
+ ...expr,
3562
+ args: expr.args.map((arg) => resolveCorrelatedScalarExpr(arg, row, lookups))
3563
+ };
3564
+ case "compare":
3565
+ return {
3566
+ ...expr,
3567
+ left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
3568
+ right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
3569
+ };
3570
+ case "arithmetic":
3571
+ return {
3572
+ ...expr,
3573
+ left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
3574
+ right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
3575
+ };
3576
+ case "case":
3577
+ return {
3578
+ ...expr,
3579
+ whens: expr.whens.map((branch) => ({
3580
+ when: resolveCorrelatedScalarExpr(branch.when, row, lookups),
3581
+ value: resolveCorrelatedScalarExpr(branch.value, row, lookups)
3582
+ })),
3583
+ ...expr.else === void 0 ? {} : { else: resolveCorrelatedScalarExpr(expr.else, row, lookups) }
3584
+ };
3585
+ case "logical":
3586
+ return {
3587
+ ...expr,
3588
+ operands: expr.operands.map(
3589
+ (operand) => resolveCorrelatedScalarExpr(operand, row, lookups)
3590
+ )
3591
+ };
3592
+ case "not":
3593
+ return { ...expr, operand: resolveCorrelatedScalarExpr(expr.operand, row, lookups) };
3594
+ case "between":
3595
+ return {
3596
+ ...expr,
3597
+ target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
3598
+ low: resolveCorrelatedScalarExpr(expr.low, row, lookups),
3599
+ high: resolveCorrelatedScalarExpr(expr.high, row, lookups)
3600
+ };
3601
+ case "in":
3602
+ return {
3603
+ ...expr,
3604
+ target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
3605
+ values: expr.values.map((value) => resolveCorrelatedScalarExpr(value, row, lookups))
3606
+ };
3607
+ case "like":
3608
+ return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
3609
+ case "null-check":
3610
+ return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
3611
+ case "column":
3612
+ case "literal":
3613
+ return expr;
3614
+ }
3615
+ }
3616
+ function correlatedScalarValue(expr, row, lookups) {
3617
+ const id = expr.args[0];
3618
+ if (id?.kind !== "literal" || typeof id.value !== "string") {
3619
+ throw new LakeqlError(
3620
+ "LAKEQL_SQL_UNSUPPORTED",
3621
+ "Invalid correlated scalar subquery placeholder"
3622
+ );
3623
+ }
3624
+ const lookup = lookups.get(id.value);
3625
+ if (lookup === void 0) {
3626
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Missing correlated scalar subquery metadata");
3627
+ }
3628
+ const key = correlatedScalarLeftKey(row, lookup.subquery);
3629
+ if (key === void 0) return null;
3630
+ return lookup.rows.get(key)?.[lookup.subquery.column] ?? null;
3631
+ }
3632
+ function correlatedScalarLeftKey(row, subquery) {
3633
+ const values = [];
3634
+ for (const key of subquery.leftKey) {
3635
+ const value = row[key];
3636
+ if (value === null || value === void 0) return void 0;
3637
+ values.push(value);
3638
+ }
3639
+ return JSON.stringify(values);
3640
+ }
3641
+ function correlatedScalarRightKey(row, subquery) {
3642
+ return JSON.stringify(subquery.rightKey.map((key) => row[key]));
3643
+ }
2563
3644
  async function aggregateRowsFromAst(builder, ast) {
2564
3645
  if (hasWindowSemantics(ast)) {
2565
3646
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over aggregates is not supported");
@@ -2567,6 +3648,8 @@ async function aggregateRowsFromAst(builder, ast) {
2567
3648
  validateAggregateProjection(ast);
2568
3649
  let next = builder;
2569
3650
  if (ast.where) next = next.where(ast.where);
3651
+ const preProjections = aggregatePreProjections(ast);
3652
+ if (preProjections !== void 0) next = next.select(["*"]).project(preProjections);
2570
3653
  const aggregates = ast.aggregates ?? {};
2571
3654
  const hiddenCountAlias = hiddenAggregateAlias(ast);
2572
3655
  const aggregateSpec = Object.keys(aggregates).length > 0 ? aggregates : { [hiddenCountAlias]: { op: "count" } };
@@ -2596,12 +3679,13 @@ function validateAggregateProjection(ast) {
2596
3679
  }
2597
3680
  function projectAggregateRows(rows, ast, hiddenCountAlias) {
2598
3681
  const aggregates = Object.entries(ast.aggregates ?? {});
3682
+ const hiddenAggregates = new Set(ast.hiddenAggregates ?? []);
2599
3683
  const hasWildcardProjection = ast.select?.includes("*") ?? false;
2600
3684
  return rows.map((row) => {
2601
3685
  const out = {};
2602
3686
  if (hasWildcardProjection) {
2603
3687
  for (const [column, value] of Object.entries(row)) {
2604
- if (column !== hiddenCountAlias) out[column] = value;
3688
+ if (column !== hiddenCountAlias && !hiddenAggregates.has(column)) out[column] = value;
2605
3689
  }
2606
3690
  } else if (ast.select !== void 0) {
2607
3691
  for (const select of ast.select ?? []) {
@@ -2609,13 +3693,24 @@ function projectAggregateRows(rows, ast, hiddenCountAlias) {
2609
3693
  out[alias] = row[column];
2610
3694
  }
2611
3695
  }
2612
- for (const [alias] of aggregates) out[alias] = row[alias];
3696
+ for (const [alias] of aggregates) {
3697
+ if (!hiddenAggregates.has(alias)) out[alias] = row[alias];
3698
+ }
2613
3699
  for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
2614
- out[alias] = evaluate(expr, row);
3700
+ out[alias] = aggregateProjectionValue(alias, expr, row, ast);
2615
3701
  }
2616
3702
  return out;
2617
3703
  });
2618
3704
  }
3705
+ function aggregatePreProjections(ast) {
3706
+ const groupBy = new Set(ast.groupBy ?? []);
3707
+ const entries = Object.entries(ast.projections ?? {}).filter(([alias]) => groupBy.has(alias));
3708
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
3709
+ }
3710
+ function aggregateProjectionValue(alias, expr, row, ast) {
3711
+ if (ast.groupBy?.includes(alias) && alias in row) return row[alias];
3712
+ return evaluate(expr, row);
3713
+ }
2619
3714
  function hiddenAggregateAlias(ast) {
2620
3715
  const reserved = /* @__PURE__ */ new Set([...ast.select ?? [], ...Object.keys(ast.aggregates ?? {})]);
2621
3716
  let alias = "__lakeql_group_count";
@@ -2639,20 +3734,27 @@ function distinctRows(rows) {
2639
3734
  }
2640
3735
  return out;
2641
3736
  }
2642
- function sortRows(rows, orderBy) {
3737
+ function sortRows(rows, orderBy, ast) {
2643
3738
  return [...rows].sort((a, b) => {
2644
3739
  for (const term of orderBy) {
2645
- const av = a[term.column] ?? null;
2646
- const bv = b[term.column] ?? null;
3740
+ const av = sortValue(a, term.column, ast);
3741
+ const bv = sortValue(b, term.column, ast);
2647
3742
  if (av === bv) continue;
2648
- if (av === null) return term.nulls === "first" ? -1 : 1;
2649
- if (bv === null) return term.nulls === "first" ? 1 : -1;
3743
+ if (av === null || av === void 0) return term.nulls === "first" ? -1 : 1;
3744
+ if (bv === null || bv === void 0) return term.nulls === "first" ? 1 : -1;
2650
3745
  const direction = term.direction === "desc" ? -1 : 1;
2651
3746
  return (av < bv ? -1 : 1) * direction;
2652
3747
  }
2653
3748
  return 0;
2654
3749
  });
2655
3750
  }
3751
+ function sortValue(row, column, ast) {
3752
+ const expr = ast?.projections?.[column];
3753
+ if (expr !== void 0) return evaluate(expr, row) ?? null;
3754
+ const select = ast?.select?.find((value) => selectColumn(value).alias === column);
3755
+ if (select !== void 0) return row[selectColumn(select).column] ?? null;
3756
+ return row[column] ?? null;
3757
+ }
2656
3758
  function offsetLimitRows(rows, ast) {
2657
3759
  const offset = ast.offset ?? 0;
2658
3760
  if (ast.limit !== void 0) return rows.slice(offset, offset + ast.limit);