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_cast.c ADDED
@@ -0,0 +1,523 @@
1
+ /*
2
+ * op_cast.c - Type conversion.
3
+ *
4
+ * Config: {"mapping": {"age": "int", "score": "float", "active": "bool"}}
5
+ */
6
+
7
+ #include "internal.h"
8
+ #include "date_utils.h"
9
+ #include "cJSON.h"
10
+ #include <errno.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <stdio.h>
14
+
15
+ /* Cast is row-local. Audit records are opt-in and capped so parse/coercion
16
+ * diagnostics cannot become an unbounded second output stream. */
17
+ typedef enum {
18
+ CAST_ON_ERROR_COERCE = 0,
19
+ CAST_ON_ERROR_FAIL,
20
+ CAST_ON_ERROR_NULL,
21
+ } cast_error_policy;
22
+
23
+ typedef struct {
24
+ char **col_names;
25
+ tf_type *target_types;
26
+ size_t n;
27
+ size_t row_index;
28
+ size_t audit_limit;
29
+ size_t audit_emitted;
30
+ size_t coercion_failures;
31
+ size_t coercion_nulled;
32
+ int audit;
33
+ cast_error_policy on_error;
34
+ tf_audit_options audit_opts;
35
+ } cast_state;
36
+
37
+ typedef struct {
38
+ size_t col;
39
+ tf_type src_t;
40
+ tf_type dst_t;
41
+ const char *event;
42
+ const char *reason;
43
+ } cast_audit_event;
44
+
45
+ static const char *cast_error_policy_name(cast_error_policy p) {
46
+ switch (p) {
47
+ case CAST_ON_ERROR_COERCE: return "coerce";
48
+ case CAST_ON_ERROR_FAIL: return "fail";
49
+ case CAST_ON_ERROR_NULL: return "null";
50
+ default: return "coerce";
51
+ }
52
+ }
53
+
54
+ static int parse_cast_error_policy(const char *s, cast_error_policy *out) {
55
+ if (!s || !s[0] || strcmp(s, "coerce") == 0 || strcmp(s, "legacy") == 0 ||
56
+ strcmp(s, "default") == 0) {
57
+ *out = CAST_ON_ERROR_COERCE;
58
+ return 1;
59
+ }
60
+ if (strcmp(s, "fail") == 0 || strcmp(s, "error") == 0 || strcmp(s, "strict") == 0) {
61
+ *out = CAST_ON_ERROR_FAIL;
62
+ return 1;
63
+ }
64
+ if (strcmp(s, "null") == 0 || strcmp(s, "nulling") == 0) {
65
+ *out = CAST_ON_ERROR_NULL;
66
+ return 1;
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ static const char *cast_type_name(tf_type t) {
72
+ switch (t) {
73
+ case TF_TYPE_BOOL: return "bool";
74
+ case TF_TYPE_INT64: return "int";
75
+ case TF_TYPE_FLOAT64: return "float";
76
+ case TF_TYPE_STRING: return "string";
77
+ case TF_TYPE_DATE: return "date";
78
+ case TF_TYPE_TIMESTAMP: return "timestamp";
79
+ case TF_TYPE_NULL: return "null";
80
+ default: return "unknown";
81
+ }
82
+ }
83
+
84
+ static tf_type parse_type(const char *s) {
85
+ if (strcmp(s, "int") == 0 || strcmp(s, "int64") == 0) return TF_TYPE_INT64;
86
+ if (strcmp(s, "float") == 0 || strcmp(s, "float64") == 0) return TF_TYPE_FLOAT64;
87
+ if (strcmp(s, "string") == 0 || strcmp(s, "str") == 0) return TF_TYPE_STRING;
88
+ if (strcmp(s, "bool") == 0 || strcmp(s, "boolean") == 0) return TF_TYPE_BOOL;
89
+ if (strcmp(s, "date") == 0) return TF_TYPE_DATE;
90
+ if (strcmp(s, "timestamp") == 0 || strcmp(s, "datetime") == 0) return TF_TYPE_TIMESTAMP;
91
+ return TF_TYPE_NULL;
92
+ }
93
+
94
+ static int only_ascii_ws(const char *s) {
95
+ if (!s) return 1;
96
+ while (*s) {
97
+ if (*s != ' ' && *s != '\t' && *s != '\n' && *s != '\r' && *s != '\f' && *s != '\v') return 0;
98
+ s++;
99
+ }
100
+ return 1;
101
+ }
102
+
103
+ static int strict_int_parse_ok(const char *s, const char **reason) {
104
+ char *end = NULL;
105
+ errno = 0;
106
+ (void)strtoll(s ? s : "", &end, 10);
107
+ if (!s || end == s) { if (reason) *reason = "invalid_integer"; return 0; }
108
+ if (errno == ERANGE) { if (reason) *reason = "integer_out_of_range"; return 0; }
109
+ if (!only_ascii_ws(end)) { if (reason) *reason = "trailing_characters"; return 0; }
110
+ return 1;
111
+ }
112
+
113
+ static int strict_float_parse_ok(const char *s, const char **reason) {
114
+ char *end = NULL;
115
+ errno = 0;
116
+ (void)strtod(s ? s : "", &end);
117
+ if (!s || end == s) { if (reason) *reason = "invalid_float"; return 0; }
118
+ if (errno == ERANGE) { if (reason) *reason = "float_out_of_range"; return 0; }
119
+ if (!only_ascii_ws(end)) { if (reason) *reason = "trailing_characters"; return 0; }
120
+ return 1;
121
+ }
122
+
123
+ static int strict_date_parse_ok(const char *s, const char **reason) {
124
+ int y, m, d, nread = 0;
125
+ if (s && sscanf(s, "%d-%d-%d%n", &y, &m, &d, &nread) == 3) {
126
+ if (only_ascii_ws(s + nread)) return 1;
127
+ if (reason) *reason = "trailing_characters";
128
+ return 0;
129
+ }
130
+ if (reason) *reason = "invalid_date";
131
+ return 0;
132
+ }
133
+
134
+ static int strict_timestamp_parse_ok(const char *s, const char **reason) {
135
+ int y, mo, d, h, mi, se, nread = 0;
136
+ if (s && sscanf(s, "%d-%d-%dT%d:%d:%d%n", &y, &mo, &d, &h, &mi, &se, &nread) == 6) {
137
+ if (only_ascii_ws(s + nread)) return 1;
138
+ if (reason) *reason = "trailing_characters";
139
+ return 0;
140
+ }
141
+ nread = 0;
142
+ if (s && sscanf(s, "%d-%d-%d %d:%d:%d%n", &y, &mo, &d, &h, &mi, &se, &nread) == 6) {
143
+ if (only_ascii_ws(s + nread)) return 1;
144
+ if (reason) *reason = "trailing_characters";
145
+ return 0;
146
+ }
147
+ nread = 0;
148
+ if (s && sscanf(s, "%d-%d-%d%n", &y, &mo, &d, &nread) == 3) {
149
+ if (only_ascii_ws(s + nread)) return 1;
150
+ if (reason) *reason = "trailing_characters";
151
+ return 0;
152
+ }
153
+ if (reason) *reason = "invalid_timestamp";
154
+ return 0;
155
+ }
156
+
157
+ static int emit_cast_audit(cast_state *st, const tf_batch *before_b, const tf_batch *after_b,
158
+ size_t row, size_t col, size_t row_no, tf_type src_t, tf_type dst_t,
159
+ const char *event, const char *reason, tf_side_channels *side) {
160
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
161
+ cJSON *obj = cJSON_CreateObject();
162
+ if (!obj) return TF_ERROR;
163
+ int rc = TF_ERROR;
164
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
165
+ tf_json_add_string(obj, "op", "cast") != TF_OK ||
166
+ tf_json_add_string(obj, "event", event ? event : "value_changed") != TF_OK ||
167
+ tf_json_add_string(obj, "reason", reason ? reason : "type_cast") != TF_OK ||
168
+ tf_json_add_string(obj, "channel", "audit") != TF_OK) {
169
+ goto done;
170
+ }
171
+ const char *column_name = before_b->col_names[col] ? before_b->col_names[col] : "";
172
+ if (tf_json_add_string(obj, "column", column_name) != TF_OK ||
173
+ tf_json_add_string(obj, "from_type", cast_type_name(src_t)) != TF_OK ||
174
+ tf_json_add_string(obj, "to_type", cast_type_name(dst_t)) != TF_OK ||
175
+ tf_json_add_string(obj, "expected", cast_type_name(dst_t)) != TF_OK ||
176
+ tf_json_add_string(obj, "on_error", cast_error_policy_name(st->on_error)) != TF_OK) {
177
+ goto done;
178
+ }
179
+ if (src_t == TF_TYPE_STRING && !tf_batch_is_null(before_b, row, col)) {
180
+ if (tf_json_add_audit_string(obj, "actual", &st->audit_opts, column_name,
181
+ tf_batch_get_string(before_b, row, col)) != TF_OK)
182
+ goto done;
183
+ }
184
+ if (tf_json_add_string(obj, "action", "cast") != TF_OK ||
185
+ tf_json_add_number(obj, "row", (double)row_no) != TF_OK) {
186
+ goto done;
187
+ }
188
+ cJSON *before = tf_audit_cell_to_json(before_b, row, col, &st->audit_opts);
189
+ if (!before || tf_json_add_item(obj, "before", before) != TF_OK) goto done;
190
+ cJSON *after = tf_audit_cell_to_json(after_b, row, col, &st->audit_opts);
191
+ if (!after || tf_json_add_item(obj, "after", after) != TF_OK) goto done;
192
+ cJSON *row_obj = tf_audit_row_to_json(after_b, row, &st->audit_opts);
193
+ if (row_obj) {
194
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
195
+ } else if (st->audit_opts.include_row) {
196
+ goto done;
197
+ }
198
+ rc = tf_buffer_write_json_line(side->stats, obj);
199
+ done:
200
+ cJSON_Delete(obj);
201
+ if (rc == TF_OK) st->audit_emitted++;
202
+ return rc;
203
+ }
204
+
205
+ static int cast_process(tf_step *self, tf_batch *in, tf_batch **out,
206
+ tf_side_channels *side) {
207
+ cast_state *st = self->state;
208
+ *out = NULL;
209
+ size_t row_base = st->row_index;
210
+
211
+ tf_type *out_types = tf_mallocarray_checked(in->n_cols > 0 ? in->n_cols : 1,
212
+ sizeof(tf_type));
213
+ if (!out_types) return TF_ERROR;
214
+ for (size_t c = 0; c < in->n_cols; c++) out_types[c] = in->col_types[c];
215
+ for (size_t k = 0; k < st->n; k++) {
216
+ int ci = tf_batch_col_index(in, st->col_names[k]);
217
+ if (ci >= 0) out_types[ci] = st->target_types[k];
218
+ }
219
+
220
+ cast_audit_event *audit_events = NULL;
221
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
222
+ if (!ob) { free(out_types); return TF_ERROR; }
223
+ for (size_t c = 0; c < in->n_cols; c++) {
224
+ if (tf_batch_set_schema(ob, c, in->col_names[c], out_types[c]) != TF_OK) goto fail;
225
+ }
226
+ if (st->audit) {
227
+ audit_events = tf_callocarray_checked(in->n_cols > 0 ? in->n_cols : 1,
228
+ sizeof(cast_audit_event));
229
+ if (!audit_events) goto fail;
230
+ }
231
+
232
+ for (size_t r = 0; r < in->n_rows; r++) {
233
+ size_t needed_rows = 0;
234
+ if (tf_size_add(r, 1, &needed_rows) != TF_OK ||
235
+ tf_batch_ensure_capacity(ob, needed_rows) != TF_OK) {
236
+ goto fail;
237
+ }
238
+ size_t audit_count = 0;
239
+ int row_failed = 0;
240
+ size_t row_failed_col = 0;
241
+ const char *row_failed_reason = NULL;
242
+ for (size_t c = 0; c < in->n_cols; c++) {
243
+ if (row_failed) {
244
+ if (tf_batch_is_null(in, r, c)) {
245
+ if (tf_batch_set_null(ob, r, c) != TF_OK) goto fail;
246
+ } else if (in->col_types[c] == out_types[c]) {
247
+ if (tf_batch_copy_cell(ob, r, c, in, r, c) != TF_OK) goto fail;
248
+ } else if (tf_batch_set_null(ob, r, c) != TF_OK) {
249
+ goto fail;
250
+ }
251
+ continue;
252
+ }
253
+ if (tf_batch_is_null(in, r, c)) {
254
+ if (tf_batch_set_null(ob, r, c) != TF_OK) goto fail;
255
+ continue;
256
+ }
257
+ tf_type src_t = in->col_types[c];
258
+ tf_type dst_t = out_types[c];
259
+ const char *failure_reason = NULL;
260
+ int write_rc = TF_OK;
261
+
262
+ if (src_t == dst_t) {
263
+ if (tf_batch_copy_cell(ob, r, c, in, r, c) != TF_OK) goto fail;
264
+ continue;
265
+ }
266
+
267
+ if (dst_t == TF_TYPE_STRING) {
268
+ switch (src_t) {
269
+ case TF_TYPE_INT64:
270
+ case TF_TYPE_FLOAT64:
271
+ case TF_TYPE_BOOL:
272
+ case TF_TYPE_DATE:
273
+ case TF_TYPE_TIMESTAMP:
274
+ write_rc = tf_batch_copy_cell_as_string(ob, r, c, in, r, c);
275
+ break;
276
+ default:
277
+ failure_reason = "unsupported_conversion";
278
+ write_rc = tf_batch_set_string(ob, r, c, "");
279
+ break;
280
+ }
281
+ } else if (dst_t == TF_TYPE_INT64) {
282
+ int64_t v = 0;
283
+ if (src_t == TF_TYPE_FLOAT64) v = (int64_t)tf_batch_get_float64(in, r, c);
284
+ else if (src_t == TF_TYPE_STRING) {
285
+ const char *s = tf_batch_get_string(in, r, c);
286
+ v = strtoll(s ? s : "", NULL, 10);
287
+ strict_int_parse_ok(s, &failure_reason);
288
+ } else if (src_t == TF_TYPE_BOOL) v = tf_batch_get_bool(in, r, c) ? 1 : 0;
289
+ else if (src_t == TF_TYPE_TIMESTAMP) v = tf_batch_get_timestamp(in, r, c);
290
+ else failure_reason = "unsupported_conversion";
291
+ write_rc = tf_batch_set_int64(ob, r, c, v);
292
+ } else if (dst_t == TF_TYPE_FLOAT64) {
293
+ double v = 0;
294
+ if (src_t == TF_TYPE_INT64) v = (double)tf_batch_get_int64(in, r, c);
295
+ else if (src_t == TF_TYPE_STRING) {
296
+ const char *s = tf_batch_get_string(in, r, c);
297
+ v = strtod(s ? s : "", NULL);
298
+ strict_float_parse_ok(s, &failure_reason);
299
+ } else if (src_t == TF_TYPE_BOOL) v = tf_batch_get_bool(in, r, c) ? 1.0 : 0.0;
300
+ else failure_reason = "unsupported_conversion";
301
+ write_rc = tf_batch_set_float64(ob, r, c, v);
302
+ } else if (dst_t == TF_TYPE_BOOL) {
303
+ bool v = false;
304
+ if (src_t == TF_TYPE_INT64) v = tf_batch_get_int64(in, r, c) != 0;
305
+ else if (src_t == TF_TYPE_FLOAT64) v = tf_batch_get_float64(in, r, c) != 0.0;
306
+ else if (src_t == TF_TYPE_STRING) {
307
+ const char *s = tf_batch_get_string(in, r, c);
308
+ v = s && strlen(s) > 0 && strcmp(s, "false") != 0;
309
+ if (!s || (strcmp(s, "true") != 0 && strcmp(s, "false") != 0)) failure_reason = "non_canonical_bool";
310
+ } else failure_reason = "unsupported_conversion";
311
+ write_rc = tf_batch_set_bool(ob, r, c, v);
312
+ } else if (dst_t == TF_TYPE_DATE) {
313
+ int32_t v = 0;
314
+ if (src_t == TF_TYPE_STRING) {
315
+ const char *s = tf_batch_get_string(in, r, c);
316
+ int y, m, d;
317
+ if (s && sscanf(s, "%d-%d-%d", &y, &m, &d) == 3)
318
+ v = tf_date_from_ymd(y, m, d);
319
+ strict_date_parse_ok(s, &failure_reason);
320
+ } else if (src_t == TF_TYPE_TIMESTAMP) {
321
+ v = (int32_t)(tf_batch_get_timestamp(in, r, c) / (86400LL * 1000000LL));
322
+ } else failure_reason = "unsupported_conversion";
323
+ write_rc = tf_batch_set_date(ob, r, c, v);
324
+ } else if (dst_t == TF_TYPE_TIMESTAMP) {
325
+ int64_t v = 0;
326
+ if (src_t == TF_TYPE_STRING) {
327
+ const char *s = tf_batch_get_string(in, r, c);
328
+ size_t slen = s ? strlen(s) : 0;
329
+ int32_t dv;
330
+ if (slen >= 19) {
331
+ int y, mo, d, h, mi, se;
332
+ if (sscanf(s, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &se) == 6)
333
+ v = tf_timestamp_from_parts(y, mo, d, h, mi, se, 0);
334
+ else if (sscanf(s, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &se) == 6)
335
+ v = tf_timestamp_from_parts(y, mo, d, h, mi, se, 0);
336
+ } else if (slen == 10) {
337
+ int y, m, d;
338
+ if (sscanf(s, "%d-%d-%d", &y, &m, &d) == 3) {
339
+ dv = tf_date_from_ymd(y, m, d);
340
+ v = (int64_t)dv * 86400LL * 1000000LL;
341
+ }
342
+ }
343
+ strict_timestamp_parse_ok(s, &failure_reason);
344
+ } else if (src_t == TF_TYPE_DATE) {
345
+ v = (int64_t)tf_batch_get_date(in, r, c) * 86400LL * 1000000LL;
346
+ } else if (src_t == TF_TYPE_INT64) {
347
+ v = tf_batch_get_int64(in, r, c);
348
+ } else failure_reason = "unsupported_conversion";
349
+ write_rc = tf_batch_set_timestamp(ob, r, c, v);
350
+ } else {
351
+ failure_reason = dst_t == TF_TYPE_NULL ? "invalid_target_type" : "unsupported_conversion";
352
+ write_rc = tf_batch_set_null(ob, r, c);
353
+ }
354
+ if (write_rc != TF_OK) goto fail;
355
+
356
+ if (failure_reason) {
357
+ st->coercion_failures++;
358
+ if (st->on_error == CAST_ON_ERROR_NULL || st->on_error == CAST_ON_ERROR_FAIL) {
359
+ if (tf_batch_set_null(ob, r, c) != TF_OK) goto fail;
360
+ if (st->on_error == CAST_ON_ERROR_NULL) st->coercion_nulled++;
361
+ }
362
+ }
363
+
364
+ if (st->audit) {
365
+ const char *event = failure_reason ? "coercion_failed" : "value_changed";
366
+ const char *reason = failure_reason ? failure_reason : "type_cast";
367
+ if (st->audit_emitted < st->audit_limit &&
368
+ audit_count < st->audit_limit - st->audit_emitted) {
369
+ audit_events[audit_count++] = (cast_audit_event){
370
+ .col = c,
371
+ .src_t = src_t,
372
+ .dst_t = dst_t,
373
+ .event = event,
374
+ .reason = reason,
375
+ };
376
+ }
377
+ }
378
+
379
+ if (failure_reason && st->on_error == CAST_ON_ERROR_FAIL) {
380
+ row_failed = 1;
381
+ row_failed_col = c;
382
+ row_failed_reason = failure_reason;
383
+ }
384
+ }
385
+ if (tf_batch_expose_row(ob, r) != TF_OK) goto fail;
386
+ if (audit_count > 0 || row_failed) {
387
+ size_t row_no = 0;
388
+ if (tf_size_add(row_base, r, &row_no) != TF_OK ||
389
+ tf_size_add(row_no, 1, &row_no) != TF_OK) {
390
+ goto fail;
391
+ }
392
+ for (size_t i = 0; i < audit_count; i++) {
393
+ cast_audit_event *ev = &audit_events[i];
394
+ if (emit_cast_audit(st, in, ob, r, ev->col, row_no, ev->src_t,
395
+ ev->dst_t, ev->event, ev->reason, side) != TF_OK) {
396
+ goto fail;
397
+ }
398
+ }
399
+ if (row_failed) {
400
+ char msg[512];
401
+ snprintf(msg, sizeof(msg), "cast failed at row %zu column '%s': %s",
402
+ row_no,
403
+ in->col_names[row_failed_col] ? in->col_names[row_failed_col] : "",
404
+ row_failed_reason ? row_failed_reason : "type_cast");
405
+ tf_set_last_error(msg);
406
+ goto fail;
407
+ }
408
+ }
409
+ }
410
+
411
+ if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) goto fail;
412
+ free(audit_events);
413
+ free(out_types);
414
+ *out = ob;
415
+ return TF_OK;
416
+
417
+ fail:
418
+ free(audit_events);
419
+ tf_batch_free(ob);
420
+ free(out_types);
421
+ return TF_ERROR;
422
+ }
423
+
424
+ static int cast_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
425
+ (void)self; (void)side; *out = NULL; return TF_OK;
426
+ }
427
+
428
+ static int cast_append_stats(tf_step *self, tf_buffer *out) {
429
+ if (!self || !self->state || !out) return TF_ERROR;
430
+ cast_state *st = (cast_state *)self->state;
431
+ char buf[256];
432
+ snprintf(buf, sizeof(buf),
433
+ ",\"on_error\":\"%s\",\"coercion_failures\":%zu,"
434
+ "\"coercion_nulled\":%zu,\"audit_emitted\":%zu",
435
+ cast_error_policy_name(st->on_error), st->coercion_failures,
436
+ st->coercion_nulled, st->audit_emitted);
437
+ return tf_buffer_write_str(out, buf);
438
+ }
439
+
440
+ static void cast_state_free(cast_state *st) {
441
+ if (!st) return;
442
+ tf_audit_options_free(&st->audit_opts);
443
+ if (st->col_names) {
444
+ for (size_t i = 0; i < st->n; i++) free(st->col_names[i]);
445
+ }
446
+ free(st->col_names);
447
+ free(st->target_types);
448
+ free(st);
449
+ }
450
+
451
+ static void cast_destroy(tf_step *self) {
452
+ if (self) cast_state_free(self->state);
453
+ free(self);
454
+ }
455
+
456
+ tf_step *tf_cast_create(const cJSON *args) {
457
+ if (!args) return NULL;
458
+ cJSON *mapping = cJSON_GetObjectItemCaseSensitive(args, "mapping");
459
+ if (!mapping || !cJSON_IsObject(mapping)) return NULL;
460
+
461
+ int n = cJSON_GetArraySize(mapping);
462
+ cast_state *st = calloc(1, sizeof(cast_state));
463
+ if (!st) return NULL;
464
+ tf_audit_options_init(&st->audit_opts, 1);
465
+ st->col_names = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(char *));
466
+ st->target_types = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(tf_type));
467
+ st->n = (size_t)n;
468
+ st->audit_limit = 1000;
469
+ st->on_error = CAST_ON_ERROR_COERCE;
470
+ if (!st->col_names || !st->target_types) { cast_state_free(st); return NULL; }
471
+
472
+ int i = 0;
473
+ cJSON *entry = NULL;
474
+ cJSON_ArrayForEach(entry, mapping) {
475
+ if (!entry->string || !entry->string[0]) { cast_state_free(st); return NULL; }
476
+ st->col_names[i] = tf_strdup_checked(entry->string);
477
+ st->target_types[i] = cJSON_IsString(entry) ? parse_type(entry->valuestring) : TF_TYPE_NULL;
478
+ if (!st->col_names[i]) { cast_state_free(st); return NULL; }
479
+ i++;
480
+ }
481
+
482
+ cJSON *on_error_j = cJSON_GetObjectItemCaseSensitive(args, "on_error");
483
+ if (!on_error_j) on_error_j = cJSON_GetObjectItemCaseSensitive(args, "onError");
484
+ if (!on_error_j) on_error_j = cJSON_GetObjectItemCaseSensitive(args, "on_coercion_error");
485
+ if (!on_error_j) on_error_j = cJSON_GetObjectItemCaseSensitive(args, "onCoercionError");
486
+ if (on_error_j) {
487
+ if (!cJSON_IsString(on_error_j) ||
488
+ !parse_cast_error_policy(on_error_j->valuestring, &st->on_error)) {
489
+ tf_set_last_error("cast: on_error must be coerce, fail, or null");
490
+ cast_state_free(st);
491
+ return NULL;
492
+ }
493
+ }
494
+
495
+ cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
496
+ st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
497
+ cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
498
+ if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
499
+ if (audit_limit_j) {
500
+ size_t parsed_limit = 0;
501
+ if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
502
+ 1, TF_MAX_AUDIT_RECORDS,
503
+ &parsed_limit, "cast") < 0) {
504
+ cast_state_free(st);
505
+ return NULL;
506
+ }
507
+ st->audit_limit = parsed_limit;
508
+ }
509
+
510
+ if (tf_audit_options_parse(&st->audit_opts, args, "cast") != TF_OK) {
511
+ cast_state_free(st);
512
+ return NULL;
513
+ }
514
+
515
+ tf_step *step = calloc(1, sizeof(tf_step));
516
+ if (!step) { cast_state_free(st); return NULL; }
517
+ step->process = cast_process;
518
+ step->flush = cast_flush;
519
+ step->destroy = cast_destroy;
520
+ step->append_stats = cast_append_stats;
521
+ step->state = st;
522
+ return step;
523
+ }
package/csrc/op_clip.c ADDED
@@ -0,0 +1,99 @@
1
+ /*
2
+ * op_clip.c — Clamp numeric values to [min, max].
3
+ *
4
+ * Config: {"column": "score", "min": 0, "max": 100}
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
+ double min_val;
15
+ double max_val;
16
+ int has_min;
17
+ int has_max;
18
+ } clip_state;
19
+
20
+ static int clip_process(tf_step *self, tf_batch *in, tf_batch **out,
21
+ tf_side_channels *side) {
22
+ (void)side;
23
+ clip_state *st = self->state;
24
+ *out = NULL;
25
+
26
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
27
+ if (!ob) return TF_ERROR;
28
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
29
+ tf_batch_free(ob);
30
+ return TF_ERROR;
31
+ }
32
+ int ci = tf_batch_col_index(in, st->column);
33
+ for (size_t r = 0; r < in->n_rows; r++) {
34
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
35
+ tf_batch_free(ob);
36
+ return TF_ERROR;
37
+ }
38
+ if (ci >= 0 && !tf_batch_is_null(in, r, (size_t)ci)) {
39
+ if (in->col_types[ci] == TF_TYPE_INT64) {
40
+ int64_t v = tf_batch_get_int64(in, r, (size_t)ci);
41
+ if (st->has_min && v < (int64_t)st->min_val) v = (int64_t)st->min_val;
42
+ if (st->has_max && v > (int64_t)st->max_val) v = (int64_t)st->max_val;
43
+ if (tf_batch_set_int64(ob, r, (size_t)ci, v) != TF_OK) {
44
+ tf_batch_free(ob);
45
+ return TF_ERROR;
46
+ }
47
+ } else if (in->col_types[ci] == TF_TYPE_FLOAT64) {
48
+ double v = tf_batch_get_float64(in, r, (size_t)ci);
49
+ if (st->has_min && v < st->min_val) v = st->min_val;
50
+ if (st->has_max && v > st->max_val) v = st->max_val;
51
+ if (tf_batch_set_float64(ob, r, (size_t)ci, v) != TF_OK) {
52
+ tf_batch_free(ob);
53
+ return TF_ERROR;
54
+ }
55
+ }
56
+ }
57
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
58
+ tf_batch_free(ob);
59
+ return TF_ERROR;
60
+ }
61
+ }
62
+
63
+ *out = ob;
64
+ return TF_OK;
65
+ }
66
+
67
+ static int clip_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
68
+ (void)self; (void)side; *out = NULL; return TF_OK;
69
+ }
70
+
71
+ static void clip_destroy(tf_step *self) {
72
+ clip_state *st = self->state;
73
+ if (st) { free(st->column); free(st); }
74
+ free(self);
75
+ }
76
+
77
+ tf_step *tf_clip_create(const cJSON *args) {
78
+ if (!args) return NULL;
79
+ cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
80
+ if (!cJSON_IsString(col_j)) return NULL;
81
+
82
+ clip_state *st = tf_callocarray_checked(1, sizeof(clip_state));
83
+ if (!st) return NULL;
84
+ st->column = tf_strdup_checked(col_j->valuestring);
85
+ if (!st->column) { free(st); return NULL; }
86
+
87
+ cJSON *min_j = cJSON_GetObjectItemCaseSensitive(args, "min");
88
+ if (cJSON_IsNumber(min_j)) { st->min_val = min_j->valuedouble; st->has_min = 1; }
89
+ cJSON *max_j = cJSON_GetObjectItemCaseSensitive(args, "max");
90
+ if (cJSON_IsNumber(max_j)) { st->max_val = max_j->valuedouble; st->has_max = 1; }
91
+
92
+ tf_step *step = tf_callocarray_checked(1, sizeof(tf_step));
93
+ if (!step) { free(st->column); free(st); return NULL; }
94
+ step->process = clip_process;
95
+ step->flush = clip_flush;
96
+ step->destroy = clip_destroy;
97
+ step->state = st;
98
+ return step;
99
+ }