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/batch.c CHANGED
@@ -3,6 +3,9 @@
3
3
  */
4
4
 
5
5
  #include "internal.h"
6
+ #include "cJSON.h"
7
+ #include "date_utils.h"
8
+ #include <stdio.h>
6
9
  #include <stdlib.h>
7
10
  #include <string.h>
8
11
 
@@ -19,6 +22,15 @@ static size_t type_size(tf_type t) {
19
22
  }
20
23
 
21
24
  tf_batch *tf_batch_create(size_t n_cols, size_t capacity) {
25
+ if (n_cols > TF_MAX_COLUMNS) {
26
+ char err[256];
27
+ snprintf(err, sizeof(err),
28
+ "batch: columns exceeds maximum %zu, observed %zu",
29
+ TF_MAX_COLUMNS, n_cols);
30
+ tf_set_last_error(err);
31
+ return NULL;
32
+ }
33
+
22
34
  tf_arena *arena = tf_arena_create(0);
23
35
  if (!arena) return NULL;
24
36
 
@@ -29,10 +41,19 @@ tf_batch *tf_batch_create(size_t n_cols, size_t capacity) {
29
41
  b->n_cols = n_cols;
30
42
  b->n_rows = 0;
31
43
  b->capacity = capacity;
32
- b->col_names = tf_arena_alloc(arena, n_cols * sizeof(char *));
33
- b->col_types = tf_arena_alloc(arena, n_cols * sizeof(tf_type));
34
- b->columns = tf_arena_alloc(arena, n_cols * sizeof(void *));
35
- b->nulls = tf_arena_alloc(arena, n_cols * sizeof(uint8_t *));
44
+ b->schema_name_bytes = 0;
45
+ size_t names_bytes = 0, types_bytes = 0, columns_bytes = 0, nulls_bytes = 0;
46
+ if (tf_size_mul(n_cols, sizeof(char *), &names_bytes) != TF_OK ||
47
+ tf_size_mul(n_cols, sizeof(tf_type), &types_bytes) != TF_OK ||
48
+ tf_size_mul(n_cols, sizeof(void *), &columns_bytes) != TF_OK ||
49
+ tf_size_mul(n_cols, sizeof(uint8_t *), &nulls_bytes) != TF_OK) {
50
+ tf_arena_free(arena);
51
+ return NULL;
52
+ }
53
+ b->col_names = tf_arena_alloc(arena, names_bytes);
54
+ b->col_types = tf_arena_alloc(arena, types_bytes);
55
+ b->columns = tf_arena_alloc(arena, columns_bytes);
56
+ b->nulls = tf_arena_alloc(arena, nulls_bytes);
36
57
 
37
58
  if (!b->col_names || !b->col_types || !b->columns || !b->nulls) {
38
59
  tf_arena_free(arena);
@@ -50,18 +71,38 @@ tf_batch *tf_batch_create(size_t n_cols, size_t capacity) {
50
71
  }
51
72
 
52
73
  int tf_batch_set_schema(tf_batch *b, size_t col, const char *name, tf_type type) {
53
- if (col >= b->n_cols) return TF_ERROR;
54
- b->col_names[col] = tf_arena_strdup(b->arena, name);
55
- b->col_types[col] = type;
56
-
57
- /* Allocate column storage */
74
+ if (!b || col >= b->n_cols) return TF_ERROR;
75
+ const char *safe_name = name ? name : "";
76
+ size_t name_len = 0, name_bytes = 0, schema_bytes = 0;
77
+ if (tf_string_length_bounded(safe_name, TF_MAX_COLUMN_NAME_BYTES,
78
+ &name_len, "batch", "column name") != TF_OK ||
79
+ tf_size_add(name_len, 1, &name_bytes) != TF_OK ||
80
+ tf_size_add(b->schema_name_bytes, name_bytes, &schema_bytes) != TF_OK ||
81
+ tf_check_byte_limit(schema_bytes, TF_MAX_SCHEMA_BYTES,
82
+ "batch", "schema") != TF_OK)
83
+ return TF_ERROR;
84
+ char *name_copy = tf_arena_alloc(b->arena, name_bytes);
85
+ if (!name_copy) return TF_ERROR;
86
+ memcpy(name_copy, safe_name, name_len);
87
+ name_copy[name_len] = '\0';
88
+
89
+ void *column = NULL;
90
+ uint8_t *nulls = NULL;
58
91
  size_t sz = type_size(type);
59
92
  if (sz > 0 && b->capacity > 0) {
60
- b->columns[col] = tf_arena_alloc(b->arena, sz * b->capacity);
61
- b->nulls[col] = tf_arena_alloc(b->arena, b->capacity);
62
- if (!b->columns[col] || !b->nulls[col]) return TF_ERROR;
63
- memset(b->nulls[col], 1, b->capacity); /* all null by default */
93
+ size_t column_bytes = 0;
94
+ if (tf_size_mul(sz, b->capacity, &column_bytes) != TF_OK) return TF_ERROR;
95
+ column = tf_arena_alloc(b->arena, column_bytes);
96
+ nulls = tf_arena_alloc(b->arena, b->capacity);
97
+ if (!column || !nulls) return TF_ERROR;
98
+ memset(nulls, 1, b->capacity); /* all null by default */
64
99
  }
100
+
101
+ b->col_names[col] = name_copy;
102
+ b->col_types[col] = type;
103
+ b->columns[col] = column;
104
+ b->nulls[col] = nulls;
105
+ b->schema_name_bytes = schema_bytes;
65
106
  return TF_OK;
66
107
  }
67
108
 
@@ -70,19 +111,23 @@ int tf_batch_ensure_capacity(tf_batch *b, size_t min_rows) {
70
111
 
71
112
  /* We need to reallocate columns — but arena doesn't support realloc.
72
113
  * Allocate new larger arrays and copy. */
73
- size_t new_cap = b->capacity ? b->capacity : 16;
74
- while (new_cap < min_rows) new_cap *= 2;
114
+ size_t new_cap = 0;
115
+ if (tf_size_grow_pow2(b->capacity, min_rows, 16, &new_cap) != TF_OK) return TF_ERROR;
75
116
 
76
117
  for (size_t i = 0; i < b->n_cols; i++) {
77
118
  size_t sz = type_size(b->col_types[i]);
78
119
  if (sz == 0) continue;
79
120
 
80
- void *new_col = tf_arena_alloc(b->arena, sz * new_cap);
121
+ size_t column_bytes = 0;
122
+ size_t copy_bytes = 0;
123
+ if (tf_size_mul(sz, new_cap, &column_bytes) != TF_OK ||
124
+ tf_size_mul(sz, b->n_rows, &copy_bytes) != TF_OK) return TF_ERROR;
125
+ void *new_col = tf_arena_alloc(b->arena, column_bytes);
81
126
  uint8_t *new_null = tf_arena_alloc(b->arena, new_cap);
82
127
  if (!new_col || !new_null) return TF_ERROR;
83
128
 
84
129
  if (b->columns[i] && b->n_rows > 0) {
85
- memcpy(new_col, b->columns[i], sz * b->n_rows);
130
+ memcpy(new_col, b->columns[i], copy_bytes);
86
131
  memcpy(new_null, b->nulls[i], b->n_rows);
87
132
  }
88
133
  memset(new_null + b->n_rows, 1, new_cap - b->n_rows);
@@ -93,127 +138,536 @@ int tf_batch_ensure_capacity(tf_batch *b, size_t min_rows) {
93
138
  return TF_OK;
94
139
  }
95
140
 
141
+ int tf_batch_expose_row(tf_batch *b, size_t row) {
142
+ if (!b) return TF_ERROR;
143
+ size_t next_rows = 0;
144
+ if (tf_size_add(row, 1, &next_rows) != TF_OK) return TF_ERROR;
145
+ if (next_rows > b->capacity) return TF_ERROR;
146
+ b->n_rows = next_rows;
147
+ return TF_OK;
148
+ }
149
+
96
150
  /* ---- Setters ---- */
97
151
 
98
- void tf_batch_set_null(tf_batch *b, size_t row, size_t col) {
99
- if (row < b->capacity && col < b->n_cols && b->nulls[col])
100
- b->nulls[col][row] = 1;
152
+ int tf_batch_set_null(tf_batch *b, size_t row, size_t col) {
153
+ if (!b || row >= b->capacity || col >= b->n_cols) return TF_ERROR;
154
+ if (!b->nulls[col]) return TF_OK;
155
+ b->nulls[col][row] = 1;
156
+ return TF_OK;
101
157
  }
102
158
 
103
- void tf_batch_set_bool(tf_batch *b, size_t row, size_t col, bool val) {
104
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_BOOL) {
105
- ((uint8_t *)b->columns[col])[row] = val ? 1 : 0;
106
- b->nulls[col][row] = 0;
107
- }
159
+ int tf_batch_set_bool(tf_batch *b, size_t row, size_t col, bool val) {
160
+ if (!b || row >= b->capacity || col >= b->n_cols ||
161
+ b->col_types[col] != TF_TYPE_BOOL || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
162
+ ((uint8_t *)b->columns[col])[row] = val ? 1 : 0;
163
+ b->nulls[col][row] = 0;
164
+ return TF_OK;
108
165
  }
109
166
 
110
- void tf_batch_set_int64(tf_batch *b, size_t row, size_t col, int64_t val) {
111
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_INT64) {
112
- ((int64_t *)b->columns[col])[row] = val;
113
- b->nulls[col][row] = 0;
114
- }
167
+ int tf_batch_set_int64(tf_batch *b, size_t row, size_t col, int64_t val) {
168
+ if (!b || row >= b->capacity || col >= b->n_cols ||
169
+ b->col_types[col] != TF_TYPE_INT64 || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
170
+ ((int64_t *)b->columns[col])[row] = val;
171
+ b->nulls[col][row] = 0;
172
+ return TF_OK;
115
173
  }
116
174
 
117
- void tf_batch_set_float64(tf_batch *b, size_t row, size_t col, double val) {
118
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_FLOAT64) {
119
- ((double *)b->columns[col])[row] = val;
120
- b->nulls[col][row] = 0;
121
- }
175
+ int tf_batch_set_float64(tf_batch *b, size_t row, size_t col, double val) {
176
+ if (!b || row >= b->capacity || col >= b->n_cols ||
177
+ b->col_types[col] != TF_TYPE_FLOAT64 || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
178
+ ((double *)b->columns[col])[row] = val;
179
+ b->nulls[col][row] = 0;
180
+ return TF_OK;
122
181
  }
123
182
 
124
- void tf_batch_set_string(tf_batch *b, size_t row, size_t col, const char *val) {
125
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_STRING) {
126
- ((char **)b->columns[col])[row] = tf_arena_strdup(b->arena, val);
127
- b->nulls[col][row] = 0;
128
- }
183
+ int tf_batch_set_string(tf_batch *b, size_t row, size_t col, const char *val) {
184
+ size_t len = 0;
185
+ if (tf_string_length_bounded(val, TF_MAX_CELL_BYTES,
186
+ &len, "batch", "cell") != TF_OK)
187
+ return TF_ERROR;
188
+ return tf_batch_set_string_len(b, row, col, val, len);
189
+ }
190
+
191
+ int tf_batch_set_string_len(tf_batch *b, size_t row, size_t col,
192
+ const char *val, size_t len) {
193
+ if (!b || row >= b->capacity || col >= b->n_cols || !val ||
194
+ b->col_types[col] != TF_TYPE_STRING || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
195
+ if (tf_check_byte_limit(len, TF_MAX_CELL_BYTES, "batch", "cell") != TF_OK)
196
+ return TF_ERROR;
197
+ size_t bytes = 0;
198
+ if (tf_size_add(len, 1, &bytes) != TF_OK) return TF_ERROR;
199
+ char *copy = tf_arena_alloc(b->arena, bytes);
200
+ if (!copy) return TF_ERROR;
201
+ memcpy(copy, val, len);
202
+ copy[len] = '\0';
203
+ ((char **)b->columns[col])[row] = copy;
204
+ b->nulls[col][row] = 0;
205
+ return TF_OK;
129
206
  }
130
207
 
131
- void tf_batch_set_date(tf_batch *b, size_t row, size_t col, int32_t val) {
132
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_DATE) {
133
- ((int32_t *)b->columns[col])[row] = val;
134
- b->nulls[col][row] = 0;
208
+ int tf_batch_set_date(tf_batch *b, size_t row, size_t col, int32_t val) {
209
+ if (!b || row >= b->capacity || col >= b->n_cols ||
210
+ b->col_types[col] != TF_TYPE_DATE || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
211
+ ((int32_t *)b->columns[col])[row] = val;
212
+ b->nulls[col][row] = 0;
213
+ return TF_OK;
214
+ }
215
+
216
+ int tf_batch_set_timestamp(tf_batch *b, size_t row, size_t col, int64_t val) {
217
+ if (!b || row >= b->capacity || col >= b->n_cols ||
218
+ b->col_types[col] != TF_TYPE_TIMESTAMP || !b->columns[col] || !b->nulls[col]) return TF_ERROR;
219
+ ((int64_t *)b->columns[col])[row] = val;
220
+ b->nulls[col][row] = 0;
221
+ return TF_OK;
222
+ }
223
+
224
+ int tf_batch_set_cell_value(tf_batch *b, size_t row, size_t col,
225
+ tf_type type, int is_null, const tf_cell_value *value) {
226
+ if (!b || col >= b->n_cols || b->col_types[col] != type) return TF_ERROR;
227
+ if (is_null) return tf_batch_set_null(b, row, col);
228
+ if (!value) return TF_ERROR;
229
+
230
+ switch (type) {
231
+ case TF_TYPE_BOOL:
232
+ return tf_batch_set_bool(b, row, col, value->b);
233
+ case TF_TYPE_INT64:
234
+ return tf_batch_set_int64(b, row, col, value->i64);
235
+ case TF_TYPE_FLOAT64:
236
+ return tf_batch_set_float64(b, row, col, value->f64);
237
+ case TF_TYPE_STRING:
238
+ return tf_batch_set_string(b, row, col, value->str ? value->str : "");
239
+ case TF_TYPE_DATE:
240
+ return tf_batch_set_date(b, row, col, value->date);
241
+ case TF_TYPE_TIMESTAMP:
242
+ return tf_batch_set_timestamp(b, row, col, value->i64);
243
+ default:
244
+ return tf_batch_set_null(b, row, col);
135
245
  }
136
246
  }
137
247
 
138
- void tf_batch_set_timestamp(tf_batch *b, size_t row, size_t col, int64_t val) {
139
- if (row < b->capacity && col < b->n_cols && b->col_types[col] == TF_TYPE_TIMESTAMP) {
140
- ((int64_t *)b->columns[col])[row] = val;
141
- b->nulls[col][row] = 0;
248
+ int tf_batch_set_owned_cell_value(tf_batch *b, size_t row, size_t col,
249
+ tf_type type, int is_null,
250
+ const tf_owned_cell_value *value) {
251
+ tf_cell_value tmp;
252
+ memset(&tmp, 0, sizeof(tmp));
253
+ if (value) {
254
+ switch (type) {
255
+ case TF_TYPE_BOOL:
256
+ tmp.b = value->b != 0;
257
+ break;
258
+ case TF_TYPE_INT64:
259
+ tmp.i64 = value->i64;
260
+ break;
261
+ case TF_TYPE_FLOAT64:
262
+ tmp.f64 = value->f64;
263
+ break;
264
+ case TF_TYPE_STRING:
265
+ tmp.str = value->str;
266
+ break;
267
+ case TF_TYPE_DATE:
268
+ tmp.date = value->date;
269
+ break;
270
+ case TF_TYPE_TIMESTAMP:
271
+ tmp.i64 = value->i64;
272
+ break;
273
+ default:
274
+ break;
275
+ }
142
276
  }
277
+ return tf_batch_set_cell_value(b, row, col, type, is_null, value ? &tmp : NULL);
143
278
  }
144
279
 
145
280
  /* ---- Getters ---- */
146
281
 
282
+ size_t tf_batch_num_rows(const tf_batch *b) {
283
+ return b ? b->n_rows : 0;
284
+ }
285
+
286
+ size_t tf_batch_num_cols(const tf_batch *b) {
287
+ return b ? b->n_cols : 0;
288
+ }
289
+
290
+ const char *tf_batch_col_name(const tf_batch *b, size_t col) {
291
+ if (!b || col >= b->n_cols) return NULL;
292
+ return b->col_names[col];
293
+ }
294
+
295
+ tf_type tf_batch_col_type(const tf_batch *b, size_t col) {
296
+ if (!b || col >= b->n_cols) return TF_TYPE_NULL;
297
+ return b->col_types[col];
298
+ }
299
+
147
300
  bool tf_batch_is_null(const tf_batch *b, size_t row, size_t col) {
148
- if (row >= b->n_rows || col >= b->n_cols) return true;
301
+ if (!b || row >= b->n_rows || col >= b->n_cols) return true;
302
+ if (!b->nulls[col]) return true;
149
303
  return b->nulls[col][row] != 0;
150
304
  }
151
305
 
152
306
  bool tf_batch_get_bool(const tf_batch *b, size_t row, size_t col) {
153
- if (row >= b->n_rows || col >= b->n_cols) return false;
307
+ if (!b || row >= b->n_rows || col >= b->n_cols) return false;
154
308
  return ((uint8_t *)b->columns[col])[row] != 0;
155
309
  }
156
310
 
157
311
  int64_t tf_batch_get_int64(const tf_batch *b, size_t row, size_t col) {
158
- if (row >= b->n_rows || col >= b->n_cols) return 0;
312
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
159
313
  return ((int64_t *)b->columns[col])[row];
160
314
  }
161
315
 
162
316
  double tf_batch_get_float64(const tf_batch *b, size_t row, size_t col) {
163
- if (row >= b->n_rows || col >= b->n_cols) return 0.0;
317
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0.0;
164
318
  return ((double *)b->columns[col])[row];
165
319
  }
166
320
 
167
321
  const char *tf_batch_get_string(const tf_batch *b, size_t row, size_t col) {
168
- if (row >= b->n_rows || col >= b->n_cols) return NULL;
322
+ if (!b || row >= b->n_rows || col >= b->n_cols) return NULL;
169
323
  return ((char **)b->columns[col])[row];
170
324
  }
171
325
 
172
326
  int32_t tf_batch_get_date(const tf_batch *b, size_t row, size_t col) {
173
- if (row >= b->n_rows || col >= b->n_cols) return 0;
327
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
174
328
  return ((int32_t *)b->columns[col])[row];
175
329
  }
176
330
 
177
331
  int64_t tf_batch_get_timestamp(const tf_batch *b, size_t row, size_t col) {
178
- if (row >= b->n_rows || col >= b->n_cols) return 0;
332
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
179
333
  return ((int64_t *)b->columns[col])[row];
180
334
  }
181
335
 
182
- int tf_batch_copy_row(tf_batch *dst, size_t dst_row,
183
- const tf_batch *src, size_t src_row) {
184
- if (tf_batch_ensure_capacity(dst, dst_row + 1) != TF_OK) return TF_ERROR;
185
- for (size_t c = 0; c < src->n_cols; c++) {
186
- if (tf_batch_is_null(src, src_row, c)) {
187
- tf_batch_set_null(dst, dst_row, c);
188
- continue;
189
- }
190
- switch (src->col_types[c]) {
336
+ int tf_batch_copy_cell(tf_batch *dst, size_t dst_row, size_t dst_col,
337
+ const tf_batch *src, size_t src_row, size_t src_col) {
338
+ if (!dst || !src || src_row >= src->n_rows || src_col >= src->n_cols || dst_col >= dst->n_cols)
339
+ return TF_ERROR;
340
+
341
+ tf_type type = src->col_types[src_col];
342
+ int is_null = tf_batch_is_null(src, src_row, src_col);
343
+ tf_cell_value value;
344
+ memset(&value, 0, sizeof(value));
345
+ if (!is_null) {
346
+ switch (type) {
191
347
  case TF_TYPE_BOOL:
192
- tf_batch_set_bool(dst, dst_row, c, tf_batch_get_bool(src, src_row, c));
348
+ value.b = tf_batch_get_bool(src, src_row, src_col);
193
349
  break;
194
350
  case TF_TYPE_INT64:
195
- tf_batch_set_int64(dst, dst_row, c, tf_batch_get_int64(src, src_row, c));
351
+ value.i64 = tf_batch_get_int64(src, src_row, src_col);
196
352
  break;
197
353
  case TF_TYPE_FLOAT64:
198
- tf_batch_set_float64(dst, dst_row, c, tf_batch_get_float64(src, src_row, c));
354
+ value.f64 = tf_batch_get_float64(src, src_row, src_col);
199
355
  break;
200
356
  case TF_TYPE_STRING:
201
- tf_batch_set_string(dst, dst_row, c, tf_batch_get_string(src, src_row, c));
357
+ value.str = tf_batch_get_string(src, src_row, src_col);
202
358
  break;
203
359
  case TF_TYPE_DATE:
204
- tf_batch_set_date(dst, dst_row, c, tf_batch_get_date(src, src_row, c));
360
+ value.date = tf_batch_get_date(src, src_row, src_col);
205
361
  break;
206
362
  case TF_TYPE_TIMESTAMP:
207
- tf_batch_set_timestamp(dst, dst_row, c, tf_batch_get_timestamp(src, src_row, c));
363
+ value.i64 = tf_batch_get_timestamp(src, src_row, src_col);
208
364
  break;
209
365
  default:
210
- tf_batch_set_null(dst, dst_row, c);
211
366
  break;
212
367
  }
213
368
  }
369
+ return tf_batch_set_cell_value(dst, dst_row, dst_col, type, is_null, &value);
370
+ }
371
+
372
+ int tf_batch_copy_cell_index(tf_batch *dst, size_t dst_row, size_t dst_col,
373
+ const tf_batch *src, size_t src_row, int src_col) {
374
+ if (src_col < 0) return TF_ERROR;
375
+ return tf_batch_copy_cell(dst, dst_row, dst_col, src, src_row, (size_t)src_col);
376
+ }
377
+
378
+ int tf_batch_format_cell_as_string(const tf_batch *src, size_t src_row, size_t src_col,
379
+ tf_cell_string_format format, char *buf, size_t buf_size,
380
+ const char **out) {
381
+ if (!src || !buf || buf_size == 0 || !out || src_row >= src->n_rows || src_col >= src->n_cols) {
382
+ return TF_ERROR;
383
+ }
384
+ *out = NULL;
385
+ buf[0] = '\0';
386
+ if (tf_batch_is_null(src, src_row, src_col)) return TF_OK;
387
+
388
+ int n = 0;
389
+ switch (src->col_types[src_col]) {
390
+ case TF_TYPE_STRING:
391
+ *out = tf_batch_get_string(src, src_row, src_col);
392
+ return TF_OK;
393
+ case TF_TYPE_INT64:
394
+ n = snprintf(buf, buf_size, "%lld",
395
+ (long long)tf_batch_get_int64(src, src_row, src_col));
396
+ if (n < 0 || (size_t)n >= buf_size) return TF_ERROR;
397
+ *out = buf;
398
+ return TF_OK;
399
+ case TF_TYPE_FLOAT64:
400
+ if (format == TF_CELL_STRING_HUMAN) {
401
+ n = snprintf(buf, buf_size, "%g", tf_batch_get_float64(src, src_row, src_col));
402
+ if (n < 0 || (size_t)n >= buf_size) return TF_ERROR;
403
+ } else {
404
+ if (tf_format_float64(buf, buf_size,
405
+ tf_batch_get_float64(src, src_row, src_col)) != TF_OK) {
406
+ return TF_ERROR;
407
+ }
408
+ }
409
+ *out = buf;
410
+ return TF_OK;
411
+ case TF_TYPE_BOOL:
412
+ *out = tf_batch_get_bool(src, src_row, src_col) ? "true" : "false";
413
+ return TF_OK;
414
+ case TF_TYPE_DATE:
415
+ if (format == TF_CELL_STRING_NUMERIC_TIME) {
416
+ n = snprintf(buf, buf_size, "%d", (int)tf_batch_get_date(src, src_row, src_col));
417
+ if (n < 0 || (size_t)n >= buf_size) return TF_ERROR;
418
+ } else {
419
+ tf_date_format(tf_batch_get_date(src, src_row, src_col), buf, buf_size);
420
+ }
421
+ *out = buf;
422
+ return TF_OK;
423
+ case TF_TYPE_TIMESTAMP:
424
+ if (format == TF_CELL_STRING_NUMERIC_TIME) {
425
+ n = snprintf(buf, buf_size, "%lld",
426
+ (long long)tf_batch_get_timestamp(src, src_row, src_col));
427
+ if (n < 0 || (size_t)n >= buf_size) return TF_ERROR;
428
+ } else {
429
+ tf_timestamp_format(tf_batch_get_timestamp(src, src_row, src_col), buf, buf_size);
430
+ }
431
+ *out = buf;
432
+ return TF_OK;
433
+ default:
434
+ return TF_OK;
435
+ }
436
+ }
437
+
438
+ int tf_batch_copy_cell_as_string(tf_batch *dst, size_t dst_row, size_t dst_col,
439
+ const tf_batch *src, size_t src_row, size_t src_col) {
440
+ if (!dst || !src || src_row >= src->n_rows || src_col >= src->n_cols ||
441
+ dst_col >= dst->n_cols || dst->col_types[dst_col] != TF_TYPE_STRING) {
442
+ return TF_ERROR;
443
+ }
444
+
445
+ char buf[64];
446
+ const char *text = NULL;
447
+ if (tf_batch_format_cell_as_string(src, src_row, src_col, TF_CELL_STRING_ROUNDTRIP,
448
+ buf, sizeof(buf), &text) != TF_OK) {
449
+ return TF_ERROR;
450
+ }
451
+ if (!text) {
452
+ return tf_batch_set_null(dst, dst_row, dst_col);
453
+ }
454
+ return tf_batch_set_string(dst, dst_row, dst_col, text);
455
+ }
456
+
457
+ int tf_batch_clone_schema(tf_batch *dst, const tf_batch *src) {
458
+ if (!dst || !src || dst->n_cols < src->n_cols) return TF_ERROR;
459
+ for (size_t c = 0; c < src->n_cols; c++) {
460
+ if (tf_batch_set_schema(dst, c, src->col_names[c], src->col_types[c]) != TF_OK)
461
+ return TF_ERROR;
462
+ }
214
463
  return TF_OK;
215
464
  }
216
465
 
466
+ int tf_batch_clone_with_extra_cols(tf_batch *dst, const tf_batch *src,
467
+ const char **names, const tf_type *types,
468
+ size_t n_extra) {
469
+ if (!dst || !src) return TF_ERROR;
470
+ size_t needed_cols = 0;
471
+ if (tf_size_add(src->n_cols, n_extra, &needed_cols) != TF_OK || dst->n_cols < needed_cols)
472
+ return TF_ERROR;
473
+ if (n_extra > 0 && (!names || !types)) return TF_ERROR;
474
+ if (tf_batch_clone_schema(dst, src) != TF_OK) return TF_ERROR;
475
+ for (size_t i = 0; i < n_extra; i++) {
476
+ if (tf_batch_set_schema(dst, src->n_cols + i, names[i], types[i]) != TF_OK)
477
+ return TF_ERROR;
478
+ }
479
+ return TF_OK;
480
+ }
481
+
482
+ int tf_batch_clone_with_selected_extra_cols(tf_batch *dst,
483
+ const tf_batch *src,
484
+ const tf_batch *extra,
485
+ const int *extra_cols,
486
+ size_t n_extra) {
487
+ if (!dst || !src) return TF_ERROR;
488
+ size_t needed_cols = 0;
489
+ if (tf_size_add(src->n_cols, n_extra, &needed_cols) != TF_OK || dst->n_cols < needed_cols)
490
+ return TF_ERROR;
491
+ if (n_extra > 0 && (!extra || !extra_cols)) return TF_ERROR;
492
+ if (tf_batch_clone_schema(dst, src) != TF_OK) return TF_ERROR;
493
+ for (size_t i = 0; i < n_extra; i++) {
494
+ if (extra_cols[i] < 0 || (size_t)extra_cols[i] >= extra->n_cols)
495
+ return TF_ERROR;
496
+ size_t src_col = (size_t)extra_cols[i];
497
+ if (tf_batch_set_schema(dst, src->n_cols + i,
498
+ extra->col_names[src_col],
499
+ extra->col_types[src_col]) != TF_OK)
500
+ return TF_ERROR;
501
+ }
502
+ return TF_OK;
503
+ }
504
+
505
+ int tf_batch_copy_selected_row(tf_batch *dst, size_t dst_row,
506
+ const tf_batch *src, size_t src_row,
507
+ const size_t *cols, size_t n_cols) {
508
+ if (!dst || !src || !cols || src_row >= src->n_rows || dst->n_cols < n_cols) return TF_ERROR;
509
+ size_t need_rows = 0;
510
+ if (tf_size_add(dst_row, 1, &need_rows) != TF_OK) return TF_ERROR;
511
+ if (tf_batch_ensure_capacity(dst, need_rows) != TF_OK) return TF_ERROR;
512
+ for (size_t i = 0; i < n_cols; i++) {
513
+ if (cols[i] == SIZE_MAX) {
514
+ if (tf_batch_set_null(dst, dst_row, i) != TF_OK) return TF_ERROR;
515
+ continue;
516
+ }
517
+ if (cols[i] >= src->n_cols) return TF_ERROR;
518
+ if (tf_batch_copy_cell(dst, dst_row, i, src, src_row, cols[i]) != TF_OK)
519
+ return TF_ERROR;
520
+ }
521
+ return TF_OK;
522
+ }
523
+
524
+ int tf_batch_copy_selected_columns(tf_batch *dst,
525
+ const tf_batch *src,
526
+ const size_t *cols,
527
+ size_t n_cols) {
528
+ if (!dst || !src || !cols || dst->n_cols < n_cols) return TF_ERROR;
529
+ if (tf_batch_ensure_capacity(dst, src->n_rows) != TF_OK) return TF_ERROR;
530
+ dst->n_rows = 0;
531
+
532
+ for (size_t dc = 0; dc < n_cols; dc++) {
533
+ size_t sc = cols[dc];
534
+ if (sc == SIZE_MAX) {
535
+ if (dst->nulls[dc] && src->n_rows > 0)
536
+ memset(dst->nulls[dc], 1, src->n_rows);
537
+ continue;
538
+ }
539
+ if (sc >= src->n_cols || dst->col_types[dc] != src->col_types[sc])
540
+ return TF_ERROR;
541
+
542
+ if (src->n_rows == 0 || dst->col_types[dc] == TF_TYPE_NULL)
543
+ continue;
544
+ if (!dst->columns[dc] || !dst->nulls[dc] || !src->columns[sc] || !src->nulls[sc])
545
+ return TF_ERROR;
546
+
547
+ memcpy(dst->nulls[dc], src->nulls[sc], src->n_rows);
548
+ size_t bytes = 0;
549
+ switch (dst->col_types[dc]) {
550
+ case TF_TYPE_BOOL:
551
+ if (tf_size_mul(src->n_rows, sizeof(uint8_t), &bytes) != TF_OK) return TF_ERROR;
552
+ memcpy(dst->columns[dc], src->columns[sc], bytes);
553
+ break;
554
+ case TF_TYPE_INT64:
555
+ case TF_TYPE_TIMESTAMP:
556
+ if (tf_size_mul(src->n_rows, sizeof(int64_t), &bytes) != TF_OK) return TF_ERROR;
557
+ memcpy(dst->columns[dc], src->columns[sc], bytes);
558
+ break;
559
+ case TF_TYPE_FLOAT64:
560
+ if (tf_size_mul(src->n_rows, sizeof(double), &bytes) != TF_OK) return TF_ERROR;
561
+ memcpy(dst->columns[dc], src->columns[sc], bytes);
562
+ break;
563
+ case TF_TYPE_DATE:
564
+ if (tf_size_mul(src->n_rows, sizeof(int32_t), &bytes) != TF_OK) return TF_ERROR;
565
+ memcpy(dst->columns[dc], src->columns[sc], bytes);
566
+ break;
567
+ case TF_TYPE_STRING: {
568
+ char **dst_col = (char **)dst->columns[dc];
569
+ char **src_col = (char **)src->columns[sc];
570
+ for (size_t r = 0; r < src->n_rows; r++) {
571
+ if (src->nulls[sc][r]) continue;
572
+ const char *value = src_col[r] ? src_col[r] : "";
573
+ size_t len = 0, alloc_bytes = 0;
574
+ if (tf_string_length_bounded(value, TF_MAX_CELL_BYTES,
575
+ &len, "batch", "cell") != TF_OK ||
576
+ tf_size_add(len, 1, &alloc_bytes) != TF_OK)
577
+ return TF_ERROR;
578
+ char *copy = tf_arena_alloc(dst->arena, alloc_bytes);
579
+ if (!copy) return TF_ERROR;
580
+ memcpy(copy, value, alloc_bytes);
581
+ dst_col[r] = copy;
582
+ }
583
+ break;
584
+ }
585
+ default:
586
+ return TF_ERROR;
587
+ }
588
+ }
589
+
590
+ if (src->n_rows > 0 && tf_batch_expose_row(dst, src->n_rows - 1) != TF_OK)
591
+ return TF_ERROR;
592
+ return TF_OK;
593
+ }
594
+
595
+ int tf_batch_copy_row(tf_batch *dst, size_t dst_row,
596
+ const tf_batch *src, size_t src_row) {
597
+ if (!dst || !src || src_row >= src->n_rows || dst->n_cols < src->n_cols) return TF_ERROR;
598
+ size_t need_rows = 0;
599
+ if (tf_size_add(dst_row, 1, &need_rows) != TF_OK) return TF_ERROR;
600
+ if (tf_batch_ensure_capacity(dst, need_rows) != TF_OK) return TF_ERROR;
601
+ for (size_t c = 0; c < src->n_cols; c++) {
602
+ if (tf_batch_copy_cell(dst, dst_row, c, src, src_row, c) != TF_OK)
603
+ return TF_ERROR;
604
+ }
605
+ return TF_OK;
606
+ }
607
+
608
+ int tf_batch_append_row(tf_batch *dst, const tf_batch *src, size_t src_row) {
609
+ if (!dst || !src || dst->n_cols != src->n_cols) return TF_ERROR;
610
+ size_t dst_row = dst->n_rows;
611
+ if (tf_batch_copy_row(dst, dst_row, src, src_row) != TF_OK) return TF_ERROR;
612
+ return tf_batch_expose_row(dst, dst_row);
613
+ }
614
+
615
+ int tf_batch_append_row_with_selected_extra(tf_batch *dst,
616
+ const tf_batch *left,
617
+ size_t left_row,
618
+ const tf_batch *extra,
619
+ size_t extra_row,
620
+ const int *extra_cols,
621
+ size_t n_extra) {
622
+ if (!dst || !left || left_row >= left->n_rows) return TF_ERROR;
623
+ size_t needed_cols = 0;
624
+ if (tf_size_add(left->n_cols, n_extra, &needed_cols) != TF_OK || dst->n_cols < needed_cols)
625
+ return TF_ERROR;
626
+ if (n_extra > 0 && (!extra || !extra_cols || extra_row >= extra->n_rows))
627
+ return TF_ERROR;
628
+
629
+ size_t dst_row = dst->n_rows;
630
+ size_t need_rows = 0;
631
+ if (tf_size_add(dst_row, 1, &need_rows) != TF_OK) return TF_ERROR;
632
+ if (tf_batch_ensure_capacity(dst, need_rows) != TF_OK) return TF_ERROR;
633
+
634
+ for (size_t c = 0; c < left->n_cols; c++) {
635
+ if (tf_batch_copy_cell(dst, dst_row, c, left, left_row, c) != TF_OK)
636
+ return TF_ERROR;
637
+ }
638
+ for (size_t i = 0; i < n_extra; i++) {
639
+ if (tf_batch_copy_cell_index(dst, dst_row, left->n_cols + i,
640
+ extra, extra_row, extra_cols[i]) != TF_OK)
641
+ return TF_ERROR;
642
+ }
643
+ return tf_batch_expose_row(dst, dst_row);
644
+ }
645
+
646
+ int tf_batch_append_row_with_null_extra(tf_batch *dst,
647
+ const tf_batch *left,
648
+ size_t left_row,
649
+ size_t n_extra) {
650
+ if (!dst || !left || left_row >= left->n_rows) return TF_ERROR;
651
+ size_t needed_cols = 0;
652
+ if (tf_size_add(left->n_cols, n_extra, &needed_cols) != TF_OK || dst->n_cols < needed_cols)
653
+ return TF_ERROR;
654
+
655
+ size_t dst_row = dst->n_rows;
656
+ size_t need_rows = 0;
657
+ if (tf_size_add(dst_row, 1, &need_rows) != TF_OK) return TF_ERROR;
658
+ if (tf_batch_ensure_capacity(dst, need_rows) != TF_OK) return TF_ERROR;
659
+
660
+ for (size_t c = 0; c < left->n_cols; c++) {
661
+ if (tf_batch_copy_cell(dst, dst_row, c, left, left_row, c) != TF_OK)
662
+ return TF_ERROR;
663
+ }
664
+ for (size_t i = 0; i < n_extra; i++) {
665
+ if (tf_batch_set_null(dst, dst_row, left->n_cols + i) != TF_OK)
666
+ return TF_ERROR;
667
+ }
668
+ return tf_batch_expose_row(dst, dst_row);
669
+ }
670
+
217
671
  int tf_batch_col_index(const tf_batch *b, const char *name) {
218
672
  for (size_t i = 0; i < b->n_cols; i++) {
219
673
  if (b->col_names[i] && strcmp(b->col_names[i], name) == 0)
@@ -222,8 +676,301 @@ int tf_batch_col_index(const tf_batch *b, const char *name) {
222
676
  return -1;
223
677
  }
224
678
 
679
+
680
+ /* ---- Audit / side-channel row serialization helpers ---- */
681
+
682
+ void tf_audit_options_init(tf_audit_options *opts, int default_include_row) {
683
+ if (!opts) return;
684
+ memset(opts, 0, sizeof(*opts));
685
+ opts->include_row = default_include_row ? 1 : 0;
686
+ }
687
+
688
+ static void audit_string_list_free(char **items, size_t n_items) {
689
+ if (!items) return;
690
+ for (size_t i = 0; i < n_items; i++) free(items[i]);
691
+ free(items);
692
+ }
693
+
694
+ void tf_audit_options_free(tf_audit_options *opts) {
695
+ if (!opts) return;
696
+ audit_string_list_free(opts->columns, opts->n_columns);
697
+ audit_string_list_free(opts->redact_columns, opts->n_redact_columns);
698
+ audit_string_list_free(opts->hash_columns, opts->n_hash_columns);
699
+ memset(opts, 0, sizeof(*opts));
700
+ }
701
+
702
+ static int audit_add_string(char ***items, size_t *n_items, const char *value) {
703
+ if (!items || !n_items || !value || !value[0]) return TF_ERROR;
704
+ char *dup = strdup(value);
705
+ if (!dup) return TF_ERROR;
706
+ size_t next_count = 0;
707
+ if (tf_size_add(*n_items, 1, &next_count) != TF_OK) {
708
+ free(dup);
709
+ return TF_ERROR;
710
+ }
711
+ char **next = tf_reallocarray_checked(*items, next_count, sizeof(char *));
712
+ if (!next) {
713
+ free(dup);
714
+ return TF_ERROR;
715
+ }
716
+ *items = next;
717
+ (*items)[*n_items] = dup;
718
+ (*n_items)++;
719
+ return TF_OK;
720
+ }
721
+
722
+ static char *audit_trim(char *s) {
723
+ while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
724
+ char *end = s + strlen(s);
725
+ while (end > s && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\n' || end[-1] == '\r')) end--;
726
+ *end = '\0';
727
+ return s;
728
+ }
729
+
730
+ static int audit_parse_string_list_value(const cJSON *value, char ***items, size_t *n_items,
731
+ const char *context, const char *name) {
732
+ if (!value) return TF_OK;
733
+ if (cJSON_IsArray(value)) {
734
+ int n = cJSON_GetArraySize((cJSON *)value);
735
+ for (int i = 0; i < n; i++) {
736
+ cJSON *item = cJSON_GetArrayItem((cJSON *)value, i);
737
+ if (!cJSON_IsString(item) || !item->valuestring[0]) goto invalid;
738
+ if (audit_add_string(items, n_items, item->valuestring) != TF_OK) return TF_ERROR;
739
+ }
740
+ return TF_OK;
741
+ }
742
+ if (cJSON_IsString(value)) {
743
+ char *copy = strdup(value->valuestring ? value->valuestring : "");
744
+ if (!copy) return TF_ERROR;
745
+ char *save = NULL;
746
+ for (char *tok = strtok_r(copy, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) {
747
+ char *trimmed = audit_trim(tok);
748
+ if (trimmed[0] && audit_add_string(items, n_items, trimmed) != TF_OK) {
749
+ free(copy);
750
+ return TF_ERROR;
751
+ }
752
+ }
753
+ free(copy);
754
+ return TF_OK;
755
+ }
756
+ invalid:
757
+ {
758
+ char msg[256];
759
+ snprintf(msg, sizeof(msg), "%s: %s must be a string or string array",
760
+ context ? context : "audit", name ? name : "audit option");
761
+ tf_set_last_error(msg);
762
+ }
763
+ return TF_ERROR;
764
+ }
765
+
766
+ static const cJSON *audit_get_arg(const cJSON *args, const char *snake, const char *camel) {
767
+ const cJSON *item = args && snake ? cJSON_GetObjectItemCaseSensitive(args, snake) : NULL;
768
+ if (!item && args && camel) item = cJSON_GetObjectItemCaseSensitive(args, camel);
769
+ return item;
770
+ }
771
+
772
+ int tf_audit_options_parse(tf_audit_options *opts, const cJSON *args, const char *context) {
773
+ if (!opts) return TF_ERROR;
774
+ if (!args) return TF_OK;
775
+
776
+ const cJSON *include = audit_get_arg(args, "audit_include_row", "auditIncludeRow");
777
+ if (include) {
778
+ if (!cJSON_IsBool(include)) {
779
+ char msg[256];
780
+ snprintf(msg, sizeof(msg), "%s: audit_include_row must be true or false", context ? context : "audit");
781
+ tf_set_last_error(msg);
782
+ return TF_ERROR;
783
+ }
784
+ opts->include_row = cJSON_IsTrue(include) ? 1 : 0;
785
+ }
786
+
787
+ if (audit_parse_string_list_value(audit_get_arg(args, "audit_columns", "auditColumns"),
788
+ &opts->columns, &opts->n_columns, context, "audit_columns") != TF_OK ||
789
+ audit_parse_string_list_value(audit_get_arg(args, "audit_redact", "auditRedact"),
790
+ &opts->redact_columns, &opts->n_redact_columns, context, "audit_redact") != TF_OK ||
791
+ audit_parse_string_list_value(audit_get_arg(args, "audit_hash_columns", "auditHashColumns"),
792
+ &opts->hash_columns, &opts->n_hash_columns, context, "audit_hash_columns") != TF_OK) {
793
+ return TF_ERROR;
794
+ }
795
+
796
+ if (tf_json_get_size_arg(args, "audit_max_bytes", 0, TF_MAX_ERROR_BYTES, &opts->max_bytes, context) < 0 ||
797
+ tf_json_get_size_arg(args, "auditMaxBytes", 0, TF_MAX_ERROR_BYTES, &opts->max_bytes, context) < 0 ||
798
+ tf_json_get_size_arg(args, "audit_max_cell_bytes", 0, TF_MAX_ERROR_BYTES, &opts->max_cell_bytes, context) < 0 ||
799
+ tf_json_get_size_arg(args, "auditMaxCellBytes", 0, TF_MAX_ERROR_BYTES, &opts->max_cell_bytes, context) < 0) {
800
+ return TF_ERROR;
801
+ }
802
+ return TF_OK;
803
+ }
804
+
805
+ static int audit_name_in_list(char **items, size_t n_items, const char *name) {
806
+ if (!name) return 0;
807
+ for (size_t i = 0; i < n_items; i++) {
808
+ if (items[i] && strcmp(items[i], name) == 0) return 1;
809
+ }
810
+ return 0;
811
+ }
812
+
813
+ int tf_audit_column_is_redacted(const tf_audit_options *opts, const char *column) {
814
+ return opts ? audit_name_in_list(opts->redact_columns, opts->n_redact_columns, column) : 0;
815
+ }
816
+
817
+ int tf_audit_column_is_hashed(const tf_audit_options *opts, const char *column) {
818
+ return opts ? audit_name_in_list(opts->hash_columns, opts->n_hash_columns, column) : 0;
819
+ }
820
+
821
+ int tf_audit_hash_string(const char *value, char *out, size_t out_size) {
822
+ if (!out || out_size < 25) return TF_ERROR;
823
+ uint64_t h = UINT64_C(1469598103934665603);
824
+ const unsigned char *p = (const unsigned char *)(value ? value : "");
825
+ while (*p) {
826
+ h ^= (uint64_t)*p++;
827
+ h *= UINT64_C(1099511628211);
828
+ }
829
+ snprintf(out, out_size, "fnv1a64:%016llx", (unsigned long long)h);
830
+ return TF_OK;
831
+ }
832
+
833
+ const char *tf_audit_format_string_for_column(const tf_audit_options *opts, const char *column,
834
+ const char *value, char *buf, size_t buf_size) {
835
+ const char *src = value ? value : "";
836
+ if (!opts || !buf || buf_size == 0) return src;
837
+ if (tf_audit_column_is_redacted(opts, column)) {
838
+ snprintf(buf, buf_size, "[REDACTED]");
839
+ return buf;
840
+ }
841
+ if (tf_audit_column_is_hashed(opts, column)) {
842
+ if (tf_audit_hash_string(src, buf, buf_size) == TF_OK) return buf;
843
+ return "[HASHED]";
844
+ }
845
+ if (opts->max_cell_bytes > 0 && strlen(src) > opts->max_cell_bytes) {
846
+ size_t n = opts->max_cell_bytes;
847
+ if (n + 4 > buf_size) n = buf_size > 4 ? buf_size - 4 : 0;
848
+ if (n > 0) memcpy(buf, src, n);
849
+ snprintf(buf + n, buf_size - n, "...");
850
+ return buf;
851
+ }
852
+ return src;
853
+ }
854
+
855
+ cJSON *tf_audit_string_to_json_for_column(const tf_audit_options *opts, const char *column,
856
+ const char *value) {
857
+ char *safe = tf_audit_format_string_dup_for_column(opts, column, value);
858
+ if (!safe) return NULL;
859
+ cJSON *item = cJSON_CreateString(safe);
860
+ free(safe);
861
+ return item;
862
+ }
863
+
864
+ char *tf_audit_format_string_dup_for_column(const tf_audit_options *opts,
865
+ const char *column,
866
+ const char *value) {
867
+ const char *src = value ? value : "";
868
+ if (!opts) return tf_strdup_checked(src);
869
+ if (tf_audit_column_is_redacted(opts, column)) return tf_strdup_checked("[REDACTED]");
870
+ if (tf_audit_column_is_hashed(opts, column)) {
871
+ char hash[32];
872
+ if (tf_audit_hash_string(src, hash, sizeof(hash)) != TF_OK) return tf_strdup_checked("[HASHED]");
873
+ return tf_strdup_checked(hash);
874
+ }
875
+ if (opts->max_cell_bytes > 0 && strlen(src) > opts->max_cell_bytes) {
876
+ size_t payload_len = opts->max_cell_bytes;
877
+ size_t with_ellipsis = 0;
878
+ size_t alloc_len = 0;
879
+ if (tf_size_add(payload_len, 3, &with_ellipsis) != TF_OK ||
880
+ tf_size_add(with_ellipsis, 1, &alloc_len) != TF_OK) {
881
+ return NULL;
882
+ }
883
+ char *out = tf_mallocarray_checked(alloc_len, sizeof(char));
884
+ if (!out) return NULL;
885
+ if (payload_len > 0) memcpy(out, src, payload_len);
886
+ memcpy(out + payload_len, "...", 3);
887
+ out[with_ellipsis] = '\0';
888
+ return out;
889
+ }
890
+ return tf_strdup_checked(src);
891
+ }
892
+
893
+ int tf_json_add_audit_string(cJSON *obj, const char *name,
894
+ const tf_audit_options *opts,
895
+ const char *column, const char *value) {
896
+ cJSON *item = tf_audit_string_to_json_for_column(opts, column, value);
897
+ if (!item) return TF_ERROR;
898
+ return tf_json_add_item(obj, name, item);
899
+ }
900
+
901
+ static cJSON *audit_string_json(const tf_audit_options *opts, const char *column, const char *value) {
902
+ return tf_audit_string_to_json_for_column(opts, column, value);
903
+ }
904
+
905
+ cJSON *tf_audit_cell_to_json(const tf_batch *b, size_t row, size_t col, const tf_audit_options *opts) {
906
+ const char *name = b->col_names[col] ? b->col_names[col] : "";
907
+ if (tf_batch_is_null(b, row, col)) return cJSON_CreateNull();
908
+ if (tf_audit_column_is_redacted(opts, name) || tf_audit_column_is_hashed(opts, name)) {
909
+ char raw[128];
910
+ const char *text = NULL;
911
+ if (tf_batch_format_cell_as_string(b, row, col, TF_CELL_STRING_NUMERIC_TIME,
912
+ raw, sizeof(raw), &text) != TF_OK)
913
+ return NULL;
914
+ return audit_string_json(opts, name, text ? text : "");
915
+ }
916
+ switch (b->col_types[col]) {
917
+ case TF_TYPE_BOOL: return cJSON_CreateBool(tf_batch_get_bool(b, row, col));
918
+ case TF_TYPE_INT64: return cJSON_CreateNumber((double)tf_batch_get_int64(b, row, col));
919
+ case TF_TYPE_FLOAT64: return cJSON_CreateNumber(tf_batch_get_float64(b, row, col));
920
+ case TF_TYPE_STRING: return audit_string_json(opts, name, tf_batch_get_string(b, row, col));
921
+ case TF_TYPE_DATE: return cJSON_CreateNumber((double)tf_batch_get_date(b, row, col));
922
+ case TF_TYPE_TIMESTAMP: return cJSON_CreateNumber((double)tf_batch_get_timestamp(b, row, col));
923
+ default: return cJSON_CreateNull();
924
+ }
925
+ }
926
+
927
+ cJSON *tf_audit_row_to_json(const tf_batch *b, size_t row, const tf_audit_options *opts) {
928
+ if (!b || row >= b->n_rows) return NULL;
929
+ if (opts && !opts->include_row) return NULL;
930
+ cJSON *obj = cJSON_CreateObject();
931
+ if (!obj) return NULL;
932
+ for (size_t c = 0; c < b->n_cols; c++) {
933
+ const char *name = b->col_names[c] ? b->col_names[c] : "";
934
+ if (opts && opts->n_columns > 0 && !audit_name_in_list(opts->columns, opts->n_columns, name)) continue;
935
+ cJSON *value = tf_audit_cell_to_json(b, row, c, opts);
936
+ if (!value || tf_json_add_item(obj, name, value) != TF_OK) {
937
+ cJSON_Delete(obj);
938
+ return NULL;
939
+ }
940
+ }
941
+ if (opts && opts->max_bytes > 0) {
942
+ char *printed = cJSON_PrintUnformatted(obj);
943
+ if (!printed) { cJSON_Delete(obj); return NULL; }
944
+ size_t n = strlen(printed);
945
+ free(printed);
946
+ if (n > opts->max_bytes) {
947
+ cJSON_Delete(obj);
948
+ obj = cJSON_CreateObject();
949
+ if (!obj) return NULL;
950
+ if (tf_json_add_bool(obj, "_audit_truncated", 1) != TF_OK ||
951
+ tf_json_add_number(obj, "max_bytes", (double)opts->max_bytes) != TF_OK) {
952
+ cJSON_Delete(obj);
953
+ return NULL;
954
+ }
955
+ }
956
+ }
957
+ return obj;
958
+ }
959
+
225
960
  void tf_batch_free(tf_batch *b) {
226
961
  if (!b) return;
227
962
  tf_arena *arena = b->arena;
228
963
  tf_arena_free(arena);
229
964
  }
965
+
966
+ void tf_batch_array_free_items(tf_batch **batches, size_t n_batches) {
967
+ if (!batches) return;
968
+ for (size_t i = 0; i < n_batches; i++) {
969
+ if (batches[i]) tf_batch_free(batches[i]);
970
+ }
971
+ }
972
+
973
+ void tf_batch_array_free(tf_batch **batches, size_t n_batches) {
974
+ tf_batch_array_free_items(batches, n_batches);
975
+ free(batches);
976
+ }