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.
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createParquetLake, readParquetMetadata, memoryStore, parseSql, writePartitionedParquet, broadcastJoin, partitionedParquetOutputEntries, parseSqlStatement } from './chunk-32PE6IK4.js';
2
+ import { createParquetLake, readParquetMetadata, memoryStore, parseSql, writePartitionedParquet, broadcastJoin, crossJoin, partitionedParquetOutputEntries, parseSqlStatement } from './chunk-AUL4DYT4.js';
3
3
  import { createOutputManifest, fingerprint, writeOutputManifest } from './chunk-LWF5FKHB.js';
4
4
  import { LakeqlError, matches, evaluate } from './chunk-WGHR35QU.js';
5
5
  import { readFile, mkdir, writeFile } from 'fs/promises';
@@ -47,7 +47,7 @@ function usage() {
47
47
  "commands:",
48
48
  " compact --path <file.parquet> --output <prefix> [--max-rows-per-file n]",
49
49
  " query --path <file.parquet> --sql <query> [--format csv|json|ndjson]",
50
- " query --table name=file.parquet [--table name=file.parquet ...] --sql <join-query> [--join-max-right-rows n]",
50
+ " query --table name=file.parquet [--table name=file.parquet ...] --sql <join-query> [--join-max-right-rows n] [--join-max-output-rows n]",
51
51
  " explain --path <file.parquet> --sql <query>",
52
52
  " inspect --path <file.parquet>",
53
53
  " write --path <file.parquet> --sql <query> --output <prefix> [--partition-by a,b] [--max-rows-per-file n] [--manifest <path>] [--job-id id]",
@@ -70,21 +70,20 @@ async function query(args) {
70
70
  const executableAst = await materializeCteIfNeeded(store, lake, ast);
71
71
  const executableLake = createParquetLake({ store });
72
72
  const resolvedAst = await resolveScalarSubqueries(executableLake, executableAst);
73
- if (resolvedAst.subqueryJoin !== void 0) {
73
+ if (sqlSubqueryJoins(resolvedAst).length > 0) {
74
74
  const rows = await subqueryJoinRowsFromAst(executableLake, resolvedAst, args);
75
75
  if (args.format === "json") return `${jsonStringify(rows)}
76
76
  `;
77
77
  if (args.format === "csv") return rowsToCsv(rows);
78
78
  return rows.map((row) => jsonStringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
79
79
  }
80
- if (resolvedAst.join !== void 0) {
80
+ if (sqlJoins(resolvedAst).length > 0) {
81
81
  const rows = await joinRowsFromAst(executableLake, resolvedAst, args);
82
82
  if (args.format === "json") return `${jsonStringify(rows)}
83
83
  `;
84
84
  if (args.format === "csv") return rowsToCsv(rows);
85
85
  return rows.map((row) => jsonStringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
86
86
  }
87
- const result2 = builderFromAst(executableLake.path(resolvedAst.source), resolvedAst);
88
87
  if (hasAggregation(resolvedAst)) {
89
88
  const rows = await aggregateRowsFromAst(executableLake.path(resolvedAst.source), resolvedAst);
90
89
  if (args.format === "json") return `${jsonStringify(rows)}
@@ -92,6 +91,14 @@ async function query(args) {
92
91
  if (args.format === "csv") return rowsToCsv(rows);
93
92
  return rows.map((row) => jsonStringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
94
93
  }
94
+ if (hasCorrelatedScalarSubqueries(resolvedAst)) {
95
+ const rows = await correlatedScalarRowsFromAst(executableLake, resolvedAst);
96
+ if (args.format === "json") return `${jsonStringify(rows)}
97
+ `;
98
+ if (args.format === "csv") return rowsToCsv(rows);
99
+ return rows.map((row) => jsonStringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
100
+ }
101
+ const result2 = builderFromAst(executableLake.path(resolvedAst.source), resolvedAst);
95
102
  if (args.format === "json") return `${jsonStringify(await result2.toArray())}
96
103
  `;
97
104
  if (args.format === "csv") return new Response(result2.streamCsv()).text();
@@ -147,7 +154,7 @@ async function write(args) {
147
154
  const executableAst = await materializeCteIfNeeded(store, lake, ast);
148
155
  const executableLake = createParquetLake({ store });
149
156
  const resolvedAst = await resolveScalarSubqueries(executableLake, executableAst);
150
- const rows = resolvedAst.subqueryJoin !== void 0 ? await subqueryJoinRowsFromAst(executableLake, resolvedAst, args) : resolvedAst.join !== void 0 ? await joinRowsFromAst(executableLake, resolvedAst, args) : hasAggregation(resolvedAst) ? await aggregateRowsFromAst(executableLake.path(resolvedAst.source), resolvedAst) : await builderFromAst(executableLake.path(resolvedAst.source), resolvedAst).toArray();
157
+ const rows = sqlSubqueryJoins(resolvedAst).length > 0 ? await subqueryJoinRowsFromAst(executableLake, resolvedAst, args) : sqlJoins(resolvedAst).length > 0 ? await joinRowsFromAst(executableLake, resolvedAst, args) : hasAggregation(resolvedAst) ? await aggregateRowsFromAst(executableLake.path(resolvedAst.source), resolvedAst) : hasCorrelatedScalarSubqueries(resolvedAst) ? await correlatedScalarRowsFromAst(executableLake, resolvedAst) : await builderFromAst(executableLake.path(resolvedAst.source), resolvedAst).toArray();
151
158
  return writeRows(outputPrefix, rows, args);
152
159
  }
153
160
  async function writeRows(outputPrefix, rows, args) {
@@ -226,7 +233,7 @@ async function materializeCteIfNeeded(store, lake, ast) {
226
233
  "CTEs are only supported as the outer FROM source"
227
234
  );
228
235
  }
229
- if (ast.join !== void 0 || ast.subqueryJoin !== void 0) {
236
+ if (sqlJoins(ast).length > 0 || sqlSubqueryJoins(ast).length > 0) {
230
237
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "CTEs inside JOINs are not supported yet");
231
238
  }
232
239
  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();
@@ -246,6 +253,10 @@ async function resolveScalarSubqueries(lake, ast) {
246
253
  const values = /* @__PURE__ */ new Map();
247
254
  for (const [id, subquery] of Object.entries(ast.scalarSubqueries)) {
248
255
  const rows = hasAggregation(subquery.query) ? await aggregateRowsFromAst(lake.path(subquery.query.source), subquery.query) : await builderFromAst(lake.path(subquery.query.source), subquery.query).toArray();
256
+ if (subquery.mode === "exists") {
257
+ values.set(id, rows.length > 0);
258
+ continue;
259
+ }
249
260
  if (rows.length > 1) {
250
261
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
251
262
  }
@@ -262,6 +273,21 @@ async function resolveScalarSubqueries(lake, ast) {
262
273
  ])
263
274
  );
264
275
  }
276
+ if (ast.subqueryJoins !== void 0) {
277
+ out.subqueryJoins = ast.subqueryJoins.map((join) => ({
278
+ ...join,
279
+ ...join.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(join.where, values) },
280
+ ...join.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(join.predicate, values) }
281
+ }));
282
+ const firstSubqueryJoin = out.subqueryJoins[0];
283
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
284
+ } else if (ast.subqueryJoin?.where !== void 0 || ast.subqueryJoin?.predicate !== void 0) {
285
+ out.subqueryJoin = {
286
+ ...ast.subqueryJoin,
287
+ ...ast.subqueryJoin.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(ast.subqueryJoin.where, values) },
288
+ ...ast.subqueryJoin.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(ast.subqueryJoin.predicate, values) }
289
+ };
290
+ }
265
291
  delete out.scalarSubqueries;
266
292
  return out;
267
293
  }
@@ -327,81 +353,91 @@ function replaceScalarSubqueryExpr(expr, values) {
327
353
  }
328
354
  }
329
355
  async function joinRowsFromAst(lake, ast, args) {
330
- if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
331
- const join = ast.join;
356
+ const joins = sqlJoins(ast);
357
+ const firstJoin = joins[0];
358
+ if (firstJoin === void 0) {
359
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
360
+ }
332
361
  if (hasWindowSemantics(ast)) {
333
362
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over JOIN is not supported");
334
363
  }
335
364
  if (hasAggregation(ast)) {
336
365
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
337
366
  }
338
- const leftAlias = leftJoinAlias(ast);
339
- const plan = planJoinSides(ast, leftAlias, join.alias);
340
- let leftBuilder = lake.path(ast.source);
341
- let rightBuilder = lake.path(join.source);
342
- if (plan.leftWhere !== void 0) leftBuilder = leftBuilder.where(plan.leftWhere);
343
- if (plan.rightWhere !== void 0) rightBuilder = rightBuilder.where(plan.rightWhere);
344
- if (plan.leftColumns !== void 0) leftBuilder = leftBuilder.select(plan.leftColumns);
345
- if (plan.rightColumns !== void 0) rightBuilder = rightBuilder.select(plan.rightColumns);
346
- const leftRows = (await leftBuilder.toArray()).map((row) => qualifyRow(row, leftAlias));
347
- const rightRows = (await rightBuilder.toArray()).map((row) => qualifyRow(row, join.alias));
348
- let rows = await broadcastJoin(leftRows, rightRows, {
349
- leftKey: join.leftKey,
350
- rightKey: join.rightKey,
351
- type: join.type,
352
- rightPrefix: `${join.alias}.`,
353
- maxRightRows: args.joinMaxRightRows ?? 1e5
354
- });
355
- if (join.type === "left") {
356
- rows = fillLeftJoinNulls(rows, join.alias, leftJoinRightColumns(ast, join.alias, rightRows));
367
+ let rows = (await lake.path(ast.source).toArray()).map(
368
+ (row) => qualifyRow(row, firstJoin.leftAlias)
369
+ );
370
+ for (const join of joins) {
371
+ const rightRows = (await lake.path(join.source).toArray()).map(
372
+ (row) => qualifyRow(row, join.alias)
373
+ );
374
+ if (join.type === "right") {
375
+ const leftRows = rows;
376
+ rows = await broadcastJoin(rightRows, leftRows, {
377
+ leftKey: join.rightKey,
378
+ rightKey: join.leftKey,
379
+ type: "left",
380
+ rightPrefix: `${join.leftAlias}.`,
381
+ maxRightRows: args.joinMaxRightRows ?? 1e5
382
+ });
383
+ rows = fillRightJoinNulls(rows, ast, join.alias, leftRows);
384
+ } else if (join.type === "full") {
385
+ const leftRows = rows;
386
+ const leftJoined = await broadcastJoin(leftRows, rightRows, {
387
+ leftKey: join.leftKey,
388
+ rightKey: join.rightKey,
389
+ type: "left",
390
+ rightPrefix: `${join.alias}.`,
391
+ maxRightRows: args.joinMaxRightRows ?? 1e5
392
+ });
393
+ const unmatchedRight = await broadcastJoin(rightRows, leftRows, {
394
+ leftKey: join.rightKey,
395
+ rightKey: join.leftKey,
396
+ type: "anti",
397
+ rightPrefix: `${join.leftAlias}.`,
398
+ maxRightRows: args.joinMaxRightRows ?? 1e5
399
+ });
400
+ rows = [
401
+ ...fillLeftJoinNulls(
402
+ leftJoined,
403
+ join.alias,
404
+ leftJoinRightColumns(ast, join.alias, rightRows)
405
+ ),
406
+ ...fillRightJoinNulls(unmatchedRight, ast, join.alias, leftRows)
407
+ ];
408
+ } else {
409
+ rows = join.type === "cross" ? await crossJoin(rows, rightRows, {
410
+ rightPrefix: `${join.alias}.`,
411
+ maxRightRows: args.joinMaxRightRows ?? 1e5,
412
+ maxOutputRows: args.joinMaxOutputRows ?? 1e5
413
+ }) : await broadcastJoin(rows, rightRows, {
414
+ leftKey: join.leftKey,
415
+ rightKey: join.rightKey,
416
+ type: join.type,
417
+ rightPrefix: `${join.alias}.`,
418
+ maxRightRows: args.joinMaxRightRows ?? 1e5
419
+ });
420
+ }
421
+ if (join.type === "left") {
422
+ rows = fillLeftJoinNulls(rows, join.alias, leftJoinRightColumns(ast, join.alias, rightRows));
423
+ }
357
424
  }
358
- if (plan.residualWhere !== void 0)
359
- rows = rows.filter((row) => matches(plan.residualWhere, row));
425
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
360
426
  if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
361
- rows = projectRows(rows, ast);
427
+ rows = await projectRows(lake, rows, ast);
362
428
  if (ast.distinct === true) rows = distinctRows(rows);
363
429
  const offset = ast.offset ?? 0;
364
430
  if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
365
431
  else if (offset > 0) rows = rows.slice(offset);
366
432
  return rows;
367
433
  }
368
- function planJoinSides(ast, leftAlias, rightAlias) {
369
- if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
370
- const leftPredicates = [];
371
- const rightPredicates = [];
372
- const residualPredicates = [];
373
- for (const predicate of splitAndPredicate(ast.where)) {
374
- const columns = exprColumns(predicate);
375
- if (columns.length > 0 && columns.every((column) => isQualifiedBy(column, leftAlias))) {
376
- leftPredicates.push(stripQualifiedExpr(predicate, leftAlias));
377
- } else if (ast.join.type === "inner" && columns.length > 0 && columns.every((column) => isQualifiedBy(column, rightAlias))) {
378
- rightPredicates.push(stripQualifiedExpr(predicate, rightAlias));
379
- } else {
380
- residualPredicates.push(predicate);
381
- }
382
- }
383
- const plan = {};
384
- const leftWhere = combineAndPredicate(leftPredicates);
385
- const rightWhere = combineAndPredicate(rightPredicates);
386
- const residualWhere = combineAndPredicate(residualPredicates);
387
- if (leftWhere !== void 0) plan.leftWhere = leftWhere;
388
- if (rightWhere !== void 0) plan.rightWhere = rightWhere;
389
- if (residualWhere !== void 0) plan.residualWhere = residualWhere;
390
- if (canProjectJoinSides(ast, leftAlias, rightAlias)) {
391
- plan.leftColumns = joinSideColumns(ast, leftAlias, ast.join.leftKey);
392
- plan.rightColumns = joinSideColumns(ast, rightAlias, ast.join.rightKey);
393
- }
394
- return plan;
395
- }
396
- function splitAndPredicate(expr) {
397
- if (expr === void 0) return [];
398
- if (expr.kind === "logical" && expr.op === "and") return expr.operands.flatMap(splitAndPredicate);
399
- return [expr];
400
- }
401
- function combineAndPredicate(predicates) {
402
- if (predicates.length === 0) return void 0;
403
- if (predicates.length === 1) return predicates[0];
404
- return { kind: "logical", op: "and", operands: predicates };
434
+ function sqlJoins(ast) {
435
+ if (ast.joins !== void 0) return ast.joins;
436
+ return ast.join === void 0 ? [] : [ast.join];
437
+ }
438
+ function sqlSubqueryJoins(ast) {
439
+ if (ast.subqueryJoins !== void 0) return ast.subqueryJoins;
440
+ return ast.subqueryJoin === void 0 ? [] : [ast.subqueryJoin];
405
441
  }
406
442
  function isQualifiedBy(column, alias) {
407
443
  return column.startsWith(`${alias}.`) && column.length > alias.length + 1;
@@ -409,66 +445,6 @@ function isQualifiedBy(column, alias) {
409
445
  function stripQualifiedColumn(column, alias) {
410
446
  return isQualifiedBy(column, alias) ? column.slice(alias.length + 1) : column;
411
447
  }
412
- function stripQualifiedExpr(expr, alias) {
413
- switch (expr.kind) {
414
- case "column":
415
- return { ...expr, name: stripQualifiedColumn(expr.name, alias) };
416
- case "compare":
417
- return {
418
- ...expr,
419
- left: stripQualifiedExpr(expr.left, alias),
420
- right: stripQualifiedExpr(expr.right, alias)
421
- };
422
- case "between":
423
- return {
424
- ...expr,
425
- target: stripQualifiedExpr(expr.target, alias),
426
- low: stripQualifiedExpr(expr.low, alias),
427
- high: stripQualifiedExpr(expr.high, alias)
428
- };
429
- case "in":
430
- return {
431
- ...expr,
432
- target: stripQualifiedExpr(expr.target, alias),
433
- values: expr.values.map((value) => stripQualifiedExpr(value, alias))
434
- };
435
- case "logical":
436
- return {
437
- ...expr,
438
- operands: expr.operands.map((operand) => stripQualifiedExpr(operand, alias))
439
- };
440
- case "not":
441
- return { ...expr, operand: stripQualifiedExpr(expr.operand, alias) };
442
- case "null-check":
443
- return { ...expr, target: stripQualifiedExpr(expr.target, alias) };
444
- case "like":
445
- return { ...expr, target: stripQualifiedExpr(expr.target, alias) };
446
- case "call":
447
- return { ...expr, args: expr.args.map((arg) => stripQualifiedExpr(arg, alias)) };
448
- case "arithmetic":
449
- return {
450
- ...expr,
451
- left: stripQualifiedExpr(expr.left, alias),
452
- right: stripQualifiedExpr(expr.right, alias)
453
- };
454
- case "case":
455
- return {
456
- ...expr,
457
- whens: expr.whens.map((branch) => ({
458
- when: stripQualifiedExpr(branch.when, alias),
459
- value: stripQualifiedExpr(branch.value, alias)
460
- })),
461
- ...expr.else === void 0 ? {} : { else: stripQualifiedExpr(expr.else, alias) }
462
- };
463
- case "literal":
464
- return expr;
465
- }
466
- }
467
- function exprColumns(expr) {
468
- const columns = /* @__PURE__ */ new Set();
469
- collectExprColumns(expr, columns);
470
- return [...columns];
471
- }
472
448
  function collectExprColumns(expr, columns) {
473
449
  switch (expr.kind) {
474
450
  case "column":
@@ -517,12 +493,6 @@ function collectExprColumns(expr, columns) {
517
493
  return;
518
494
  }
519
495
  }
520
- function canProjectJoinSides(ast, leftAlias, rightAlias) {
521
- if (ast.select?.includes("*") ?? false) return false;
522
- return referencedJoinColumns(ast).every(
523
- (column) => isQualifiedBy(column, leftAlias) || isQualifiedBy(column, rightAlias)
524
- );
525
- }
526
496
  function referencedJoinColumns(ast) {
527
497
  const columns = /* @__PURE__ */ new Set();
528
498
  for (const select of ast.select ?? []) {
@@ -534,17 +504,6 @@ function referencedJoinColumns(ast) {
534
504
  for (const term of ast.orderBy ?? []) columns.add(term.column);
535
505
  return [...columns].filter((column) => column !== "*");
536
506
  }
537
- function joinSideColumns(ast, alias, joinKey) {
538
- const columns = new Set(
539
- (Array.isArray(joinKey) ? joinKey : [joinKey]).map(
540
- (column) => stripQualifiedColumn(column, alias)
541
- )
542
- );
543
- for (const column of referencedJoinColumns(ast)) {
544
- if (isQualifiedBy(column, alias)) columns.add(stripQualifiedColumn(column, alias));
545
- }
546
- return [...columns];
547
- }
548
507
  function leftJoinRightColumns(ast, rightAlias, rightRows) {
549
508
  const columns = /* @__PURE__ */ new Set();
550
509
  if (ast.select?.includes("*") ?? false) {
@@ -573,47 +532,241 @@ function fillLeftJoinNulls(rows, rightAlias, rightColumns) {
573
532
  return out ?? row;
574
533
  });
575
534
  }
535
+ function fillRightJoinNulls(rows, ast, preservedAlias, leftRows) {
536
+ const columns = rightJoinLeftColumns(ast, preservedAlias, leftRows);
537
+ if (columns.length === 0) return rows;
538
+ return rows.map((row) => {
539
+ let out;
540
+ for (const column of columns) {
541
+ if (column in row) continue;
542
+ out ??= { ...row };
543
+ out[column] = null;
544
+ }
545
+ return out ?? row;
546
+ });
547
+ }
548
+ function rightJoinLeftColumns(ast, preservedAlias, leftRows) {
549
+ const columns = /* @__PURE__ */ new Set();
550
+ if (ast.select?.includes("*") ?? false) {
551
+ for (const row of leftRows) {
552
+ for (const column of Object.keys(row)) {
553
+ if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
554
+ }
555
+ }
556
+ }
557
+ for (const column of referencedJoinColumns(ast)) {
558
+ if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
559
+ }
560
+ return [...columns];
561
+ }
576
562
  async function subqueryJoinRowsFromAst(lake, ast, args) {
577
- if (ast.subqueryJoin === void 0) {
563
+ const joins = sqlSubqueryJoins(ast);
564
+ if (joins.length === 0) {
578
565
  throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL IN subquery");
579
566
  }
580
567
  if (hasWindowSemantics(ast)) {
581
568
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over IN subquery is not supported");
582
569
  }
583
570
  if (hasAggregation(ast)) {
584
- throw new LakeqlError(
585
- "LAKEQL_SQL_UNSUPPORTED",
586
- "Aggregate SQL over IN subquery is not supported yet"
587
- );
571
+ return await aggregateSubqueryJoinRowsFromAst(lake, ast, joins, args);
572
+ }
573
+ let rows = await lake.path(ast.source).toArray();
574
+ for (const join of joins) {
575
+ rows = await applySubqueryJoinRows(lake, rows, join, args);
588
576
  }
589
- const join = ast.subqueryJoin;
590
- let rightRows = await lake.path(join.source).toArray();
591
- if (join.where !== void 0) rightRows = rightRows.filter((row) => matches(join.where, row));
592
- let rows = await broadcastJoin(await lake.path(ast.source).toArray(), rightRows, {
593
- leftKey: join.leftKey,
594
- rightKey: join.rightKey,
595
- type: join.type,
596
- maxRightRows: args.joinMaxRightRows ?? 1e5
597
- });
598
577
  if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
599
578
  if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
600
- rows = projectRows(rows, ast);
579
+ rows = await projectRows(lake, rows, ast);
601
580
  if (ast.distinct === true) rows = distinctRows(rows);
602
581
  const offset = ast.offset ?? 0;
603
582
  if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
604
583
  else if (offset > 0) rows = rows.slice(offset);
605
584
  return rows;
606
585
  }
607
- function leftJoinAlias(ast) {
608
- if (ast.join === void 0) return ast.source;
609
- return ast.join.leftAlias;
586
+ async function aggregateSubqueryJoinRowsFromAst(lake, ast, joins, args) {
587
+ let rows = await lake.path(ast.source).toArray();
588
+ for (const join of joins) rows = await applySubqueryJoinRows(lake, rows, join, args);
589
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
590
+ const aggregateAst = subqueryJoinAggregateAst(ast);
591
+ if (rows.length === 0) {
592
+ if ((ast.groupBy?.length ?? 0) > 0) return [];
593
+ return emptyAggregateRowsFromAst(aggregateAst);
594
+ }
595
+ const prefix = `__lakeql_subquery/${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
596
+ const written = await writePartitionedParquet(lake.store, prefix, {
597
+ rows,
598
+ maxRowsPerFile: rows.length
599
+ });
600
+ try {
601
+ return await aggregateRowsFromAst(lake.path(`${prefix}/*.parquet`), {
602
+ ...aggregateAst,
603
+ source: `${prefix}/*.parquet`
604
+ });
605
+ } finally {
606
+ await Promise.all(written.files.map((file) => lake.store.delete(file.path)));
607
+ }
608
+ }
609
+ function subqueryJoinAggregateAst(ast) {
610
+ const {
611
+ subqueryJoin: _subqueryJoin,
612
+ subqueryJoins: _subqueryJoins,
613
+ where: _where,
614
+ ...rest
615
+ } = ast;
616
+ return rest;
617
+ }
618
+ function emptyAggregateRowsFromAst(ast) {
619
+ validateAggregateProjection(ast);
620
+ const hiddenCountAlias = hiddenAggregateAlias(ast);
621
+ const aggregateSpec = ast.aggregates === void 0 || Object.keys(ast.aggregates).length === 0 ? { [hiddenCountAlias]: { op: "count" } } : ast.aggregates;
622
+ let rows = [emptyAggregateRow(aggregateSpec)];
623
+ if (ast.having !== void 0) rows = rows.filter((row) => matches(ast.having, row));
624
+ rows = projectAggregateRows(rows, ast, hiddenCountAlias);
625
+ if (ast.distinct === true) rows = distinctRows(rows);
626
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
627
+ const offset = ast.offset ?? 0;
628
+ if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
629
+ else if (offset > 0) rows = rows.slice(offset);
630
+ return rows;
631
+ }
632
+ function emptyAggregateRow(spec) {
633
+ return Object.fromEntries(
634
+ Object.entries(spec).map(([alias, aggregate]) => [alias, emptyAggregateValue(aggregate.op)])
635
+ );
636
+ }
637
+ function emptyAggregateValue(op) {
638
+ switch (op) {
639
+ case "count":
640
+ case "count_distinct":
641
+ case "approx_count_distinct":
642
+ return 0;
643
+ case "sum":
644
+ case "avg":
645
+ case "var_samp":
646
+ case "var_pop":
647
+ case "stddev_samp":
648
+ case "stddev_pop":
649
+ case "median":
650
+ case "quantile":
651
+ case "min":
652
+ case "max":
653
+ case "mode":
654
+ case "first":
655
+ case "last":
656
+ case "any":
657
+ return null;
658
+ }
659
+ }
660
+ async function applySubqueryJoinRows(lake, leftRows, join, args) {
661
+ let rightRows = await subqueryJoinRightRows(lake, join);
662
+ rightRows = offsetLimitSubqueryRows(rightRows, join);
663
+ return join.predicate === void 0 ? await broadcastJoin(leftRows, rightRows, {
664
+ leftKey: join.leftKey,
665
+ rightKey: join.rightKey,
666
+ type: join.type,
667
+ maxRightRows: args.joinMaxRightRows ?? 1e5
668
+ }) : predicateSubqueryJoinRows(leftRows, rightRows, join, args);
669
+ }
670
+ async function subqueryJoinRightRows(lake, join) {
671
+ if (subqueryJoinHasAggregation(join)) {
672
+ return await aggregateRowsFromAst(lake.path(join.source), {
673
+ source: join.source,
674
+ select: join.rightKey,
675
+ ...join.where === void 0 ? {} : { where: join.where },
676
+ ...join.groupBy === void 0 ? {} : { groupBy: join.groupBy },
677
+ ...join.aggregates === void 0 ? {} : { aggregates: join.aggregates },
678
+ ...join.hiddenAggregates === void 0 ? {} : { hiddenAggregates: join.hiddenAggregates },
679
+ ...join.having === void 0 ? {} : { having: join.having },
680
+ ...join.orderBy === void 0 ? {} : { orderBy: join.orderBy }
681
+ });
682
+ }
683
+ let rows = await lake.path(join.source).toArray();
684
+ if (join.where !== void 0) rows = rows.filter((row) => matches(join.where, row));
685
+ if (join.orderBy !== void 0) rows = sortRows(rows, join.orderBy);
686
+ return rows;
687
+ }
688
+ function subqueryJoinHasAggregation(join) {
689
+ return join.aggregates !== void 0 || join.having !== void 0 || join.groupBy !== void 0 && join.groupBy.length > 0;
690
+ }
691
+ function predicateSubqueryJoinRows(leftRows, rightRows, join, args) {
692
+ if (join.predicate === void 0) return leftRows;
693
+ const maxRightRows = args.joinMaxRightRows ?? 1e5;
694
+ if (rightRows.length > maxRightRows) {
695
+ throw new LakeqlError(
696
+ "LAKEQL_BUDGET_EXCEEDED",
697
+ `Predicate subquery join exceeded maxRightRows (${rightRows.length} > ${maxRightRows})`,
698
+ { metric: "maxRightRows", limit: maxRightRows, actual: rightRows.length }
699
+ );
700
+ }
701
+ const maxOutputRows = args.joinMaxOutputRows ?? 1e5;
702
+ const out = [];
703
+ const leftAlias = join.leftAlias ?? "";
704
+ const rightAlias = join.alias ?? join.source;
705
+ let candidateRows = 0;
706
+ for (const leftRow of leftRows) {
707
+ let matched = false;
708
+ for (const rightRow of rightRows) {
709
+ candidateRows += 1;
710
+ if (candidateRows > maxOutputRows) {
711
+ throw new LakeqlError(
712
+ "LAKEQL_BUDGET_EXCEEDED",
713
+ `Predicate subquery join exceeded maxOutputRows (${candidateRows} > ${maxOutputRows})`,
714
+ { metric: "maxOutputRows", limit: maxOutputRows, actual: candidateRows }
715
+ );
716
+ }
717
+ if (!subqueryKeysMatch(leftRow, rightRow, join)) continue;
718
+ const row = {
719
+ ...leftRow,
720
+ ...leftAlias.length === 0 ? {} : qualifyOnlyRow(leftRow, leftAlias),
721
+ ...qualifyOnlyRow(rightRow, rightAlias)
722
+ };
723
+ if (!matches(join.predicate, row)) continue;
724
+ matched = true;
725
+ break;
726
+ }
727
+ if (join.type === "semi" && matched || join.type === "anti" && !matched) out.push(leftRow);
728
+ }
729
+ return out;
730
+ }
731
+ function subqueryKeysMatch(leftRow, rightRow, join) {
732
+ for (const [index, leftKey] of join.leftKey.entries()) {
733
+ const rightKey = join.rightKey[index];
734
+ if (rightKey === void 0) return false;
735
+ const left = leftRow[leftKey];
736
+ const right = rightRow[rightKey];
737
+ if (left === null || right === null || left !== right) return false;
738
+ }
739
+ return true;
740
+ }
741
+ function offsetLimitSubqueryRows(rows, subquery) {
742
+ const offset = subquery.offset ?? 0;
743
+ if (subquery.limit !== void 0) return rows.slice(offset, offset + subquery.limit);
744
+ return offset > 0 ? rows.slice(offset) : rows;
610
745
  }
611
746
  function qualifyRow(row, alias) {
612
747
  const out = { ...row };
613
748
  for (const [key, value] of Object.entries(row)) out[`${alias}.${key}`] = value;
614
749
  return out;
615
750
  }
616
- function projectRows(rows, ast) {
751
+ function qualifyOnlyRow(row, alias) {
752
+ return Object.fromEntries(Object.entries(row).map(([key, value]) => [`${alias}.${key}`, value]));
753
+ }
754
+ async function correlatedScalarRowsFromAst(lake, ast) {
755
+ let rows = await lake.path(ast.source).toArray();
756
+ if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
757
+ if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
758
+ rows = await projectRows(lake, rows, ast);
759
+ if (ast.distinct === true) rows = distinctRows(rows);
760
+ const offset = ast.offset ?? 0;
761
+ if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
762
+ else if (offset > 0) rows = rows.slice(offset);
763
+ return rows;
764
+ }
765
+ function hasCorrelatedScalarSubqueries(ast) {
766
+ return ast.correlatedScalarSubqueries !== void 0 && Object.keys(ast.correlatedScalarSubqueries).length > 0;
767
+ }
768
+ async function projectRows(lake, rows, ast) {
769
+ const lookups = await correlatedScalarLookups(lake, ast);
617
770
  const hasWildcardProjection = ast.select?.includes("*") ?? false;
618
771
  return rows.map((row) => {
619
772
  if (hasWildcardProjection && ast.projections === void 0) return row;
@@ -627,11 +780,123 @@ function projectRows(rows, ast) {
627
780
  }
628
781
  }
629
782
  for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
630
- out[alias] = evaluate(expr, row);
783
+ out[alias] = evaluate(resolveCorrelatedScalarExpr(expr, row, lookups), row);
631
784
  }
632
785
  return out;
633
786
  });
634
787
  }
788
+ async function correlatedScalarLookups(lake, ast) {
789
+ const lookups = /* @__PURE__ */ new Map();
790
+ for (const [id, subquery] of Object.entries(ast.correlatedScalarSubqueries ?? {})) {
791
+ const rightRows = await aggregateRowsFromAst(lake.path(subquery.source), {
792
+ source: subquery.source,
793
+ select: subquery.rightKey,
794
+ groupBy: subquery.groupBy,
795
+ aggregates: subquery.aggregates,
796
+ ...subquery.hiddenAggregates === void 0 ? {} : { hiddenAggregates: subquery.hiddenAggregates },
797
+ ...subquery.where === void 0 ? {} : { where: subquery.where },
798
+ ...subquery.having === void 0 ? {} : { having: subquery.having }
799
+ });
800
+ lookups.set(id, {
801
+ subquery,
802
+ rows: new Map(rightRows.map((row) => [correlatedScalarRightKey(row, subquery), row]))
803
+ });
804
+ }
805
+ return lookups;
806
+ }
807
+ function resolveCorrelatedScalarExpr(expr, row, lookups) {
808
+ switch (expr.kind) {
809
+ case "call":
810
+ if (expr.fn === "__lakeql_correlated_scalar_subquery") {
811
+ return {
812
+ kind: "literal",
813
+ value: correlatedScalarValue(expr, row, lookups)
814
+ };
815
+ }
816
+ return {
817
+ ...expr,
818
+ args: expr.args.map((arg) => resolveCorrelatedScalarExpr(arg, row, lookups))
819
+ };
820
+ case "compare":
821
+ return {
822
+ ...expr,
823
+ left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
824
+ right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
825
+ };
826
+ case "arithmetic":
827
+ return {
828
+ ...expr,
829
+ left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
830
+ right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
831
+ };
832
+ case "case":
833
+ return {
834
+ ...expr,
835
+ whens: expr.whens.map((branch) => ({
836
+ when: resolveCorrelatedScalarExpr(branch.when, row, lookups),
837
+ value: resolveCorrelatedScalarExpr(branch.value, row, lookups)
838
+ })),
839
+ ...expr.else === void 0 ? {} : { else: resolveCorrelatedScalarExpr(expr.else, row, lookups) }
840
+ };
841
+ case "logical":
842
+ return {
843
+ ...expr,
844
+ operands: expr.operands.map(
845
+ (operand) => resolveCorrelatedScalarExpr(operand, row, lookups)
846
+ )
847
+ };
848
+ case "not":
849
+ return { ...expr, operand: resolveCorrelatedScalarExpr(expr.operand, row, lookups) };
850
+ case "between":
851
+ return {
852
+ ...expr,
853
+ target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
854
+ low: resolveCorrelatedScalarExpr(expr.low, row, lookups),
855
+ high: resolveCorrelatedScalarExpr(expr.high, row, lookups)
856
+ };
857
+ case "in":
858
+ return {
859
+ ...expr,
860
+ target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
861
+ values: expr.values.map((value) => resolveCorrelatedScalarExpr(value, row, lookups))
862
+ };
863
+ case "like":
864
+ return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
865
+ case "null-check":
866
+ return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
867
+ case "column":
868
+ case "literal":
869
+ return expr;
870
+ }
871
+ }
872
+ function correlatedScalarValue(expr, row, lookups) {
873
+ const id = expr.args[0];
874
+ if (id?.kind !== "literal" || typeof id.value !== "string") {
875
+ throw new LakeqlError(
876
+ "LAKEQL_SQL_UNSUPPORTED",
877
+ "Invalid correlated scalar subquery placeholder"
878
+ );
879
+ }
880
+ const lookup = lookups.get(id.value);
881
+ if (lookup === void 0) {
882
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Missing correlated scalar subquery metadata");
883
+ }
884
+ const key = correlatedScalarLeftKey(row, lookup.subquery);
885
+ if (key === void 0) return null;
886
+ return lookup.rows.get(key)?.[lookup.subquery.column] ?? null;
887
+ }
888
+ function correlatedScalarLeftKey(row, subquery) {
889
+ const values = [];
890
+ for (const key of subquery.leftKey) {
891
+ const value = row[key];
892
+ if (value === null || value === void 0) return void 0;
893
+ values.push(value);
894
+ }
895
+ return JSON.stringify(values);
896
+ }
897
+ function correlatedScalarRightKey(row, subquery) {
898
+ return JSON.stringify(subquery.rightKey.map((key) => row[key]));
899
+ }
635
900
  async function aggregateRowsFromAst(builder, ast) {
636
901
  if (hasWindowSemantics(ast)) {
637
902
  throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over aggregates is not supported");
@@ -639,6 +904,8 @@ async function aggregateRowsFromAst(builder, ast) {
639
904
  validateAggregateProjection(ast);
640
905
  let next = builder;
641
906
  if (ast.where) next = next.where(ast.where);
907
+ const preProjections = aggregatePreProjections(ast);
908
+ if (preProjections !== void 0) next = next.select(["*"]).project(preProjections);
642
909
  const aggregates = ast.aggregates ?? {};
643
910
  const hiddenCountAlias = hiddenAggregateAlias(ast);
644
911
  const aggregateSpec = Object.keys(aggregates).length > 0 ? aggregates : { [hiddenCountAlias]: { op: "count" } };
@@ -672,12 +939,13 @@ function validateAggregateProjection(ast) {
672
939
  }
673
940
  function projectAggregateRows(rows, ast, hiddenCountAlias) {
674
941
  const aggregates = Object.entries(ast.aggregates ?? {});
942
+ const hiddenAggregates = new Set(ast.hiddenAggregates ?? []);
675
943
  const hasWildcardProjection = ast.select?.includes("*") ?? false;
676
944
  return rows.map((row) => {
677
945
  const out = {};
678
946
  if (hasWildcardProjection) {
679
947
  for (const [column, value] of Object.entries(row)) {
680
- if (column !== hiddenCountAlias) out[column] = value;
948
+ if (column !== hiddenCountAlias && !hiddenAggregates.has(column)) out[column] = value;
681
949
  }
682
950
  } else if (ast.select !== void 0) {
683
951
  for (const select of ast.select ?? []) {
@@ -685,13 +953,24 @@ function projectAggregateRows(rows, ast, hiddenCountAlias) {
685
953
  out[alias] = row[column];
686
954
  }
687
955
  }
688
- for (const [alias] of aggregates) out[alias] = row[alias];
956
+ for (const [alias] of aggregates) {
957
+ if (!hiddenAggregates.has(alias)) out[alias] = row[alias];
958
+ }
689
959
  for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
690
- out[alias] = evaluate(expr, row);
960
+ out[alias] = aggregateProjectionValue(alias, expr, row, ast);
691
961
  }
692
962
  return out;
693
963
  });
694
964
  }
965
+ function aggregatePreProjections(ast) {
966
+ const groupBy = new Set(ast.groupBy ?? []);
967
+ const entries = Object.entries(ast.projections ?? {}).filter(([alias]) => groupBy.has(alias));
968
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
969
+ }
970
+ function aggregateProjectionValue(alias, expr, row, ast) {
971
+ if (ast.groupBy?.includes(alias) && alias in row) return row[alias];
972
+ return evaluate(expr, row);
973
+ }
695
974
  function hiddenAggregateAlias(ast) {
696
975
  const reserved = /* @__PURE__ */ new Set([...ast.select ?? [], ...Object.keys(ast.aggregates ?? {})]);
697
976
  let alias = "__lakeql_group_count";
@@ -790,8 +1069,19 @@ function applyDefaultSource(ast, defaultSource, includeScalarSubqueries = true,
790
1069
  } else {
791
1070
  out.source = defaultSource;
792
1071
  }
793
- if (out.subqueryJoin !== void 0)
1072
+ if (out.join !== void 0) out.join = { ...out.join, source: defaultSource };
1073
+ if (out.joins !== void 0) {
1074
+ out.joins = out.joins.map((join) => ({ ...join, source: defaultSource }));
1075
+ const firstJoin = out.joins[0];
1076
+ if (firstJoin !== void 0) out.join = firstJoin;
1077
+ }
1078
+ if (out.subqueryJoin !== void 0 && out.subqueryJoins === void 0)
794
1079
  out.subqueryJoin = { ...out.subqueryJoin, source: defaultSource };
1080
+ if (out.subqueryJoins !== void 0) {
1081
+ out.subqueryJoins = out.subqueryJoins.map((join) => ({ ...join, source: defaultSource }));
1082
+ const firstSubqueryJoin = out.subqueryJoins[0];
1083
+ if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
1084
+ }
795
1085
  if (includeScalarSubqueries && out.scalarSubqueries !== void 0) {
796
1086
  out.scalarSubqueries = Object.fromEntries(
797
1087
  Object.entries(out.scalarSubqueries).map(([id, subquery]) => [
@@ -800,6 +1090,14 @@ function applyDefaultSource(ast, defaultSource, includeScalarSubqueries = true,
800
1090
  ])
801
1091
  );
802
1092
  }
1093
+ if (out.correlatedScalarSubqueries !== void 0) {
1094
+ out.correlatedScalarSubqueries = Object.fromEntries(
1095
+ Object.entries(out.correlatedScalarSubqueries).map(([id, subquery]) => [
1096
+ id,
1097
+ { ...subquery, source: defaultSource }
1098
+ ])
1099
+ );
1100
+ }
803
1101
  return out;
804
1102
  }
805
1103
  function addDefaultFrom(sql) {
@@ -861,6 +1159,9 @@ function parseArgs(argv) {
861
1159
  } else if (arg === "--join-max-right-rows") {
862
1160
  index += 1;
863
1161
  args.joinMaxRightRows = parsePositiveInt(requireValue(rest, index, arg), arg);
1162
+ } else if (arg === "--join-max-output-rows") {
1163
+ index += 1;
1164
+ args.joinMaxOutputRows = parsePositiveInt(requireValue(rest, index, arg), arg);
864
1165
  } else if (arg === "--manifest") {
865
1166
  index += 1;
866
1167
  args.manifest = requireValue(rest, index, arg);