tranfi 0.1.2 → 0.2.1

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 (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +443 -51
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +352 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +81 -41
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
package/csrc/op_cast.c CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * op_cast.c — Type conversion.
2
+ * op_cast.c - Type conversion.
3
3
  *
4
4
  * Config: {"mapping": {"age": "int", "score": "float", "active": "bool"}}
5
5
  */
@@ -7,16 +7,80 @@
7
7
  #include "internal.h"
8
8
  #include "date_utils.h"
9
9
  #include "cJSON.h"
10
+ #include <errno.h>
10
11
  #include <stdlib.h>
11
12
  #include <string.h>
12
13
  #include <stdio.h>
13
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
+
14
23
  typedef struct {
15
24
  char **col_names;
16
25
  tf_type *target_types;
17
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;
18
35
  } cast_state;
19
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
+
20
84
  static tf_type parse_type(const char *s) {
21
85
  if (strcmp(s, "int") == 0 || strcmp(s, "int64") == 0) return TF_TYPE_INT64;
22
86
  if (strcmp(s, "float") == 0 || strcmp(s, "float64") == 0) return TF_TYPE_FLOAT64;
@@ -27,14 +91,125 @@ static tf_type parse_type(const char *s) {
27
91
  return TF_TYPE_NULL;
28
92
  }
29
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
+
30
205
  static int cast_process(tf_step *self, tf_batch *in, tf_batch **out,
31
206
  tf_side_channels *side) {
32
- (void)side;
33
207
  cast_state *st = self->state;
34
208
  *out = NULL;
209
+ size_t row_base = st->row_index;
35
210
 
36
- /* Determine output types */
37
- tf_type *out_types = malloc(in->n_cols * sizeof(tf_type));
211
+ tf_type *out_types = tf_mallocarray_checked(in->n_cols > 0 ? in->n_cols : 1,
212
+ sizeof(tf_type));
38
213
  if (!out_types) return TF_ERROR;
39
214
  for (size_t c = 0; c < in->n_cols; c++) out_types[c] = in->col_types[c];
40
215
  for (size_t k = 0; k < st->n; k++) {
@@ -42,84 +217,116 @@ static int cast_process(tf_step *self, tf_batch *in, tf_batch **out,
42
217
  if (ci >= 0) out_types[ci] = st->target_types[k];
43
218
  }
44
219
 
220
+ cast_audit_event *audit_events = NULL;
45
221
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
46
222
  if (!ob) { free(out_types); return TF_ERROR; }
47
- for (size_t c = 0; c < in->n_cols; c++)
48
- tf_batch_set_schema(ob, c, in->col_names[c], out_types[c]);
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
+ }
49
231
 
50
232
  for (size_t r = 0; r < in->n_rows; r++) {
51
- tf_batch_ensure_capacity(ob, r + 1);
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;
52
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
+ }
53
253
  if (tf_batch_is_null(in, r, c)) {
54
- tf_batch_set_null(ob, r, c);
254
+ if (tf_batch_set_null(ob, r, c) != TF_OK) goto fail;
55
255
  continue;
56
256
  }
57
257
  tf_type src_t = in->col_types[c];
58
258
  tf_type dst_t = out_types[c];
259
+ const char *failure_reason = NULL;
260
+ int write_rc = TF_OK;
59
261
 
60
262
  if (src_t == dst_t) {
61
- /* No conversion needed */
62
- switch (src_t) {
63
- case TF_TYPE_BOOL: tf_batch_set_bool(ob, r, c, tf_batch_get_bool(in, r, c)); break;
64
- case TF_TYPE_INT64: tf_batch_set_int64(ob, r, c, tf_batch_get_int64(in, r, c)); break;
65
- case TF_TYPE_FLOAT64: tf_batch_set_float64(ob, r, c, tf_batch_get_float64(in, r, c)); break;
66
- case TF_TYPE_STRING: tf_batch_set_string(ob, r, c, tf_batch_get_string(in, r, c)); break;
67
- case TF_TYPE_DATE: tf_batch_set_date(ob, r, c, tf_batch_get_date(in, r, c)); break;
68
- case TF_TYPE_TIMESTAMP: tf_batch_set_timestamp(ob, r, c, tf_batch_get_timestamp(in, r, c)); break;
69
- default: tf_batch_set_null(ob, r, c); break;
70
- }
263
+ if (tf_batch_copy_cell(ob, r, c, in, r, c) != TF_OK) goto fail;
71
264
  continue;
72
265
  }
73
266
 
74
- /* Conversion */
75
267
  if (dst_t == TF_TYPE_STRING) {
76
- char buf[64];
77
268
  switch (src_t) {
78
- case TF_TYPE_INT64: snprintf(buf, sizeof(buf), "%lld", (long long)tf_batch_get_int64(in, r, c)); break;
79
- case TF_TYPE_FLOAT64: snprintf(buf, sizeof(buf), "%g", tf_batch_get_float64(in, r, c)); break;
80
- case TF_TYPE_BOOL: snprintf(buf, sizeof(buf), "%s", tf_batch_get_bool(in, r, c) ? "true" : "false"); break;
81
- case TF_TYPE_DATE: tf_date_format(tf_batch_get_date(in, r, c), buf, sizeof(buf)); break;
82
- case TF_TYPE_TIMESTAMP: tf_timestamp_format(tf_batch_get_timestamp(in, r, c), buf, sizeof(buf)); break;
83
- default: buf[0] = '\0'; break;
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;
84
280
  }
85
- tf_batch_set_string(ob, r, c, buf);
86
281
  } else if (dst_t == TF_TYPE_INT64) {
87
282
  int64_t v = 0;
88
283
  if (src_t == TF_TYPE_FLOAT64) v = (int64_t)tf_batch_get_float64(in, r, c);
89
- else if (src_t == TF_TYPE_STRING) { char *end; v = strtoll(tf_batch_get_string(in, r, c), &end, 10); }
90
- else if (src_t == TF_TYPE_BOOL) v = tf_batch_get_bool(in, r, c) ? 1 : 0;
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;
91
289
  else if (src_t == TF_TYPE_TIMESTAMP) v = tf_batch_get_timestamp(in, r, c);
92
- tf_batch_set_int64(ob, r, c, v);
290
+ else failure_reason = "unsupported_conversion";
291
+ write_rc = tf_batch_set_int64(ob, r, c, v);
93
292
  } else if (dst_t == TF_TYPE_FLOAT64) {
94
293
  double v = 0;
95
294
  if (src_t == TF_TYPE_INT64) v = (double)tf_batch_get_int64(in, r, c);
96
- else if (src_t == TF_TYPE_STRING) { char *end; v = strtod(tf_batch_get_string(in, r, c), &end); }
97
- else if (src_t == TF_TYPE_BOOL) v = tf_batch_get_bool(in, r, c) ? 1.0 : 0.0;
98
- tf_batch_set_float64(ob, r, c, v);
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);
99
302
  } else if (dst_t == TF_TYPE_BOOL) {
100
303
  bool v = false;
101
304
  if (src_t == TF_TYPE_INT64) v = tf_batch_get_int64(in, r, c) != 0;
102
305
  else if (src_t == TF_TYPE_FLOAT64) v = tf_batch_get_float64(in, r, c) != 0.0;
103
- else if (src_t == TF_TYPE_STRING) v = strlen(tf_batch_get_string(in, r, c)) > 0 && strcmp(tf_batch_get_string(in, r, c), "false") != 0;
104
- tf_batch_set_bool(ob, r, c, v);
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);
105
312
  } else if (dst_t == TF_TYPE_DATE) {
106
313
  int32_t v = 0;
107
314
  if (src_t == TF_TYPE_STRING) {
108
315
  const char *s = tf_batch_get_string(in, r, c);
109
316
  int y, m, d;
110
- if (sscanf(s, "%d-%d-%d", &y, &m, &d) == 3)
317
+ if (s && sscanf(s, "%d-%d-%d", &y, &m, &d) == 3)
111
318
  v = tf_date_from_ymd(y, m, d);
319
+ strict_date_parse_ok(s, &failure_reason);
112
320
  } else if (src_t == TF_TYPE_TIMESTAMP) {
113
321
  v = (int32_t)(tf_batch_get_timestamp(in, r, c) / (86400LL * 1000000LL));
114
- }
115
- tf_batch_set_date(ob, r, c, v);
322
+ } else failure_reason = "unsupported_conversion";
323
+ write_rc = tf_batch_set_date(ob, r, c, v);
116
324
  } else if (dst_t == TF_TYPE_TIMESTAMP) {
117
325
  int64_t v = 0;
118
326
  if (src_t == TF_TYPE_STRING) {
119
327
  const char *s = tf_batch_get_string(in, r, c);
120
- size_t slen = strlen(s);
328
+ size_t slen = s ? strlen(s) : 0;
121
329
  int32_t dv;
122
- /* Try timestamp first, then date at midnight */
123
330
  if (slen >= 19) {
124
331
  int y, mo, d, h, mi, se;
125
332
  if (sscanf(s, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &se) == 6)
@@ -133,35 +340,116 @@ static int cast_process(tf_step *self, tf_batch *in, tf_batch **out,
133
340
  v = (int64_t)dv * 86400LL * 1000000LL;
134
341
  }
135
342
  }
343
+ strict_timestamp_parse_ok(s, &failure_reason);
136
344
  } else if (src_t == TF_TYPE_DATE) {
137
345
  v = (int64_t)tf_batch_get_date(in, r, c) * 86400LL * 1000000LL;
138
346
  } else if (src_t == TF_TYPE_INT64) {
139
347
  v = tf_batch_get_int64(in, r, c);
140
- }
141
- tf_batch_set_timestamp(ob, r, c, v);
348
+ } else failure_reason = "unsupported_conversion";
349
+ write_rc = tf_batch_set_timestamp(ob, r, c, v);
142
350
  } else {
143
- tf_batch_set_null(ob, r, c);
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;
144
407
  }
