lakeql 0.1.2 → 0.1.8

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,6 @@
1
1
  #!/usr/bin/env node
2
- import { __commonJS, __toESM, LaQLError, readParquetMetadata, createParquetLake, memoryStore, writePartitionedParquet, matches, broadcastJoin, createOutputManifest, partitionedParquetOutputEntries, fingerprint, writeOutputManifest, evaluate } from './chunk-D3A4VN3U.js';
2
+ import { createParquetLake, readParquetMetadata, memoryStore, writePartitionedParquet, broadcastJoin, createOutputManifest, partitionedParquetOutputEntries, fingerprint, writeOutputManifest } from './chunk-MXGEAVHL.js';
3
+ import { __commonJS, __toESM, LakeqlError, matches, evaluate, isTimestampValue } from './chunk-TFD5RFKB.js';
3
4
  import { readFile, mkdir, writeFile } from 'fs/promises';
4
5
  import { dirname } from 'path';
5
6
 
@@ -7292,7 +7293,7 @@ var require_pgsql_ast_parser = __commonJS({
7292
7293
  var import_pgsql_ast_parser = __toESM(require_pgsql_ast_parser(), 1);
7293
7294
  var MAX_SQL_LENGTH = 128e3;
7294
7295
  var MAX_AST_DEPTH = 128;
7295
- function parseSql(sql) {
7296
+ function parseSql(sql, options = {}) {
7296
7297
  if (sql.length > MAX_SQL_LENGTH) {
7297
7298
  throwParse(`SQL input length exceeds ${MAX_SQL_LENGTH}`);
7298
7299
  }
@@ -7308,13 +7309,37 @@ function parseSql(sql) {
7308
7309
  throwUnsupported("Only one SELECT statement is supported");
7309
7310
  const statement = asNode(statements[0], "statement");
7310
7311
  assertAstDepth(statement);
7312
+ const context = newSqlParseContext(options);
7311
7313
  if (statement.type === "with")
7312
- return withStatementToAst(statement);
7314
+ return withStatementToAst(statement, context);
7313
7315
  if (statement.type !== "select")
7314
7316
  throwUnsupported("Only SELECT statements are supported");
7315
- return selectStatementToAst(statement);
7317
+ return selectStatementToAst(statement, context);
7316
7318
  }
7317
- function withStatementToAst(statement) {
7319
+ function parseSqlStatement(sql, options = {}) {
7320
+ const describe = parseDescribeStatement(sql);
7321
+ if (describe !== void 0)
7322
+ return describe;
7323
+ return parseSql(sql, options);
7324
+ }
7325
+ function parseDescribeStatement(sql) {
7326
+ if (sql.length > MAX_SQL_LENGTH) {
7327
+ throwParse(`SQL input length exceeds ${MAX_SQL_LENGTH}`);
7328
+ }
7329
+ const match = /^\s*describe\s+([A-Za-z_][A-Za-z0-9_]*|"(?:""|[^"])+")\s*;?\s*$/iu.exec(sql);
7330
+ if (match === null)
7331
+ return void 0;
7332
+ const source = match[1];
7333
+ if (source === void 0)
7334
+ throwParse("DESCRIBE requires a table name");
7335
+ return { type: "describe", source: unquoteIdentifier(source) };
7336
+ }
7337
+ function unquoteIdentifier(value2) {
7338
+ if (!value2.startsWith('"'))
7339
+ return value2;
7340
+ return value2.slice(1, -1).replaceAll('""', '"');
7341
+ }
7342
+ function withStatementToAst(statement, context) {
7318
7343
  const bindings = optionalArray(statement.bind);
7319
7344
  if (bindings.length !== 1)
7320
7345
  throwUnsupported("Only one CTE is supported");
@@ -7323,12 +7348,12 @@ function withStatementToAst(statement) {
7323
7348
  const inner = asNode(binding.statement, "CTE statement");
7324
7349
  if (inner.type !== "select")
7325
7350
  throwUnsupported("Only SELECT CTEs are supported");
7326
- const cteQuery = selectStatementToAst(inner);
7351
+ const cteQuery = selectStatementToAst(inner, context);
7327
7352
  validateCteQuery(cteQuery);
7328
7353
  const outer = asNode(statement.in, "CTE outer query");
7329
7354
  if (outer.type !== "select")
7330
7355
  throwUnsupported("Only SELECT after WITH is supported");
7331
- const ast = selectStatementToAst(outer);
7356
+ const ast = selectStatementToAst(outer, context);
7332
7357
  ast.cte = { name, query: cteQuery };
7333
7358
  return ast;
7334
7359
  }
@@ -7337,7 +7362,7 @@ function validateCteQuery(ast) {
7337
7362
  throwUnsupported("Nested CTE joins and subqueries are not supported");
7338
7363
  }
7339
7364
  }
7340
- function selectStatementToAst(statement) {
7365
+ function selectStatementToAst(statement, context = newSqlParseContext()) {
7341
7366
  rejectPresent(statement, "with", "CTEs are not supported");
7342
7367
  rejectPresent(statement, "windows", "Window functions are not supported");
7343
7368
  const from = optionalArray(statement.from);
@@ -7347,7 +7372,6 @@ function selectStatementToAst(statement) {
7347
7372
  const leftSource = sourceTable(from[0]);
7348
7373
  const ast = { source: leftSource.source };
7349
7374
  const scope = new SqlScope(leftSource);
7350
- const context = newSqlParseContext();
7351
7375
  if (from.length === 2) {
7352
7376
  const join = joinToAst(from[1], leftSource);
7353
7377
  ast.join = join;
@@ -7376,7 +7400,7 @@ function selectStatementToAst(statement) {
7376
7400
  }
7377
7401
  if (expr.type === "call" && isAggregateCall(expr)) {
7378
7402
  const alias2 = aliasName(item.alias) ?? functionName(expr.function);
7379
- aggregates[alias2] = aggregateCallToSpec(expr, scope);
7403
+ aggregates[alias2] = aggregateCallToSpec(expr, scope, context);
7380
7404
  continue;
7381
7405
  }
7382
7406
  if (expr.type === "ref") {
@@ -7392,7 +7416,7 @@ function selectStatementToAst(statement) {
7392
7416
  if (alias === void 0) {
7393
7417
  throwUnsupported("Computed projections require an explicit alias");
7394
7418
  }
7395
- projections[alias] = exprToLaql(expr, scope, context);
7419
+ projections[alias] = exprToLakeql(expr, scope, context);
7396
7420
  }
7397
7421
  if (select.length > 0)
7398
7422
  ast.select = select;
@@ -7411,7 +7435,7 @@ function selectStatementToAst(statement) {
7411
7435
  ast.groupBy = optionalArray(statement.groupBy).map((expr) => scope.refName(asNode(expr, "GROUP BY expression")));
7412
7436
  }
7413
7437
  if (statement.having !== void 0) {
7414
- ast.having = exprToLaql(asNode(statement.having, "HAVING"), scope, context);
7438
+ ast.having = exprToLakeql(asNode(statement.having, "HAVING"), scope, context);
7415
7439
  }
7416
7440
  if (statement.orderBy !== void 0) {
7417
7441
  ast.orderBy = optionalArray(statement.orderBy).map((term) => orderByToTerm(term, scope));
@@ -7420,19 +7444,23 @@ function selectStatementToAst(statement) {
7420
7444
  if (limit !== void 0) {
7421
7445
  const node = asNode(limit, "LIMIT");
7422
7446
  if (node.limit !== void 0)
7423
- ast.limit = nonNegativeInteger(node.limit, "LIMIT");
7447
+ ast.limit = nonNegativeInteger(node.limit, "LIMIT", context);
7424
7448
  if (node.offset !== void 0)
7425
- ast.offset = nonNegativeInteger(node.offset, "OFFSET");
7449
+ ast.offset = nonNegativeInteger(node.offset, "OFFSET", context);
7426
7450
  }
7427
7451
  if (Object.keys(context.scalarSubqueries).length > 0) {
7428
7452
  ast.scalarSubqueries = context.scalarSubqueries;
7429
7453
  }
7430
7454
  return ast;
7431
7455
  }
7432
- function newSqlParseContext() {
7433
- return { scalarSubqueries: {}, nextScalarSubqueryId: 0 };
7456
+ function newSqlParseContext(options = {}) {
7457
+ return {
7458
+ scalarSubqueries: {},
7459
+ nextScalarSubqueryId: 0,
7460
+ ...options.parameters === void 0 ? {} : { parameters: options.parameters }
7461
+ };
7434
7462
  }
7435
- function exprToLaql(expr, scope = SqlScope.empty(), context = newSqlParseContext()) {
7463
+ function exprToLakeql(expr, scope = SqlScope.empty(), context = newSqlParseContext()) {
7436
7464
  switch (expr.type) {
7437
7465
  case "ref":
7438
7466
  return { kind: "column", name: scope.refName(expr) };
@@ -7443,6 +7471,8 @@ function exprToLaql(expr, scope = SqlScope.empty(), context = newSqlParseContext
7443
7471
  return literal(expr.value);
7444
7472
  case "null":
7445
7473
  return literal(null);
7474
+ case "parameter":
7475
+ return literal(parameterValue(expr, context));
7446
7476
  case "unary":
7447
7477
  return unaryToExpr(expr, scope, context);
7448
7478
  case "binary":
@@ -7460,7 +7490,7 @@ function exprToLaql(expr, scope = SqlScope.empty(), context = newSqlParseContext
7460
7490
  return {
7461
7491
  kind: "call",
7462
7492
  fn: functionName(expr.function),
7463
- args: optionalArray(expr.args).map((arg) => exprToLaql(asNode(arg, "function argument"), scope, context))
7493
+ args: optionalArray(expr.args).map((arg) => exprToLakeql(asNode(arg, "function argument"), scope, context))
7464
7494
  };
7465
7495
  default:
7466
7496
  throwUnsupported(`Unsupported SQL expression ${String(expr.type)}`);
@@ -7468,10 +7498,10 @@ function exprToLaql(expr, scope = SqlScope.empty(), context = newSqlParseContext
7468
7498
  }
7469
7499
  function binaryToExpr(expr, scope = SqlScope.empty(), context = newSqlParseContext()) {
7470
7500
  const op = String(expr.op).toUpperCase();
7471
- const left = exprToLaql(asNode(expr.left, "left expression"), scope, context);
7501
+ const left = exprToLakeql(asNode(expr.left, "left expression"), scope, context);
7472
7502
  const right = asNode(expr.right, "right expression");
7473
7503
  if (op === "AND" || op === "OR") {
7474
- const operands = flattenLogical(op.toLowerCase(), left, exprToLaql(right, scope, context));
7504
+ const operands = flattenLogical(op.toLowerCase(), left, exprToLakeql(right, scope, context));
7475
7505
  return { kind: "logical", op: op.toLowerCase(), operands };
7476
7506
  }
7477
7507
  if (op === "IN" || op === "NOT IN") {
@@ -7481,11 +7511,11 @@ function binaryToExpr(expr, scope = SqlScope.empty(), context = newSqlParseConte
7481
7511
  kind: "in",
7482
7512
  negated: op === "NOT IN",
7483
7513
  target: left,
7484
- values: optionalArray(right.expressions).map((value2) => exprToLaql(asNode(value2, "IN value"), scope, context))
7514
+ values: optionalArray(right.expressions).map((value2) => exprToLakeql(asNode(value2, "IN value"), scope, context))
7485
7515
  };
7486
7516
  }
7487
7517
  if (op === "LIKE" || op === "NOT LIKE" || op === "ILIKE" || op === "NOT ILIKE") {
7488
- const pattern = exprToLaql(right, scope, context);
7518
+ const pattern = exprToLakeql(right, scope, context);
7489
7519
  if (pattern.kind !== "literal" || typeof pattern.value !== "string") {
7490
7520
  throwUnsupported("LIKE pattern must be a string literal");
7491
7521
  }
@@ -7502,10 +7532,10 @@ function binaryToExpr(expr, scope = SqlScope.empty(), context = newSqlParseConte
7502
7532
  kind: "arithmetic",
7503
7533
  op: arithmeticOp(op),
7504
7534
  left,
7505
- right: exprToLaql(right, scope, context)
7535
+ right: exprToLakeql(right, scope, context)
7506
7536
  };
7507
7537
  }
7508
- return { kind: "compare", op: compareOp(op), left, right: exprToLaql(right, scope, context) };
7538
+ return { kind: "compare", op: compareOp(op), left, right: exprToLakeql(right, scope, context) };
7509
7539
  }
7510
7540
  function whereToAst(expr, scope, context) {
7511
7541
  const predicates = flattenWhereConjuncts(expr);
@@ -7523,12 +7553,12 @@ function whereToAst(expr, scope, context) {
7523
7553
  }
7524
7554
  const out = {};
7525
7555
  if (residual.length === 1)
7526
- out.where = exprToLaql(residual[0], scope, context);
7556
+ out.where = exprToLakeql(residual[0], scope, context);
7527
7557
  else if (residual.length > 1) {
7528
7558
  out.where = {
7529
7559
  kind: "logical",
7530
7560
  op: "and",
7531
- operands: residual.map((predicate) => exprToLaql(predicate, scope, context))
7561
+ operands: residual.map((predicate) => exprToLakeql(predicate, scope, context))
7532
7562
  };
7533
7563
  }
7534
7564
  if (subqueryJoin !== void 0)
@@ -7571,7 +7601,7 @@ function subqueryJoinToAst(left, subquery, outerScope, negated) {
7571
7601
  rightKey
7572
7602
  };
7573
7603
  if (subquery.where !== void 0) {
7574
- out.where = exprToLaql(asNode(subquery.where, "IN subquery WHERE"), subqueryScope);
7604
+ out.where = exprToLakeql(asNode(subquery.where, "IN subquery WHERE"), subqueryScope);
7575
7605
  }
7576
7606
  return out;
7577
7607
  }
@@ -7591,7 +7621,7 @@ function flattenWhereConjuncts(expr) {
7591
7621
  return [expr];
7592
7622
  }
7593
7623
  function scalarSubqueryToExpr(subquery, context) {
7594
- const query2 = selectStatementToAst(subquery);
7624
+ const query2 = selectStatementToAst(subquery, context);
7595
7625
  const outputColumns = scalarSubqueryOutputColumns(query2);
7596
7626
  if (outputColumns.length !== 1) {
7597
7627
  throwUnsupported("Scalar subqueries must return exactly one column");
@@ -7602,7 +7632,7 @@ function scalarSubqueryToExpr(subquery, context) {
7602
7632
  const id = `scalar_${context.nextScalarSubqueryId}`;
7603
7633
  context.nextScalarSubqueryId += 1;
7604
7634
  context.scalarSubqueries[id] = { query: query2, column: outputColumns[0] };
7605
- return { kind: "call", fn: "__laql_scalar_subquery", args: [{ kind: "literal", value: id }] };
7635
+ return { kind: "call", fn: "__lakeql_scalar_subquery", args: [{ kind: "literal", value: id }] };
7606
7636
  }
7607
7637
  function scalarSubqueryOutputColumns(query2) {
7608
7638
  return [
@@ -7613,7 +7643,7 @@ function scalarSubqueryOutputColumns(query2) {
7613
7643
  }
7614
7644
  function unaryToExpr(expr, scope = SqlScope.empty(), context = newSqlParseContext()) {
7615
7645
  const op = String(expr.op).toUpperCase();
7616
- const operand = exprToLaql(asNode(expr.operand, "unary operand"), scope, context);
7646
+ const operand = exprToLakeql(asNode(expr.operand, "unary operand"), scope, context);
7617
7647
  if (op === "NOT")
7618
7648
  return { kind: "not", operand };
7619
7649
  if (op === "IS NULL")
@@ -7632,13 +7662,13 @@ function caseToExpr(expr, scope = SqlScope.empty(), context = newSqlParseContext
7632
7662
  const whens = optionalArray(expr.whens).map((branch) => {
7633
7663
  const node = asNode(branch, "CASE branch");
7634
7664
  return {
7635
- when: exprToLaql(asNode(node.when, "CASE WHEN expression"), scope, context),
7636
- value: exprToLaql(asNode(node.value, "CASE THEN expression"), scope, context)
7665
+ when: exprToLakeql(asNode(node.when, "CASE WHEN expression"), scope, context),
7666
+ value: exprToLakeql(asNode(node.value, "CASE THEN expression"), scope, context)
7637
7667
  };
7638
7668
  });
7639
7669
  const out = { kind: "case", whens };
7640
7670
  if (expr.else !== void 0) {
7641
- out.else = exprToLaql(asNode(expr.else, "CASE ELSE expression"), scope, context);
7671
+ out.else = exprToLakeql(asNode(expr.else, "CASE ELSE expression"), scope, context);
7642
7672
  }
7643
7673
  return out;
7644
7674
  }
@@ -7649,13 +7679,13 @@ function ternaryToExpr(expr, scope = SqlScope.empty(), context = newSqlParseCont
7649
7679
  }
7650
7680
  const between = {
7651
7681
  kind: "between",
7652
- target: exprToLaql(asNode(expr.value, "BETWEEN target"), scope, context),
7653
- low: exprToLaql(asNode(expr.lo, "BETWEEN low value"), scope, context),
7654
- high: exprToLaql(asNode(expr.hi, "BETWEEN high value"), scope, context)
7682
+ target: exprToLakeql(asNode(expr.value, "BETWEEN target"), scope, context),
7683
+ low: exprToLakeql(asNode(expr.lo, "BETWEEN low value"), scope, context),
7684
+ high: exprToLakeql(asNode(expr.hi, "BETWEEN high value"), scope, context)
7655
7685
  };
7656
7686
  return op === "NOT BETWEEN" ? { kind: "not", operand: between } : between;
7657
7687
  }
7658
- function aggregateCallToSpec(expr, scope = SqlScope.empty()) {
7688
+ function aggregateCallToSpec(expr, scope = SqlScope.empty(), context = newSqlParseContext()) {
7659
7689
  if ("over" in expr && expr.over !== void 0) {
7660
7690
  throwUnsupported("Window functions are not supported");
7661
7691
  }
@@ -7671,6 +7701,8 @@ function aggregateCallToSpec(expr, scope = SqlScope.empty()) {
7671
7701
  }
7672
7702
  op = "count_distinct";
7673
7703
  }
7704
+ if (op === "quantile")
7705
+ return quantileAggregateCallToSpec(args, scope, context);
7674
7706
  if (op === "count" && (args.length === 0 || args.length === 1 && isWildcard(asNode(args[0], "COUNT argument")))) {
7675
7707
  return { op };
7676
7708
  }
@@ -7679,22 +7711,46 @@ function aggregateCallToSpec(expr, scope = SqlScope.empty()) {
7679
7711
  const arg = asNode(args[0], "aggregate argument");
7680
7712
  if (arg.type === "ref")
7681
7713
  return { op, column: scope.refName(arg) };
7682
- return { op, expr: exprToLaql(arg, scope) };
7714
+ return { op, expr: exprToLakeql(arg, scope, context) };
7715
+ }
7716
+ function quantileAggregateCallToSpec(args, scope, context) {
7717
+ if (args.length !== 2)
7718
+ throwUnsupported("quantile_cont requires value and percentile arguments");
7719
+ const value2 = asNode(args[0], "quantile_cont value argument");
7720
+ const percentile = requiredQuantilePercentile(exprToLakeql(asNode(args[1], "quantile_cont percentile argument"), scope, context));
7721
+ if (value2.type === "ref") {
7722
+ return { op: "quantile", column: scope.refName(value2), quantile: percentile };
7723
+ }
7724
+ return { op: "quantile", expr: exprToLakeql(value2, scope, context), quantile: percentile };
7725
+ }
7726
+ function requiredQuantilePercentile(expr) {
7727
+ if (expr.kind !== "literal" || typeof expr.value !== "number" || !Number.isFinite(expr.value) || expr.value < 0 || expr.value > 1) {
7728
+ throwType("quantile_cont percentile must be a numeric literal between 0 and 1", {
7729
+ function: "quantile_cont"
7730
+ });
7731
+ }
7732
+ return expr.value;
7683
7733
  }
7684
7734
  function isAggregateCall(expr) {
7685
7735
  if (expr.type !== "call")
7686
7736
  return false;
7687
- return isAggregateOp(functionName(expr.function));
7737
+ return aggregateOpOrUndefined(functionName(expr.function)) !== void 0;
7688
7738
  }
7689
7739
  function isAggregateOp(op) {
7690
7740
  return [
7691
7741
  "count",
7692
7742
  "sum",
7693
7743
  "avg",
7744
+ "var_samp",
7745
+ "var_pop",
7746
+ "stddev_samp",
7747
+ "stddev_pop",
7748
+ "median",
7694
7749
  "min",
7695
7750
  "max",
7696
7751
  "count_distinct",
7697
7752
  "approx_count_distinct",
7753
+ "mode",
7698
7754
  "first",
7699
7755
  "last",
7700
7756
  "any"
@@ -7863,9 +7919,9 @@ function optionalArray(value2) {
7863
7919
  throwUnsupported("Expected SQL AST array");
7864
7920
  return value2;
7865
7921
  }
7866
- function nonNegativeInteger(value2, label) {
7922
+ function nonNegativeInteger(value2, label, context = newSqlParseContext()) {
7867
7923
  const node = asNode(value2, label);
7868
- const parsed = node.value;
7924
+ const parsed = node.type === "parameter" ? parameterValue(node, context) : node.value;
7869
7925
  if (typeof parsed !== "number" || !Number.isInteger(parsed) || parsed < 0) {
7870
7926
  throwUnsupported(`${label} must be a non-negative integer`);
7871
7927
  }
@@ -7913,13 +7969,44 @@ function arithmeticOp(op) {
7913
7969
  }
7914
7970
  }
7915
7971
  function aggregateOp(op) {
7972
+ const aggregate = aggregateOpOrUndefined(op);
7973
+ if (aggregate !== void 0)
7974
+ return aggregate;
7975
+ throwUnsupported(`Unsupported aggregate ${op}`);
7976
+ }
7977
+ function aggregateOpOrUndefined(op) {
7916
7978
  if (isAggregateOp(op))
7917
7979
  return op;
7918
- throwUnsupported(`Unsupported aggregate ${op}`);
7980
+ if (op === "var" || op === "variance")
7981
+ return "var_samp";
7982
+ if (op === "stddev")
7983
+ return "stddev_samp";
7984
+ if (op === "quantile_cont")
7985
+ return "quantile";
7986
+ return void 0;
7919
7987
  }
7920
7988
  function literal(value2) {
7921
7989
  return { kind: "literal", value: value2 };
7922
7990
  }
7991
+ function parameterValue(expr, context) {
7992
+ const index = parameterIndex(expr.name);
7993
+ const value2 = context.parameters?.[index - 1];
7994
+ if (value2 === void 0) {
7995
+ throwUnsupported(`SQL parameter $${index} has no bound value`);
7996
+ }
7997
+ if (isScalar(value2))
7998
+ return value2;
7999
+ throwType(`SQL parameter $${index} must be a scalar value`, { parameter: index });
8000
+ }
8001
+ function parameterIndex(name) {
8002
+ if (typeof name !== "string" || !/^\$[1-9][0-9]*$/u.test(name)) {
8003
+ throwUnsupported(`Unsupported SQL parameter ${String(name)}`);
8004
+ }
8005
+ return Number(name.slice(1));
8006
+ }
8007
+ function isScalar(value2) {
8008
+ return value2 === null || typeof value2 === "string" || typeof value2 === "boolean" || typeof value2 === "bigint" || isTimestampValue(value2) || typeof value2 === "number" && Number.isFinite(value2);
8009
+ }
7923
8010
  function assertAstDepth(value2, depth = 0) {
7924
8011
  if (depth > MAX_AST_DEPTH)
7925
8012
  throwParse(`SQL AST nesting exceeds ${MAX_AST_DEPTH}`);
@@ -7945,10 +8032,13 @@ function asNode(value2, label) {
7945
8032
  return value2;
7946
8033
  }
7947
8034
  function throwParse(message) {
7948
- throw new LaQLError("LAQL_PARSE_ERROR", message);
8035
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", message);
7949
8036
  }
7950
8037
  function throwUnsupported(message) {
7951
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", message);
8038
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", message);
8039
+ }
8040
+ function throwType(message, details) {
8041
+ throw new LakeqlError("LAKEQL_TYPE_ERROR", message, details);
7952
8042
  }
7953
8043
 
7954
8044
  // ../cli/src/index.ts
@@ -7977,7 +8067,7 @@ ${usage()}
7977
8067
  `, 2);
7978
8068
  }
7979
8069
  } catch (error) {
7980
- if (error instanceof LaQLError) return fail(`${error.code}: ${error.message}
8070
+ if (error instanceof LakeqlError) return fail(`${error.code}: ${error.message}
7981
8071
  `, 1);
7982
8072
  if (error instanceof Error) return fail(`${error.message}
7983
8073
  `, 1);
@@ -8008,7 +8098,11 @@ function usage() {
8008
8098
  async function query(args) {
8009
8099
  const sql = requireOption(args.sql, "--sql");
8010
8100
  const { store, key, preserveSqlSource } = await queryStore(args);
8011
- const ast = parseCliSql(sql, key, preserveSqlSource);
8101
+ const statement = parseCliSqlStatement(sql, key, preserveSqlSource);
8102
+ if (isDescribeStatement(statement)) {
8103
+ return formatRows([await schemaSummary(store, statement.source)], args.format);
8104
+ }
8105
+ const ast = statement;
8012
8106
  const lake = createParquetLake({ store });
8013
8107
  const executableAst = await materializeCteIfNeeded(store, lake, ast);
8014
8108
  const executableLake = createParquetLake({ store });
@@ -8050,7 +8144,7 @@ async function explain(args) {
8050
8144
  const lake = createParquetLake({ store });
8051
8145
  const ast = parseCliSql(sql, key);
8052
8146
  if (hasAggregation(ast)) {
8053
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "EXPLAIN for aggregate SQL is not supported");
8147
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "EXPLAIN for aggregate SQL is not supported");
8054
8148
  }
8055
8149
  return `${(await builderFromAst(lake.path(key), ast).explain()).text}
8056
8150
  `;
@@ -8066,9 +8160,7 @@ async function compact(args) {
8066
8160
  async function schema(args) {
8067
8161
  const path = requireOption(args.path, "--path");
8068
8162
  const { store, key } = await localStore(path);
8069
- const metadata = await readParquetMetadata(store, key);
8070
- const columns = metadata.schema.filter((field) => field.name !== "root").map((field) => ({ name: field.name, type: field.type ?? field.converted_type ?? "group" }));
8071
- return `${JSON.stringify({ path, rows: Number(totalRows(metadata.row_groups)), columns })}
8163
+ return `${JSON.stringify(await schemaSummary(store, key, path))}
8072
8164
  `;
8073
8165
  }
8074
8166
  async function inspect(args) {
@@ -8106,7 +8198,7 @@ async function writeRows(outputPrefix, rows, args) {
8106
8198
  for (const file of result2.files) {
8107
8199
  const bytes = await outStore.get(file.path);
8108
8200
  if (bytes === null) {
8109
- throw new LaQLError("LAQL_OBJECT_NOT_FOUND", `No generated output at ${file.path}`, {
8201
+ throw new LakeqlError("LAKEQL_OBJECT_NOT_FOUND", `No generated output at ${file.path}`, {
8110
8202
  path: file.path
8111
8203
  });
8112
8204
  }
@@ -8133,7 +8225,7 @@ async function writeRows(outputPrefix, rows, args) {
8133
8225
  await writeOutputManifest(outStore, args.manifest, manifest);
8134
8226
  const bytes = await outStore.get(args.manifest);
8135
8227
  if (bytes === null) {
8136
- throw new LaQLError("LAQL_OBJECT_NOT_FOUND", `No generated output at ${args.manifest}`, {
8228
+ throw new LakeqlError("LAKEQL_OBJECT_NOT_FOUND", `No generated output at ${args.manifest}`, {
8137
8229
  path: args.manifest
8138
8230
  });
8139
8231
  }
@@ -8161,16 +8253,19 @@ function hasAggregation(ast) {
8161
8253
  async function materializeCteIfNeeded(store, lake, ast) {
8162
8254
  if (ast.cte === void 0) return ast;
8163
8255
  if (ast.source !== ast.cte.name) {
8164
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "CTEs are only supported as the outer FROM source");
8256
+ throw new LakeqlError(
8257
+ "LAKEQL_SQL_UNSUPPORTED",
8258
+ "CTEs are only supported as the outer FROM source"
8259
+ );
8165
8260
  }
8166
8261
  if (ast.join !== void 0 || ast.subqueryJoin !== void 0) {
8167
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "CTEs inside JOINs are not supported yet");
8262
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "CTEs inside JOINs are not supported yet");
8168
8263
  }
8169
8264
  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();
8170
8265
  if (cteRows.length === 0) {
8171
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "Empty CTE results are not supported yet");
8266
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Empty CTE results are not supported yet");
8172
8267
  }
8173
- const prefix = `__laql_cte/${ast.cte.name}`;
8268
+ const prefix = `__lakeql_cte/${ast.cte.name}`;
8174
8269
  await writePartitionedParquet(store, prefix, {
8175
8270
  rows: cteRows,
8176
8271
  maxRowsPerFile: cteRows.length
@@ -8184,7 +8279,7 @@ async function resolveScalarSubqueries(lake, ast) {
8184
8279
  for (const [id, subquery] of Object.entries(ast.scalarSubqueries)) {
8185
8280
  const rows = hasAggregation(subquery.query) ? await aggregateRowsFromAst(lake.path(subquery.query.source), subquery.query) : await builderFromAst(lake.path(subquery.query.source), subquery.query).toArray();
8186
8281
  if (rows.length > 1) {
8187
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
8282
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
8188
8283
  }
8189
8284
  values.set(id, rows.length === 0 ? null : rows[0]?.[subquery.column] ?? null);
8190
8285
  }
@@ -8205,10 +8300,10 @@ async function resolveScalarSubqueries(lake, ast) {
8205
8300
  function replaceScalarSubqueryExpr(expr, values) {
8206
8301
  switch (expr.kind) {
8207
8302
  case "call":
8208
- if (expr.fn === "__laql_scalar_subquery") {
8303
+ if (expr.fn === "__lakeql_scalar_subquery") {
8209
8304
  const id = expr.args[0];
8210
8305
  if (id?.kind !== "literal" || typeof id.value !== "string" || !values.has(id.value)) {
8211
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "Invalid scalar subquery placeholder");
8306
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Invalid scalar subquery placeholder");
8212
8307
  }
8213
8308
  return { kind: "literal", value: values.get(id.value) };
8214
8309
  }
@@ -8264,10 +8359,10 @@ function replaceScalarSubqueryExpr(expr, values) {
8264
8359
  }
8265
8360
  }
8266
8361
  async function joinRowsFromAst(lake, ast, args) {
8267
- if (ast.join === void 0) throw new LaQLError("LAQL_VALIDATION_ERROR", "Missing SQL JOIN");
8362
+ if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
8268
8363
  const join = ast.join;
8269
8364
  if (hasAggregation(ast)) {
8270
- throw new LaQLError("LAQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
8365
+ throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
8271
8366
  }
8272
8367
  const leftAlias = leftJoinAlias(ast);
8273
8368
  const plan = planJoinSides(ast, leftAlias, join.alias);
@@ -8300,7 +8395,7 @@ async function joinRowsFromAst(lake, ast, args) {
8300
8395
  return rows;
8301
8396
  }
8302
8397
  function planJoinSides(ast, leftAlias, rightAlias) {
8303
- if (ast.join === void 0) throw new LaQLError("LAQL_VALIDATION_ERROR", "Missing SQL JOIN");
8398
+ if (ast.join === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
8304
8399
  const leftPredicates = [];
8305
8400
  const rightPredicates = [];
8306
8401
  const residualPredicates = [];
@@ -8509,11 +8604,11 @@ function fillLeftJoinNulls(rows, rightAlias, rightColumns) {
8509
8604
  }
8510
8605
  async function subqueryJoinRowsFromAst(lake, ast, args) {
8511
8606
  if (ast.subqueryJoin === void 0) {
8512
- throw new LaQLError("LAQL_VALIDATION_ERROR", "Missing SQL IN subquery");
8607
+ throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL IN subquery");
8513
8608
  }
8514
8609
  if (hasAggregation(ast)) {
8515
- throw new LaQLError(
8516
- "LAQL_SQL_UNSUPPORTED",
8610
+ throw new LakeqlError(
8611
+ "LAKEQL_SQL_UNSUPPORTED",
8517
8612
  "Aggregate SQL over IN subquery is not supported yet"
8518
8613
  );
8519
8614
  }
@@ -8591,8 +8686,8 @@ function validateAggregateProjection(ast) {
8591
8686
  const { column } = selectColumn(select);
8592
8687
  if (column === "*") continue;
8593
8688
  if (!groupColumns.has(column)) {
8594
- throw new LaQLError(
8595
- "LAQL_SQL_UNSUPPORTED",
8689
+ throw new LakeqlError(
8690
+ "LAKEQL_SQL_UNSUPPORTED",
8596
8691
  `Aggregate SQL can only select grouped columns or aggregate expressions, not ${column}`
8597
8692
  );
8598
8693
  }
@@ -8622,7 +8717,7 @@ function projectAggregateRows(rows, ast, hiddenCountAlias) {
8622
8717
  }
8623
8718
  function hiddenAggregateAlias(ast) {
8624
8719
  const reserved = /* @__PURE__ */ new Set([...ast.select ?? [], ...Object.keys(ast.aggregates ?? {})]);
8625
- let alias = "__laql_group_count";
8720
+ let alias = "__lakeql_group_count";
8626
8721
  while (reserved.has(alias)) alias = `_${alias}`;
8627
8722
  return alias;
8628
8723
  }
@@ -8666,6 +8761,17 @@ function rowsToCsv(rows) {
8666
8761
  return `${lines.join("\n")}
8667
8762
  `;
8668
8763
  }
8764
+ function formatRows(rows, format) {
8765
+ if (format === "json") return `${JSON.stringify(rows)}
8766
+ `;
8767
+ if (format === "csv") return rowsToCsv(rows);
8768
+ return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
8769
+ }
8770
+ async function schemaSummary(store, key, displayPath = key) {
8771
+ const metadata = await readParquetMetadata(store, key);
8772
+ const columns = metadata.schema.filter((field) => field.name !== "root").map((field) => ({ name: field.name, type: field.type ?? field.converted_type ?? "group" }));
8773
+ return { path: displayPath, rows: Number(totalRows(metadata.row_groups)), columns };
8774
+ }
8669
8775
  function csvCell(value2) {
8670
8776
  if (value2 === null || value2 === void 0) return "";
8671
8777
  const text = String(value2);
@@ -8679,20 +8785,38 @@ function parseCliSql(sql, defaultSource, preserveSqlSource = false) {
8679
8785
  }
8680
8786
  return { ...parseSql(addDefaultFrom(trimmed)), source: defaultSource };
8681
8787
  }
8682
- function applyDefaultSource(ast, defaultSource) {
8788
+ function parseCliSqlStatement(sql, defaultSource, preserveSqlSource = false) {
8789
+ const trimmed = sql.trim();
8790
+ if (/^\s*describe\b/iu.test(trimmed)) {
8791
+ const statement = parseSqlStatement(trimmed);
8792
+ if (!isDescribeStatement(statement)) return statement;
8793
+ if (preserveSqlSource || statement.source !== "input") return statement;
8794
+ return { ...statement, source: defaultSource };
8795
+ }
8796
+ return parseCliSql(sql, defaultSource, preserveSqlSource);
8797
+ }
8798
+ function isDescribeStatement(statement) {
8799
+ return "type" in statement && statement.type === "describe";
8800
+ }
8801
+ function applyDefaultSource(ast, defaultSource, includeScalarSubqueries = true, seen = /* @__PURE__ */ new WeakSet()) {
8802
+ if (seen.has(ast)) return ast.cte === void 0 ? { ...ast, source: defaultSource } : ast;
8803
+ seen.add(ast);
8683
8804
  const out = { ...ast };
8684
8805
  if (out.cte !== void 0) {
8685
- out.cte = { ...out.cte, query: applyDefaultSource(out.cte.query, defaultSource) };
8806
+ out.cte = {
8807
+ ...out.cte,
8808
+ query: applyDefaultSource(out.cte.query, defaultSource, includeScalarSubqueries, seen)
8809
+ };
8686
8810
  } else {
8687
8811
  out.source = defaultSource;
8688
8812
  }
8689
8813
  if (out.subqueryJoin !== void 0)
8690
8814
  out.subqueryJoin = { ...out.subqueryJoin, source: defaultSource };
8691
- if (out.scalarSubqueries !== void 0) {
8815
+ if (includeScalarSubqueries && out.scalarSubqueries !== void 0) {
8692
8816
  out.scalarSubqueries = Object.fromEntries(
8693
8817
  Object.entries(out.scalarSubqueries).map(([id, subquery]) => [
8694
8818
  id,
8695
- { ...subquery, query: applyDefaultSource(subquery.query, defaultSource) }
8819
+ { ...subquery, query: applyDefaultSource(subquery.query, defaultSource, false, seen) }
8696
8820
  ])
8697
8821
  );
8698
8822
  }
@@ -8700,7 +8824,7 @@ function applyDefaultSource(ast, defaultSource) {
8700
8824
  }
8701
8825
  function addDefaultFrom(sql) {
8702
8826
  if (!/^\s*select\b/iu.test(sql)) {
8703
- throw new LaQLError("LAQL_PARSE_ERROR", "SQL must start with SELECT");
8827
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", "SQL must start with SELECT");
8704
8828
  }
8705
8829
  const clause = /\b(where|group\s+by|having|order\s+by|limit|offset)\b/iu.exec(sql);
8706
8830
  if (clause === null || clause.index === void 0) return `${sql} from input`;
@@ -8763,45 +8887,45 @@ function parseArgs(argv) {
8763
8887
  } else if (arg === "--job-id") {
8764
8888
  index += 1;
8765
8889
  args.jobId = requireValue(rest, index, arg);
8766
- } else throw new LaQLError("LAQL_PARSE_ERROR", `Unknown argument ${arg}`);
8890
+ } else throw new LakeqlError("LAKEQL_PARSE_ERROR", `Unknown argument ${arg}`);
8767
8891
  }
8768
8892
  return args;
8769
8893
  }
8770
8894
  function parseTableArg(value2) {
8771
8895
  const separator = value2.indexOf("=");
8772
8896
  if (separator <= 0 || separator === value2.length - 1) {
8773
- throw new LaQLError("LAQL_PARSE_ERROR", "--table must be name=path.parquet");
8897
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", "--table must be name=path.parquet");
8774
8898
  }
8775
8899
  const name = value2.slice(0, separator);
8776
8900
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
8777
- throw new LaQLError("LAQL_PARSE_ERROR", "--table name must be a SQL identifier");
8901
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", "--table name must be a SQL identifier");
8778
8902
  }
8779
8903
  return { name, path: value2.slice(separator + 1) };
8780
8904
  }
8781
8905
  function parseFormat(value2) {
8782
8906
  if (value2 === "csv" || value2 === "json" || value2 === "ndjson") return value2;
8783
- throw new LaQLError("LAQL_PARSE_ERROR", "--format must be csv, json, or ndjson");
8907
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", "--format must be csv, json, or ndjson");
8784
8908
  }
8785
8909
  function requireValue(args, index, flag) {
8786
8910
  const value2 = args[index];
8787
8911
  if (value2 === void 0 || value2.startsWith("--")) {
8788
- throw new LaQLError("LAQL_PARSE_ERROR", `${flag} requires a value`);
8912
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", `${flag} requires a value`);
8789
8913
  }
8790
8914
  return value2;
8791
8915
  }
8792
8916
  function requireOption(value2, flag) {
8793
- if (value2 === void 0) throw new LaQLError("LAQL_PARSE_ERROR", `${flag} is required`);
8917
+ if (value2 === void 0) throw new LakeqlError("LAKEQL_PARSE_ERROR", `${flag} is required`);
8794
8918
  return value2;
8795
8919
  }
8796
8920
  function parseCsv(value2, flag) {
8797
8921
  const values = value2.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
8798
- if (values.length === 0) throw new LaQLError("LAQL_PARSE_ERROR", `${flag} must not be empty`);
8922
+ if (values.length === 0) throw new LakeqlError("LAKEQL_PARSE_ERROR", `${flag} must not be empty`);
8799
8923
  return values;
8800
8924
  }
8801
8925
  function parsePositiveInt(value2, flag) {
8802
8926
  const parsed = Number(value2);
8803
8927
  if (!Number.isInteger(parsed) || parsed <= 0) {
8804
- throw new LaQLError("LAQL_PARSE_ERROR", `${flag} must be a positive integer`);
8928
+ throw new LakeqlError("LAKEQL_PARSE_ERROR", `${flag} must be a positive integer`);
8805
8929
  }
8806
8930
  return parsed;
8807
8931
  }