tranfi 0.0.2 → 0.2.0

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 (151) hide show
  1. package/LICENSE +177 -21
  2. package/NOTICE +8 -0
  3. package/README.md +627 -0
  4. package/app/assets/index-6quYZ5Ap.css +5 -0
  5. package/app/assets/index-BIAIKnrp.js +160 -0
  6. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  7. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  8. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  9. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  10. package/app/index.html +13 -0
  11. package/binding.gyp +121 -0
  12. package/csrc/arena.c +93 -0
  13. package/csrc/batch.c +976 -0
  14. package/csrc/buffer.c +154 -0
  15. package/csrc/cJSON.c +3386 -0
  16. package/csrc/cJSON.h +316 -0
  17. package/csrc/codec_csv.c +1951 -0
  18. package/csrc/codec_jsonl.c +1086 -0
  19. package/csrc/codec_table.c +248 -0
  20. package/csrc/codec_text.c +447 -0
  21. package/csrc/compiler.c +130 -0
  22. package/csrc/config.h +21 -0
  23. package/csrc/date_utils.h +94 -0
  24. package/csrc/dsl.c +5417 -0
  25. package/csrc/dsl.h +22 -0
  26. package/csrc/expr.c +1553 -0
  27. package/csrc/expr.h +58 -0
  28. package/csrc/internal.h +539 -0
  29. package/csrc/ir.c +166 -0
  30. package/csrc/ir.h +208 -0
  31. package/csrc/ir_schema.c +75 -0
  32. package/csrc/ir_serialize.c +166 -0
  33. package/csrc/ir_sql.c +1822 -0
  34. package/csrc/ir_validate.c +576 -0
  35. package/csrc/json_path.c +210 -0
  36. package/csrc/main.c +1241 -0
  37. package/csrc/memory_estimate.c +477 -0
  38. package/csrc/op_acf.c +283 -0
  39. package/csrc/op_across.c +477 -0
  40. package/csrc/op_anomaly.c +255 -0
  41. package/csrc/op_assert.c +761 -0
  42. package/csrc/op_bin.c +248 -0
  43. package/csrc/op_cast.c +523 -0
  44. package/csrc/op_clip.c +99 -0
  45. package/csrc/op_date_trunc.c +355 -0
  46. package/csrc/op_datetime.c +394 -0
  47. package/csrc/op_derive.c +216 -0
  48. package/csrc/op_diff.c +250 -0
  49. package/csrc/op_ewma.c +222 -0
  50. package/csrc/op_explode.c +206 -0
  51. package/csrc/op_fill_down.c +235 -0
  52. package/csrc/op_fill_null.c +268 -0
  53. package/csrc/op_filter.c +181 -0
  54. package/csrc/op_frequency.c +721 -0
  55. package/csrc/op_grep.c +181 -0
  56. package/csrc/op_group_agg.c +1956 -0
  57. package/csrc/op_hash.c +159 -0
  58. package/csrc/op_head.c +84 -0
  59. package/csrc/op_interpolate.c +445 -0
  60. package/csrc/op_join.c +2902 -0
  61. package/csrc/op_json_extract.c +227 -0
  62. package/csrc/op_json_filter.c +384 -0
  63. package/csrc/op_json_flatten.c +293 -0
  64. package/csrc/op_json_schema.c +503 -0
  65. package/csrc/op_label_encode.c +419 -0
  66. package/csrc/op_lag.c +181 -0
  67. package/csrc/op_lead.c +242 -0
  68. package/csrc/op_normalize.c +510 -0
  69. package/csrc/op_onehot.c +457 -0
  70. package/csrc/op_pivot.c +1754 -0
  71. package/csrc/op_quarantine.c +189 -0
  72. package/csrc/op_registry.c +3044 -0
  73. package/csrc/op_rename.c +129 -0
  74. package/csrc/op_replace.c +354 -0
  75. package/csrc/op_rleid.c +297 -0
  76. package/csrc/op_rowid.c +559 -0
  77. package/csrc/op_sample.c +158 -0
  78. package/csrc/op_schema.c +1341 -0
  79. package/csrc/op_schema_infer.c +252 -0
  80. package/csrc/op_select.c +340 -0
  81. package/csrc/op_set.c +3449 -0
  82. package/csrc/op_skip.c +95 -0
  83. package/csrc/op_sort.c +819 -0
  84. package/csrc/op_source_name.c +120 -0
  85. package/csrc/op_split.c +151 -0
  86. package/csrc/op_split_data.c +119 -0
  87. package/csrc/op_stack.c +271 -0
  88. package/csrc/op_stats.c +875 -0
  89. package/csrc/op_step.c +333 -0
  90. package/csrc/op_tail.c +105 -0
  91. package/csrc/op_tee.c +338 -0
  92. package/csrc/op_top.c +357 -0
  93. package/csrc/op_trim.c +138 -0
  94. package/csrc/op_unique.c +1343 -0
  95. package/csrc/op_unpivot.c +193 -0
  96. package/csrc/op_validate.c +648 -0
  97. package/csrc/op_window.c +591 -0
  98. package/csrc/path_policy.c +85 -0
  99. package/csrc/pipeline.c +1088 -0
  100. package/csrc/recipes.c +104 -0
  101. package/csrc/recipes.h +27 -0
  102. package/csrc/report.c +506 -0
  103. package/csrc/report.h +22 -0
  104. package/csrc/selector.c +1097 -0
  105. package/csrc/size_utils.c +348 -0
  106. package/csrc/spill.c +317 -0
  107. package/csrc/spill.h +21 -0
  108. package/csrc/tranfi.h +291 -0
  109. package/csrc/transform.h +209 -0
  110. package/csrc/transform_api.c +2237 -0
  111. package/csrc/transform_categorical.c +923 -0
  112. package/csrc/transform_internal.h +472 -0
  113. package/csrc/transform_json.c +3812 -0
  114. package/csrc/transform_numeric.c +1966 -0
  115. package/csrc/transform_sha256.c +154 -0
  116. package/csrc/transform_wasm.h +162 -0
  117. package/csrc/transform_wasm_api.c +1373 -0
  118. package/csrc/wasm_api.c +218 -0
  119. package/napi_api.c +534 -0
  120. package/napi_transform.c +1648 -0
  121. package/napi_transform.h +8 -0
  122. package/package.json +64 -59
  123. package/scripts/install-native.js +76 -0
  124. package/scripts/prepack.js +64 -0
  125. package/scripts/sync-csrc.js +23 -0
  126. package/src/cli.js +190 -0
  127. package/src/engines/duckdb.js +142 -0
  128. package/src/index.js +925 -0
  129. package/src/memory_policy.js +411 -0
  130. package/src/native.js +18 -0
  131. package/src/pipeline.js +709 -0
  132. package/src/recipe_json.js +80 -0
  133. package/src/server.js +279 -0
  134. package/src/transform.js +403 -0
  135. package/src/transform_error.js +10 -0
  136. package/src/wasm.js +21 -0
  137. package/wasm/index.js +732 -0
  138. package/wasm/package.json +1 -0
  139. package/wasm/tranfi_core.js +0 -0
  140. package/wasm/transform.js +1156 -0
  141. package/wasm/worker.js +786 -0
  142. package/dist/bundle.js +0 -1
  143. package/index.html +0 -18
  144. package/src/app.css +0 -169
  145. package/src/app.js +0 -203
  146. package/src/app.vue +0 -250
  147. package/src/bulma-input.vue +0 -110
  148. package/src/common-inputs.js +0 -28
  149. package/src/main.js +0 -20
  150. package/src/transforms.js +0 -166
  151. package/webpack.config.js +0 -108
