tranfi 0.1.2 → 0.2.1

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 +443 -51
  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 +352 -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 +81 -41
  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
package/csrc/op_step.c CHANGED
@@ -11,6 +11,7 @@
11
11
  #include <string.h>
12
12
  #include <stdio.h>
13
13
  #include <math.h>
14
+ #include <stdint.h>
14
15
 
15
16
  typedef enum {
16
17
  STEP_RUNNING_SUM,
@@ -23,6 +24,17 @@ typedef enum {
23
24
  STEP_RATIO,
24
25
  } step_func;
25
26
 
27
+ typedef enum {
28
+ STEP_MISSING_ERROR,
29
+ STEP_MISSING_NULL,
30
+ STEP_MISSING_IGNORE,
31
+ } step_missing_policy;
32
+
33
+ typedef enum {
34
+ STEP_TYPE_FAIL,
35
+ STEP_TYPE_NULL,
36
+ } step_type_policy;
37
+
26
38
  typedef struct {
27
39
  char *column;
28
40
  char *result;
@@ -33,24 +45,94 @@ typedef struct {
33
45
  size_t running_count;
34
46
  double prev_val;
35
47
  int has_prev;
48
+ step_missing_policy missing;
49
+ step_type_policy on_type_error;
36
50
  } step_state;
37
51
 
38
- static step_func parse_func(const char *s) {
39
- if (strcmp(s, "running-sum") == 0 || strcmp(s, "cumsum") == 0) return STEP_RUNNING_SUM;
40
- if (strcmp(s, "running-avg") == 0 || strcmp(s, "cumavg") == 0) return STEP_RUNNING_AVG;
41
- if (strcmp(s, "running-min") == 0) return STEP_RUNNING_MIN;
42
- if (strcmp(s, "running-max") == 0) return STEP_RUNNING_MAX;
43
- if (strcmp(s, "running-count") == 0) return STEP_RUNNING_COUNT;
44
- if (strcmp(s, "delta") == 0) return STEP_DELTA;
45
- if (strcmp(s, "lag") == 0) return STEP_LAG;
46
- if (strcmp(s, "ratio") == 0) return STEP_RATIO;
47
- return STEP_RUNNING_SUM;
52
+ static int parse_func(const char *s, step_func *out) {
53
+ if (!s || !out) return TF_ERROR;
54
+ if (strcmp(s, "running-sum") == 0 || strcmp(s, "cumsum") == 0) *out = STEP_RUNNING_SUM;
55
+ else if (strcmp(s, "running-avg") == 0 || strcmp(s, "cumavg") == 0) *out = STEP_RUNNING_AVG;
56
+ else if (strcmp(s, "running-min") == 0) *out = STEP_RUNNING_MIN;
57
+ else if (strcmp(s, "running-max") == 0) *out = STEP_RUNNING_MAX;
58
+ else if (strcmp(s, "running-count") == 0) *out = STEP_RUNNING_COUNT;
59
+ else if (strcmp(s, "delta") == 0) *out = STEP_DELTA;
60
+ else if (strcmp(s, "lag") == 0) *out = STEP_LAG;
61
+ else if (strcmp(s, "ratio") == 0) *out = STEP_RATIO;
62
+ else return TF_ERROR;
63
+ return TF_OK;
64
+ }
65
+
66
+ static int step_is_numeric_func(step_func func) {
67
+ return func != STEP_RUNNING_COUNT;
68
+ }
69
+
70
+ static int step_is_numeric_type(tf_type type) {
71
+ return type == TF_TYPE_INT64 || type == TF_TYPE_FLOAT64;
48
72
  }
49
73
 
50
74
  static double get_numeric(const tf_batch *b, size_t r, int ci) {
51
75
  if (b->col_types[ci] == TF_TYPE_INT64) return (double)tf_batch_get_int64(b, r, ci);
52
- if (b->col_types[ci] == TF_TYPE_FLOAT64) return tf_batch_get_float64(b, r, ci);
53
- return 0;
76
+ return tf_batch_get_float64(b, r, ci);
77
+ }
78
+
79
+ static void step_set_col_error(const char *column, const char *suffix) {
80
+ char msg[512];
81
+ snprintf(msg, sizeof(msg), "step: column '%s' %s", column ? column : "", suffix);
82
+ tf_set_last_error(msg);
83
+ }
84
+
85
+ static int step_parse_missing_policy(const cJSON *args, step_missing_policy *out) {
86
+ *out = STEP_MISSING_ERROR;
87
+ const cJSON *j = cJSON_GetObjectItemCaseSensitive(args, "missing");
88
+ if (!j) return TF_OK;
89
+ if (!cJSON_IsString(j)) {
90
+ tf_set_last_error("step: missing must be error, null, or ignore");
91
+ return TF_ERROR;
92
+ }
93
+ if (strcmp(j->valuestring, "error") == 0) *out = STEP_MISSING_ERROR;
94
+ else if (strcmp(j->valuestring, "null") == 0) *out = STEP_MISSING_NULL;
95
+ else if (strcmp(j->valuestring, "ignore") == 0) *out = STEP_MISSING_IGNORE;
96
+ else {
97
+ tf_set_last_error("step: missing must be error, null, or ignore");
98
+ return TF_ERROR;
99
+ }
100
+ return TF_OK;
101
+ }
102
+
103
+ static int step_parse_type_policy(const cJSON *args, step_type_policy *out) {
104
+ *out = STEP_TYPE_FAIL;
105
+ const cJSON *j = cJSON_GetObjectItemCaseSensitive(args, "on_type_error");
106
+ if (!j) return TF_OK;
107
+ if (!cJSON_IsString(j)) {
108
+ tf_set_last_error("step: on_type_error must be fail or null");
109
+ return TF_ERROR;
110
+ }
111
+ if (strcmp(j->valuestring, "fail") == 0) *out = STEP_TYPE_FAIL;
112
+ else if (strcmp(j->valuestring, "null") == 0) *out = STEP_TYPE_NULL;
113
+ else {
114
+ tf_set_last_error("step: on_type_error must be fail or null");
115
+ return TF_ERROR;
116
+ }
117
+ return TF_OK;
118
+ }
119
+
120
+ static int step_passthrough(tf_batch *in, tf_batch **out) {
121
+ tf_batch *ob = tf_batch_create(in->n_cols, in->n_rows);
122
+ if (!ob) return TF_ERROR;
123
+ if (tf_batch_clone_schema(ob, in) != TF_OK) {
124
+ tf_batch_free(ob);
125
+ return TF_ERROR;
126
+ }
127
+ for (size_t r = 0; r < in->n_rows; r++) {
128
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK ||
129
+ tf_batch_expose_row(ob, r) != TF_OK) {
130
+ tf_batch_free(ob);
131
+ return TF_ERROR;
132
+ }
133
+ }
134
+ *out = ob;
135
+ return TF_OK;
54
136
  }
55
137
 
56
138
  static int step_process(tf_step *self, tf_batch *in, tf_batch **out,
@@ -59,71 +141,129 @@ static int step_process(tf_step *self, tf_batch *in, tf_batch **out,
59
141
  step_state *st = self->state;
60
142
  *out = NULL;
61
143
 
144
+ int ci = tf_batch_col_index(in, st->column);
145
+ int force_null = 0;
146
+ if (ci < 0) {
147
+ if (st->missing == STEP_MISSING_ERROR) {
148
+ step_set_col_error(st->column, "not found");
149
+ return TF_ERROR;
150
+ }
151
+ if (st->missing == STEP_MISSING_IGNORE) {
152
+ return step_passthrough(in, out);
153
+ }
154
+ force_null = 1;
155
+ } else if (step_is_numeric_func(st->func) &&
156
+ !step_is_numeric_type(in->col_types[(size_t)ci])) {
157
+ if (st->on_type_error == STEP_TYPE_FAIL) {
158
+ step_set_col_error(st->column, "must be numeric");
159
+ return TF_ERROR;
160
+ }
161
+ force_null = 1;
162
+ }
163
+
164
+ const char *extra_names[1] = {st->result};
165
+ tf_type extra_types[1] = {TF_TYPE_FLOAT64};
62
166
  tf_batch *ob = tf_batch_create(in->n_cols + 1, in->n_rows);
63
167
  if (!ob) return TF_ERROR;
64
- for (size_t c = 0; c < in->n_cols; c++)
65
- tf_batch_set_schema(ob, c, in->col_names[c], in->col_types[c]);
66
- tf_batch_set_schema(ob, in->n_cols, st->result, TF_TYPE_FLOAT64);
67
-
68
- int ci = tf_batch_col_index(in, st->column);
168
+ if (tf_batch_clone_with_extra_cols(ob, in, extra_names, extra_types, 1) != TF_OK) {
169
+ tf_batch_free(ob);
170
+ return TF_ERROR;
171
+ }
69
172
 
70
173
  for (size_t r = 0; r < in->n_rows; r++) {
71
- tf_batch_copy_row(ob, r, in, r);
174
+ if (tf_batch_copy_row(ob, r, in, r) != TF_OK) {
175
+ tf_batch_free(ob);
176
+ return TF_ERROR;
177
+ }
72
178
 
73
- if (ci < 0 || tf_batch_is_null(in, r, ci)) {
74
- tf_batch_set_null(ob, r, in->n_cols);
75
- ob->n_rows = r + 1;
179
+ if (force_null || tf_batch_is_null(in, r, (size_t)ci)) {
180
+ if (tf_batch_set_null(ob, r, in->n_cols) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
181
+ if (tf_batch_expose_row(ob, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
76
182
  continue;
77
183
  }
78
184
 
79
- double val = get_numeric(in, r, ci);
185
+ double val = step_is_numeric_func(st->func) ? get_numeric(in, r, ci) : 0.0;
80
186
  double result = 0;
187
+ double next_running_sum = st->running_sum;
188
+ double next_running_min = st->running_min;
189
+ double next_running_max = st->running_max;
190
+ size_t next_running_count = st->running_count;
191
+ double next_prev_val = st->prev_val;
192
+ int next_has_prev = st->has_prev;
193
+ int write_null = 0;
81
194
 
82
195
  switch (st->func) {
83
196
  case STEP_RUNNING_SUM:
84
- st->running_sum += val;
85
- result = st->running_sum;
197
+ next_running_sum += val;
198
+ result = next_running_sum;
86
199
  break;
87
200
  case STEP_RUNNING_AVG:
88
- st->running_sum += val;
89
- st->running_count++;
90
- result = st->running_sum / st->running_count;
201
+ if (next_running_count == SIZE_MAX) { tf_batch_free(ob); return TF_ERROR; }
202
+ next_running_sum += val;
203
+ next_running_count++;
204
+ result = next_running_sum / next_running_count;
91
205
  break;
92
206
  case STEP_RUNNING_MIN:
93
- if (st->running_count == 0 || val < st->running_min)
94
- st->running_min = val;
95
- st->running_count++;
96
- result = st->running_min;
207
+ if (next_running_count == SIZE_MAX) { tf_batch_free(ob); return TF_ERROR; }
208
+ if (next_running_count == 0 || val < next_running_min)
209
+ next_running_min = val;
210
+ next_running_count++;
211
+ result = next_running_min;
97
212
  break;
98
213
  case STEP_RUNNING_MAX:
99
- if (st->running_count == 0 || val > st->running_max)
100
- st->running_max = val;
101
- st->running_count++;
102
- result = st->running_max;
214
+ if (next_running_count == SIZE_MAX) { tf_batch_free(ob); return TF_ERROR; }
215
+ if (next_running_count == 0 || val > next_running_max)
216
+ next_running_max = val;
217
+ next_running_count++;
218
+ result = next_running_max;
103
219
  break;
104
220
  case STEP_RUNNING_COUNT:
105
- st->running_count++;
106
- result = (double)st->running_count;
221
+ if (next_running_count == SIZE_MAX) { tf_batch_free(ob); return TF_ERROR; }
222
+ next_running_count++;
223
+ result = (double)next_running_count;
107
224
  break;
108
225
  case STEP_DELTA:
109
226
  if (st->has_prev) result = val - st->prev_val;
110
- else { tf_batch_set_null(ob, r, in->n_cols); st->prev_val = val; st->has_prev = 1; ob->n_rows = r + 1; continue; }
111
- st->prev_val = val;
227
+ else write_null = 1;
228
+ next_prev_val = val;
229
+ next_has_prev = 1;
112
230
  break;
113
231
  case STEP_LAG:
114
232
  if (st->has_prev) result = st->prev_val;
115
- else { tf_batch_set_null(ob, r, in->n_cols); st->prev_val = val; st->has_prev = 1; ob->n_rows = r + 1; continue; }
116
- st->prev_val = val;
233
+ else write_null = 1;
234
+ next_prev_val = val;
235
+ next_has_prev = 1;
117
236
  break;
118
237
  case STEP_RATIO:
119
238
  if (st->has_prev && st->prev_val != 0) result = val / st->prev_val;
120
- else { tf_batch_set_null(ob, r, in->n_cols); st->prev_val = val; st->has_prev = 1; ob->n_rows = r + 1; continue; }
121
- st->prev_val = val;
239
+ else write_null = 1;
240
+ next_prev_val = val;
241
+ next_has_prev = 1;
122
242
  break;
123
243
  }
124
244
 
125
- tf_batch_set_float64(ob, r, in->n_cols, result);
126
- ob->n_rows = r + 1;
245
+ if (write_null) {
246
+ if (tf_batch_set_null(ob, r, in->n_cols) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
247
+ if (tf_batch_expose_row(ob, r) != TF_OK) { tf_batch_free(ob); return TF_ERROR; }
248
+ st->prev_val = next_prev_val;
249
+ st->has_prev = next_has_prev;
250
+ continue;
251
+ }
252
+
253
+ if (tf_batch_set_float64(ob, r, in->n_cols, result) != TF_OK) {
254
+ tf_batch_free(ob);
255
+ return TF_ERROR;
256
+ }
257
+ if (tf_batch_expose_row(ob, r) != TF_OK) {
258
+ tf_batch_free(ob);
259
+ return TF_ERROR;
260
+ }
261
+ st->running_sum = next_running_sum;
262
+ st->running_min = next_running_min;
263
+ st->running_max = next_running_max;
264
+ st->running_count = next_running_count;
265
+ st->prev_val = next_prev_val;
266
+ st->has_prev = next_has_prev;
127
267
  }
128
268
 
129
269
  *out = ob;
@@ -140,28 +280,50 @@ static void step_destroy(tf_step *self) {
140
280
  free(self);
141
281
  }
142
282
 
283
+ static char *step_default_result_name(const char *column, const char *func) {
284
+ char *suffix = tf_string_append_suffix_checked("_", func);
285
+ if (!suffix) return NULL;
286
+ char *result = tf_string_append_suffix_checked(column, suffix);
287
+ free(suffix);
288
+ return result;
289
+ }
290
+
143
291
  tf_step *tf_step_create(const cJSON *args) {
144
292
  if (!args) return NULL;
145
293
  cJSON *col_j = cJSON_GetObjectItemCaseSensitive(args, "column");
146
294
  cJSON *func_j = cJSON_GetObjectItemCaseSensitive(args, "func");
147
- if (!cJSON_IsString(col_j) || !cJSON_IsString(func_j)) return NULL;
295
+ if (!cJSON_IsString(col_j) || !col_j->valuestring[0] ||
296
+ !cJSON_IsString(func_j) || !func_j->valuestring[0]) {
297
+ tf_set_last_error("step: column and func are required");
298
+ return NULL;
299
+ }
148
300
 
149
- step_state *st = calloc(1, sizeof(step_state));
301
+ step_state *st = tf_callocarray_checked(1, sizeof(step_state));
150
302
  if (!st) return NULL;
151
- st->column = strdup(col_j->valuestring);
152
- st->func = parse_func(func_j->valuestring);
303
+ st->column = tf_strdup_checked(col_j->valuestring);
304
+ if (!st->column) { free(st); return NULL; }
305
+ if (parse_func(func_j->valuestring, &st->func) != TF_OK) {
306
+ tf_set_last_error("step: func must be running-sum, running-avg, running-min, running-max, running-count, delta, lag, or ratio");
307
+ free(st->column);
308
+ free(st);
309
+ return NULL;
310
+ }
311
+ if (step_parse_missing_policy(args, &st->missing) != TF_OK ||
312
+ step_parse_type_policy(args, &st->on_type_error) != TF_OK) {
313
+ free(st->column);
314
+ free(st);
315
+ return NULL;
316
+ }
153
317
 
154
318
  cJSON *res_j = cJSON_GetObjectItemCaseSensitive(args, "result");
155
319
  if (cJSON_IsString(res_j)) {
156
- st->result = strdup(res_j->valuestring);
320
+ st->result = tf_strdup_checked(res_j->valuestring);
157
321
  } else {
158
- /* Default result name */
159
- char buf[256];
160
- snprintf(buf, sizeof(buf), "%s_%s", st->column, func_j->valuestring);
161
- st->result = strdup(buf);
322
+ st->result = step_default_result_name(st->column, func_j->valuestring);
162
323
  }
324
+ if (!st->result) { free(st->column); free(st); return NULL; }
163
325
 
164
- tf_step *step = malloc(sizeof(tf_step));
326
+ tf_step *step = tf_callocarray_checked(1, sizeof(tf_step));
165
327
  if (!step) { free(st->column); free(st->result); free(st); return NULL; }
166
328
  step->process = step_process;
167
329
  step->flush = step_flush;
package/csrc/op_tail.c CHANGED
@@ -26,18 +26,18 @@ static int tail_process(tf_step *self, tf_batch *in, tf_batch **out,
26
26
  if (!st->has_schema) {
27
27
  st->buf = tf_batch_create(in->n_cols, st->limit);
28
28
  if (!st->buf) return TF_ERROR;
29
- for (size_t c = 0; c < in->n_cols; c++)
30
- tf_batch_set_schema(st->buf, c, in->col_names[c], in->col_types[c]);
29
+ if (tf_batch_clone_schema(st->buf, in) != TF_OK) return TF_ERROR;
31
30
  st->buf->n_rows = 0;
32
31
  st->has_schema = 1;
33
32
  }
34
33
 
35
34
  for (size_t r = 0; r < in->n_rows; r++) {
36
35
  size_t dst = st->head % st->limit;
37
- tf_batch_copy_row(st->buf, dst, in, r);
36
+ if (tf_batch_copy_row(st->buf, dst, in, r) != TF_OK) return TF_ERROR;
38
37
  st->head++;
39
38
  st->count++;
40
- if (st->buf->n_rows < st->limit) st->buf->n_rows++;
39
+ if (st->buf->n_rows < st->limit &&
40
+ tf_batch_expose_row(st->buf, st->buf->n_rows) != TF_OK) return TF_ERROR;
41
41
  }
42
42
 
43
43
  return TF_OK;
@@ -53,15 +53,23 @@ static int tail_flush(tf_step *self, tf_batch **out, tf_side_channels *side) {
53
53
  size_t n = st->buf->n_rows;
54
54
  tf_batch *ob = tf_batch_create(st->buf->n_cols, n);
55
55
  if (!ob) return TF_ERROR;
56
- for (size_t c = 0; c < st->buf->n_cols; c++)
57
- tf_batch_set_schema(ob, c, st->buf->col_names[c], st->buf->col_types[c]);
56
+ if (tf_batch_clone_schema(ob, st->buf) != TF_OK) {
57
+ tf_batch_free(ob);
58
+ return TF_ERROR;
59
+ }
58
60
 
59
61
  /* Read from circular buffer in order */
60
62
  size_t start = (st->head >= n) ? (st->head - n) % st->limit : 0;
61
63
  for (size_t i = 0; i < n; i++) {
62
64
  size_t src = (start + i) % st->limit;
63
- tf_batch_copy_row(ob, i, st->buf, src);
64
- ob->n_rows = i + 1;
65
+ if (tf_batch_copy_row(ob, i, st->buf, src) != TF_OK) {
66
+ tf_batch_free(ob);
67
+ return TF_ERROR;
68
+ }
69
+ if (tf_batch_expose_row(ob, i) != TF_OK) {
70
+ tf_batch_free(ob);
71
+ return TF_ERROR;
72
+ }
65
73
  }
66
74
 
67
75
  *out = ob;
@@ -79,14 +87,15 @@ static void tail_destroy(tf_step *self) {
79
87
 
80
88
  tf_step *tf_tail_create(const cJSON *args) {
81
89
  if (!args) return NULL;
82
- cJSON *n_json = cJSON_GetObjectItemCaseSensitive(args, "n");
83
- if (!cJSON_IsNumber(n_json) || n_json->valueint <= 0) return NULL;
90
+ size_t n = 0;
91
+ int has_n = tf_json_get_size_arg(args, "n", 1, TF_MAX_OUTPUT_ROWS_PER_BATCH, &n, "tail");
92
+ if (has_n <= 0) return NULL;
84
93
 
85
94
  tail_state *st = calloc(1, sizeof(tail_state));
86
95
  if (!st) return NULL;
87
- st->limit = (size_t)n_json->valueint;
96
+ st->limit = n;
88
97
 
89
- tf_step *step = malloc(sizeof(tf_step));
98
+ tf_step *step = calloc(1, sizeof(tf_step));
90
99
  if (!step) { free(st); return NULL; }
91
100
  step->process = tail_process;
92
101
  step->flush = tail_flush;