tranfi 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +177 -21
  2. package/NOTICE +8 -0
  3. package/README.md +627 -0
  4. package/app/assets/index-6quYZ5Ap.css +5 -0
  5. package/app/assets/index-BIAIKnrp.js +160 -0
  6. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  7. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  8. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  9. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  10. package/app/index.html +13 -0
  11. package/binding.gyp +121 -0
  12. package/csrc/arena.c +93 -0
  13. package/csrc/batch.c +976 -0
  14. package/csrc/buffer.c +154 -0
  15. package/csrc/cJSON.c +3386 -0
  16. package/csrc/cJSON.h +316 -0
  17. package/csrc/codec_csv.c +1951 -0
  18. package/csrc/codec_jsonl.c +1086 -0
  19. package/csrc/codec_table.c +248 -0
  20. package/csrc/codec_text.c +447 -0
  21. package/csrc/compiler.c +130 -0
  22. package/csrc/config.h +21 -0
  23. package/csrc/date_utils.h +94 -0
  24. package/csrc/dsl.c +5417 -0
  25. package/csrc/dsl.h +22 -0
  26. package/csrc/expr.c +1553 -0
  27. package/csrc/expr.h +58 -0
  28. package/csrc/internal.h +539 -0
  29. package/csrc/ir.c +166 -0
  30. package/csrc/ir.h +208 -0
  31. package/csrc/ir_schema.c +75 -0
  32. package/csrc/ir_serialize.c +166 -0
  33. package/csrc/ir_sql.c +1822 -0
  34. package/csrc/ir_validate.c +576 -0
  35. package/csrc/json_path.c +210 -0
  36. package/csrc/main.c +1241 -0
  37. package/csrc/memory_estimate.c +477 -0
  38. package/csrc/op_acf.c +283 -0
  39. package/csrc/op_across.c +477 -0
  40. package/csrc/op_anomaly.c +255 -0
  41. package/csrc/op_assert.c +761 -0
  42. package/csrc/op_bin.c +248 -0
  43. package/csrc/op_cast.c +523 -0
  44. package/csrc/op_clip.c +99 -0
  45. package/csrc/op_date_trunc.c +355 -0
  46. package/csrc/op_datetime.c +394 -0
  47. package/csrc/op_derive.c +216 -0
  48. package/csrc/op_diff.c +250 -0
  49. package/csrc/op_ewma.c +222 -0
  50. package/csrc/op_explode.c +206 -0
  51. package/csrc/op_fill_down.c +235 -0
  52. package/csrc/op_fill_null.c +268 -0
  53. package/csrc/op_filter.c +181 -0
  54. package/csrc/op_frequency.c +721 -0
  55. package/csrc/op_grep.c +181 -0
  56. package/csrc/op_group_agg.c +1956 -0
  57. package/csrc/op_hash.c +159 -0
  58. package/csrc/op_head.c +84 -0
  59. package/csrc/op_interpolate.c +445 -0
  60. package/csrc/op_join.c +2902 -0
  61. package/csrc/op_json_extract.c +227 -0
  62. package/csrc/op_json_filter.c +384 -0
  63. package/csrc/op_json_flatten.c +293 -0
  64. package/csrc/op_json_schema.c +503 -0
  65. package/csrc/op_label_encode.c +419 -0
  66. package/csrc/op_lag.c +181 -0
  67. package/csrc/op_lead.c +242 -0
  68. package/csrc/op_normalize.c +510 -0
  69. package/csrc/op_onehot.c +457 -0
  70. package/csrc/op_pivot.c +1754 -0
  71. package/csrc/op_quarantine.c +189 -0
  72. package/csrc/op_registry.c +3044 -0
  73. package/csrc/op_rename.c +129 -0
  74. package/csrc/op_replace.c +354 -0
  75. package/csrc/op_rleid.c +297 -0
  76. package/csrc/op_rowid.c +559 -0
  77. package/csrc/op_sample.c +158 -0
  78. package/csrc/op_schema.c +1341 -0
  79. package/csrc/op_schema_infer.c +252 -0
  80. package/csrc/op_select.c +340 -0
  81. package/csrc/op_set.c +3449 -0
  82. package/csrc/op_skip.c +95 -0
  83. package/csrc/op_sort.c +819 -0
  84. package/csrc/op_source_name.c +120 -0
  85. package/csrc/op_split.c +151 -0
  86. package/csrc/op_split_data.c +119 -0
  87. package/csrc/op_stack.c +271 -0
  88. package/csrc/op_stats.c +875 -0
  89. package/csrc/op_step.c +333 -0
  90. package/csrc/op_tail.c +105 -0
  91. package/csrc/op_tee.c +338 -0
  92. package/csrc/op_top.c +357 -0
  93. package/csrc/op_trim.c +138 -0
  94. package/csrc/op_unique.c +1343 -0
  95. package/csrc/op_unpivot.c +193 -0
  96. package/csrc/op_validate.c +648 -0
  97. package/csrc/op_window.c +591 -0
  98. package/csrc/path_policy.c +85 -0
  99. package/csrc/pipeline.c +1088 -0
  100. package/csrc/recipes.c +104 -0
  101. package/csrc/recipes.h +27 -0
  102. package/csrc/report.c +506 -0
  103. package/csrc/report.h +22 -0
  104. package/csrc/selector.c +1097 -0
  105. package/csrc/size_utils.c +348 -0
  106. package/csrc/spill.c +317 -0
  107. package/csrc/spill.h +21 -0
  108. package/csrc/tranfi.h +291 -0
  109. package/csrc/transform.h +209 -0
  110. package/csrc/transform_api.c +2237 -0
  111. package/csrc/transform_categorical.c +923 -0
  112. package/csrc/transform_internal.h +472 -0
  113. package/csrc/transform_json.c +3812 -0
  114. package/csrc/transform_numeric.c +1966 -0
  115. package/csrc/transform_sha256.c +154 -0
  116. package/csrc/transform_wasm.h +162 -0
  117. package/csrc/transform_wasm_api.c +1373 -0
  118. package/csrc/wasm_api.c +218 -0
  119. package/napi_api.c +534 -0
  120. package/napi_transform.c +1648 -0
  121. package/napi_transform.h +8 -0
  122. package/package.json +64 -59
  123. package/scripts/install-native.js +76 -0
  124. package/scripts/prepack.js +64 -0
  125. package/scripts/sync-csrc.js +23 -0
  126. package/src/cli.js +190 -0
  127. package/src/engines/duckdb.js +142 -0
  128. package/src/index.js +925 -0
  129. package/src/memory_policy.js +411 -0
  130. package/src/native.js +18 -0
  131. package/src/pipeline.js +709 -0
  132. package/src/recipe_json.js +80 -0
  133. package/src/server.js +279 -0
  134. package/src/transform.js +403 -0
  135. package/src/transform_error.js +10 -0
  136. package/src/wasm.js +21 -0
  137. package/wasm/index.js +732 -0
  138. package/wasm/package.json +1 -0
  139. package/wasm/tranfi_core.js +0 -0
  140. package/wasm/transform.js +1156 -0
  141. package/wasm/worker.js +786 -0
  142. package/dist/bundle.js +0 -1
  143. package/index.html +0 -18
  144. package/src/app.css +0 -169
  145. package/src/app.js +0 -203
  146. package/src/app.vue +0 -250
  147. package/src/bulma-input.vue +0 -110
  148. package/src/common-inputs.js +0 -28
  149. package/src/main.js +0 -20
  150. package/src/transforms.js +0 -166
  151. package/webpack.config.js +0 -108
