tranfi 0.0.1 → 0.1.2

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 (109) hide show
  1. package/README.md +395 -0
  2. package/app/assets/index-6quYZ5Ap.css +5 -0
  3. package/app/assets/index-pDFMluyz.js +160 -0
  4. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  5. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  6. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  7. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  8. package/app/index.html +13 -0
  9. package/binding.gyp +69 -0
  10. package/csrc/arena.c +91 -0
  11. package/csrc/batch.c +229 -0
  12. package/csrc/buffer.c +78 -0
  13. package/csrc/cJSON.c +3143 -0
  14. package/csrc/cJSON.h +300 -0
  15. package/csrc/codec_csv.c +1058 -0
  16. package/csrc/codec_jsonl.c +374 -0
  17. package/csrc/codec_table.c +218 -0
  18. package/csrc/codec_text.c +229 -0
  19. package/csrc/compiler.c +102 -0
  20. package/csrc/date_utils.h +94 -0
  21. package/csrc/dsl.c +1180 -0
  22. package/csrc/dsl.h +22 -0
  23. package/csrc/expr.c +1245 -0
  24. package/csrc/expr.h +56 -0
  25. package/csrc/internal.h +250 -0
  26. package/csrc/ir.c +119 -0
  27. package/csrc/ir.h +167 -0
  28. package/csrc/ir_schema.c +60 -0
  29. package/csrc/ir_serialize.c +104 -0
  30. package/csrc/ir_sql.c +1211 -0
  31. package/csrc/ir_validate.c +120 -0
  32. package/csrc/main.c +392 -0
  33. package/csrc/op_acf.c +133 -0
  34. package/csrc/op_anomaly.c +120 -0
  35. package/csrc/op_bin.c +109 -0
  36. package/csrc/op_cast.c +195 -0
  37. package/csrc/op_clip.c +88 -0
  38. package/csrc/op_date_trunc.c +181 -0
  39. package/csrc/op_datetime.c +212 -0
  40. package/csrc/op_derive.c +248 -0
  41. package/csrc/op_diff.c +134 -0
  42. package/csrc/op_ewma.c +103 -0
  43. package/csrc/op_explode.c +108 -0
  44. package/csrc/op_fill_down.c +163 -0
  45. package/csrc/op_fill_null.c +123 -0
  46. package/csrc/op_filter.c +132 -0
  47. package/csrc/op_frequency.c +193 -0
  48. package/csrc/op_grep.c +163 -0
  49. package/csrc/op_group_agg.c +285 -0
  50. package/csrc/op_hash.c +126 -0
  51. package/csrc/op_head.c +149 -0
  52. package/csrc/op_interpolate.c +239 -0
  53. package/csrc/op_join.c +384 -0
  54. package/csrc/op_label_encode.c +144 -0
  55. package/csrc/op_lead.c +190 -0
  56. package/csrc/op_normalize.c +226 -0
  57. package/csrc/op_onehot.c +185 -0
  58. package/csrc/op_pivot.c +370 -0
  59. package/csrc/op_registry.c +1148 -0
  60. package/csrc/op_rename.c +138 -0
  61. package/csrc/op_replace.c +202 -0
  62. package/csrc/op_sample.c +101 -0
  63. package/csrc/op_select.c +140 -0
  64. package/csrc/op_skip.c +152 -0
  65. package/csrc/op_sort.c +273 -0
  66. package/csrc/op_split.c +114 -0
  67. package/csrc/op_split_data.c +87 -0
  68. package/csrc/op_stack.c +315 -0
  69. package/csrc/op_stats.c +779 -0
  70. package/csrc/op_step.c +171 -0
  71. package/csrc/op_tail.c +96 -0
  72. package/csrc/op_top.c +150 -0
  73. package/csrc/op_trim.c +109 -0
  74. package/csrc/op_unique.c +300 -0
  75. package/csrc/op_unpivot.c +159 -0
  76. package/csrc/op_validate.c +71 -0
  77. package/csrc/op_window.c +150 -0
  78. package/csrc/pipeline.c +315 -0
  79. package/csrc/plan.c +206 -0
  80. package/csrc/recipes.c +102 -0
  81. package/csrc/recipes.h +27 -0
  82. package/csrc/report.c +463 -0
  83. package/csrc/report.h +22 -0
  84. package/csrc/tranfi.h +123 -0
  85. package/csrc/wasm_api.c +157 -0
  86. package/napi_api.c +326 -0
  87. package/package.json +46 -57
  88. package/src/cli.js +193 -0
  89. package/src/engines/duckdb.js +109 -0
  90. package/src/index.js +306 -0
  91. package/src/native.js +22 -0
  92. package/src/pipeline.js +286 -0
  93. package/src/server.js +277 -0
  94. package/src/wasm.js +19 -0
  95. package/wasm/index.js +244 -0
  96. package/wasm/package.json +1 -0
  97. package/wasm/tranfi_core.js +0 -0
  98. package/LICENSE +0 -21
  99. package/dist/bundle.js +0 -1
  100. package/index.html +0 -18
  101. package/logo.png +0 -0
  102. package/src/app.css +0 -160
  103. package/src/app.js +0 -203
  104. package/src/app.vue +0 -253
  105. package/src/bulma-input.vue +0 -110
  106. package/src/common-inputs.js +0 -28
  107. package/src/main.js +0 -18
  108. package/src/transforms.js +0 -166
  109. package/webpack.config.js +0 -108
