tranfi 0.1.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 (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +272 -40
  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 +348 -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 +8 -11
  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
@@ -15,15 +15,67 @@
15
15
  #include <stdlib.h>
16
16
  #include <string.h>
17
17
  #include <stdio.h>
18
+ #include <math.h>
18
19
 
19
20
  #define DEFAULT_BATCH_SIZE 1024
21
+ #define DEFAULT_MAX_ERROR_BYTES 4096
22
+ #define DEFAULT_MAX_RECORD_BYTES (64 * 1024 * 1024)
20
23
 
21
24
  /* ================================================================
22
25
  * JSONL Decoder
23
26
  * ================================================================ */
24
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
+
25
70
  typedef struct {
26
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. */
27
79
 
28
80
  /* Line accumulator */
29
81
  tf_buffer line_buf;
@@ -33,6 +85,9 @@ typedef struct {
33
85
  tf_type *col_types;
34
86
  size_t n_cols;
35
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. */
36
91
 
37
92
  /* Current batch */
38
93
  tf_batch *batch;
@@ -43,12 +98,13 @@ static tf_type json_to_type(const cJSON *val) {
43
98
  if (cJSON_IsNumber(val)) {
44
99
  /* Check if integer */
45
100
  double d = val->valuedouble;
46
- if (d == (double)(int64_t)d && d >= -9007199254740992.0 && d <= 9007199254740992.0)
101
+ if (d >= -9007199254740992.0 && d <= 9007199254740992.0 && d == (double)(int64_t)d)
47
102
  return TF_TYPE_INT64;
48
103
  return TF_TYPE_FLOAT64;
49
104
  }
50
105
  if (cJSON_IsString(val)) return TF_TYPE_STRING;
51
106
  if (cJSON_IsBool(val)) return TF_TYPE_BOOL;
107
+ if (cJSON_IsArray(val) || cJSON_IsObject(val)) return TF_TYPE_STRING;
52
108
  return TF_TYPE_NULL;
53
109
  }
54
110
 
@@ -61,117 +117,601 @@ static tf_type widen_type(tf_type current, tf_type incoming) {
61
117
  return TF_TYPE_STRING;
62
118
  }
63
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
+
64
427
  static tf_batch *make_jsonl_batch(jsonl_decoder_state *st) {
65
428
  tf_batch *b = tf_batch_create(st->n_cols, st->batch_size);
66
429
  if (!b) return NULL;
67
430
  for (size_t i = 0; i < st->n_cols; i++) {
68
- tf_batch_set_schema(b, i, st->col_names[i], st->col_types[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
+ }
69
435
  }
70
436
  return b;
71
437
  }
72
438
 
73
- static int add_json_row(jsonl_decoder_state *st, cJSON *obj) {
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) {
74
471
  if (!st->batch) {
75
472
  st->batch = make_jsonl_batch(st);
76
473
  if (!st->batch) return TF_ERROR;
77
474
  }
78
475
 
79
476
  size_t row = st->batch->n_rows;
80
- if (tf_batch_ensure_capacity(st->batch, row + 1) != TF_OK) return TF_ERROR;
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;
81
481
 
82
482
  for (size_t c = 0; c < st->n_cols; c++) {
83
- cJSON *val = cJSON_GetObjectItemCaseSensitive(obj, st->col_names[c]);
483
+ cJSON *val = st->values[st->aliases[c]];
484
+ int rc = TF_OK;
84
485
  if (!val || cJSON_IsNull(val)) {
85
- tf_batch_set_null(st->batch, row, c);
86
- continue;
87
- }
88
- switch (st->col_types[c]) {
89
- case TF_TYPE_BOOL:
90
- tf_batch_set_bool(st->batch, row, c, cJSON_IsTrue(val));
91
- break;
92
- case TF_TYPE_INT64:
93
- tf_batch_set_int64(st->batch, row, c, (int64_t)val->valuedouble);
94
- break;
95
- case TF_TYPE_FLOAT64:
96
- tf_batch_set_float64(st->batch, row, c, val->valuedouble);
97
- break;
98
- case TF_TYPE_STRING:
99
- if (cJSON_IsString(val))
100
- tf_batch_set_string(st->batch, row, c, val->valuestring);
101
- else {
102
- char *printed = cJSON_PrintUnformatted(val);
103
- tf_batch_set_string(st->batch, row, c, printed ? printed : "");
104
- free(printed);
105
- }
106
- break;
107
- default:
108
- tf_batch_set_null(st->batch, row, c);
109
- break;
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
+ }
110
512
  }
513
+ if (rc != TF_OK) return TF_ERROR;
111
514
  }
112
- st->batch->n_rows = row + 1;
515
+ if (tf_batch_expose_row(st->batch, row) != TF_OK) return TF_ERROR;
113
516
  st->rows_buffered++;
114
517
  return TF_OK;
115
518
  }
116
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
+
117
605
  static int process_jsonl_line(jsonl_decoder_state *st, const char *line, size_t len,
118
- tf_batch ***out, size_t *n_out, size_t *out_cap) {
606
+ size_t line_no, tf_batch ***out, size_t *n_out,
607
+ size_t *out_cap, tf_side_channels *side) {
119
608
  /* Skip empty lines */
120
609
  if (len == 0) return TF_OK;
121
610
 
122
- /* Parse JSON */
123
- cJSON *obj = cJSON_ParseWithLength(line, len);
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
+ }
124
623
  if (!obj || !cJSON_IsObject(obj)) {
624
+ const char *reason = obj ? "JSONL record is not an object" : "invalid JSON";
125
625
  cJSON_Delete(obj);
126
- return TF_OK; /* skip bad lines */
626
+ return handle_jsonl_malformed(st, line, len, line_no, reason, side);
127
627
  }
128
628
 
129
629
  if (!st->schema_ready) {
130
630
  /* Discover schema from first object */
131
631
  int n = cJSON_GetArraySize(obj);
132
- st->n_cols = (size_t)n;
133
- st->col_names = malloc(n * sizeof(char *));
134
- st->col_types = malloc(n * sizeof(tf_type));
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
+ }
135
651
 
136
- int i = 0;
652
+ size_t i = 0;
653
+ size_t schema_bytes = 0;
137
654
  cJSON *item;
138
655
  cJSON_ArrayForEach(item, obj) {
139
- st->col_names[i] = strdup(item->string);
140
- st->col_types[i] = json_to_type(item);
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;
141
678
  i++;
142
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; }
143
684
  st->schema_ready = 1;
144
- } else {
145
- /* Update types from new row */
146
- cJSON *item;
147
- cJSON_ArrayForEach(item, obj) {
148
- for (size_t c = 0; c < st->n_cols; c++) {
149
- if (strcmp(st->col_names[c], item->string) == 0) {
150
- st->col_types[c] = widen_type(st->col_types[c], json_to_type(item));
151
- break;
152
- }
153
- }
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;
154
697
  }
155
698
  }
156
-
157
- /* Update batch column types (since they might have widened) */
158
- if (st->batch) {
159
- for (size_t c = 0; c < st->n_cols; c++) {
160
- st->batch->col_types[c] = st->col_types[c];
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;
161
706
  }
162
707
  }
163
-
164
- int rc = add_json_row(st, obj);
708
+ int rc = add_json_row(st);
165
709
  cJSON_Delete(obj);
166
710
  if (rc != TF_OK) return rc;
167
711
 
168
712
  /* Emit batch if full */
169
713
  if (st->rows_buffered >= st->batch_size) {
170
- if (*n_out >= *out_cap) {
171
- *out_cap = (*out_cap == 0) ? 4 : *out_cap * 2;
172
- *out = realloc(*out, *out_cap * sizeof(tf_batch *));
173
- }
174
- (*out)[(*n_out)++] = st->batch;
714
+ if (emit_jsonl_batch(st->batch, out, n_out, out_cap) != TF_OK) return TF_ERROR;
175
715
  st->batch = NULL;
176
716
  st->rows_buffered = 0;
177
717
  }
@@ -180,11 +720,21 @@ static int process_jsonl_line(jsonl_decoder_state *st, const char *line, size_t
180
720
  }
181
721
 
182
722
  static int jsonl_decode(tf_decoder *self, const uint8_t *data, size_t len,
183
- tf_batch ***out, size_t *n_out) {
723
+ tf_batch ***out, size_t *n_out, tf_side_channels *side) {
184
724
  jsonl_decoder_state *st = self->state;
185
725
  *out = NULL;
186
726
  *n_out = 0;
187
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;
188
738
  if (tf_buffer_write(&st->line_buf, data, len) != TF_OK) return TF_ERROR;
189
739
 
190
740
  size_t out_cap = 0;
@@ -195,10 +745,14 @@ static int jsonl_decode(tf_decoder *self, const uint8_t *data, size_t len,
195
745
  for (size_t i = 0; i < buf_len; i++) {
196
746
  if (buf[i] == '\n' || buf[i] == '\r') {
197
747
  size_t line_len = i - line_start;
198
- if (buf[i] == '\r' && i + 1 < buf_len && buf[i + 1] == '\n') i++;
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
+ }
199
753
  if (line_len > 0) {
200
754
  if (process_jsonl_line(st, (const char *)buf + line_start, line_len,
201
- out, n_out, &out_cap) != TF_OK)
755
+ line_no, out, n_out, &out_cap, side) != TF_OK)
202
756
  return TF_ERROR;
203
757
  }
204
758
  line_start = i + 1;
@@ -206,11 +760,12 @@ static int jsonl_decode(tf_decoder *self, const uint8_t *data, size_t len,
206
760
  }
207
761
 
208
762
  st->line_buf.read_pos += line_start;
763
+ st->byte_offset += line_start;
209
764
  tf_buffer_compact(&st->line_buf);
210
765
  return TF_OK;
211
766
  }
212
767
 
213
- static int jsonl_flush(tf_decoder *self, tf_batch ***out, size_t *n_out) {
768
+ static int jsonl_flush(tf_decoder *self, tf_batch ***out, size_t *n_out, tf_side_channels *side) {
214
769
  jsonl_decoder_state *st = self->state;
215
770
  *out = NULL;
216
771
  *n_out = 0;
@@ -220,17 +775,20 @@ static int jsonl_flush(tf_decoder *self, tf_batch ***out, size_t *n_out) {
220
775
  size_t remaining = tf_buffer_readable(&st->line_buf);
221
776
  if (remaining > 0) {
222
777
  uint8_t *buf = st->line_buf.data + st->line_buf.read_pos;
223
- process_jsonl_line(st, (const char *)buf, remaining, out, n_out, &out_cap);
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;
224
785
  st->line_buf.read_pos = st->line_buf.len;
786
+ st->byte_offset += remaining;
225
787
  }
226
788
 
227
789
  /* Emit remaining batch */
228
790
  if (st->batch && st->rows_buffered > 0) {
229
- if (*n_out >= out_cap) {
230
- out_cap = (*n_out == 0) ? 1 : out_cap * 2;
231
- *out = realloc(*out, out_cap * sizeof(tf_batch *));
232
- }
233
- (*out)[(*n_out)++] = st->batch;
791
+ if (emit_jsonl_batch(st->batch, out, n_out, &out_cap) != TF_OK) return TF_ERROR;
234
792
  st->batch = NULL;
235
793
  st->rows_buffered = 0;
236
794
  }
@@ -243,9 +801,14 @@ static void jsonl_decoder_destroy(tf_decoder *self) {
243
801
  if (st) {
244
802
  tf_buffer_free(&st->line_buf);
245
803
  if (st->batch) tf_batch_free(st->batch);
246
- for (size_t i = 0; i < st->n_cols; i++) free(st->col_names[i]);
804
+ if (st->col_names) {
805
+ for (size_t i = 0; i < st->n_cols; i++) free(st->col_names[i]);
806
+ }
247
807
  free(st->col_names);
248
808
  free(st->col_types);
809
+ free(st->fields);
810
+ free(st->aliases);
811
+ free(st->values);
249
812
  free(st);
250
813
  }
251
814
  free(self);
@@ -256,10 +819,28 @@ tf_decoder *tf_jsonl_decoder_create(const cJSON *args) {
256
819
  if (!st) return NULL;
257
820
 
258
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;
259
825
  if (args) {
260
- cJSON *bs = cJSON_GetObjectItemCaseSensitive(args, "batch_size");
261
- if (cJSON_IsNumber(bs) && bs->valueint > 0)
262
- st->batch_size = (size_t)bs->valueint;
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;
263
844
  }
264
845
 
265
846
  tf_buffer_init(&st->line_buf);
@@ -277,79 +858,200 @@ tf_decoder *tf_jsonl_decoder_create(const cJSON *args) {
277
858
  * JSONL Encoder
278
859
  * ================================================================ */
279
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
+
280
996
  static int jsonl_encode(tf_encoder *self, tf_batch *in, tf_buffer *out) {
281
- (void)self;
997
+ jsonl_encoder_state *st = self->state;
998
+ if (jsonl_encoder_ensure_cache(st, in) != TF_OK) return TF_ERROR;
282
999
  char numbuf[64];
1000
+ #define JSONL_WRITE(expr) do { if ((expr) != TF_OK) return TF_ERROR; } while (0)
283
1001
 
284
1002
  for (size_t r = 0; r < in->n_rows; r++) {
285
- tf_buffer_write(out, (const uint8_t *)"{", 1);
1003
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)"{", 1));
286
1004
  for (size_t c = 0; c < in->n_cols; c++) {
287
- if (c > 0) tf_buffer_write(out, (const uint8_t *)",", 1);
288
-
289
- /* Key */
290
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
291
- tf_buffer_write_str(out, in->col_names[c]);
292
- tf_buffer_write(out, (const uint8_t *)"\":", 2);
1005
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)st->key_prefixes[c],
1006
+ st->key_prefix_lens[c]));
293
1007
 
294
1008
  /* Value */
295
1009
  if (tf_batch_is_null(in, r, c)) {
296
- tf_buffer_write_str(out, "null");
1010
+ JSONL_WRITE(tf_buffer_write_str(out, "null"));
297
1011
  continue;
298
1012
  }
299
1013
  switch (in->col_types[c]) {
300
1014
  case TF_TYPE_BOOL:
301
- tf_buffer_write_str(out, tf_batch_get_bool(in, r, c) ? "true" : "false");
1015
+ JSONL_WRITE(tf_buffer_write_str(out, tf_batch_get_bool(in, r, c) ? "true" : "false"));
302
1016
  break;
303
1017
  case TF_TYPE_INT64:
304
1018
  snprintf(numbuf, sizeof(numbuf), "%lld",
305
1019
  (long long)tf_batch_get_int64(in, r, c));
306
- tf_buffer_write_str(out, numbuf);
1020
+ JSONL_WRITE(tf_buffer_write_str(out, numbuf));
307
1021
  break;
308
1022
  case TF_TYPE_FLOAT64:
309
- snprintf(numbuf, sizeof(numbuf), "%g",
310
- tf_batch_get_float64(in, r, c));
311
- tf_buffer_write_str(out, numbuf);
312
- break;
313
- case TF_TYPE_STRING: {
314
- const char *s = tf_batch_get_string(in, r, c);
315
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
316
- /* Escape special characters */
317
- for (const char *p = s; *p; p++) {
318
- switch (*p) {
319
- case '"': tf_buffer_write_str(out, "\\\""); break;
320
- case '\\': tf_buffer_write_str(out, "\\\\"); break;
321
- case '\n': tf_buffer_write_str(out, "\\n"); break;
322
- case '\r': tf_buffer_write_str(out, "\\r"); break;
323
- case '\t': tf_buffer_write_str(out, "\\t"); break;
324
- default: tf_buffer_write(out, (const uint8_t *)p, 1); break;
325
- }
1023
+ if (!isfinite(tf_batch_get_float64(in, r, c))) {
1024
+ tf_set_last_error("jsonl cannot encode nonfinite numbers");
1025
+ return TF_ERROR;
326
1026
  }
327
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
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)));
328
1034
  break;
329
- }
330
1035
  case TF_TYPE_DATE: {
331
- char dbuf[16];
1036
+ char dbuf[32];
332
1037
  tf_date_format(tf_batch_get_date(in, r, c), dbuf, sizeof(dbuf));
333
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
334
- tf_buffer_write_str(out, dbuf);
335
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
1038
+ JSONL_WRITE(jsonl_write_escaped_string(out, dbuf));
336
1039
  break;
337
1040
  }
338
1041
  case TF_TYPE_TIMESTAMP: {
339
1042
  char tsbuf[40];
340
1043
  tf_timestamp_format(tf_batch_get_timestamp(in, r, c), tsbuf, sizeof(tsbuf));
341
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
342
- tf_buffer_write_str(out, tsbuf);
343
- tf_buffer_write(out, (const uint8_t *)"\"", 1);
1044
+ JSONL_WRITE(jsonl_write_escaped_string(out, tsbuf));
344
1045
  break;
345
1046
  }
346
1047
  default:
347
- tf_buffer_write_str(out, "null");
1048
+ JSONL_WRITE(tf_buffer_write_str(out, "null"));
348
1049
  break;
349
1050
  }
350
1051
  }
351
- tf_buffer_write(out, (const uint8_t *)"}\n", 2);
1052
+ JSONL_WRITE(tf_buffer_write(out, (const uint8_t *)"}\n", 2));
352
1053
  }
1054
+ #undef JSONL_WRITE
353
1055
  return TF_OK;
354
1056
  }
355
1057
 
@@ -359,16 +1061,26 @@ static int jsonl_encoder_flush(tf_encoder *self, tf_buffer *out) {
359
1061
  }
360
1062
 
361
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
+ }
362
1069
  free(self);
363
1070
  }
364
1071
 
365
1072
  tf_encoder *tf_jsonl_encoder_create(const cJSON *args) {
366
1073
  (void)args;
1074
+ jsonl_encoder_state *st = calloc(1, sizeof(jsonl_encoder_state));
1075
+ if (!st) return NULL;
367
1076
  tf_encoder *enc = malloc(sizeof(tf_encoder));
368
- if (!enc) return NULL;
1077
+ if (!enc) {
1078
+ free(st);
1079
+ return NULL;
1080
+ }
369
1081
  enc->encode = jsonl_encode;
370
1082
  enc->flush = jsonl_encoder_flush;
371
1083
  enc->destroy = jsonl_encoder_destroy;
372
- enc->state = NULL;
1084
+ enc->state = st;
373
1085
  return enc;
374
1086
  }