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_hash.c ADDED
@@ -0,0 +1,159 @@
1
+ /*
2
+ * op_hash.c — DJB2 hash of columns, adds _hash column.
3
+ *
4
+ * Config: {"columns": ["name", "city"]}
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
+ } hash_state;
19
+
20
+ static int hash_process(tf_step *self, tf_batch *in, tf_batch **out,
21
+ tf_side_channels *side) {
22
+ (void)side;
23
+ hash_state *st = self->state;
24
+ *out = NULL;
25
+
26
+ tf_batch *ob = tf_batch_create(in->n_cols + 1, in->n_rows);
27
+ if (!ob) return TF_ERROR;
28
+ const char *extra_names[] = {"_hash"};
29
+ const tf_type extra_types[] = {TF_TYPE_INT64};
30
+ if (tf_batch_clone_with_extra_cols(ob, in, extra_names, extra_types, 1) != TF_OK) {
31
+ tf_batch_free(ob);
32
+ return TF_ERROR;
33
+ }
34
+
35
+ /* Resolve column indices */
36
+ size_t n_keys;
37
+ int *col_indices;
38
+ if (st->n_cols > 0) {
39
+ n_keys = st->n_cols;
40
+ col_indices = malloc(n_keys * sizeof(int));
41
+ if (!col_indices) { tf_batch_free(ob); return TF_ERROR; }
42
+ for (size_t k = 0; k < n_keys; k++)
43
+ col_indices[k] = tf_batch_col_index(in, st->cols[k]);
44
+ } else {
45
+ n_keys = in->n_cols;
46
+ col_indices = malloc(n_keys * sizeof(int));
47
+ if (!col_indices) { tf_batch_free(ob); return TF_ERROR; }
48
+ for (size_t k = 0; k < n_keys; k++) col_indices[k] = (int)k;
49
+ }
50
+
51
+ for (size_t r = 0; r < in->n_rows; r++) {
52
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
53
+ free(col_indices);
54
+ tf_batch_free(ob);
55
+ return TF_ERROR;
56
+ }
57
+ /* Compute hash */
58
+ uint32_t h = 5381;
59
+ char val_buf[64];
60
+ for (size_t k = 0; k < n_keys; k++) {
61
+ int ci = col_indices[k];
62
+ if (ci < 0 || tf_batch_is_null(in, r, ci)) continue;
63
+ const char *val;
64
+ switch (in->col_types[ci]) {
65
+ case TF_TYPE_STRING: val = tf_batch_get_string(in, r, ci); break;
66
+ case TF_TYPE_INT64:
67
+ snprintf(val_buf, sizeof(val_buf), "%lld", (long long)tf_batch_get_int64(in, r, ci));
68
+ val = val_buf; break;
69
+ case TF_TYPE_FLOAT64:
70
+ snprintf(val_buf, sizeof(val_buf), "%.17g", tf_batch_get_float64(in, r, ci));
71
+ val = val_buf; break;
72
+ case TF_TYPE_BOOL:
73
+ val = tf_batch_get_bool(in, r, ci) ? "T" : "F"; break;
74
+ case TF_TYPE_DATE:
75
+ snprintf(val_buf, sizeof(val_buf), "%d", (int)tf_batch_get_date(in, r, ci));
76
+ val = val_buf; break;
77
+ case TF_TYPE_TIMESTAMP:
78
+ snprintf(val_buf, sizeof(val_buf), "%lld", (long long)tf_batch_get_timestamp(in, r, ci));
79
+ val = val_buf; break;
80
+ default: val = ""; break;
81
+ }
82
+ for (const unsigned char *p = (const unsigned char *)val; *p; p++)
83
+ h = ((h << 5) + h) ^ *p;
84
+ }
85
+ if (tf_batch_set_int64(ob, r, in->n_cols, (int64_t)h) != TF_OK) {
86
+ free(col_indices);
87
+ tf_batch_free(ob);
88
+ return TF_ERROR;
89
+ }
90
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
91
+ free(col_indices);
92
+ tf_batch_free(ob);
93
+ return TF_ERROR;
94
+ }
95
+ }
96
+
97
+ free(col_indices);
98
+ *out = ob;
99
+ return TF_OK;
100
+ }
101
+
102
+ static int hash_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
103
+ (void)self; (void)side; *out = NULL; return TF_OK;
104
+ }
105
+
106
+ static void hash_state_free(hash_state *st) {
107
+ if (st) {
108
+ if (st->cols) {
109
+ for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
110
+ }
111
+ free(st->cols);
112
+ free(st);
113
+ }
114
+ }
115
+
116
+ static void hash_destroy(tf_step *self) {
117
+ hash_state_free(self ? self->state : NULL);
118
+ free(self);
119
+ }
120
+
121
+ tf_step *tf_hash_create(const cJSON *args) {
122
+ hash_state *st = calloc(1, sizeof(hash_state));
123
+ if (!st) return NULL;
124
+
125
+ if (args) {
126
+ cJSON *columns = cJSON_GetObjectItemCaseSensitive(args, "columns");
127
+ if (columns) {
128
+ if (!cJSON_IsArray(columns)) {
129
+ tf_set_last_error("hash: columns must be an array");
130
+ hash_state_free(st);
131
+ return NULL;
132
+ }
133
+ int n = cJSON_GetArraySize(columns);
134
+ if (n > 0) {
135
+ st->cols = calloc(n, sizeof(char *));
136
+ if (!st->cols) { hash_state_free(st); return NULL; }
137
+ st->n_cols = (size_t)n;
138
+ for (int i = 0; i < n; i++) {
139
+ cJSON *item = cJSON_GetArrayItem(columns, i);
140
+ if (!cJSON_IsString(item) || !item->valuestring || !item->valuestring[0]) {
141
+ tf_set_last_error("hash: column names must be non-empty strings");
142
+ hash_state_free(st);
143
+ return NULL;
144
+ }
145
+ st->cols[i] = strdup(item->valuestring);
146
+ if (!st->cols[i]) { hash_state_free(st); return NULL; }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ tf_step *step = calloc(1, sizeof(tf_step));
153
+ if (!step) { hash_state_free(st); return NULL; }
154
+ step->process = hash_process;
155
+ step->flush = hash_flush;
156
+ step->destroy = hash_destroy;
157
+ step->state = st;
158
+ return step;
159
+ }
package/csrc/op_head.c ADDED
@@ -0,0 +1,84 @@
1
+ /*
2
+ * op_head.c — Take first N rows.
3
+ *
4
+ * Config: {"n": 5}
5
+ * Passes through rows until N have been seen, then emits empty batches.
6
+ */
7
+
8
+ #include "internal.h"
9
+ #include "cJSON.h"
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+
13
+ typedef struct {
14
+ size_t limit;
15
+ size_t seen;
16
+ } head_state;
17
+
18
+ static int head_process(tf_step *self, tf_batch *in, tf_batch **out,
19
+ tf_side_channels *side) {
20
+ (void)side;
21
+ head_state *st = self->state;
22
+ *out = NULL;
23
+
24
+ if (st->seen >= st->limit) {
25
+ /* Already past limit, discard */
26
+ return TF_OK;
27
+ }
28
+
29
+ size_t remaining = st->limit - st->seen;
30
+ size_t take = in->n_rows < remaining ? in->n_rows : remaining;
31
+ if (take == 0) return TF_OK;
32
+
33
+ tf_batch *ob = tf_batch_create(in->n_cols, take);
34
+ if (!ob) return TF_ERROR;
35
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
36
+ tf_batch_free(ob);
37
+ return TF_ERROR;
38
+ }
39
+ for (size_t r = 0; r < take; r++) {
40
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
41
+ tf_batch_free(ob);
42
+ return TF_ERROR;
43
+ }
44
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
45
+ tf_batch_free(ob);
46
+ return TF_ERROR;
47
+ }
48
+ }
49
+
50
+ st->seen += take;
51
+ *out = ob;
52
+ return TF_OK;
53
+ }
54
+
55
+ static int head_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
56
+ (void)self; (void)side;
57
+ *out = NULL;
58
+ return TF_OK;
59
+ }
60
+
61
+ static void head_destroy(tf_step *self) {
62
+ free(self->state);
63
+ free(self);
64
+ }
65
+
66
+ tf_step *tf_head_create(const cJSON *args) {
67
+ if (!args) return NULL;
68
+ size_t n = 0;
69
+ int has_n = tf_json_get_size_arg(args, "n", 0, TF_MAX_COUNT_ARG, &n, "head");
70
+ if (has_n <= 0) return NULL;
71
+
72
+ head_state *st = calloc(1, sizeof(head_state));
73
+ if (!st) return NULL;
74
+ st->limit = n;
75
+ st->seen = 0;
76
+
77
+ tf_step *step = calloc(1, sizeof(tf_step));
78
+ if (!step) { free(st); return NULL; }
79
+ step->process = head_process;
80
+ step->flush = head_flush;
81
+ step->destroy = head_destroy;
82
+ step->state = st;
83
+ return step;
84
+ }
@@ -0,0 +1,445 @@
1
+ /*
2
+ * op_interpolate.c — Fill null values via interpolation.
3
+ * Methods: forward, backward, linear.
4
+ *
5
+ * For backward/linear: buffers rows with null target values and emits
6
+ * them when the next non-null value arrives.
7
+ *
8
+ * Config: {"column": "price", "method": "linear"}
9
+ */
10
+
11
+ #include "internal.h"
12
+ #include "cJSON.h"
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+ #include <stdio.h>
16
+
17
+ typedef enum {
18
+ INTERP_FORWARD,
19
+ INTERP_BACKWARD,
20
+ INTERP_LINEAR
21
+ } interp_method;
22
+
23
+ typedef enum {
24
+ INTERP_MISSING_ERROR,
25
+ INTERP_MISSING_NULL,
26
+ INTERP_MISSING_IGNORE,
27
+ } interp_missing_policy;
28
+
29
+ typedef enum {
30
+ INTERP_TYPE_FAIL,
31
+ INTERP_TYPE_NULL,
32
+ } interp_type_policy;
33
+
34
+ /* Buffered row: we store complete batches and track which rows are pending */
35
+ typedef struct pending_row {
36
+ tf_batch *batch; /* single-row batch (copy of original row) */
37
+ size_t target_col; /* column index in the batch */
38
+ } pending_row;
39
+
40
+ typedef struct {
41
+ char *column;
42
+ interp_method method;
43
+ double last_val;
44
+ int has_last;
45
+ interp_missing_policy missing;
46
+ interp_type_policy on_type_error;
47
+ /* Pending null rows (for backward/linear) */
48
+ pending_row *pending;
49
+ size_t n_pending;
50
+ size_t cap_pending;
51
+ } interpolate_state;
52
+
53
+ static int parse_method(const char *s, interp_method *out) {
54
+ if (!s) { *out = INTERP_LINEAR; return TF_OK; }
55
+ if (strcmp(s, "forward") == 0) { *out = INTERP_FORWARD; return TF_OK; }
56
+ if (strcmp(s, "backward") == 0) { *out = INTERP_BACKWARD; return TF_OK; }
57
+ if (strcmp(s, "linear") == 0) { *out = INTERP_LINEAR; return TF_OK; }
58
+ return TF_ERROR;
59
+ }
60
+
61
+ static int interpolate_is_numeric_type(tf_type type) {
62
+ return type == TF_TYPE_INT64 || type == TF_TYPE_FLOAT64;
63
+ }
64
+
65
+ static double interpolate_get_numeric(const tf_batch *b, size_t r, int ci) {
66
+ if (b->col_types[ci] == TF_TYPE_INT64) return (double)tf_batch_get_int64(b, r, ci);
67
+ return tf_batch_get_float64(b, r, ci);
68
+ }
69
+
70
+ static int interpolate_set_numeric(tf_batch *b, size_t r, size_t ci, double val) {
71
+ if (b->col_types[ci] == TF_TYPE_INT64)
72
+ return tf_batch_set_int64(b, r, ci, (int64_t)val);
73
+ if (b->col_types[ci] == TF_TYPE_FLOAT64)
74
+ return tf_batch_set_float64(b, r, ci, val);
75
+ return TF_ERROR;
76
+ }
77
+
78
+ static void interpolate_set_col_error(const char *column, const char *suffix) {
79
+ char msg[512];
80
+ snprintf(msg, sizeof(msg), "interpolate: column '%s' %s", column ? column : "", suffix);
81
+ tf_set_last_error(msg);
82
+ }
83
+
84
+ static int interpolate_parse_missing_policy(const cJSON *args, interp_missing_policy *out) {
85
+ *out = INTERP_MISSING_ERROR;
86
+ const cJSON *j = cJSON_GetObjectItemCaseSensitive(args, "missing");
87
+ if (!j) return TF_OK;
88
+ if (!cJSON_IsString(j)) {
89
+ tf_set_last_error("interpolate: missing must be error, null, or ignore");
90
+ return TF_ERROR;
91
+ }
92
+ if (strcmp(j->valuestring, "error") == 0) *out = INTERP_MISSING_ERROR;
93
+ else if (strcmp(j->valuestring, "null") == 0) *out = INTERP_MISSING_NULL;
94
+ else if (strcmp(j->valuestring, "ignore") == 0) *out = INTERP_MISSING_IGNORE;
95
+ else {
96
+ tf_set_last_error("interpolate: missing must be error, null, or ignore");
97
+ return TF_ERROR;
98
+ }
99
+ return TF_OK;
100
+ }
101
+
102
+ static int interpolate_parse_type_policy(const cJSON *args, interp_type_policy *out) {
103
+ *out = INTERP_TYPE_FAIL;
104
+ const cJSON *j = cJSON_GetObjectItemCaseSensitive(args, "on_type_error");
105
+ if (!j) return TF_OK;
106
+ if (!cJSON_IsString(j)) {
107
+ tf_set_last_error("interpolate: on_type_error must be fail or null");
108
+ return TF_ERROR;
109
+ }
110
+ if (strcmp(j->valuestring, "fail") == 0) *out = INTERP_TYPE_FAIL;
111
+ else if (strcmp(j->valuestring, "null") == 0) *out = INTERP_TYPE_NULL;
112
+ else {
113
+ tf_set_last_error("interpolate: on_type_error must be fail or null");
114
+ return TF_ERROR;
115
+ }
116
+ return TF_OK;
117
+ }
118
+
119
+ static int interpolate_passthrough(tf_batch *in, tf_batch **out) {
120
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
121
+ if (!ob) return TF_ERROR;
122
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
123
+ tf_batch_free(ob);
124
+ return TF_ERROR;
125
+ }
126
+ for (size_t r = 0; r < in->n_rows; r++) {
127
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
128
+ tf_batch_free(ob);
129
+ return TF_ERROR;
130
+ }
131
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
132
+ tf_batch_free(ob);
133
+ return TF_ERROR;
134
+ }
135
+ }
136
+ *out = ob;
137
+ return TF_OK;
138
+ }
139
+
140
+ static int interpolate_null_output(interpolate_state *st, tf_batch *in, int ci, tf_batch **out) {
141
+ size_t result_col = ci < 0 ? in->n_cols : (size_t)ci;
142
+ tf_batch *ob = NULL;
143
+ if (ci < 0) {
144
+ const char *extra_names[1] = {st->column};
145
+ tf_type extra_types[1] = {TF_TYPE_FLOAT64};
146
+ ob = tf_batch_create(in->n_cols + 1, in->n_rows);
147
+ if (!ob) return TF_ERROR;
148
+ if (tf_batch_clone_with_extra_cols(ob, in, extra_names, extra_types, 1) != TF_OK) {
149
+ tf_batch_free(ob);
150
+ return TF_ERROR;
151
+ }
152
+ } else {
153
+ ob = tf_batch_create(in->n_cols, in->n_rows);
154
+ if (!ob) return TF_ERROR;
155
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
156
+ tf_batch_free(ob);
157
+ return TF_ERROR;
158
+ }
159
+ }
160
+
161
+ for (size_t r = 0; r < in->n_rows; r++) {
162
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK ||
163
+ tf_batch_set_null(ob, r, result_col) != TF_OK) {
164
+ tf_batch_free(ob);
165
+ return TF_ERROR;
166
+ }
167
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
168
+ tf_batch_free(ob);
169
+ return TF_ERROR;
170
+ }
171
+ }
172
+ *out = ob;
173
+ return TF_OK;
174
+ }
175
+
176
+ static int add_pending(interpolate_state *st, tf_batch *row_batch, size_t target_col) {
177
+ size_t next_pending = 0;
178
+ if (tf_size_add(st->n_pending, 1, &next_pending) != TF_OK) return TF_ERROR;
179
+ if (next_pending > st->cap_pending) {
180
+ size_t newcap = 0;
181
+ if (tf_size_grow_pow2(st->cap_pending, next_pending, 16, &newcap) != TF_OK) {
182
+ return TF_ERROR;
183
+ }
184
+ pending_row *tmp = tf_reallocarray_checked(st->pending, newcap, sizeof(pending_row));
185
+ if (!tmp) return TF_ERROR;
186
+ st->pending = tmp;
187
+ st->cap_pending = newcap;
188
+ }
189
+ st->pending[st->n_pending].batch = row_batch;
190
+ st->pending[st->n_pending].target_col = target_col;
191
+ st->n_pending = next_pending;
192
+ return TF_OK;
193
+ }
194
+
195
+ static tf_batch *copy_single_row(const tf_batch *in, size_t row) {
196
+ tf_batch *row_copy = tf_batch_create(in->n_cols, 1);
197
+ if (!row_copy) return NULL;
198
+ if (tf_batch_clone_schema(row_copy, in) != TF_OK ||
199
+ tf_batch_copy_row(row_copy, 0, in, row) != TF_OK) {
200
+ tf_batch_free(row_copy);
201
+ return NULL;
202
+ }
203
+ if (tf_batch_expose_row(row_copy, 0) != TF_OK) {
204
+ tf_batch_free(row_copy);
205
+ return NULL;
206
+ }
207
+ return row_copy;
208
+ }
209
+
210
+ /* Emit all pending rows into output batch, interpolating values */
211
+ static int flush_pending(interpolate_state *st, tf_batch *ob, size_t *out_row,
212
+ double end_val, size_t target_col) {
213
+ if (st->n_pending == 0) return TF_OK;
214
+
215
+ for (size_t i = 0; i < st->n_pending; i++) {
216
+ tf_batch *pb = st->pending[i].batch;
217
+ size_t r = *out_row;
218
+ if (tf_batch_copy_row(ob, r, pb, 0) != TF_OK) return TF_ERROR;
219
+
220
+ double interp_val;
221
+ if (st->method == INTERP_BACKWARD) {
222
+ interp_val = end_val;
223
+ } else {
224
+ /* Linear: interpolate between last_val and end_val */
225
+ if (st->has_last) {
226
+ double t = (double)(i + 1) / (double)(st->n_pending + 1);
227
+ interp_val = st->last_val + t * (end_val - st->last_val);
228
+ } else {
229
+ interp_val = end_val;
230
+ }
231
+ }
232
+
233
+ if (interpolate_set_numeric(ob, r, target_col, interp_val) != TF_OK) return TF_ERROR;
234
+ if (tf_batch_expose_row(ob, r) != TF_OK) return TF_ERROR;
235
+ (*out_row)++;
236
+ }
237
+
238
+ for (size_t i = 0; i < st->n_pending; i++) {
239
+ tf_batch_free(st->pending[i].batch);
240
+ st->pending[i].batch = NULL;
241
+ }
242
+ st->n_pending = 0;
243
+ return TF_OK;
244
+ }
245
+
246
+ static int interpolate_process(tf_step *self, tf_batch *in, tf_batch **out,
247
+ tf_side_channels *side) {
248
+ (void)side;
249
+ interpolate_state *st = self->state;
250
+ *out = NULL;
251
+
252
+ int ci = tf_batch_col_index(in, st->column);
253
+ int force_null = 0;
254
+ if (ci < 0) {
255
+ if (st->missing == INTERP_MISSING_ERROR) {
256
+ interpolate_set_col_error(st->column, "not found");
257
+ return TF_ERROR;
258
+ }
259
+ if (st->missing == INTERP_MISSING_IGNORE) return interpolate_passthrough(in, out);
260
+ force_null = 1;
261
+ } else if (!interpolate_is_numeric_type(in->col_types[ci])) {
262
+ if (st->on_type_error == INTERP_TYPE_FAIL) {
263
+ interpolate_set_col_error(st->column, "must be numeric");
264
+ return TF_ERROR;
265
+ }
266
+ force_null = 1;
267
+ }
268
+ if (force_null) return interpolate_null_output(st, in, ci, out);
269
+
270
+ size_t max_rows = 0;
271
+ if (tf_size_add(st->n_pending, in->n_rows, &max_rows) != TF_OK) return TF_ERROR;
272
+ tf_batch *ob = tf_batch_create(in->n_cols, max_rows);
273
+ if (!ob) return TF_ERROR;
274
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
275
+ tf_batch_free(ob);
276
+ return TF_ERROR;
277
+ }
278
+
279
+ size_t out_row = 0;
280
+
281
+ for (size_t r = 0; r < in->n_rows; r++) {
282
+ int is_null = tf_batch_is_null(in, r, ci);
283
+
284
+ if (is_null) {
285
+ if (st->method == INTERP_FORWARD && st->has_last) {
286
+ /* Forward fill: use last known value */
287
+ if (tf_batch_copy_row(ob, out_row, in, r) != TF_OK ||
288
+ interpolate_set_numeric(ob, out_row, (size_t)ci, st->last_val) != TF_OK) {
289
+ tf_batch_free(ob);
290
+ return TF_ERROR;
291
+ }
292
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) {
293
+ tf_batch_free(ob);
294
+ return TF_ERROR;
295
+ }
296
+ out_row++;
297
+ } else if (st->method == INTERP_FORWARD) {
298
+ /* No previous value yet — pass null through */
299
+ if (tf_batch_copy_row(ob, out_row, in, r) != TF_OK) {
300
+ tf_batch_free(ob);
301
+ return TF_ERROR;
302
+ }
303
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) {
304
+ tf_batch_free(ob);
305
+ return TF_ERROR;
306
+ }
307
+ out_row++;
308
+ } else {
309
+ /* Backward/linear: buffer this row */
310
+ tf_batch *row_copy = copy_single_row(in, r);
311
+ if (!row_copy || add_pending(st, row_copy, (size_t)ci) != TF_OK) {
312
+ tf_batch_free(row_copy);
313
+ tf_batch_free(ob);
314
+ return TF_ERROR;
315
+ }
316
+ }
317
+ } else {
318
+ double val = interpolate_get_numeric(in, r, ci);
319
+
320
+ /* Flush any pending rows */
321
+ if (st->n_pending > 0) {
322
+ size_t needed = 0;
323
+ if (tf_size_add(out_row, st->n_pending, &needed) != TF_OK ||
324
+ tf_size_add(needed, 1, &needed) != TF_OK ||
325
+ tf_batch_ensure_capacity(ob, needed) != TF_OK ||
326
+ flush_pending(st, ob, &out_row, val, (size_t)ci) != TF_OK) {
327
+ tf_batch_free(ob);
328
+ return TF_ERROR;
329
+ }
330
+ }
331
+
332
+ /* Output current row */
333
+ if (tf_batch_copy_row(ob, out_row, in, r) != TF_OK) {
334
+ tf_batch_free(ob);
335
+ return TF_ERROR;
336
+ }
337
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) {
338
+ tf_batch_free(ob);
339
+ return TF_ERROR;
340
+ }
341
+ out_row++;
342
+
343
+ st->last_val = val;
344
+ st->has_last = 1;
345
+ }
346
+ }
347
+
348
+ if (ob->n_rows == 0) {
349
+ tf_batch_free(ob);
350
+ *out = NULL;
351
+ } else {
352
+ *out = ob;
353
+ }
354
+ return TF_OK;
355
+ }
356
+
357
+ static int interpolate_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
358
+ (void)side;
359
+ interpolate_state *st = self->state;
360
+ *out = NULL;
361
+
362
+ /* Emit any remaining pending rows as nulls (or forward-fill with last known) */
363
+ if (st->n_pending > 0) {
364
+ tf_batch *first = st->pending[0].batch;
365
+ tf_batch *ob = tf_batch_create(first->n_cols, st->n_pending);
366
+ if (!ob) return TF_ERROR;
367
+ if (tf_batch_clone_schema(ob, first) != TF_OK) {
368
+ tf_batch_free(ob);
369
+ return TF_ERROR;
370
+ }
371
+
372
+ for (size_t i = 0; i < st->n_pending; i++) {
373
+ tf_batch *pb = st->pending[i].batch;
374
+ if (tf_batch_copy_row(ob, i, pb, 0) != TF_OK) {
375
+ tf_batch_free(ob);
376
+ return TF_ERROR;
377
+ }
378
+ /* For linear/backward at end of stream: use last known if available */
379
+ if (st->has_last && interpolate_set_numeric(ob, i, st->pending[i].target_col, st->last_val) != TF_OK) {
380
+ tf_batch_free(ob);
381
+ return TF_ERROR;
382
+ }
383
+ if (tf_batch_expose_row(ob, i) != TF_OK) {
384
+ tf_batch_free(ob);
385
+ return TF_ERROR;
386
+ }
387
+ }
388
+ for (size_t i = 0; i < st->n_pending; i++) {
389
+ tf_batch_free(st->pending[i].batch);
390
+ st->pending[i].batch = NULL;
391
+ }
392
+ st->n_pending = 0;
393
+ *out = ob;
394
+ }
395
+
396
+ return TF_OK;
397
+ }
398
+
399
+ static void interpolate_destroy(tf_step *self) {
400
+ interpolate_state *st = self->state;
401
+ if (st) {
402
+ for (size_t i = 0; i < st->n_pending; i++)
403
+ tf_batch_free(st->pending[i].batch);
404
+ free(st->pending);
405
+ free(st->column);
406
+ free(st);
407
+ }
408
+ free(self);
409
+ }
410
+
411
+ tf_step *tf_interpolate_create(const cJSON *args) {
412
+ if (!args) return NULL;
413
+ cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
414
+ if (!cJSON_IsString(col_j) || !col_j->valuestring[0]) {
415
+ tf_set_last_error("interpolate: column is required");
416
+ return NULL;
417
+ }
418
+
419
+ interpolate_state *st = calloc(1, sizeof(interpolate_state));
420
+ if (!st) return NULL;
421
+ st->column = strdup(col_j->valuestring);
422
+ if (!st->column) { free(st); return NULL; }
423
+
424
+ cJSON *method_j = cJSON_GetObjectItemCaseSensitive(args, "method");
425
+ if (parse_method(cJSON_IsString(method_j) ? method_j->valuestring : NULL, &st->method) != TF_OK) {
426
+ tf_set_last_error("interpolate: method must be forward, backward, or linear");
427
+ free(st->column);
428
+ free(st);
429
+ return NULL;
430
+ }
431
+ if (interpolate_parse_missing_policy(args, &st->missing) != TF_OK ||
432
+ interpolate_parse_type_policy(args, &st->on_type_error) != TF_OK) {
433
+ free(st->column);
434
+ free(st);
435
+ return NULL;
436
+ }
437
+
438
+ tf_step *step = calloc(1, sizeof(tf_step));
439
+ if (!step) { free(st->column); free(st); return NULL; }
440
+ step->process = interpolate_process;
441
+ step->flush = interpolate_flush;
442
+ step->destroy = interpolate_destroy;
443
+ step->state = st;
444
+ return step;
445
+ }