@@ -0,0 +1,108 @@
1
+ /*
2
+ * op_explode.c — Split delimited string into multiple rows.
3
+ *
4
+ * Config: {"column": "tags", "delimiter": ","}
5
+ */
6
+
7
+ #include "internal.h"
8
+ #include "cJSON.h"
9
+ #include <stdlib.h>
10
+ #include <string.h>
11
+
12
+ typedef struct {
13
+ char *column;
14
+ char *delimiter;
15
+ } explode_state;
16
+
17
+ static int explode_process(tf_step *self, tf_batch *in, tf_batch **out,
18
+ tf_side_channels *side) {
19
+ (void)side;
20
+ explode_state *st = self->state;
21
+ *out = NULL;
22
+
23
+ int ci = tf_batch_col_index(in, st->column);
24
+
25
+ /* Estimate max output rows */
26
+ size_t max_out = in->n_rows * 4;
27
+ tf_batch *ob = tf_batch_create(in->n_cols, max_out > 0 ? max_out : 16);
28
+ if (!ob) return TF_ERROR;
29
+ for (size_t c = 0; c < in->n_cols; c++)
30
+ tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
31
+
32
+ size_t out_row = 0;
33
+ size_t delim_len = strlen(st->delimiter);
34
+
35
+ for (size_t r = 0; r < in->n_rows; r++) {
36
+ if (ci < 0 || tf_batch_is_null(in, r, ci) || in->col_types[ci] != TF_TYPE_STRING) {
37
+ tf_batch_copy_row(ob, out_row, in, r);
38
+ ob->n_rows = ++out_row;
39
+ continue;
40
+ }
41
+
42
+ const char *val = tf_batch_get_string(in, r, ci);
43
+ const char *p = val;
44
+
45
+ while (*p) {
46
+ const char *found = strstr(p, st->delimiter);
47
+ size_t tok_len = found ? (size_t)(found - p) : strlen(p);
48
+
49
+ tf_batch_copy_row(ob, out_row, in, r);
50
+ /* Override the exploded column */
51
+ char *tok = malloc(tok_len + 1);
52
+ if (tok) {
53
+ memcpy(tok, p, tok_len);
54
+ tok[tok_len] = '\0';
55
+ /* Trim leading/trailing whitespace */
56
+ char *s = tok;
57
+ while (*s == ' ') s++;
58
+ char *e = s + strlen(s);
59
+ while (e > s && *(e - 1) == ' ') e--;
60
+ *e = '\0';
61
+ tf_batch_set_string(ob, out_row, ci, s);
62
+ free(tok);
63
+ }
64
+ ob->n_rows = ++out_row;
65
+
66
+ if (found) p = found + delim_len;
67
+ else break;
68
+ }
69
+ }
70
+
71
+ if (out_row > 0) {
72
+ *out = ob;
73
+ } else {
74
+ tf_batch_free(ob);
75
+ }
76
+ return TF_OK;
77
+ }
78
+
79
+ static int explode_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
80
+ (void)self; (void)side; *out = NULL; return TF_OK;
81
+ }
82
+
83
+ static void explode_destroy(tf_step *self) {
84
+ explode_state *st = self->state;
85
+ if (st) { free(st->column); free(st->delimiter); free(st); }
86
+ free(self);
87
+ }
88
+
89
+ tf_step *tf_explode_create(const cJSON *args) {
90
+ if (!args) return NULL;
91
+ cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
92
+ if (!cJSON_IsString(col_j)) return NULL;
93
+
94
+ explode_state *st = calloc(1, sizeof(explode_state));
95
+ if (!st) return NULL;
96
+ st->column = strdup(col_j->valuestring);
97
+
98
+ cJSON *delim_j = cJSON_GetObjectItemCaseSensitive(args, "delimiter");
99
+ st->delimiter = strdup(cJSON_IsString(delim_j) ? delim_j->valuestring : ",");
100
+
101
+ tf_step *step = malloc(sizeof(tf_step));
102
+ if (!step) { free(st->column); free(st->delimiter); free(st); return NULL; }
103
+ step->process = explode_process;
104
+ step->flush = explode_flush;
105
+ step->destroy = explode_destroy;
106
+ step->state = st;
107
+ return step;
108
+ }
@@ -0,0 +1,163 @@
1
+ /*
2
+ * op_fill_down.c — Forward-fill nulls with last non-null value.
3
+ *
4
+ * Config: {"columns": ["city", "state"]}
5
+ * or {} for all columns.
6
+ */
7
+
8
+ #include "internal.h"
9
+ #include "cJSON.h"
10
+ #include "date_utils.h"
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <stdio.h>
14
+
15
+ typedef struct {
16
+ char **cols;
17
+ size_t n_cols;
18
+ /* Last non-null value per column (stored as strings for simplicity) */
19
+ char **last_vals;
20
+ int *last_types; /* tf_type */
21
+ int64_t *last_int;
22
+ double *last_float;
23
+ int *last_bool;
24
+ int32_t *last_date;
25
+ int64_t *last_timestamp;
26
+ int n_tracked;
27
+ int initialized;
28
+ } fill_down_state;
29
+
30
+ static int fill_down_process(tf_step *self, tf_batch *in, tf_batch **out,
31
+ tf_side_channels *side) {
32
+ (void)side;
33
+ fill_down_state *st = self->state;
34
+ *out = NULL;
35
+
36
+ if (!st->initialized) {
37
+ st->n_tracked = (int)in->n_cols;
38
+ st->last_vals = calloc(in->n_cols, sizeof(char *));
39
+ st->last_types = calloc(in->n_cols, sizeof(int));
40
+ st->last_int = calloc(in->n_cols, sizeof(int64_t));
41
+ st->last_float = calloc(in->n_cols, sizeof(double));
42
+ st->last_bool = calloc(in->n_cols, sizeof(int));
43
+ st->last_date = calloc(in->n_cols, sizeof(int32_t));
44
+ st->last_timestamp = calloc(in->n_cols, sizeof(int64_t));
45
+ for (size_t c = 0; c < in->n_cols; c++) st->last_types[c] = -1; /* no value yet */
46
+ st->initialized = 1;
47
+ }
48
+
49
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
50
+ if (!ob) return TF_ERROR;
51
+ for (size_t c = 0; c < in->n_cols; c++)
52
+ tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
53
+
54
+ for (size_t r = 0; r < in->n_rows; r++) {
55
+ tf_batch_copy_row(ob, r, in, r);
56
+
57
+ for (size_t c = 0; c < in->n_cols; c++) {
58
+ /* Check if this column is targeted */
59
+ int target = 0;
60
+ if (st->n_cols == 0) {
61
+ target = 1;
62
+ } else {
63
+ for (size_t k = 0; k < st->n_cols; k++) {
64
+ if (strcmp(in->col_names[c], st->cols[k]) == 0) { target = 1; break; }
65
+ }
66
+ }
67
+ if (!target) continue;
68
+
69
+ if (!tf_batch_is_null(in, r, c)) {
70
+ /* Update last value */
71
+ st->last_types[c] = (int)in->col_types[c];
72
+ switch (in->col_types[c]) {
73
+ case TF_TYPE_STRING:
74
+ free(st->last_vals[c]);
75
+ st->last_vals[c] = strdup(tf_batch_get_string(in, r, c));
76
+ break;
77
+ case TF_TYPE_INT64:
78
+ st->last_int[c] = tf_batch_get_int64(in, r, c); break;
79
+ case TF_TYPE_FLOAT64:
80
+ st->last_float[c] = tf_batch_get_float64(in, r, c); break;
81
+ case TF_TYPE_BOOL:
82
+ st->last_bool[c] = tf_batch_get_bool(in, r, c); break;
83
+ case TF_TYPE_DATE:
84
+ st->last_date[c] = tf_batch_get_date(in, r, c); break;
85
+ case TF_TYPE_TIMESTAMP:
86
+ st->last_timestamp[c] = tf_batch_get_timestamp(in, r, c); break;
87
+ default: break;
88
+ }
89
+ } else if (st->last_types[c] >= 0) {
90
+ /* Fill with last value */
91
+ switch (in->col_types[c]) {
92
+ case TF_TYPE_STRING:
93
+ if (st->last_vals[c]) tf_batch_set_string(ob, r, c, st->last_vals[c]);
94
+ break;
95
+ case TF_TYPE_INT64:
96
+ tf_batch_set_int64(ob, r, c, st->last_int[c]); break;
97
+ case TF_TYPE_FLOAT64:
98
+ tf_batch_set_float64(ob, r, c, st->last_float[c]); break;
99
+ case TF_TYPE_BOOL:
100
+ tf_batch_set_bool(ob, r, c, st->last_bool[c]); break;
101
+ case TF_TYPE_DATE:
102
+ tf_batch_set_date(ob, r, c, st->last_date[c]); break;
103
+ case TF_TYPE_TIMESTAMP:
104
+ tf_batch_set_timestamp(ob, r, c, st->last_timestamp[c]); break;
105
+ default: break;
106
+ }
107
+ }
108
+ }
109
+ ob->n_rows = r + 1;
110
+ }
111
+
112
+ *out = ob;
113
+ return TF_OK;
114
+ }
115
+
116
+ static int fill_down_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
117
+ (void)self; (void)side; *out = NULL; return TF_OK;
118
+ }
119
+
120
+ static void fill_down_destroy(tf_step *self) {
121
+ fill_down_state *st = self->state;
122
+ if (st) {
123
+ for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
124
+ free(st->cols);
125
+ if (st->last_vals) {
126
+ for (int i = 0; i < st->n_tracked; i++) free(st->last_vals[i]);
127
+ free(st->last_vals);
128
+ }
129
+ free(st->last_types); free(st->last_int);
130
+ free(st->last_float); free(st->last_bool);
131
+ free(st->last_date); free(st->last_timestamp);
132
+ free(st);
133
+ }
134
+ free(self);
135
+ }
136
+
137
+ tf_step *tf_fill_down_create(const cJSON *args) {
138
+ fill_down_state *st = calloc(1, sizeof(fill_down_state));
139
+ if (!st) return NULL;
140
+
141
+ if (args) {
142
+ cJSON *columns = cJSON_GetObjectItemCaseSensitive(args, "columns");
143
+ if (columns && cJSON_IsArray(columns)) {
144
+ int n = cJSON_GetArraySize(columns);
145
+ if (n > 0) {
146
+ st->cols = calloc(n, sizeof(char *));
147
+ st->n_cols = n;
148
+ for (int i = 0; i < n; i++) {
149
+ cJSON *item = cJSON_GetArrayItem(columns, i);
150
+ if (cJSON_IsString(item)) st->cols[i] = strdup(item->valuestring);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ tf_step *step = malloc(sizeof(tf_step));
157
+ if (!step) { free(st); return NULL; }
158
+ step->process = fill_down_process;
159
+ step->flush = fill_down_flush;
160
+ step->destroy = fill_down_destroy;
161
+ step->state = st;
162
+ return step;
163
+ }
@@ -0,0 +1,123 @@
1
+ /*
2
+ * op_fill_null.c — Replace nulls with default values.
3
+ *
4
+ * Config: {"mapping": {"col1": "default1", "col2": "0"}}
5
+ */
6
+
7
+ #include "internal.h"
8
+ #include "cJSON.h"
9
+ #include "date_utils.h"
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+
13
+ typedef struct {
14
+ char **col_names;
15
+ char **defaults;
16
+ size_t n;
17
+ } fill_null_state;
18
+
19
+ static int fill_null_process(tf_step *self, tf_batch *in, tf_batch **out,
20
+ tf_side_channels *side) {
21
+ (void)side;
22
+ fill_null_state *st = self->state;
23
+ *out = NULL;
24
+
25
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
26
+ if (!ob) return TF_ERROR;
27
+ for (size_t c = 0; c < in->n_cols; c++)
28
+ tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
29
+ for (size_t r = 0; r < in->n_rows; r++) {
30
+ tf_batch_copy_row(ob, r, in, r);
31
+ ob->n_rows = r + 1;
32
+ }
33
+
34
+ /* Fill nulls */
35
+ for (size_t k = 0; k < st->n; k++) {
36
+ int ci = tf_batch_col_index(ob, st->col_names[k]);
37
+ if (ci < 0) continue;
38
+ for (size_t r = 0; r < ob->n_rows; r++) {
39
+ if (!tf_batch_is_null(ob, r, ci)) continue;
40
+ const char *def = st->defaults[k];
41
+ switch (ob->col_types[ci]) {
42
+ case TF_TYPE_STRING:
43
+ tf_batch_set_string(ob, r, ci, def);
44
+ break;
45
+ case TF_TYPE_INT64: {
46
+ char *end;
47
+ int64_t v = strtoll(def, &end, 10);
48
+ if (*end == '\0') tf_batch_set_int64(ob, r, ci, v);
49
+ break;
50
+ }
51
+ case TF_TYPE_FLOAT64: {
52
+ char *end;
53
+ double v = strtod(def, &end);
54
+ if (*end == '\0') tf_batch_set_float64(ob, r, ci, v);
55
+ break;
56
+ }
57
+ case TF_TYPE_BOOL:
58
+ tf_batch_set_bool(ob, r, ci, strcmp(def, "true") == 0);
59
+ break;
60
+ case TF_TYPE_DATE: {
61
+ char *end;
62
+ int32_t v = (int32_t)strtol(def, &end, 10);
63
+ if (*end == '\0') tf_batch_set_date(ob, r, ci, v);
64
+ break;
65
+ }
66
+ case TF_TYPE_TIMESTAMP: {
67
+ char *end;
68
+ int64_t v = strtoll(def, &end, 10);
69
+ if (*end == '\0') tf_batch_set_timestamp(ob, r, ci, v);
70
+ break;
71
+ }
72
+ default:
73
+ break;
74
+ }
75
+ }
76
+ }
77
+
78
+ *out = ob;
79
+ return TF_OK;
80
+ }
81
+
82
+ static int fill_null_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
83
+ (void)self; (void)side; *out = NULL; return TF_OK;
84
+ }
85
+
86
+ static void fill_null_destroy(tf_step *self) {
87
+ fill_null_state *st = self->state;
88
+ if (st) {
89
+ for (size_t i = 0; i < st->n; i++) { free(st->col_names[i]); free(st->defaults[i]); }
90
+ free(st->col_names); free(st->defaults);
91
+ free(st);
92
+ }
93
+ free(self);
94
+ }
95
+
96
+ tf_step *tf_fill_null_create(const cJSON *args) {
97
+ if (!args) return NULL;
98
+ cJSON *mapping = cJSON_GetObjectItemCaseSensitive(args, "mapping");
99
+ if (!mapping || !cJSON_IsObject(mapping)) return NULL;
100
+
101
+ int n = cJSON_GetArraySize(mapping);
102
+ fill_null_state *st = calloc(1, sizeof(fill_null_state));
103
+ if (!st) return NULL;
104
+ st->col_names = calloc(n, sizeof(char *));
105
+ st->defaults = calloc(n, sizeof(char *));
106
+ st->n = n;
107
+
108
+ int i = 0;
109
+ cJSON *entry = NULL;
110
+ cJSON_ArrayForEach(entry, mapping) {
111
+ st->col_names[i] = strdup(entry->string);
112
+ st->defaults[i] = strdup(cJSON_IsString(entry) ? entry->valuestring : "");
113
+ i++;
114
+ }
115
+
116
+ tf_step *step = malloc(sizeof(tf_step));
117
+ if (!step) { fill_null_destroy(&(tf_step){.state = st}); return NULL; }
118
+ step->process = fill_null_process;
119
+ step->flush = fill_null_flush;
120
+ step->destroy = fill_null_destroy;
121
+ step->state = st;
122
+ return step;
123
+ }
@@ -0,0 +1,132 @@
1
+ /*
2
+ * op_filter.c — Filter rows by expression.
3
+ *
4
+ * Config: {"expr": "col('age') > 25"}
5
+ * Evaluates expression for each row, keeps rows where result is true.
6
+ */
7
+
8
+ #include "internal.h"
9
+ #include "cJSON.h"
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+ #include <stdio.h>
13
+
14
+ typedef struct {
15
+ tf_expr *expr;
16
+ size_t rows_in;
17
+ size_t rows_out;
18
+ } filter_state;
19
+
20
+ static int filter_process(tf_step *self, tf_batch *in, tf_batch **out,
21
+ tf_side_channels *side) {
22
+ filter_state *st = self->state;
23
+ *out = NULL;
24
+
25
+ /* Create output batch with same schema */
26
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
27
+ if (!ob) return TF_ERROR;
28
+ for (size_t i = 0; i < in->n_cols; i++) {
29
+ tf_batch_set_schema(ob, i, in->col_names[i], in->col_types[i]);
30
+ }
31
+
32
+ /* Filter rows */
33
+ size_t out_row = 0;
34
+ for (size_t r = 0; r < in->n_rows; r++) {
35
+ bool keep = false;
36
+ if (tf_expr_eval(st->expr, in, r, &keep) != TF_OK) continue;
37
+ if (!keep) continue;
38
+
39
+ if (tf_batch_ensure_capacity(ob, out_row + 1) != TF_OK) {
40
+ tf_batch_free(ob);
41
+ return TF_ERROR;
42
+ }
43
+
44
+ /* Copy row */
45
+ for (size_t c = 0; c < in->n_cols; c++) {
46
+ if (tf_batch_is_null(in, r, c)) {
47
+ tf_batch_set_null(ob, out_row, c);
48
+ continue;
49
+ }
50
+ switch (in->col_types[c]) {
51
+ case TF_TYPE_BOOL:
52
+ tf_batch_set_bool(ob, out_row, c, tf_batch_get_bool(in, r, c));
53
+ break;
54
+ case TF_TYPE_INT64:
55
+ tf_batch_set_int64(ob, out_row, c, tf_batch_get_int64(in, r, c));
56
+ break;
57
+ case TF_TYPE_FLOAT64:
58
+ tf_batch_set_float64(ob, out_row, c, tf_batch_get_float64(in, r, c));
59
+ break;
60
+ case TF_TYPE_STRING:
61
+ tf_batch_set_string(ob, out_row, c, tf_batch_get_string(in, r, c));
62
+ break;
63
+ case TF_TYPE_DATE:
64
+ tf_batch_set_date(ob, out_row, c, tf_batch_get_date(in, r, c));
65
+ break;
66
+ case TF_TYPE_TIMESTAMP:
67
+ tf_batch_set_timestamp(ob, out_row, c, tf_batch_get_timestamp(in, r, c));
68
+ break;
69
+ default:
70
+ tf_batch_set_null(ob, out_row, c);
71
+ break;
72
+ }
73
+ }
74
+ out_row++;
75
+ }
76
+ ob->n_rows = out_row;
77
+
78
+ st->rows_in += in->n_rows;
79
+ st->rows_out += out_row;
80
+
81
+ /* Emit stats to side channel */
82
+ if (side && side->stats) {
83
+ char stats_buf[128];
84
+ snprintf(stats_buf, sizeof(stats_buf),
85
+ "{\"op\":\"filter\",\"rows_in\":%zu,\"rows_out\":%zu}\n",
86
+ in->n_rows, out_row);
87
+ tf_buffer_write_str(side->stats, stats_buf);
88
+ }
89
+
90
+ if (out_row > 0) {
91
+ *out = ob;
92
+ } else {
93
+ tf_batch_free(ob);
94
+ }
95
+ return TF_OK;
96
+ }
97
+
98
+ static int filter_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
99
+ (void)self; (void)side;
100
+ *out = NULL;
101
+ return TF_OK;
102
+ }
103
+
104
+ static void filter_destroy(tf_step *self) {
105
+ filter_state *st = self->state;
106
+ if (st) {
107
+ tf_expr_free(st->expr);
108
+ free(st);
109
+ }
110
+ free(self);
111
+ }
112
+
113
+ tf_step *tf_filter_create(const cJSON *args) {
114
+ if (!args) return NULL;
115
+ cJSON *expr_json = cJSON_GetObjectItemCaseSensitive(args, "expr");
116
+ if (!cJSON_IsString(expr_json)) return NULL;
117
+
118
+ tf_expr *expr = tf_expr_parse(expr_json->valuestring);
119
+ if (!expr) return NULL;
120
+
121
+ filter_state *st = calloc(1, sizeof(filter_state));
122
+ if (!st) { tf_expr_free(expr); return NULL; }
123
+ st->expr = expr;
124
+
125
+ tf_step *step = malloc(sizeof(tf_step));
126
+ if (!step) { tf_expr_free(expr); free(st); return NULL; }
127
+ step->process = filter_process;
128
+ step->flush = filter_flush;
129
+ step->destroy = filter_destroy;
130
+ step->state = st;
131
+ return step;
132
+ }