145
408
  }
146
- ob->n_rows = r + 1;
147
409
  }
148
410
 
411
+ if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) goto fail;
412
+ free(audit_events);
149
413
  free(out_types);
150
414
  *out = ob;
151
415
  return TF_OK;
416
+
417
+ fail:
418
+ free(audit_events);
419
+ tf_batch_free(ob);
420
+ free(out_types);
421
+ return TF_ERROR;
152
422
  }
153
423
 
154
424
  static int cast_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
155
425
  (void)self; (void)side; *out = NULL; return TF_OK;
156
426
  }
157
427
 
158
- static void cast_destroy(tf_step *self) {
159
- cast_state *st = self->state;
160
- if (st) {
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) {
161
444
  for (size_t i = 0; i < st->n; i++) free(st->col_names[i]);
162
- free(st->col_names); free(st->target_types);
163
- free(st);
164
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);
165
453
  free(self);
166
454
  }
167
455
 
@@ -173,23 +461,63 @@ tf_step *tf_cast_create(const cJSON *args) {
173
461
  int n = cJSON_GetArraySize(mapping);
174
462
  cast_state *st = calloc(1, sizeof(cast_state));
175
463
  if (!st) return NULL;
176
- st->col_names = calloc(n, sizeof(char *));
177
- st->target_types = calloc(n, sizeof(tf_type));
178
- st->n = n;
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; }
179
471
 
