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
package/csrc/op_tee.c ADDED
@@ -0,0 +1,338 @@
1
+ /*
2
+ * op_tee.c -- Streaming side-channel row snapshots.
3
+ *
4
+ * Config: {
5
+ * "expr": "optional row predicate",
6
+ * "channel": "samples|errors|stats",
7
+ * "columns": ["optional", "selectors"],
8
+ * "limit": 1000,
9
+ * "every": 1,
10
+ * "name": "tee",
11
+ * "include_row": true
12
+ * }
13
+ */
14
+
15
+ #include "internal.h"
16
+ #include "cJSON.h"
17
+ #include <stdbool.h>
18
+ #include <stdio.h>
19
+ #include <stdlib.h>
20
+ #include <string.h>
21
+
22
+ typedef struct {
23
+ tf_expr *expr;
24
+ char *expr_text;
25
+ char *name;
26
+ char *channel_name;
27
+ int channel;
28
+ char **selectors;
29
+ size_t n_selectors;
30
+ int *indices;
31
+ size_t n_indices;
32
+ int resolved;
33
+ size_t limit;
34
+ size_t every;
35
+ size_t row_index;
36
+ size_t emitted;
37
+ int include_row;
38
+ tf_audit_options audit_opts;
39
+ } tee_state;
40
+
41
+ static int parse_channel(const char *s, int *out, const char **name_out) {
42
+ if (!s || !*s || strcmp(s, "samples") == 0 || strcmp(s, "sample") == 0) {
43
+ *out = TF_CHAN_SAMPLES;
44
+ *name_out = "samples";
45
+ return 1;
46
+ }
47
+ if (strcmp(s, "errors") == 0 || strcmp(s, "error") == 0) {
48
+ *out = TF_CHAN_ERRORS;
49
+ *name_out = "errors";
50
+ return 1;
51
+ }
52
+ if (strcmp(s, "stats") == 0 || strcmp(s, "audit") == 0) {
53
+ *out = TF_CHAN_STATS;
54
+ *name_out = strcmp(s, "audit") == 0 ? "audit" : "stats";
55
+ return 1;
56
+ }
57
+ return 0;
58
+ }
59
+
60
+ static tf_buffer *tee_side_buffer(tf_side_channels *side, int channel) {
61
+ if (!side) return NULL;
62
+ switch (channel) {
63
+ case TF_CHAN_ERRORS: return side->errors;
64
+ case TF_CHAN_STATS: return side->stats;
65
+ case TF_CHAN_SAMPLES: return side->samples;
66
+ default: return NULL;
67
+ }
68
+ }
69
+
70
+ static int tee_audit_column_allowed(const tee_state *st, const char *name) {
71
+ if (!st || st->audit_opts.n_columns == 0) return 1;
72
+ for (size_t i = 0; i < st->audit_opts.n_columns; i++) {
73
+ if (st->audit_opts.columns[i] && strcmp(st->audit_opts.columns[i], name ? name : "") == 0)
74
+ return 1;
75
+ }
76
+ return 0;
77
+ }
78
+
79
+ static cJSON *tee_selected_row_to_json(const tee_state *st, const tf_batch *in, size_t row) {
80
+ cJSON *data = cJSON_CreateObject();
81
+ if (!data) return NULL;
82
+ for (size_t i = 0; i < st->n_indices; i++) {
83
+ int ci = st->indices[i];
84
+ if (ci < 0 || (size_t)ci >= in->n_cols) continue;
85
+ const char *name = in->col_names[ci] ? in->col_names[ci] : "";
86
+ if (!tee_audit_column_allowed(st, name)) continue;
87
+ cJSON *value = tf_audit_cell_to_json(in, row, (size_t)ci, &st->audit_opts);
88
+ if (!value) { cJSON_Delete(data); return NULL; }
89
+ if (tf_json_add_item(data, name, value) != TF_OK) {
90
+ cJSON_Delete(data);
91
+ return NULL;
92
+ }
93
+ }
94
+ if (st->audit_opts.max_bytes > 0) {
95
+ char *printed = cJSON_PrintUnformatted(data);
96
+ if (!printed) { cJSON_Delete(data); return NULL; }
97
+ size_t n = strlen(printed);
98
+ free(printed);
99
+ if (n > st->audit_opts.max_bytes) {
100
+ cJSON_Delete(data);
101
+ data = cJSON_CreateObject();
102
+ if (!data) return NULL;
103
+ if (tf_json_add_bool(data, "_audit_truncated", 1) != TF_OK ||
104
+ tf_json_add_number(data, "max_bytes", (double)st->audit_opts.max_bytes) != TF_OK) {
105
+ cJSON_Delete(data);
106
+ return NULL;
107
+ }
108
+ }
109
+ }
110
+ return data;
111
+ }
112
+
113
+ static int tee_resolve_columns(tee_state *st, const tf_batch *in) {
114
+ if (st->resolved) return TF_OK;
115
+ free(st->indices);
116
+ st->indices = NULL;
117
+ st->n_indices = 0;
118
+
119
+ if (st->n_selectors == 0) {
120
+ st->indices = malloc((in->n_cols ? in->n_cols : 1) * sizeof(int));
121
+ if (!st->indices) return TF_ERROR;
122
+ for (size_t i = 0; i < in->n_cols; i++) st->indices[i] = (int)i;
123
+ st->n_indices = in->n_cols;
124
+ st->resolved = 1;
125
+ return TF_OK;
126
+ }
127
+
128
+ char *error = NULL;
129
+ if (tf_column_selectors_resolve(st->selectors, st->n_selectors,
130
+ in->col_names, in->col_types, in->n_cols,
131
+ &st->indices, &st->n_indices, &error) != TF_OK) {
132
+ char msg[512];
133
+ snprintf(msg, sizeof(msg), "tee: %s", error ? error : "invalid columns");
134
+ tf_set_last_error(msg);
135
+ free(error);
136
+ return TF_ERROR;
137
+ }
138
+ free(error);
139
+ st->resolved = 1;
140
+ return TF_OK;
141
+ }
142
+
143
+ static int tee_emit_row(tee_state *st, const tf_batch *in, size_t row,
144
+ tf_side_channels *side) {
145
+ tf_buffer *buf = tee_side_buffer(side, st->channel);
146
+ if (!buf) return TF_OK;
147
+
148
+ cJSON *obj = cJSON_CreateObject();
149
+ if (!obj) return TF_ERROR;
150
+ int rc = TF_ERROR;
151
+ if (tf_json_add_string(obj, "type", "tee") != TF_OK ||
152
+ tf_json_add_string(obj, "op", "tee") != TF_OK ||
153
+ tf_json_add_string(obj, "name", st->name ? st->name : "tee") != TF_OK ||
154
+ tf_json_add_string(obj, "channel", st->channel_name ? st->channel_name : "samples") != TF_OK) {
155
+ goto done;
156
+ }
157
+ if (st->expr_text && st->expr_text[0] &&
158
+ tf_json_add_string(obj, "expr", st->expr_text) != TF_OK) {
159
+ goto done;
160
+ }
161
+ if (st->include_row && tf_json_add_number(obj, "row", (double)st->row_index) != TF_OK) goto done;
162
+
163
+ if (st->audit_opts.include_row) {
164
+ cJSON *data = tee_selected_row_to_json(st, in, row);
165
+ if (!data || tf_json_add_item(obj, "data", data) != TF_OK) goto done;
166
+ }
167
+
168
+ rc = tf_buffer_write_json_line(buf, obj);
169
+ done:
170
+ cJSON_Delete(obj);
171
+ return rc;
172
+ }
173
+
174
+ static int tee_process(tf_step *self, tf_batch *in, tf_batch **out,
175
+ tf_side_channels *side) {
176
+ tee_state *st = self->state;
177
+ *out = NULL;
178
+
179
+ if (tee_resolve_columns(st, in) != TF_OK) return TF_ERROR;
180
+
181
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows > 0 ? in->n_rows : 1);
182
+ if (!ob) return TF_ERROR;
183
+ if (tf_batch_clone_schema(ob, in) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
184
+
185
+ for (size_t r = 0; r < in->n_rows; r++) {
186
+ st->row_index++;
187
+ bool should_emit = true;
188
+ if (st->expr) {
189
+ if (tf_expr_eval(st->expr, in, r, &should_emit) != TF_OK) should_emit = false;
190
+ }
191
+ if (should_emit && st->every > 1 && ((st->row_index - 1) % st->every) != 0)
192
+ should_emit = false;
193
+ if (should_emit && st->emitted < st->limit) {
194
+ if (tee_emit_row(st, in, r, side) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
195
+ st->emitted++;
196
+ }
197
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
198
+ if (tf_batch_expose_row(ob, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
199
+ }
200
+
201
+ *out = ob;
202
+ return TF_OK;
203
+ }
204
+
205
+ static int tee_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
206
+ (void)self; (void)side;
207
+ *out = NULL;
208
+ return TF_OK;
209
+ }
210
+
211
+ static void tee_state_free(tee_state *st) {
212
+ if (!st) return;
213
+ tf_expr_free(st->expr);
214
+ free(st->expr_text);
215
+ free(st->name);
216
+ free(st->channel_name);
217
+ tf_audit_options_free(&st->audit_opts);
218
+ for (size_t i = 0; i < st->n_selectors; i++) free(st->selectors[i]);
219
+ free(st->selectors);
220
+ free(st->indices);
221
+ free(st);
222
+ }
223
+
224
+ static void tee_destroy(tf_step *self) {
225
+ if (!self) return;
226
+ tee_state_free(self->state);
227
+ free(self);
228
+ }
229
+
230
+ static int tee_copy_columns_arg(tee_state *st, const cJSON *cols) {
231
+ if (!cols) return TF_OK;
232
+ if (!cJSON_IsArray(cols)) {
233
+ tf_set_last_error("tee: columns must be an array");
234
+ return TF_ERROR;
235
+ }
236
+ int n = cJSON_GetArraySize((cJSON *)cols);
237
+ if (n <= 0) return TF_OK;
238
+ st->selectors = calloc((size_t)n, sizeof(char *));
239
+ if (!st->selectors) return TF_ERROR;
240
+ st->n_selectors = (size_t)n;
241
+ for (int i = 0; i < n; i++) {
242
+ cJSON *item = cJSON_GetArrayItem((cJSON *)cols, i);
243
+ if (!cJSON_IsString(item) || !item->valuestring[0]) {
244
+ tf_set_last_error("tee: columns must contain non-empty strings");
245
+ return TF_ERROR;
246
+ }
247
+ st->selectors[i] = strdup(item->valuestring);
248
+ if (!st->selectors[i]) return TF_ERROR;
249
+ }
250
+ return TF_OK;
251
+ }
252
+
253
+ tf_step *tf_tee_create(const cJSON *args) {
254
+ if (!args) return NULL;
255
+
256
+ tee_state *st = calloc(1, sizeof(tee_state));
257
+ if (!st) return NULL;
258
+ st->limit = 1000;
259
+ st->every = 1;
260
+ st->include_row = 1;
261
+ tf_audit_options_init(&st->audit_opts, 1);
262
+
263
+ cJSON *expr_json = cJSON_GetObjectItemCaseSensitive(args, "expr");
264
+ if (cJSON_IsString(expr_json) && expr_json->valuestring[0]) {
265
+ st->expr = tf_expr_parse(expr_json->valuestring);
266
+ if (!st->expr) { tee_state_free(st); return NULL; }
267
+ st->expr_text = strdup(expr_json->valuestring);
268
+ if (!st->expr_text) { tee_state_free(st); return NULL; }
269
+ }
270
+
271
+ cJSON *channel_json = cJSON_GetObjectItemCaseSensitive(args, "channel");
272
+ const char *channel_name = NULL;
273
+ if (!parse_channel(cJSON_IsString(channel_json) ? channel_json->valuestring : NULL,
274
+ &st->channel, &channel_name)) {
275
+ tf_set_last_error("tee: channel must be samples, errors, stats, or audit");
276
+ tee_state_free(st);
277
+ return NULL;
278
+ }
279
+ st->channel_name = strdup(channel_name);
280
+
281
+ cJSON *name_json = cJSON_GetObjectItemCaseSensitive(args, "name");
282
+ st->name = strdup(cJSON_IsString(name_json) && name_json->valuestring[0] ? name_json->valuestring : "tee");
283
+
284
+ cJSON *limit_json = cJSON_GetObjectItemCaseSensitive(args, "limit");
285
+ if (!limit_json) limit_json = cJSON_GetObjectItemCaseSensitive(args, "max_rows");
286
+ if (limit_json) {
287
+ size_t parsed_limit = 0;
288
+ if (tf_json_get_size_arg_any(args, "limit", "max_rows",
289
+ 1, TF_MAX_AUDIT_RECORDS,
290
+ &parsed_limit, "tee") < 0) {
291
+ tee_state_free(st);
292
+ return NULL;
293
+ }
294
+ st->limit = parsed_limit;
295
+ }
296
+
297
+ cJSON *every_json = cJSON_GetObjectItemCaseSensitive(args, "every");
298
+ if (every_json) {
299
+ size_t parsed_every = 0;
300
+ if (tf_json_get_size_arg(args, "every", 1, TF_MAX_COUNT_ARG,
301
+ &parsed_every, "tee") < 0) {
302
+ tee_state_free(st);
303
+ return NULL;
304
+ }
305
+ st->every = parsed_every;
306
+ }
307
+
308
+ cJSON *include_json = cJSON_GetObjectItemCaseSensitive(args, "include_row");
309
+ if (!include_json) include_json = cJSON_GetObjectItemCaseSensitive(args, "includeRow");
310
+ if (include_json) st->include_row = cJSON_IsTrue(include_json);
311
+
312
+ cJSON *columns_json = cJSON_GetObjectItemCaseSensitive(args, "columns");
313
+ if (tee_copy_columns_arg(st, columns_json) != TF_OK) {
314
+ tee_state_free(st);
315
+ return NULL;
316
+ }
317
+
318
+ if (tf_audit_options_parse(&st->audit_opts, args, "tee") != TF_OK) {
319
+ tee_state_free(st);
320
+ return NULL;
321
+ }
322
+
323
+ if (!st->name || !st->channel_name) {
324
+ tee_state_free(st);
325
+ return NULL;
326
+ }
327
+
328
+ tf_step *step = calloc(1, sizeof(tf_step));
329
+ if (!step) {
330
+ tee_state_free(st);
331
+ return NULL;
332
+ }
333
+ step->process = tee_process;
334
+ step->flush = tee_flush;
335
+ step->destroy = tee_destroy;
336
+ step->state = st;
337
+ return step;
338
+ }
package/csrc/op_top.c ADDED
@@ -0,0 +1,357 @@
1
+ /*
2
+ * op_top.c — Heap-backed bounded top/bottom N rows by column.
3
+ *
4
+ * Config: {"n": 10, "column": "score", "desc": true}
5
+ */
6
+
7
+ #include "internal.h"
8
+ #include "cJSON.h"
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+
13
+ typedef struct {
14
+ size_t n;
15
+ char *column;
16
+ int desc; /* 1 = highest first (default) */
17
+ tf_batch *buf;
18
+ size_t *heap; /* row indices in buf; root is worst retained row */
19
+ size_t heap_len;
20
+ size_t replacements_since_compact;
21
+ int has_schema;
22
+ int col_idx;
23
+ } top_state;
24
+
25
+ static int top_compare_cells(const tf_batch *a, size_t ra,
26
+ const tf_batch *b, size_t rb,
27
+ int ci, int desc) {
28
+ int null_a = tf_batch_is_null(a, ra, ci);
29
+ int null_b = tf_batch_is_null(b, rb, ci);
30
+
31
+ /* Match sort semantics: nulls rank last for both ascending and descending. */
32
+ if (null_a && null_b) return 0;
33
+ if (null_a) return 1;
34
+ if (null_b) return -1;
35
+
36
+ int cmp = 0;
37
+ switch (a->col_types[ci]) {
38
+ case TF_TYPE_BOOL: {
39
+ bool va = tf_batch_get_bool(a, ra, ci);
40
+ bool vb = tf_batch_get_bool(b, rb, ci);
41
+ cmp = (int)va - (int)vb;
42
+ break;
43
+ }
44
+ case TF_TYPE_INT64: {
45
+ int64_t va = tf_batch_get_int64(a, ra, ci);
46
+ int64_t vb = tf_batch_get_int64(b, rb, ci);
47
+ cmp = (va > vb) - (va < vb);
48
+ break;
49
+ }
50
+ case TF_TYPE_FLOAT64: {
51
+ double va = tf_batch_get_float64(a, ra, ci);
52
+ double vb = tf_batch_get_float64(b, rb, ci);
53
+ cmp = (va > vb) - (va < vb);
54
+ break;
55
+ }
56
+ case TF_TYPE_STRING:
57
+ cmp = strcmp(tf_batch_get_string(a, ra, ci),
58
+ tf_batch_get_string(b, rb, ci));
59
+ break;
60
+ case TF_TYPE_DATE: {
61
+ int32_t va = tf_batch_get_date(a, ra, ci);
62
+ int32_t vb = tf_batch_get_date(b, rb, ci);
63
+ cmp = (va > vb) - (va < vb);
64
+ break;
65
+ }
66
+ case TF_TYPE_TIMESTAMP: {
67
+ int64_t va = tf_batch_get_timestamp(a, ra, ci);
68
+ int64_t vb = tf_batch_get_timestamp(b, rb, ci);
69
+ cmp = (va > vb) - (va < vb);
70
+ break;
71
+ }
72
+ default:
73
+ break;
74
+ }
75
+
76
+ return desc ? -cmp : cmp;
77
+ }
78
+
79
+ static int top_heap_worse(const top_state *st, size_t a, size_t b) {
80
+ return top_compare_cells(st->buf, a, st->buf, b, st->col_idx, st->desc) > 0;
81
+ }
82
+
83
+ static void top_heap_swap(size_t *a, size_t *b) {
84
+ size_t tmp = *a;
85
+ *a = *b;
86
+ *b = tmp;
87
+ }
88
+
89
+ static void top_heap_sift_up(top_state *st, size_t pos) {
90
+ while (pos > 0) {
91
+ size_t parent = (pos - 1) / 2;
92
+ if (!top_heap_worse(st, st->heap[pos], st->heap[parent])) break;
93
+ top_heap_swap(&st->heap[pos], &st->heap[parent]);
94
+ pos = parent;
95
+ }
96
+ }
97
+
98
+ static void top_heap_sift_down(top_state *st, size_t pos) {
99
+ for (;;) {
100
+ size_t left = pos * 2 + 1;
101
+ size_t right = left + 1;
102
+ size_t worst = pos;
103
+
104
+ if (left < st->heap_len && top_heap_worse(st, st->heap[left], st->heap[worst])) {
105
+ worst = left;
106
+ }
107
+ if (right < st->heap_len && top_heap_worse(st, st->heap[right], st->heap[worst])) {
108
+ worst = right;
109
+ }
110
+ if (worst == pos) break;
111
+ top_heap_swap(&st->heap[pos], &st->heap[worst]);
112
+ pos = worst;
113
+ }
114
+ }
115
+
116
+ static void top_heap_push(top_state *st, size_t row) {
117
+ st->heap[st->heap_len] = row;
118
+ top_heap_sift_up(st, st->heap_len);
119
+ st->heap_len++;
120
+ }
121
+
122
+ static void top_heap_rebuild(top_state *st) {
123
+ st->heap_len = 0;
124
+ for (size_t r = 0; r < st->buf->n_rows; r++) {
125
+ top_heap_push(st, r);
126
+ }
127
+ }
128
+
129
+ static int top_compact_buffer(top_state *st) {
130
+ if (!st->buf) return TF_OK;
131
+
132
+ tf_batch *old = st->buf;
133
+ size_t cap = st->n > 0 ? st->n : 1;
134
+ tf_batch *nb = tf_batch_create(old->n_cols, cap);
135
+ if (!nb) return TF_ERROR;
136
+
137
+ if (tf_batch_clone_schema(nb, old) != TF_OK) {
138
+ tf_batch_free(nb);
139
+ return TF_ERROR;
140
+ }
141
+ for (size_t r = 0; r < old->n_rows; r++) {
142
+ if (tf_batch_copy_row(nb, r, old, r) != TF_OK) {
143
+ tf_batch_free(nb);
144
+ return TF_ERROR;
145
+ }
146
+ if (tf_batch_expose_row(nb, r) != TF_OK) {
147
+ tf_batch_free(nb);
148
+ return TF_ERROR;
149
+ }
150
+ }
151
+
152
+ st->buf = nb;
153
+ tf_batch_free(old);
154
+ top_heap_rebuild(st);
155
+ st->replacements_since_compact = 0;
156
+ return TF_OK;
157
+ }
158
+
159
+ static int top_init_schema(top_state *st, const tf_batch *in) {
160
+ size_t cap = st->n > 0 ? st->n : 1;
161
+ tf_batch *buf = tf_batch_create(in->n_cols, cap);
162
+ if (!buf) return TF_ERROR;
163
+
164
+ size_t *heap = NULL;
165
+ if (st->n > 0) {
166
+ heap = calloc(st->n, sizeof(size_t));
167
+ if (!heap) {
168
+ tf_batch_free(buf);
169
+ return TF_ERROR;
170
+ }
171
+ }
172
+
173
+ int col_idx = tf_batch_col_index(in, st->column);
174
+ if (col_idx < 0) {
175
+ char msg[256];
176
+ snprintf(msg, sizeof(msg), "top: column '%s' not found", st->column ? st->column : "");
177
+ tf_set_last_error(msg);
178
+ free(heap);
179
+ tf_batch_free(buf);
180
+ return TF_ERROR;
181
+ }
182
+
183
+ if (tf_batch_clone_schema(buf, in) != TF_OK) {
184
+ free(heap);
185
+ tf_batch_free(buf);
186
+ return TF_ERROR;
187
+ }
188
+
189
+ st->buf = buf;
190
+ st->heap = heap;
191
+ st->col_idx = col_idx;
192
+ st->has_schema = 1;
193
+ return TF_OK;
194
+ }
195
+
196
+ static int top_process(tf_step *self, tf_batch *in, tf_batch **out,
197
+ tf_side_channels *side) {
198
+ (void)side;
199
+ top_state *st = self->state;
200
+ *out = NULL;
201
+
202
+ if (!st->has_schema) {
203
+ if (top_init_schema(st, in) != TF_OK) return TF_ERROR;
204
+ }
205
+
206
+ if (st->n == 0) return TF_OK;
207
+
208
+ int ci = st->col_idx;
209
+ for (size_t r = 0; r < in->n_rows; r++) {
210
+ if (st->buf->n_rows < st->n) {
211
+ size_t dst = st->buf->n_rows;
212
+ if (tf_batch_copy_row(st->buf, dst, in, r) != TF_OK) return TF_ERROR;
213
+ if (tf_batch_expose_row(st->buf, dst) != TF_OK) return TF_ERROR;
214
+ top_heap_push(st, dst);
215
+ } else {
216
+ size_t worst_idx = st->heap[0];
217
+ if (top_compare_cells(in, r, st->buf, worst_idx, ci, st->desc) < 0) {
218
+ if (tf_batch_copy_row(st->buf, worst_idx, in, r) != TF_OK) return TF_ERROR;
219
+ top_heap_sift_down(st, 0);
220
+ st->replacements_since_compact++;
221
+ size_t compact_every = st->n < 4096 ? st->n : 4096;
222
+ if (compact_every > 0 && st->replacements_since_compact >= compact_every) {
223
+ if (top_compact_buffer(st) != TF_OK) return TF_ERROR;
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ return TF_OK;
230
+ }
231
+
232
+ typedef struct {
233
+ const tf_batch *batch;
234
+ int col_idx;
235
+ int desc;
236
+ } top_sort_ctx;
237
+
238
+ static int top_sort_cmp(const top_sort_ctx *ctx, size_t a, size_t b) {
239
+ return top_compare_cells(ctx->batch, a, ctx->batch, b, ctx->col_idx, ctx->desc);
240
+ }
241
+
242
+ static int top_sort_cmp_index(const void *ctx, size_t a, size_t b) {
243
+ return top_sort_cmp((const top_sort_ctx *)ctx, a, b);
244
+ }
245
+
246
+ static int top_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
247
+ (void)side;
248
+ top_state *st = self->state;
249
+ *out = NULL;
250
+
251
+ if (!st->buf || st->buf->n_rows == 0) return TF_OK;
252
+
253
+ /* Sort the buffer */
254
+ size_t n = st->buf->n_rows;
255
+ size_t *indices = malloc(n * sizeof(size_t));
256
+ if (!indices) return TF_ERROR;
257
+ for (size_t i = 0; i < n; i++) indices[i] = i;
258
+
259
+ top_sort_ctx ctx = { .batch = st->buf, .col_idx = st->col_idx, .desc = st->desc };
260
+ tf_sort_indices(indices, n, top_sort_cmp_index, &ctx);
261
+
262
+ tf_batch *ob = tf_batch_create(st->buf->n_cols, n);
263
+ if (!ob) { free(indices); return TF_ERROR; }
264
+ if (tf_batch_clone_schema(ob, st->buf) != TF_OK) {
265
+ free(indices);
266
+ tf_batch_free(ob);
267
+ return TF_ERROR;
268
+ }
269
+ for (size_t i = 0; i < n; i++) {
270
+ if (tf_batch_copy_row(ob, i, st->buf, indices[i]) != TF_OK) {
271
+ free(indices);
272
+ tf_batch_free(ob);
273
+ return TF_ERROR;
274
+ }
275
+ if (tf_batch_expose_row(ob, i) != TF_OK) {
276
+ free(indices);
277
+ tf_batch_free(ob);
278
+ return TF_ERROR;
279
+ }
280
+ }
281
+
282
+ free(indices);
283
+ *out = ob;
284
+ return TF_OK;
285
+ }
286
+
287
+ static void top_destroy(tf_step *self) {
288
+ top_state *st = self->state;
289
+ if (st) {
290
+ free(st->column);
291
+ free(st->heap);
292
+ if (st->buf) tf_batch_free(st->buf);
293
+ free(st);
294
+ }
295
+ free(self);
296
+ }
297
+
298
+ tf_step *tf_top_create(const cJSON *args) {
299
+ if (!args) return NULL;
300
+ cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
301
+ size_t n = 0;
302
+ int has_n = tf_json_get_size_arg(args, "n", 0, TF_MAX_OUTPUT_ROWS_PER_BATCH, &n, "top");
303
+ if (has_n <= 0) return NULL;
304
+ if (!cJSON_IsString(col_j) || !col_j->valuestring || col_j->valuestring[0] == '\0') {
305
+ tf_set_last_error("top: column must be a non-empty string");
306
+ return NULL;
307
+ }
308
+
309
+ top_state *st = calloc(1, sizeof(top_state));
310
+ if (!st) return NULL;
311
+ st->n = n;
312
+ st->col_idx = -1;
313
+ st->column = strdup(col_j->valuestring);
314
+ if (!st->column) { free(st); return NULL; }
315
+
316
+ cJSON *desc_j = cJSON_GetObjectItemCaseSensitive(args, "desc");
317
+ st->desc = (desc_j && cJSON_IsBool(desc_j)) ? cJSON_IsTrue(desc_j) : 1; /* default desc */
318
+
319
+ tf_step *step = calloc(1, sizeof(tf_step));
320
+ if (!step) { free(st->column); free(st); return NULL; }
321
+ step->process = top_process;
322
+ step->flush = top_flush;
323
+ step->destroy = top_destroy;
324
+ step->state = st;
325
+ return step;
326
+ }
327
+
328
+ static tf_step *top_create_with_default_desc(const cJSON *args, int default_desc) {
329
+ if (!args) return NULL;
330
+ cJSON *copy = cJSON_Duplicate(args, 1);
331
+ if (!copy) return NULL;
332
+ if (!cJSON_GetObjectItemCaseSensitive(copy, "desc")) {
333
+ if (tf_json_add_bool(copy, "desc", default_desc) != TF_OK) {
334
+ cJSON_Delete(copy);
335
+ return NULL;
336
+ }
337
+ }
338
+ tf_step *step = tf_top_create(copy);
339
+ cJSON_Delete(copy);
340
+ return step;
341
+ }
342
+
343
+ tf_step *tf_top_k_create(const cJSON *args) {
344
+ return top_create_with_default_desc(args, 1);
345
+ }
346
+
347
+ tf_step *tf_bottom_k_create(const cJSON *args) {
348
+ return top_create_with_default_desc(args, 0);
349
+ }
350
+
351
+ tf_step *tf_slice_min_create(const cJSON *args) {
352
+ return top_create_with_default_desc(args, 0);
353
+ }
354
+
355
+ tf_step *tf_slice_max_create(const cJSON *args) {
356
+ return top_create_with_default_desc(args, 1);
357
+ }