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,503 @@
1
+ /*
2
+ * op_json_schema.c -- Row-local JSON Schema subset validation.
3
+ *
4
+ * Config: {"column":"_line", "schema":{...}, "mode":"annotate", "result":"_valid"}
5
+ * Supported schema keywords: type, required, properties, items, enum, const,
6
+ * minLength, maxLength, minimum, maximum, exclusiveMinimum, exclusiveMaximum,
7
+ * minItems, maxItems. This is intentionally not a full JSON Schema engine.
8
+ */
9
+
10
+ #include "internal.h"
11
+ #include "cJSON.h"
12
+ #include <math.h>
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+
16
+ typedef enum {
17
+ JSON_SCHEMA_ANNOTATE,
18
+ JSON_SCHEMA_FILTER,
19
+ } json_schema_mode;
20
+
21
+ typedef struct {
22
+ char *column;
23
+ char *result;
24
+ cJSON *schema;
25
+ json_schema_mode mode;
26
+ size_t row_index;
27
+ size_t audit_limit;
28
+ size_t audit_emitted;
29
+ int audit;
30
+ tf_audit_options audit_opts;
31
+ } json_schema_state;
32
+
33
+ static int type_name_valid(const char *s) {
34
+ return strcmp(s, "object") == 0 || strcmp(s, "array") == 0 ||
35
+ strcmp(s, "string") == 0 || strcmp(s, "number") == 0 ||
36
+ strcmp(s, "integer") == 0 || strcmp(s, "boolean") == 0 ||
37
+ strcmp(s, "bool") == 0 || strcmp(s, "null") == 0;
38
+ }
39
+
40
+ static int schema_type_supported(const cJSON *type) {
41
+ if (!type) return 1;
42
+ if (cJSON_IsString(type)) return type_name_valid(type->valuestring);
43
+ if (!cJSON_IsArray(type)) return 0;
44
+ const cJSON *item = NULL;
45
+ cJSON_ArrayForEach(item, type) {
46
+ if (!cJSON_IsString(item) || !type_name_valid(item->valuestring)) return 0;
47
+ }
48
+ return 1;
49
+ }
50
+
51
+ static int string_array_supported(const cJSON *arr) {
52
+ if (!cJSON_IsArray(arr)) return 0;
53
+ const cJSON *item = NULL;
54
+ cJSON_ArrayForEach(item, arr) {
55
+ if (!cJSON_IsString(item)) return 0;
56
+ }
57
+ return 1;
58
+ }
59
+
60
+ static int schema_supported(const cJSON *schema);
61
+
62
+ static int properties_supported(const cJSON *props) {
63
+ if (!cJSON_IsObject(props)) return 0;
64
+ const cJSON *prop = NULL;
65
+ cJSON_ArrayForEach(prop, props) {
66
+ if (!schema_supported(prop)) return 0;
67
+ }
68
+ return 1;
69
+ }
70
+
71
+ static int schema_size_keyword_supported(const cJSON *item, const char *key) {
72
+ size_t parsed = 0;
73
+ size_t max_value = (strcmp(key, "minLength") == 0 || strcmp(key, "maxLength") == 0)
74
+ ? TF_MAX_RECORD_BYTES
75
+ : TF_MAX_COUNT_ARG;
76
+ return tf_json_size_value(item, key, 0, max_value, &parsed, "json-schema") == 1;
77
+ }
78
+
79
+ static int schema_number_keyword_supported(const cJSON *item) {
80
+ return cJSON_IsNumber(item) && isfinite(item->valuedouble);
81
+ }
82
+
83
+ static int schema_supported(const cJSON *schema) {
84
+ if (cJSON_IsBool(schema)) return 1;
85
+ if (!cJSON_IsObject(schema)) return 0;
86
+
87
+ const cJSON *child = NULL;
88
+ cJSON_ArrayForEach(child, schema) {
89
+ const char *key = child->string ? child->string : "";
90
+ if (strcmp(key, "$schema") == 0 || strcmp(key, "$id") == 0 ||
91
+ strcmp(key, "title") == 0 || strcmp(key, "description") == 0) {
92
+ continue;
93
+ }
94
+ if (strcmp(key, "type") == 0) {
95
+ if (!schema_type_supported(child)) return 0;
96
+ } else if (strcmp(key, "required") == 0) {
97
+ if (!string_array_supported(child)) return 0;
98
+ } else if (strcmp(key, "properties") == 0) {
99
+ if (!properties_supported(child)) return 0;
100
+ } else if (strcmp(key, "items") == 0) {
101
+ if (!schema_supported(child)) return 0;
102
+ } else if (strcmp(key, "enum") == 0) {
103
+ if (!cJSON_IsArray(child)) return 0;
104
+ } else if (strcmp(key, "const") == 0) {
105
+ continue;
106
+ } else if (strcmp(key, "minLength") == 0 || strcmp(key, "maxLength") == 0 ||
107
+ strcmp(key, "minItems") == 0 || strcmp(key, "maxItems") == 0) {
108
+ if (!schema_size_keyword_supported(child, key)) return 0;
109
+ } else if (strcmp(key, "minimum") == 0 || strcmp(key, "maximum") == 0) {
110
+ if (!schema_number_keyword_supported(child)) return 0;
111
+ } else if (strcmp(key, "exclusiveMinimum") == 0 || strcmp(key, "exclusiveMaximum") == 0) {
112
+ if (cJSON_IsNumber(child)) {
113
+ if (!schema_number_keyword_supported(child)) return 0;
114
+ } else if (!cJSON_IsBool(child)) {
115
+ return 0;
116
+ }
117
+ } else {
118
+ return 0;
119
+ }
120
+ }
121
+ return 1;
122
+ }
123
+
124
+ static int json_is_integer(const cJSON *v) {
125
+ if (!cJSON_IsNumber(v)) return 0;
126
+ double d = v->valuedouble;
127
+ if (!isfinite(d)) return 0;
128
+ double integral = 0.0;
129
+ return modf(d, &integral) == 0.0;
130
+ }
131
+
132
+ static int value_matches_type(const cJSON *value, const char *type) {
133
+ if (strcmp(type, "object") == 0) return cJSON_IsObject(value);
134
+ if (strcmp(type, "array") == 0) return cJSON_IsArray(value);
135
+ if (strcmp(type, "string") == 0) return cJSON_IsString(value);
136
+ if (strcmp(type, "number") == 0) return cJSON_IsNumber(value);
137
+ if (strcmp(type, "integer") == 0) return json_is_integer(value);
138
+ if (strcmp(type, "boolean") == 0 || strcmp(type, "bool") == 0) return cJSON_IsBool(value);
139
+ if (strcmp(type, "null") == 0) return cJSON_IsNull(value);
140
+ return 0;
141
+ }
142
+
143
+ static int value_matches_type_schema(const cJSON *value, const cJSON *type) {
144
+ if (!type) return 1;
145
+ if (cJSON_IsString(type)) return value_matches_type(value, type->valuestring);
146
+ if (!cJSON_IsArray(type)) return 0;
147
+ const cJSON *item = NULL;
148
+ cJSON_ArrayForEach(item, type) {
149
+ if (cJSON_IsString(item) && value_matches_type(value, item->valuestring)) return 1;
150
+ }
151
+ return 0;
152
+ }
153
+
154
+ static int validate_against_schema(const cJSON *schema, const cJSON *value);
155
+
156
+ static int validate_enum(const cJSON *schema, const cJSON *value) {
157
+ const cJSON *choices = cJSON_GetObjectItemCaseSensitive(schema, "enum");
158
+ if (!choices) return 1;
159
+ const cJSON *item = NULL;
160
+ cJSON_ArrayForEach(item, choices) {
161
+ if (cJSON_Compare(value, item, 1)) return 1;
162
+ }
163
+ return 0;
164
+ }
165
+
166
+ static int validate_const(const cJSON *schema, const cJSON *value) {
167
+ const cJSON *constant = cJSON_GetObjectItemCaseSensitive(schema, "const");
168
+ if (!constant) return 1;
169
+ return cJSON_Compare(value, constant, 1) ? 1 : 0;
170
+ }
171
+
172
+ static int validate_string_constraints(const cJSON *schema, const cJSON *value) {
173
+ const cJSON *min_len = cJSON_GetObjectItemCaseSensitive(schema, "minLength");
174
+ const cJSON *max_len = cJSON_GetObjectItemCaseSensitive(schema, "maxLength");
175
+ if (!min_len && !max_len) return 1;
176
+ if (!cJSON_IsString(value)) return 0;
177
+ size_t len = strlen(value->valuestring ? value->valuestring : "");
178
+ size_t limit = 0;
179
+ if (min_len) {
180
+ if (tf_json_size_value(min_len, "minLength", 0, TF_MAX_RECORD_BYTES,
181
+ &limit, "json-schema") != 1) return 0;
182
+ if (len < limit) return 0;
183
+ }
184
+ if (max_len) {
185
+ if (tf_json_size_value(max_len, "maxLength", 0, TF_MAX_RECORD_BYTES,
186
+ &limit, "json-schema") != 1) return 0;
187
+ if (len > limit) return 0;
188
+ }
189
+ return 1;
190
+ }
191
+
192
+ static int validate_number_constraints(const cJSON *schema, const cJSON *value) {
193
+ const cJSON *minimum = cJSON_GetObjectItemCaseSensitive(schema, "minimum");
194
+ const cJSON *maximum = cJSON_GetObjectItemCaseSensitive(schema, "maximum");
195
+ const cJSON *exclusive_min = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMinimum");
196
+ const cJSON *exclusive_max = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMaximum");
197
+ if (!minimum && !maximum && !exclusive_min && !exclusive_max) return 1;
198
+ if (!cJSON_IsNumber(value)) return 0;
199
+ double v = value->valuedouble;
200
+ if (minimum) {
201
+ int exclusive = cJSON_IsTrue(exclusive_min);
202
+ if ((exclusive && !(v > minimum->valuedouble)) || (!exclusive && !(v >= minimum->valuedouble))) return 0;
203
+ }
204
+ if (maximum) {
205
+ int exclusive = cJSON_IsTrue(exclusive_max);
206
+ if ((exclusive && !(v < maximum->valuedouble)) || (!exclusive && !(v <= maximum->valuedouble))) return 0;
207
+ }
208
+ if (exclusive_min && cJSON_IsNumber(exclusive_min) && !(v > exclusive_min->valuedouble)) return 0;
209
+ if (exclusive_max && cJSON_IsNumber(exclusive_max) && !(v < exclusive_max->valuedouble)) return 0;
210
+ return 1;
211
+ }
212
+
213
+ static int validate_object_constraints(const cJSON *schema, const cJSON *value) {
214
+ const cJSON *required = cJSON_GetObjectItemCaseSensitive(schema, "required");
215
+ const cJSON *props = cJSON_GetObjectItemCaseSensitive(schema, "properties");
216
+ if (!required && !props) return 1;
217
+ if (!cJSON_IsObject(value)) return 0;
218
+
219
+ const cJSON *item = NULL;
220
+ cJSON_ArrayForEach(item, required) {
221
+ if (!cJSON_GetObjectItemCaseSensitive((cJSON *)value, item->valuestring)) return 0;
222
+ }
223
+
224
+ const cJSON *prop_schema = NULL;
225
+ cJSON_ArrayForEach(prop_schema, props) {
226
+ const char *name = prop_schema->string ? prop_schema->string : "";
227
+ const cJSON *child = cJSON_GetObjectItemCaseSensitive((cJSON *)value, name);
228
+ if (child && !validate_against_schema(prop_schema, child)) return 0;
229
+ }
230
+ return 1;
231
+ }
232
+
233
+ static int validate_array_constraints(const cJSON *schema, const cJSON *value) {
234
+ const cJSON *items = cJSON_GetObjectItemCaseSensitive(schema, "items");
235
+ const cJSON *min_items = cJSON_GetObjectItemCaseSensitive(schema, "minItems");
236
+ const cJSON *max_items = cJSON_GetObjectItemCaseSensitive(schema, "maxItems");
237
+ if (!items && !min_items && !max_items) return 1;
238
+ if (!cJSON_IsArray(value)) return 0;
239
+
240
+ int raw_n = cJSON_GetArraySize((cJSON *)value);
241
+ size_t n = raw_n > 0 ? (size_t)raw_n : 0;
242
+ size_t limit = 0;
243
+ if (min_items) {
244
+ if (tf_json_size_value(min_items, "minItems", 0, TF_MAX_COUNT_ARG,
245
+ &limit, "json-schema") != 1) return 0;
246
+ if (n < limit) return 0;
247
+ }
248
+ if (max_items) {
249
+ if (tf_json_size_value(max_items, "maxItems", 0, TF_MAX_COUNT_ARG,
250
+ &limit, "json-schema") != 1) return 0;
251
+ if (n > limit) return 0;
252
+ }
253
+ if (items) {
254
+ const cJSON *item = NULL;
255
+ cJSON_ArrayForEach(item, value) {
256
+ if (!validate_against_schema(items, item)) return 0;
257
+ }
258
+ }
259
+ return 1;
260
+ }
261
+
262
+ static int validate_against_schema(const cJSON *schema, const cJSON *value) {
263
+ if (cJSON_IsBool(schema)) return cJSON_IsTrue(schema) ? 1 : 0;
264
+ if (!cJSON_IsObject(schema) || !value) return 0;
265
+
266
+ const cJSON *type = cJSON_GetObjectItemCaseSensitive(schema, "type");
267
+ if (!value_matches_type_schema(value, type)) return 0;
268
+ if (!validate_enum(schema, value)) return 0;
269
+ if (!validate_const(schema, value)) return 0;
270
+ if (!validate_string_constraints(schema, value)) return 0;
271
+ if (!validate_number_constraints(schema, value)) return 0;
272
+ if (!validate_object_constraints(schema, value)) return 0;
273
+ if (!validate_array_constraints(schema, value)) return 0;
274
+ return 1;
275
+ }
276
+
277
+ static const char *json_schema_mode_name(json_schema_mode mode) {
278
+ return mode == JSON_SCHEMA_FILTER ? "filter" : "annotate";
279
+ }
280
+
281
+ static int emit_json_schema_audit(json_schema_state *st, const tf_batch *b, size_t row,
282
+ tf_side_channels *side, const char *actual) {
283
+ if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
284
+ cJSON *obj = cJSON_CreateObject();
285
+ if (!obj) return TF_ERROR;
286
+ int rc = TF_ERROR;
287
+ if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
288
+ tf_json_add_string(obj, "op", "json-schema") != TF_OK ||
289
+ tf_json_add_string(obj, "event", "row_dropped") != TF_OK ||
290
+ tf_json_add_string(obj, "reason", "json_schema_failed") != TF_OK ||
291
+ tf_json_add_string(obj, "channel", "audit") != TF_OK ||
292
+ tf_json_add_string(obj, "mode", json_schema_mode_name(st->mode)) != TF_OK ||
293
+ tf_json_add_string(obj, "rule", "json-schema") != TF_OK ||
294
+ tf_json_add_string(obj, "column", st->column ? st->column : "_line") != TF_OK) {
295
+ goto done;
296
+ }
297
+ if (tf_json_add_audit_string(obj, "actual", &st->audit_opts, st->column,
298
+ actual ? actual : "schema_mismatch") != TF_OK ||
299
+ tf_json_add_number(obj, "row", (double)st->row_index) != TF_OK) {
300
+ goto done;
301
+ }
302
+ cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
303
+ if (row_obj) {
304
+ if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
305
+ } else if (st->audit_opts.include_row) {
306
+ goto done;
307
+ }
308
+ rc = tf_buffer_write_json_line(side->stats, obj);
309
+ done:
310
+ cJSON_Delete(obj);
311
+ if (rc == TF_OK) st->audit_emitted++;
312
+ return rc;
313
+ }
314
+
315
+ static int json_schema_process(tf_step *self, tf_batch *in, tf_batch **out,
316
+ tf_side_channels *side) {
317
+ json_schema_state *st = self->state;
318
+ *out = NULL;
319
+
320
+ size_t out_cols = in->n_cols + (st->mode == JSON_SCHEMA_ANNOTATE ? 1 : 0);
321
+ tf_batch *ob = tf_batch_create(out_cols, in->n_rows > 0 ? in->n_rows : 1);
322
+ if (!ob) return TF_ERROR;
323
+ if (st->mode == JSON_SCHEMA_ANNOTATE) {
324
+ const char *extra_names[] = {st->result};
325
+ const tf_type extra_types[] = {TF_TYPE_BOOL};
326
+ if (tf_batch_clone_with_extra_cols(ob, in, extra_names, extra_types, 1) != TF_OK) {
327
+ tf_batch_free(ob);
328
+ return TF_ERROR;
329
+ }
330
+ } else if (tf_batch_clone_schema(ob, in) != TF_OK) {
331
+ tf_batch_free(ob);
332
+ return TF_ERROR;
333
+ }
334
+
335
+ int ci = tf_batch_col_index(in, st->column);
336
+ int can_parse = ci >= 0 && in->col_types[(size_t)ci] == TF_TYPE_STRING;
337
+ size_t out_row = 0;
338
+
339
+ for (size_t r = 0; r < in->n_rows; r++) {
340
+ st->row_index++;
341
+ int valid = 0;
342
+ const char *actual = "schema_mismatch";
343
+ cJSON *root = NULL;
344
+ if (ci < 0) {
345
+ actual = "missing_column";
346
+ } else if (!can_parse) {
347
+ actual = "non_string";
348
+ } else if (tf_batch_is_null(in, r, (size_t)ci)) {
349
+ actual = "null";
350
+ } else {
351
+ const char *json = tf_batch_get_string(in, r, (size_t)ci);
352
+ if (!json || json[0] == '\0') {
353
+ actual = "empty_json";
354
+ } else {
355
+ root = cJSON_Parse(json);
356
+ if (root) {
357
+ valid = validate_against_schema(st->schema, root);
358
+ actual = valid ? "valid" : "schema_mismatch";
359
+ } else if (cJSON_ParseHadAllocationFailure()) {
360
+ tf_batch_free(ob);
361
+ return TF_ERROR;
362
+ } else {
363
+ actual = "invalid_json";
364
+ }
365
+ }
366
+ }
367
+
368
+ if (st->mode == JSON_SCHEMA_FILTER && !valid) {
369
+ if (emit_json_schema_audit(st, in, r, side, actual) != TF_OK) {
370
+ if (root) cJSON_Delete(root);
371
+ tf_batch_free(ob);
372
+ return TF_ERROR;
373
+ }
374
+ if (root) cJSON_Delete(root);
375
+ continue;
376
+ }
377
+ if (tf_batch_copy_row(ob, out_row, in, r) != TF_OK) {
378
+ if (root) cJSON_Delete(root);
379
+ tf_batch_free(ob);
380
+ return TF_ERROR;
381
+ }
382
+ if (st->mode == JSON_SCHEMA_ANNOTATE &&
383
+ tf_batch_set_bool(ob, out_row, in->n_cols, valid != 0) != TF_OK) {
384
+ if (root) cJSON_Delete(root);
385
+ tf_batch_free(ob);
386
+ return TF_ERROR;
387
+ }
388
+ if (tf_batch_expose_row(ob, out_row) != TF_OK) {
389
+ if (root) cJSON_Delete(root);
390
+ tf_batch_free(ob);
391
+ return TF_ERROR;
392
+ }
393
+ out_row++;
394
+ if (root) cJSON_Delete(root);
395
+ }
396
+
397
+ if (ob->n_rows > 0) *out = ob;
398
+ else tf_batch_free(ob);
399
+ return TF_OK;
400
+ }
401
+
402
+ static int json_schema_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
403
+ (void)self; (void)side; *out = NULL; return TF_OK;
404
+ }
405
+
406
+ static void json_schema_state_free(json_schema_state *st) {
407
+ if (!st) return;
408
+ free(st->column);
409
+ free(st->result);
410
+ if (st->schema) cJSON_Delete(st->schema);
411
+ tf_audit_options_free(&st->audit_opts);
412
+ free(st);
413
+ }
414
+
415
+ static void json_schema_destroy(tf_step *self) {
416
+ json_schema_state_free(self ? self->state : NULL);
417
+ free(self);
418
+ }
419
+
420
+ static int parse_mode(const char *s, json_schema_mode *out) {
421
+ if (!s || strcmp(s, "annotate") == 0 || strcmp(s, "check") == 0) {
422
+ *out = JSON_SCHEMA_ANNOTATE;
423
+ return 1;
424
+ }
425
+ if (strcmp(s, "filter") == 0 || strcmp(s, "keep") == 0) {
426
+ *out = JSON_SCHEMA_FILTER;
427
+ return 1;
428
+ }
429
+ return 0;
430
+ }
431
+
432
+ static cJSON *schema_arg_to_json(const cJSON *schema_j) {
433
+ if (!schema_j) return NULL;
434
+ if (cJSON_IsString(schema_j)) return cJSON_Parse(schema_j->valuestring);
435
+ if (cJSON_IsObject(schema_j) || cJSON_IsArray(schema_j) || cJSON_IsBool(schema_j)) {
436
+ return cJSON_Duplicate((cJSON *)schema_j, 1);
437
+ }
438
+ return NULL;
439
+ }
440
+
441
+ tf_step *tf_json_schema_create(const cJSON *args) {
442
+ if (!args) return NULL;
443
+ cJSON *schema_j = cJSON_GetObjectItemCaseSensitive(args, "schema");
444
+ cJSON *schema = schema_arg_to_json(schema_j);
445
+ if (!schema || !schema_supported(schema)) {
446
+ if (schema) cJSON_Delete(schema);
447
+ return NULL;
448
+ }
449
+
450
+ cJSON *column_j = cJSON_GetObjectItemCaseSensitive(args, "column");
451
+ const char *column = cJSON_IsString(column_j) && column_j->valuestring[0] != '\0'
452
+ ? column_j->valuestring : "_line";
453
+
454
+ cJSON *result_j = cJSON_GetObjectItemCaseSensitive(args, "result");
455
+ const char *result = cJSON_IsString(result_j) && result_j->valuestring[0] != '\0'
456
+ ? result_j->valuestring : "_valid";
457
+
458
+ json_schema_mode mode = JSON_SCHEMA_ANNOTATE;
459
+ cJSON *mode_j = cJSON_GetObjectItemCaseSensitive(args, "mode");
460
+ if (mode_j && (!cJSON_IsString(mode_j) || !parse_mode(mode_j->valuestring, &mode))) {
461
+ cJSON_Delete(schema);
462
+ return NULL;
463
+ }
464
+
465
+ json_schema_state *st = calloc(1, sizeof(json_schema_state));
466
+ if (!st) { cJSON_Delete(schema); return NULL; }
467
+ st->column = strdup(column);
468
+ st->result = strdup(result);
469
+ st->schema = schema;
470
+ st->mode = mode;
471
+ st->audit_limit = 1000;
472
+ tf_audit_options_init(&st->audit_opts, 1);
473
+ cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
474
+ st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
475
+ cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
476
+ if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
477
+ if (audit_limit_j) {
478
+ size_t parsed_limit = 0;
479
+ if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
480
+ 1, TF_MAX_AUDIT_RECORDS,
481
+ &parsed_limit, "json-schema") < 0) {
482
+ json_schema_state_free(st);
483
+ return NULL;
484
+ }
485
+ st->audit_limit = parsed_limit;
486
+ }
487
+ if (tf_audit_options_parse(&st->audit_opts, args, "json-schema") != TF_OK) {
488
+ json_schema_state_free(st);
489
+ return NULL;
490
+ }
491
+ if (!st->column || !st->result) {
492
+ json_schema_state_free(st);
493
+ return NULL;
494
+ }
495
+
496
+ tf_step *step = calloc(1, sizeof(tf_step));
497
+ if (!step) { json_schema_state_free(st); return NULL; }
498
+ step->process = json_schema_process;
499
+ step->flush = json_schema_flush;
500
+ step->destroy = json_schema_destroy;
501
+ step->state = st;
502
+ return step;
503
+ }