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.
- package/LICENSE +177 -21
- package/NOTICE +8 -0
- package/README.md +627 -0
- package/app/assets/index-6quYZ5Ap.css +5 -0
- package/app/assets/index-BIAIKnrp.js +160 -0
- package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/app/index.html +13 -0
- package/binding.gyp +121 -0
- package/csrc/arena.c +93 -0
- package/csrc/batch.c +976 -0
- package/csrc/buffer.c +154 -0
- package/csrc/cJSON.c +3386 -0
- package/csrc/cJSON.h +316 -0
- package/csrc/codec_csv.c +1951 -0
- package/csrc/codec_jsonl.c +1086 -0
- package/csrc/codec_table.c +248 -0
- package/csrc/codec_text.c +447 -0
- package/csrc/compiler.c +130 -0
- package/csrc/config.h +21 -0
- package/csrc/date_utils.h +94 -0
- package/csrc/dsl.c +5417 -0
- package/csrc/dsl.h +22 -0
- package/csrc/expr.c +1553 -0
- package/csrc/expr.h +58 -0
- package/csrc/internal.h +539 -0
- package/csrc/ir.c +166 -0
- package/csrc/ir.h +208 -0
- package/csrc/ir_schema.c +75 -0
- package/csrc/ir_serialize.c +166 -0
- package/csrc/ir_sql.c +1822 -0
- package/csrc/ir_validate.c +576 -0
- package/csrc/json_path.c +210 -0
- package/csrc/main.c +1241 -0
- package/csrc/memory_estimate.c +477 -0
- package/csrc/op_acf.c +283 -0
- package/csrc/op_across.c +477 -0
- package/csrc/op_anomaly.c +255 -0
- package/csrc/op_assert.c +761 -0
- package/csrc/op_bin.c +248 -0
- package/csrc/op_cast.c +523 -0
- package/csrc/op_clip.c +99 -0
- package/csrc/op_date_trunc.c +355 -0
- package/csrc/op_datetime.c +394 -0
- package/csrc/op_derive.c +216 -0
- package/csrc/op_diff.c +250 -0
- package/csrc/op_ewma.c +222 -0
- package/csrc/op_explode.c +206 -0
- package/csrc/op_fill_down.c +235 -0
- package/csrc/op_fill_null.c +268 -0
- package/csrc/op_filter.c +181 -0
- package/csrc/op_frequency.c +721 -0
- package/csrc/op_grep.c +181 -0
- package/csrc/op_group_agg.c +1956 -0
- package/csrc/op_hash.c +159 -0
- package/csrc/op_head.c +84 -0
- package/csrc/op_interpolate.c +445 -0
- package/csrc/op_join.c +2902 -0
- package/csrc/op_json_extract.c +227 -0
- package/csrc/op_json_filter.c +384 -0
- package/csrc/op_json_flatten.c +293 -0
- package/csrc/op_json_schema.c +503 -0
- package/csrc/op_label_encode.c +419 -0
- package/csrc/op_lag.c +181 -0
- package/csrc/op_lead.c +242 -0
- package/csrc/op_normalize.c +510 -0
- package/csrc/op_onehot.c +457 -0
- package/csrc/op_pivot.c +1754 -0
- package/csrc/op_quarantine.c +189 -0
- package/csrc/op_registry.c +3044 -0
- package/csrc/op_rename.c +129 -0
- package/csrc/op_replace.c +354 -0
- package/csrc/op_rleid.c +297 -0
- package/csrc/op_rowid.c +559 -0
- package/csrc/op_sample.c +158 -0
- package/csrc/op_schema.c +1341 -0
- package/csrc/op_schema_infer.c +252 -0
- package/csrc/op_select.c +340 -0
- package/csrc/op_set.c +3449 -0
- package/csrc/op_skip.c +95 -0
- package/csrc/op_sort.c +819 -0
- package/csrc/op_source_name.c +120 -0
- package/csrc/op_split.c +151 -0
- package/csrc/op_split_data.c +119 -0
- package/csrc/op_stack.c +271 -0
- package/csrc/op_stats.c +875 -0
- package/csrc/op_step.c +333 -0
- package/csrc/op_tail.c +105 -0
- package/csrc/op_tee.c +338 -0
- package/csrc/op_top.c +357 -0
- package/csrc/op_trim.c +138 -0
- package/csrc/op_unique.c +1343 -0
- package/csrc/op_unpivot.c +193 -0
- package/csrc/op_validate.c +648 -0
- package/csrc/op_window.c +591 -0
- package/csrc/path_policy.c +85 -0
- package/csrc/pipeline.c +1088 -0
- package/csrc/recipes.c +104 -0
- package/csrc/recipes.h +27 -0
- package/csrc/report.c +506 -0
- package/csrc/report.h +22 -0
- package/csrc/selector.c +1097 -0
- package/csrc/size_utils.c +348 -0
- package/csrc/spill.c +317 -0
- package/csrc/spill.h +21 -0
- package/csrc/tranfi.h +291 -0
- package/csrc/transform.h +209 -0
- package/csrc/transform_api.c +2237 -0
- package/csrc/transform_categorical.c +923 -0
- package/csrc/transform_internal.h +472 -0
- package/csrc/transform_json.c +3812 -0
- package/csrc/transform_numeric.c +1966 -0
- package/csrc/transform_sha256.c +154 -0
- package/csrc/transform_wasm.h +162 -0
- package/csrc/transform_wasm_api.c +1373 -0
- package/csrc/wasm_api.c +218 -0
- package/napi_api.c +534 -0
- package/napi_transform.c +1648 -0
- package/napi_transform.h +8 -0
- package/package.json +64 -59
- package/scripts/install-native.js +76 -0
- package/scripts/prepack.js +64 -0
- package/scripts/sync-csrc.js +23 -0
- package/src/cli.js +190 -0
- package/src/engines/duckdb.js +142 -0
- package/src/index.js +925 -0
- package/src/memory_policy.js +411 -0
- package/src/native.js +18 -0
- package/src/pipeline.js +709 -0
- package/src/recipe_json.js +80 -0
- package/src/server.js +279 -0
- package/src/transform.js +403 -0
- package/src/transform_error.js +10 -0
- package/src/wasm.js +21 -0
- package/wasm/index.js +732 -0
- package/wasm/package.json +1 -0
- package/wasm/tranfi_core.js +0 -0
- package/wasm/transform.js +1156 -0
- package/wasm/worker.js +786 -0
- package/dist/bundle.js +0 -1
- package/index.html +0 -18
- package/src/app.css +0 -169
- package/src/app.js +0 -203
- package/src/app.vue +0 -250
- package/src/bulma-input.vue +0 -110
- package/src/common-inputs.js +0 -28
- package/src/main.js +0 -20
- package/src/transforms.js +0 -166
- package/webpack.config.js +0 -108
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* op_fill_down.c — Forward-fill nulls with last non-null value.
|
|
3
|
+
*
|
|
4
|
+
* Config: {"columns": ["city", "state"]}
|
|
5
|
+
* or {} for all columns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "internal.h"
|
|
9
|
+
#include "cJSON.h"
|
|
10
|
+
#include "date_utils.h"
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include <stdio.h>
|
|
14
|
+
|
|
15
|
+
typedef struct {
|
|
16
|
+
char **cols;
|
|
17
|
+
size_t n_cols;
|
|
18
|
+
/* Last non-null value per column (stored as strings for simplicity) */
|
|
19
|
+
char **last_vals;
|
|
20
|
+
int *last_types; /* tf_type */
|
|
21
|
+
int64_t *last_int;
|
|
22
|
+
double *last_float;
|
|
23
|
+
int *last_bool;
|
|
24
|
+
int32_t *last_date;
|
|
25
|
+
int64_t *last_timestamp;
|
|
26
|
+
int n_tracked;
|
|
27
|
+
int initialized;
|
|
28
|
+
} fill_down_state;
|
|
29
|
+
|
|
30
|
+
static int fill_down_is_target(const fill_down_state *st, const tf_batch *in, size_t c) {
|
|
31
|
+
if (st->n_cols == 0) return 1;
|
|
32
|
+
for (size_t k = 0; k < st->n_cols; k++) {
|
|
33
|
+
if (strcmp(in->col_names[c], st->cols[k]) == 0) return 1;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static void fill_down_free_string_slots(char **slots, size_t n) {
|
|
39
|
+
if (!slots) return;
|
|
40
|
+
for (size_t i = 0; i < n; i++) {
|
|
41
|
+
free(slots[i]);
|
|
42
|
+
slots[i] = NULL;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static int fill_down_process(tf_step *self, tf_batch *in, tf_batch **out,
|
|
47
|
+
tf_side_channels *side) {
|
|
48
|
+
(void)side;
|
|
49
|
+
fill_down_state *st = self->state;
|
|
50
|
+
*out = NULL;
|
|
51
|
+
|
|
52
|
+
if (!st->initialized) {
|
|
53
|
+
char **last_vals = calloc(in->n_cols, sizeof(char *));
|
|
54
|
+
int *last_types = calloc(in->n_cols, sizeof(int));
|
|
55
|
+
int64_t *last_int = calloc(in->n_cols, sizeof(int64_t));
|
|
56
|
+
double *last_float = calloc(in->n_cols, sizeof(double));
|
|
57
|
+
int *last_bool = calloc(in->n_cols, sizeof(int));
|
|
58
|
+
int32_t *last_date = calloc(in->n_cols, sizeof(int32_t));
|
|
59
|
+
int64_t *last_timestamp = calloc(in->n_cols, sizeof(int64_t));
|
|
60
|
+
if (!last_vals || !last_types || !last_int || !last_float ||
|
|
61
|
+
!last_bool || !last_date || !last_timestamp) {
|
|
62
|
+
free(last_vals);
|
|
63
|
+
free(last_types);
|
|
64
|
+
free(last_int);
|
|
65
|
+
free(last_float);
|
|
66
|
+
free(last_bool);
|
|
67
|
+
free(last_date);
|
|
68
|
+
free(last_timestamp);
|
|
69
|
+
return TF_ERROR;
|
|
70
|
+
}
|
|
71
|
+
for (size_t c = 0; c < in->n_cols; c++) last_types[c] = -1; /* no value yet */
|
|
72
|
+
st->n_tracked = (int)in->n_cols;
|
|
73
|
+
st->last_vals = last_vals;
|
|
74
|
+
st->last_types = last_types;
|
|
75
|
+
st->last_int = last_int;
|
|
76
|
+
st->last_float = last_float;
|
|
77
|
+
st->last_bool = last_bool;
|
|
78
|
+
st->last_date = last_date;
|
|
79
|
+
st->last_timestamp = last_timestamp;
|
|
80
|
+
st->initialized = 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
|
|
84
|
+
if (!ob) return TF_ERROR;
|
|
85
|
+
if (tf_batch_clone_schema(ob, in) != TF_OK) {
|
|
86
|
+
tf_batch_free(ob);
|
|
87
|
+
return TF_ERROR;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
char **pending_strings = calloc(in->n_cols, sizeof(char *));
|
|
91
|
+
if (!pending_strings) {
|
|
92
|
+
tf_batch_free(ob);
|
|
93
|
+
return TF_ERROR;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (size_t r = 0; r < in->n_rows; r++) {
|
|
97
|
+
memset(pending_strings, 0, sizeof(char *) * in->n_cols);
|
|
98
|
+
if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
|
|
99
|
+
goto fail;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (size_t c = 0; c < in->n_cols; c++) {
|
|
103
|
+
if (!fill_down_is_target(st, in, c)) continue;
|
|
104
|
+
|
|
105
|
+
if (!tf_batch_is_null(in, r, c)) {
|
|
106
|
+
if (in->col_types[c] == TF_TYPE_STRING) {
|
|
107
|
+
const char *src = tf_batch_get_string(in, r, c);
|
|
108
|
+
pending_strings[c] = strdup(src ? src : "");
|
|
109
|
+
if (!pending_strings[c]) {
|
|
110
|
+
goto fail;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (st->last_types[c] >= 0) {
|
|
114
|
+
/* Fill with last value */
|
|
115
|
+
int write_rc = TF_OK;
|
|
116
|
+
switch (in->col_types[c]) {
|
|
117
|
+
case TF_TYPE_STRING:
|
|
118
|
+
if (st->last_vals[c]) write_rc = tf_batch_set_string(ob, r, c, st->last_vals[c]);
|
|
119
|
+
break;
|
|
120
|
+
case TF_TYPE_INT64:
|
|
121
|
+
write_rc = tf_batch_set_int64(ob, r, c, st->last_int[c]); break;
|
|
122
|
+
case TF_TYPE_FLOAT64:
|
|
123
|
+
write_rc = tf_batch_set_float64(ob, r, c, st->last_float[c]); break;
|
|
124
|
+
case TF_TYPE_BOOL:
|
|
125
|
+
write_rc = tf_batch_set_bool(ob, r, c, st->last_bool[c]); break;
|
|
126
|
+
case TF_TYPE_DATE:
|
|
127
|
+
write_rc = tf_batch_set_date(ob, r, c, st->last_date[c]); break;
|
|
128
|
+
case TF_TYPE_TIMESTAMP:
|
|
129
|
+
write_rc = tf_batch_set_timestamp(ob, r, c, st->last_timestamp[c]); break;
|
|
130
|
+
default: break;
|
|
131
|
+
}
|
|
132
|
+
if (write_rc != TF_OK) goto fail;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (size_t c = 0; c < in->n_cols; c++) {
|
|
137
|
+
if (!fill_down_is_target(st, in, c) || tf_batch_is_null(in, r, c)) continue;
|
|
138
|
+
|
|
139
|
+
st->last_types[c] = (int)in->col_types[c];
|
|
140
|
+
switch (in->col_types[c]) {
|
|
141
|
+
case TF_TYPE_STRING:
|
|
142
|
+
free(st->last_vals[c]);
|
|
143
|
+
st->last_vals[c] = pending_strings[c];
|
|
144
|
+
pending_strings[c] = NULL;
|
|
145
|
+
break;
|
|
146
|
+
case TF_TYPE_INT64:
|
|
147
|
+
st->last_int[c] = tf_batch_get_int64(in, r, c); break;
|
|
148
|
+
case TF_TYPE_FLOAT64:
|
|
149
|
+
st->last_float[c] = tf_batch_get_float64(in, r, c); break;
|
|
150
|
+
case TF_TYPE_BOOL:
|
|
151
|
+
st->last_bool[c] = tf_batch_get_bool(in, r, c); break;
|
|
152
|
+
case TF_TYPE_DATE:
|
|
153
|
+
st->last_date[c] = tf_batch_get_date(in, r, c); break;
|
|
154
|
+
case TF_TYPE_TIMESTAMP:
|
|
155
|
+
st->last_timestamp[c] = tf_batch_get_timestamp(in, r, c); break;
|
|
156
|
+
default:
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (tf_batch_expose_row(ob, r) != TF_OK) goto fail;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
free(pending_strings);
|
|
164
|
+
*out = ob;
|
|
165
|
+
return TF_OK;
|
|
166
|
+
|
|
167
|
+
fail:
|
|
168
|
+
fill_down_free_string_slots(pending_strings, in->n_cols);
|
|
169
|
+
free(pending_strings);
|
|
170
|
+
tf_batch_free(ob);
|
|
171
|
+
return TF_ERROR;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static int fill_down_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
|
|
175
|
+
(void)self; (void)side; *out = NULL; return TF_OK;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static void fill_down_state_free(fill_down_state *st) {
|
|
179
|
+
if (st) {
|
|
180
|
+
if (st->cols) {
|
|
181
|
+
for (size_t i = 0; i < st->n_cols; i++) free(st->cols[i]);
|
|
182
|
+
}
|
|
183
|
+
free(st->cols);
|
|
184
|
+
if (st->last_vals) {
|
|
185
|
+
for (int i = 0; i < st->n_tracked; i++) free(st->last_vals[i]);
|
|
186
|
+
free(st->last_vals);
|
|
187
|
+
}
|
|
188
|
+
free(st->last_types); free(st->last_int);
|
|
189
|
+
free(st->last_float); free(st->last_bool);
|
|
190
|
+
free(st->last_date); free(st->last_timestamp);
|
|
191
|
+
free(st);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static void fill_down_destroy(tf_step *self) {
|
|
196
|
+
fill_down_state_free(self->state);
|
|
197
|
+
free(self);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
tf_step *tf_fill_down_create(const cJSON *args) {
|
|
201
|
+
fill_down_state *st = calloc(1, sizeof(fill_down_state));
|
|
202
|
+
if (!st) return NULL;
|
|
203
|
+
|
|
204
|
+
if (args) {
|
|
205
|
+
cJSON *columns = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
206
|
+
if (columns && cJSON_IsArray(columns)) {
|
|
207
|
+
int n = cJSON_GetArraySize(columns);
|
|
208
|
+
if (n > 0) {
|
|
209
|
+
st->cols = calloc(n, sizeof(char *));
|
|
210
|
+
if (!st->cols) { fill_down_state_free(st); return NULL; }
|
|
211
|
+
st->n_cols = (size_t)n;
|
|
212
|
+
for (int i = 0; i < n; i++) {
|
|
213
|
+
cJSON *item = cJSON_GetArrayItem(columns, i);
|
|
214
|
+
if (!cJSON_IsString(item) || !item->valuestring[0]) {
|
|
215
|
+
fill_down_state_free(st);
|
|
216
|
+
return NULL;
|
|
217
|
+
}
|
|
218
|
+
st->cols[i] = strdup(item->valuestring);
|
|
219
|
+
if (!st->cols[i]) {
|
|
220
|
+
fill_down_state_free(st);
|
|
221
|
+
return NULL;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
tf_step *step = calloc(1, sizeof(tf_step));
|
|
229
|
+
if (!step) { fill_down_state_free(st); return NULL; }
|
|
230
|
+
step->process = fill_down_process;
|
|
231
|
+
step->flush = fill_down_flush;
|
|
232
|
+
step->destroy = fill_down_destroy;
|
|
233
|
+
step->state = st;
|
|
234
|
+
return step;
|
|
235
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* op_fill_null.c — Replace nulls with default values.
|
|
3
|
+
*
|
|
4
|
+
* Config: {"mapping": {"col1": "default1", "col2": "0"}}
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#include "internal.h"
|
|
8
|
+
#include "cJSON.h"
|
|
9
|
+
#include "date_utils.h"
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
|
|
13
|
+
/* Fill-null is row-local. Audit records are opt-in and capped so diagnostics
|
|
14
|
+
* cannot become an unbounded second output stream. */
|
|
15
|
+
typedef struct {
|
|
16
|
+
char **col_names;
|
|
17
|
+
char **defaults;
|
|
18
|
+
size_t n;
|
|
19
|
+
size_t row_index;
|
|
20
|
+
size_t audit_limit;
|
|
21
|
+
size_t audit_emitted;
|
|
22
|
+
int audit;
|
|
23
|
+
tf_audit_options audit_opts;
|
|
24
|
+
} fill_null_state;
|
|
25
|
+
|
|
26
|
+
static int emit_fill_null_audit(fill_null_state *st, const tf_batch *b, size_t row,
|
|
27
|
+
size_t col, size_t row_no, tf_side_channels *side) {
|
|
28
|
+
if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
|
|
29
|
+
cJSON *obj = cJSON_CreateObject();
|
|
30
|
+
if (!obj) return TF_ERROR;
|
|
31
|
+
int rc = TF_ERROR;
|
|
32
|
+
if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
|
|
33
|
+
tf_json_add_string(obj, "op", "fill-null") != TF_OK ||
|
|
34
|
+
tf_json_add_string(obj, "event", "null_filled") != TF_OK ||
|
|
35
|
+
tf_json_add_string(obj, "reason", "null_filled") != TF_OK ||
|
|
36
|
+
tf_json_add_string(obj, "channel", "audit") != TF_OK ||
|
|
37
|
+
tf_json_add_string(obj, "column", b->col_names[col] ? b->col_names[col] : "") != TF_OK ||
|
|
38
|
+
tf_json_add_number(obj, "row", (double)row_no) != TF_OK ||
|
|
39
|
+
tf_json_add_null(obj, "before") != TF_OK) {
|
|
40
|
+
goto done;
|
|
41
|
+
}
|
|
42
|
+
cJSON *after = tf_audit_cell_to_json(b, row, col, &st->audit_opts);
|
|
43
|
+
if (!after || tf_json_add_item(obj, "after", after) != TF_OK) goto done;
|
|
44
|
+
cJSON *row_obj = tf_audit_row_to_json(b, row, &st->audit_opts);
|
|
45
|
+
if (row_obj) {
|
|
46
|
+
if (tf_json_add_item(obj, "data", row_obj) != TF_OK) goto done;
|
|
47
|
+
} else if (st->audit_opts.include_row) {
|
|
48
|
+
goto done;
|
|
49
|
+
}
|
|
50
|
+
rc = tf_buffer_write_json_line(side->stats, obj);
|
|
51
|
+
done:
|
|
52
|
+
cJSON_Delete(obj);
|
|
53
|
+
if (rc == TF_OK) st->audit_emitted++;
|
|
54
|
+
return rc;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static int fill_null_process(tf_step *self, tf_batch *in, tf_batch **out,
|
|
58
|
+
tf_side_channels *side) {
|
|
59
|
+
fill_null_state *st = self->state;
|
|
60
|
+
*out = NULL;
|
|
61
|
+
size_t row_base = st->row_index;
|
|
62
|
+
|
|
63
|
+
tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
|
|
64
|
+
if (!ob) return TF_ERROR;
|
|
65
|
+
if (tf_batch_clone_schema(ob, in) != TF_OK) {
|
|
66
|
+
tf_batch_free(ob);
|
|
67
|
+
return TF_ERROR;
|
|
68
|
+
}
|
|
69
|
+
size_t *changed_cols = tf_callocarray_checked(st->n > 0 ? st->n : 1, sizeof(size_t));
|
|
70
|
+
if (!changed_cols) {
|
|
71
|
+
tf_batch_free(ob);
|
|
72
|
+
return TF_ERROR;
|
|
73
|
+
}
|
|
74
|
+
for (size_t r = 0; r < in->n_rows; r++) {
|
|
75
|
+
if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
|
|
76
|
+
free(changed_cols);
|
|
77
|
+
tf_batch_free(ob);
|
|
78
|
+
return TF_ERROR;
|
|
79
|
+
}
|
|
80
|
+
size_t changed_count = 0;
|
|
81
|
+
for (size_t k = 0; k < st->n; k++) {
|
|
82
|
+
int ci = tf_batch_col_index(in, st->col_names[k]);
|
|
83
|
+
if (ci < 0 || !ob->nulls[ci] || !ob->nulls[ci][r]) continue;
|
|
84
|
+
const char *def = st->defaults[k];
|
|
85
|
+
int changed = 0;
|
|
86
|
+
switch (in->col_types[ci]) {
|
|
87
|
+
case TF_TYPE_STRING:
|
|
88
|
+
if (tf_batch_set_string(ob, r, (size_t)ci, def) != TF_OK) {
|
|
89
|
+
free(changed_cols);
|
|
90
|
+
tf_batch_free(ob);
|
|
91
|
+
return TF_ERROR;
|
|
92
|
+
}
|
|
93
|
+
changed = 1;
|
|
94
|
+
break;
|
|
95
|
+
case TF_TYPE_INT64: {
|
|
96
|
+
char *end;
|
|
97
|
+
int64_t v = strtoll(def, &end, 10);
|
|
98
|
+
if (*end == '\0') {
|
|
99
|
+
if (tf_batch_set_int64(ob, r, (size_t)ci, v) != TF_OK) {
|
|
100
|
+
free(changed_cols);
|
|
101
|
+
tf_batch_free(ob);
|
|
102
|
+
return TF_ERROR;
|
|
103
|
+
}
|
|
104
|
+
changed = 1;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case TF_TYPE_FLOAT64: {
|
|
109
|
+
char *end;
|
|
110
|
+
double v = strtod(def, &end);
|
|
111
|
+
if (*end == '\0') {
|
|
112
|
+
if (tf_batch_set_float64(ob, r, (size_t)ci, v) != TF_OK) {
|
|
113
|
+
free(changed_cols);
|
|
114
|
+
tf_batch_free(ob);
|
|
115
|
+
return TF_ERROR;
|
|
116
|
+
}
|
|
117
|
+
changed = 1;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case TF_TYPE_BOOL:
|
|
122
|
+
if (tf_batch_set_bool(ob, r, (size_t)ci, strcmp(def, "true") == 0) != TF_OK) {
|
|
123
|
+
free(changed_cols);
|
|
124
|
+
tf_batch_free(ob);
|
|
125
|
+
return TF_ERROR;
|
|
126
|
+
}
|
|
127
|
+
changed = 1;
|
|
128
|
+
break;
|
|
129
|
+
case TF_TYPE_DATE: {
|
|
130
|
+
char *end;
|
|
131
|
+
int32_t v = (int32_t)strtol(def, &end, 10);
|
|
132
|
+
if (*end == '\0') {
|
|
133
|
+
if (tf_batch_set_date(ob, r, (size_t)ci, v) != TF_OK) {
|
|
134
|
+
free(changed_cols);
|
|
135
|
+
tf_batch_free(ob);
|
|
136
|
+
return TF_ERROR;
|
|
137
|
+
}
|
|
138
|
+
changed = 1;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case TF_TYPE_TIMESTAMP: {
|
|
143
|
+
char *end;
|
|
144
|
+
int64_t v = strtoll(def, &end, 10);
|
|
145
|
+
if (*end == '\0') {
|
|
146
|
+
if (tf_batch_set_timestamp(ob, r, (size_t)ci, v) != TF_OK) {
|
|
147
|
+
free(changed_cols);
|
|
148
|
+
tf_batch_free(ob);
|
|
149
|
+
return TF_ERROR;
|
|
150
|
+
}
|
|
151
|
+
changed = 1;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
if (changed) changed_cols[changed_count++] = (size_t)ci;
|
|
159
|
+
}
|
|
160
|
+
if (tf_batch_expose_row(ob, r) != TF_OK) {
|
|
161
|
+
free(changed_cols);
|
|
162
|
+
tf_batch_free(ob);
|
|
163
|
+
return TF_ERROR;
|
|
164
|
+
}
|
|
165
|
+
if (changed_count > 0) {
|
|
166
|
+
size_t row_no = 0;
|
|
167
|
+
if (tf_size_add(row_base, r, &row_no) != TF_OK ||
|
|
168
|
+
tf_size_add(row_no, 1, &row_no) != TF_OK) {
|
|
169
|
+
free(changed_cols);
|
|
170
|
+
tf_batch_free(ob);
|
|
171
|
+
return TF_ERROR;
|
|
172
|
+
}
|
|
173
|
+
for (size_t i = 0; i < changed_count; i++) {
|
|
174
|
+
if (emit_fill_null_audit(st, ob, r, changed_cols[i], row_no, side) != TF_OK) {
|
|
175
|
+
free(changed_cols);
|
|
176
|
+
tf_batch_free(ob);
|
|
177
|
+
return TF_ERROR;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (tf_size_add(st->row_index, in->n_rows, &st->row_index) != TF_OK) {
|
|
184
|
+
free(changed_cols);
|
|
185
|
+
tf_batch_free(ob);
|
|
186
|
+
return TF_ERROR;
|
|
187
|
+
}
|
|
188
|
+
free(changed_cols);
|
|
189
|
+
*out = ob;
|
|
190
|
+
return TF_OK;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static int fill_null_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
|
|
194
|
+
(void)self; (void)side; *out = NULL; return TF_OK;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static void fill_null_state_free(fill_null_state *st) {
|
|
198
|
+
if (!st) return;
|
|
199
|
+
tf_audit_options_free(&st->audit_opts);
|
|
200
|
+
if (st->col_names) {
|
|
201
|
+
for (size_t i = 0; i < st->n; i++) free(st->col_names[i]);
|
|
202
|
+
}
|
|
203
|
+
if (st->defaults) {
|
|
204
|
+
for (size_t i = 0; i < st->n; i++) free(st->defaults[i]);
|
|
205
|
+
}
|
|
206
|
+
free(st->col_names);
|
|
207
|
+
free(st->defaults);
|
|
208
|
+
free(st);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static void fill_null_destroy(tf_step *self) {
|
|
212
|
+
if (self) fill_null_state_free(self->state);
|
|
213
|
+
free(self);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
tf_step *tf_fill_null_create(const cJSON *args) {
|
|
217
|
+
if (!args) return NULL;
|
|
218
|
+
cJSON *mapping = cJSON_GetObjectItemCaseSensitive(args, "mapping");
|
|
219
|
+
if (!mapping || !cJSON_IsObject(mapping)) return NULL;
|
|
220
|
+
|
|
221
|
+
int n = cJSON_GetArraySize(mapping);
|
|
222
|
+
fill_null_state *st = calloc(1, sizeof(fill_null_state));
|
|
223
|
+
if (!st) return NULL;
|
|
224
|
+
tf_audit_options_init(&st->audit_opts, 1);
|
|
225
|
+
st->col_names = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(char *));
|
|
226
|
+
st->defaults = tf_callocarray_checked(n > 0 ? (size_t)n : 1, sizeof(char *));
|
|
227
|
+
st->n = (size_t)n;
|
|
228
|
+
st->audit_limit = 1000;
|
|
229
|
+
if (!st->col_names || !st->defaults) { fill_null_state_free(st); return NULL; }
|
|
230
|
+
|
|
231
|
+
int i = 0;
|
|
232
|
+
cJSON *entry = NULL;
|
|
233
|
+
cJSON_ArrayForEach(entry, mapping) {
|
|
234
|
+
if (!entry->string || !entry->string[0]) { fill_null_state_free(st); return NULL; }
|
|
235
|
+
st->col_names[i] = strdup(entry->string);
|
|
236
|
+
st->defaults[i] = strdup(cJSON_IsString(entry) ? entry->valuestring : "");
|
|
237
|
+
if (!st->col_names[i] || !st->defaults[i]) { fill_null_state_free(st); return NULL; }
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
cJSON *audit_j = cJSON_GetObjectItemCaseSensitive(args, "audit");
|
|
242
|
+
st->audit = cJSON_IsTrue(audit_j) ? 1 : 0;
|
|
243
|
+
cJSON *audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
|
|
244
|
+
if (!audit_limit_j) audit_limit_j = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
|
|
245
|
+
if (audit_limit_j) {
|
|
246
|
+
size_t parsed_limit = 0;
|
|
247
|
+
if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
|
|
248
|
+
1, TF_MAX_AUDIT_RECORDS,
|
|
249
|
+
&parsed_limit, "fill-null") < 0) {
|
|
250
|
+
fill_null_state_free(st);
|
|
251
|
+
return NULL;
|
|
252
|
+
}
|
|
253
|
+
st->audit_limit = parsed_limit;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (tf_audit_options_parse(&st->audit_opts, args, "fill-null") != TF_OK) {
|
|
257
|
+
fill_null_state_free(st);
|
|
258
|
+
return NULL;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
tf_step *step = calloc(1, sizeof(tf_step));
|
|
262
|
+
if (!step) { fill_null_state_free(st); return NULL; }
|
|
263
|
+
step->process = fill_null_process;
|
|
264
|
+
step->flush = fill_null_flush;
|
|
265
|
+
step->destroy = fill_null_destroy;
|
|
266
|
+
step->state = st;
|
|
267
|
+
return step;
|
|
268
|
+
}
|
package/csrc/op_filter.c
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* op_filter.c — Filter rows by expression.
|
|
3
|
+
*
|
|
4
|
+
* Config: {"expr": "col('age') > 25"}
|
|
5
|
+
* Evaluates expression for each row, keeps rows where result is true.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "internal.h"
|
|
9
|
+
#include "cJSON.h"
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
#include <stdio.h>
|
|
13
|
+
#include <stdbool.h>
|
|
14
|
+
|
|
15
|
+
typedef struct {
|
|
16
|
+
tf_expr *expr;
|
|
17
|
+
char *expr_text;
|
|
18
|
+
size_t rows_in;
|
|
19
|
+
size_t rows_out;
|
|
20
|
+
size_t row_index;
|
|
21
|
+
size_t audit_limit;
|
|
22
|
+
size_t audit_emitted;
|
|
23
|
+
int audit;
|
|
24
|
+
tf_audit_options audit_opts;
|
|
25
|
+
} filter_state;
|
|
26
|
+
|
|
27
|
+
static int filter_emit_drop_audit(filter_state *st, const tf_batch *b, size_t row,
|
|
28
|
+
tf_side_channels *side, const char *reason) {
|
|
29
|
+
if (!st->audit || st->audit_emitted >= st->audit_limit || !side || !side->stats) return TF_OK;
|
|
30
|
+
cJSON *obj = cJSON_CreateObject();
|
|
31
|
+
if (!obj) return TF_ERROR;
|
|
32
|
+
int rc = TF_ERROR;
|
|
33
|
+
if (tf_json_add_string(obj, "type", "audit") != TF_OK ||
|
|
34
|
+
tf_json_add_string(obj, "op", "filter") != TF_OK ||
|
|
35
|
+
tf_json_add_string(obj, "event", "row_dropped") != TF_OK ||
|
|
36
|
+
tf_json_add_string(obj, "reason", reason ? reason : "predicate_false") != TF_OK ||
|
|
37
|
+
tf_json_add_string(obj, "channel", "audit") != TF_OK ||
|
|
38
|
+
tf_json_add_string(obj, "expr", st->expr_text ? st->expr_text : "") != TF_OK ||
|
|
39
|
+
tf_json_add_number(obj, "row", (double)st->row_index) != TF_OK) {
|
|
40
|
+
goto done;
|
|
41
|
+
}
|
|
42
|
+
cJSON *data = tf_audit_row_to_json(b, row, &st->audit_opts);
|
|
43
|
+
if (data) {
|
|
44
|
+
if (tf_json_add_item(obj, "data", data) != TF_OK) goto done;
|
|
45
|
+
} else if (st->audit_opts.include_row) {
|
|
46
|
+
goto done;
|
|
47
|
+
}
|
|
48
|
+
rc = tf_buffer_write_json_line(side->stats, obj);
|
|
49
|
+
done:
|
|
50
|
+
cJSON_Delete(obj);
|
|
51
|
+
if (rc == TF_OK) st->audit_emitted++;
|
|
52
|
+
return rc;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static int filter_process(tf_step *self, tf_batch *in, tf_batch **out,
|
|
56
|
+
tf_side_channels *side) {
|
|
57
|
+
filter_state *st = self->state;
|
|
58
|
+
*out = NULL;
|
|
59
|
+
|
|
60
|
+
/* Create output batch with same schema */
|
|
61
|
+
tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
|
|
62
|
+
if (!ob) return TF_ERROR;
|
|
63
|
+
if (tf_batch_clone_schema(ob, in) != TF_OK) {
|
|
64
|
+
tf_batch_free(ob);
|
|
65
|
+
return TF_ERROR;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Filter rows */
|
|
69
|
+
size_t out_row = 0;
|
|
70
|
+
for (size_t r = 0; r < in->n_rows; r++) {
|
|
71
|
+
st->row_index++;
|
|
72
|
+
bool keep = false;
|
|
73
|
+
int eval_ok = tf_expr_eval(st->expr, in, r, &keep) == TF_OK;
|
|
74
|
+
if (!eval_ok || !keep) {
|
|
75
|
+
if (filter_emit_drop_audit(st, in, r, side,
|
|
76
|
+
eval_ok ? "predicate_false" : "predicate_error") != TF_OK) {
|
|
77
|
+
tf_batch_free(ob);
|
|
78
|
+
return TF_ERROR;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tf_batch_copy_row(ob, out_row, in, r) != TF_OK) {
|
|
84
|
+
tf_batch_free(ob);
|
|
85
|
+
return TF_ERROR;
|
|
86
|
+
}
|
|
87
|
+
if (tf_batch_expose_row(ob, out_row) != TF_OK) {
|
|
88
|
+
tf_batch_free(ob);
|
|
89
|
+
return TF_ERROR;
|
|
90
|
+
}
|
|
91
|
+
out_row++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
st->rows_in += in->n_rows;
|
|
95
|
+
st->rows_out += out_row;
|
|
96
|
+
|
|
97
|
+
/* Emit stats to side channel */
|
|
98
|
+
if (side && side->stats) {
|
|
99
|
+
char stats_buf[128];
|
|
100
|
+
snprintf(stats_buf, sizeof(stats_buf),
|
|
101
|
+
"{\"op\":\"filter\",\"rows_in\":%zu,\"rows_out\":%zu}",
|
|
102
|
+
in->n_rows, out_row);
|
|
103
|
+
if (tf_buffer_write_line(side->stats, stats_buf) != TF_OK) {
|
|
104
|
+
tf_batch_free(ob);
|
|
105
|
+
return TF_ERROR;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (out_row > 0) {
|
|
110
|
+
*out = ob;
|
|
111
|
+
} else {
|
|
112
|
+
tf_batch_free(ob);
|
|
113
|
+
}
|
|
114
|
+
return TF_OK;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static int filter_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
|
|
118
|
+
(void)self; (void)side;
|
|
119
|
+
*out = NULL;
|
|
120
|
+
return TF_OK;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static void filter_destroy(tf_step *self) {
|
|
124
|
+
filter_state *st = self->state;
|
|
125
|
+
if (st) {
|
|
126
|
+
tf_expr_free(st->expr);
|
|
127
|
+
free(st->expr_text);
|
|
128
|
+
tf_audit_options_free(&st->audit_opts);
|
|
129
|
+
free(st);
|
|
130
|
+
}
|
|
131
|
+
free(self);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tf_step *tf_filter_create(const cJSON *args) {
|
|
135
|
+
if (!args) return NULL;
|
|
136
|
+
cJSON *expr_json = cJSON_GetObjectItemCaseSensitive(args, "expr");
|
|
137
|
+
if (!cJSON_IsString(expr_json)) return NULL;
|
|
138
|
+
|
|
139
|
+
tf_expr *expr = tf_expr_parse(expr_json->valuestring);
|
|
140
|
+
if (!expr) return NULL;
|
|
141
|
+
|
|
142
|
+
filter_state *st = calloc(1, sizeof(filter_state));
|
|
143
|
+
if (!st) { tf_expr_free(expr); return NULL; }
|
|
144
|
+
st->expr = expr;
|
|
145
|
+
st->expr_text = strdup(expr_json->valuestring);
|
|
146
|
+
st->audit_limit = 1000;
|
|
147
|
+
tf_audit_options_init(&st->audit_opts, 1);
|
|
148
|
+
cJSON *audit_json = cJSON_GetObjectItemCaseSensitive(args, "audit");
|
|
149
|
+
st->audit = cJSON_IsTrue(audit_json) ? 1 : 0;
|
|
150
|
+
cJSON *audit_limit_json = cJSON_GetObjectItemCaseSensitive(args, "audit_limit");
|
|
151
|
+
if (!audit_limit_json) audit_limit_json = cJSON_GetObjectItemCaseSensitive(args, "auditLimit");
|
|
152
|
+
if (audit_limit_json) {
|
|
153
|
+
size_t parsed_limit = 0;
|
|
154
|
+
if (tf_json_get_size_arg_any(args, "audit_limit", "auditLimit",
|
|
155
|
+
1, TF_MAX_AUDIT_RECORDS,
|
|
156
|
+
&parsed_limit, "filter") < 0) {
|
|
157
|
+
tf_expr_free(expr);
|
|
158
|
+
free(st->expr_text);
|
|
159
|
+
tf_audit_options_free(&st->audit_opts);
|
|
160
|
+
free(st);
|
|
161
|
+
return NULL;
|
|
162
|
+
}
|
|
163
|
+
st->audit_limit = parsed_limit;
|
|
164
|
+
}
|
|
165
|
+
if (tf_audit_options_parse(&st->audit_opts, args, "filter") != TF_OK) {
|
|
166
|
+
tf_expr_free(expr);
|
|
167
|
+
free(st->expr_text);
|
|
168
|
+
tf_audit_options_free(&st->audit_opts);
|
|
169
|
+
free(st);
|
|
170
|
+
return NULL;
|
|
171
|
+
}
|
|
172
|
+
if (!st->expr_text) { tf_expr_free(expr); tf_audit_options_free(&st->audit_opts); free(st); return NULL; }
|
|
173
|
+
|
|
174
|
+
tf_step *step = calloc(1, sizeof(tf_step));
|
|
175
|
+
if (!step) { tf_expr_free(expr); free(st->expr_text); tf_audit_options_free(&st->audit_opts); free(st); return NULL; }
|
|
176
|
+
step->process = filter_process;
|
|
177
|
+
step->flush = filter_flush;
|
|
178
|
+
step->destroy = filter_destroy;
|
|
179
|
+
step->state = st;
|
|
180
|
+
return step;
|
|
181
|
+
}
|