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
@@ -0,0 +1,1086 @@
1
+ /*
2
+ * codec_jsonl.c — JSON Lines streaming decoder and encoder.
3
+ *
4
+ * Decoder: splits on newlines, parses each line as JSON object using cJSON.
5
+ * - First line establishes schema (column names and types).
6
+ * - Subsequent lines match schema; missing keys → null.
7
+ * - Emits batch at batch_size.
8
+ *
9
+ * Encoder: batch → one JSON object per row, newline-separated.
10
+ */
11
+
12
+ #include "internal.h"
13
+ #include "date_utils.h"
14
+ #include "cJSON.h"
15
+ #include <stdlib.h>
16
+ #include <string.h>
17
+ #include <stdio.h>
18
+ #include <math.h>
19
+
20
+ #define DEFAULT_BATCH_SIZE 1024
21
+ #define DEFAULT_MAX_ERROR_BYTES 4096
22
+ #define DEFAULT_MAX_RECORD_BYTES (64 * 1024 * 1024)
23
+
24
+ /* ================================================================
25
+ * JSONL Decoder
26
+ * ================================================================ */
27
+
28
+ typedef enum {
29
+ JSONL_ERROR_SKIP = 0,
30
+ JSONL_ERROR_FAIL,
31
+ JSONL_ERROR_WARN,
32
+ JSONL_ERROR_QUARANTINE,
33
+ } jsonl_error_action;
34
+
35
+ static const char *jsonl_error_action_name(jsonl_error_action action) {
36
+ switch (action) {
37
+ case JSONL_ERROR_FAIL: return "fail";
38
+ case JSONL_ERROR_WARN: return "warn";
39
+ case JSONL_ERROR_QUARANTINE: return "quarantine";
40
+ case JSONL_ERROR_SKIP:
41
+ default: return "skip";
42
+ }
43
+ }
44
+
45
+ static int parse_jsonl_error_action(const char *s, jsonl_error_action *out) {
46
+ if (!s || strcmp(s, "skip") == 0 || strcmp(s, "ignore") == 0) {
47
+ *out = JSONL_ERROR_SKIP;
48
+ return 1;
49
+ }
50
+ if (strcmp(s, "fail") == 0 || strcmp(s, "error") == 0 || strcmp(s, "stop") == 0) {
51
+ *out = JSONL_ERROR_FAIL;
52
+ return 1;
53
+ }
54
+ if (strcmp(s, "warn") == 0 || strcmp(s, "warning") == 0) {
55
+ *out = JSONL_ERROR_WARN;
56
+ return 1;
57
+ }
58
+ if (strcmp(s, "quarantine") == 0 || strcmp(s, "dead-letter") == 0 || strcmp(s, "dead_letter") == 0) {
59
+ *out = JSONL_ERROR_QUARANTINE;
60
+ return 1;
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ typedef struct {
66
+ const char *name;
67
+ size_t column;
68
+ } jsonl_field;
69
+
70
+ typedef struct {
71
+ size_t batch_size;
72
+ jsonl_error_action on_error;
73
+ size_t max_error_bytes;
74
+ size_t max_record_bytes;
75
+ size_t line_number;
76
+ size_t byte_offset;
77
+ size_t bad_records;
78
+ int pending_lf; /* A CR at the end of the previous input chunk. */
79
+
80
+ /* Line accumulator */
81
+ tf_buffer line_buf;
82
+
83
+ /* Schema (discovered from first object) */
84
+ char **col_names;
85
+ tf_type *col_types;
86
+ size_t n_cols;
87
+ int schema_ready;
88
+ jsonl_field *fields; /* Sorted names, with original order breaking duplicate ties. */
89
+ size_t *aliases;
90
+ cJSON **values; /* Borrowed from the current parsed record only. */
91
+
92
+ /* Current batch */
93
+ tf_batch *batch;
94
+ size_t rows_buffered;
95
+ } jsonl_decoder_state;
96
+
97
+ static tf_type json_to_type(const cJSON *val) {
98
+ if (cJSON_IsNumber(val)) {
99
+ /* Check if integer */
100
+ double d = val->valuedouble;
101
+ if (d >= -9007199254740992.0 && d <= 9007199254740992.0 && d == (double)(int64_t)d)
102
+ return TF_TYPE_INT64;
103
+ return TF_TYPE_FLOAT64;
104
+ }
105
+ if (cJSON_IsString(val)) return TF_TYPE_STRING;
106
+ if (cJSON_IsBool(val)) return TF_TYPE_BOOL;
107
+ if (cJSON_IsArray(val) || cJSON_IsObject(val)) return TF_TYPE_STRING;
108
+ return TF_TYPE_NULL;
109
+ }
110
+
111
+ static tf_type widen_type(tf_type current, tf_type incoming) {
112
+ if (current == incoming) return current;
113
+ if (current == TF_TYPE_NULL) return incoming;
114
+ if (incoming == TF_TYPE_NULL) return current;
115
+ if (current == TF_TYPE_INT64 && incoming == TF_TYPE_FLOAT64) return TF_TYPE_FLOAT64;
116
+ if (current == TF_TYPE_FLOAT64 && incoming == TF_TYPE_INT64) return TF_TYPE_FLOAT64;
117
+ return TF_TYPE_STRING;
118
+ }
119
+
120
+ static size_t jsonl_type_size(tf_type t) {
121
+ switch (t) {
122
+ case TF_TYPE_BOOL: return sizeof(uint8_t);
123
+ case TF_TYPE_INT64: return sizeof(int64_t);
124
+ case TF_TYPE_FLOAT64: return sizeof(double);
125
+ case TF_TYPE_STRING: return sizeof(char *);
126
+ case TF_TYPE_DATE: return sizeof(int32_t);
127
+ case TF_TYPE_TIMESTAMP: return sizeof(int64_t);
128
+ default: return 0;
129
+ }
130
+ }
131
+
132
+ static void jsonl_free_schema_arrays(char **col_names, tf_type *col_types, size_t n_names) {
133
+ if (col_names) {
134
+ for (size_t i = 0; i < n_names; i++) free(col_names[i]);
135
+ }
136
+ free(col_names);
137
+ free(col_types);
138
+ }
139
+
140
+ static int jsonl_convert_batch_column(tf_batch *batch, size_t col, tf_type new_type) {
141
+ if (!batch || col >= batch->n_cols || batch->col_types[col] == new_type) return TF_OK;
142
+
143
+ tf_type old_type = batch->col_types[col];
144
+ size_t sz = jsonl_type_size(new_type);
145
+ void *new_col = NULL;
146
+ uint8_t *new_nulls = NULL;
147
+ if (sz == 0) {
148
+ batch->col_types[col] = new_type;
149
+ batch->columns[col] = NULL;
150
+ batch->nulls[col] = NULL;
151
+ return TF_OK;
152
+ }
153
+
154
+ size_t column_bytes = 0;
155
+ if (tf_size_mul(sz, batch->capacity, &column_bytes) != TF_OK) return TF_ERROR;
156
+ new_col = tf_arena_alloc(batch->arena, column_bytes);
157
+ new_nulls = tf_arena_alloc(batch->arena, batch->capacity);
158
+ if (!new_col || !new_nulls) return TF_ERROR;
159
+ memset(new_nulls, 1, batch->capacity);
160
+
161
+ for (size_t r = 0; r < batch->n_rows; r++) {
162
+ int was_null = (!batch->nulls[col] || batch->nulls[col][r] != 0);
163
+ if (was_null) continue;
164
+
165
+ if (new_type == TF_TYPE_FLOAT64 && old_type == TF_TYPE_INT64) {
166
+ ((double *)new_col)[r] = (double)((int64_t *)batch->columns[col])[r];
167
+ new_nulls[r] = 0;
168
+ } else if (new_type == TF_TYPE_FLOAT64 && old_type == TF_TYPE_FLOAT64) {
169
+ ((double *)new_col)[r] = ((double *)batch->columns[col])[r];
170
+ new_nulls[r] = 0;
171
+ } else if (new_type == TF_TYPE_STRING) {
172
+ char numbuf[64];
173
+ const char *s = NULL;
174
+ switch (old_type) {
175
+ case TF_TYPE_BOOL:
176
+ s = ((uint8_t *)batch->columns[col])[r] ? "true" : "false";
177
+ break;
178
+ case TF_TYPE_INT64:
179
+ snprintf(numbuf, sizeof(numbuf), "%lld",
180
+ (long long)((int64_t *)batch->columns[col])[r]);
181
+ s = numbuf;
182
+ break;
183
+ case TF_TYPE_FLOAT64:
184
+ if (tf_format_float64(numbuf, sizeof(numbuf),
185
+ ((double *)batch->columns[col])[r]) != TF_OK)
186
+ return TF_ERROR;
187
+ s = numbuf;
188
+ break;
189
+ case TF_TYPE_STRING:
190
+ s = ((char **)batch->columns[col])[r];
191
+ break;
192
+ default:
193
+ s = "";
194
+ break;
195
+ }
196
+ size_t len = 0, bytes = 0;
197
+ const char *safe_s = s ? s : "";
198
+ if (tf_string_length_bounded(safe_s, TF_MAX_CELL_BYTES,
199
+ &len, "jsonl", "cell") != TF_OK ||
200
+ tf_size_add(len, 1, &bytes) != TF_OK)
201
+ return TF_ERROR;
202
+ char *copy = tf_arena_alloc(batch->arena, bytes);
203
+ if (!copy) return TF_ERROR;
204
+ memcpy(copy, safe_s, len);
205
+ copy[len] = '\0';
206
+ ((char **)new_col)[r] = copy;
207
+ new_nulls[r] = 0;
208
+ }
209
+ }
210
+
211
+ batch->col_types[col] = new_type;
212
+ batch->columns[col] = new_col;
213
+ batch->nulls[col] = new_nulls;
214
+ return TF_OK;
215
+ }
216
+
217
+ static int jsonl_widen_column(jsonl_decoder_state *st, size_t col, tf_type incoming) {
218
+ tf_type widened = widen_type(st->col_types[col], incoming);
219
+ if (widened == st->col_types[col]) return TF_OK;
220
+ if (jsonl_convert_batch_column(st->batch, col, widened) != TF_OK) return TF_ERROR;
221
+ st->col_types[col] = widened;
222
+ return TF_OK;
223
+ }
224
+
225
+ static int emit_jsonl_malformed(jsonl_decoder_state *st, const char *line, size_t len,
226
+ size_t line_no, const char *reason,
227
+ tf_side_channels *side) {
228
+ st->bad_records++;
229
+ if (!side || !side->errors) return TF_OK;
230
+
231
+ size_t keep = len;
232
+ int truncated = 0;
233
+ if (st->max_error_bytes > 0 && keep > st->max_error_bytes) {
234
+ keep = st->max_error_bytes;
235
+ truncated = 1;
236
+ }
237
+ char *raw = malloc(keep + 1);
238
+ if (!raw) return TF_ERROR;
239
+ memcpy(raw, line, keep);
240
+ raw[keep] = '\0';
241
+
242
+ cJSON *obj = cJSON_CreateObject();
243
+ if (!obj) { free(raw); return TF_ERROR; }
244
+ int rc = TF_ERROR;
245
+ if (tf_json_add_string(obj, "type", "jsonl_malformed") != TF_OK ||
246
+ tf_json_add_string(obj, "op", "codec.jsonl.decode") != TF_OK ||
247
+ tf_json_add_string(obj, "action", jsonl_error_action_name(st->on_error)) != TF_OK ||
248
+ tf_json_add_string(obj, "severity", st->on_error == JSONL_ERROR_WARN ? "warning" : "error") != TF_OK ||
249
+ tf_json_add_number(obj, "line", (double)line_no) != TF_OK ||
250
+ tf_json_add_string(obj, "message", reason ? reason : "invalid JSONL record") != TF_OK ||
251
+ tf_json_add_number(obj, "raw_bytes", (double)len) != TF_OK ||
252
+ tf_json_add_string(obj, "raw", raw) != TF_OK ||
253
+ (truncated && tf_json_add_bool(obj, "truncated", 1) != TF_OK)) {
254
+ goto done;
255
+ }
256
+ rc = tf_buffer_write_json_line(side->errors, obj);
257
+ done:
258
+ cJSON_Delete(obj);
259
+ free(raw);
260
+ return rc;
261
+ }
262
+
263
+ static int handle_jsonl_malformed(jsonl_decoder_state *st, const char *line, size_t len,
264
+ size_t line_no, const char *reason,
265
+ tf_side_channels *side) {
266
+ if (st->on_error == JSONL_ERROR_SKIP) {
267
+ st->bad_records++;
268
+ return TF_OK;
269
+ }
270
+ if (emit_jsonl_malformed(st, line, len, line_no, reason, side) != TF_OK)
271
+ return TF_ERROR;
272
+ if (st->on_error == JSONL_ERROR_FAIL) {
273
+ char msg[256];
274
+ snprintf(msg, sizeof(msg), "jsonl decode failed at line %zu: %s",
275
+ line_no, reason ? reason : "invalid JSONL record");
276
+ tf_set_last_error(msg);
277
+ return TF_ERROR;
278
+ }
279
+ return TF_OK;
280
+ }
281
+
282
+ static char *jsonl_record_preview(jsonl_decoder_state *st,
283
+ const uint8_t *prefix, size_t prefix_len,
284
+ const uint8_t *suffix, size_t suffix_len,
285
+ int *truncated_out) {
286
+ size_t total = prefix_len + suffix_len;
287
+ size_t keep = total;
288
+ int truncated = 0;
289
+ if (st->max_error_bytes > 0 && keep > st->max_error_bytes) {
290
+ keep = st->max_error_bytes;
291
+ truncated = 1;
292
+ }
293
+ char *raw = malloc(keep + 1);
294
+ if (!raw) return NULL;
295
+ size_t copied = 0;
296
+ size_t n = prefix_len < keep ? prefix_len : keep;
297
+ if (n > 0 && prefix) {
298
+ memcpy(raw, prefix, n);
299
+ copied += n;
300
+ }
301
+ if (copied < keep && suffix) {
302
+ size_t m = keep - copied;
303
+ if (m > suffix_len) m = suffix_len;
304
+ if (m > 0) {
305
+ memcpy(raw + copied, suffix, m);
306
+ copied += m;
307
+ }
308
+ }
309
+ raw[copied] = '\0';
310
+ if (truncated_out) *truncated_out = truncated;
311
+ return raw;
312
+ }
313
+
314
+ static int emit_jsonl_record_size_diagnostic(jsonl_decoder_state *st,
315
+ const uint8_t *prefix, size_t prefix_len,
316
+ const uint8_t *suffix, size_t suffix_len,
317
+ size_t line_no, size_t byte_offset,
318
+ size_t observed_len,
319
+ tf_side_channels *side) {
320
+ if (!side || !side->errors) return TF_OK;
321
+ int truncated = 0;
322
+ char *raw = jsonl_record_preview(st, prefix, prefix_len, suffix, suffix_len, &truncated);
323
+ if (!raw) return TF_ERROR;
324
+
325
+ cJSON *obj = cJSON_CreateObject();
326
+ if (!obj) { free(raw); return TF_ERROR; }
327
+ int rc = TF_ERROR;
328
+ if (tf_json_add_string(obj, "type", "jsonl_record_too_large") != TF_OK ||
329
+ tf_json_add_string(obj, "op", "codec.jsonl.decode") != TF_OK ||
330
+ tf_json_add_string(obj, "action", "fail") != TF_OK ||
331
+ tf_json_add_string(obj, "severity", "error") != TF_OK ||
332
+ tf_json_add_number(obj, "line", (double)line_no) != TF_OK ||
333
+ tf_json_add_number(obj, "byte_offset", (double)byte_offset) != TF_OK ||
334
+ tf_json_add_number(obj, "max_record_bytes", (double)st->max_record_bytes) != TF_OK ||
335
+ tf_json_add_number(obj, "observed_bytes", (double)observed_len) != TF_OK ||
336
+ tf_json_add_string(obj, "message", "JSONL record exceeds max_record_bytes") != TF_OK ||
337
+ tf_json_add_number(obj, "raw_bytes", (double)observed_len) != TF_OK ||
338
+ tf_json_add_string(obj, "raw", raw) != TF_OK ||
339
+ (truncated && tf_json_add_bool(obj, "truncated", 1) != TF_OK)) {
340
+ goto done;
341
+ }
342
+ rc = tf_buffer_write_json_line(side->errors, obj);
343
+ done:
344
+ cJSON_Delete(obj);
345
+ free(raw);
346
+ return rc;
347
+ }
348
+
349
+ static int fail_jsonl_record_limit(jsonl_decoder_state *st,
350
+ const uint8_t *prefix, size_t prefix_len,
351
+ const uint8_t *suffix, size_t suffix_len,
352
+ size_t line_no, size_t byte_offset,
353
+ size_t observed_len,
354
+ tf_side_channels *side) {
355
+ if (emit_jsonl_record_size_diagnostic(st, prefix, prefix_len, suffix, suffix_len,
356
+ line_no, byte_offset, observed_len, side) != TF_OK)
357
+ return TF_ERROR;
358
+ char err[256];
359
+ snprintf(err, sizeof(err),
360
+ "jsonl record exceeds max_record_bytes at line %zu: max %zu bytes, observed %zu bytes",
361
+ line_no, st->max_record_bytes, observed_len);
362
+ tf_set_last_error(err);
363
+ return TF_ERROR;
364
+ }
365
+
366
+ static int check_jsonl_incoming_record_limit(jsonl_decoder_state *st,
367
+ const uint8_t *data, size_t len,
368
+ tf_side_channels *side) {
369
+ if (st->max_record_bytes == 0 || len == 0) return TF_OK;
370
+
371
+ size_t prefix_len = tf_buffer_readable(&st->line_buf);
372
+ const uint8_t *prefix = prefix_len > 0 ? st->line_buf.data + st->line_buf.read_pos : NULL;
373
+ size_t incoming_base = st->byte_offset + prefix_len;
374
+ size_t line_no = st->line_number + 1;
375
+ size_t line_offset = st->byte_offset;
376
+ size_t segment_start = 0;
377
+ size_t current_len = prefix_len;
378
+
379
+ for (size_t i = 0; i < len; i++) {
380
+ if (data[i] == '\n' || data[i] == '\r') {
381
+ if (data[i] == '\r' && i + 1 < len && data[i + 1] == '\n') i++;
382
+ prefix = NULL;
383
+ prefix_len = 0;
384
+ current_len = 0;
385
+ segment_start = i + 1;
386
+ line_offset = incoming_base + i + 1;
387
+ line_no++;
388
+ continue;
389
+ }
390
+ current_len++;
391
+ if (current_len > st->max_record_bytes) {
392
+ size_t suffix_len = i - segment_start + 1;
393
+ return fail_jsonl_record_limit(st, prefix, prefix_len,
394
+ data + segment_start, suffix_len,
395
+ line_no, line_offset, current_len, side);
396
+ }
397
+ }
398
+ return TF_OK;
399
+ }
400
+
401
+ static int check_jsonl_buffer_record_limit(jsonl_decoder_state *st, size_t observed_len,
402
+ size_t line_no, size_t byte_offset,
403
+ tf_side_channels *side) {
404
+ if (st->max_record_bytes == 0 || observed_len <= st->max_record_bytes) return TF_OK;
405
+ const uint8_t *buf = st->line_buf.data + st->line_buf.read_pos;
406
+ return fail_jsonl_record_limit(st, buf, observed_len, NULL, 0,
407
+ line_no, byte_offset, observed_len, side);
408
+ }
409
+
410
+ static int emit_jsonl_batch(tf_batch *batch, tf_batch ***out, size_t *n_out, size_t *out_cap) {
411
+ if (*n_out >= *out_cap) {
412
+ size_t need = 0;
413
+ size_t new_cap = 0;
414
+ if (tf_size_add(*n_out, 1, &need) != TF_OK ||
415
+ tf_size_grow_pow2(*out_cap, need, 4, &new_cap) != TF_OK) {
416
+ return TF_ERROR;
417
+ }
418
+ tf_batch **tmp = tf_reallocarray_checked(*out, new_cap, sizeof(tf_batch *));
419
+ if (!tmp) return TF_ERROR;
420
+ *out = tmp;
421
+ *out_cap = new_cap;
422
+ }
423
+ (*out)[(*n_out)++] = batch;
424
+ return TF_OK;
425
+ }
426
+
427
+ static tf_batch *make_jsonl_batch(jsonl_decoder_state *st) {
428
+ tf_batch *b = tf_batch_create(st->n_cols, st->batch_size);
429
+ if (!b) return NULL;
430
+ for (size_t i = 0; i < st->n_cols; i++) {
431
+ if (tf_batch_set_schema(b, i, st->col_names[i], st->col_types[i]) != TF_OK) {
432
+ tf_batch_free(b);
433
+ return NULL;
434
+ }
435
+ }
436
+ return b;
437
+ }
438
+
439
+ static int jsonl_field_compare(const void *a, const void *b) {
440
+ const jsonl_field *x = a, *y = b;
441
+ int cmp = strcmp(x->name, y->name);
442
+ return cmp ? cmp : (x->column > y->column) - (x->column < y->column);
443
+ }
444
+
445
+ static size_t jsonl_find_column(const jsonl_decoder_state *st, const char *name) {
446
+ size_t lo = 0, hi = st->n_cols;
447
+ while (lo < hi) {
448
+ size_t mid = lo + (hi - lo) / 2;
449
+ if (strcmp(st->fields[mid].name, name) < 0) lo = mid + 1;
450
+ else hi = mid;
451
+ }
452
+ return lo < st->n_cols && strcmp(st->fields[lo].name, name) == 0
453
+ ? st->fields[lo].column : st->n_cols;
454
+ }
455
+
456
+ static int jsonl_index_schema(jsonl_decoder_state *st) {
457
+ size_t n = st->n_cols ? st->n_cols : 1;
458
+ st->fields = tf_callocarray_checked(n, sizeof(*st->fields));
459
+ st->aliases = tf_callocarray_checked(n, sizeof(*st->aliases));
460
+ st->values = tf_callocarray_checked(n, sizeof(*st->values));
461
+ if (!st->fields || !st->aliases || !st->values) return TF_ERROR;
462
+ for (size_t c = 0; c < st->n_cols; c++)
463
+ st->fields[c] = (jsonl_field){st->col_names[c], c};
464
+ qsort(st->fields, st->n_cols, sizeof(*st->fields), jsonl_field_compare);
465
+ for (size_t c = 0; c < st->n_cols; c++)
466
+ st->aliases[c] = jsonl_find_column(st, st->col_names[c]);
467
+ return TF_OK;
468
+ }
469
+
470
+ static int add_json_row(jsonl_decoder_state *st) {
471
+ if (!st->batch) {
472
+ st->batch = make_jsonl_batch(st);
473
+ if (!st->batch) return TF_ERROR;
474
+ }
475
+
476
+ size_t row = st->batch->n_rows;
477
+ size_t need_rows = 0;
478
+ if (tf_size_add(row, 1, &need_rows) != TF_OK ||
479
+ tf_batch_ensure_capacity(st->batch, need_rows) != TF_OK)
480
+ return TF_ERROR;
481
+
482
+ for (size_t c = 0; c < st->n_cols; c++) {
483
+ cJSON *val = st->values[st->aliases[c]];
484
+ int rc = TF_OK;
485
+ if (!val || cJSON_IsNull(val)) {
486
+ rc = tf_batch_set_null(st->batch, row, c);
487
+ } else {
488
+ switch (st->col_types[c]) {
489
+ case TF_TYPE_BOOL:
490
+ rc = tf_batch_set_bool(st->batch, row, c, cJSON_IsTrue(val));
491
+ break;
492
+ case TF_TYPE_INT64:
493
+ rc = tf_batch_set_int64(st->batch, row, c, (int64_t)val->valuedouble);
494
+ break;
495
+ case TF_TYPE_FLOAT64:
496
+ rc = tf_batch_set_float64(st->batch, row, c, val->valuedouble);
497
+ break;
498
+ case TF_TYPE_STRING:
499
+ if (cJSON_IsString(val)) {
500
+ rc = tf_batch_set_string(st->batch, row, c, val->valuestring ? val->valuestring : "");
501
+ } else {
502
+ char *printed = cJSON_PrintUnformatted(val);
503
+ if (!printed) return TF_ERROR;
504
+ rc = tf_batch_set_string(st->batch, row, c, printed);
505
+ free(printed);
506
+ }
507
+ break;
508
+ default:
509
+ rc = tf_batch_set_null(st->batch, row, c);
510
+ break;
511
+ }
512
+ }
513
+ if (rc != TF_OK) return TF_ERROR;
514
+ }
515
+ if (tf_batch_expose_row(st->batch, row) != TF_OK) return TF_ERROR;
516
+ st->rows_buffered++;
517
+ return TF_OK;
518
+ }
519
+
520
+
521
+ static int jsonl_space(unsigned char c) {
522
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
523
+ }
524
+
525
+ /* cJSON owns structural parsing. Check lexemes it deliberately accepts more
526
+ * loosely, before its C strings can lose embedded NULs. */
527
+ static int jsonl_valid_lexemes(const char *text, size_t n) {
528
+ const unsigned char *s = (const unsigned char *)text;
529
+ for (size_t i = 0; i < n;) {
530
+ unsigned char c = s[i++];
531
+ if (c == '"') {
532
+ int closed = 0;
533
+ while (i < n) {
534
+ c = s[i++];
535
+ if (c == '"') { closed = 1; break; }
536
+ if (c < 0x20) return 0;
537
+ if (c == '\\') {
538
+ if (i == n) return 0;
539
+ c = s[i++];
540
+ if (c == 'u') {
541
+ if (n - i < 4) return 0;
542
+ unsigned value = 0;
543
+ for (size_t k = 0; k < 4; k++) {
544
+ unsigned char h = s[i++];
545
+ if (h >= '0' && h <= '9') value = value * 16 + h - '0';
546
+ else if (h >= 'a' && h <= 'f') value = value * 16 + h - 'a' + 10;
547
+ else if (h >= 'A' && h <= 'F') value = value * 16 + h - 'A' + 10;
548
+ else return 0;
549
+ }
550
+ if (!value) return 0;
551
+ } else if (!strchr("\"\\/bfnrt", c)) return 0;
552
+ } else if (c >= 0x80) {
553
+ unsigned value, minimum;
554
+ size_t extra;
555
+ if (c >= 0xc2 && c <= 0xdf) { value = c & 31; extra = 1; minimum = 0x80; }
556
+ else if (c >= 0xe0 && c <= 0xef) { value = c & 15; extra = 2; minimum = 0x800; }
557
+ else if (c >= 0xf0 && c <= 0xf4) { value = c & 7; extra = 3; minimum = 0x10000; }
558
+ else return 0;
559
+ if (n - i < extra) return 0;
560
+ while (extra--) {
561
+ if ((s[i] & 0xc0) != 0x80) return 0;
562
+ value = (value << 6) | (s[i++] & 63);
563
+ }
564
+ if (value < minimum || value > 0x10ffff ||
565
+ (value >= 0xd800 && value <= 0xdfff)) return 0;
566
+ }
567
+ }
568
+ if (!closed) return 0;
569
+ } else if (c == '-' || (c >= '0' && c <= '9')) {
570
+ i--;
571
+ if (s[i] == '-' && ++i == n) return 0;
572
+ if (s[i] == '0') i++;
573
+ else {
574
+ if (s[i] < '1' || s[i] > '9') return 0;
575
+ do { i++; } while (i < n && s[i] >= '0' && s[i] <= '9');
576
+ }
577
+ if (i < n && s[i] == '.') {
578
+ i++;
579
+ size_t start = i;
580
+ while (i < n && s[i] >= '0' && s[i] <= '9') i++;
581
+ if (i == start) return 0;
582
+ }
583
+ if (i < n && (s[i] == 'e' || s[i] == 'E')) {
584
+ i++;
585
+ if (i < n && (s[i] == '+' || s[i] == '-')) i++;
586
+ size_t start = i;
587
+ while (i < n && s[i] >= '0' && s[i] <= '9') i++;
588
+ if (i == start) return 0;
589
+ }
590
+ if (i < n && !jsonl_space(s[i]) && s[i] != ',' && s[i] != ']' && s[i] != '}')
591
+ return 0;
592
+ } else if (c >= 0x80 || (c < 0x20 && !jsonl_space(c))) return 0;
593
+ }
594
+ return 1;
595
+ }
596
+
597
+ static int jsonl_finite_values(const cJSON *obj) {
598
+ for (const cJSON *v = obj; v; v = v->next) {
599
+ if (cJSON_IsNumber(v) && !isfinite(v->valuedouble)) return 0;
600
+ if (v->child && !jsonl_finite_values(v->child)) return 0;
601
+ }
602
+ return 1;
603
+ }
604
+
605
+ static int process_jsonl_line(jsonl_decoder_state *st, const char *line, size_t len,
606
+ size_t line_no, tf_batch ***out, size_t *n_out,
607
+ size_t *out_cap, tf_side_channels *side) {
608
+ /* Skip empty lines */
609
+ if (len == 0) return TF_OK;
610
+
611
+ if (!jsonl_valid_lexemes(line, len))
612
+ return handle_jsonl_malformed(st, line, len, line_no, "invalid JSON", side);
613
+ const char *end = NULL;
614
+ cJSON *obj = cJSON_ParseWithLengthOpts(line, len, &end, 0);
615
+ if (!obj && cJSON_ParseHadAllocationFailure()) {
616
+ return TF_ERROR;
617
+ }
618
+ while (end && end < line + len && jsonl_space((unsigned char)*end)) end++;
619
+ if (obj && (end != line + len || !jsonl_finite_values(obj))) {
620
+ cJSON_Delete(obj);
621
+ return handle_jsonl_malformed(st, line, len, line_no, "invalid JSON", side);
622
+ }
623
+ if (!obj || !cJSON_IsObject(obj)) {
624
+ const char *reason = obj ? "JSONL record is not an object" : "invalid JSON";
625
+ cJSON_Delete(obj);
626
+ return handle_jsonl_malformed(st, line, len, line_no, reason, side);
627
+ }
628
+
629
+ if (!st->schema_ready) {
630
+ /* Discover schema from first object */
631
+ int n = cJSON_GetArraySize(obj);
632
+ if (n < 0) { cJSON_Delete(obj); return TF_ERROR; }
633
+ size_t n_cols = (size_t)n;
634
+ if (n_cols > TF_MAX_COLUMNS) {
635
+ char err[256];
636
+ snprintf(err, sizeof(err),
637
+ "jsonl schema exceeds max_columns: max %zu columns, observed %zu",
638
+ TF_MAX_COLUMNS, n_cols);
639
+ tf_set_last_error(err);
640
+ cJSON_Delete(obj);
641
+ return TF_ERROR;
642
+ }
643
+ char **col_names = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(char *));
644
+ tf_type *col_types = tf_callocarray_checked(n_cols ? n_cols : 1, sizeof(tf_type));
645
+ if (!col_names || !col_types) {
646
+ free(col_names);
647
+ free(col_types);
648
+ cJSON_Delete(obj);
649
+ return TF_ERROR;
650
+ }
651
+
652
+ size_t i = 0;
653
+ size_t schema_bytes = 0;
654
+ cJSON *item;
655
+ cJSON_ArrayForEach(item, obj) {
656
+ const char *raw_name = item->string ? item->string : "";
657
+ size_t name_len = 0, name_bytes = 0, next_schema_bytes = 0;
658
+ if (tf_string_length_bounded(raw_name, TF_MAX_COLUMN_NAME_BYTES,
659
+ &name_len, "jsonl", "column name") != TF_OK ||
660
+ tf_size_add(name_len, 1, &name_bytes) != TF_OK ||
661
+ tf_size_add(schema_bytes, name_bytes, &next_schema_bytes) != TF_OK ||
662
+ tf_check_byte_limit(next_schema_bytes, TF_MAX_SCHEMA_BYTES,
663
+ "jsonl", "schema") != TF_OK) {
664
+ jsonl_free_schema_arrays(col_names, col_types, i);
665
+ cJSON_Delete(obj);
666
+ return TF_ERROR;
667
+ }
668
+ col_names[i] = malloc(name_bytes);
669
+ if (!col_names[i]) {
670
+ jsonl_free_schema_arrays(col_names, col_types, i);
671
+ cJSON_Delete(obj);
672
+ return TF_ERROR;
673
+ }
674
+ memcpy(col_names[i], raw_name, name_len);
675
+ col_names[i][name_len] = '\0';
676
+ col_types[i] = json_to_type(item);
677
+ schema_bytes = next_schema_bytes;
678
+ i++;
679
+ }
680
+ st->n_cols = n_cols;
681
+ st->col_names = col_names;
682
+ st->col_types = col_types;
683
+ if (jsonl_index_schema(st) != TF_OK) { cJSON_Delete(obj); return TF_ERROR; }
684
+ st->schema_ready = 1;
685
+ }
686
+ memset(st->values, 0, st->n_cols * sizeof(*st->values));
687
+ cJSON *item;
688
+ cJSON_ArrayForEach(item, obj) {
689
+ if (!item->string) continue;
690
+ size_t c = jsonl_find_column(st, item->string);
691
+ if (c == st->n_cols) continue;
692
+ /* cJSON lookup historically selects the first duplicate value. */
693
+ if (!st->values[c]) st->values[c] = item;
694
+ if (jsonl_widen_column(st, c, json_to_type(item)) != TF_OK) {
695
+ cJSON_Delete(obj);
696
+ return TF_ERROR;
697
+ }
698
+ }
699
+ for (size_t c = 0; c < st->n_cols; c++) {
700
+ cJSON *value = st->values[st->aliases[c]];
701
+ /* Duplicate schema names also receive that first value; ensure its
702
+ * numeric range is representable before a typed setter casts it. */
703
+ if (value && jsonl_widen_column(st, c, json_to_type(value)) != TF_OK) {
704
+ cJSON_Delete(obj);
705
+ return TF_ERROR;
706
+ }
707
+ }
708
+ int rc = add_json_row(st);
709
+ cJSON_Delete(obj);
710
+ if (rc != TF_OK) return rc;
711
+
712
+ /* Emit batch if full */
713
+ if (st->rows_buffered >= st->batch_size) {
714
+ if (emit_jsonl_batch(st->batch, out, n_out, out_cap) != TF_OK) return TF_ERROR;
715
+ st->batch = NULL;
716
+ st->rows_buffered = 0;
717
+ }
718
+
719
+ return TF_OK;
720
+ }
721
+
722
+ static int jsonl_decode(tf_decoder *self, const uint8_t *data, size_t len,
723
+ tf_batch ***out, size_t *n_out, tf_side_channels *side) {
724
+ jsonl_decoder_state *st = self->state;
725
+ *out = NULL;
726
+ *n_out = 0;
727
+
728
+ if (len && st->pending_lf) {
729
+ st->pending_lf = 0;
730
+ if (data[0] == '\n') {
731
+ data++;
732
+ len--;
733
+ st->byte_offset++;
734
+ }
735
+ }
736
+ if (!len) return TF_OK;
737
+ if (check_jsonl_incoming_record_limit(st, data, len, side) != TF_OK) return TF_ERROR;
738
+ if (tf_buffer_write(&st->line_buf, data, len) != TF_OK) return TF_ERROR;
739
+
740
+ size_t out_cap = 0;
741
+ uint8_t *buf = st->line_buf.data + st->line_buf.read_pos;
742
+ size_t buf_len = st->line_buf.len - st->line_buf.read_pos;
743
+
744
+ size_t line_start = 0;
745
+ for (size_t i = 0; i < buf_len; i++) {
746
+ if (buf[i] == '\n' || buf[i] == '\r') {
747
+ size_t line_len = i - line_start;
748
+ size_t line_no = ++st->line_number;
749
+ if (buf[i] == '\r') {
750
+ if (i + 1 < buf_len && buf[i + 1] == '\n') i++;
751
+ else if (i + 1 == buf_len) st->pending_lf = 1;
752
+ }
753
+ if (line_len > 0) {
754
+ if (process_jsonl_line(st, (const char *)buf + line_start, line_len,
755
+ line_no, out, n_out, &out_cap, side) != TF_OK)
756
+ return TF_ERROR;
757
+ }
758
+ line_start = i + 1;
759
+ }
760
+ }
761
+
762
+ st->line_buf.read_pos += line_start;
763
+ st->byte_offset += line_start;
764
+ tf_buffer_compact(&st->line_buf);
765
+ return TF_OK;
766
+ }
767
+
768
+ static int jsonl_flush(tf_decoder *self, tf_batch ***out, size_t *n_out, tf_side_channels *side) {
769
+ jsonl_decoder_state *st = self->state;
770
+ *out = NULL;
771
+ *n_out = 0;
772
+ size_t out_cap = 0;
773
+
774
+ /* Process remaining data */
775
+ size_t remaining = tf_buffer_readable(&st->line_buf);
776
+ if (remaining > 0) {
777
+ uint8_t *buf = st->line_buf.data + st->line_buf.read_pos;
778
+ size_t line_no = ++st->line_number;
779
+ size_t record_offset = st->byte_offset;
780
+ if (check_jsonl_buffer_record_limit(st, remaining, line_no, record_offset, side) != TF_OK)
781
+ return TF_ERROR;
782
+ if (process_jsonl_line(st, (const char *)buf, remaining, line_no,
783
+ out, n_out, &out_cap, side) != TF_OK)
784
+ return TF_ERROR;
785
+ st->line_buf.read_pos = st->line_buf.len;
786
+ st->byte_offset += remaining;
787
+ }
788
+
789
+ /* Emit remaining batch */
790
+ if (st->batch && st->rows_buffered > 0) {
791
+ if (emit_jsonl_batch(st->batch, out, n_out, &out_cap) != TF_OK) return TF_ERROR;
792
+ st->batch = NULL;
793
+ st->rows_buffered = 0;
794
+ }
795
+
796
+ return TF_OK;
797
+ }
798
+
799
+ static void jsonl_decoder_destroy(tf_decoder *self) {
800
+ jsonl_decoder_state *st = self->state;
801
+ if (st) {
802
+ tf_buffer_free(&st->line_buf);
803
+ if (st->batch) tf_batch_free(st->batch);
804
+ if (st->col_names) {
805
+ for (size_t i = 0; i < st->n_cols; i++) free(st->col_names[i]);
806
+ }
807
+ free(st->col_names);
808
+ free(st->col_types);
809
+ free(st->fields);
810
+ free(st->aliases);
811
+ free(st->values);
812
+ free(st);
813
+ }
814
+ free(self);
815
+ }
816
+
817
+ tf_decoder *tf_jsonl_decoder_create(const cJSON *args) {
818
+ jsonl_decoder_state *st = calloc(1, sizeof(jsonl_decoder_state));
819
+ if (!st) return NULL;
820
+
821
+ st->batch_size = DEFAULT_BATCH_SIZE;
822
+ st->on_error = JSONL_ERROR_SKIP;
823
+ st->max_error_bytes = DEFAULT_MAX_ERROR_BYTES;
824
+ st->max_record_bytes = DEFAULT_MAX_RECORD_BYTES;
825
+ if (args) {
826
+ size_t parsed_size = 0;
827
+ int has_batch_size = tf_json_get_size_arg(args, "batch_size", 1, TF_MAX_BATCH_ROWS, &parsed_size, "jsonl");
828
+ if (has_batch_size < 0) { free(st); return NULL; }
829
+ if (has_batch_size > 0) st->batch_size = parsed_size;
830
+ cJSON *on_error = cJSON_GetObjectItemCaseSensitive(args, "on_error");
831
+ if (cJSON_IsString(on_error)) {
832
+ if (!parse_jsonl_error_action(on_error->valuestring, &st->on_error)) {
833
+ tf_set_last_error("jsonl: on_error must be skip, fail, warn, or quarantine");
834
+ free(st);
835
+ return NULL;
836
+ }
837
+ }
838
+ int has_max_error = tf_json_get_size_arg(args, "max_error_bytes", 0, TF_MAX_ERROR_BYTES, &parsed_size, "jsonl");
839
+ if (has_max_error < 0) { free(st); return NULL; }
840
+ if (has_max_error > 0) st->max_error_bytes = parsed_size;
841
+ int has_max_record = tf_json_get_size_arg(args, "max_record_bytes", 0, TF_MAX_RECORD_BYTES, &parsed_size, "jsonl");
842
+ if (has_max_record < 0) { free(st); return NULL; }
843
+ if (has_max_record > 0) st->max_record_bytes = parsed_size;
844
+ }
845
+
846
+ tf_buffer_init(&st->line_buf);
847
+
848
+ tf_decoder *dec = malloc(sizeof(tf_decoder));
849
+ if (!dec) { free(st); return NULL; }
850
+ dec->decode = jsonl_decode;
851
+ dec->flush = jsonl_flush;
852
+ dec->destroy = jsonl_decoder_destroy;
853
+ dec->state = st;
854
+ return dec;
855
+ }
856
+
857
+ /* ================================================================
858
+ * JSONL Encoder
859
+ * ================================================================ */
860
+
861
+ typedef struct {
862
+ char **names;
863
+ char **key_prefixes;
864
+ size_t *key_prefix_lens;
865
+ size_t n_cols;
866
+ } jsonl_encoder_state;
867
+
868
+ static int jsonl_write_hex_escape(tf_buffer *out, unsigned char ch) {
869
+ static const char hex[] = "0123456789abcdef";
870
+ char esc[6] = {
871
+ '\\', 'u', '0', '0',
872
+ hex[(ch >> 4) & 0x0f],
873
+ hex[ch & 0x0f]
874
+ };
875
+ return tf_buffer_write(out, (const uint8_t *)esc, sizeof(esc));
876
+ }
877
+
878
+ static int jsonl_write_escaped_string(tf_buffer *out, const char *s) {
879
+ if (tf_buffer_write(out, (const uint8_t *)"\"", 1) != TF_OK) return TF_ERROR;
880
+ const unsigned char *start = (const unsigned char *)(s ? s : "");
881
+ const unsigned char *p = start;
882
+ while (*p) {
883
+ unsigned char ch = *p;
884
+ int needs_escape = ch < 0x20 || ch == '"' || ch == '\\';
885
+ if (!needs_escape) {
886
+ p++;
887
+ continue;
888
+ }
889
+
890
+ if (p > start && tf_buffer_write(out, start, (size_t)(p - start)) != TF_OK)
891
+ return TF_ERROR;
892
+
893
+ int rc = TF_OK;
894
+ switch (ch) {
895
+ case '"': rc = tf_buffer_write_str(out, "\\\""); break;
896
+ case '\\': rc = tf_buffer_write_str(out, "\\\\"); break;
897
+ case '\b': rc = tf_buffer_write_str(out, "\\b"); break;
898
+ case '\f': rc = tf_buffer_write_str(out, "\\f"); break;
899
+ case '\n': rc = tf_buffer_write_str(out, "\\n"); break;
900
+ case '\r': rc = tf_buffer_write_str(out, "\\r"); break;
901
+ case '\t': rc = tf_buffer_write_str(out, "\\t"); break;
902
+ default: rc = jsonl_write_hex_escape(out, ch); break;
903
+ }
904
+ if (rc != TF_OK) return TF_ERROR;
905
+ p++;
906
+ start = p;
907
+ }
908
+ if (p > start && tf_buffer_write(out, start, (size_t)(p - start)) != TF_OK)
909
+ return TF_ERROR;
910
+ return tf_buffer_write(out, (const uint8_t *)"\"", 1);
911
+ }
912
+
913
+ static void jsonl_encoder_clear_cache(jsonl_encoder_state *st) {
914
+ if (!st) return;
915
+ if (st->names) {
916
+ for (size_t i = 0; i < st->n_cols; i++) free(st->names[i]);
917
+ }
918
+ if (st->key_prefixes) {
919
+ for (size_t i = 0; i < st->n_cols; i++) free(st->key_prefixes[i]);
920
+ }
921
+ free(st->names);
922
+ free(st->key_prefixes);
923
+ free(st->key_prefix_lens);
924
+ st->names = NULL;
925
+ st->key_prefixes = NULL;
926
+ st->key_prefix_lens = NULL;
927
+ st->n_cols = 0;
928
+ }
929
+
930
+ static int jsonl_encoder_cache_matches(const jsonl_encoder_state *st, const tf_batch *in) {
931
+ if (!st || !in || st->n_cols != in->n_cols || !st->names) return 0;
932
+ for (size_t c = 0; c < in->n_cols; c++) {
933
+ const char *name = in->col_names[c] ? in->col_names[c] : "";
934
+ if (strcmp(st->names[c], name) != 0) return 0;
935
+ }
936
+ return 1;
937
+ }
938
+
939
+ static int jsonl_build_key_prefix(const char *name, int comma,
940
+ char **out_prefix, size_t *out_len) {
941
+ tf_buffer tmp;
942
+ tf_buffer_init(&tmp);
943
+ int rc = TF_ERROR;
944
+ if (comma && tf_buffer_write(&tmp, (const uint8_t *)",", 1) != TF_OK) goto done;
945
+ if (jsonl_write_escaped_string(&tmp, name) != TF_OK) goto done;
946
+ if (tf_buffer_write(&tmp, (const uint8_t *)":", 1) != TF_OK) goto done;
947
+
948
+ size_t len = tf_buffer_readable(&tmp);
949
+ size_t bytes = 0;
950
+ if (tf_size_add(len, 1, &bytes) != TF_OK) goto done;
951
+ char *copy = tf_mallocarray_checked(bytes, sizeof(char));
952
+ if (!copy) goto done;
953
+ if (len > 0) memcpy(copy, tmp.data + tmp.read_pos, len);
954
+ copy[len] = '\0';
955
+ *out_prefix = copy;
956
+ *out_len = len;
957
+ rc = TF_OK;
958
+ done:
959
+ tf_buffer_free(&tmp);
960
+ return rc;
961
+ }
962
+
963
+ static int jsonl_encoder_build_cache(jsonl_encoder_state *st, const tf_batch *in) {
964
+ jsonl_encoder_clear_cache(st);
965
+ size_t n = in->n_cols;
966
+ st->names = tf_callocarray_checked(n ? n : 1, sizeof(char *));
967
+ st->key_prefixes = tf_callocarray_checked(n ? n : 1, sizeof(char *));
968
+ st->key_prefix_lens = tf_callocarray_checked(n ? n : 1, sizeof(size_t));
969
+ if (!st->names || !st->key_prefixes || !st->key_prefix_lens) {
970
+ jsonl_encoder_clear_cache(st);
971
+ return TF_ERROR;
972
+ }
973
+ st->n_cols = n;
974
+
975
+ for (size_t c = 0; c < n; c++) {
976
+ const char *name = in->col_names[c] ? in->col_names[c] : "";
977
+ st->names[c] = tf_strdup_checked(name);
978
+ if (!st->names[c]) {
979
+ jsonl_encoder_clear_cache(st);
980
+ return TF_ERROR;
981
+ }
982
+ if (jsonl_build_key_prefix(name, c > 0, &st->key_prefixes[c],
983
+ &st->key_prefix_lens[c]) != TF_OK) {
984
+ jsonl_encoder_clear_cache(st);
985
+ return TF_ERROR;
986
+ }
987
+ }
988
+ return TF_OK;
989
+ }
990
+
991
+ static int jsonl_encoder_ensure_cache(jsonl_encoder_state *st, const tf_batch *in) {
992
+ if (jsonl_encoder_cache_matches(st, in)) return TF_OK;
993
+ return jsonl_encoder_build_cache(st, in);
994
+ }
995
+
996
+ static int jsonl_encode(tf_encoder *self, tf_batch *in, tf_buffer *out) {
997
+ jsonl_encoder_state *st = self->state;
998
+ if (jsonl_encoder_ensure_cache(st, in) != TF_OK) return TF_ERROR;
999
+ char numbuf[64];
1000
+ #define JSONL_WRITE(expr) do { if ((expr) != TF_OK) return TF_ERROR; } while (0)
1001
+
1002
+ for (size_t r = 0; r < in->n_rows; r++) {
1003
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)"{", 1));
1004
+ for (size_t c = 0; c < in->n_cols; c++) {
1005
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)st->key_prefixes[c],
1006
+ st->key_prefix_lens[c]));
1007
+
1008
+ /* Value */
1009
+ if (tf_batch_is_null(in, r, c)) {
1010
+ JSONL_WRITE(tf_buffer_write_str(out, "null"));
1011
+ continue;
1012
+ }
1013
+ switch (in->col_types[c]) {
1014
+ case TF_TYPE_BOOL:
1015
+ JSONL_WRITE(tf_buffer_write_str(out, tf_batch_get_bool(in, r, c) ? "true" : "false"));
1016
+ break;
1017
+ case TF_TYPE_INT64:
1018
+ snprintf(numbuf, sizeof(numbuf), "%lld",
1019
+ (long long)tf_batch_get_int64(in, r, c));
1020
+ JSONL_WRITE(tf_buffer_write_str(out, numbuf));
1021
+ break;
1022
+ case TF_TYPE_FLOAT64:
1023
+ if (!isfinite(tf_batch_get_float64(in, r, c))) {
1024
+ tf_set_last_error("jsonl cannot encode nonfinite numbers");
1025
+ return TF_ERROR;
1026
+ }
1027
+ if (tf_format_float64(numbuf, sizeof(numbuf),
1028
+ tf_batch_get_float64(in, r, c)) != TF_OK)
1029
+ return TF_ERROR;
1030
+ JSONL_WRITE(tf_buffer_write_str(out, numbuf));
1031
+ break;
1032
+ case TF_TYPE_STRING:
1033
+ JSONL_WRITE(jsonl_write_escaped_string(out, tf_batch_get_string(in, r, c)));
1034
+ break;
1035
+ case TF_TYPE_DATE: {
1036
+ char dbuf[32];
1037
+ tf_date_format(tf_batch_get_date(in, r, c), dbuf, sizeof(dbuf));
1038
+ JSONL_WRITE(jsonl_write_escaped_string(out, dbuf));
1039
+ break;
1040
+ }
1041
+ case TF_TYPE_TIMESTAMP: {
1042
+ char tsbuf[40];
1043
+ tf_timestamp_format(tf_batch_get_timestamp(in, r, c), tsbuf, sizeof(tsbuf));
1044
+ JSONL_WRITE(jsonl_write_escaped_string(out, tsbuf));
1045
+ break;
1046
+ }
1047
+ default:
1048
+ JSONL_WRITE(tf_buffer_write_str(out, "null"));
1049
+ break;
1050
+ }
1051
+ }
1052
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)"}\n", 2));
1053
+ }
1054
+ #undef JSONL_WRITE
1055
+ return TF_OK;
1056
+ }
1057
+
1058
+ static int jsonl_encoder_flush(tf_encoder *self, tf_buffer *out) {
1059
+ (void)self; (void)out;
1060
+ return TF_OK;
1061
+ }
1062
+
1063
+ static void jsonl_encoder_destroy(tf_encoder *self) {
1064
+ jsonl_encoder_state *st = self->state;
1065
+ if (st) {
1066
+ jsonl_encoder_clear_cache(st);
1067
+ free(st);
1068
+ }
1069
+ free(self);
1070
+ }
1071
+
1072
+ tf_encoder *tf_jsonl_encoder_create(const cJSON *args) {
1073
+ (void)args;
1074
+ jsonl_encoder_state *st = calloc(1, sizeof(jsonl_encoder_state));
1075
+ if (!st) return NULL;
1076
+ tf_encoder *enc = malloc(sizeof(tf_encoder));
1077
+ if (!enc) {
1078
+ free(st);
1079
+ return NULL;
1080
+ }
1081
+ enc->encode = jsonl_encode;
1082
+ enc->flush = jsonl_encoder_flush;
1083
+ enc->destroy = jsonl_encoder_destroy;
1084
+ enc->state = st;
1085
+ return enc;
1086
+ }