180
472
  int i = 0;
181
473
  cJSON *entry = NULL;
182
474
  cJSON_ArrayForEach(entry, mapping) {
183
- st->col_names[i] = strdup(entry->string);
475
+ if (!entry->string || !entry->string[0]) { cast_state_free(st); return NULL; }
476
+ st->col_names[i] = tf_strdup_checked(entry->string);
184
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; }
185
479
  i++;
186
480
  }
187
481
 
188
- tf_step *step = malloc(sizeof(tf_step));
189
- if (!step) { cast_destroy(&(tf_step){.state = st}); return NULL; }
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; }
190
517
  step->process = cast_process;
191
518
  step->flush = cast_flush;
192
519
  step->destroy = cast_destroy;
520
+ step->append_stats = cast_append_stats;
193
521
  step->state = st;
194
522
  return step;
195
523
  }
package/csrc/op_clip.c CHANGED
@@ -25,29 +25,39 @@ static int clip_process(tf_step *self, tf_batch *in, tf_batch **out,
25
25
 
26
26
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
27
27
  if (!ob) return TF_ERROR;
28
- for (size_t c = 0; c < in->n_cols; c++)
29
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
30
- for (size_t r = 0; r < in->n_rows; r++) {
31
- tf_batch_copy_row(ob, r, in, r);
32
- ob->n_rows = r + 1;
28
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
29
+ tf_batch_free(ob);
30
+ return TF_ERROR;
33
31
  }
34
-
35
- int ci = tf_batch_col_index(ob, st->column);
36
- if (ci >= 0) {
37
- for (size_t r = 0; r < ob->n_rows; r++) {
38
- if (tf_batch_is_null(ob, r, ci)) continue;
39
- if (ob->col_types[ci] == TF_TYPE_INT64) {
40
- int64_t v = tf_batch_get_int64(ob, r, ci);
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
41
  if (st->has_min && v < (int64_t)st->min_val) v = (int64_t)st->min_val;
42
42
  if (st->has_max && v > (int64_t)st->max_val) v = (int64_t)st->max_val;
43
- tf_batch_set_int64(ob, r, ci, v);
44
- } else if (ob->col_types[ci] == TF_TYPE_FLOAT64) {
45
- double v = tf_batch_get_float64(ob, r, ci);
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);
46
49
  if (st->has_min && v < st->min_val) v = st->min_val;
47
50
  if (st->has_max && v > st->max_val) v = st->max_val;
48
- tf_batch_set_float64(ob, r, ci, v);
51
+ if (tf_batch_set_float64(ob, r, (size_t)ci, v) != TF_OK) {
52
+ tf_batch_free(ob);
53
+ return TF_ERROR;
54
+ }
49
55
  }
50
56
  }
57
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
58
+ tf_batch_free(ob);
59
+ return TF_ERROR;
60
+ }
51
61
  }