@@ -0,0 +1,1956 @@
1
+ /*
2
+ * op_group_agg.c — Group by + aggregate (sum/avg/count/min/max).
3
+ *
4
+ * Config: {"group_by": ["city"], "aggs": [{"column": "sales", "func": "sum", "name": "total"}]}
5
+ * Maintains one accumulator per group, emits on flush.
6
+ */
7
+
8
+ #include "internal.h"
9
+ #include "spill.h"
10
+ #include "cJSON.h"
11
+ #include "date_utils.h"
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+ #include <stdio.h>
15
+ #include <stdint.h>
16
+ #include <errno.h>
17
+ #include <unistd.h>
18
+ #include <ctype.h>
19
+
20
+ #define GROUP_AGG_DEFAULT_RUN_ROWS 8192
21
+ #define GROUP_AGG_DEFAULT_OUTPUT_ROWS 1024
22
+ #define GROUP_AGG_MIN_RUN_ROWS 16
23
+
24
+ typedef enum {
25
+ AGG_SUM, AGG_AVG, AGG_COUNT, AGG_MIN, AGG_MAX,
26
+ } agg_func;
27
+
28
+ typedef struct {
29
+ char *column;
30
+ char *name;
31
+ agg_func func;
32
+ } agg_spec;
33
+
34
+ typedef struct {
35
+ double *sums;
36
+ double *mins;
37
+ double *maxs;
38
+ size_t *counts;
39
+ size_t n_aggs;
40
+ } group_accum;
41
+
42
+ typedef struct {
43
+ char **keys; /* serialized lookup keys, indexed by group id */
44
+ group_accum *accums;
45
+ tf_batch *key_batch; /* first-seen group keys with their original types */
46
+ size_t *slots; /* open-addressed hash table: group id + 1, 0 = empty */
47
+ size_t slot_cap;
48
+ size_t count;
49
+ size_t cap;
50
+ size_t key_bytes;
51
+ } group_map;
52
+
53
+ typedef tf_owned_cell_value group_spill_cell;
54
+
55
+ typedef struct {
56
+ uint64_t ordinal;
57
+ uint8_t *nulls;
58
+ group_spill_cell *cells;
59
+ } group_spill_row;
60
+
61
+ typedef struct {
62
+ FILE *file;
63
+ group_spill_row row;
64
+ int has_row;
65
+ int done;
66
+ } group_run_reader;
67
+
68
+ typedef struct {
69
+ char **group_cols;
70
+ size_t n_group_cols;
71
+ size_t max_groups; /* 0 = unlimited */
72
+ size_t max_state_bytes; /* 0 = unlimited */
73
+ agg_spec *aggs;
74
+ size_t n_aggs;
75
+ int sorted;
76
+ int use_spill;
77
+ int sorted_have_current;
78
+ size_t sorted_groups_started;
79
+ char *sorted_key;
80
+ group_accum sorted_accum;
81
+ tf_batch *sorted_key_batch;
82
+ group_map map;
83
+
84
+ char *spill_dir;
85
+ tf_spill_session *spill;
86
+ size_t spill_memory_bytes;
87
+ size_t configured_run_rows;
88
+ size_t run_rows;
89
+ size_t output_batch_rows;
90
+
91
+ int has_schema;
92
+ char **schema_names;
93
+ tf_type *schema_types;
94
+ size_t n_schema_cols;
95
+ int *group_indices;
96
+ int *agg_indices;
97
+
98
+ char **out_schema_names;
99
+ tf_type *out_schema_types;
100
+ size_t n_out_schema_cols;
101
+
102
+ tf_batch *buf;
103
+ uint64_t *buf_ordinals;
104
+ size_t buf_ordinal_cap;
105
+ uint64_t next_ordinal;
106
+
107
+ char **run_paths;
108
+ size_t n_runs;
109
+ size_t cap_runs;
110
+ char **out_run_paths;
111
+ size_t n_out_runs;
112
+ size_t cap_out_runs;
113
+ size_t run_seq;
114
+ size_t out_run_seq;
115
+
116
+ group_run_reader *readers;
117
+ size_t n_readers;
118
+ group_run_reader *out_readers;
119
+ size_t n_out_readers;
120
+
121
+ tf_batch *out_buf;
122
+ uint64_t *out_ordinals;
123
+ size_t out_ordinal_cap;
124
+
125
+ int key_merge_done;
126
+ int output_merge_started;
127
+ int output_merge_done;
128
+
129
+ size_t spilled_bytes;
130
+ size_t spill_runs_created;
131
+ size_t spill_output_batches;
132
+ size_t spill_output_rows;
133
+ size_t spill_distinct_groups;
134
+ size_t spill_key_bytes;
135
+ } group_agg_state;
136
+
137
+ static size_t group_agg_retained_state_bytes(const group_agg_state *st);
138
+
139
+ static int group_agg_write_error(tf_side_channels *side, const char *msg) {
140
+ return tf_side_write_error(side, msg);
141
+ }
142
+
143
+ static int group_agg_limit_error(const group_agg_state *st, tf_side_channels *side) {
144
+ char msg[160];
145
+ snprintf(msg, sizeof(msg),
146
+ "group-agg: max_groups=%zu exceeded while tracking exact groups",
147
+ st->max_groups);
148
+ return group_agg_write_error(side, msg);
149
+ }
150
+
151
+ static int group_agg_check_state_bytes(group_agg_state *st, tf_side_channels *side) {
152
+ if (!st || st->max_state_bytes == 0) return 0;
153
+ size_t retained = group_agg_retained_state_bytes(st);
154
+ if (retained <= st->max_state_bytes) return 0;
155
+ char msg[208];
156
+ snprintf(msg, sizeof(msg),
157
+ "group-agg: max_state_bytes=%zu exceeded while tracking exact groups (%zu bytes retained)",
158
+ st->max_state_bytes, retained);
159
+ if (group_agg_write_error(side, msg) != TF_OK) return -1;
160
+ return -1;
161
+ }
162
+
163
+ static agg_func parse_agg_func(const char *s) {
164
+ if (strcmp(s, "sum") == 0) return AGG_SUM;
165
+ if (strcmp(s, "avg") == 0) return AGG_AVG;
166
+ if (strcmp(s, "count") == 0) return AGG_COUNT;
167
+ if (strcmp(s, "min") == 0) return AGG_MIN;
168
+ if (strcmp(s, "max") == 0) return AGG_MAX;
169
+ return AGG_COUNT;
170
+ }
171
+
172
+ static char *group_agg_default_name(const char *column, const char *func) {
173
+ char *suffix = tf_string_append_suffix_checked("_", func ? func : "count");
174
+ if (!suffix) return NULL;
175
+ char *name = tf_string_append_suffix_checked(column ? column : "", suffix);
176
+ free(suffix);
177
+ return name;
178
+ }
179
+
180
+ static int key_append(char **buf, size_t *buf_cap, size_t *buf_len,
181
+ const char *data, size_t data_len) {
182
+ size_t need = 0;
183
+ size_t min_cap = 0;
184
+ if (tf_size_add(*buf_len, data_len, &need) != TF_OK ||
185
+ tf_size_add(need, 1, &min_cap) != TF_OK) {
186
+ return -1;
187
+ }
188
+ if (min_cap >= *buf_cap) {
189
+ size_t new_cap = 0;
190
+ if (tf_size_grow_pow2(*buf_cap, min_cap, 256, &new_cap) != TF_OK) return -1;
191
+ char *tmp = tf_reallocarray_checked(*buf, new_cap, sizeof(char));
192
+ if (!tmp) return -1;
193
+ *buf = tmp;
194
+ *buf_cap = new_cap;
195
+ }
196
+ memcpy(*buf + *buf_len, data, data_len);
197
+ *buf_len += data_len;
198
+ (*buf)[*buf_len] = '\0';
199
+ return 0;
200
+ }
201
+
202
+ static int key_append_cstr(char **buf, size_t *buf_cap, size_t *buf_len,
203
+ const char *s) {
204
+ return key_append(buf, buf_cap, buf_len, s, strlen(s));
205
+ }
206
+
207
+ static int key_append_size(char **buf, size_t *buf_cap, size_t *buf_len,
208
+ size_t value) {
209
+ char tmp[32];
210
+ int n = snprintf(tmp, sizeof(tmp), "%zu", value);
211
+ if (n < 0 || (size_t)n >= sizeof(tmp)) return -1;
212
+ return key_append(buf, buf_cap, buf_len, tmp, (size_t)n);
213
+ }
214
+
215
+ static char *build_group_key(const tf_batch *b, size_t row,
216
+ int *col_indices, size_t n_keys) {
217
+ size_t buf_cap = 256;
218
+ char *buf = malloc(buf_cap);
219
+ if (!buf) return NULL;
220
+ size_t buf_len = 0;
221
+ buf[0] = '\0';
222
+
223
+ for (size_t k = 0; k < n_keys; k++) {
224
+ int c = col_indices[k];
225
+ char val_buf[64];
226
+ const char *val = "";
227
+ size_t val_len = 0;
228
+
229
+ if (c < 0 || tf_batch_is_null(b, row, c)) {
230
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "N|") != 0) {
231
+ free(buf);
232
+ return NULL;
233
+ }
234
+ continue;
235
+ }
236
+
237
+ switch (b->col_types[c]) {
238
+ case TF_TYPE_STRING:
239
+ val = tf_batch_get_string(b, row, c);
240
+ if (!val) val = "";
241
+ val_len = strlen(val);
242
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "S") != 0 ||
243
+ key_append_size(&buf, &buf_cap, &buf_len, val_len) != 0 ||
244
+ key_append_cstr(&buf, &buf_cap, &buf_len, ":") != 0 ||
245
+ key_append(&buf, &buf_cap, &buf_len, val, val_len) != 0 ||
246
+ key_append_cstr(&buf, &buf_cap, &buf_len, "|") != 0) {
247
+ free(buf);
248
+ return NULL;
249
+ }
250
+ continue;
251
+ case TF_TYPE_INT64: {
252
+ int n = snprintf(val_buf, sizeof(val_buf), "%lld",
253
+ (long long)tf_batch_get_int64(b, row, c));
254
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
255
+ val = val_buf;
256
+ val_len = (size_t)n;
257
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "I") != 0) { free(buf); return NULL; }
258
+ break;
259
+ }
260
+ case TF_TYPE_FLOAT64: {
261
+ int n = snprintf(val_buf, sizeof(val_buf), "%.17g",
262
+ tf_batch_get_float64(b, row, c));
263
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
264
+ val = val_buf;
265
+ val_len = (size_t)n;
266
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "F") != 0) { free(buf); return NULL; }
267
+ break;
268
+ }
269
+ case TF_TYPE_BOOL:
270
+ val = tf_batch_get_bool(b, row, c) ? "1" : "0";
271
+ val_len = 1;
272
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "B") != 0) { free(buf); return NULL; }
273
+ break;
274
+ case TF_TYPE_DATE: {
275
+ int n = snprintf(val_buf, sizeof(val_buf), "%d",
276
+ (int)tf_batch_get_date(b, row, c));
277
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
278
+ val = val_buf;
279
+ val_len = (size_t)n;
280
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "D") != 0) { free(buf); return NULL; }
281
+ break;
282
+ }
283
+ case TF_TYPE_TIMESTAMP: {
284
+ int n = snprintf(val_buf, sizeof(val_buf), "%lld",
285
+ (long long)tf_batch_get_timestamp(b, row, c));
286
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
287
+ val = val_buf;
288
+ val_len = (size_t)n;
289
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "T") != 0) { free(buf); return NULL; }
290
+ break;
291
+ }
292
+ default:
293
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "N|") != 0) {
294
+ free(buf);
295
+ return NULL;
296
+ }
297
+ continue;
298
+ }
299
+
300
+ if (key_append(&buf, &buf_cap, &buf_len, val, val_len) != 0 ||
301
+ key_append_cstr(&buf, &buf_cap, &buf_len, "|") != 0) {
302
+ free(buf);
303
+ return NULL;
304
+ }
305
+ }
306
+
307
+ return buf;
308
+ }
309
+
310
+ static size_t group_hash(const char *key) {
311
+ uint64_t h = 1469598103934665603ULL;
312
+ for (const unsigned char *p = (const unsigned char *)key; *p; p++) {
313
+ h ^= (uint64_t)*p;
314
+ h *= 1099511628211ULL;
315
+ }
316
+ return (size_t)h;
317
+ }
318
+
319
+ static int group_accum_init(group_accum *a, size_t n_aggs) {
320
+ memset(a, 0, sizeof(*a));
321
+ double *sums = tf_callocarray_checked(n_aggs, sizeof(double));
322
+ double *mins = tf_mallocarray_checked(n_aggs, sizeof(double));
323
+ double *maxs = tf_mallocarray_checked(n_aggs, sizeof(double));
324
+ size_t *counts = tf_callocarray_checked(n_aggs, sizeof(size_t));
325
+ if (!sums || !mins || !maxs || !counts) {
326
+ free(sums);
327
+ free(mins);
328
+ free(maxs);
329
+ free(counts);
330
+ return -1;
331
+ }
332
+ a->n_aggs = n_aggs;
333
+ a->sums = sums;
334
+ a->mins = mins;
335
+ a->maxs = maxs;
336
+ a->counts = counts;
337
+ for (size_t i = 0; i < n_aggs; i++) { a->mins[i] = 1e308; a->maxs[i] = -1e308; }
338
+ return 0;
339
+ }
340
+
341
+ static void group_accum_free(group_accum *a) {
342
+ free(a->sums);
343
+ free(a->mins);
344
+ free(a->maxs);
345
+ free(a->counts);
346
+ }
347
+
348
+ static int group_map_init(group_map *map) {
349
+ memset(map, 0, sizeof(*map));
350
+ map->cap = 64;
351
+ map->slot_cap = 256;
352
+ map->keys = tf_callocarray_checked(map->cap, sizeof(char *));
353
+ map->accums = tf_callocarray_checked(map->cap, sizeof(group_accum));
354
+ map->slots = tf_callocarray_checked(map->slot_cap, sizeof(size_t));
355
+ if (!map->keys || !map->accums || !map->slots) {
356
+ free(map->keys);
357
+ free(map->accums);
358
+ free(map->slots);
359
+ memset(map, 0, sizeof(*map));
360
+ return -1;
361
+ }
362
+ return 0;
363
+ }
364
+
365
+ static void group_map_free(group_map *map) {
366
+ for (size_t i = 0; i < map->count; i++) {
367
+ free(map->keys[i]);
368
+ group_accum_free(&map->accums[i]);
369
+ }
370
+ free(map->keys);
371
+ free(map->accums);
372
+ free(map->slots);
373
+ if (map->key_batch) tf_batch_free(map->key_batch);
374
+ }
375
+
376
+ static size_t group_map_find_slot(const group_map *map, const char *key, int *found) {
377
+ size_t idx = group_hash(key) % map->slot_cap;
378
+ while (map->slots[idx] != 0) {
379
+ size_t group_idx = map->slots[idx] - 1;
380
+ if (strcmp(map->keys[group_idx], key) == 0) {
381
+ *found = 1;
382
+ return idx;
383
+ }
384
+ idx = (idx + 1) % map->slot_cap;
385
+ }
386
+ *found = 0;
387
+ return idx;
388
+ }
389
+
390
+ static int group_map_rehash(group_map *map, size_t new_slot_cap) {
391
+ size_t *new_slots = tf_callocarray_checked(new_slot_cap, sizeof(size_t));
392
+ if (!new_slots) return -1;
393
+
394
+ for (size_t group_idx = 0; group_idx < map->count; group_idx++) {
395
+ size_t idx = group_hash(map->keys[group_idx]) % new_slot_cap;
396
+ while (new_slots[idx] != 0) idx = (idx + 1) % new_slot_cap;
397
+ new_slots[idx] = group_idx + 1;
398
+ }
399
+
400
+ free(map->slots);
401
+ map->slots = new_slots;
402
+ map->slot_cap = new_slot_cap;
403
+ return 0;
404
+ }
405
+
406
+ static int group_map_ensure_group_capacity(group_map *map, size_t min_groups) {
407
+ if (min_groups <= map->cap) return 0;
408
+ size_t old_cap = map->cap;
409
+ size_t new_cap = 0;
410
+ if (tf_size_grow_pow2(map->cap, min_groups, 64, &new_cap) != TF_OK) return -1;
411
+
412
+ char **new_keys = tf_reallocarray_checked(map->keys, new_cap, sizeof(char *));
413
+ if (!new_keys) return -1;
414
+ map->keys = new_keys;
415
+ size_t zero_count = new_cap - old_cap;
416
+ size_t zero_bytes = 0;
417
+ if (tf_size_mul(zero_count, sizeof(char *), &zero_bytes) != TF_OK) return -1;
418
+ memset(map->keys + old_cap, 0, zero_bytes);
419
+
420
+ group_accum *new_accums = tf_reallocarray_checked(map->accums, new_cap, sizeof(group_accum));
421
+ if (!new_accums) return -1;
422
+ map->accums = new_accums;
423
+ if (tf_size_mul(zero_count, sizeof(group_accum), &zero_bytes) != TF_OK) return -1;
424
+ memset(map->accums + old_cap, 0, zero_bytes);
425
+ map->cap = new_cap;
426
+ return 0;
427
+ }
428
+
429
+ static int ensure_key_batch(group_agg_state *st, const tf_batch *in, int *group_indices) {
430
+ if (st->map.key_batch) return 0;
431
+ st->map.key_batch = tf_batch_create(st->n_group_cols, st->map.cap ? st->map.cap : 64);
432
+ if (!st->map.key_batch) return -1;
433
+ for (size_t k = 0; k < st->n_group_cols; k++) {
434
+ int ci = group_indices[k];
435
+ tf_type type = (ci >= 0) ? in->col_types[ci] : TF_TYPE_STRING;
436
+ const char *name = st->group_cols[k] ? st->group_cols[k] : "?";
437
+ if (tf_batch_set_schema(st->map.key_batch, k, name, type) != TF_OK) return -1;
438
+ }
439
+ return 0;
440
+ }
441
+
442
+ static int copy_group_key_values(tf_batch *keys, size_t dst_row,
443
+ const tf_batch *in, size_t src_row,
444
+ int *group_indices, size_t n_group_cols) {
445
+ for (size_t k = 0; k < n_group_cols; k++) {
446
+ int ci = group_indices[k];
447
+ if (ci < 0) {
448
+ if (tf_batch_set_null(keys, dst_row, k) != TF_OK) return TF_ERROR;
449
+ continue;
450
+ }
451
+ if (tf_batch_copy_cell(keys, dst_row, k, in, src_row, (size_t)ci) != TF_OK)
452
+ return TF_ERROR;
453
+ }
454
+ return TF_OK;
455
+ }
456
+
457
+ static int ensure_sorted_key_batch(group_agg_state *st, const tf_batch *in, int *group_indices) {
458
+ if (st->sorted_key_batch) return 0;
459
+ st->sorted_key_batch = tf_batch_create(st->n_group_cols, 1);
460
+ if (!st->sorted_key_batch) return -1;
461
+ for (size_t k = 0; k < st->n_group_cols; k++) {
462
+ int ci = group_indices[k];
463
+ tf_type type = (ci >= 0) ? in->col_types[ci] : TF_TYPE_STRING;
464
+ const char *name = st->group_cols[k] ? st->group_cols[k] : "?";
465
+ if (tf_batch_set_schema(st->sorted_key_batch, k, name, type) != TF_OK) return -1;
466
+ }
467
+ return 0;
468
+ }
469
+
470
+ static int agg_counts_rows(const agg_spec *agg) {
471
+ return agg->func == AGG_COUNT &&
472
+ (!agg->column || agg->column[0] == '\0' || strcmp(agg->column, "*") == 0);
473
+ }
474
+
475
+ static int group_agg_parse_numeric_string(const char *s, double *out) {
476
+ if (!s) return 0;
477
+ while (*s && isspace((unsigned char)*s)) s++;
478
+ if (!*s) return 0;
479
+ errno = 0;
480
+ char *end = NULL;
481
+ double v = strtod(s, &end);
482
+ if (end == s || errno == ERANGE) return 0;
483
+ while (*end && isspace((unsigned char)*end)) end++;
484
+ if (*end != '\0') return 0;
485
+ *out = v;
486
+ return 1;
487
+ }
488
+
489
+ static void group_agg_numeric_type_error(const group_agg_state *st, size_t agg_idx) {
490
+ char msg[256];
491
+ snprintf(msg, sizeof(msg), "group-agg: aggregate column '%s' must be numeric",
492
+ st->aggs[agg_idx].column ? st->aggs[agg_idx].column : "");
493
+ tf_set_last_error(msg);
494
+ }
495
+
496
+ static int group_agg_batch_numeric_value(const group_agg_state *st, size_t agg_idx,
497
+ const tf_batch *in, size_t row, size_t col,
498
+ double *out) {
499
+ switch (in->col_types[col]) {
500
+ case TF_TYPE_INT64:
501
+ *out = (double)tf_batch_get_int64(in, row, col);
502
+ return 1;
503
+ case TF_TYPE_FLOAT64:
504
+ *out = tf_batch_get_float64(in, row, col);
505
+ return 1;
506
+ case TF_TYPE_STRING:
507
+ if (group_agg_parse_numeric_string(tf_batch_get_string(in, row, col), out)) return 1;
508
+ group_agg_numeric_type_error(st, agg_idx);
509
+ return -1;
510
+ default:
511
+ group_agg_numeric_type_error(st, agg_idx);
512
+ return -1;
513
+ }
514
+ }
515
+
516
+ static int group_agg_spill_numeric_value(const group_agg_state *st, size_t agg_idx,
517
+ const group_spill_row *row,
518
+ size_t col, double *out) {
519
+ switch (st->schema_types[col]) {
520
+ case TF_TYPE_INT64:
521
+ *out = (double)row->cells[col].i64;
522
+ return 1;
523
+ case TF_TYPE_FLOAT64:
524
+ *out = row->cells[col].f64;
525
+ return 1;
526
+ case TF_TYPE_STRING:
527
+ if (group_agg_parse_numeric_string(row->cells[col].str, out)) return 1;
528
+ group_agg_numeric_type_error(st, agg_idx);
529
+ return -1;
530
+ default:
531
+ group_agg_numeric_type_error(st, agg_idx);
532
+ return -1;
533
+ }
534
+ }
535
+
536
+ static int group_accum_add_row(group_accum *a, const group_agg_state *st,
537
+ const tf_batch *in, size_t row, int *agg_indices) {
538
+ for (size_t k = 0; k < st->n_aggs; k++) {
539
+ int ci = agg_indices[k];
540
+ if (st->aggs[k].func == AGG_COUNT) {
541
+ if (agg_counts_rows(&st->aggs[k]) || (ci >= 0 && !tf_batch_is_null(in, row, ci))) {
542
+ a->counts[k]++;
543
+ }
544
+ continue;
545
+ }
546
+ if (ci < 0 || tf_batch_is_null(in, row, ci)) continue;
547
+ double v = 0.0;
548
+ int numeric_rc = group_agg_batch_numeric_value(st, k, in, row, (size_t)ci, &v);
549
+ if (numeric_rc < 0) return TF_ERROR;
550
+ a->sums[k] += v;
551
+ if (v < a->mins[k]) a->mins[k] = v;
552
+ if (v > a->maxs[k]) a->maxs[k] = v;
553
+ a->counts[k]++;
554
+ }
555
+ return TF_OK;
556
+ }
557
+
558
+ static int group_agg_set_aggregate_cell(group_agg_state *st, tf_batch *ob,
559
+ size_t row, size_t agg_idx,
560
+ const group_accum *a) {
561
+ size_t col = st->n_group_cols + agg_idx;
562
+ double v = 0;
563
+ int is_null = 0;
564
+ switch (st->aggs[agg_idx].func) {
565
+ case AGG_SUM:
566
+ if (a->counts[agg_idx] == 0) is_null = 1;
567
+ else v = a->sums[agg_idx];
568
+ break;
569
+ case AGG_AVG:
570
+ if (a->counts[agg_idx] == 0) is_null = 1;
571
+ else v = a->sums[agg_idx] / a->counts[agg_idx];
572
+ break;
573
+ case AGG_COUNT:
574
+ v = (double)a->counts[agg_idx];
575
+ break;
576
+ case AGG_MIN:
577
+ if (a->counts[agg_idx] == 0) is_null = 1;
578
+ else v = a->mins[agg_idx];
579
+ break;
580
+ case AGG_MAX:
581
+ if (a->counts[agg_idx] == 0) is_null = 1;
582
+ else v = a->maxs[agg_idx];
583
+ break;
584
+ }
585
+ return is_null ? tf_batch_set_null(ob, row, col)
586
+ : tf_batch_set_float64(ob, row, col, v);
587
+ }
588
+
589
+ static int group_agg_set_output_schema(group_agg_state *st, tf_batch *ob,
590
+ const tf_batch *key_batch) {
591
+ for (size_t k = 0; k < st->n_group_cols; k++) {
592
+ tf_type type = key_batch ? key_batch->col_types[k] : TF_TYPE_STRING;
593
+ if (tf_batch_set_schema(ob, k, st->group_cols[k], type) != TF_OK) return -1;
594
+ }
595
+ for (size_t k = 0; k < st->n_aggs; k++) {
596
+ if (tf_batch_set_schema(ob, st->n_group_cols + k, st->aggs[k].name, TF_TYPE_FLOAT64) != TF_OK) return -1;
597
+ }
598
+ return 0;
599
+ }
600
+
601
+ static int group_agg_emit_row(group_agg_state *st, tf_batch *ob, size_t out_row,
602
+ const tf_batch *key_batch, size_t key_row,
603
+ const group_accum *a) {
604
+ if (tf_batch_ensure_capacity(ob, out_row + 1) != TF_OK) return -1;
605
+ if (key_batch && tf_batch_copy_row(ob, out_row, key_batch, key_row) != TF_OK) return -1;
606
+
607
+ for (size_t k = 0; k < st->n_aggs; k++) {
608
+ if (group_agg_set_aggregate_cell(st, ob, out_row, k, a) != TF_OK) return -1;
609
+ }
610
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) return -1;
611
+ return 0;
612
+ }
613
+
614
+ static int ensure_ordinals(uint64_t **ord, size_t *cap, size_t need) {
615
+ if (*cap >= need) return TF_OK;
616
+ size_t new_cap = 0;
617
+ if (tf_size_grow_pow2(*cap, need, 16, &new_cap) != TF_OK) return TF_ERROR;
618
+ uint64_t *tmp = tf_reallocarray_checked(*ord, new_cap, sizeof(uint64_t));
619
+ if (!tmp) return TF_ERROR;
620
+ *ord = tmp;
621
+ *cap = new_cap;
622
+ return TF_OK;
623
+ }
624
+
625
+ static tf_batch *group_agg_create_input_buffer(const group_agg_state *st, size_t capacity) {
626
+ tf_batch *b = tf_batch_create(st->n_schema_cols, capacity ? capacity : 16);
627
+ if (!b) return NULL;
628
+ for (size_t c = 0; c < st->n_schema_cols; c++) {
629
+ if (tf_batch_set_schema(b, c, st->schema_names[c], st->schema_types[c]) != TF_OK) {
630
+ tf_batch_free(b);
631
+ return NULL;
632
+ }
633
+ }
634
+ return b;
635
+ }
636
+
637
+ static tf_batch *group_agg_create_output_buffer(const group_agg_state *st, size_t capacity) {
638
+ tf_batch *b = tf_batch_create(st->n_out_schema_cols, capacity ? capacity : 16);
639
+ if (!b) return NULL;
640
+ for (size_t c = 0; c < st->n_out_schema_cols; c++) {
641
+ if (tf_batch_set_schema(b, c, st->out_schema_names[c], st->out_schema_types[c]) != TF_OK) {
642
+ tf_batch_free(b);
643
+ return NULL;
644
+ }
645
+ }
646
+ return b;
647
+ }
648
+
649
+ static tf_batch *group_agg_create_key_batch(const group_agg_state *st) {
650
+ tf_batch *b = tf_batch_create(st->n_group_cols, 1);
651
+ if (!b) return NULL;
652
+ for (size_t c = 0; c < st->n_group_cols; c++) {
653
+ if (tf_batch_set_schema(b, c, st->out_schema_names[c], st->out_schema_types[c]) != TF_OK) {
654
+ tf_batch_free(b);
655
+ return NULL;
656
+ }
657
+ }
658
+ return b;
659
+ }
660
+
661
+ static size_t group_agg_estimated_spill_row_bytes(const group_agg_state *st) {
662
+ size_t bytes = 48;
663
+ for (size_t c = 0; c < st->n_schema_cols; c++) {
664
+ bytes += 1;
665
+ switch (st->schema_types[c]) {
666
+ case TF_TYPE_BOOL: bytes += 1; break;
667
+ case TF_TYPE_INT64: bytes += sizeof(int64_t); break;
668
+ case TF_TYPE_FLOAT64: bytes += sizeof(double); break;
669
+ case TF_TYPE_STRING: bytes += sizeof(char *) + 64; break;
670
+ case TF_TYPE_DATE: bytes += sizeof(int32_t); break;
671
+ case TF_TYPE_TIMESTAMP: bytes += sizeof(int64_t); break;
672
+ default: break;
673
+ }
674
+ }
675
+ for (size_t c = 0; c < st->n_out_schema_cols; c++) {
676
+ bytes += 1;
677
+ switch (st->out_schema_types[c]) {
678
+ case TF_TYPE_BOOL: bytes += 1; break;
679
+ case TF_TYPE_INT64: bytes += sizeof(int64_t); break;
680
+ case TF_TYPE_FLOAT64: bytes += sizeof(double); break;
681
+ case TF_TYPE_STRING: bytes += sizeof(char *) + 64; break;
682
+ case TF_TYPE_DATE: bytes += sizeof(int32_t); break;
683
+ case TF_TYPE_TIMESTAMP: bytes += sizeof(int64_t); break;
684
+ default: break;
685
+ }
686
+ }
687
+ return bytes < 64 ? 64 : bytes;
688
+ }
689
+
690
+ static int group_agg_init_spill_schema(group_agg_state *st, const tf_batch *in) {
691
+ if (st->has_schema) return TF_OK;
692
+
693
+ st->n_schema_cols = in->n_cols;
694
+ st->schema_names = tf_callocarray_checked(in->n_cols ? in->n_cols : 1, sizeof(char *));
695
+ st->schema_types = tf_callocarray_checked(in->n_cols ? in->n_cols : 1, sizeof(tf_type));
696
+ if (!st->schema_names || !st->schema_types) return TF_ERROR;
697
+ for (size_t c = 0; c < in->n_cols; c++) {
698
+ st->schema_names[c] = tf_strdup_checked(in->col_names[c] ? in->col_names[c] : "");
699
+ if (!st->schema_names[c]) return TF_ERROR;
700
+ st->schema_types[c] = in->col_types[c];
701
+ }
702
+
703
+ st->group_indices = tf_callocarray_checked(st->n_group_cols ? st->n_group_cols : 1, sizeof(int));
704
+ st->agg_indices = tf_callocarray_checked(st->n_aggs ? st->n_aggs : 1, sizeof(int));
705
+ if (!st->group_indices || !st->agg_indices) return TF_ERROR;
706
+ for (size_t k = 0; k < st->n_group_cols; k++) st->group_indices[k] = tf_batch_col_index(in, st->group_cols[k]);
707
+ for (size_t k = 0; k < st->n_aggs; k++) st->agg_indices[k] = tf_batch_col_index(in, st->aggs[k].column);
708
+
709
+ st->n_out_schema_cols = st->n_group_cols + st->n_aggs;
710
+ st->out_schema_names = tf_callocarray_checked(st->n_out_schema_cols ? st->n_out_schema_cols : 1, sizeof(char *));
711
+ st->out_schema_types = tf_callocarray_checked(st->n_out_schema_cols ? st->n_out_schema_cols : 1, sizeof(tf_type));
712
+ if (!st->out_schema_names || !st->out_schema_types) return TF_ERROR;
713
+ for (size_t k = 0; k < st->n_group_cols; k++) {
714
+ int ci = st->group_indices[k];
715
+ st->out_schema_names[k] = tf_strdup_checked(st->group_cols[k] ? st->group_cols[k] : "?");
716
+ if (!st->out_schema_names[k]) return TF_ERROR;
717
+ st->out_schema_types[k] = ci >= 0 ? in->col_types[ci] : TF_TYPE_STRING;
718
+ }
719
+ for (size_t k = 0; k < st->n_aggs; k++) {
720
+ size_t c = st->n_group_cols + k;
721
+ st->out_schema_names[c] = tf_strdup_checked(st->aggs[k].name ? st->aggs[k].name : "agg");
722
+ if (!st->out_schema_names[c]) return TF_ERROR;
723
+ st->out_schema_types[c] = TF_TYPE_FLOAT64;
724
+ }
725
+
726
+ if (st->configured_run_rows > 0) {
727
+ st->run_rows = st->configured_run_rows;
728
+ } else if (st->spill_memory_bytes > 0) {
729
+ size_t row_bytes = group_agg_estimated_spill_row_bytes(st);
730
+ st->run_rows = st->spill_memory_bytes / (row_bytes * 4);
731
+ if (st->run_rows < GROUP_AGG_MIN_RUN_ROWS) st->run_rows = GROUP_AGG_MIN_RUN_ROWS;
732
+ } else {
733
+ st->run_rows = GROUP_AGG_DEFAULT_RUN_ROWS;
734
+ }
735
+ if (st->run_rows == 0 || st->run_rows == SIZE_MAX) st->run_rows = GROUP_AGG_DEFAULT_RUN_ROWS;
736
+
737
+ st->buf = group_agg_create_input_buffer(st, st->run_rows);
738
+ st->out_buf = group_agg_create_output_buffer(st, st->run_rows);
739
+ if (!st->buf || !st->out_buf) return TF_ERROR;
740
+ st->has_schema = 1;
741
+ return TF_OK;
742
+ }
743
+
744
+ static int compare_batch_group_key_rows(const group_agg_state *st, const tf_batch *batch, size_t ra, size_t rb) {
745
+ for (size_t k = 0; k < st->n_group_cols; k++) {
746
+ int ci = st->group_indices[k];
747
+ if (ci < 0) continue;
748
+ size_t c = (size_t)ci;
749
+ int null_a = tf_batch_is_null(batch, ra, c);
750
+ int null_b = tf_batch_is_null(batch, rb, c);
751
+ if (null_a && null_b) continue;
752
+ if (null_a) return 1;
753
+ if (null_b) return -1;
754
+ int cmp = 0;
755
+ switch (batch->col_types[c]) {
756
+ case TF_TYPE_BOOL: cmp = (int)tf_batch_get_bool(batch, ra, c) - (int)tf_batch_get_bool(batch, rb, c); break;
757
+ case TF_TYPE_INT64: {
758
+ int64_t a = tf_batch_get_int64(batch, ra, c), b = tf_batch_get_int64(batch, rb, c);
759
+ cmp = (a > b) - (a < b);
760
+ break;
761
+ }
762
+ case TF_TYPE_FLOAT64: {
763
+ double a = tf_batch_get_float64(batch, ra, c), b = tf_batch_get_float64(batch, rb, c);
764
+ cmp = (a > b) - (a < b);
765
+ break;
766
+ }
767
+ case TF_TYPE_STRING: cmp = strcmp(tf_batch_get_string(batch, ra, c), tf_batch_get_string(batch, rb, c)); break;
768
+ case TF_TYPE_DATE: {
769
+ int32_t a = tf_batch_get_date(batch, ra, c), b = tf_batch_get_date(batch, rb, c);
770
+ cmp = (a > b) - (a < b);
771
+ break;
772
+ }
773
+ case TF_TYPE_TIMESTAMP: {
774
+ int64_t a = tf_batch_get_timestamp(batch, ra, c), b = tf_batch_get_timestamp(batch, rb, c);
775
+ cmp = (a > b) - (a < b);
776
+ break;
777
+ }
778
+ default: break;
779
+ }
780
+ if (cmp != 0) return cmp;
781
+ }
782
+ uint64_t oa = st->buf_ordinals[ra], ob = st->buf_ordinals[rb];
783
+ return (oa > ob) - (oa < ob);
784
+ }
785
+
786
+ typedef struct {
787
+ const group_agg_state *st;
788
+ int by_ordinal;
789
+ } group_agg_sort_ctx;
790
+
791
+ static int group_agg_compare_indices(const void *ctx, size_t ra, size_t rb) {
792
+ const group_agg_sort_ctx *sort = (const group_agg_sort_ctx *)ctx;
793
+ if (!sort->by_ordinal) return compare_batch_group_key_rows(sort->st, sort->st->buf, ra, rb);
794
+ uint64_t oa = sort->st->out_ordinals[ra];
795
+ uint64_t ob = sort->st->out_ordinals[rb];
796
+ return (oa > ob) - (oa < ob);
797
+ }
798
+
799
+ static size_t *group_agg_sorted_indices(size_t n, int by_ordinal, const group_agg_state *st) {
800
+ size_t *idx = tf_mallocarray_checked(n ? n : 1, sizeof(size_t));
801
+ if (!idx) return NULL;
802
+ for (size_t i = 0; i < n; i++) idx[i] = i;
803
+ group_agg_sort_ctx ctx = { .st = st, .by_ordinal = by_ordinal };
804
+ tf_sort_indices(idx, n, group_agg_compare_indices, &ctx);
805
+ return idx;
806
+ }
807
+
808
+ static int write_exact(FILE *f, const void *ptr, size_t len) {
809
+ return fwrite(ptr, 1, len, f) == len ? TF_OK : TF_ERROR;
810
+ }
811
+
812
+ static int read_exact(FILE *f, void *ptr, size_t len) {
813
+ return fread(ptr, 1, len, f) == len ? TF_OK : TF_ERROR;
814
+ }
815
+
816
+ static int write_cell(FILE *f, const tf_batch *b, size_t r, size_t c) {
817
+ uint8_t is_null = tf_batch_is_null(b, r, c) ? 1 : 0;
818
+ if (write_exact(f, &is_null, sizeof(is_null)) != TF_OK) return TF_ERROR;
819
+ if (is_null) return TF_OK;
820
+ switch (b->col_types[c]) {
821
+ case TF_TYPE_BOOL: { uint8_t v = tf_batch_get_bool(b, r, c) ? 1 : 0; return write_exact(f, &v, sizeof(v)); }
822
+ case TF_TYPE_INT64: { int64_t v = tf_batch_get_int64(b, r, c); return write_exact(f, &v, sizeof(v)); }
823
+ case TF_TYPE_FLOAT64: { double v = tf_batch_get_float64(b, r, c); return write_exact(f, &v, sizeof(v)); }
824
+ case TF_TYPE_STRING: {
825
+ const char *str = tf_batch_get_string(b, r, c);
826
+ uint64_t len = str ? (uint64_t)strlen(str) : 0;
827
+ if (write_exact(f, &len, sizeof(len)) != TF_OK) return TF_ERROR;
828
+ return len ? write_exact(f, str, (size_t)len) : TF_OK;
829
+ }
830
+ case TF_TYPE_DATE: { int32_t v = tf_batch_get_date(b, r, c); return write_exact(f, &v, sizeof(v)); }
831
+ case TF_TYPE_TIMESTAMP: { int64_t v = tf_batch_get_timestamp(b, r, c); return write_exact(f, &v, sizeof(v)); }
832
+ default: return TF_OK;
833
+ }
834
+ }
835
+
836
+ static int append_path(char ***paths, size_t *n, size_t *cap, char *path) {
837
+ if (*n == *cap) {
838
+ size_t need = 0;
839
+ size_t new_cap = 0;
840
+ if (tf_size_add(*n, 1, &need) != TF_OK ||
841
+ tf_size_grow_pow2(*cap, need, 8, &new_cap) != TF_OK) {
842
+ return TF_ERROR;
843
+ }
844
+ char **tmp = tf_reallocarray_checked(*paths, new_cap, sizeof(char *));
845
+ if (!tmp) return TF_ERROR;
846
+ *paths = tmp;
847
+ *cap = new_cap;
848
+ }
849
+ (*paths)[(*n)++] = path;
850
+ return TF_OK;
851
+ }
852
+
853
+ static int group_agg_write_batch_run(group_agg_state *st, tf_batch *batch, const uint64_t *ordinals,
854
+ size_t *indices, size_t n, int output_run) {
855
+ char *path = NULL;
856
+ FILE *f = tf_spill_open_run_file(st->spill, output_run ? "group-agg-out" : "group-agg-key", &path);
857
+ if (!f) return TF_ERROR;
858
+ for (size_t i = 0; i < n; i++) {
859
+ size_t r = indices[i];
860
+ uint64_t ordinal = ordinals[r];
861
+ if (write_exact(f, &ordinal, sizeof(ordinal)) != TF_OK) goto write_fail;
862
+ for (size_t c = 0; c < batch->n_cols; c++) {
863
+ if (write_cell(f, batch, r, c) != TF_OK) goto write_fail;
864
+ }
865
+ }
866
+ long pos = ftell(f);
867
+ if (pos > 0) st->spilled_bytes += (size_t)pos;
868
+ if (fclose(f) != 0) {
869
+ tf_set_last_error("group-agg spill: failed closing run file");
870
+ remove(path);
871
+ free(path);
872
+ return TF_ERROR;
873
+ }
874
+ if (output_run) {
875
+ if (append_path(&st->out_run_paths, &st->n_out_runs, &st->cap_out_runs, path) != TF_OK) {
876
+ remove(path); free(path); return TF_ERROR;
877
+ }
878
+ } else {
879
+ if (append_path(&st->run_paths, &st->n_runs, &st->cap_runs, path) != TF_OK) {
880
+ remove(path); free(path); return TF_ERROR;
881
+ }
882
+ }
883
+ st->spill_runs_created++;
884
+ return TF_OK;
885
+
886
+ write_fail:
887
+ tf_set_last_error("group-agg spill: failed writing run file");
888
+ fclose(f);
889
+ remove(path);
890
+ free(path);
891
+ return TF_ERROR;
892
+ }
893
+
894
+ static int group_agg_write_key_run(group_agg_state *st) {
895
+ if (!st->buf || st->buf->n_rows == 0) return TF_OK;
896
+ size_t *idx = group_agg_sorted_indices(st->buf->n_rows, 0, st);
897
+ if (!idx) return TF_ERROR;
898
+ int rc = group_agg_write_batch_run(st, st->buf, st->buf_ordinals, idx, st->buf->n_rows, 0);
899
+ free(idx);
900
+ if (rc != TF_OK) return TF_ERROR;
901
+ tf_batch_free(st->buf);
902
+ st->buf = group_agg_create_input_buffer(st, st->run_rows);
903
+ st->buf_ordinal_cap = 0;
904
+ free(st->buf_ordinals);
905
+ st->buf_ordinals = NULL;
906
+ return st->buf ? TF_OK : TF_ERROR;
907
+ }
908
+
909
+ static int group_agg_write_output_run(group_agg_state *st) {
910
+ if (!st->out_buf || st->out_buf->n_rows == 0) return TF_OK;
911
+ size_t *idx = group_agg_sorted_indices(st->out_buf->n_rows, 1, st);
912
+ if (!idx) return TF_ERROR;
913
+ int rc = group_agg_write_batch_run(st, st->out_buf, st->out_ordinals, idx, st->out_buf->n_rows, 1);
914
+ free(idx);
915
+ if (rc != TF_OK) return TF_ERROR;
916
+ tf_batch_free(st->out_buf);
917
+ st->out_buf = group_agg_create_output_buffer(st, st->run_rows);
918
+ st->out_ordinal_cap = 0;
919
+ free(st->out_ordinals);
920
+ st->out_ordinals = NULL;
921
+ return st->out_buf ? TF_OK : TF_ERROR;
922
+ }
923
+
924
+ static void spill_row_clear(group_spill_row *row, const tf_type *types, size_t n_cols) {
925
+ if (!row || !row->cells || !row->nulls) return;
926
+ for (size_t c = 0; c < n_cols; c++) {
927
+ if (!row->nulls[c] && types[c] == TF_TYPE_STRING) free(row->cells[c].str);
928
+ row->cells[c].str = NULL;
929
+ row->nulls[c] = 1;
930
+ }
931
+ }
932
+
933
+ static int spill_row_init(group_spill_row *row, size_t n_cols) {
934
+ row->ordinal = 0;
935
+ row->nulls = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(uint8_t));
936
+ row->cells = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(group_spill_cell));
937
+ if (!row->nulls || !row->cells) {
938
+ free(row->nulls);
939
+ free(row->cells);
940
+ row->nulls = NULL;
941
+ row->cells = NULL;
942
+ return TF_ERROR;
943
+ }
944
+ for (size_t c = 0; c < n_cols; c++) row->nulls[c] = 1;
945
+ return TF_OK;
946
+ }
947
+
948
+ static void spill_row_free(group_spill_row *row, const tf_type *types, size_t n_cols) {
949
+ if (!row) return;
950
+ spill_row_clear(row, types, n_cols);
951
+ free(row->nulls);
952
+ free(row->cells);
953
+ row->nulls = NULL;
954
+ row->cells = NULL;
955
+ }
956
+
957
+ static int read_cell_value(FILE *f, group_spill_row *row, tf_type type, size_t c) {
958
+ switch (type) {
959
+ case TF_TYPE_BOOL: { uint8_t v = 0; if (read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].b = v; return TF_OK; }
960
+ case TF_TYPE_INT64: { int64_t v = 0; if (read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].i64 = v; return TF_OK; }
961
+ case TF_TYPE_FLOAT64: { double v = 0; if (read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].f64 = v; return TF_OK; }
962
+ case TF_TYPE_STRING: {
963
+ uint64_t len = 0;
964
+ if (read_exact(f, &len, sizeof(len)) != TF_OK) return TF_ERROR;
965
+ if (len > (uint64_t)SIZE_MAX - 1) return TF_ERROR;
966
+ char *str = malloc((size_t)len + 1);
967
+ if (!str) return TF_ERROR;
968
+ if (len && read_exact(f, str, (size_t)len) != TF_OK) { free(str); return TF_ERROR; }
969
+ str[len] = '\0';
970
+ row->cells[c].str = str;
971
+ return TF_OK;
972
+ }
973
+ case TF_TYPE_DATE: { int32_t v = 0; if (read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].date = v; return TF_OK; }
974
+ case TF_TYPE_TIMESTAMP: { int64_t v = 0; if (read_exact(f, &v, sizeof(v)) != TF_OK) return TF_ERROR; row->cells[c].i64 = v; return TF_OK; }
975
+ default: return TF_OK;
976
+ }
977
+ }
978
+
979
+ static const tf_type *group_agg_reader_types(const group_agg_state *st, int output_reader) {
980
+ return output_reader ? st->out_schema_types : st->schema_types;
981
+ }
982
+
983
+ static size_t group_agg_reader_cols(const group_agg_state *st, int output_reader) {
984
+ return output_reader ? st->n_out_schema_cols : st->n_schema_cols;
985
+ }
986
+
987
+ static int group_agg_reader_advance(group_agg_state *st, group_run_reader *reader, int output_reader) {
988
+ if (!reader || !reader->file || reader->done) return 0;
989
+ const tf_type *types = group_agg_reader_types(st, output_reader);
990
+ size_t n_cols = group_agg_reader_cols(st, output_reader);
991
+ spill_row_clear(&reader->row, types, n_cols);
992
+ if (fread(&reader->row.ordinal, sizeof(reader->row.ordinal), 1, reader->file) != 1) {
993
+ if (feof(reader->file)) {
994
+ reader->done = 1;
995
+ reader->has_row = 0;
996
+ return 0;
997
+ }
998
+ tf_set_last_error("group-agg spill: failed reading run file");
999
+ return -1;
1000
+ }
1001
+ for (size_t c = 0; c < n_cols; c++) {
1002
+ uint8_t is_null = 1;
1003
+ if (read_exact(reader->file, &is_null, sizeof(is_null)) != TF_OK) {
1004
+ tf_set_last_error("group-agg spill: corrupt run file");
1005
+ return -1;
1006
+ }
1007
+ reader->row.nulls[c] = is_null ? 1 : 0;
1008
+ if (!reader->row.nulls[c] && read_cell_value(reader->file, &reader->row, types[c], c) != TF_OK) {
1009
+ tf_set_last_error("group-agg spill: corrupt run file");
1010
+ return -1;
1011
+ }
1012
+ }
1013
+ reader->has_row = 1;
1014
+ return 1;
1015
+ }
1016
+
1017
+ static int compare_spill_group_key_rows(const group_agg_state *st, const group_spill_row *a, const group_spill_row *b) {
1018
+ for (size_t k = 0; k < st->n_group_cols; k++) {
1019
+ int ci = st->group_indices[k];
1020
+ if (ci < 0) continue;
1021
+ size_t c = (size_t)ci;
1022
+ int null_a = a->nulls[c] != 0;
1023
+ int null_b = b->nulls[c] != 0;
1024
+ if (null_a && null_b) continue;
1025
+ if (null_a) return 1;
1026
+ if (null_b) return -1;
1027
+ int cmp = 0;
1028
+ switch (st->schema_types[c]) {
1029
+ case TF_TYPE_BOOL: cmp = (int)a->cells[c].b - (int)b->cells[c].b; break;
1030
+ case TF_TYPE_INT64:
1031
+ case TF_TYPE_TIMESTAMP: cmp = (a->cells[c].i64 > b->cells[c].i64) - (a->cells[c].i64 < b->cells[c].i64); break;
1032
+ case TF_TYPE_FLOAT64: cmp = (a->cells[c].f64 > b->cells[c].f64) - (a->cells[c].f64 < b->cells[c].f64); break;
1033
+ case TF_TYPE_STRING: cmp = strcmp(a->cells[c].str, b->cells[c].str); break;
1034
+ case TF_TYPE_DATE: cmp = (a->cells[c].date > b->cells[c].date) - (a->cells[c].date < b->cells[c].date); break;
1035
+ default: break;
1036
+ }
1037
+ if (cmp != 0) return cmp;
1038
+ }
1039
+ return (a->ordinal > b->ordinal) - (a->ordinal < b->ordinal);
1040
+ }
1041
+
1042
+ static char *build_spill_row_key(const group_agg_state *st, const group_spill_row *row) {
1043
+ size_t buf_cap = 256;
1044
+ char *buf = malloc(buf_cap);
1045
+ if (!buf) return NULL;
1046
+ size_t buf_len = 0;
1047
+ buf[0] = '\0';
1048
+
1049
+ for (size_t k = 0; k < st->n_group_cols; k++) {
1050
+ int ci = st->group_indices[k];
1051
+ char val_buf[64];
1052
+ const char *val = "";
1053
+ size_t val_len = 0;
1054
+
1055
+ if (ci < 0 || row->nulls[(size_t)ci]) {
1056
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "N|") != 0) { free(buf); return NULL; }
1057
+ continue;
1058
+ }
1059
+
1060
+ size_t c = (size_t)ci;
1061
+ switch (st->schema_types[c]) {
1062
+ case TF_TYPE_STRING:
1063
+ val = row->cells[c].str ? row->cells[c].str : "";
1064
+ val_len = strlen(val);
1065
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "S") != 0 ||
1066
+ key_append_size(&buf, &buf_cap, &buf_len, val_len) != 0 ||
1067
+ key_append_cstr(&buf, &buf_cap, &buf_len, ":") != 0 ||
1068
+ key_append(&buf, &buf_cap, &buf_len, val, val_len) != 0 ||
1069
+ key_append_cstr(&buf, &buf_cap, &buf_len, "|") != 0) {
1070
+ free(buf);
1071
+ return NULL;
1072
+ }
1073
+ continue;
1074
+ case TF_TYPE_INT64: {
1075
+ int n = snprintf(val_buf, sizeof(val_buf), "%lld", (long long)row->cells[c].i64);
1076
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
1077
+ val = val_buf; val_len = (size_t)n;
1078
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "I") != 0) { free(buf); return NULL; }
1079
+ break;
1080
+ }
1081
+ case TF_TYPE_FLOAT64: {
1082
+ int n = snprintf(val_buf, sizeof(val_buf), "%.17g", row->cells[c].f64);
1083
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
1084
+ val = val_buf; val_len = (size_t)n;
1085
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "F") != 0) { free(buf); return NULL; }
1086
+ break;
1087
+ }
1088
+ case TF_TYPE_BOOL:
1089
+ val = row->cells[c].b ? "1" : "0"; val_len = 1;
1090
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "B") != 0) { free(buf); return NULL; }
1091
+ break;
1092
+ case TF_TYPE_DATE: {
1093
+ int n = snprintf(val_buf, sizeof(val_buf), "%d", (int)row->cells[c].date);
1094
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
1095
+ val = val_buf; val_len = (size_t)n;
1096
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "D") != 0) { free(buf); return NULL; }
1097
+ break;
1098
+ }
1099
+ case TF_TYPE_TIMESTAMP: {
1100
+ int n = snprintf(val_buf, sizeof(val_buf), "%lld", (long long)row->cells[c].i64);
1101
+ if (n < 0 || (size_t)n >= sizeof(val_buf)) { free(buf); return NULL; }
1102
+ val = val_buf; val_len = (size_t)n;
1103
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "T") != 0) { free(buf); return NULL; }
1104
+ break;
1105
+ }
1106
+ default:
1107
+ if (key_append_cstr(&buf, &buf_cap, &buf_len, "N|") != 0) { free(buf); return NULL; }
1108
+ continue;
1109
+ }
1110
+ if (key_append(&buf, &buf_cap, &buf_len, val, val_len) != 0 ||
1111
+ key_append_cstr(&buf, &buf_cap, &buf_len, "|") != 0) {
1112
+ free(buf);
1113
+ return NULL;
1114
+ }
1115
+ }
1116
+ return buf;
1117
+ }
1118
+
1119
+ static int set_spill_row_cell(tf_batch *out, size_t dst_row, size_t dst_col,
1120
+ const group_spill_row *row, size_t src_col, tf_type type) {
1121
+ return tf_batch_set_owned_cell_value(out, dst_row, dst_col, type,
1122
+ row->nulls[src_col], &row->cells[src_col]);
1123
+ }
1124
+
1125
+ static int spill_row_to_batch(const group_agg_state *st, tf_batch *out,
1126
+ size_t dst_row, const group_spill_row *row,
1127
+ int output_schema) {
1128
+ const tf_type *types = output_schema ? st->out_schema_types : st->schema_types;
1129
+ size_t n_cols = output_schema ? st->n_out_schema_cols : st->n_schema_cols;
1130
+ if (tf_batch_ensure_capacity(out, dst_row + 1) != TF_OK) return TF_ERROR;
1131
+ for (size_t c = 0; c < n_cols; c++) {
1132
+ if (set_spill_row_cell(out, dst_row, c, row, c, types[c]) != TF_OK)
1133
+ return TF_ERROR;
1134
+ }
1135
+ return TF_OK;
1136
+ }
1137
+
1138
+ static void group_agg_close_readers(group_agg_state *st, int output_readers) {
1139
+ group_run_reader **readers = output_readers ? &st->out_readers : &st->readers;
1140
+ size_t *n_readers = output_readers ? &st->n_out_readers : &st->n_readers;
1141
+ if (!*readers) return;
1142
+ const tf_type *types = group_agg_reader_types(st, output_readers);
1143
+ size_t n_cols = group_agg_reader_cols(st, output_readers);
1144
+ for (size_t i = 0; i < *n_readers; i++) {
1145
+ if ((*readers)[i].file) fclose((*readers)[i].file);
1146
+ spill_row_free(&(*readers)[i].row, types, n_cols);
1147
+ }
1148
+ free(*readers);
1149
+ *readers = NULL;
1150
+ *n_readers = 0;
1151
+ }
1152
+
1153
+ static void group_agg_remove_paths(char ***paths, size_t *n, size_t *cap) {
1154
+ for (size_t i = 0; i < *n; i++) {
1155
+ if ((*paths)[i]) {
1156
+ remove((*paths)[i]);
1157
+ free((*paths)[i]);
1158
+ (*paths)[i] = NULL;
1159
+ }
1160
+ }
1161
+ free(*paths);
1162
+ *paths = NULL;
1163
+ *n = 0;
1164
+ *cap = 0;
1165
+ }
1166
+
1167
+ static int group_agg_open_readers(group_agg_state *st, int output_readers) {
1168
+ char **paths = output_readers ? st->out_run_paths : st->run_paths;
1169
+ size_t n_paths = output_readers ? st->n_out_runs : st->n_runs;
1170
+ group_run_reader **readers = output_readers ? &st->out_readers : &st->readers;
1171
+ size_t *n_readers = output_readers ? &st->n_out_readers : &st->n_readers;
1172
+ if (n_paths == 0) return TF_OK;
1173
+ *readers = tf_callocarray_checked(n_paths, sizeof(group_run_reader));
1174
+ if (!*readers) return TF_ERROR;
1175
+ *n_readers = n_paths;
1176
+ size_t n_cols = group_agg_reader_cols(st, output_readers);
1177
+ for (size_t i = 0; i < n_paths; i++) {
1178
+ (*readers)[i].file = fopen(paths[i], "rb");
1179
+ if (!(*readers)[i].file) { tf_set_last_error("group-agg spill: cannot reopen run file"); return TF_ERROR; }
1180
+ if (spill_row_init(&(*readers)[i].row, n_cols) != TF_OK) return TF_ERROR;
1181
+ int rc = group_agg_reader_advance(st, &(*readers)[i], output_readers);
1182
+ if (rc < 0) return TF_ERROR;
1183
+ }
1184
+ return TF_OK;
1185
+ }
1186
+
1187
+ static int group_agg_best_key_reader(const group_agg_state *st) {
1188
+ int best = -1;
1189
+ for (size_t i = 0; i < st->n_readers; i++) {
1190
+ const group_run_reader *r = &st->readers[i];
1191
+ if (!r->has_row || r->done) continue;
1192
+ if (best < 0) { best = (int)i; continue; }
1193
+ int cmp = compare_spill_group_key_rows(st, &r->row, &st->readers[best].row);
1194
+ if (cmp < 0 || (cmp == 0 && i < (size_t)best)) best = (int)i;
1195
+ }
1196
+ return best;
1197
+ }
1198
+
1199
+ static int group_agg_best_ordinal_reader(const group_agg_state *st) {
1200
+ int best = -1;
1201
+ for (size_t i = 0; i < st->n_out_readers; i++) {
1202
+ const group_run_reader *r = &st->out_readers[i];
1203
+ if (!r->has_row || r->done) continue;
1204
+ if (best < 0) { best = (int)i; continue; }
1205
+ uint64_t a = r->row.ordinal;
1206
+ uint64_t b = st->out_readers[best].row.ordinal;
1207
+ if (a < b || (a == b && i < (size_t)best)) best = (int)i;
1208
+ }
1209
+ return best;
1210
+ }
1211
+
1212
+ static int copy_spill_group_key_values(tf_batch *keys, const group_agg_state *st,
1213
+ const group_spill_row *row) {
1214
+ if (tf_batch_ensure_capacity(keys, 1) != TF_OK) return TF_ERROR;
1215
+ for (size_t k = 0; k < st->n_group_cols; k++) {
1216
+ int ci = st->group_indices[k];
1217
+ if (ci < 0) {
1218
+ if (tf_batch_set_null(keys, 0, k) != TF_OK) return TF_ERROR;
1219
+ continue;
1220
+ }
1221
+ size_t c = (size_t)ci;
1222
+ if (set_spill_row_cell(keys, 0, k, row, c, keys->col_types[k]) != TF_OK)
1223
+ return TF_ERROR;
1224
+ }
1225
+ return tf_batch_expose_row(keys, 0);
1226
+ }
1227
+
1228
+ static int group_accum_add_spill_row(group_accum *a, const group_agg_state *st, const group_spill_row *row) {
1229
+ for (size_t k = 0; k < st->n_aggs; k++) {
1230
+ int ci = st->agg_indices[k];
1231
+ if (st->aggs[k].func == AGG_COUNT) {
1232
+ if (agg_counts_rows(&st->aggs[k]) || (ci >= 0 && !row->nulls[(size_t)ci])) {
1233
+ a->counts[k]++;
1234
+ }
1235
+ continue;
1236
+ }
1237
+ if (ci < 0 || row->nulls[(size_t)ci]) continue;
1238
+ size_t c = (size_t)ci;
1239
+ double v = 0.0;
1240
+ int numeric_rc = group_agg_spill_numeric_value(st, k, row, c, &v);
1241
+ if (numeric_rc < 0) return TF_ERROR;
1242
+ a->sums[k] += v;
1243
+ if (v < a->mins[k]) a->mins[k] = v;
1244
+ if (v > a->maxs[k]) a->maxs[k] = v;
1245
+ a->counts[k]++;
1246
+ }
1247
+ return TF_OK;
1248
+ }
1249
+
1250
+ static int group_agg_append_spill_group(group_agg_state *st, const tf_batch *key_batch,
1251
+ const group_accum *accum, uint64_t first_ordinal) {
1252
+ size_t dst = st->out_buf->n_rows;
1253
+ if (ensure_ordinals(&st->out_ordinals, &st->out_ordinal_cap, dst + 1) != TF_OK) return TF_ERROR;
1254
+ if (group_agg_emit_row(st, st->out_buf, dst, key_batch, 0, accum) != 0) return TF_ERROR;
1255
+ st->out_ordinals[dst] = first_ordinal;
1256
+ st->spill_distinct_groups++;
1257
+ if (st->out_buf->n_rows >= st->run_rows) return group_agg_write_output_run(st);
1258
+ return TF_OK;
1259
+ }
1260
+
1261
+ static int group_agg_start_spill_group(group_agg_state *st, const char *key,
1262
+ const group_spill_row *row,
1263
+ char **current_key,
1264
+ tf_batch **current_key_batch,
1265
+ group_accum *accum,
1266
+ uint64_t *first_ordinal) {
1267
+ *current_key = tf_strdup_checked(key);
1268
+ if (!*current_key) return TF_ERROR;
1269
+ *current_key_batch = group_agg_create_key_batch(st);
1270
+ if (!*current_key_batch) {
1271
+ free(*current_key);
1272
+ *current_key = NULL;
1273
+ return TF_ERROR;
1274
+ }
1275
+ if (copy_spill_group_key_values(*current_key_batch, st, row) != TF_OK) {
1276
+ tf_batch_free(*current_key_batch);
1277
+ *current_key_batch = NULL;
1278
+ free(*current_key);
1279
+ *current_key = NULL;
1280
+ return TF_ERROR;
1281
+ }
1282
+ if (group_accum_init(accum, st->n_aggs) != 0) {
1283
+ tf_batch_free(*current_key_batch);
1284
+ *current_key_batch = NULL;
1285
+ free(*current_key);
1286
+ *current_key = NULL;
1287
+ return TF_ERROR;
1288
+ }
1289
+ *first_ordinal = row->ordinal;
1290
+ return TF_OK;
1291
+ }
1292
+
1293
+ static int group_agg_finish_spill_group(group_agg_state *st, char **current_key,
1294
+ tf_batch **current_key_batch,
1295
+ group_accum *accum,
1296
+ uint64_t first_ordinal) {
1297
+ if (!*current_key || !*current_key_batch) return TF_OK;
1298
+ size_t key_bytes_delta = 0;
1299
+ size_t new_spill_key_bytes = 0;
1300
+ int rc = TF_ERROR;
1301
+ if (tf_size_add(strlen(*current_key), 1, &key_bytes_delta) == TF_OK &&
1302
+ tf_size_add(st->spill_key_bytes, key_bytes_delta,
1303
+ &new_spill_key_bytes) == TF_OK) {
1304
+ st->spill_key_bytes = new_spill_key_bytes;
1305
+ rc = group_agg_append_spill_group(st, *current_key_batch, accum, first_ordinal);
1306
+ }
1307
+ group_accum_free(accum);
1308
+ memset(accum, 0, sizeof(*accum));
1309
+ free(*current_key);
1310
+ *current_key = NULL;
1311
+ tf_batch_free(*current_key_batch);
1312
+ *current_key_batch = NULL;
1313
+ return rc;
1314
+ }
1315
+
1316
+ static int group_agg_produce_output_runs(group_agg_state *st) {
1317
+ if (st->key_merge_done) return TF_OK;
1318
+ if (!st->has_schema) {
1319
+ st->key_merge_done = 1;
1320
+ return TF_OK;
1321
+ }
1322
+ if (st->buf && st->buf->n_rows > 0 && group_agg_write_key_run(st) != TF_OK) return TF_ERROR;
1323
+ if (st->buf) { tf_batch_free(st->buf); st->buf = NULL; }
1324
+ free(st->buf_ordinals); st->buf_ordinals = NULL; st->buf_ordinal_cap = 0;
1325
+
1326
+ if (group_agg_open_readers(st, 0) != TF_OK) return TF_ERROR;
1327
+
1328
+ char *current_key = NULL;
1329
+ tf_batch *current_key_batch = NULL;
1330
+ group_accum accum;
1331
+ memset(&accum, 0, sizeof(accum));
1332
+ int have_group = 0;
1333
+ uint64_t first_ordinal = 0;
1334
+
1335
+ for (;;) {
1336
+ int best = group_agg_best_key_reader(st);
1337
+ if (best < 0) break;
1338
+ group_run_reader *reader = &st->readers[best];
1339
+ char *key = build_spill_row_key(st, &reader->row);
1340
+ if (!key) goto fail;
1341
+
1342
+ if (!have_group) {
1343
+ if (group_agg_start_spill_group(st, key, &reader->row, &current_key,
1344
+ &current_key_batch, &accum, &first_ordinal) != TF_OK) {
1345
+ free(key);
1346
+ goto fail;
1347
+ }
1348
+ have_group = 1;
1349
+ } else if (strcmp(current_key, key) != 0) {
1350
+ if (group_agg_finish_spill_group(st, &current_key, &current_key_batch, &accum, first_ordinal) != TF_OK) {
1351
+ free(key);
1352
+ goto fail;
1353
+ }
1354
+ if (group_agg_start_spill_group(st, key, &reader->row, &current_key,
1355
+ &current_key_batch, &accum, &first_ordinal) != TF_OK) {
1356
+ free(key);
1357
+ goto fail;
1358
+ }
1359
+ }
1360
+ free(key);
1361
+
1362
+ if (group_accum_add_spill_row(&accum, st, &reader->row) != TF_OK) goto fail;
1363
+ int rc = group_agg_reader_advance(st, reader, 0);
1364
+ if (rc < 0) goto fail;
1365
+ }
1366
+
1367
+ if (have_group && group_agg_finish_spill_group(st, &current_key, &current_key_batch, &accum, first_ordinal) != TF_OK) goto fail;
1368
+ if (st->out_buf && st->out_buf->n_rows > 0 && group_agg_write_output_run(st) != TF_OK) goto fail;
1369
+ group_agg_close_readers(st, 0);
1370
+ group_agg_remove_paths(&st->run_paths, &st->n_runs, &st->cap_runs);
1371
+ st->key_merge_done = 1;
1372
+ return TF_OK;
1373
+
1374
+ fail:
1375
+ if (current_key_batch) tf_batch_free(current_key_batch);
1376
+ if (have_group) group_accum_free(&accum);
1377
+ free(current_key);
1378
+ return TF_ERROR;
1379
+ }
1380
+
1381
+ static int group_agg_begin_output_merge(group_agg_state *st) {
1382
+ if (st->output_merge_started) return TF_OK;
1383
+ st->output_merge_started = 1;
1384
+ if (st->out_buf) { tf_batch_free(st->out_buf); st->out_buf = NULL; }
1385
+ free(st->out_ordinals); st->out_ordinals = NULL; st->out_ordinal_cap = 0;
1386
+ if (st->n_out_runs == 0) {
1387
+ tf_spill_cleanup(st->spill);
1388
+ st->spill = NULL;
1389
+ st->output_merge_done = 1;
1390
+ return TF_OK;
1391
+ }
1392
+ return group_agg_open_readers(st, 1);
1393
+ }
1394
+
1395
+ static int group_agg_output_next_batch(group_agg_state *st, tf_batch **out) {
1396
+ *out = NULL;
1397
+ if (group_agg_produce_output_runs(st) != TF_OK) return TF_ERROR;
1398
+ if (group_agg_begin_output_merge(st) != TF_OK) return TF_ERROR;
1399
+ if (st->output_merge_done) return TF_OK;
1400
+
1401
+ tf_batch *ob = group_agg_create_output_buffer(st, st->output_batch_rows);
1402
+ if (!ob) return TF_ERROR;
1403
+ while (ob->n_rows < st->output_batch_rows) {
1404
+ int best = group_agg_best_ordinal_reader(st);
1405
+ if (best < 0) break;
1406
+ group_run_reader *reader = &st->out_readers[best];
1407
+ size_t out_row = ob->n_rows;
1408
+ if (spill_row_to_batch(st, ob, out_row, &reader->row, 1) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
1409
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
1410
+ int rc = group_agg_reader_advance(st, reader, 1);
1411
+ if (rc < 0) { tf_batch_free(ob); return TF_ERROR; }
1412
+ }
1413
+ if (ob->n_rows == 0) {
1414
+ tf_batch_free(ob);
1415
+ group_agg_close_readers(st, 1);
1416
+ group_agg_remove_paths(&st->out_run_paths, &st->n_out_runs, &st->cap_out_runs);
1417
+ tf_spill_cleanup(st->spill);
1418
+ st->spill = NULL;
1419
+ st->output_merge_done = 1;
1420
+ return TF_OK;
1421
+ }
1422
+ st->spill_output_batches++;
1423
+ st->spill_output_rows += ob->n_rows;
1424
+ *out = ob;
1425
+ return TF_OK;
1426
+ }
1427
+
1428
+ static int group_agg_process_spill(group_agg_state *st, tf_batch *in) {
1429
+ if (group_agg_init_spill_schema(st, in) != TF_OK) return TF_ERROR;
1430
+ for (size_t r = 0; r < in->n_rows; r++) {
1431
+ size_t dst = st->buf->n_rows;
1432
+ if (ensure_ordinals(&st->buf_ordinals, &st->buf_ordinal_cap, dst + 1) != TF_OK) return TF_ERROR;
1433
+ if (tf_batch_copy_row(st->buf, dst, in, r) != TF_OK) return TF_ERROR;
1434
+ st->buf_ordinals[dst] = st->next_ordinal++;
1435
+ if (tf_batch_expose_row(st->buf, dst) != TF_OK) return TF_ERROR;
1436
+ if (st->buf->n_rows >= st->run_rows && group_agg_write_key_run(st) != TF_OK) return TF_ERROR;
1437
+ }
1438
+ return TF_OK;
1439
+ }
1440
+
1441
+ static int group_agg_start_sorted_group(group_agg_state *st, char *key,
1442
+ const tf_batch *in, size_t row,
1443
+ int *group_indices, tf_side_channels *side) {
1444
+ if (st->max_groups > 0 && st->sorted_groups_started >= st->max_groups) {
1445
+ if (group_agg_limit_error(st, side) != TF_OK) {
1446
+ free(key);
1447
+ return -1;
1448
+ }
1449
+ free(key);
1450
+ return -1;
1451
+ }
1452
+ if (ensure_sorted_key_batch(st, in, group_indices) != 0 ||
1453
+ tf_batch_ensure_capacity(st->sorted_key_batch, 1) != TF_OK) {
1454
+ free(key);
1455
+ return -1;
1456
+ }
1457
+
1458
+ if (st->sorted_have_current) {
1459
+ group_accum_free(&st->sorted_accum);
1460
+ memset(&st->sorted_accum, 0, sizeof(st->sorted_accum));
1461
+ free(st->sorted_key);
1462
+ st->sorted_key = NULL;
1463
+ st->sorted_have_current = 0;
1464
+ }
1465
+ if (group_accum_init(&st->sorted_accum, st->n_aggs) != 0) {
1466
+ free(key);
1467
+ return -1;
1468
+ }
1469
+
1470
+ if (copy_group_key_values(st->sorted_key_batch, 0, in, row, group_indices, st->n_group_cols) != TF_OK) {
1471
+ group_accum_free(&st->sorted_accum);
1472
+ memset(&st->sorted_accum, 0, sizeof(st->sorted_accum));
1473
+ free(key);
1474
+ return -1;
1475
+ }
1476
+ st->sorted_key = key;
1477
+ st->sorted_have_current = 1;
1478
+ st->sorted_groups_started++;
1479
+ if (tf_batch_expose_row(st->sorted_key_batch, 0) != TF_OK) return -1;
1480
+ if (group_agg_check_state_bytes(st, side) != 0) return -1;
1481
+ return 0;
1482
+ }
1483
+
1484
+ static int group_agg_process_sorted(tf_step *self, tf_batch *in, tf_batch **out,
1485
+ tf_side_channels *side) {
1486
+ group_agg_state *st = self->state;
1487
+ *out = NULL;
1488
+
1489
+ int *group_indices = tf_mallocarray_checked(st->n_group_cols, sizeof(int));
1490
+ int *agg_indices = tf_mallocarray_checked(st->n_aggs, sizeof(int));
1491
+ if (!group_indices || !agg_indices) {
1492
+ free(group_indices);
1493
+ free(agg_indices);
1494
+ return TF_ERROR;
1495
+ }
1496
+
1497
+ for (size_t k = 0; k < st->n_group_cols; k++)
1498
+ group_indices[k] = tf_batch_col_index(in, st->group_cols[k]);
1499
+ for (size_t k = 0; k < st->n_aggs; k++)
1500
+ agg_indices[k] = tf_batch_col_index(in, st->aggs[k].column);
1501
+
1502
+ tf_batch *ob = NULL;
1503
+ size_t out_rows = 0;
1504
+ size_t n_out_cols = st->n_group_cols + st->n_aggs;
1505
+
1506
+ for (size_t r = 0; r < in->n_rows; r++) {
1507
+ char *key = build_group_key(in, r, group_indices, st->n_group_cols);
1508
+ if (!key) goto fail;
1509
+
1510
+ if (!st->sorted_have_current) {
1511
+ if (group_agg_start_sorted_group(st, key, in, r, group_indices, side) != 0) goto fail;
1512
+ key = NULL;
1513
+ } else if (strcmp(st->sorted_key, key) != 0) {
1514
+ if (!ob) {
1515
+ size_t cap = in->n_rows > 0 ? in->n_rows : 1;
1516
+ ob = tf_batch_create(n_out_cols, cap);
1517
+ if (!ob) { free(key); goto fail; }
1518
+ if (group_agg_set_output_schema(st, ob, st->sorted_key_batch) != 0) { free(key); goto fail; }
1519
+ }
1520
+ if (group_agg_emit_row(st, ob, out_rows++, st->sorted_key_batch, 0, &st->sorted_accum) != 0) {
1521
+ free(key);
1522
+ goto fail;
1523
+ }
1524
+ if (group_agg_start_sorted_group(st, key, in, r, group_indices, side) != 0) goto fail;
1525
+ key = NULL;
1526
+ } else {
1527
+ free(key);
1528
+ key = NULL;
1529
+ }
1530
+
1531
+ if (group_accum_add_row(&st->sorted_accum, st, in, r, agg_indices) != TF_OK) goto fail;
1532
+ }
1533
+
1534
+ free(group_indices);
1535
+ free(agg_indices);
1536
+ if (ob && out_rows > 0) *out = ob;
1537
+ else if (ob) tf_batch_free(ob);
1538
+ return TF_OK;
1539
+
1540
+ fail:
1541
+ free(group_indices);
1542
+ free(agg_indices);
1543
+ if (ob) tf_batch_free(ob);
1544
+ return TF_ERROR;
1545
+ }
1546
+
1547
+ static int group_agg_flush_sorted(tf_step *self, tf_batch **out, tf_side_channels *side) {
1548
+ (void)side;
1549
+ group_agg_state *st = self->state;
1550
+ *out = NULL;
1551
+
1552
+ if (!st->sorted_have_current) return TF_OK;
1553
+
1554
+ size_t n_out_cols = st->n_group_cols + st->n_aggs;
1555
+ tf_batch *ob = tf_batch_create(n_out_cols, 1);
1556
+ if (!ob) return TF_ERROR;
1557
+ if (group_agg_set_output_schema(st, ob, st->sorted_key_batch) != 0 ||
1558
+ group_agg_emit_row(st, ob, 0, st->sorted_key_batch, 0, &st->sorted_accum) != 0) {
1559
+ tf_batch_free(ob);
1560
+ return TF_ERROR;
1561
+ }
1562
+
1563
+ group_accum_free(&st->sorted_accum);
1564
+ memset(&st->sorted_accum, 0, sizeof(st->sorted_accum));
1565
+ free(st->sorted_key);
1566
+ st->sorted_key = NULL;
1567
+ st->sorted_have_current = 0;
1568
+ if (st->sorted_key_batch) st->sorted_key_batch->n_rows = 0;
1569
+
1570
+ *out = ob;
1571
+ return TF_OK;
1572
+ }
1573
+
1574
+ static int find_or_add_group(group_agg_state *st, const char *key,
1575
+ const tf_batch *in, size_t row,
1576
+ int *group_indices, tf_side_channels *side) {
1577
+ group_map *map = &st->map;
1578
+ int found = 0;
1579
+ size_t slot = group_map_find_slot(map, key, &found);
1580
+ if (found) return (int)(map->slots[slot] - 1);
1581
+
1582
+ if (st->max_groups > 0 && map->count >= st->max_groups) {
1583
+ if (group_agg_limit_error(st, side) != TF_OK) return -1;
1584
+ return -1;
1585
+ }
1586
+
1587
+ size_t load_count = 0;
1588
+ size_t load_limit = 0;
1589
+ if (tf_size_mul(map->count, 4, &load_count) != TF_OK ||
1590
+ tf_size_mul(map->slot_cap, 3, &load_limit) != TF_OK) {
1591
+ return -1;
1592
+ }
1593
+ if (load_count >= load_limit) {
1594
+ size_t new_slot_cap = 0;
1595
+ if (tf_size_mul(map->slot_cap, 2, &new_slot_cap) != TF_OK) return -1;
1596
+ if (group_map_rehash(map, new_slot_cap) != 0) return -1;
1597
+ slot = group_map_find_slot(map, key, &found);
1598
+ }
1599
+ size_t next_count = 0;
1600
+ if (tf_size_add(map->count, 1, &next_count) != TF_OK) return -1;
1601
+ if (group_map_ensure_group_capacity(map, next_count) != 0) return -1;
1602
+ if (ensure_key_batch(st, in, group_indices) != 0) return -1;
1603
+ if (tf_batch_ensure_capacity(map->key_batch, next_count) != TF_OK) return -1;
1604
+
1605
+ char *dup = tf_strdup_checked(key);
1606
+ if (!dup) return -1;
1607
+ group_accum accum;
1608
+ if (group_accum_init(&accum, st->n_aggs) != 0) { free(dup); return -1; }
1609
+ size_t key_bytes_delta = 0;
1610
+ size_t new_key_bytes = 0;
1611
+ if (tf_size_add(strlen(dup), 1, &key_bytes_delta) != TF_OK ||
1612
+ tf_size_add(map->key_bytes, key_bytes_delta, &new_key_bytes) != TF_OK) {
1613
+ group_accum_free(&accum);
1614
+ free(dup);
1615
+ return -1;
1616
+ }
1617
+
1618
+ size_t idx = map->count;
1619
+ if (copy_group_key_values(map->key_batch, idx, in, row, group_indices, st->n_group_cols) != TF_OK) {
1620
+ group_accum_free(&accum);
1621
+ free(dup);
1622
+ return -1;
1623
+ }
1624
+ if (tf_batch_expose_row(map->key_batch, idx) != TF_OK) {
1625
+ group_accum_free(&accum);
1626
+ free(dup);
1627
+ return -1;
1628
+ }
1629
+ map->keys[idx] = dup;
1630
+ map->key_bytes = new_key_bytes;
1631
+ map->accums[idx] = accum;
1632
+ map->slots[slot] = idx + 1;
1633
+ map->count++;
1634
+ if (group_agg_check_state_bytes(st, side) != 0) return -1;
1635
+ return (int)idx;
1636
+ }
1637
+
1638
+ static int group_agg_process(tf_step *self, tf_batch *in, tf_batch **out,
1639
+ tf_side_channels *side) {
1640
+ group_agg_state *st = self->state;
1641
+ if (st->sorted) return group_agg_process_sorted(self, in, out, side);
1642
+ *out = NULL;
1643
+ if (st->use_spill) return group_agg_process_spill(st, in);
1644
+
1645
+ int *group_indices = tf_mallocarray_checked(st->n_group_cols, sizeof(int));
1646
+ int *agg_indices = tf_mallocarray_checked(st->n_aggs, sizeof(int));
1647
+ if (!group_indices || !agg_indices) { free(group_indices); free(agg_indices); return TF_ERROR; }
1648
+
1649
+ for (size_t k = 0; k < st->n_group_cols; k++)
1650
+ group_indices[k] = tf_batch_col_index(in, st->group_cols[k]);
1651
+ for (size_t k = 0; k < st->n_aggs; k++)
1652
+ agg_indices[k] = tf_batch_col_index(in, st->aggs[k].column);
1653
+
1654
+ for (size_t r = 0; r < in->n_rows; r++) {
1655
+ char *key = build_group_key(in, r, group_indices, st->n_group_cols);
1656
+ if (!key) { free(group_indices); free(agg_indices); return TF_ERROR; }
1657
+ int gi = find_or_add_group(st, key, in, r, group_indices, side);
1658
+ free(key);
1659
+ if (gi < 0) { free(group_indices); free(agg_indices); return TF_ERROR; }
1660
+ if (group_accum_add_row(&st->map.accums[gi], st, in, r, agg_indices) != TF_OK) {
1661
+ free(group_indices);
1662
+ free(agg_indices);
1663
+ return TF_ERROR;
1664
+ }
1665
+ }
1666
+
1667
+ free(group_indices);
1668
+ free(agg_indices);
1669
+ return TF_OK;
1670
+ }
1671
+
1672
+ static int group_agg_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
1673
+ group_agg_state *st = self->state;
1674
+ if (st->sorted) return group_agg_flush_sorted(self, out, side);
1675
+ (void)side;
1676
+ *out = NULL;
1677
+ if (st->use_spill) return group_agg_output_next_batch(st, out);
1678
+
1679
+ if (st->map.count == 0) return TF_OK;
1680
+
1681
+ size_t n_out_cols = st->n_group_cols + st->n_aggs;
1682
+ tf_batch *ob = tf_batch_create(n_out_cols, st->map.count);
1683
+ if (!ob) return TF_ERROR;
1684
+
1685
+ if (group_agg_set_output_schema(st, ob, st->map.key_batch) != 0) {
1686
+ tf_batch_free(ob);
1687
+ return TF_ERROR;
1688
+ }
1689
+
1690
+ for (size_t g = 0; g < st->map.count; g++) {
1691
+ if (group_agg_emit_row(st, ob, g, st->map.key_batch, g, &st->map.accums[g]) != 0) {
1692
+ tf_batch_free(ob);
1693
+ return TF_ERROR;
1694
+ }
1695
+ }
1696
+
1697
+ *out = ob;
1698
+ return TF_OK;
1699
+ }
1700
+
1701
+ static int group_agg_flush_next(tf_step *self, tf_batch **out, tf_side_channels *side) {
1702
+ (void)side;
1703
+ group_agg_state *st = self->state;
1704
+ *out = NULL;
1705
+ if (!st->use_spill) return TF_OK;
1706
+ return group_agg_output_next_batch(st, out);
1707
+ }
1708
+
1709
+ static size_t group_agg_accum_bytes(size_t n_groups, size_t n_aggs) {
1710
+ return n_groups * n_aggs * (3 * sizeof(double) + sizeof(size_t));
1711
+ }
1712
+
1713
+ static void group_agg_add_bytes(size_t *acc, size_t value) {
1714
+ if (*acc > SIZE_MAX - value) *acc = SIZE_MAX;
1715
+ else *acc += value;
1716
+ }
1717
+
1718
+ static void group_agg_add_mul_bytes(size_t *acc, size_t a, size_t b) {
1719
+ if (a != 0 && b > SIZE_MAX / a) *acc = SIZE_MAX;
1720
+ else group_agg_add_bytes(acc, a * b);
1721
+ }
1722
+
1723
+ static size_t group_agg_type_size(tf_type t) {
1724
+ switch (t) {
1725
+ case TF_TYPE_BOOL: return sizeof(uint8_t);
1726
+ case TF_TYPE_INT64: return sizeof(int64_t);
1727
+ case TF_TYPE_FLOAT64: return sizeof(double);
1728
+ case TF_TYPE_STRING: return sizeof(char *);
1729
+ case TF_TYPE_DATE: return sizeof(int32_t);
1730
+ case TF_TYPE_TIMESTAMP: return sizeof(int64_t);
1731
+ default: return 0;
1732
+ }
1733
+ }
1734
+
1735
+ static size_t group_agg_batch_retained_bytes(const tf_batch *b) {
1736
+ if (!b) return 0;
1737
+ size_t bytes = sizeof(tf_batch);
1738
+ group_agg_add_mul_bytes(&bytes, b->n_cols, sizeof(char *));
1739
+ group_agg_add_mul_bytes(&bytes, b->n_cols, sizeof(tf_type));
1740
+ group_agg_add_mul_bytes(&bytes, b->n_cols, sizeof(void *));
1741
+ group_agg_add_mul_bytes(&bytes, b->n_cols, sizeof(uint8_t *));
1742
+ for (size_t c = 0; c < b->n_cols; c++) {
1743
+ if (b->col_names && b->col_names[c]) group_agg_add_bytes(&bytes, strlen(b->col_names[c]) + 1);
1744
+ size_t sz = group_agg_type_size(b->col_types[c]);
1745
+ group_agg_add_mul_bytes(&bytes, sz, b->capacity);
1746
+ group_agg_add_bytes(&bytes, b->capacity);
1747
+ if (b->col_types[c] == TF_TYPE_STRING && b->columns && b->columns[c]) {
1748
+ char **vals = (char **)b->columns[c];
1749
+ for (size_t r = 0; r < b->n_rows; r++) {
1750
+ if (b->nulls && b->nulls[c] && b->nulls[c][r]) continue;
1751
+ if (vals[r]) group_agg_add_bytes(&bytes, strlen(vals[r]) + 1);
1752
+ }
1753
+ }
1754
+ }
1755
+ return bytes;
1756
+ }
1757
+
1758
+ static size_t group_agg_sorted_key_bytes(const group_agg_state *st) {
1759
+ return (st && st->sorted_have_current && st->sorted_key) ? strlen(st->sorted_key) + 1 : 0;
1760
+ }
1761
+
1762
+ static size_t group_agg_retained_state_bytes(const group_agg_state *st) {
1763
+ if (!st) return 0;
1764
+ if (st->use_spill) {
1765
+ size_t bytes = 0;
1766
+ bytes += group_agg_batch_retained_bytes(st->buf);
1767
+ bytes += group_agg_batch_retained_bytes(st->out_buf);
1768
+ group_agg_add_mul_bytes(&bytes, st->buf_ordinal_cap, sizeof(uint64_t));
1769
+ group_agg_add_mul_bytes(&bytes, st->out_ordinal_cap, sizeof(uint64_t));
1770
+ group_agg_add_mul_bytes(&bytes, st->n_readers + st->n_out_readers, sizeof(group_run_reader));
1771
+ return bytes;
1772
+ }
1773
+ if (st->sorted) {
1774
+ size_t current_key = group_agg_sorted_key_bytes(st);
1775
+ if (!st->sorted_have_current) return 0;
1776
+ return current_key + sizeof(group_accum) +
1777
+ group_agg_accum_bytes(1, st->n_aggs) +
1778
+ group_agg_batch_retained_bytes(st->sorted_key_batch);
1779
+ }
1780
+ return st->map.key_bytes +
1781
+ st->map.cap * (sizeof(char *) + sizeof(group_accum)) +
1782
+ st->map.slot_cap * sizeof(size_t) +
1783
+ group_agg_accum_bytes(st->map.count, st->n_aggs) +
1784
+ group_agg_batch_retained_bytes(st->map.key_batch);
1785
+ }
1786
+
1787
+ static int group_agg_append_stats(tf_step *self, tf_buffer *out) {
1788
+ if (!self || !self->state || !out) return TF_ERROR;
1789
+ group_agg_state *st = self->state;
1790
+ size_t tracked_groups = st->use_spill ? st->spill_distinct_groups :
1791
+ (st->sorted ? (st->sorted_have_current ? 1u : 0u) : st->map.count);
1792
+ size_t tracked_key_bytes = st->use_spill ? st->spill_key_bytes :
1793
+ (st->sorted ? group_agg_sorted_key_bytes(st) : st->map.key_bytes);
1794
+ size_t retained = group_agg_retained_state_bytes(st);
1795
+ char buf[420];
1796
+ snprintf(buf, sizeof(buf),
1797
+ ",\"tracked_groups\":%zu,\"tracked_key_bytes\":%zu,"
1798
+ "\"retained_state_bytes\":%zu,\"max_state_bytes\":%zu",
1799
+ tracked_groups, tracked_key_bytes, retained, st->max_state_bytes);
1800
+ if (tf_buffer_write_str(out, buf) != TF_OK) return TF_ERROR;
1801
+ if (st->use_spill) {
1802
+ snprintf(buf, sizeof(buf),
1803
+ ",\"spill_bytes\":%zu,\"spill_runs\":%zu,"
1804
+ "\"spill_output_batches\":%zu,\"spill_output_rows\":%zu,"
1805
+ "\"spill_distinct_groups\":%zu",
1806
+ st->spilled_bytes, st->spill_runs_created,
1807
+ st->spill_output_batches, st->spill_output_rows,
1808
+ st->spill_distinct_groups);
1809
+ return tf_buffer_write_str(out, buf);
1810
+ }
1811
+ return TF_OK;
1812
+ }
1813
+
1814
+ static void group_agg_state_free(group_agg_state *st) {
1815
+ if (st) {
1816
+ if (st->group_cols) {
1817
+ for (size_t i = 0; i < st->n_group_cols; i++) free(st->group_cols[i]);
1818
+ }
1819
+ free(st->group_cols);
1820
+ if (st->aggs) {
1821
+ for (size_t i = 0; i < st->n_aggs; i++) { free(st->aggs[i].column); free(st->aggs[i].name); }
1822
+ }
1823
+ free(st->aggs);
1824
+ if (st->sorted_have_current) group_accum_free(&st->sorted_accum);
1825
+ free(st->sorted_key);
1826
+ if (st->sorted_key_batch) tf_batch_free(st->sorted_key_batch);
1827
+ if (st->buf) tf_batch_free(st->buf);
1828
+ if (st->out_buf) tf_batch_free(st->out_buf);
1829
+ free(st->buf_ordinals);
1830
+ free(st->out_ordinals);
1831
+ if (st->has_schema) {
1832
+ group_agg_close_readers(st, 0);
1833
+ group_agg_close_readers(st, 1);
1834
+ }
1835
+ group_agg_remove_paths(&st->run_paths, &st->n_runs, &st->cap_runs);
1836
+ group_agg_remove_paths(&st->out_run_paths, &st->n_out_runs, &st->cap_out_runs);
1837
+ for (size_t i = 0; i < st->n_schema_cols; i++) free(st->schema_names ? st->schema_names[i] : NULL);
1838
+ free(st->schema_names);
1839
+ free(st->schema_types);
1840
+ free(st->group_indices);
1841
+ free(st->agg_indices);
1842
+ for (size_t i = 0; i < st->n_out_schema_cols; i++) free(st->out_schema_names ? st->out_schema_names[i] : NULL);
1843
+ free(st->out_schema_names);
1844
+ free(st->out_schema_types);
1845
+ tf_spill_cleanup(st->spill);
1846
+ free(st->spill_dir);
1847
+ group_map_free(&st->map);
1848
+ free(st);
1849
+ }
1850
+ }
1851
+
1852
+ static void group_agg_destroy(tf_step *self) {
1853
+ if (self) group_agg_state_free(self->state);
1854
+ free(self);
1855
+ }
1856
+
1857
+ tf_step *tf_group_agg_create(const cJSON *args) {
1858
+ if (!args) return NULL;
1859
+ cJSON *group_by = cJSON_GetObjectItemCaseSensitive(args, "group_by");
1860
+ cJSON *aggs_j = cJSON_GetObjectItemCaseSensitive(args, "aggs");
1861
+ if (!cJSON_IsArray(group_by) || !cJSON_IsArray(aggs_j)) return NULL;
1862
+
1863
+ group_agg_state *st = tf_callocarray_checked(1, sizeof(group_agg_state));
1864
+ if (!st) return NULL;
1865
+ st->output_batch_rows = GROUP_AGG_DEFAULT_OUTPUT_ROWS;
1866
+
1867
+ cJSON *sorted = cJSON_GetObjectItemCaseSensitive(args, "sorted");
1868
+ if (cJSON_IsBool(sorted) && cJSON_IsTrue(sorted)) st->sorted = 1;
1869
+
1870
+ cJSON *spill_dir_j = cJSON_GetObjectItemCaseSensitive(args, "spill_dir");
1871
+ if (cJSON_IsString(spill_dir_j) && spill_dir_j->valuestring && spill_dir_j->valuestring[0]) {
1872
+ st->use_spill = 1;
1873
+ st->spill_dir = tf_strdup_checked(spill_dir_j->valuestring);
1874
+ if (!st->spill_dir) { group_agg_state_free(st); return NULL; }
1875
+ if (tf_spill_session_create(st->spill_dir, &st->spill) != TF_OK) { group_agg_state_free(st); return NULL; }
1876
+ size_t parsed_size = 0;
1877
+ int has_spill_memory = tf_json_get_size_arg(args, "spill_memory_bytes",
1878
+ 1, TF_MAX_SPILL_MEMORY_BYTES,
1879
+ &parsed_size, "group-agg");
1880
+ if (has_spill_memory < 0) { group_agg_state_free(st); return NULL; }
1881
+ if (has_spill_memory > 0) st->spill_memory_bytes = parsed_size;
1882
+ int has_spill_rows = tf_json_get_size_arg(args, "spill_run_rows",
1883
+ 1, TF_MAX_SPILL_RUN_ROWS,
1884
+ &parsed_size, "group-agg");
1885
+ if (has_spill_rows < 0) { group_agg_state_free(st); return NULL; }
1886
+ if (has_spill_rows > 0) st->configured_run_rows = parsed_size;
1887
+ int has_output_rows = tf_json_get_size_arg(args, "spill_output_rows",
1888
+ 1, TF_MAX_SPILL_OUTPUT_ROWS,
1889
+ &parsed_size, "group-agg");
1890
+ if (has_output_rows < 0) { group_agg_state_free(st); return NULL; }
1891
+ if (has_output_rows > 0) st->output_batch_rows = parsed_size;
1892
+ }
1893
+
1894
+ if (st->use_spill && st->sorted) {
1895
+ tf_set_last_error("group-agg: spill_dir and sorted=true are mutually exclusive");
1896
+ group_agg_state_free(st);
1897
+ return NULL;
1898
+ }
1899
+ if (!st->sorted && !st->use_spill && group_map_init(&st->map) != 0) { group_agg_state_free(st); return NULL; }
1900
+
1901
+ size_t parsed_size = 0;
1902
+ int has_max_groups = tf_json_get_size_arg(args, "max_groups",
1903
+ 1, TF_MAX_COUNT_ARG,
1904
+ &parsed_size, "group-agg");
1905
+ if (has_max_groups < 0) { group_agg_state_free(st); return NULL; }
1906
+ if (has_max_groups > 0) st->max_groups = parsed_size;
1907
+
1908
+ int has_max_state = tf_json_get_size_arg(args, "max_state_bytes",
1909
+ 1, TF_MAX_STATE_BYTES,
1910
+ &parsed_size, "group-agg");
1911
+ if (has_max_state < 0) { group_agg_state_free(st); return NULL; }
1912
+ if (has_max_state > 0) st->max_state_bytes = parsed_size;
1913
+
1914
+ int ng = cJSON_GetArraySize(group_by);
1915
+ st->group_cols = tf_callocarray_checked((size_t)ng, sizeof(char *));
1916
+ st->n_group_cols = ng;
1917
+ if (ng > 0 && !st->group_cols) { group_agg_state_free(st); return NULL; }
1918
+ for (int i = 0; i < ng; i++) {
1919
+ cJSON *item = cJSON_GetArrayItem(group_by, i);
1920
+ if (cJSON_IsString(item)) {
1921
+ st->group_cols[i] = tf_strdup_checked(item->valuestring);
1922
+ if (!st->group_cols[i]) { group_agg_state_free(st); return NULL; }
1923
+ }
1924
+ }
1925
+
1926
+ int na = cJSON_GetArraySize(aggs_j);
1927
+ st->aggs = tf_callocarray_checked((size_t)na, sizeof(agg_spec));
1928
+ st->n_aggs = na;
1929
+ if (na > 0 && !st->aggs) { group_agg_state_free(st); return NULL; }
1930
+ for (int i = 0; i < na; i++) {
1931
+ cJSON *item = cJSON_GetArrayItem(aggs_j, i);
1932
+ cJSON *col = cJSON_GetObjectItemCaseSensitive(item, "column");
1933
+ cJSON *func = cJSON_GetObjectItemCaseSensitive(item, "func");
1934
+ cJSON *name = cJSON_GetObjectItemCaseSensitive(item, "name");
1935
+ st->aggs[i].column = tf_strdup_checked(cJSON_IsString(col) ? col->valuestring : "");
1936
+ if (!st->aggs[i].column) { group_agg_state_free(st); return NULL; }
1937
+ st->aggs[i].func = cJSON_IsString(func) ? parse_agg_func(func->valuestring) : AGG_COUNT;
1938
+ if (cJSON_IsString(name)) {
1939
+ st->aggs[i].name = tf_strdup_checked(name->valuestring);
1940
+ } else {
1941
+ st->aggs[i].name = group_agg_default_name(st->aggs[i].column,
1942
+ cJSON_IsString(func) ? func->valuestring : "count");
1943
+ }
1944
+ if (!st->aggs[i].name) { group_agg_state_free(st); return NULL; }
1945
+ }
1946
+
1947
+ tf_step *step = tf_callocarray_checked(1, sizeof(tf_step));
1948
+ if (!step) { group_agg_state_free(st); return NULL; }
1949
+ step->process = group_agg_process;
1950
+ step->flush = group_agg_flush;
1951
+ step->flush_next = st->use_spill ? group_agg_flush_next : NULL;
1952
+ step->append_stats = group_agg_append_stats;
1953
+ step->destroy = group_agg_destroy;
1954
+ step->state = st;
1955
+ return step;
1956
+ }