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_rename.c CHANGED
@@ -17,6 +17,7 @@ typedef struct {
17
17
  } rename_state;
18
18
 
19
19
  static const char *find_rename(rename_state *st, const char *name) {
20
+ if (!name) return "";
20
21
  for (size_t i = 0; i < st->n_mappings; i++) {
21
22
  if (strcmp(st->old_names[i], name) == 0)
22
23
  return st->new_names[i];
@@ -28,6 +29,7 @@ static int rename_process(tf_step *self, tf_batch *in, tf_batch **out,
28
29
  tf_side_channels *side) {
29
30
  (void)side;
30
31
  rename_state *st = self->state;
32
+ *out = NULL;
31
33
 
32
34
  /* Create output batch with renamed columns */
33
35
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
@@ -35,42 +37,23 @@ static int rename_process(tf_step *self, tf_batch *in, tf_batch **out,
35
37
 
36
38
  for (size_t i = 0; i < in->n_cols; i++) {
37
39
  const char *new_name = find_rename(st, in->col_names[i]);
38
- tf_batch_set_schema(ob, i, new_name, in->col_types[i]);
40
+ if (tf_batch_set_schema(ob, i, new_name, in->col_types[i]) != TF_OK) {
41
+ tf_batch_free(ob);
42
+ return TF_ERROR;
43
+ }
39
44
  }
40
45
 
41
- /* Copy all rows */
42
- for (size_t r = 0; r < in->n_rows; r++) {
43
- tf_batch_ensure_capacity(ob, r + 1);
44
- for (size_t c = 0; c < in->n_cols; c++) {
45
- if (tf_batch_is_null(in, r, c)) {
46
- tf_batch_set_null(ob, r, c);
47
- continue;
48
- }
49
- switch (in->col_types[c]) {
50
- case TF_TYPE_BOOL:
51
- tf_batch_set_bool(ob, r, c, tf_batch_get_bool(in, r, c));
52
- break;
53
- case TF_TYPE_INT64:
54
- tf_batch_set_int64(ob, r, c, tf_batch_get_int64(in, r, c));
55
- break;
56
- case TF_TYPE_FLOAT64:
57
- tf_batch_set_float64(ob, r, c, tf_batch_get_float64(in, r, c));
58
- break;
59
- case TF_TYPE_STRING:
60
- tf_batch_set_string(ob, r, c, tf_batch_get_string(in, r, c));
61
- break;
62
- case TF_TYPE_DATE:
63
- tf_batch_set_date(ob, r, c, tf_batch_get_date(in, r, c));
64
- break;
65
- case TF_TYPE_TIMESTAMP:
66
- tf_batch_set_timestamp(ob, r, c, tf_batch_get_timestamp(in, r, c));
67
- break;
68
- default:
69
- tf_batch_set_null(ob, r, c);
70
- break;
71
- }
72
- }
73
- ob->n_rows = r + 1;
46
+ size_t *cols = tf_mallocarray_checked(in->n_cols ? in->n_cols : 1, sizeof(size_t));
47
+ if (!cols) {
48
+ tf_batch_free(ob);
49
+ return TF_ERROR;
50
+ }
51
+ for (size_t i = 0; i < in->n_cols; i++) cols[i] = i;
52
+ int rc = tf_batch_copy_selected_columns(ob, in, cols, in->n_cols);
53
+ free(cols);
54
+ if (rc != TF_OK) {
55
+ tf_batch_free(ob);
56
+ return TF_ERROR;
74
57
  }
75
58
 
76
59
  *out = ob;
@@ -83,17 +66,19 @@ static int rename_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
83
66
  return TF_OK;
84
67
  }
85
68
 
86
- static void rename_destroy(tf_step *self) {
87
- rename_state *st = self->state;
88
- if (st) {
89
- for (size_t i = 0; i < st->n_mappings; i++) {
90
- free(st->old_names[i]);
91
- free(st->new_names[i]);
92
- }
93
- free(st->old_names);
94
- free(st->new_names);
95
- free(st);
69
+ static void rename_state_free(rename_state *st) {
70
+ if (!st) return;
71
+ for (size_t i = 0; i < st->n_mappings; i++) {
72
+ if (st->old_names) free(st->old_names[i]);
73
+ if (st->new_names) free(st->new_names[i]);
96
74
  }
75
+ free(st->old_names);
76
+ free(st->new_names);
77
+ free(st);
78
+ }
79
+
80
+ static void rename_destroy(tf_step *self) {
81
+ rename_state_free(self ? self->state : NULL);
97
82
  free(self);
98
83
  }
99
84
 
@@ -108,8 +93,8 @@ tf_step *tf_rename_create(const cJSON *args) {
108
93
  rename_state *st = calloc(1, sizeof(rename_state));
109
94
  if (!st) return NULL;
110
95
  st->n_mappings = (size_t)n;
111
- st->old_names = malloc(n * sizeof(char *));
112
- st->new_names = malloc(n * sizeof(char *));
96
+ st->old_names = calloc((size_t)n, sizeof(char *));
97
+ st->new_names = calloc((size_t)n, sizeof(char *));
113
98
  if (!st->old_names || !st->new_names) {
114
99
  free(st->old_names);
115
100
  free(st->new_names);
@@ -120,14 +105,20 @@ tf_step *tf_rename_create(const cJSON *args) {
120
105
  int i = 0;
121
106
  cJSON *item;
122
107
  cJSON_ArrayForEach(item, mapping) {
123
- st->old_names[i] = strdup(item->string);
124
- st->new_names[i] = strdup(cJSON_IsString(item) ? item->valuestring : item->string);
108
+ const char *old_name = item->string ? item->string : "";
109
+ const char *new_name = cJSON_IsString(item) ? item->valuestring : old_name;
110
+ st->old_names[i] = strdup(old_name);
111
+ st->new_names[i] = strdup(new_name ? new_name : "");
112
+ if (!st->old_names[i] || !st->new_names[i]) {
113
+ rename_state_free(st);
114
+ return NULL;
115
+ }
125
116
  i++;
126
117
  }
127
118
 
128
- tf_step *step = malloc(sizeof(tf_step));
119
+ tf_step *step = calloc(1, sizeof(tf_step));
129
120
  if (!step) {
130
- rename_destroy(&(tf_step){.state = st});
121
+ rename_state_free(st);
131
122
  return NULL;
132
123
  }
133
124
  step->process = rename_process;
package/csrc/op_replace.c CHANGED
@@ -17,132 +17,259 @@ typedef struct {
17
17
  char *replacement;
18
18
  int use_regex;
19
19
  regex_t compiled;
20
+ size_t row_index;
21
+ size_t audit_limit;
22
+ size_t audit_emitted;
23
+ int audit;
24
+ tf_audit_options audit_opts;
20
25
  } replace_state;
21
26
 
27
+ static int replace_ensure_buffer(char **buf, size_t *cap, size_t need) {
28
+ if (!buf || !cap) return TF_ERROR;
29
+ if (need > 0) {
30
+ size_t payload_len = need - 1; /* reserve space for the trailing NUL */
31
+ if (tf_check_byte_limit(payload_len, TF_MAX_CELL_BYTES, "replace", "output cell") != TF_OK) {
32
+ return TF_ERROR;
33
+ }
34
+ }
35
+ if (need <= *cap) return TF_OK;
36
+ size_t new_cap = 0;
37
+ if (tf_size_grow_pow2(*cap, need, 64, &new_cap) != TF_OK) return TF_ERROR;
38
+ size_t max_cap = 0;
39
+ if (tf_size_add(TF_MAX_CELL_BYTES, 1, &max_cap) != TF_OK) return TF_ERROR;
40
+ if (new_cap > max_cap) new_cap = need;
41
+ char *nb = tf_reallocarray_checked(*buf, new_cap, sizeof(char));
42
+ if (!nb) return TF_ERROR;
43
+ *buf = nb;
44
+ *cap = new_cap;
45
+ return TF_OK;
46
+ }
47
+
48
+ static int replace_append_bytes(char **buf, size_t *cap, size_t *len,
49
+ const char *src, size_t n) {
50
+ if (!buf || !cap || !len || (!src && n > 0)) return TF_ERROR;
51
+ size_t end = 0;
52
+ size_t need = 0;
53
+ if (tf_size_add(*len, n, &end) != TF_OK ||
54
+ tf_size_add(end, 1, &need) != TF_OK) {
55
+ return TF_ERROR;
56
+ }
57
+ if (replace_ensure_buffer(buf, cap, need) != TF_OK) return TF_ERROR;
58
+ if (n > 0) memcpy(*buf + *len, src, n);
59
+ *len = end;
60
+ (*buf)[*len] = '\0';
61
+ return TF_OK;
62
+ }
63
+
64
+ static int replace_append_char(char **buf, size_t *cap, size_t *len, char ch) {
65
+ return replace_append_bytes(buf, cap, len, &ch, 1);
66
+ }
67
+
68
+ static int emit_replace_audit(replace_state *st, const tf_batch *b, size_t row, size_t col,
69
+ size_t row_no, const char *before, const char *after,
70
+ tf_side_channels *side) {
71
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
72
+ cJSON *obj = cJSON_CreateObject();
73
+ if (!obj) return TF_ERROR;
74
+ int rc = TF_ERROR;
75
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
76
+ tf_json_add_string(obj, "op", "replace") != TF_OK ||
77
+ tf_json_add_string(obj, "event", "value_changed") != TF_OK ||
78
+ tf_json_add_string(obj, "reason", "replace_match") != TF_OK ||
79
+ tf_json_add_string(obj, "channel", "audit") != TF_OK) {
80
+ goto done;
81
+ }
82
+ const char *column_name = b->col_names[col] ? b->col_names[col] : "";
83
+ if (tf_json_add_string(obj, "column", column_name) != TF_OK) goto done;
84
+ if (tf_json_add_audit_string(obj, "pattern", &st->audit_opts, column_name, st->pattern) != TF_OK ||
85
+ tf_json_add_audit_string(obj, "replacement", &st->audit_opts, column_name, st->replacement) != TF_OK ||
86
+ tf_json_add_bool(obj, "regex", st->use_regex ? 1 : 0) != TF_OK ||
87
+ tf_json_add_number(obj, "row", (double)row_no) != TF_OK) {
88
+ goto done;
89
+ }
90
+ if (tf_json_add_audit_string(obj, "before", &st->audit_opts, column_name, before) != TF_OK ||
91
+ tf_json_add_audit_string(obj, "after", &st->audit_opts, column_name, after) != TF_OK) {
92
+ goto done;
93
+ }
94
+ cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
95
+ if (row_obj) {
96
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
97
+ } else if (st->audit_opts.include_row) {
98
+ goto done;
99
+ }
100
+ rc = tf_buffer_write_json_line(side->stats, obj);
101
+ done:
102
+ cJSON_Delete(obj);
103
+ if (rc == TF_OK) st->audit_emitted++;
104
+ return rc;
105
+ }
106
+
107
+ static int replace_build_regex(replace_state *st, const char *val,
108
+ char **out_buf, int *out_replaced) {
109
+ *out_buf = NULL;
110
+ *out_replaced = 0;
111
+ size_t buf_cap = 0;
112
+ size_t buf_len = 0;
113
+ char *buf = NULL;
114
+ const char *p = val;
115
+ regmatch_t match;
116
+ while (regexec(&st->compiled, p, 1, &match, 0) == 0 && match.rm_so >= 0) {
117
+ *out_replaced = 1;
118
+ size_t prefix = (size_t)match.rm_so;
119
+ size_t match_len = (size_t)(match.rm_eo - match.rm_so);
120
+ if (replace_append_bytes(&buf, &buf_cap, &buf_len, p, prefix) != TF_OK) {
121
+ free(buf);
122
+ return TF_ERROR;
123
+ }
124
+ for (const char *rp = st->replacement; *rp; rp++) {
125
+ if (*rp == '&') {
126
+ if (replace_append_bytes(&buf, &buf_cap, &buf_len,
127
+ p + match.rm_so, match_len) != TF_OK) {
128
+ free(buf);
129
+ return TF_ERROR;
130
+ }
131
+ } else if (replace_append_char(&buf, &buf_cap, &buf_len, *rp) != TF_OK) {
132
+ free(buf);
133
+ return TF_ERROR;
134
+ }
135
+ }
136
+ p += match.rm_eo;
137
+ if (match_len == 0) {
138
+ if (*p) {
139
+ if (replace_append_char(&buf, &buf_cap, &buf_len, *p++) != TF_OK) {
140
+ free(buf);
141
+ return TF_ERROR;
142
+ }
143
+ } else {
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ if (*out_replaced) {
149
+ if (replace_append_bytes(&buf, &buf_cap, &buf_len, p, strlen(p)) != TF_OK) {
150
+ free(buf);
151
+ return TF_ERROR;
152
+ }
153
+ *out_buf = buf;
154
+ }
155
+ return TF_OK;
156
+ }
157
+
158
+ static int replace_build_literal(replace_state *st, const char *val,
159
+ char **out_buf, int *out_replaced) {
160
+ *out_buf = NULL;
161
+ *out_replaced = 0;
162
+ size_t pat_len = strlen(st->pattern);
163
+ if (pat_len == 0 || !strstr(val, st->pattern)) return TF_OK;
164
+
165
+ size_t rep_len = strlen(st->replacement);
166
+ size_t buf_cap = 0;
167
+ size_t buf_len = 0;
168
+ char *buf = NULL;
169
+ const char *p = val;
170
+ const char *found;
171
+ while ((found = strstr(p, st->pattern)) != NULL) {
172
+ size_t prefix = (size_t)(found - p);
173
+ if (replace_append_bytes(&buf, &buf_cap, &buf_len, p, prefix) != TF_OK ||
174
+ replace_append_bytes(&buf, &buf_cap, &buf_len, st->replacement, rep_len) != TF_OK) {
175
+ free(buf);
176
+ return TF_ERROR;
177
+ }
178
+ p = found + pat_len;
179
+ }
180
+ if (replace_append_bytes(&buf, &buf_cap, &buf_len, p, strlen(p)) != TF_OK) {
181
+ free(buf);
182
+ return TF_ERROR;
183
+ }
184
+ *out_replaced = 1;
185
+ *out_buf = buf;
186
+ return TF_OK;
187
+ }
188
+
22
189
  static int replace_process(tf_step *self, tf_batch *in, tf_batch **out,
23
190
  tf_side_channels *side) {
24
- (void)side;
25
191
  replace_state *st = self->state;
26
192
  *out = NULL;
193
+ size_t row_base = st->row_index;
27
194
 
28
195
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
29
196
  if (!ob) return TF_ERROR;
30
- for (size_t c = 0; c < in->n_cols; c++)
31
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
197
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
198
+ tf_batch_free(ob);
199
+ return TF_ERROR;
200
+ }
201
+
202
+ int ci = tf_batch_col_index(in, st->column);
203
+ int can_replace = ci >= 0 && in->col_types[ci] == TF_TYPE_STRING;
32
204
  for (size_t r = 0; r < in->n_rows; r++) {
33
- tf_batch_copy_row(ob, r, in, r);
34
- ob->n_rows = r + 1;
35
- }
36
-
37
- int ci = tf_batch_col_index(ob, st->column);
38
- if (ci >= 0 && ob->col_types[ci] == TF_TYPE_STRING) {
39
- if (st->use_regex) {
40
- /* Regex mode: use regexec to find matches */
41
- for (size_t r = 0; r < ob->n_rows; r++) {
42
- if (tf_batch_is_null(ob, r, ci)) continue;
43
- const char *val = tf_batch_get_string(ob, r, ci);
44
- /* Build result by repeatedly matching and replacing */
45
- size_t val_len = strlen(val);
46
- size_t buf_cap = val_len * 2 + 64;
47
- char *buf = malloc(buf_cap);
48
- if (!buf) continue;
49
- size_t buf_len = 0;
50
- const char *p = val;
51
- regmatch_t match;
52
- int replaced = 0;
53
- while (regexec(&st->compiled, p, 1, &match, 0) == 0 && match.rm_so >= 0) {
54
- replaced = 1;
55
- size_t prefix = (size_t)match.rm_so;
56
- size_t match_len = (size_t)(match.rm_eo - match.rm_so);
57
- /* Expand replacement: & refers to whole match */
58
- size_t rep_need = 0;
59
- for (const char *rp = st->replacement; *rp; rp++) {
60
- if (*rp == '&') rep_need += match_len;
61
- else rep_need++;
62
- }
63
- size_t need = buf_len + prefix + rep_need + 1;
64
- if (need > buf_cap) {
65
- buf_cap = need * 2;
66
- char *nb = realloc(buf, buf_cap);
67
- if (!nb) { free(buf); buf = NULL; break; }
68
- buf = nb;
69
- }
70
- memcpy(buf + buf_len, p, prefix);
71
- buf_len += prefix;
72
- for (const char *rp = st->replacement; *rp; rp++) {
73
- if (*rp == '&') {
74
- memcpy(buf + buf_len, p + match.rm_so, match_len);
75
- buf_len += match_len;
76
- } else {
77
- buf[buf_len++] = *rp;
205
+ char *buf = NULL;
206
+ char *before_copy = NULL;
207
+ int replaced = 0;
208
+ const char *val = NULL;
209
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
210
+ tf_batch_free(ob);
211
+ return TF_ERROR;
212
+ }
213
+ if (can_replace && !tf_batch_is_null(in, r, (size_t)ci)) {
214
+ val = tf_batch_get_string(in, r, (size_t)ci);
215
+ if (val) {
216
+ int build_rc = st->use_regex
217
+ ? replace_build_regex(st, val, &buf, &replaced)
218
+ : replace_build_literal(st, val, &buf, &replaced);
219
+ if (build_rc != TF_OK) {
220
+ tf_batch_free(ob);
221
+ return TF_ERROR;
222
+ }
223
+ if (replaced) {
224
+ if (st->audit && st->audit_emitted < st->audit_limit) {
225
+ before_copy = strdup(val);
226
+ if (!before_copy) {
227
+ free(buf);
228
+ tf_batch_free(ob);
229
+ return TF_ERROR;
78
230
  }
79
231
  }
80
- p += match.rm_eo;
81
- if (match_len == 0) {
82
- /* Zero-length match: copy one char to avoid infinite loop */
83
- if (*p) {
84
- if (buf_len + 2 > buf_cap) {
85
- buf_cap = (buf_len + 2) * 2;
86
- char *nb = realloc(buf, buf_cap);
87
- if (!nb) { free(buf); buf = NULL; break; }
88
- buf = nb;
89
- }
90
- buf[buf_len++] = *p++;
91
- } else break;
92
- }
93
- }
94
- if (buf && replaced) {
95
- /* Copy remaining */
96
- size_t rest = strlen(p);
97
- if (buf_len + rest + 1 > buf_cap) {
98
- buf_cap = buf_len + rest + 1;
99
- char *nb = realloc(buf, buf_cap);
100
- if (!nb) { free(buf); continue; }
101
- buf = nb;
232
+ if (tf_batch_set_string(ob, r, (size_t)ci, buf) != TF_OK) {
233
+ free(before_copy);
234
+ free(buf);
235
+ tf_batch_free(ob);
236
+ return TF_ERROR;
102
237
  }
103
- memcpy(buf + buf_len, p, rest);
104
- buf_len += rest;
105
- buf[buf_len] = '\0';
106
- tf_batch_set_string(ob, r, ci, buf);
107
238
  }
239
+ }
240
+ }
241
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
242
+ free(before_copy);
243
+ free(buf);
244
+ tf_batch_free(ob);
245
+ return TF_ERROR;
246
+ }
247
+ if (replaced) {
248
+ size_t row_no = 0;
249
+ if (tf_size_add(row_base, r, &row_no) != TF_OK ||
250
+ tf_size_add(row_no, 1, &row_no) != TF_OK) {
251
+ free(before_copy);
108
252
  free(buf);
253
+ tf_batch_free(ob);
254
+ return TF_ERROR;
109
255
  }
110
- } else {
111
- /* Substring mode */
112
- size_t pat_len = strlen(st->pattern);
113
- size_t rep_len = strlen(st->replacement);
114
- if (pat_len > 0) {
115
- for (size_t r = 0; r < ob->n_rows; r++) {
116
- if (tf_batch_is_null(ob, r, ci)) continue;
117
- const char *val = tf_batch_get_string(ob, r, ci);
118
- /* Count occurrences */
119
- size_t count = 0;
120
- const char *p = val;
121
- while ((p = strstr(p, st->pattern)) != NULL) { count++; p += pat_len; }
122
- if (count == 0) continue;
123
-
124
- size_t new_len = strlen(val) + count * (rep_len - pat_len);
125
- char *buf = malloc(new_len + 1);
126
- if (!buf) continue;
127
- char *dst = buf;
128
- p = val;
129
- const char *found;
130
- while ((found = strstr(p, st->pattern)) != NULL) {
131
- size_t prefix = found - p;
132
- memcpy(dst, p, prefix);
133
- dst += prefix;
134
- memcpy(dst, st->replacement, rep_len);
135
- dst += rep_len;
136
- p = found + pat_len;
137
- }
138
- strcpy(dst, p);
139
- tf_batch_set_string(ob, r, ci, buf);
140
- free(buf);
141
- }
256
+ const char *after = tf_batch_get_string(ob, r, (size_t)ci);
257
+ if (emit_replace_audit(st, ob, r, (size_t)ci, row_no,
258
+ before_copy ? before_copy : val, after, side) != TF_OK) {
259
+ free(before_copy);
260
+ free(buf);
261
+ tf_batch_free(ob);
262
+ return TF_ERROR;
142
263
  }
143
264
  }
265
+ free(before_copy);
266
+ free(buf);
144
267
  }
145
268
 
269
+ if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) {
270
+ tf_batch_free(ob);
271
+ return TF_ERROR;
272
+ }
146
273
  *out = ob;
147
274
  return TF_OK;
148
275
  }
@@ -151,15 +278,18 @@ static int replace_flush(tf_step *self, tf_batch **out, tf_side_channels *side)
151
278
  (void)self; (void)side; *out = NULL; return TF_OK;
152
279
  }
153
280
 
281
+ static void replace_state_free(replace_state *st) {
282
+ if (!st) return;
283
+ if (st->use_regex) regfree(&st->compiled);
284
+ tf_audit_options_free(&st->audit_opts);
285
+ free(st->column);
286
+ free(st->pattern);
287
+ free(st->replacement);
288
+ free(st);
289
+ }
290
+
154
291
  static void replace_destroy(tf_step *self) {
155
- replace_state *st = self->state;
156
- if (st) {
157
- if (st->use_regex) regfree(&st->compiled);
158
- free(st->column);
159
- free(st->pattern);
160
- free(st->replacement);
161
- free(st);
162
- }
292
+ if (self) replace_state_free(self->state);
163
293
  free(self);
164
294
  }
165
295
 
@@ -173,25 +303,47 @@ tf_step *tf_replace_create(const cJSON *args) {
173
303
 
174
304
  replace_state *st = calloc(1, sizeof(replace_state));
175
305
  if (!st) return NULL;
306
+ tf_audit_options_init(&st->audit_opts, 1);
176
307
  st->column = strdup(col_j->valuestring);
177
308
  st->pattern = strdup(pat_j->valuestring);
178
309
  st->replacement = strdup(rep_j->valuestring);
310
+ st->audit_limit = 1000;
311
+ if (!st->column || !st->pattern || !st->replacement) { replace_state_free(st); return NULL; }
179
312
 
180
313
  cJSON *regex_j = cJSON_GetObjectItemCaseSensitive(args, "regex");
181
314
  st->use_regex = (cJSON_IsBool(regex_j) && cJSON_IsTrue(regex_j)) ? 1 : 0;
182
315
 
316
+ cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
317
+ st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
318
+ cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
319
+ if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
320
+ if (audit_limit_j) {
321
+ size_t parsed_limit = 0;
322
+ if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
323
+ 1, TF_MAX_AUDIT_RECORDS,
324
+ &parsed_limit, "replace") < 0) {
325
+ replace_state_free(st);
326
+ return NULL;
327
+ }
328
+ st->audit_limit = parsed_limit;
329
+ }
330
+
331
+ if (tf_audit_options_parse(&st->audit_opts, args, "replace") != TF_OK) {
332
+ replace_state_free(st);
333
+ return NULL;
334
+ }
335
+
183
336
  if (st->use_regex) {
184
337
  int rc = regcomp(&st->compiled, st->pattern, REG_EXTENDED);
185
338
  if (rc != 0) {
186
- free(st->column); free(st->pattern); free(st->replacement); free(st);
339
+ replace_state_free(st);
187
340
  return NULL;
188
341
  }
189
342
  }
190
343
 
191
- tf_step *step = malloc(sizeof(tf_step));
344
+ tf_step *step = calloc(1, sizeof(tf_step));
192
345
  if (!step) {
193
- if (st->use_regex) regfree(&st->compiled);
194
- free(st->column); free(st->pattern); free(st->replacement); free(st);
346
+ replace_state_free(st);
195
347
  return NULL;
196
348
  }
197
349
  step->process = replace_process;