52
62
 
53
63
  *out = ob;
@@ -69,16 +79,17 @@ tf_step *tf_clip_create(const cJSON *args) {
69
79
  cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
70
80
  if (!cJSON_IsString(col_j)) return NULL;
71
81
 
72
- clip_state *st = calloc(1, sizeof(clip_state));
82
+ clip_state *st = tf_callocarray_checked(1, sizeof(clip_state));
73
83
  if (!st) return NULL;
74
- st->column = strdup(col_j->valuestring);
84
+ st->column = tf_strdup_checked(col_j->valuestring);
85
+ if (!st->column) { free(st); return NULL; }
75
86
 
76
87
  cJSON *min_j = cJSON_GetObjectItemCaseSensitive(args, "min");
77
88
  if (cJSON_IsNumber(min_j)) { st->min_val = min_j->valuedouble; st->has_min = 1; }
78
89
  cJSON *max_j = cJSON_GetObjectItemCaseSensitive(args, "max");
79
90
  if (cJSON_IsNumber(max_j)) { st->max_val = max_j->valuedouble; st->has_max = 1; }
80
91
 
81
- tf_step *step = malloc(sizeof(tf_step));
92
+ tf_step *step = tf_callocarray_checked(1, sizeof(tf_step));
82
93
  if (!step) { free(st->column); free(st); return NULL; }
83
94
  step->process = clip_process;
84
95
  step->flush = clip_flush;