tranfi 0.1.2 → 0.2.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.
Files changed (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +443 -51
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +352 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +81 -41
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
package/csrc/expr.c CHANGED
@@ -11,7 +11,7 @@
11
11
  * add_expr = mul_expr (('+' | '-') mul_expr)*
12
12
  * mul_expr = unary (('*' | '/') unary)*
13
13
  * unary = '-' unary | atom
14
- * atom = NUMBER | STRING | col_ref | '(' expr ')'
14
+ * atom = NUMBER | STRING | BOOL | col_ref | '(' expr ')'
15
15
  * col_ref = 'col(' STRING ')'
16
16
  * NUMBER = [0-9]+('.'[0-9]+)?
17
17
  * STRING = '\'' [^']* '\'' | '"' [^"]* '"'
@@ -35,6 +35,7 @@ typedef struct {
35
35
  const char *src;
36
36
  size_t pos;
37
37
  size_t len;
38
+ int depth_error;
38
39
  } parser_state;
39
40
 
40
41
  static void skip_ws(parser_state *p) {
@@ -91,7 +92,15 @@ static void consume_cmp_op(parser_state *p, cmp_op op) {
91
92
 
92
93
  /* ---- Recursive descent parser ---- */
93
94
 
94
- static tf_expr *parse_expr(parser_state *p);
95
+ #define TF_EXPR_MAX_DEPTH 256
96
+
97
+ static tf_expr *parse_expr_d(parser_state *p, unsigned depth);
98
+
99
+ static int expr_depth_exceeded(parser_state *p, unsigned depth) {
100
+ if (depth <= TF_EXPR_MAX_DEPTH) return 0;
101
+ p->depth_error = 1;
102
+ return 1;
103
+ }
95
104
 
96
105
  static tf_expr *make_expr(expr_kind kind) {
97
106
  tf_expr *e = calloc(1, sizeof(tf_expr));
@@ -115,7 +124,16 @@ static tf_expr *parse_string_literal(parser_state *p) {
115
124
 
116
125
  tf_expr *e = make_expr(EXPR_LIT_STR);
117
126
  if (!e) return NULL;
118
- e->lit_str = malloc(slen + 1);
127
+ size_t alloc_len = 0;
128
+ if (tf_size_add(slen, 1, &alloc_len) != TF_OK) {
129
+ tf_expr_free(e);
130
+ return NULL;
131
+ }
132
+ e->lit_str = tf_mallocarray_checked(alloc_len, sizeof(char));
133
+ if (!e->lit_str) {
134
+ tf_expr_free(e);
135
+ return NULL;
136
+ }
119
137
  memcpy(e->lit_str, p->src + start, slen);
120
138
  e->lit_str[slen] = '\0';
121
139
  return e;
@@ -124,10 +142,10 @@ static tf_expr *parse_string_literal(parser_state *p) {
124
142
  /* Parse a comma-separated argument list inside parentheses.
125
143
  * Assumes '(' has already been consumed. Consumes the closing ')'.
126
144
  * Returns allocated array of expressions, sets *n_args. */
127
- static tf_expr **parse_arg_list(parser_state *p, int *n_args) {
145
+ static tf_expr **parse_arg_list(parser_state *p, int *n_args, unsigned depth) {
128
146
  *n_args = 0;
129
- int cap = 4;
130
- tf_expr **args = malloc(cap * sizeof(tf_expr *));
147
+ size_t cap = 4;
148
+ tf_expr **args = tf_mallocarray_checked(cap, sizeof(tf_expr *));
131
149
  if (!args) return NULL;
132
150
 
133
151
  skip_ws(p);
@@ -138,13 +156,20 @@ static tf_expr **parse_arg_list(parser_state *p, int *n_args) {
138
156
  }
139
157
 
140
158
  for (;;) {
141
- tf_expr *arg = parse_expr(p);
159
+ tf_expr *arg = parse_expr_d(p, depth);
142
160
  if (!arg) goto fail;
143
- if (*n_args >= cap) {
144
- cap *= 2;
145
- tf_expr **na = realloc(args, cap * sizeof(tf_expr *));
161
+ if ((size_t)*n_args >= cap) {
162
+ size_t need = 0;
163
+ size_t new_cap = 0;
164
+ if (tf_size_add((size_t)*n_args, 1, &need) != TF_OK ||
165
+ tf_size_grow_pow2(cap, need, 4, &new_cap) != TF_OK) {
166
+ tf_expr_free(arg);
167
+ goto fail;
168
+ }
169
+ tf_expr **na = tf_reallocarray_checked(args, new_cap, sizeof(tf_expr *));
146
170
  if (!na) { tf_expr_free(arg); goto fail; }
147
171
  args = na;
172
+ cap = new_cap;
148
173
  }
149
174
  args[(*n_args)++] = arg;
150
175
  skip_ws(p);
@@ -164,14 +189,15 @@ fail:
164
189
  return NULL;
165
190
  }
166
191
 
167
- static tf_expr *parse_atom(parser_state *p) {
192
+ static tf_expr *parse_atom(parser_state *p, unsigned depth) {
193
+ if (expr_depth_exceeded(p, depth)) return NULL;
168
194
  skip_ws(p);
169
195
  if (p->pos >= p->len) return NULL;
170
196
 
171
197
  /* Parenthesized expression */
172
198
  if (p->src[p->pos] == '(') {
173
199
  p->pos++;
174
- tf_expr *e = parse_expr(p);
200
+ tf_expr *e = parse_expr_d(p, depth + 1);
175
201
  if (!e) return NULL;
176
202
  if (!match_char(p, ')')) { tf_expr_free(e); return NULL; }
177
203
  return e;
@@ -201,7 +227,10 @@ static tf_expr *parse_atom(parser_state *p) {
201
227
  }
202
228
 
203
229
  size_t numlen = p->pos - start;
204
- char *numstr = malloc(numlen + 1);
230
+ size_t alloc_len = 0;
231
+ if (tf_size_add(numlen, 1, &alloc_len) != TF_OK) return NULL;
232
+ char *numstr = tf_mallocarray_checked(alloc_len, sizeof(char));
233
+ if (!numstr) return NULL;
205
234
  memcpy(numstr, p->src + start, numlen);
206
235
  numstr[numlen] = '\0';
207
236
 
@@ -234,7 +263,9 @@ static tf_expr *parse_atom(parser_state *p) {
234
263
  return NULL;
235
264
  }
236
265
 
237
- char *name = malloc(id_len + 1);
266
+ size_t name_len = 0;
267
+ if (tf_size_add(id_len, 1, &name_len) != TF_OK) return NULL;
268
+ char *name = tf_mallocarray_checked(name_len, sizeof(char));
238
269
  if (!name) return NULL;
239
270
  memcpy(name, p->src + start, id_len);
240
271
  name[id_len] = '\0';
@@ -262,10 +293,13 @@ static tf_expr *parse_atom(parser_state *p) {
262
293
  (isalnum((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_'))
263
294
  p->pos++;
264
295
  if (p->pos == cstart) return NULL;
265
- col_name = malloc(p->pos - cstart + 1);
296
+ size_t col_len = p->pos - cstart;
297
+ size_t col_alloc_len = 0;
298
+ if (tf_size_add(col_len, 1, &col_alloc_len) != TF_OK) return NULL;
299
+ col_name = tf_mallocarray_checked(col_alloc_len, sizeof(char));
266
300
  if (!col_name) return NULL;
267
- memcpy(col_name, p->src + cstart, p->pos - cstart);
268
- col_name[p->pos - cstart] = '\0';
301
+ memcpy(col_name, p->src + cstart, col_len);
302
+ col_name[col_len] = '\0';
269
303
  }
270
304
 
271
305
  if (!match_char(p, ')')) { free(col_name); return NULL; }
@@ -277,7 +311,7 @@ static tf_expr *parse_atom(parser_state *p) {
277
311
 
278
312
  /* General function call: name(arg1, arg2, ...) */
279
313
  int n_args = 0;
280
- tf_expr **args = parse_arg_list(p, &n_args);
314
+ tf_expr **args = parse_arg_list(p, &n_args, depth + 1);
281
315
  if (!args) { free(name); return NULL; }
282
316
 
283
317
  tf_expr *e = make_expr(EXPR_FUNC_CALL);
@@ -293,6 +327,14 @@ static tf_expr *parse_atom(parser_state *p) {
293
327
  return e;
294
328
  }
295
329
 
330
+ if (strcmp(name, "true") == 0 || strcmp(name, "false") == 0) {
331
+ tf_expr *e = make_expr(EXPR_LIT_BOOL);
332
+ if (!e) { free(name); return NULL; }
333
+ e->lit_bool = (strcmp(name, "true") == 0);
334
+ free(name);
335
+ return e;
336
+ }
337
+
296
338
  /* Bare identifier — treat as column reference */
297
339
  tf_expr *e = make_expr(EXPR_COL_REF);
298
340
  if (!e) { free(name); return NULL; }
@@ -304,12 +346,13 @@ static tf_expr *parse_atom(parser_state *p) {
304
346
  }
305
347
 
306
348
  /* Unary minus: -expr */
307
- static tf_expr *parse_unary(parser_state *p) {
349
+ static tf_expr *parse_unary(parser_state *p, unsigned depth) {
350
+ if (expr_depth_exceeded(p, depth)) return NULL;
308
351
  skip_ws(p);
309
352
  if (p->pos < p->len && p->src[p->pos] == '-') {
310
353
  /* Check it's not a comparison op like != */
311
354
  p->pos++;
312
- tf_expr *child = parse_unary(p);
355
+ tf_expr *child = parse_unary(p, depth + 1);
313
356
  if (!child) return NULL;
314
357
  /* Optimize: fold literal negation */
315
358
  if (child->kind == EXPR_LIT_INT) {
@@ -325,12 +368,12 @@ static tf_expr *parse_unary(parser_state *p) {
325
368
  e->child = child;
326
369
  return e;
327
370
  }
328
- return parse_atom(p);
371
+ return parse_atom(p, depth);
329
372
  }
330
373
 
331
374
  /* Multiplicative: unary (('*' | '/') unary)* */
332
- static tf_expr *parse_mul(parser_state *p) {
333
- tf_expr *left = parse_unary(p);
375
+ static tf_expr *parse_mul(parser_state *p, unsigned depth) {
376
+ tf_expr *left = parse_unary(p, depth);
334
377
  if (!left) return NULL;
335
378
 
336
379
  for (;;) {
@@ -340,7 +383,7 @@ static tf_expr *parse_mul(parser_state *p) {
340
383
  if (op_char != '*' && op_char != '/') break;
341
384
  p->pos++;
342
385
 
343
- tf_expr *right = parse_unary(p);
386
+ tf_expr *right = parse_unary(p, depth);
344
387
  if (!right) { tf_expr_free(left); return NULL; }
345
388
 
346
389
  expr_kind kind = (op_char == '*') ? EXPR_MUL : EXPR_DIV;
@@ -354,8 +397,8 @@ static tf_expr *parse_mul(parser_state *p) {
354
397
  }
355
398
 
356
399
  /* Additive: mul_expr (('+' | '-') mul_expr)* */
357
- static tf_expr *parse_add(parser_state *p) {
358
- tf_expr *left = parse_mul(p);
400
+ static tf_expr *parse_add(parser_state *p, unsigned depth) {
401
+ tf_expr *left = parse_mul(p, depth);
359
402
  if (!left) return NULL;
360
403
 
361
404
  for (;;) {
@@ -368,7 +411,7 @@ static tf_expr *parse_add(parser_state *p) {
368
411
  * Since we're at the add level, '+' and '-' are always arithmetic. */
369
412
  p->pos++;
370
413
 
371
- tf_expr *right = parse_mul(p);
414
+ tf_expr *right = parse_mul(p, depth);
372
415
  if (!right) { tf_expr_free(left); return NULL; }
373
416
 
374
417
  expr_kind kind = (op_char == '+') ? EXPR_ADD : EXPR_SUB;
@@ -381,14 +424,14 @@ static tf_expr *parse_add(parser_state *p) {
381
424
  return left;
382
425
  }
383
426
 
384
- static tf_expr *parse_cmp(parser_state *p) {
385
- tf_expr *left = parse_add(p);
427
+ static tf_expr *parse_cmp(parser_state *p, unsigned depth) {
428
+ tf_expr *left = parse_add(p, depth);
386
429
  if (!left) return NULL;
387
430
 
388
431
  cmp_op op;
389
432
  if (peek_cmp_op(p, &op)) {
390
433
  consume_cmp_op(p, op);
391
- tf_expr *right = parse_add(p);
434
+ tf_expr *right = parse_add(p, depth);
392
435
  if (!right) { tf_expr_free(left); return NULL; }
393
436
 
394
437
  tf_expr *e = make_expr(EXPR_CMP);
@@ -402,23 +445,24 @@ static tf_expr *parse_cmp(parser_state *p) {
402
445
  return left;
403
446
  }
404
447
 
405
- static tf_expr *parse_not(parser_state *p) {
448
+ static tf_expr *parse_not(parser_state *p, unsigned depth) {
449
+ if (expr_depth_exceeded(p, depth)) return NULL;
406
450
  if (match_keyword(p, "not")) {
407
- tf_expr *child = parse_not(p);
451
+ tf_expr *child = parse_not(p, depth + 1);
408
452
  if (!child) return NULL;
409
453
  tf_expr *e = make_expr(EXPR_NOT);
410
454
  if (!e) { tf_expr_free(child); return NULL; }
411
455
  e->child = child;
412
456
  return e;
413
457
  }
414
- return parse_cmp(p);
458
+ return parse_cmp(p, depth);
415
459
  }
416
460
 
417
- static tf_expr *parse_and(parser_state *p) {
418
- tf_expr *left = parse_not(p);
461
+ static tf_expr *parse_and(parser_state *p, unsigned depth) {
462
+ tf_expr *left = parse_not(p, depth);
419
463
  if (!left) return NULL;
420
464
  while (match_keyword(p, "and")) {
421
- tf_expr *right = parse_not(p);
465
+ tf_expr *right = parse_not(p, depth);
422
466
  if (!right) { tf_expr_free(left); return NULL; }
423
467
  tf_expr *e = make_expr(EXPR_AND);
424
468
  if (!e) { tf_expr_free(left); tf_expr_free(right); return NULL; }
@@ -429,11 +473,11 @@ static tf_expr *parse_and(parser_state *p) {
429
473
  return left;
430
474
  }
431
475
 
432
- static tf_expr *parse_or(parser_state *p) {
433
- tf_expr *left = parse_and(p);
476
+ static tf_expr *parse_or(parser_state *p, unsigned depth) {
477
+ tf_expr *left = parse_and(p, depth);
434
478
  if (!left) return NULL;
435
479
  while (match_keyword(p, "or")) {
436
- tf_expr *right = parse_and(p);
480
+ tf_expr *right = parse_and(p, depth);
437
481
  if (!right) { tf_expr_free(left); return NULL; }
438
482
  tf_expr *e = make_expr(EXPR_OR);
439
483
  if (!e) { tf_expr_free(left); tf_expr_free(right); return NULL; }
@@ -444,18 +488,28 @@ static tf_expr *parse_or(parser_state *p) {
444
488
  return left;
445
489
  }
446
490
 
447
- static tf_expr *parse_expr(parser_state *p) {
448
- return parse_or(p);
491
+ static tf_expr *parse_expr_d(parser_state *p, unsigned depth) {
492
+ if (expr_depth_exceeded(p, depth)) return NULL;
493
+ return parse_or(p, depth);
449
494
  }
450
495
 
451
496
  tf_expr *tf_expr_parse(const char *text) {
452
- if (!text) return NULL;
453
- parser_state p = { .src = text, .pos = 0, .len = strlen(text) };
454
- tf_expr *e = parse_expr(&p);
455
- if (!e) return NULL;
497
+ if (!text || !*text) {
498
+ tf_set_last_error("expression: empty expression");
499
+ return NULL;
500
+ }
501
+ parser_state p = { .src = text, .pos = 0, .len = strlen(text), .depth_error = 0 };
502
+ tf_expr *e = parse_expr_d(&p, 0);
503
+ if (!e) {
504
+ tf_set_last_error(p.depth_error ? "expression: nesting too deep" : "expression: invalid syntax");
505
+ return NULL;
506
+ }
456
507
  /* Ensure we consumed all input */
457
508
  skip_ws(&p);
458
509
  if (p.pos < p.len) {
510
+ char buf[128];
511
+ snprintf(buf, sizeof(buf), "expression: unexpected input near byte %zu", p.pos);
512
+ tf_set_last_error(buf);
459
513
  tf_expr_free(e);
460
514
  return NULL;
461
515
  }
@@ -511,8 +565,8 @@ typedef struct {
511
565
  * Uses a ring buffer approach: each call gets a slot, wrapping around. */
512
566
  #define SCRATCH_SLOTS 8
513
567
  #define SCRATCH_SLOT_SIZE 4096
514
- static char scratch_buf[SCRATCH_SLOTS][SCRATCH_SLOT_SIZE];
515
- static int scratch_idx = 0;
568
+ static _Thread_local char scratch_buf[SCRATCH_SLOTS][SCRATCH_SLOT_SIZE];
569
+ static _Thread_local size_t scratch_idx = 0;
516
570
 
517
571
  static char *scratch_alloc(void) {
518
572
  char *slot = scratch_buf[scratch_idx % SCRATCH_SLOTS];
@@ -530,6 +584,146 @@ static eval_val val_bool(int v) { return (eval_val){ .tag = VAL_BOOL, .b = v };
530
584
  static eval_val val_date(int32_t v) { return (eval_val){ .tag = VAL_DATE, .date = v }; }
531
585
  static eval_val val_timestamp(int64_t v) { return (eval_val){ .tag = VAL_TIMESTAMP, .ts = v }; }
532
586
 
587
+ typedef enum {
588
+ TF_EXPR_TRUNC_YEAR,
589
+ TF_EXPR_TRUNC_MONTH,
590
+ TF_EXPR_TRUNC_DAY,
591
+ TF_EXPR_TRUNC_HOUR,
592
+ TF_EXPR_TRUNC_MINUTE,
593
+ TF_EXPR_TRUNC_SECOND,
594
+ } tf_expr_trunc_level;
595
+
596
+ static int expr_parse_trunc_level(const char *s, tf_expr_trunc_level *level) {
597
+ if (!s || !level) return 0;
598
+ if (strcmp(s, "year") == 0 || strcmp(s, "years") == 0) {
599
+ *level = TF_EXPR_TRUNC_YEAR; return 1;
600
+ }
601
+ if (strcmp(s, "month") == 0 || strcmp(s, "months") == 0) {
602
+ *level = TF_EXPR_TRUNC_MONTH; return 1;
603
+ }
604
+ if (strcmp(s, "day") == 0 || strcmp(s, "days") == 0) {
605
+ *level = TF_EXPR_TRUNC_DAY; return 1;
606
+ }
607
+ if (strcmp(s, "hour") == 0 || strcmp(s, "hours") == 0) {
608
+ *level = TF_EXPR_TRUNC_HOUR; return 1;
609
+ }
610
+ if (strcmp(s, "minute") == 0 || strcmp(s, "minutes") == 0) {
611
+ *level = TF_EXPR_TRUNC_MINUTE; return 1;
612
+ }
613
+ if (strcmp(s, "second") == 0 || strcmp(s, "seconds") == 0) {
614
+ *level = TF_EXPR_TRUNC_SECOND; return 1;
615
+ }
616
+ return 0;
617
+ }
618
+
619
+ static int parse_temporal_string(const char *s, int *y, int *mo, int *d,
620
+ int *h, int *mi, int *se, int *frac_us,
621
+ int *has_time) {
622
+ if (!s) return 0;
623
+ *y = *mo = *d = *h = *mi = *se = *frac_us = 0;
624
+ *has_time = 0;
625
+
626
+ int n = sscanf(s, "%d-%d-%dT%d:%d:%d", y, mo, d, h, mi, se);
627
+ if (n >= 6) {
628
+ *has_time = 1;
629
+ const char *dot = strchr(s, '.');
630
+ if (dot) {
631
+ int frac = 0, digits = 0;
632
+ dot++;
633
+ while (digits < 6 && dot[digits] >= '0' && dot[digits] <= '9') {
634
+ frac = frac * 10 + (dot[digits] - '0');
635
+ digits++;
636
+ }
637
+ while (digits > 0 && digits < 6) { frac *= 10; digits++; }
638
+ *frac_us = frac;
639
+ }
640
+ return 1;
641
+ }
642
+ n = sscanf(s, "%d-%d-%d %d:%d:%d", y, mo, d, h, mi, se);
643
+ if (n >= 6) {
644
+ *has_time = 1;
645
+ const char *dot = strchr(s, '.');
646
+ if (dot) {
647
+ int frac = 0, digits = 0;
648
+ dot++;
649
+ while (digits < 6 && dot[digits] >= '0' && dot[digits] <= '9') {
650
+ frac = frac * 10 + (dot[digits] - '0');
651
+ digits++;
652
+ }
653
+ while (digits > 0 && digits < 6) { frac *= 10; digits++; }
654
+ *frac_us = frac;
655
+ }
656
+ return 1;
657
+ }
658
+ n = sscanf(s, "%d-%d-%d", y, mo, d);
659
+ return n >= 3;
660
+ }
661
+
662
+ static int eval_temporal_parts(eval_val v, int *y, int *mo, int *d,
663
+ int *h, int *mi, int *se, int *frac_us,
664
+ int32_t *date_days, int64_t *timestamp_us,
665
+ int *is_timestamp, int *is_string) {
666
+ *y = *mo = *d = *h = *mi = *se = *frac_us = 0;
667
+ *date_days = 0;
668
+ *timestamp_us = 0;
669
+ *is_timestamp = 0;
670
+ *is_string = 0;
671
+
672
+ if (v.tag == VAL_DATE) {
673
+ tf_date_to_ymd(v.date, y, mo, d);
674
+ *date_days = v.date;
675
+ return 1;
676
+ }
677
+ if (v.tag == VAL_TIMESTAMP) {
678
+ tf_timestamp_to_parts(v.ts, y, mo, d, h, mi, se, frac_us);
679
+ *date_days = tf_date_from_ymd(*y, *mo, *d);
680
+ *timestamp_us = v.ts;
681
+ *is_timestamp = 1;
682
+ return 1;
683
+ }
684
+ if (v.tag == VAL_STR) {
685
+ int has_time = 0;
686
+ if (!parse_temporal_string(v.s, y, mo, d, h, mi, se, frac_us, &has_time)) return 0;
687
+ *date_days = tf_date_from_ymd(*y, *mo, *d);
688
+ if (has_time) {
689
+ *timestamp_us = tf_timestamp_from_parts(*y, *mo, *d, *h, *mi, *se, *frac_us);
690
+ *is_timestamp = 1;
691
+ }
692
+ *is_string = 1;
693
+ return 1;
694
+ }
695
+ return 0;
696
+ }
697
+
698
+ static int32_t expr_trunc_date(int32_t days, tf_expr_trunc_level level) {
699
+ int y, mo, d;
700
+ tf_date_to_ymd(days, &y, &mo, &d);
701
+ switch (level) {
702
+ case TF_EXPR_TRUNC_YEAR: return tf_date_from_ymd(y, 1, 1);
703
+ case TF_EXPR_TRUNC_MONTH: return tf_date_from_ymd(y, mo, 1);
704
+ default: return days;
705
+ }
706
+ }
707
+
708
+ static int64_t expr_trunc_timestamp(int64_t us, tf_expr_trunc_level level) {
709
+ int y, mo, d, h, mi, se, frac;
710
+ tf_timestamp_to_parts(us, &y, &mo, &d, &h, &mi, &se, &frac);
711
+ switch (level) {
712
+ case TF_EXPR_TRUNC_YEAR: return tf_timestamp_from_parts(y, 1, 1, 0, 0, 0, 0);
713
+ case TF_EXPR_TRUNC_MONTH: return tf_timestamp_from_parts(y, mo, 1, 0, 0, 0, 0);
714
+ case TF_EXPR_TRUNC_DAY: return tf_timestamp_from_parts(y, mo, d, 0, 0, 0, 0);
715
+ case TF_EXPR_TRUNC_HOUR: return tf_timestamp_from_parts(y, mo, d, h, 0, 0, 0);
716
+ case TF_EXPR_TRUNC_MINUTE: return tf_timestamp_from_parts(y, mo, d, h, mi, 0, 0);
717
+ case TF_EXPR_TRUNC_SECOND: return tf_timestamp_from_parts(y, mo, d, h, mi, se, 0);
718
+ }
719
+ return us;
720
+ }
721
+
722
+ static int64_t timestamp_epoch_seconds(int64_t us) {
723
+ if (us >= 0) return us / 1000000LL;
724
+ return -(((-us) + 999999LL) / 1000000LL);
725
+ }
726
+
533
727
  /* Convert to double for numeric comparison */
534
728
  static double to_double(eval_val v) {
535
729
  if (v.tag == VAL_INT) return (double)v.i;
@@ -541,6 +735,13 @@ static int is_numeric(eval_val v) {
541
735
  return v.tag == VAL_INT || v.tag == VAL_FLOAT;
542
736
  }
543
737
 
738
+ static int eval_truthy(eval_val v) {
739
+ if (v.tag == VAL_BOOL) return v.b;
740
+ if (v.tag == VAL_INT) return v.i != 0;
741
+ if (v.tag == VAL_FLOAT) return v.f != 0.0;
742
+ return v.tag != VAL_NULL;
743
+ }
744
+
544
745
  static eval_val eval_cmp(eval_val lv, eval_val rv, cmp_op op) {
545
746
  /* Null comparisons: null != anything is true, null == null is true */
546
747
  if (lv.tag == VAL_NULL || rv.tag == VAL_NULL) {
@@ -565,6 +766,20 @@ static eval_val eval_cmp(eval_val lv, eval_val rv, cmp_op op) {
565
766
  }
566
767
  }
567
768
 
769
+ /* Boolean comparison */
770
+ if (lv.tag == VAL_BOOL && rv.tag == VAL_BOOL) {
771
+ int l = lv.b ? 1 : 0;
772
+ int r = rv.b ? 1 : 0;
773
+ switch (op) {
774
+ case CMP_GT: return val_bool(l > r);
775
+ case CMP_GE: return val_bool(l >= r);
776
+ case CMP_LT: return val_bool(l < r);
777
+ case CMP_LE: return val_bool(l <= r);
778
+ case CMP_EQ: return val_bool(l == r);
779
+ case CMP_NE: return val_bool(l != r);
780
+ }
781
+ }
782
+
568
783
  /* Numeric comparison */
569
784
  if (is_numeric(lv) && is_numeric(rv)) {
570
785
  double l = to_double(lv);
@@ -688,7 +903,9 @@ static const char *val_to_str(eval_val v, char *buf, size_t buf_sz) {
688
903
  switch (v.tag) {
689
904
  case VAL_STR: return v.s;
690
905
  case VAL_INT: snprintf(buf, buf_sz, "%lld", (long long)v.i); return buf;
691
- case VAL_FLOAT: snprintf(buf, buf_sz, "%g", v.f); return buf;
906
+ case VAL_FLOAT:
907
+ if (tf_format_float64(buf, buf_sz, v.f) != TF_OK && buf_sz > 0) buf[0] = '\0';
908
+ return buf;
692
909
  case VAL_BOOL: return v.b ? "true" : "false";
693
910
  case VAL_DATE: tf_date_format(v.date, buf, buf_sz); return buf;
694
911
  case VAL_TIMESTAMP: tf_timestamp_format(v.ts, buf, buf_sz); return buf;
@@ -870,13 +1087,43 @@ static eval_val eval_func(const char *name, eval_val *args, int n_args) {
870
1087
 
871
1088
  /* if(cond, then, else) */
872
1089
  if (strcmp(name, "if") == 0 && n_args == 3) {
873
- if (args[0].tag == VAL_BOOL)
874
- return args[0].b ? args[1] : args[2];
875
- /* Truthy: non-null, non-zero */
876
- int truthy = (args[0].tag != VAL_NULL);
877
- if (args[0].tag == VAL_INT) truthy = (args[0].i != 0);
878
- if (args[0].tag == VAL_FLOAT) truthy = (args[0].f != 0.0);
879
- return truthy ? args[1] : args[2];
1090
+ return eval_truthy(args[0]) ? args[1] : args[2];
1091
+ }
1092
+
1093
+ /* case_when(cond, value, ..., default) — first matching condition */
1094
+ if (strcmp(name, "case_when") == 0 && n_args >= 2) {
1095
+ int pair_count = n_args / 2;
1096
+ int default_idx = (n_args % 2 == 1) ? n_args - 1 : -1;
1097
+ for (int i = 0; i < pair_count; i++) {
1098
+ if (eval_truthy(args[i * 2])) return args[i * 2 + 1];
1099
+ }
1100
+ return default_idx >= 0 ? args[default_idx] : val_null();
1101
+ }
1102
+
1103
+ /* case_match(value, key, result, ..., default) — first equal key */
1104
+ if (strcmp(name, "case_match") == 0 && n_args >= 3) {
1105
+ int remaining = n_args - 1;
1106
+ int pair_count = remaining / 2;
1107
+ int default_idx = (remaining % 2 == 1) ? n_args - 1 : -1;
1108
+ for (int i = 0; i < pair_count; i++) {
1109
+ eval_val eq = eval_cmp(args[0], args[1 + i * 2], CMP_EQ);
1110
+ if (eq.tag == VAL_BOOL && eq.b) return args[2 + i * 2];
1111
+ }
1112
+ return default_idx >= 0 ? args[default_idx] : val_null();
1113
+ }
1114
+
1115
+ /* if_any(pred, ...) / if_all(pred, ...) — row-local boolean reduction */
1116
+ if (strcmp(name, "if_any") == 0) {
1117
+ for (int i = 0; i < n_args; i++) {
1118
+ if (eval_truthy(args[i])) return val_bool(1);
1119
+ }
1120
+ return val_bool(0);
1121
+ }
1122
+ if (strcmp(name, "if_all") == 0) {
1123
+ for (int i = 0; i < n_args; i++) {
1124
+ if (!eval_truthy(args[i])) return val_bool(0);
1125
+ }
1126
+ return val_bool(1);
880
1127
  }
881
1128
 
882
1129
  /* coalesce(a, b, ...) — first non-null */
@@ -887,6 +1134,65 @@ static eval_val eval_func(const char *name, eval_val *args, int n_args) {
887
1134
  return val_null();
888
1135
  }
889
1136
 
1137
+ /* between(x, lower, upper) / inrange(x, lower, upper) — inclusive range predicate */
1138
+ if ((strcmp(name, "between") == 0 || strcmp(name, "inrange") == 0) && n_args == 3) {
1139
+ if (args[0].tag == VAL_NULL || args[1].tag == VAL_NULL || args[2].tag == VAL_NULL)
1140
+ return val_null();
1141
+ eval_val ge = eval_cmp(args[0], args[1], CMP_GE);
1142
+ eval_val le = eval_cmp(args[0], args[2], CMP_LE);
1143
+ if (ge.tag != VAL_BOOL || le.tag != VAL_BOOL) return val_null();
1144
+ return val_bool(ge.b && le.b);
1145
+ }
1146
+
1147
+ /* ---- Date/time functions ---- */
1148
+
1149
+ if ((strcmp(name, "year") == 0 || strcmp(name, "month") == 0 ||
1150
+ strcmp(name, "day") == 0 || strcmp(name, "hour") == 0 ||
1151
+ strcmp(name, "minute") == 0 || strcmp(name, "second") == 0 ||
1152
+ strcmp(name, "weekday") == 0 || strcmp(name, "epoch") == 0) && n_args == 1) {
1153
+ if (args[0].tag == VAL_NULL) return val_null();
1154
+ int y, mo, d, h, mi, se, frac, is_ts, is_str;
1155
+ int32_t days;
1156
+ int64_t us;
1157
+ if (!eval_temporal_parts(args[0], &y, &mo, &d, &h, &mi, &se, &frac,
1158
+ &days, &us, &is_ts, &is_str)) return val_null();
1159
+ (void)frac; (void)is_str;
1160
+ if (strcmp(name, "year") == 0) return val_int(y);
1161
+ if (strcmp(name, "month") == 0) return val_int(mo);
1162
+ if (strcmp(name, "day") == 0) return val_int(d);
1163
+ if (strcmp(name, "hour") == 0) return val_int(h);
1164
+ if (strcmp(name, "minute") == 0) return val_int(mi);
1165
+ if (strcmp(name, "second") == 0) return val_int(se);
1166
+ if (strcmp(name, "weekday") == 0) return val_int(tf_date_weekday(days));
1167
+ if (strcmp(name, "epoch") == 0) {
1168
+ if (is_ts) return val_int(timestamp_epoch_seconds(us));
1169
+ return val_int((int64_t)days * 86400LL);
1170
+ }
1171
+ }
1172
+
1173
+ if (strcmp(name, "date_trunc") == 0 && n_args == 2) {
1174
+ if (args[0].tag == VAL_NULL || args[1].tag != VAL_STR) return val_null();
1175
+ tf_expr_trunc_level level;
1176
+ if (!expr_parse_trunc_level(args[1].s, &level)) return val_null();
1177
+ int y, mo, d, h, mi, se, frac, is_ts, is_str;
1178
+ int32_t days;
1179
+ int64_t us;
1180
+ if (!eval_temporal_parts(args[0], &y, &mo, &d, &h, &mi, &se, &frac,
1181
+ &days, &us, &is_ts, &is_str)) return val_null();
1182
+ (void)y; (void)mo; (void)d; (void)h; (void)mi; (void)se; (void)frac;
1183
+ if (args[0].tag == VAL_DATE) return val_date(expr_trunc_date(days, level));
1184
+ if (args[0].tag == VAL_TIMESTAMP) return val_timestamp(expr_trunc_timestamp(us, level));
1185
+ if (is_str) {
1186
+ char *out = scratch_alloc();
1187
+ if (is_ts) {
1188
+ tf_timestamp_format(expr_trunc_timestamp(us, level), out, SCRATCH_SLOT_SIZE);
1189
+ } else {
1190
+ tf_date_format(expr_trunc_date(days, level), out, SCRATCH_SLOT_SIZE);
1191
+ }
1192
+ return val_str(out);
1193
+ }
1194
+ }
1195
+
890
1196
  /* ---- Math functions ---- */
891
1197
 
892
1198
  /* abs(x) */
@@ -1137,6 +1443,8 @@ static eval_val eval_node(const tf_expr *e, const tf_batch *batch, size_t row) {
1137
1443
  return val_float(e->lit_float);
1138
1444
  case EXPR_LIT_STR:
1139
1445
  return val_str(e->lit_str);
1446
+ case EXPR_LIT_BOOL:
1447
+ return val_bool(e->lit_bool);
1140
1448
  case EXPR_COL_REF: {
1141
1449
  int ci = tf_batch_col_index(batch, e->col_name);
1142
1450
  if (ci < 0) return val_null();
package/csrc/expr.h CHANGED
@@ -11,6 +11,7 @@ typedef enum {
11
11
  EXPR_LIT_INT,
12
12
  EXPR_LIT_FLOAT,
13
13
  EXPR_LIT_STR,
14
+ EXPR_LIT_BOOL,
14
15
  EXPR_COL_REF,
15
16
  EXPR_CMP,
16
17
  EXPR_AND,
@@ -34,6 +35,7 @@ struct tf_expr {
34
35
  int64_t lit_int;
35
36
  double lit_float;
36
37
  char *lit_str;
38
+ int lit_bool;
37
39
  char *col_name; /* for EXPR_COL_REF */
38
40
  struct { /* for EXPR_CMP */
39
41
  struct tf_expr *left;