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
@@ -27,6 +27,22 @@ typedef struct {
27
27
  int initialized;
28
28
  } fill_down_state;
29
29
 
30
+ static int fill_down_is_target(const fill_down_state *st, const tf_batch *in, size_t c) {
31
+ if (st->n_cols == 0) return 1;
32
+ for (size_t k = 0; k < st->n_cols; k++) {
33
+ if (strcmp(in->col_names[c], st->cols[k]) == 0) return 1;
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ static void fill_down_free_string_slots(char **slots, size_t n) {
39
+ if (!slots) return;
40
+ for (size_t i = 0; i < n; i++) {
41
+ free(slots[i]);
42
+ slots[i] = NULL;
43
+ }
44
+ }
45
+
30
46
  static int fill_down_process(tf_step *self, tf_batch *in, tf_batch **out,
31
47
  tf_side_channels *side) {
32
48
  (void)side;
@@ -34,93 +50,136 @@ static int fill_down_process(tf_step *self, tf_batch *in, tf_batch **out,
34
50
  *out = NULL;
35
51
 
36
52
  if (!st->initialized) {
53
+ char **last_vals = calloc(in->n_cols, sizeof(char *));
54
+ int *last_types = calloc(in->n_cols, sizeof(int));
55
+ int64_t *last_int = calloc(in->n_cols, sizeof(int64_t));
56
+ double *last_float = calloc(in->n_cols, sizeof(double));
57
+ int *last_bool = calloc(in->n_cols, sizeof(int));
58
+ int32_t *last_date = calloc(in->n_cols, sizeof(int32_t));
59
+ int64_t *last_timestamp = calloc(in->n_cols, sizeof(int64_t));
60
+ if (!last_vals || !last_types || !last_int || !last_float ||
61
+ !last_bool || !last_date || !last_timestamp) {
62
+ free(last_vals);
63
+ free(last_types);
64
+ free(last_int);
65
+ free(last_float);
66
+ free(last_bool);
67
+ free(last_date);
68
+ free(last_timestamp);
69
+ return TF_ERROR;
70
+ }
71
+ for (size_t c = 0; c < in->n_cols; c++) last_types[c] = -1; /* no value yet */
37
72
  st->n_tracked = (int)in->n_cols;
38
- st->last_vals = calloc(in->n_cols, sizeof(char *));
39
- st->last_types = calloc(in->n_cols, sizeof(int));
40
- st->last_int = calloc(in->n_cols, sizeof(int64_t));
41
- st->last_float = calloc(in->n_cols, sizeof(double));
42
- st->last_bool = calloc(in->n_cols, sizeof(int));
43
- st->last_date = calloc(in->n_cols, sizeof(int32_t));
44
- st->last_timestamp = calloc(in->n_cols, sizeof(int64_t));
45
- for (size_t c = 0; c < in->n_cols; c++) st->last_types[c] = -1; /* no value yet */
73
+ st->last_vals = last_vals;
74
+ st->last_types = last_types;
75
+ st->last_int = last_int;
76
+ st->last_float = last_float;
77
+ st->last_bool = last_bool;
78
+ st->last_date = last_date;
79
+ st->last_timestamp = last_timestamp;
46
80
  st->initialized = 1;
47
81
  }
48
82
 
49
83
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
50
84
  if (!ob) return TF_ERROR;
51
- for (size_t c = 0; c < in->n_cols; c++)
52
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
85
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
86
+ tf_batch_free(ob);
87
+ return TF_ERROR;
88
+ }
89
+
90
+ char **pending_strings = calloc(in->n_cols, sizeof(char *));
91
+ if (!pending_strings) {
92
+ tf_batch_free(ob);
93
+ return TF_ERROR;
94
+ }
53
95
 
54
96
  for (size_t r = 0; r < in->n_rows; r++) {
55
- tf_batch_copy_row(ob, r, in, r);
97
+ memset(pending_strings, 0, sizeof(char *) * in->n_cols);
98
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
99
+ goto fail;
100
+ }
56
101
 
57
102
  for (size_t c = 0; c < in->n_cols; c++) {
58
- /* Check if this column is targeted */
59
- int target = 0;
60
- if (st->n_cols == 0) {
61
- target = 1;
62
- } else {
63
- for (size_t k = 0; k < st->n_cols; k++) {
64
- if (strcmp(in->col_names[c], st->cols[k]) == 0) { target = 1; break; }
65
- }
66
- }
67
- if (!target) continue;
103
+ if (!fill_down_is_target(st, in, c)) continue;
68
104
 
69
105
  if (!tf_batch_is_null(in, r, c)) {
70
- /* Update last value */
71
- st->last_types[c] = (int)in->col_types[c];
72
- switch (in->col_types[c]) {
73
- case TF_TYPE_STRING:
74
- free(st->last_vals[c]);
75
- st->last_vals[c] = strdup(tf_batch_get_string(in, r, c));
76
- break;
77
- case TF_TYPE_INT64:
78
- st->last_int[c] = tf_batch_get_int64(in, r, c); break;
79
- case TF_TYPE_FLOAT64:
80
- st->last_float[c] = tf_batch_get_float64(in, r, c); break;
81
- case TF_TYPE_BOOL:
82
- st->last_bool[c] = tf_batch_get_bool(in, r, c); break;
83
- case TF_TYPE_DATE:
84
- st->last_date[c] = tf_batch_get_date(in, r, c); break;
85
- case TF_TYPE_TIMESTAMP:
86
- st->last_timestamp[c] = tf_batch_get_timestamp(in, r, c); break;
87
- default: break;
106
+ if (in->col_types[c] == TF_TYPE_STRING) {
107
+ const char *src = tf_batch_get_string(in, r, c);
108
+ pending_strings[c] = strdup(src ? src : "");
109
+ if (!pending_strings[c]) {
110
+ goto fail;
111
+ }
88
112
  }
89
113
  } else if (st->last_types[c] >= 0) {
90
114
  /* Fill with last value */
115
+ int write_rc = TF_OK;
91
116
  switch (in->col_types[c]) {
92
117
  case TF_TYPE_STRING:
93
- if (st->last_vals[c]) tf_batch_set_string(ob, r, c, st->last_vals[c]);
118
+ if (st->last_vals[c]) write_rc = tf_batch_set_string(ob, r, c, st->last_vals[c]);
94
119
  break;
95
120
  case TF_TYPE_INT64:
96
- tf_batch_set_int64(ob, r, c, st->last_int[c]); break;
121
+ write_rc = tf_batch_set_int64(ob, r, c, st->last_int[c]); break;
97
122
  case TF_TYPE_FLOAT64:
98
- tf_batch_set_float64(ob, r, c, st->last_float[c]); break;
123
+ write_rc = tf_batch_set_float64(ob, r, c, st->last_float[c]); break;
99
124
  case TF_TYPE_BOOL:
100
- tf_batch_set_bool(ob, r, c, st->last_bool[c]); break;
125
+ write_rc = tf_batch_set_bool(ob, r, c, st->last_bool[c]); break;
101
126
  case TF_TYPE_DATE:
102
- tf_batch_set_date(ob, r, c, st->last_date[c]); break;
127
+ write_rc = tf_batch_set_date(ob, r, c, st->last_date[c]); break;
103
128
  case TF_TYPE_TIMESTAMP:
104
- tf_batch_set_timestamp(ob, r, c, st->last_timestamp[c]); break;
129
+ write_rc = tf_batch_set_timestamp(ob, r, c, st->last_timestamp[c]); break;
105
130
  default: break;
106
131
  }
132
+ if (write_rc != TF_OK) goto fail;
107
133
  }
108
134
  }
109
- ob->n_rows = r + 1;
135
+
136
+ for (size_t c = 0; c < in->n_cols; c++) {
137
+ if (!fill_down_is_target(st, in, c) || tf_batch_is_null(in, r, c)) continue;
138
+
139
+ st->last_types[c] = (int)in->col_types[c];
140
+ switch (in->col_types[c]) {
141
+ case TF_TYPE_STRING:
142
+ free(st->last_vals[c]);
143
+ st->last_vals[c] = pending_strings[c];
144
+ pending_strings[c] = NULL;
145
+ break;
146
+ case TF_TYPE_INT64:
147
+ st->last_int[c] = tf_batch_get_int64(in, r, c); break;
148
+ case TF_TYPE_FLOAT64:
149
+ st->last_float[c] = tf_batch_get_float64(in, r, c); break;
150
+ case TF_TYPE_BOOL:
151
+ st->last_bool[c] = tf_batch_get_bool(in, r, c); break;
152
+ case TF_TYPE_DATE:
153
+ st->last_date[c] = tf_batch_get_date(in, r, c); break;
154
+ case TF_TYPE_TIMESTAMP:
155
+ st->last_timestamp[c] = tf_batch_get_timestamp(in, r, c); break;
156
+ default:
157
+ break;
158
+ }
159
+ }
160
+ if (tf_batch_expose_row(ob, r) != TF_OK) goto fail;
110
161
  }
111
162
 
163
+ free(pending_strings);
112
164
  *out = ob;
113
165
  return TF_OK;
166
+
167
+ fail:
168
+ fill_down_free_string_slots(pending_strings, in->n_cols);
169
+ free(pending_strings);
170
+ tf_batch_free(ob);
171
+ return TF_ERROR;
114
172
  }
115
173
 
116
174
  static int fill_down_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
117
175
  (void)self; (void)side; *out = NULL; return TF_OK;
118
176
  }
119
177
 
120
- static void fill_down_destroy(tf_step *self) {
121
- fill_down_state *st = self->state;
178
+ static void fill_down_state_free(fill_down_state *st) {
122
179
  if (st) {
123
- for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
180
+ if (st->cols) {
181
+ for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
182
+ }
124
183
  free(st->cols);
125
184
  if (st->last_vals) {
126
185
  for (int i = 0; i < st->n_tracked; i++) free(st->last_vals[i]);
@@ -131,6 +190,10 @@ static void fill_down_destroy(tf_step *self) {
131
190
  free(st->last_date); free(st->last_timestamp);
132
191
  free(st);
133
192
  }
193
+ }
194
+
195
+ static void fill_down_destroy(tf_step *self) {
196
+ fill_down_state_free(self->state);
134
197
  free(self);
135
198
  }
136
199
 
@@ -144,17 +207,26 @@ tf_step *tf_fill_down_create(const cJSON *args) {
144
207
  int n = cJSON_GetArraySize(columns);
145
208
  if (n > 0) {
146
209
  st->cols = calloc(n, sizeof(char *));
147
- st->n_cols = n;
210
+ if (!st->cols) { fill_down_state_free(st); return NULL; }
211
+ st->n_cols = (size_t)n;
148
212
  for (int i = 0; i < n; i++) {
149
213
  cJSON *item = cJSON_GetArrayItem(columns, i);
150
- if (cJSON_IsString(item)) st->cols[i] = strdup(item->valuestring);
214
+ if (!cJSON_IsString(item) || !item->valuestring[0]) {
215
+ fill_down_state_free(st);
216
+ return NULL;
217
+ }
218
+ st->cols[i] = strdup(item->valuestring);
219
+ if (!st->cols[i]) {
220
+ fill_down_state_free(st);
221
+ return NULL;
222
+ }
151
223
  }
152
224
  }
153
225
  }
154
226
  }
155
227
 
156
- tf_step *step = malloc(sizeof(tf_step));
157
- if (!step) { free(st); return NULL; }
228
+ tf_step *step = calloc(1, sizeof(tf_step));
229
+ if (!step) { fill_down_state_free(st); return NULL; }
158
230
  step->process = fill_down_process;
159
231
  step->flush = fill_down_flush;
160
232
  step->destroy = fill_down_destroy;
@@ -10,71 +10,182 @@
10
10
  #include <stdlib.h>
11
11
  #include <string.h>
12
12
 
13
+ /* Fill-null is row-local. Audit records are opt-in and capped so diagnostics
14
+ * cannot become an unbounded second output stream. */
13
15
  typedef struct {
14
16
  char **col_names;
15
17
  char **defaults;
16
18
  size_t n;
19
+ size_t row_index;
20
+ size_t audit_limit;
21
+ size_t audit_emitted;
22
+ int audit;
23
+ tf_audit_options audit_opts;
17
24
  } fill_null_state;
18
25
 
26
+ static int emit_fill_null_audit(fill_null_state *st, const tf_batch *b, size_t row,
27
+ size_t col, size_t row_no, tf_side_channels *side) {
28
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
29
+ cJSON *obj = cJSON_CreateObject();
30
+ if (!obj) return TF_ERROR;
31
+ int rc = TF_ERROR;
32
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
33
+ tf_json_add_string(obj, "op", "fill-null") != TF_OK ||
34
+ tf_json_add_string(obj, "event", "null_filled") != TF_OK ||
35
+ tf_json_add_string(obj, "reason", "null_filled") != TF_OK ||
36
+ tf_json_add_string(obj, "channel", "audit") != TF_OK ||
37
+ tf_json_add_string(obj, "column", b->col_names[col] ? b->col_names[col] : "") != TF_OK ||
38
+ tf_json_add_number(obj, "row", (double)row_no) != TF_OK ||
39
+ tf_json_add_null(obj, "before") != TF_OK) {
40
+ goto done;
41
+ }
42
+ cJSON *after = tf_audit_cell_to_json(b, row, col, &st->audit_opts);
43
+ if (!after || tf_json_add_item(obj, "after", after) != TF_OK) goto done;
44
+ cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
45
+ if (row_obj) {
46
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
47
+ } else if (st->audit_opts.include_row) {
48
+ goto done;
49
+ }
50
+ rc = tf_buffer_write_json_line(side->stats, obj);
51
+ done:
52
+ cJSON_Delete(obj);
53
+ if (rc == TF_OK) st->audit_emitted++;
54
+ return rc;
55
+ }
56
+
19
57
  static int fill_null_process(tf_step *self, tf_batch *in, tf_batch **out,
20
58
  tf_side_channels *side) {
21
- (void)side;
22
59
  fill_null_state *st = self->state;
23
60
  *out = NULL;
61
+ size_t row_base = st->row_index;
24
62
 
25
63
  tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
26
64
  if (!ob) return TF_ERROR;
27
- for (size_t c = 0; c < in->n_cols; c++)
28
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
29
- for (size_t r = 0; r < in->n_rows; r++) {
30
- tf_batch_copy_row(ob, r, in, r);
31
- ob->n_rows = r + 1;
65
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
66
+ tf_batch_free(ob);
67
+ return TF_ERROR;
32
68
  }
33
-
34
- /* Fill nulls */
35
- for (size_t k = 0; k < st->n; k++) {
36
- int ci = tf_batch_col_index(ob, st->col_names[k]);
37
- if (ci < 0) continue;
38
- for (size_t r = 0; r < ob->n_rows; r++) {
39
- if (!tf_batch_is_null(ob, r, ci)) continue;
69
+ size_t *changed_cols = tf_callocarray_checked(st->n > 0 ? st->n : 1, sizeof(size_t));
70
+ if (!changed_cols) {
71
+ tf_batch_free(ob);
72
+ return TF_ERROR;
73
+ }
74
+ for (size_t r = 0; r < in->n_rows; r++) {
75
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
76
+ free(changed_cols);
77
+ tf_batch_free(ob);
78
+ return TF_ERROR;
79
+ }
80
+ size_t changed_count = 0;
81
+ for (size_t k = 0; k < st->n; k++) {
82
+ int ci = tf_batch_col_index(in, st->col_names[k]);
83
+ if (ci < 0 || !ob->nulls[ci] || !ob->nulls[ci][r]) continue;
40
84
  const char *def = st->defaults[k];
41
- switch (ob->col_types[ci]) {
85
+ int changed = 0;
86
+ switch (in->col_types[ci]) {
42
87
  case TF_TYPE_STRING:
43
- tf_batch_set_string(ob, r, ci, def);
88
+ if (tf_batch_set_string(ob, r, (size_t)ci, def) != TF_OK) {
89
+ free(changed_cols);
90
+ tf_batch_free(ob);
91
+ return TF_ERROR;
92
+ }
93
+ changed = 1;
44
94
  break;
45
95
  case TF_TYPE_INT64: {
46
96
  char *end;
47
97
  int64_t v = strtoll(def, &end, 10);
48
- if (*end == '\0') tf_batch_set_int64(ob, r, ci, v);
98
+ if (*end == '\0') {
99
+ if (tf_batch_set_int64(ob, r, (size_t)ci, v) != TF_OK) {
100
+ free(changed_cols);
101
+ tf_batch_free(ob);
102
+ return TF_ERROR;
103
+ }
104
+ changed = 1;
105
+ }
49
106
  break;
50
107
  }
51
108
  case TF_TYPE_FLOAT64: {
52
109
  char *end;
53
110
  double v = strtod(def, &end);
54
- if (*end == '\0') tf_batch_set_float64(ob, r, ci, v);
111
+ if (*end == '\0') {
112
+ if (tf_batch_set_float64(ob, r, (size_t)ci, v) != TF_OK) {
113
+ free(changed_cols);
114
+ tf_batch_free(ob);
115
+ return TF_ERROR;
116
+ }
117
+ changed = 1;
118
+ }
55
119
  break;
56
120
  }
57
121
  case TF_TYPE_BOOL:
58
- tf_batch_set_bool(ob, r, ci, strcmp(def, "true") == 0);
122
+ if (tf_batch_set_bool(ob, r, (size_t)ci, strcmp(def, "true") == 0) != TF_OK) {
123
+ free(changed_cols);
124
+ tf_batch_free(ob);
125
+ return TF_ERROR;
126
+ }
127
+ changed = 1;
59
128
  break;
60
129
  case TF_TYPE_DATE: {
61
130
  char *end;
62
131
  int32_t v = (int32_t)strtol(def, &end, 10);
63
- if (*end == '\0') tf_batch_set_date(ob, r, ci, v);
132
+ if (*end == '\0') {
133
+ if (tf_batch_set_date(ob, r, (size_t)ci, v) != TF_OK) {
134
+ free(changed_cols);
135
+ tf_batch_free(ob);
136
+ return TF_ERROR;
137
+ }
138
+ changed = 1;
139
+ }
64
140
  break;
65
141
  }
66
142
  case TF_TYPE_TIMESTAMP: {
67
143
  char *end;
68
144
  int64_t v = strtoll(def, &end, 10);
69
- if (*end == '\0') tf_batch_set_timestamp(ob, r, ci, v);
145
+ if (*end == '\0') {
146
+ if (tf_batch_set_timestamp(ob, r, (size_t)ci, v) != TF_OK) {
147
+ free(changed_cols);
148
+ tf_batch_free(ob);
149
+ return TF_ERROR;
150
+ }
151
+ changed = 1;
152
+ }
70
153
  break;
71
154
  }
72
155
  default:
73
156
  break;
74
157
  }
158
+ if (changed) changed_cols[changed_count++] = (size_t)ci;
159
+ }
160
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
161
+ free(changed_cols);
162
+ tf_batch_free(ob);
163
+ return TF_ERROR;
164
+ }
165
+ if (changed_count > 0) {
166
+ size_t row_no = 0;
167
+ if (tf_size_add(row_base, r, &row_no) != TF_OK ||
168
+ tf_size_add(row_no, 1, &row_no) != TF_OK) {
169
+ free(changed_cols);
170
+ tf_batch_free(ob);
171
+ return TF_ERROR;
172
+ }
173
+ for (size_t i = 0; i < changed_count; i++) {
174
+ if (emit_fill_null_audit(st, ob, r, changed_cols[i], row_no, side) != TF_OK) {
175
+ free(changed_cols);
176
+ tf_batch_free(ob);
177
+ return TF_ERROR;
178
+ }
179
+ }
75
180
  }
76
181
  }
77
182
 
183
+ if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) {
184
+ free(changed_cols);
185
+ tf_batch_free(ob);
186
+ return TF_ERROR;
187
+ }
188
+ free(changed_cols);
78
189
  *out = ob;
79
190
  return TF_OK;
80
191
  }
@@ -83,13 +194,22 @@ static int fill_null_flush(tf_step *self, tf_batch **out, tf_side_channels *side
83
194
  (void)self; (void)side; *out = NULL; return TF_OK;
84
195
  }
85
196
 
86
- static void fill_null_destroy(tf_step *self) {
87
- fill_null_state *st = self->state;
88
- if (st) {
89
- for (size_t i = 0; i < st->n; i++) { free(st->col_names[i]); free(st->defaults[i]); }
90
- free(st->col_names); free(st->defaults);
91
- free(st);
197
+ static void fill_null_state_free(fill_null_state *st) {
198
+ if (!st) return;
199
+ tf_audit_options_free(&st->audit_opts);
200
+ if (st->col_names) {
201
+ for (size_t i = 0; i < st->n; i++) free(st->col_names[i]);
92
202
  }
203
+ if (st->defaults) {
204
+ for (size_t i = 0; i < st->n; i++) free(st->defaults[i]);
205
+ }
206
+ free(st->col_names);
207
+ free(st->defaults);
208
+ free(st);
209
+ }
210
+
211
+ static void fill_null_destroy(tf_step *self) {
212
+ if (self) fill_null_state_free(self->state);
93
213
  free(self);
94
214
  }
95
215
 
@@ -101,20 +221,45 @@ tf_step *tf_fill_null_create(const cJSON *args) {
101
221
  int n = cJSON_GetArraySize(mapping);
102
222
  fill_null_state *st = calloc(1, sizeof(fill_null_state));
103
223
  if (!st) return NULL;
104
- st->col_names = calloc(n, sizeof(char *));
105
- st->defaults = calloc(n, sizeof(char *));
106
- st->n = n;
224
+ tf_audit_options_init(&st->audit_opts, 1);
225
+ st->col_names = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(char *));
226
+ st->defaults = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(char *));
227
+ st->n = (size_t)n;
228
+ st->audit_limit = 1000;
229
+ if (!st->col_names || !st->defaults) { fill_null_state_free(st); return NULL; }
107
230
 
108
231
  int i = 0;
109
232
  cJSON *entry = NULL;
110
233
  cJSON_ArrayForEach(entry, mapping) {
234
+ if (!entry->string || !entry->string[0]) { fill_null_state_free(st); return NULL; }
111
235
  st->col_names[i] = strdup(entry->string);
112
236
  st->defaults[i] = strdup(cJSON_IsString(entry) ? entry->valuestring : "");
237
+ if (!st->col_names[i] || !st->defaults[i]) { fill_null_state_free(st); return NULL; }
113
238
  i++;
114
239
  }
115
240
 
116
- tf_step *step = malloc(sizeof(tf_step));
117
- if (!step) { fill_null_destroy(&(tf_step){.state = st}); return NULL; }
241
+ cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
242
+ st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
243
+ cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
244
+ if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
245
+ if (audit_limit_j) {
246
+ size_t parsed_limit = 0;
247
+ if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
248
+ 1, TF_MAX_AUDIT_RECORDS,
249
+ &parsed_limit, "fill-null") < 0) {
250
+ fill_null_state_free(st);
251
+ return NULL;
252
+ }
253
+ st->audit_limit = parsed_limit;
254
+ }
255
+
256
+ if (tf_audit_options_parse(&st->audit_opts, args, "fill-null") != TF_OK) {
257
+ fill_null_state_free(st);
258
+ return NULL;
259
+ }
260
+
261
+ tf_step *step = calloc(1, sizeof(tf_step));
262
+ if (!step) { fill_null_state_free(st); return NULL; }
118
263
  step->process = fill_null_process;
119
264
  step->flush = fill_null_flush;
120
265
  step->destroy = fill_null_destroy;