package/csrc/batch.c ADDED
@@ -0,0 +1,976 @@
1
+ /*
2
+ * batch.c — Columnar batch: typed columns with per-cell null tracking.
3
+ */
4
+
5
+ #include "internal.h"
6
+ #include "cJSON.h"
7
+ #include "date_utils.h"
8
+ #include <stdio.h>
9
+ #include <stdlib.h>
10
+ #include <string.h>
11
+
12
+ static size_t type_size(tf_type t) {
13
+ switch (t) {
14
+ case TF_TYPE_BOOL: return sizeof(uint8_t);
15
+ case TF_TYPE_INT64: return sizeof(int64_t);
16
+ case TF_TYPE_FLOAT64: return sizeof(double);
17
+ case TF_TYPE_STRING: return sizeof(char *);
18
+ case TF_TYPE_DATE: return sizeof(int32_t);
19
+ case TF_TYPE_TIMESTAMP: return sizeof(int64_t);
20
+ default: return 0;
21
+ }
22
+ }
23
+
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
+
34
+ tf_arena *arena = tf_arena_create(0);
35
+ if (!arena) return NULL;
36
+
37
+ tf_batch *b = tf_arena_alloc(arena, sizeof(tf_batch));
38
+ if (!b) { tf_arena_free(arena); return NULL; }
39
+
40
+ b->arena = arena;
41
+ b->n_cols = n_cols;
42
+ b->n_rows = 0;
43
+ b->capacity = capacity;
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);
57
+
58
+ if (!b->col_names || !b->col_types || !b->columns || !b->nulls) {
59
+ tf_arena_free(arena);
60
+ return NULL;
61
+ }
62
+
63
+ for (size_t i = 0; i < n_cols; i++) {
64
+ b->col_names[i] = NULL;
65
+ b->col_types[i] = TF_TYPE_NULL;
66
+ b->columns[i] = NULL;
67
+ b->nulls[i] = NULL;
68
+ }
69
+
70
+ return b;
71
+ }
72
+
73
+ int tf_batch_set_schema(tf_batch *b, size_t col, const char *name, tf_type type) {
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;
91
+ size_t sz = type_size(type);
92
+ if (sz > 0 && b->capacity > 0) {
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 */
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;
106
+ return TF_OK;
107
+ }
108
+
109
+ int tf_batch_ensure_capacity(tf_batch *b, size_t min_rows) {
110
+ if (min_rows <= b->capacity) return TF_OK;
111
+
112
+ /* We need to reallocate columns — but arena doesn't support realloc.
113
+ * Allocate new larger arrays and copy. */
114
+ size_t new_cap = 0;
115
+ if (tf_size_grow_pow2(b->capacity, min_rows, 16, &new_cap) != TF_OK) return TF_ERROR;
116
+
117
+ for (size_t i = 0; i < b->n_cols; i++) {
118
+ size_t sz = type_size(b->col_types[i]);
119
+ if (sz == 0) continue;
120
+
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);
126
+ uint8_t *new_null = tf_arena_alloc(b->arena, new_cap);
127
+ if (!new_col || !new_null) return TF_ERROR;
128
+
129
+ if (b->columns[i] && b->n_rows > 0) {
130
+ memcpy(new_col, b->columns[i], copy_bytes);
131
+ memcpy(new_null, b->nulls[i], b->n_rows);
132
+ }
133
+ memset(new_null + b->n_rows, 1, new_cap - b->n_rows);
134
+ b->columns[i] = new_col;
135
+ b->nulls[i] = new_null;
136
+ }
137
+ b->capacity = new_cap;
138
+ return TF_OK;
139
+ }
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
+
150
+ /* ---- Setters ---- */
151
+
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;
157
+ }
158
+
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;
165
+ }
166
+
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;
173
+ }
174
+
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;
181
+ }
182
+
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;
206
+ }
207
+
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);
245
+ }
246
+ }
247
+
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
+ }
276
+ }
277
+ return tf_batch_set_cell_value(b, row, col, type, is_null, value ? &tmp : NULL);
278
+ }
279
+
280
+ /* ---- Getters ---- */
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
+
300
+ bool tf_batch_is_null(const tf_batch *b, size_t row, size_t col) {
301
+ if (!b || row >= b->n_rows || col >= b->n_cols) return true;
302
+ if (!b->nulls[col]) return true;
303
+ return b->nulls[col][row] != 0;
304
+ }
305
+
306
+ bool tf_batch_get_bool(const tf_batch *b, size_t row, size_t col) {
307
+ if (!b || row >= b->n_rows || col >= b->n_cols) return false;
308
+ return ((uint8_t *)b->columns[col])[row] != 0;
309
+ }
310
+
311
+ int64_t tf_batch_get_int64(const tf_batch *b, size_t row, size_t col) {
312
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
313
+ return ((int64_t *)b->columns[col])[row];
314
+ }
315
+
316
+ double tf_batch_get_float64(const tf_batch *b, size_t row, size_t col) {
317
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0.0;
318
+ return ((double *)b->columns[col])[row];
319
+ }
320
+
321
+ const char *tf_batch_get_string(const tf_batch *b, size_t row, size_t col) {
322
+ if (!b || row >= b->n_rows || col >= b->n_cols) return NULL;
323
+ return ((char **)b->columns[col])[row];
324
+ }
325
+
326
+ int32_t tf_batch_get_date(const tf_batch *b, size_t row, size_t col) {
327
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
328
+ return ((int32_t *)b->columns[col])[row];
329
+ }
330
+
331
+ int64_t tf_batch_get_timestamp(const tf_batch *b, size_t row, size_t col) {
332
+ if (!b || row >= b->n_rows || col >= b->n_cols) return 0;
333
+ return ((int64_t *)b->columns[col])[row];
334
+ }
335
+
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) {
347
+ case TF_TYPE_BOOL:
348
+ value.b = tf_batch_get_bool(src, src_row, src_col);
349
+ break;
350
+ case TF_TYPE_INT64:
351
+ value.i64 = tf_batch_get_int64(src, src_row, src_col);
352
+ break;
353
+ case TF_TYPE_FLOAT64:
354
+ value.f64 = tf_batch_get_float64(src, src_row, src_col);
355
+ break;
356
+ case TF_TYPE_STRING:
357
+ value.str = tf_batch_get_string(src, src_row, src_col);
358
+ break;
359
+ case TF_TYPE_DATE:
360
+ value.date = tf_batch_get_date(src, src_row, src_col);
361
+ break;
362
+ case TF_TYPE_TIMESTAMP:
363
+ value.i64 = tf_batch_get_timestamp(src, src_row, src_col);
364
+ break;
365
+ default:
366
+ break;
367
+ }
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
+ }
463
+ return TF_OK;
464
+ }
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
+
671
+ int tf_batch_col_index(const tf_batch *b, const char *name) {
672
+ for (size_t i = 0; i < b->n_cols; i++) {
673
+ if (b->col_names[i] && strcmp(b->col_names[i], name) == 0)
674
+ return (int)i;
675
+ }
676
+ return -1;
677
+ }
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
+
960
+ void tf_batch_free(tf_batch *b) {
961
+ if (!b) return;
962
+ tf_arena *arena = b->arena;
963
+ tf_arena_free(arena);
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
+ }