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.
- package/LICENSE +177 -0
- package/NOTICE +8 -0
- package/README.md +443 -51
- package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
- package/app/index.html +1 -1
- package/binding.gyp +55 -3
- package/csrc/arena.c +7 -5
- package/csrc/batch.c +818 -71
- package/csrc/buffer.c +84 -8
- package/csrc/cJSON.c +262 -19
- package/csrc/cJSON.h +17 -1
- package/csrc/codec_csv.c +1074 -181
- package/csrc/codec_jsonl.c +830 -118
- package/csrc/codec_table.c +108 -78
- package/csrc/codec_text.c +286 -68
- package/csrc/compiler.c +31 -3
- package/csrc/config.h +21 -0
- package/csrc/dsl.c +4722 -485
- package/csrc/expr.c +363 -55
- package/csrc/expr.h +2 -0
- package/csrc/internal.h +316 -27
- package/csrc/ir.c +65 -18
- package/csrc/ir.h +41 -0
- package/csrc/ir_schema.c +20 -5
- package/csrc/ir_serialize.c +68 -6
- package/csrc/ir_sql.c +796 -185
- package/csrc/ir_validate.c +462 -6
- package/csrc/json_path.c +210 -0
- package/csrc/main.c +879 -30
- package/csrc/memory_estimate.c +477 -0
- package/csrc/op_acf.c +171 -21
- package/csrc/op_across.c +477 -0
- package/csrc/op_anomaly.c +167 -32
- package/csrc/op_assert.c +761 -0
- package/csrc/op_bin.c +168 -29
- package/csrc/op_cast.c +383 -55
- package/csrc/op_clip.c +30 -19
- package/csrc/op_date_trunc.c +208 -34
- package/csrc/op_datetime.c +259 -77
- package/csrc/op_derive.c +65 -97
- package/csrc/op_diff.c +146 -30
- package/csrc/op_ewma.c +149 -30
- package/csrc/op_explode.c +124 -26
- package/csrc/op_fill_down.c +125 -53
- package/csrc/op_fill_null.c +176 -31
- package/csrc/op_filter.c +89 -40
- package/csrc/op_frequency.c +571 -43
- package/csrc/op_grep.c +36 -18
- package/csrc/op_group_agg.c +1790 -119
- package/csrc/op_hash.c +48 -15
- package/csrc/op_head.c +21 -86
- package/csrc/op_interpolate.c +268 -62
- package/csrc/op_join.c +2700 -182
- 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 +328 -53
- package/csrc/op_lag.c +181 -0
- package/csrc/op_lead.c +141 -89
- package/csrc/op_normalize.c +363 -79
- package/csrc/op_onehot.c +345 -73
- package/csrc/op_pivot.c +1546 -162
- package/csrc/op_quarantine.c +189 -0
- package/csrc/op_registry.c +2062 -166
- package/csrc/op_rename.c +41 -50
- package/csrc/op_replace.c +270 -118
- package/csrc/op_rleid.c +297 -0
- package/csrc/op_rowid.c +559 -0
- package/csrc/op_sample.c +80 -23
- package/csrc/op_schema.c +1341 -0
- package/csrc/op_schema_infer.c +252 -0
- package/csrc/op_select.c +265 -65
- package/csrc/op_set.c +3449 -0
- package/csrc/op_skip.c +30 -87
- package/csrc/op_sort.c +670 -124
- package/csrc/op_source_name.c +120 -0
- package/csrc/op_split.c +65 -28
- package/csrc/op_split_data.c +41 -9
- package/csrc/op_stack.c +178 -222
- package/csrc/op_stats.c +206 -110
- package/csrc/op_step.c +217 -55
- package/csrc/op_tail.c +21 -12
- package/csrc/op_tee.c +338 -0
- package/csrc/op_top.c +260 -53
- package/csrc/op_trim.c +48 -19
- package/csrc/op_unique.c +1193 -150
- package/csrc/op_unpivot.c +100 -66
- package/csrc/op_validate.c +601 -24
- package/csrc/op_window.c +492 -51
- package/csrc/path_policy.c +85 -0
- package/csrc/pipeline.c +872 -99
- package/csrc/recipes.c +3 -1
- package/csrc/report.c +73 -30
- package/csrc/selector.c +1097 -0
- package/csrc/size_utils.c +352 -0
- package/csrc/spill.c +317 -0
- package/csrc/spill.h +21 -0
- package/csrc/tranfi.h +169 -1
- 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 +70 -9
- package/napi_api.c +219 -11
- package/napi_transform.c +1648 -0
- package/napi_transform.h +8 -0
- package/package.json +27 -11
- package/scripts/install-native.js +76 -0
- package/scripts/prepack.js +64 -0
- package/scripts/sync-csrc.js +23 -0
- package/src/cli.js +81 -41
- package/src/engines/duckdb.js +45 -12
- package/src/index.js +661 -42
- package/src/memory_policy.js +411 -0
- package/src/native.js +1 -5
- package/src/pipeline.js +454 -31
- package/src/recipe_json.js +80 -0
- package/src/server.js +10 -8
- package/src/transform.js +403 -0
- package/src/transform_error.js +10 -0
- package/src/wasm.js +6 -4
- package/wasm/index.js +498 -10
- package/wasm/tranfi_core.js +0 -0
- package/wasm/transform.js +1156 -0
- package/wasm/worker.js +786 -0
- package/csrc/plan.c +0 -206
package/csrc/ir_sql.c
CHANGED
|
@@ -21,34 +21,60 @@ typedef struct {
|
|
|
21
21
|
char *data;
|
|
22
22
|
size_t len;
|
|
23
23
|
size_t cap;
|
|
24
|
+
int failed;
|
|
24
25
|
} strbuf;
|
|
25
26
|
|
|
27
|
+
static void sb_free(strbuf *sb);
|
|
28
|
+
|
|
26
29
|
static void sb_init(strbuf *sb) {
|
|
27
|
-
sb->data =
|
|
30
|
+
sb->data = tf_mallocarray_checked(256, sizeof(char));
|
|
28
31
|
sb->len = 0;
|
|
29
32
|
sb->cap = 256;
|
|
33
|
+
sb->failed = 0;
|
|
30
34
|
if (sb->data) sb->data[0] = '\0';
|
|
35
|
+
else { sb->cap = 0; sb->failed = 1; }
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
static
|
|
34
|
-
if (sb
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
static int sb_ensure(strbuf *sb, size_t extra) {
|
|
39
|
+
if (!sb || sb->failed) return TF_ERROR;
|
|
40
|
+
size_t need = 0;
|
|
41
|
+
if (tf_size_add(sb->len, extra, &need) != TF_OK ||
|
|
42
|
+
tf_size_add(need, 1, &need) != TF_OK) {
|
|
43
|
+
sb->failed = 1;
|
|
44
|
+
return TF_ERROR;
|
|
45
|
+
}
|
|
46
|
+
if (need <= sb->cap) return TF_OK;
|
|
47
|
+
size_t newcap = 0;
|
|
48
|
+
if (tf_size_grow_pow2(sb->cap, need, 256, &newcap) != TF_OK) {
|
|
49
|
+
sb->failed = 1;
|
|
50
|
+
return TF_ERROR;
|
|
39
51
|
}
|
|
52
|
+
char *nd = tf_reallocarray_checked(sb->data, newcap, sizeof(char));
|
|
53
|
+
if (!nd) {
|
|
54
|
+
sb->failed = 1;
|
|
55
|
+
return TF_ERROR;
|
|
56
|
+
}
|
|
57
|
+
sb->data = nd;
|
|
58
|
+
sb->cap = newcap;
|
|
59
|
+
return TF_OK;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
static void sb_append(strbuf *sb, const char *s) {
|
|
63
|
+
if (!sb || sb->failed || !s) return;
|
|
43
64
|
size_t n = strlen(s);
|
|
44
|
-
sb_ensure(sb, n);
|
|
65
|
+
if (sb_ensure(sb, n) != TF_OK) return;
|
|
45
66
|
memcpy(sb->data + sb->len, s, n);
|
|
46
67
|
sb->len += n;
|
|
47
68
|
sb->data[sb->len] = '\0';
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
static void sb_appendn(strbuf *sb, const char *s, size_t n) {
|
|
51
|
-
|
|
72
|
+
if (!sb || sb->failed || (!s && n > 0)) {
|
|
73
|
+
if (sb) sb->failed = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (sb_ensure(sb, n) != TF_OK) return;
|
|
77
|
+
if (n == 0) return;
|
|
52
78
|
memcpy(sb->data + sb->len, s, n);
|
|
53
79
|
sb->len += n;
|
|
54
80
|
sb->data[sb->len] = '\0';
|
|
@@ -58,36 +84,103 @@ static void sb_appendf(strbuf *sb, const char *fmt, ...)
|
|
|
58
84
|
__attribute__((format(printf, 2, 3)));
|
|
59
85
|
|
|
60
86
|
static void sb_appendf(strbuf *sb, const char *fmt, ...) {
|
|
87
|
+
if (!sb || sb->failed || !fmt) return;
|
|
61
88
|
char buf[512];
|
|
62
89
|
va_list ap;
|
|
63
90
|
va_start(ap, fmt);
|
|
64
91
|
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
65
92
|
va_end(ap);
|
|
66
|
-
if (n
|
|
93
|
+
if (n == 0) return;
|
|
94
|
+
if (n < 0) {
|
|
95
|
+
sb->failed = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if ((size_t)n < sizeof(buf)) {
|
|
99
|
+
sb_appendn(sb, buf, (size_t)n);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
size_t dyn_size = 0;
|
|
103
|
+
if (tf_size_add((size_t)n, 1, &dyn_size) != TF_OK) {
|
|
104
|
+
sb->failed = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
char *dyn = tf_mallocarray_checked(dyn_size, sizeof(char));
|
|
108
|
+
if (!dyn) {
|
|
109
|
+
sb->failed = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
va_start(ap, fmt);
|
|
113
|
+
int n2 = vsnprintf(dyn, dyn_size, fmt, ap);
|
|
114
|
+
va_end(ap);
|
|
115
|
+
if (n2 < 0 || (size_t)n2 >= dyn_size) {
|
|
116
|
+
free(dyn);
|
|
117
|
+
sb->failed = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
sb_appendn(sb, dyn, (size_t)n2);
|
|
121
|
+
free(dyn);
|
|
67
122
|
}
|
|
68
123
|
|
|
69
124
|
static char *sb_detach(strbuf *sb) {
|
|
125
|
+
if (!sb || sb->failed) {
|
|
126
|
+
if (sb) sb_free(sb);
|
|
127
|
+
return NULL;
|
|
128
|
+
}
|
|
70
129
|
char *s = sb->data;
|
|
71
130
|
sb->data = NULL;
|
|
72
131
|
sb->len = sb->cap = 0;
|
|
132
|
+
sb->failed = 0;
|
|
73
133
|
return s;
|
|
74
134
|
}
|
|
75
135
|
|
|
76
136
|
static void sb_free(strbuf *sb) {
|
|
137
|
+
if (!sb) return;
|
|
77
138
|
free(sb->data);
|
|
78
139
|
sb->data = NULL;
|
|
79
140
|
sb->len = sb->cap = 0;
|
|
141
|
+
sb->failed = 0;
|
|
80
142
|
}
|
|
81
143
|
|
|
82
144
|
/* ---- Expression AST to SQL ---- */
|
|
83
145
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
for (const char *p = name; *p; p++) {
|
|
146
|
+
static void sql_quote_ident_part(strbuf *sb, const char *name) {
|
|
147
|
+
const char *safe = name ? name : "";
|
|
148
|
+
for (const char *p = safe; *p; p++) {
|
|
88
149
|
if (*p == '"') sb_append(sb, "\"\"");
|
|
89
150
|
else sb_appendn(sb, p, 1);
|
|
90
151
|
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static void sql_quote_ident_part_range(strbuf *sb, const char *name, size_t len) {
|
|
155
|
+
if (!name && len > 0) {
|
|
156
|
+
if (sb) sb->failed = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (size_t i = 0; i < len; i++) {
|
|
160
|
+
if (name[i] == '"') sb_append(sb, "\"\"");
|
|
161
|
+
else sb_appendn(sb, name + i, 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Append a SQL-quoted identifier: "name" */
|
|
166
|
+
static void sql_quote_ident(strbuf *sb, const char *name) {
|
|
167
|
+
sb_append(sb, "\"");
|
|
168
|
+
sql_quote_ident_part(sb, name);
|
|
169
|
+
sb_append(sb, "\"");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static void sql_quote_ident_range(strbuf *sb, const char *name, size_t len) {
|
|
173
|
+
sb_append(sb, "\"");
|
|
174
|
+
sql_quote_ident_part_range(sb, name, len);
|
|
175
|
+
sb_append(sb, "\"");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static void sql_quote_joined_ident(strbuf *sb, const char *prefix,
|
|
179
|
+
const char *middle, const char *suffix) {
|
|
180
|
+
sb_append(sb, "\"");
|
|
181
|
+
sql_quote_ident_part(sb, prefix);
|
|
182
|
+
sql_quote_ident_part(sb, middle);
|
|
183
|
+
sql_quote_ident_part(sb, suffix);
|
|
91
184
|
sb_append(sb, "\"");
|
|
92
185
|
}
|
|
93
186
|
|
|
@@ -121,13 +214,17 @@ static int expr_to_sql(const tf_expr *e, strbuf *sb) {
|
|
|
121
214
|
return 0;
|
|
122
215
|
|
|
123
216
|
case EXPR_LIT_FLOAT:
|
|
124
|
-
sb_appendf(sb,
|
|
217
|
+
sb_appendf(sb, TF_FLOAT64_ROUNDTRIP_FORMAT, e->lit_float);
|
|
125
218
|
return 0;
|
|
126
219
|
|
|
127
220
|
case EXPR_LIT_STR:
|
|
128
221
|
sql_quote_str(sb, e->lit_str);
|
|
129
222
|
return 0;
|
|
130
223
|
|
|
224
|
+
case EXPR_LIT_BOOL:
|
|
225
|
+
sb_append(sb, e->lit_bool ? "TRUE" : "FALSE");
|
|
226
|
+
return 0;
|
|
227
|
+
|
|
131
228
|
case EXPR_COL_REF:
|
|
132
229
|
sql_quote_ident(sb, e->col_name);
|
|
133
230
|
return 0;
|
|
@@ -224,6 +321,103 @@ static int expr_to_sql(const tf_expr *e, strbuf *sb) {
|
|
|
224
321
|
return 0;
|
|
225
322
|
}
|
|
226
323
|
|
|
324
|
+
/* Special: case_when(cond, value, ..., default) → searched CASE */
|
|
325
|
+
if (strcmp(name, "case_when") == 0 && n >= 2) {
|
|
326
|
+
int pair_count = n / 2;
|
|
327
|
+
int default_idx = (n % 2 == 1) ? n - 1 : -1;
|
|
328
|
+
sb_append(sb, "(CASE");
|
|
329
|
+
for (int i = 0; i < pair_count; i++) {
|
|
330
|
+
sb_append(sb, " WHEN ");
|
|
331
|
+
if (expr_to_sql(e->func.args[i * 2], sb) != 0) return -1;
|
|
332
|
+
sb_append(sb, " THEN ");
|
|
333
|
+
if (expr_to_sql(e->func.args[i * 2 + 1], sb) != 0) return -1;
|
|
334
|
+
}
|
|
335
|
+
sb_append(sb, " ELSE ");
|
|
336
|
+
if (default_idx >= 0) {
|
|
337
|
+
if (expr_to_sql(e->func.args[default_idx], sb) != 0) return -1;
|
|
338
|
+
} else {
|
|
339
|
+
sb_append(sb, "NULL");
|
|
340
|
+
}
|
|
341
|
+
sb_append(sb, " END)");
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Special: case_match(value, key, result, ..., default) → simple CASE */
|
|
346
|
+
if (strcmp(name, "case_match") == 0 && n >= 3) {
|
|
347
|
+
int remaining = n - 1;
|
|
348
|
+
int pair_count = remaining / 2;
|
|
349
|
+
int default_idx = (remaining % 2 == 1) ? n - 1 : -1;
|
|
350
|
+
sb_append(sb, "(CASE ");
|
|
351
|
+
if (expr_to_sql(e->func.args[0], sb) != 0) return -1;
|
|
352
|
+
for (int i = 0; i < pair_count; i++) {
|
|
353
|
+
sb_append(sb, " WHEN ");
|
|
354
|
+
if (expr_to_sql(e->func.args[1 + i * 2], sb) != 0) return -1;
|
|
355
|
+
sb_append(sb, " THEN ");
|
|
356
|
+
if (expr_to_sql(e->func.args[2 + i * 2], sb) != 0) return -1;
|
|
357
|
+
}
|
|
358
|
+
sb_append(sb, " ELSE ");
|
|
359
|
+
if (default_idx >= 0) {
|
|
360
|
+
if (expr_to_sql(e->func.args[default_idx], sb) != 0) return -1;
|
|
361
|
+
} else {
|
|
362
|
+
sb_append(sb, "NULL");
|
|
363
|
+
}
|
|
364
|
+
sb_append(sb, " END)");
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Special: if_any(pred, ...) / if_all(pred, ...) → OR/AND reduction */
|
|
369
|
+
if (strcmp(name, "if_any") == 0 || strcmp(name, "if_all") == 0) {
|
|
370
|
+
int is_any = strcmp(name, "if_any") == 0;
|
|
371
|
+
if (n == 0) {
|
|
372
|
+
sb_append(sb, is_any ? "(FALSE)" : "(TRUE)");
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
sb_append(sb, "(");
|
|
376
|
+
for (int i = 0; i < n; i++) {
|
|
377
|
+
if (i > 0) sb_append(sb, is_any ? " OR " : " AND ");
|
|
378
|
+
if (expr_to_sql(e->func.args[i], sb) != 0) return -1;
|
|
379
|
+
}
|
|
380
|
+
sb_append(sb, ")");
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* Special: between(x, left, right) / inrange(x, left, right) → inclusive SQL range */
|
|
385
|
+
if ((strcmp(name, "between") == 0 || strcmp(name, "inrange") == 0) && n == 3) {
|
|
386
|
+
sb_append(sb, "(");
|
|
387
|
+
if (expr_to_sql(e->func.args[0], sb) != 0) return -1;
|
|
388
|
+
sb_append(sb, " BETWEEN ");
|
|
389
|
+
if (expr_to_sql(e->func.args[1], sb) != 0) return -1;
|
|
390
|
+
sb_append(sb, " AND ");
|
|
391
|
+
if (expr_to_sql(e->func.args[2], sb) != 0) return -1;
|
|
392
|
+
sb_append(sb, ")");
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Special: date/time component extraction */
|
|
397
|
+
if ((strcmp(name, "year") == 0 || strcmp(name, "month") == 0 ||
|
|
398
|
+
strcmp(name, "day") == 0 || strcmp(name, "hour") == 0 ||
|
|
399
|
+
strcmp(name, "minute") == 0 || strcmp(name, "second") == 0 ||
|
|
400
|
+
strcmp(name, "weekday") == 0 || strcmp(name, "epoch") == 0) && n == 1) {
|
|
401
|
+
const char *part = name;
|
|
402
|
+
if (strcmp(name, "weekday") == 0) part = "dow";
|
|
403
|
+
sb_append(sb, "EXTRACT(");
|
|
404
|
+
sb_append(sb, part);
|
|
405
|
+
sb_append(sb, " FROM ");
|
|
406
|
+
if (expr_to_sql(e->func.args[0], sb) != 0) return -1;
|
|
407
|
+
sb_append(sb, ")");
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Special: date_trunc(value, 'unit') → SQL date_trunc('unit', value) */
|
|
412
|
+
if (strcmp(name, "date_trunc") == 0 && n == 2 && e->func.args[1]->kind == EXPR_LIT_STR) {
|
|
413
|
+
sb_append(sb, "date_trunc(");
|
|
414
|
+
sql_quote_str(sb, e->func.args[1]->lit_str);
|
|
415
|
+
sb_append(sb, ", ");
|
|
416
|
+
if (expr_to_sql(e->func.args[0], sb) != 0) return -1;
|
|
417
|
+
sb_append(sb, ")");
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
227
421
|
/* Special: mod(a, b) → (a % b) */
|
|
228
422
|
if (strcmp(name, "mod") == 0 && n == 2) {
|
|
229
423
|
sb_append(sb, "(");
|
|
@@ -275,7 +469,7 @@ static char *translate_expr(const char *expr_str) {
|
|
|
275
469
|
sb_init(&sb);
|
|
276
470
|
int rc = expr_to_sql(e, &sb);
|
|
277
471
|
tf_expr_free(e);
|
|
278
|
-
if (rc != 0) { sb_free(&sb); return NULL; }
|
|
472
|
+
if (rc != 0 || sb.failed) { sb_free(&sb); return NULL; }
|
|
279
473
|
return sb_detach(&sb);
|
|
280
474
|
}
|
|
281
475
|
|
|
@@ -300,6 +494,138 @@ static int jbool(const cJSON *obj, const char *key, int def) {
|
|
|
300
494
|
return def;
|
|
301
495
|
}
|
|
302
496
|
|
|
497
|
+
static const char *sql_type_for_cast(const char *tf_type) {
|
|
498
|
+
if (!tf_type) return "VARCHAR";
|
|
499
|
+
if (strcmp(tf_type, "int") == 0 || strcmp(tf_type, "int64") == 0) return "BIGINT";
|
|
500
|
+
if (strcmp(tf_type, "float") == 0 || strcmp(tf_type, "float64") == 0) return "DOUBLE";
|
|
501
|
+
if (strcmp(tf_type, "bool") == 0 || strcmp(tf_type, "boolean") == 0) return "BOOLEAN";
|
|
502
|
+
if (strcmp(tf_type, "string") == 0 || strcmp(tf_type, "str") == 0) return "VARCHAR";
|
|
503
|
+
if (strcmp(tf_type, "date") == 0) return "DATE";
|
|
504
|
+
if (strcmp(tf_type, "timestamp") == 0 || strcmp(tf_type, "datetime") == 0) return "TIMESTAMP";
|
|
505
|
+
return "VARCHAR";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
static const char *cast_on_error_policy(const cJSON *args) {
|
|
509
|
+
const cJSON *j = args ? cJSON_GetObjectItemCaseSensitive(args, "on_error") : NULL;
|
|
510
|
+
if (!j) j = args ? cJSON_GetObjectItemCaseSensitive(args, "onError") : NULL;
|
|
511
|
+
if (!j || !cJSON_IsString(j) || !j->valuestring) return "coerce";
|
|
512
|
+
if (strcmp(j->valuestring, "null") == 0 || strcmp(j->valuestring, "nulling") == 0) return "null";
|
|
513
|
+
if (strcmp(j->valuestring, "fail") == 0 || strcmp(j->valuestring, "error") == 0 ||
|
|
514
|
+
strcmp(j->valuestring, "strict") == 0) return "fail";
|
|
515
|
+
return "coerce";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
static int append_cast_expr(strbuf *sb, const char *column,
|
|
519
|
+
const char *tf_type, const char *policy) {
|
|
520
|
+
if (!sb || !column) return TF_ERROR;
|
|
521
|
+
const char *sql_type = sql_type_for_cast(tf_type);
|
|
522
|
+
strbuf qcol;
|
|
523
|
+
sb_init(&qcol);
|
|
524
|
+
sql_quote_ident(&qcol, column);
|
|
525
|
+
if (qcol.failed) {
|
|
526
|
+
sb_free(&qcol);
|
|
527
|
+
return TF_ERROR;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (strcmp(policy, "null") == 0) {
|
|
531
|
+
sb_append(sb, "TRY_CAST(");
|
|
532
|
+
sb_append(sb, qcol.data);
|
|
533
|
+
sb_appendf(sb, " AS %s)", sql_type);
|
|
534
|
+
sb_free(&qcol);
|
|
535
|
+
return sb->failed ? TF_ERROR : TF_OK;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (strcmp(policy, "fail") == 0) {
|
|
539
|
+
sb_append(sb, "CAST(");
|
|
540
|
+
sb_append(sb, qcol.data);
|
|
541
|
+
sb_appendf(sb, " AS %s)", sql_type);
|
|
542
|
+
sb_free(&qcol);
|
|
543
|
+
return sb->failed ? TF_ERROR : TF_OK;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (strcmp(sql_type, "BIGINT") == 0) {
|
|
547
|
+
sb_append(sb, "COALESCE(TRY_CAST(regexp_extract(CAST(");
|
|
548
|
+
sb_append(sb, qcol.data);
|
|
549
|
+
sb_append(sb, " AS VARCHAR), ");
|
|
550
|
+
sql_quote_str(sb, "^[[:space:]]*[+-]?[0-9]+");
|
|
551
|
+
sb_append(sb, ") AS BIGINT), 0)");
|
|
552
|
+
} else if (strcmp(sql_type, "DOUBLE") == 0) {
|
|
553
|
+
sb_append(sb, "COALESCE(TRY_CAST(regexp_extract(CAST(");
|
|
554
|
+
sb_append(sb, qcol.data);
|
|
555
|
+
sb_append(sb, " AS VARCHAR), ");
|
|
556
|
+
sql_quote_str(sb, "^[[:space:]]*[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?");
|
|
557
|
+
sb_append(sb, ") AS DOUBLE), 0.0)");
|
|
558
|
+
} else if (strcmp(sql_type, "BOOLEAN") == 0) {
|
|
559
|
+
sb_append(sb, "(CASE WHEN ");
|
|
560
|
+
sb_append(sb, qcol.data);
|
|
561
|
+
sb_append(sb, " IS NULL THEN NULL WHEN CAST(");
|
|
562
|
+
sb_append(sb, qcol.data);
|
|
563
|
+
sb_append(sb, " AS VARCHAR) = '' OR CAST(");
|
|
564
|
+
sb_append(sb, qcol.data);
|
|
565
|
+
sb_append(sb, " AS VARCHAR) = 'false' THEN FALSE ELSE TRUE END)");
|
|
566
|
+
} else if (strcmp(sql_type, "DATE") == 0) {
|
|
567
|
+
sb_append(sb, "COALESCE(TRY_CAST(");
|
|
568
|
+
sb_append(sb, qcol.data);
|
|
569
|
+
sb_append(sb, " AS DATE), DATE '1970-01-01')");
|
|
570
|
+
} else if (strcmp(sql_type, "TIMESTAMP") == 0) {
|
|
571
|
+
sb_append(sb, "COALESCE(TRY_CAST(");
|
|
572
|
+
sb_append(sb, qcol.data);
|
|
573
|
+
sb_append(sb, " AS TIMESTAMP), TIMESTAMP '1970-01-01 00:00:00')");
|
|
574
|
+
} else {
|
|
575
|
+
sb_append(sb, "CAST(");
|
|
576
|
+
sb_append(sb, qcol.data);
|
|
577
|
+
sb_appendf(sb, " AS %s)", sql_type);
|
|
578
|
+
}
|
|
579
|
+
sb_free(&qcol);
|
|
580
|
+
return sb->failed ? TF_ERROR : TF_OK;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
static int append_frequency_key_expr(strbuf *sb, cJSON *cols) {
|
|
584
|
+
if (!sb || !cJSON_IsArray(cols) || cJSON_GetArraySize(cols) <= 0) return TF_ERROR;
|
|
585
|
+
int n = cJSON_GetArraySize(cols);
|
|
586
|
+
for (int i = 0; i < n; i++) {
|
|
587
|
+
cJSON *c = cJSON_GetArrayItem(cols, i);
|
|
588
|
+
if (!cJSON_IsString(c) || !c->valuestring || c->valuestring[0] == '\0') {
|
|
589
|
+
return TF_ERROR;
|
|
590
|
+
}
|
|
591
|
+
if (i > 0) sb_append(sb, " || chr(1) || ");
|
|
592
|
+
sb_append(sb, "COALESCE(CAST(");
|
|
593
|
+
sql_quote_ident(sb, c->valuestring);
|
|
594
|
+
sb_append(sb, " AS VARCHAR), ");
|
|
595
|
+
sql_quote_str(sb, "\\N");
|
|
596
|
+
sb_append(sb, ")");
|
|
597
|
+
if (sb->failed) return TF_ERROR;
|
|
598
|
+
}
|
|
599
|
+
return TF_OK;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
static int append_grep_match_expr(strbuf *sb, const char *column,
|
|
603
|
+
const char *pattern, int regex) {
|
|
604
|
+
if (!sb || !column || !pattern) return TF_ERROR;
|
|
605
|
+
strbuf qcol;
|
|
606
|
+
sb_init(&qcol);
|
|
607
|
+
sql_quote_ident(&qcol, column);
|
|
608
|
+
if (qcol.failed) {
|
|
609
|
+
sb_free(&qcol);
|
|
610
|
+
return TF_ERROR;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
sb_append(sb, "(CASE WHEN typeof(");
|
|
614
|
+
sb_append(sb, qcol.data);
|
|
615
|
+
sb_append(sb, ") = 'VARCHAR' THEN COALESCE(");
|
|
616
|
+
if (regex) {
|
|
617
|
+
sb_append(sb, "regexp_matches(CAST(");
|
|
618
|
+
} else {
|
|
619
|
+
sb_append(sb, "contains(CAST(");
|
|
620
|
+
}
|
|
621
|
+
sb_append(sb, qcol.data);
|
|
622
|
+
sb_append(sb, " AS VARCHAR), ");
|
|
623
|
+
sql_quote_str(sb, pattern);
|
|
624
|
+
sb_append(sb, "), FALSE) ELSE FALSE END)");
|
|
625
|
+
sb_free(&qcol);
|
|
626
|
+
return sb->failed ? TF_ERROR : TF_OK;
|
|
627
|
+
}
|
|
628
|
+
|
|
303
629
|
/* Emit a CTE for a transform op. prev is the name of the previous CTE/source.
|
|
304
630
|
* Appends SQL like: step_N AS (SELECT ... FROM prev ...) */
|
|
305
631
|
static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
@@ -316,10 +642,48 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
316
642
|
return 0;
|
|
317
643
|
}
|
|
318
644
|
|
|
645
|
+
/* ---- relocate ---- */
|
|
646
|
+
if (strcmp(op, "relocate") == 0) {
|
|
647
|
+
cJSON *before = cJSON_GetObjectItemCaseSensitive(args, "before");
|
|
648
|
+
cJSON *after = cJSON_GetObjectItemCaseSensitive(args, "after");
|
|
649
|
+
if (before || after) {
|
|
650
|
+
*error = strdup("relocate: SQL lowering for before/after requires known schema; default-front relocate is supported");
|
|
651
|
+
return -1;
|
|
652
|
+
}
|
|
653
|
+
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
654
|
+
if (!cols || !cJSON_IsArray(cols)) { *error = strdup("relocate: missing 'columns'"); return -1; }
|
|
655
|
+
if (tf_column_selectors_have_syntax_json(cols)) {
|
|
656
|
+
*error = strdup("relocate: selector helpers require known schema for SQL lowering");
|
|
657
|
+
return -1;
|
|
658
|
+
}
|
|
659
|
+
strbuf moved;
|
|
660
|
+
sb_init(&moved);
|
|
661
|
+
int n = cJSON_GetArraySize(cols);
|
|
662
|
+
for (int i = 0; i < n; i++) {
|
|
663
|
+
cJSON *c = cJSON_GetArrayItem(cols, i);
|
|
664
|
+
if (!cJSON_IsString(c)) continue;
|
|
665
|
+
if (moved.len > 0) sb_append(&moved, ", ");
|
|
666
|
+
sql_quote_ident(&moved, c->valuestring);
|
|
667
|
+
}
|
|
668
|
+
if (moved.len == 0) {
|
|
669
|
+
sb_free(&moved);
|
|
670
|
+
*error = strdup("relocate: missing 'columns'");
|
|
671
|
+
return -1;
|
|
672
|
+
}
|
|
673
|
+
sb_appendf(sb, "%s AS (SELECT %s, * EXCLUDE (%s) FROM %s)",
|
|
674
|
+
cte_name, moved.data, moved.data, prev);
|
|
675
|
+
sb_free(&moved);
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
|
|
319
679
|
/* ---- select / reorder ---- */
|
|
320
680
|
if (strcmp(op, "select") == 0 || strcmp(op, "reorder") == 0) {
|
|
321
681
|
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
322
682
|
if (!cols || !cJSON_IsArray(cols)) { *error = strdup("select: missing 'columns'"); return -1; }
|
|
683
|
+
if (tf_column_selectors_have_syntax_json(cols)) {
|
|
684
|
+
*error = strdup("select: selector helpers require known schema for SQL lowering");
|
|
685
|
+
return -1;
|
|
686
|
+
}
|
|
323
687
|
strbuf sel;
|
|
324
688
|
sb_init(&sel);
|
|
325
689
|
int n = cJSON_GetArraySize(cols);
|
|
@@ -393,6 +757,18 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
393
757
|
|
|
394
758
|
/* ---- unique / dedup ---- */
|
|
395
759
|
if (strcmp(op, "unique") == 0 || strcmp(op, "dedup") == 0) {
|
|
760
|
+
cJSON *sorted = cJSON_GetObjectItemCaseSensitive(args, "sorted");
|
|
761
|
+
if (cJSON_IsBool(sorted) && cJSON_IsTrue(sorted)) {
|
|
762
|
+
*error = strdup("unique: sorted=true adjacent-run mode cannot be lowered to SQL");
|
|
763
|
+
return -1;
|
|
764
|
+
}
|
|
765
|
+
cJSON *mode = cJSON_GetObjectItemCaseSensitive(args, "mode");
|
|
766
|
+
cJSON *approx = cJSON_GetObjectItemCaseSensitive(args, "approx");
|
|
767
|
+
if ((cJSON_IsString(mode) && mode->valuestring && strcmp(mode->valuestring, "approx") == 0) ||
|
|
768
|
+
(cJSON_IsBool(approx) && cJSON_IsTrue(approx))) {
|
|
769
|
+
*error = strdup("unique: approximate mode cannot be lowered to SQL");
|
|
770
|
+
return -1;
|
|
771
|
+
}
|
|
396
772
|
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
397
773
|
if (cols && cJSON_IsArray(cols) && cJSON_GetArraySize(cols) > 0) {
|
|
398
774
|
strbuf dcols;
|
|
@@ -434,7 +810,7 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
434
810
|
}
|
|
435
811
|
|
|
436
812
|
/* ---- head ---- */
|
|
437
|
-
if (strcmp(op, "head") == 0) {
|
|
813
|
+
if (strcmp(op, "head") == 0 || strcmp(op, "slice-head") == 0) {
|
|
438
814
|
int n = jint(args, "n", 10);
|
|
439
815
|
sb_appendf(sb, "%s AS (SELECT * FROM %s LIMIT %d)", cte_name, prev, n);
|
|
440
816
|
return 0;
|
|
@@ -448,7 +824,7 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
448
824
|
}
|
|
449
825
|
|
|
450
826
|
/* ---- tail ---- */
|
|
451
|
-
if (strcmp(op, "tail") == 0) {
|
|
827
|
+
if (strcmp(op, "tail") == 0 || strcmp(op, "slice-tail") == 0) {
|
|
452
828
|
int n = jint(args, "n", 10);
|
|
453
829
|
sb_appendf(sb, "%s AS (SELECT * FROM (SELECT *, ROW_NUMBER() OVER () AS _rn, "
|
|
454
830
|
"COUNT(*) OVER () AS _total FROM %s) WHERE _rn > _total - %d)",
|
|
@@ -456,12 +832,14 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
456
832
|
return 0;
|
|
457
833
|
}
|
|
458
834
|
|
|
459
|
-
/* ---- top ---- */
|
|
460
|
-
if (strcmp(op, "top") == 0)
|
|
835
|
+
/* ---- bounded top-k / bottom-k ---- */
|
|
836
|
+
if (strcmp(op, "top") == 0 || strcmp(op, "top-k") == 0 ||
|
|
837
|
+
strcmp(op, "bottom-k") == 0 || strcmp(op, "slice-min") == 0 ||
|
|
838
|
+
strcmp(op, "slice-max") == 0) {
|
|
461
839
|
int n = jint(args, "n", 10);
|
|
462
840
|
const char *column = jstr(args, "column");
|
|
463
841
|
int desc = jbool(args, "desc", 1);
|
|
464
|
-
if (!column) { *error = strdup("top: missing 'column'"); return -1; }
|
|
842
|
+
if (!column) { *error = strdup("top-k: missing 'column'"); return -1; }
|
|
465
843
|
strbuf tmp;
|
|
466
844
|
sb_init(&tmp);
|
|
467
845
|
sql_quote_ident(&tmp, column);
|
|
@@ -473,9 +851,8 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
473
851
|
|
|
474
852
|
/* ---- sample ---- */
|
|
475
853
|
if (strcmp(op, "sample") == 0) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return 0;
|
|
854
|
+
*error = strdup("sample: deterministic reservoir sampling cannot be lowered to SQL");
|
|
855
|
+
return -1;
|
|
479
856
|
}
|
|
480
857
|
|
|
481
858
|
/* ---- grep ---- */
|
|
@@ -486,28 +863,16 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
486
863
|
int regex = jbool(args, "regex", 0);
|
|
487
864
|
if (!pattern) { *error = strdup("grep: missing 'pattern'"); return -1; }
|
|
488
865
|
if (!column) column = "_line";
|
|
489
|
-
strbuf
|
|
490
|
-
sb_init(&
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
sql_quote_str(sb, pattern);
|
|
496
|
-
sb_append(sb, "))");
|
|
497
|
-
} else {
|
|
498
|
-
sb_appendf(sb, "%s AS (SELECT * FROM %s WHERE ", cte_name, prev);
|
|
499
|
-
if (invert) sb_append(sb, "NOT ");
|
|
500
|
-
sb_append(sb, tmp.data);
|
|
501
|
-
sb_append(sb, " LIKE '%");
|
|
502
|
-
/* Escape pattern for LIKE */
|
|
503
|
-
for (const char *p = pattern; *p; p++) {
|
|
504
|
-
if (*p == '%' || *p == '_' || *p == '\\') sb_appendn(sb, "\\", 1);
|
|
505
|
-
if (*p == '\'') sb_append(sb, "''");
|
|
506
|
-
else sb_appendn(sb, p, 1);
|
|
507
|
-
}
|
|
508
|
-
sb_append(sb, "%')");
|
|
866
|
+
strbuf pred;
|
|
867
|
+
sb_init(&pred);
|
|
868
|
+
if (append_grep_match_expr(&pred, column, pattern, regex) != TF_OK) {
|
|
869
|
+
sb_free(&pred);
|
|
870
|
+
*error = strdup("sql: out of memory");
|
|
871
|
+
return -1;
|
|
509
872
|
}
|
|
510
|
-
|
|
873
|
+
sb_appendf(sb, "%s AS (SELECT * FROM %s WHERE %s%s)",
|
|
874
|
+
cte_name, prev, invert ? "NOT " : "", pred.data);
|
|
875
|
+
sb_free(&pred);
|
|
511
876
|
return 0;
|
|
512
877
|
}
|
|
513
878
|
|
|
@@ -515,48 +880,23 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
515
880
|
if (strcmp(op, "cast") == 0) {
|
|
516
881
|
cJSON *mapping = cJSON_GetObjectItemCaseSensitive(args, "mapping");
|
|
517
882
|
if (!mapping) { *error = strdup("cast: missing 'mapping'"); return -1; }
|
|
518
|
-
|
|
519
|
-
sb_init(&cols);
|
|
520
|
-
cJSON *item = NULL;
|
|
521
|
-
cJSON_ArrayForEach(item, mapping) {
|
|
522
|
-
if (!cJSON_IsString(item)) continue;
|
|
523
|
-
sb_append(&cols, ", CAST(");
|
|
524
|
-
sql_quote_ident(&cols, item->string);
|
|
525
|
-
/* Map tranfi types to SQL types */
|
|
526
|
-
const char *tf_type = item->valuestring;
|
|
527
|
-
const char *sql_type = "VARCHAR";
|
|
528
|
-
if (strcmp(tf_type, "int") == 0 || strcmp(tf_type, "int64") == 0) sql_type = "BIGINT";
|
|
529
|
-
else if (strcmp(tf_type, "float") == 0 || strcmp(tf_type, "float64") == 0) sql_type = "DOUBLE";
|
|
530
|
-
else if (strcmp(tf_type, "bool") == 0) sql_type = "BOOLEAN";
|
|
531
|
-
else if (strcmp(tf_type, "string") == 0) sql_type = "VARCHAR";
|
|
532
|
-
else if (strcmp(tf_type, "date") == 0) sql_type = "DATE";
|
|
533
|
-
else if (strcmp(tf_type, "timestamp") == 0) sql_type = "TIMESTAMP";
|
|
534
|
-
sb_appendf(&cols, " AS %s) AS ", sql_type);
|
|
535
|
-
sql_quote_ident(&cols, item->string);
|
|
536
|
-
}
|
|
537
|
-
/* Use COLUMNS(*) EXCLUDE + explicit casts — simpler: use REPLACE */
|
|
538
|
-
/* DuckDB REPLACE: SELECT * REPLACE (CAST(col AS type) AS col) */
|
|
883
|
+
const char *policy = cast_on_error_policy(args);
|
|
539
884
|
strbuf rep;
|
|
540
885
|
sb_init(&rep);
|
|
541
886
|
int first = 1;
|
|
887
|
+
cJSON *item = NULL;
|
|
542
888
|
cJSON_ArrayForEach(item, mapping) {
|
|
543
889
|
if (!cJSON_IsString(item)) continue;
|
|
544
890
|
if (!first) sb_append(&rep, ", ");
|
|
545
891
|
first = 0;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
else if (strcmp(tf_type, "date") == 0) sql_type = "DATE";
|
|
553
|
-
else if (strcmp(tf_type, "timestamp") == 0) sql_type = "TIMESTAMP";
|
|
554
|
-
sb_append(&rep, "CAST(");
|
|
555
|
-
sql_quote_ident(&rep, item->string);
|
|
556
|
-
sb_appendf(&rep, " AS %s) AS ", sql_type);
|
|
892
|
+
if (append_cast_expr(&rep, item->string, item->valuestring, policy) != TF_OK) {
|
|
893
|
+
sb_free(&rep);
|
|
894
|
+
*error = strdup("sql: out of memory");
|
|
895
|
+
return -1;
|
|
896
|
+
}
|
|
897
|
+
sb_append(&rep, " AS ");
|
|
557
898
|
sql_quote_ident(&rep, item->string);
|
|
558
899
|
}
|
|
559
|
-
sb_free(&cols);
|
|
560
900
|
sb_appendf(sb, "%s AS (SELECT * REPLACE (%s) FROM %s)", cte_name, rep.data, prev);
|
|
561
901
|
sb_free(&rep);
|
|
562
902
|
return 0;
|
|
@@ -570,28 +910,26 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
570
910
|
cJSON *max_v = cJSON_GetObjectItemCaseSensitive(args, "max");
|
|
571
911
|
strbuf expr;
|
|
572
912
|
sb_init(&expr);
|
|
573
|
-
sql_quote_ident(&expr, column);
|
|
574
913
|
if (min_v && max_v) {
|
|
575
914
|
strbuf tmp;
|
|
576
915
|
sb_init(&tmp);
|
|
577
916
|
sql_quote_ident(&tmp, column);
|
|
578
|
-
|
|
579
|
-
sb_appendf(&expr, "GREATEST(%g, LEAST(%g, %s))", min_v->valuedouble, max_v->valuedouble, tmp.data);
|
|
917
|
+
sb_appendf(&expr, "GREATEST(" TF_FLOAT64_ROUNDTRIP_FORMAT ", LEAST(" TF_FLOAT64_ROUNDTRIP_FORMAT ", %s))", min_v->valuedouble, max_v->valuedouble, tmp.data);
|
|
580
918
|
sb_free(&tmp);
|
|
581
919
|
} else if (min_v) {
|
|
582
920
|
strbuf tmp;
|
|
583
921
|
sb_init(&tmp);
|
|
584
922
|
sql_quote_ident(&tmp, column);
|
|
585
|
-
|
|
586
|
-
sb_appendf(&expr, "GREATEST(%g, %s)", min_v->valuedouble, tmp.data);
|
|
923
|
+
sb_appendf(&expr, "GREATEST(" TF_FLOAT64_ROUNDTRIP_FORMAT ", %s)", min_v->valuedouble, tmp.data);
|
|
587
924
|
sb_free(&tmp);
|
|
588
925
|
} else if (max_v) {
|
|
589
926
|
strbuf tmp;
|
|
590
927
|
sb_init(&tmp);
|
|
591
928
|
sql_quote_ident(&tmp, column);
|
|
592
|
-
|
|
593
|
-
sb_appendf(&expr, "LEAST(%g, %s)", max_v->valuedouble, tmp.data);
|
|
929
|
+
sb_appendf(&expr, "LEAST(" TF_FLOAT64_ROUNDTRIP_FORMAT ", %s)", max_v->valuedouble, tmp.data);
|
|
594
930
|
sb_free(&tmp);
|
|
931
|
+
} else {
|
|
932
|
+
sql_quote_ident(&expr, column);
|
|
595
933
|
}
|
|
596
934
|
strbuf qcol;
|
|
597
935
|
sb_init(&qcol);
|
|
@@ -640,27 +978,33 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
640
978
|
/* ---- trim ---- */
|
|
641
979
|
if (strcmp(op, "trim") == 0) {
|
|
642
980
|
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
643
|
-
if (cols
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
981
|
+
if (!cols || !cJSON_IsArray(cols) || cJSON_GetArraySize(cols) <= 0) {
|
|
982
|
+
*error = strdup("trim: SQL lowering requires explicit columns");
|
|
983
|
+
return -1;
|
|
984
|
+
}
|
|
985
|
+
strbuf rep;
|
|
986
|
+
sb_init(&rep);
|
|
987
|
+
int n = cJSON_GetArraySize(cols);
|
|
988
|
+
for (int i = 0; i < n; i++) {
|
|
989
|
+
cJSON *c = cJSON_GetArrayItem(cols, i);
|
|
990
|
+
if (!cJSON_IsString(c) || !c->valuestring || c->valuestring[0] == '\0') {
|
|
991
|
+
sb_free(&rep);
|
|
992
|
+
*error = strdup("trim: invalid columns");
|
|
993
|
+
return -1;
|
|
655
994
|
}
|
|
656
|
-
|
|
995
|
+
if (i > 0) sb_append(&rep, ", ");
|
|
996
|
+
sb_append(&rep, "trim(");
|
|
997
|
+
sql_quote_ident(&rep, c->valuestring);
|
|
998
|
+
sb_append(&rep, ") AS ");
|
|
999
|
+
sql_quote_ident(&rep, c->valuestring);
|
|
1000
|
+
}
|
|
1001
|
+
if (rep.failed) {
|
|
657
1002
|
sb_free(&rep);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
* DuckDB doesn't have a "trim all" so we'd need column names.
|
|
661
|
-
* Fallback: SELECT * (no trim) with a comment. */
|
|
662
|
-
sb_appendf(sb, "%s AS (SELECT * FROM %s)", cte_name, prev);
|
|
1003
|
+
*error = strdup("sql: out of memory");
|
|
1004
|
+
return -1;
|
|
663
1005
|
}
|
|
1006
|
+
sb_appendf(sb, "%s AS (SELECT * REPLACE (%s) FROM %s)", cte_name, rep.data, prev);
|
|
1007
|
+
sb_free(&rep);
|
|
664
1008
|
return 0;
|
|
665
1009
|
}
|
|
666
1010
|
|
|
@@ -724,13 +1068,12 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
724
1068
|
else if (strcmp(func, "median") == 0) sql_func = "MEDIAN";
|
|
725
1069
|
sb_append(&sel, sql_func);
|
|
726
1070
|
sb_append(&sel, "(");
|
|
727
|
-
|
|
1071
|
+
if (strcmp(func, "count") == 0 && strcmp(col, "*") == 0) sb_append(&sel, "*");
|
|
1072
|
+
else sql_quote_ident(&sel, col);
|
|
728
1073
|
sb_append(&sel, ") AS ");
|
|
729
1074
|
if (result) sql_quote_ident(&sel, result);
|
|
730
1075
|
else {
|
|
731
|
-
|
|
732
|
-
snprintf(buf, sizeof(buf), "%s_%s", func, col);
|
|
733
|
-
sql_quote_ident(&sel, buf);
|
|
1076
|
+
sql_quote_joined_ident(&sel, col, "_", func);
|
|
734
1077
|
}
|
|
735
1078
|
}
|
|
736
1079
|
/* GROUP BY clause */
|
|
@@ -749,40 +1092,53 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
749
1092
|
|
|
750
1093
|
/* ---- frequency ---- */
|
|
751
1094
|
if (strcmp(op, "frequency") == 0) {
|
|
1095
|
+
cJSON *mode = args ? cJSON_GetObjectItemCaseSensitive(args, "mode") : NULL;
|
|
1096
|
+
if (cJSON_IsString(mode) && strcmp(mode->valuestring, "approx") == 0) {
|
|
1097
|
+
*error = strdup("frequency: mode=approx is not supported by SQL lowering");
|
|
1098
|
+
return -1;
|
|
1099
|
+
}
|
|
1100
|
+
cJSON *approx = args ? cJSON_GetObjectItemCaseSensitive(args, "approx") : NULL;
|
|
1101
|
+
if (cJSON_IsTrue(approx)) {
|
|
1102
|
+
*error = strdup("frequency: mode=approx is not supported by SQL lowering");
|
|
1103
|
+
return -1;
|
|
1104
|
+
}
|
|
1105
|
+
cJSON *overflow = args ? cJSON_GetObjectItemCaseSensitive(args, "overflow") : NULL;
|
|
1106
|
+
if (cJSON_IsString(overflow) && strcmp(overflow->valuestring, "other") == 0) {
|
|
1107
|
+
*error = strdup("frequency: overflow=other is not supported by SQL lowering");
|
|
1108
|
+
return -1;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
752
1111
|
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
753
|
-
if (cols
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
strbuf grp;
|
|
757
|
-
sb_init(&grp);
|
|
758
|
-
int n = cJSON_GetArraySize(cols);
|
|
759
|
-
for (int i = 0; i < n; i++) {
|
|
760
|
-
if (i > 0) { sb_append(&sel, ", "); sb_append(&grp, ", "); }
|
|
761
|
-
cJSON *c = cJSON_GetArrayItem(cols, i);
|
|
762
|
-
if (cJSON_IsString(c)) {
|
|
763
|
-
sql_quote_ident(&sel, c->valuestring);
|
|
764
|
-
sql_quote_ident(&grp, c->valuestring);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
sb_appendf(sb, "%s AS (SELECT %s, COUNT(*) AS \"count\" FROM %s GROUP BY %s ORDER BY \"count\" DESC)",
|
|
768
|
-
cte_name, sel.data, prev, grp.data);
|
|
769
|
-
sb_free(&sel);
|
|
770
|
-
sb_free(&grp);
|
|
771
|
-
} else {
|
|
772
|
-
/* No columns specified — frequency of all columns not meaningful,
|
|
773
|
-
* fall back to counting rows */
|
|
774
|
-
sb_appendf(sb, "%s AS (SELECT COUNT(*) AS \"count\" FROM %s)", cte_name, prev);
|
|
1112
|
+
if (!cols || !cJSON_IsArray(cols) || cJSON_GetArraySize(cols) <= 0) {
|
|
1113
|
+
*error = strdup("frequency: SQL lowering requires explicit columns");
|
|
1114
|
+
return -1;
|
|
775
1115
|
}
|
|
1116
|
+
strbuf key;
|
|
1117
|
+
sb_init(&key);
|
|
1118
|
+
if (append_frequency_key_expr(&key, cols) != TF_OK) {
|
|
1119
|
+
sb_free(&key);
|
|
1120
|
+
*error = strdup("frequency: invalid columns");
|
|
1121
|
+
return -1;
|
|
1122
|
+
}
|
|
1123
|
+
sb_appendf(sb, "%s AS (SELECT \"value\", COUNT(*) AS \"count\" FROM "
|
|
1124
|
+
"(SELECT %s AS \"value\" FROM %s) __tf_frequency "
|
|
1125
|
+
"GROUP BY \"value\" ORDER BY \"count\" DESC, \"value\" ASC)",
|
|
1126
|
+
cte_name, key.data, prev);
|
|
1127
|
+
sb_free(&key);
|
|
776
1128
|
return 0;
|
|
777
1129
|
}
|
|
778
1130
|
|
|
779
1131
|
/* ---- join ---- */
|
|
780
|
-
if (strcmp(op, "join") == 0) {
|
|
1132
|
+
if (strcmp(op, "join") == 0 || strcmp(op, "semi-join") == 0 || strcmp(op, "anti-join") == 0) {
|
|
781
1133
|
const char *file = jstr(args, "file");
|
|
782
1134
|
const char *on = jstr(args, "on");
|
|
783
1135
|
const char *how = jstr(args, "how");
|
|
784
1136
|
if (!file || !on) { *error = strdup("join: missing 'file' or 'on'"); return -1; }
|
|
785
|
-
if (!how)
|
|
1137
|
+
if (!how) {
|
|
1138
|
+
if (strcmp(op, "semi-join") == 0) how = "semi";
|
|
1139
|
+
else if (strcmp(op, "anti-join") == 0) how = "anti";
|
|
1140
|
+
else how = "inner";
|
|
1141
|
+
}
|
|
786
1142
|
const char *join_type = "INNER";
|
|
787
1143
|
if (strcmp(how, "left") == 0) join_type = "LEFT";
|
|
788
1144
|
else if (strcmp(how, "right") == 0) join_type = "RIGHT";
|
|
@@ -793,27 +1149,34 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
793
1149
|
strbuf cond;
|
|
794
1150
|
sb_init(&cond);
|
|
795
1151
|
if (eq) {
|
|
796
|
-
char
|
|
1152
|
+
const char *left = on;
|
|
797
1153
|
size_t llen = (size_t)(eq - on);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
while (
|
|
805
|
-
char *rp = right_col;
|
|
806
|
-
while (*rp == ' ') rp++;
|
|
1154
|
+
while (llen > 0 && *left == ' ') { left++; llen--; }
|
|
1155
|
+
while (llen > 0 && left[llen - 1] == ' ') llen--;
|
|
1156
|
+
|
|
1157
|
+
const char *right = eq + 1;
|
|
1158
|
+
while (*right == ' ') right++;
|
|
1159
|
+
size_t rlen = strlen(right);
|
|
1160
|
+
while (rlen > 0 && right[rlen - 1] == ' ') rlen--;
|
|
807
1161
|
sb_append(&cond, "a.");
|
|
808
|
-
|
|
1162
|
+
sql_quote_ident_range(&cond, left, llen);
|
|
809
1163
|
sb_append(&cond, " = b.");
|
|
810
|
-
|
|
1164
|
+
sql_quote_ident_range(&cond, right, rlen);
|
|
811
1165
|
} else {
|
|
812
1166
|
sb_append(&cond, "a.");
|
|
813
1167
|
sql_quote_ident(&cond, on);
|
|
814
1168
|
sb_append(&cond, " = b.");
|
|
815
1169
|
sql_quote_ident(&cond, on);
|
|
816
1170
|
}
|
|
1171
|
+
if (strcmp(how, "semi") == 0 || strcmp(how, "anti") == 0) {
|
|
1172
|
+
sb_appendf(sb, "%s AS (SELECT a.* FROM %s a WHERE %sEXISTS (SELECT 1 FROM read_csv_auto(",
|
|
1173
|
+
cte_name, prev, strcmp(how, "anti") == 0 ? "NOT " : "");
|
|
1174
|
+
sql_quote_str(sb, file);
|
|
1175
|
+
sb_appendf(sb, ") b WHERE %s))", cond.data);
|
|
1176
|
+
sb_free(&cond);
|
|
1177
|
+
return 0;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
817
1180
|
sb_appendf(sb, "%s AS (SELECT a.* FROM %s a %s JOIN read_csv_auto(",
|
|
818
1181
|
cte_name, prev, join_type);
|
|
819
1182
|
sql_quote_str(sb, file);
|
|
@@ -822,6 +1185,29 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
822
1185
|
return 0;
|
|
823
1186
|
}
|
|
824
1187
|
|
|
1188
|
+
/* ---- set ops ---- */
|
|
1189
|
+
if (strcmp(op, "intersect") == 0 || strcmp(op, "setdiff") == 0 ||
|
|
1190
|
+
strcmp(op, "intersect-all") == 0 || strcmp(op, "setdiff-all") == 0 ||
|
|
1191
|
+
strcmp(op, "union") == 0 || strcmp(op, "union-all") == 0) {
|
|
1192
|
+
const char *file = jstr(args, "file");
|
|
1193
|
+
if (!file) { *error = strdup("set op: missing 'file'"); return -1; }
|
|
1194
|
+
cJSON *cols = args ? cJSON_GetObjectItemCaseSensitive(args, "columns") : NULL;
|
|
1195
|
+
if (cols && cJSON_IsArray(cols) && cJSON_GetArraySize(cols) > 0) {
|
|
1196
|
+
*error = strdup("set op SQL lowering only supports all-column set operations");
|
|
1197
|
+
return -1;
|
|
1198
|
+
}
|
|
1199
|
+
const char *sql_op = "INTERSECT";
|
|
1200
|
+
if (strcmp(op, "setdiff") == 0) sql_op = "EXCEPT";
|
|
1201
|
+
else if (strcmp(op, "intersect-all") == 0) sql_op = "INTERSECT ALL";
|
|
1202
|
+
else if (strcmp(op, "setdiff-all") == 0) sql_op = "EXCEPT ALL";
|
|
1203
|
+
else if (strcmp(op, "union") == 0) sql_op = "UNION";
|
|
1204
|
+
else if (strcmp(op, "union-all") == 0) sql_op = "UNION ALL";
|
|
1205
|
+
sb_appendf(sb, "%s AS (SELECT * FROM %s %s SELECT * FROM read_csv_auto(", cte_name, prev, sql_op);
|
|
1206
|
+
sql_quote_str(sb, file);
|
|
1207
|
+
sb_append(sb, "))");
|
|
1208
|
+
return 0;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
825
1211
|
/* ---- stack ---- */
|
|
826
1212
|
if (strcmp(op, "stack") == 0) {
|
|
827
1213
|
const char *file = jstr(args, "file");
|
|
@@ -841,9 +1227,9 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
841
1227
|
strbuf qcol;
|
|
842
1228
|
sb_init(&qcol);
|
|
843
1229
|
sql_quote_ident(&qcol, column);
|
|
844
|
-
sb_appendf(sb, "%s AS (SELECT * REPLACE (unnest(string_split(%s, ", cte_name, qcol.data);
|
|
1230
|
+
sb_appendf(sb, "%s AS (SELECT * REPLACE (unnest(CASE WHEN %s IS NULL THEN list_value(NULL::VARCHAR) ELSE string_split(%s, ", cte_name, qcol.data, qcol.data);
|
|
845
1231
|
sql_quote_str(sb, delimiter);
|
|
846
|
-
sb_appendf(sb, ")) AS %s) FROM %s)", qcol.data, prev);
|
|
1232
|
+
sb_appendf(sb, ") END) AS %s) FROM %s)", qcol.data, prev);
|
|
847
1233
|
sb_free(&qcol);
|
|
848
1234
|
return 0;
|
|
849
1235
|
}
|
|
@@ -898,6 +1284,7 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
898
1284
|
const char *name_col = jstr(args, "name_column");
|
|
899
1285
|
const char *val_col = jstr(args, "value_column");
|
|
900
1286
|
const char *agg = jstr(args, "agg");
|
|
1287
|
+
cJSON *categories = args ? cJSON_GetObjectItemCaseSensitive(args, "categories") : NULL;
|
|
901
1288
|
if (!name_col || !val_col) { *error = strdup("pivot: missing args"); return -1; }
|
|
902
1289
|
if (!agg) agg = "first";
|
|
903
1290
|
const char *sql_agg = "FIRST";
|
|
@@ -910,8 +1297,55 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
910
1297
|
sb_init(&qn); sb_init(&qv);
|
|
911
1298
|
sql_quote_ident(&qn, name_col);
|
|
912
1299
|
sql_quote_ident(&qv, val_col);
|
|
913
|
-
|
|
914
|
-
|
|
1300
|
+
if (qn.failed || qv.failed) {
|
|
1301
|
+
sb_free(&qn); sb_free(&qv);
|
|
1302
|
+
*error = strdup("sql: out of memory");
|
|
1303
|
+
return -1;
|
|
1304
|
+
}
|
|
1305
|
+
if (categories) {
|
|
1306
|
+
if (!cJSON_IsArray(categories) || cJSON_GetArraySize(categories) <= 0) {
|
|
1307
|
+
sb_free(&qn); sb_free(&qv);
|
|
1308
|
+
*error = strdup("pivot: SQL categories must be a non-empty array");
|
|
1309
|
+
return -1;
|
|
1310
|
+
}
|
|
1311
|
+
strbuf cats;
|
|
1312
|
+
sb_init(&cats);
|
|
1313
|
+
int n = cJSON_GetArraySize(categories);
|
|
1314
|
+
for (int i = 0; i < n; i++) {
|
|
1315
|
+
cJSON *cat = cJSON_GetArrayItem(categories, i);
|
|
1316
|
+
if (!cJSON_IsString(cat)) {
|
|
1317
|
+
sb_free(&cats); sb_free(&qn); sb_free(&qv);
|
|
1318
|
+
*error = strdup("pivot: SQL categories must be strings");
|
|
1319
|
+
return -1;
|
|
1320
|
+
}
|
|
1321
|
+
if (i > 0) sb_append(&cats, ", ");
|
|
1322
|
+
sql_quote_str(&cats, cat->valuestring);
|
|
1323
|
+
if (cats.failed) {
|
|
1324
|
+
sb_free(&cats); sb_free(&qn); sb_free(&qv);
|
|
1325
|
+
*error = strdup("sql: out of memory");
|
|
1326
|
+
return -1;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
sb_appendf(sb, "%s AS (PIVOT %s ON %s IN (%s) USING %s(%s))",
|
|
1330
|
+
cte_name, prev, qn.data, cats.data, sql_agg, qv.data);
|
|
1331
|
+
sb_free(&cats);
|
|
1332
|
+
} else {
|
|
1333
|
+
if (strcmp(prev, "input_data") != 0) {
|
|
1334
|
+
sb_free(&qn); sb_free(&qv);
|
|
1335
|
+
*error = strdup("pivot: dynamic SQL pivot after upstream transforms requires categories=...");
|
|
1336
|
+
return -1;
|
|
1337
|
+
}
|
|
1338
|
+
sb_appendf(sb,
|
|
1339
|
+
"%s AS (PIVOT %s ON %s IN (SELECT %s FROM "
|
|
1340
|
+
"(SELECT %s, row_number() OVER () AS \"__tf_pivot_category_ordinal\" FROM %s) __tf_pivot_cats "
|
|
1341
|
+
"GROUP BY %s ORDER BY min(\"__tf_pivot_category_ordinal\")) USING %s(%s))",
|
|
1342
|
+
cte_name, prev, qn.data, qn.data, qn.data, prev, qn.data, sql_agg, qv.data);
|
|
1343
|
+
}
|
|
1344
|
+
if (qn.failed || qv.failed || sb->failed) {
|
|
1345
|
+
sb_free(&qn); sb_free(&qv);
|
|
1346
|
+
*error = strdup("sql: out of memory");
|
|
1347
|
+
return -1;
|
|
1348
|
+
}
|
|
915
1349
|
sb_free(&qn); sb_free(&qv);
|
|
916
1350
|
return 0;
|
|
917
1351
|
}
|
|
@@ -932,18 +1366,18 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
932
1366
|
cJSON *b = cJSON_GetArrayItem(boundaries, i);
|
|
933
1367
|
double val = b->valuedouble;
|
|
934
1368
|
if (i == 0) {
|
|
935
|
-
sb_appendf(&expr, " WHEN %s <
|
|
1369
|
+
sb_appendf(&expr, " WHEN %s < " TF_FLOAT64_ROUNDTRIP_FORMAT " THEN '<" TF_FLOAT64_ROUNDTRIP_FORMAT "'", qcol.data, val, val);
|
|
936
1370
|
}
|
|
937
1371
|
if (i > 0) {
|
|
938
1372
|
cJSON *prev_b = cJSON_GetArrayItem(boundaries, i - 1);
|
|
939
|
-
sb_appendf(&expr, " WHEN %s >=
|
|
1373
|
+
sb_appendf(&expr, " WHEN %s >= " TF_FLOAT64_ROUNDTRIP_FORMAT " AND %s < " TF_FLOAT64_ROUNDTRIP_FORMAT " THEN '" TF_FLOAT64_ROUNDTRIP_FORMAT "-" TF_FLOAT64_ROUNDTRIP_FORMAT "'",
|
|
940
1374
|
qcol.data, prev_b->valuedouble, qcol.data, val,
|
|
941
1375
|
prev_b->valuedouble, val);
|
|
942
1376
|
}
|
|
943
1377
|
}
|
|
944
1378
|
if (n > 0) {
|
|
945
1379
|
cJSON *last = cJSON_GetArrayItem(boundaries, n - 1);
|
|
946
|
-
sb_appendf(&expr, " WHEN %s >=
|
|
1380
|
+
sb_appendf(&expr, " WHEN %s >= " TF_FLOAT64_ROUNDTRIP_FORMAT " THEN '" TF_FLOAT64_ROUNDTRIP_FORMAT "+'", qcol.data, last->valuedouble, last->valuedouble);
|
|
947
1381
|
}
|
|
948
1382
|
sb_append(&expr, " END");
|
|
949
1383
|
strbuf bin_col;
|
|
@@ -993,7 +1427,7 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
993
1427
|
strbuf qc;
|
|
994
1428
|
sb_init(&qc);
|
|
995
1429
|
sql_quote_ident(&qc, c->valuestring);
|
|
996
|
-
sb_appendf(&rep, "LAST_VALUE(%s IGNORE NULLS) OVER (ORDER BY
|
|
1430
|
+
sb_appendf(&rep, "LAST_VALUE(%s IGNORE NULLS) OVER (ORDER BY _rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS %s",
|
|
997
1431
|
qc.data, qc.data);
|
|
998
1432
|
sb_free(&qc);
|
|
999
1433
|
}
|
|
@@ -1005,25 +1439,70 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
1005
1439
|
return 0;
|
|
1006
1440
|
}
|
|
1007
1441
|
|
|
1008
|
-
/* ---- window ---- */
|
|
1009
|
-
if (strcmp(op, "window") == 0)
|
|
1442
|
+
/* ---- window / rolling ---- */
|
|
1443
|
+
if (strcmp(op, "window") == 0 || strcmp(op, "rolling-sum") == 0 ||
|
|
1444
|
+
strcmp(op, "rolling-mean") == 0 || strcmp(op, "rolling-min") == 0 ||
|
|
1445
|
+
strcmp(op, "rolling-max") == 0 || strcmp(op, "rolling-any") == 0 ||
|
|
1446
|
+
strcmp(op, "rolling-all") == 0) {
|
|
1010
1447
|
const char *column = jstr(args, "column");
|
|
1011
1448
|
int size = jint(args, "size", 3);
|
|
1012
1449
|
const char *func = jstr(args, "func");
|
|
1013
1450
|
const char *result = jstr(args, "result");
|
|
1451
|
+
int bool_roll = 0;
|
|
1452
|
+
if (strcmp(op, "rolling-sum") == 0) func = "sum";
|
|
1453
|
+
else if (strcmp(op, "rolling-mean") == 0) func = "avg";
|
|
1454
|
+
else if (strcmp(op, "rolling-min") == 0) func = "min";
|
|
1455
|
+
else if (strcmp(op, "rolling-max") == 0) func = "max";
|
|
1456
|
+
else if (strcmp(op, "rolling-any") == 0) { func = "any"; bool_roll = 1; }
|
|
1457
|
+
else if (strcmp(op, "rolling-all") == 0) { func = "all"; bool_roll = 1; }
|
|
1014
1458
|
if (!column || !func) { *error = strdup("window: missing args"); return -1; }
|
|
1015
|
-
|
|
1016
|
-
if (strcmp(func, "sum") == 0) sql_func = "SUM";
|
|
1017
|
-
else if (strcmp(func, "min") == 0) sql_func = "MIN";
|
|
1018
|
-
else if (strcmp(func, "max") == 0) sql_func = "MAX";
|
|
1019
|
-
else if (strcmp(func, "avg") == 0) sql_func = "AVG";
|
|
1459
|
+
|
|
1020
1460
|
strbuf qcol, qres;
|
|
1021
1461
|
sb_init(&qcol); sb_init(&qres);
|
|
1022
1462
|
sql_quote_ident(&qcol, column);
|
|
1023
1463
|
if (result) sql_quote_ident(&qres, result);
|
|
1024
|
-
else sb_appendf(&qres, "\"%s_%
|
|
1025
|
-
|
|
1026
|
-
|
|
1464
|
+
else sb_appendf(&qres, "\"%s_%s%d\"", column, func, size);
|
|
1465
|
+
|
|
1466
|
+
int preceding = size > 0 ? size - 1 : 0;
|
|
1467
|
+
if (bool_roll) {
|
|
1468
|
+
const char *nulls = jstr(args, "nulls");
|
|
1469
|
+
if (!nulls) nulls = "ignore";
|
|
1470
|
+
if (strcmp(nulls, "ignore") != 0 && strcmp(nulls, "false") != 0 &&
|
|
1471
|
+
strcmp(nulls, "true") != 0 && strcmp(nulls, "propagate") != 0) {
|
|
1472
|
+
sb_free(&qcol); sb_free(&qres);
|
|
1473
|
+
*error = strdup("rolling-any/all: nulls must be ignore, false, true, or propagate");
|
|
1474
|
+
return -1;
|
|
1475
|
+
}
|
|
1476
|
+
const char *sql_func = strcmp(func, "any") == 0 ? "BOOL_OR" : "BOOL_AND";
|
|
1477
|
+
if (strcmp(nulls, "propagate") == 0) {
|
|
1478
|
+
sb_appendf(sb, "%s AS (SELECT *, CASE WHEN COUNT(*) OVER (ORDER BY _rn ROWS BETWEEN %d PRECEDING AND CURRENT ROW) > COUNT(%s) OVER (ORDER BY _rn ROWS BETWEEN %d PRECEDING AND CURRENT ROW) THEN NULL ELSE %s(%s) OVER (ORDER BY _rn ROWS BETWEEN %d PRECEDING AND CURRENT ROW) END AS %s FROM %s)",
|
|
1479
|
+
cte_name, preceding, qcol.data, preceding, sql_func, qcol.data, preceding, qres.data, prev);
|
|
1480
|
+
} else if (strcmp(nulls, "false") == 0 || strcmp(nulls, "true") == 0) {
|
|
1481
|
+
const char *fill = strcmp(nulls, "true") == 0 ? "TRUE" : "FALSE";
|
|
1482
|
+
sb_appendf(sb, "%s AS (SELECT *, %s(COALESCE(%s, %s)) OVER (ORDER BY _rn ROWS BETWEEN %d PRECEDING AND CURRENT ROW) AS %s FROM %s)",
|
|
1483
|
+
cte_name, sql_func, qcol.data, fill, preceding, qres.data, prev);
|
|
1484
|
+
} else {
|
|
1485
|
+
sb_appendf(sb, "%s AS (SELECT *, %s(%s) OVER (ORDER BY _rn ROWS BETWEEN %d PRECEDING AND CURRENT ROW) AS %s FROM %s)",
|
|
1486
|
+
cte_name, sql_func, qcol.data, preceding, qres.data, prev);
|
|
1487
|
+
}
|
|
1488
|
+
sb_free(&qcol); sb_free(&qres);
|
|
1489
|
+
return 0;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const char *sql_func = "AVG";
|
|
1493
|
+
if (strcmp(func, "sum") == 0) sql_func = "SUM";
|
|
1494
|
+
else if (strcmp(func, "min") == 0) sql_func = "MIN";
|
|
1495
|
+
else if (strcmp(func, "max") == 0) sql_func = "MAX";
|
|
1496
|
+
else if (strcmp(func, "count") == 0) sql_func = "COUNT";
|
|
1497
|
+
else if (strcmp(func, "avg") == 0 || strcmp(func, "mean") == 0) sql_func = "AVG";
|
|
1498
|
+
sb_appendf(sb, "%s AS (SELECT * EXCLUDE (__tf_window_nn), "
|
|
1499
|
+
"CASE WHEN %s IS NULL THEN NULL ELSE %s(%s) OVER "
|
|
1500
|
+
"(ORDER BY __tf_window_nn RANGE BETWEEN %d PRECEDING AND CURRENT ROW) END AS %s "
|
|
1501
|
+
"FROM (SELECT *, COUNT(%s) OVER "
|
|
1502
|
+
"(ORDER BY _rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) "
|
|
1503
|
+
"AS __tf_window_nn FROM %s) __tf_window_src)",
|
|
1504
|
+
cte_name, qcol.data, sql_func, qcol.data, preceding,
|
|
1505
|
+
qres.data, qcol.data, prev);
|
|
1027
1506
|
sb_free(&qcol); sb_free(&qres);
|
|
1028
1507
|
return 0;
|
|
1029
1508
|
}
|
|
@@ -1044,25 +1523,123 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
1044
1523
|
sql_quote_ident(&qcol, column);
|
|
1045
1524
|
if (result) sql_quote_ident(&qres, result);
|
|
1046
1525
|
else sb_appendf(&qres, "\"%s_%s\"", func, column);
|
|
1047
|
-
sb_appendf(sb, "%s AS (SELECT *, %s(%s) OVER (ORDER BY
|
|
1526
|
+
sb_appendf(sb, "%s AS (SELECT *, %s(%s) OVER (ORDER BY _rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS %s FROM %s)",
|
|
1048
1527
|
cte_name, sql_func, qcol.data, qres.data, prev);
|
|
1049
1528
|
sb_free(&qcol); sb_free(&qres);
|
|
1050
1529
|
return 0;
|
|
1051
1530
|
}
|
|
1052
1531
|
|
|
1053
|
-
/* ----
|
|
1054
|
-
if (strcmp(op, "
|
|
1532
|
+
/* ---- rowid ---- */
|
|
1533
|
+
if (strcmp(op, "rowid") == 0) {
|
|
1534
|
+
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
1535
|
+
const char *result = jstr(args, "result");
|
|
1536
|
+
if (!result) result = "_rowid";
|
|
1537
|
+
|
|
1538
|
+
strbuf qres;
|
|
1539
|
+
sb_init(&qres);
|
|
1540
|
+
sql_quote_ident(&qres, result);
|
|
1541
|
+
|
|
1542
|
+
strbuf partition;
|
|
1543
|
+
sb_init(&partition);
|
|
1544
|
+
if (cJSON_IsArray(cols) && cJSON_GetArraySize(cols) > 0) {
|
|
1545
|
+
int n_cols = cJSON_GetArraySize(cols);
|
|
1546
|
+
for (int i = 0; i < n_cols; i++) {
|
|
1547
|
+
cJSON *item = cJSON_GetArrayItem(cols, i);
|
|
1548
|
+
if (!cJSON_IsString(item)) {
|
|
1549
|
+
sb_free(&qres);
|
|
1550
|
+
sb_free(&partition);
|
|
1551
|
+
*error = strdup("rowid: columns must be strings");
|
|
1552
|
+
return -1;
|
|
1553
|
+
}
|
|
1554
|
+
strbuf qcol;
|
|
1555
|
+
sb_init(&qcol);
|
|
1556
|
+
sql_quote_ident(&qcol, item->valuestring);
|
|
1557
|
+
if (i > 0) sb_append(&partition, ", ");
|
|
1558
|
+
sb_append(&partition, qcol.data);
|
|
1559
|
+
sb_free(&qcol);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (partition.len > 0) {
|
|
1564
|
+
sb_appendf(sb, "%s AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY %s ORDER BY _rn) AS %s FROM %s)",
|
|
1565
|
+
cte_name, partition.data, qres.data, prev);
|
|
1566
|
+
} else {
|
|
1567
|
+
sb_appendf(sb, "%s AS (SELECT *, ROW_NUMBER() OVER (ORDER BY _rn) AS %s FROM %s)",
|
|
1568
|
+
cte_name, qres.data, prev);
|
|
1569
|
+
}
|
|
1570
|
+
sb_free(&qres);
|
|
1571
|
+
sb_free(&partition);
|
|
1572
|
+
return 0;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/* ---- rleid ---- */
|
|
1576
|
+
if (strcmp(op, "rleid") == 0) {
|
|
1577
|
+
cJSON *cols = cJSON_GetObjectItemCaseSensitive(args, "columns");
|
|
1578
|
+
const char *result = jstr(args, "result");
|
|
1579
|
+
if (!result) result = "_rleid";
|
|
1580
|
+
if (!cJSON_IsArray(cols) || cJSON_GetArraySize(cols) <= 0) {
|
|
1581
|
+
*error = strdup("rleid: missing 'columns'");
|
|
1582
|
+
return -1;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
strbuf qres;
|
|
1586
|
+
sb_init(&qres);
|
|
1587
|
+
sql_quote_ident(&qres, result);
|
|
1588
|
+
|
|
1589
|
+
strbuf change;
|
|
1590
|
+
sb_init(&change);
|
|
1591
|
+
int n_cols = cJSON_GetArraySize(cols);
|
|
1592
|
+
for (int i = 0; i < n_cols; i++) {
|
|
1593
|
+
cJSON *item = cJSON_GetArrayItem(cols, i);
|
|
1594
|
+
if (!cJSON_IsString(item)) {
|
|
1595
|
+
sb_free(&qres);
|
|
1596
|
+
sb_free(&change);
|
|
1597
|
+
*error = strdup("rleid: columns must be strings");
|
|
1598
|
+
return -1;
|
|
1599
|
+
}
|
|
1600
|
+
strbuf qcol;
|
|
1601
|
+
sb_init(&qcol);
|
|
1602
|
+
sql_quote_ident(&qcol, item->valuestring);
|
|
1603
|
+
if (i > 0) sb_append(&change, " OR ");
|
|
1604
|
+
sb_appendf(&change, "%s IS DISTINCT FROM LAG(%s) OVER (ORDER BY _rn)",
|
|
1605
|
+
qcol.data, qcol.data);
|
|
1606
|
+
sb_free(&qcol);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
sb_appendf(sb, "%s AS (SELECT * EXCLUDE (__tf_rleid_changed, __tf_rleid_ord), "
|
|
1610
|
+
"SUM(__tf_rleid_changed) OVER (ORDER BY __tf_rleid_ord ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS %s "
|
|
1611
|
+
"FROM (SELECT *, _rn AS __tf_rleid_ord, "
|
|
1612
|
+
"CASE WHEN ROW_NUMBER() OVER (ORDER BY _rn) = 1 OR %s THEN 1 ELSE 0 END AS __tf_rleid_changed "
|
|
1613
|
+
"FROM %s) __tf_rleid_src)",
|
|
1614
|
+
cte_name, qres.data, change.data, prev);
|
|
1615
|
+
sb_free(&qres);
|
|
1616
|
+
sb_free(&change);
|
|
1617
|
+
return 0;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/* ---- lead / lag / shift ---- */
|
|
1621
|
+
if (strcmp(op, "lead") == 0 || strcmp(op, "lag") == 0 || strcmp(op, "shift") == 0) {
|
|
1055
1622
|
const char *column = jstr(args, "column");
|
|
1056
1623
|
int offset = jint(args, "offset", 1);
|
|
1057
1624
|
const char *result = jstr(args, "result");
|
|
1058
|
-
|
|
1625
|
+
const char *type = jstr(args, "type");
|
|
1626
|
+
const char *fn = "LAG";
|
|
1627
|
+
const char *suffix = "lag";
|
|
1628
|
+
if (strcmp(op, "lead") == 0 || (strcmp(op, "shift") == 0 && type && strcmp(type, "lead") == 0)) {
|
|
1629
|
+
fn = "LEAD";
|
|
1630
|
+
suffix = "lead";
|
|
1631
|
+
} else if (strcmp(op, "shift") == 0) {
|
|
1632
|
+
suffix = "shift";
|
|
1633
|
+
}
|
|
1634
|
+
if (!column) { *error = strdup("shift: missing 'column'"); return -1; }
|
|
1635
|
+
if (offset <= 0) offset = 1;
|
|
1059
1636
|
strbuf qcol, qres;
|
|
1060
1637
|
sb_init(&qcol); sb_init(&qres);
|
|
1061
1638
|
sql_quote_ident(&qcol, column);
|
|
1062
1639
|
if (result) sql_quote_ident(&qres, result);
|
|
1063
|
-
else sb_appendf(&qres, "\"%
|
|
1064
|
-
sb_appendf(sb, "%s AS (SELECT *,
|
|
1065
|
-
cte_name, qcol.data, offset, qres.data, prev);
|
|
1640
|
+
else sb_appendf(&qres, "\"%s_%s\"", column, suffix);
|
|
1641
|
+
sb_appendf(sb, "%s AS (SELECT *, %s(%s, %d) OVER (ORDER BY _rn) AS %s FROM %s)",
|
|
1642
|
+
cte_name, fn, qcol.data, offset, qres.data, prev);
|
|
1066
1643
|
sb_free(&qcol); sb_free(&qres);
|
|
1067
1644
|
return 0;
|
|
1068
1645
|
}
|
|
@@ -1083,10 +1660,8 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
1083
1660
|
cJSON *part = cJSON_GetArrayItem(extract, i);
|
|
1084
1661
|
if (!cJSON_IsString(part)) continue;
|
|
1085
1662
|
const char *p = part->valuestring;
|
|
1086
|
-
char result_name[128];
|
|
1087
|
-
snprintf(result_name, sizeof(result_name), "%s_%s", column, p);
|
|
1088
1663
|
sb_appendf(&der, ", EXTRACT(%s FROM %s::TIMESTAMP) AS ", p, qcol.data);
|
|
1089
|
-
|
|
1664
|
+
sql_quote_joined_ident(&der, column, "_", p);
|
|
1090
1665
|
}
|
|
1091
1666
|
}
|
|
1092
1667
|
sb_appendf(sb, "%s AS (SELECT *%s FROM %s)", cte_name, der.data, prev);
|
|
@@ -1105,23 +1680,23 @@ static int emit_cte(strbuf *sb, const char *cte_name, const char *prev,
|
|
|
1105
1680
|
sql_quote_ident(&qcol, column);
|
|
1106
1681
|
strbuf qres;
|
|
1107
1682
|
sb_init(&qres);
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1683
|
+
int in_place = result == NULL;
|
|
1684
|
+
sql_quote_ident(&qres, result ? result : column);
|
|
1685
|
+
if (in_place) {
|
|
1686
|
+
sb_appendf(sb, "%s AS (SELECT * REPLACE (date_trunc('%s', %s::TIMESTAMP) AS %s) FROM %s)",
|
|
1687
|
+
cte_name, trunc, qcol.data, qres.data, prev);
|
|
1688
|
+
} else {
|
|
1689
|
+
sb_appendf(sb, "%s AS (SELECT *, date_trunc('%s', %s::TIMESTAMP) AS %s FROM %s)",
|
|
1690
|
+
cte_name, trunc, qcol.data, qres.data, prev);
|
|
1113
1691
|
}
|
|
1114
|
-
sb_appendf(sb, "%s AS (SELECT *, date_trunc('%s', %s::TIMESTAMP) AS %s FROM %s)",
|
|
1115
|
-
cte_name, trunc, qcol.data, qres.data, prev);
|
|
1116
1692
|
sb_free(&qcol); sb_free(&qres);
|
|
1117
1693
|
return 0;
|
|
1118
1694
|
}
|
|
1119
1695
|
|
|
1120
1696
|
/* ---- stats ---- */
|
|
1121
1697
|
if (strcmp(op, "stats") == 0) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return 0;
|
|
1698
|
+
*error = strdup("stats: native report shape cannot be lowered to SQL");
|
|
1699
|
+
return -1;
|
|
1125
1700
|
}
|
|
1126
1701
|
|
|
1127
1702
|
/* ---- flatten ---- */
|
|
@@ -1174,7 +1749,25 @@ char *tf_ir_to_sql(const tf_ir_plan *plan, char **error) {
|
|
|
1174
1749
|
/* No transforms — just SELECT * */
|
|
1175
1750
|
if (first_transform >= last_transform) {
|
|
1176
1751
|
sb_appendf(&sb, "SELECT * FROM %s", input_source);
|
|
1177
|
-
|
|
1752
|
+
char *sql = sb_detach(&sb);
|
|
1753
|
+
if (!sql && error && !*error) *error = strdup("sql: out of memory");
|
|
1754
|
+
return sql;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/* Check if any operator needs row ordering (_rn column) */
|
|
1758
|
+
int needs_rn = 0;
|
|
1759
|
+
for (size_t i = first_transform; i < last_transform; i++) {
|
|
1760
|
+
const char *op = plan->nodes[i].op;
|
|
1761
|
+
if (strcmp(op, "window") == 0 || strcmp(op, "rolling-sum") == 0 ||
|
|
1762
|
+
strcmp(op, "rolling-mean") == 0 || strcmp(op, "rolling-min") == 0 ||
|
|
1763
|
+
strcmp(op, "rolling-max") == 0 || strcmp(op, "rolling-any") == 0 ||
|
|
1764
|
+
strcmp(op, "rolling-all") == 0 || strcmp(op, "step") == 0 ||
|
|
1765
|
+
strcmp(op, "lead") == 0 || strcmp(op, "lag") == 0 ||
|
|
1766
|
+
strcmp(op, "shift") == 0 || strcmp(op, "rowid") == 0 ||
|
|
1767
|
+
strcmp(op, "rleid") == 0 || strcmp(op, "fill-down") == 0) {
|
|
1768
|
+
needs_rn = 1;
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1178
1771
|
}
|
|
1179
1772
|
|
|
1180
1773
|
/* Build CTE chain — use two alternating name buffers so prev != current */
|
|
@@ -1185,6 +1778,13 @@ char *tf_ir_to_sql(const tf_ir_plan *plan, char **error) {
|
|
|
1185
1778
|
|
|
1186
1779
|
sb_append(&sb, "WITH\n");
|
|
1187
1780
|
|
|
1781
|
+
/* If row ordering is needed, inject a _rn column from the source */
|
|
1782
|
+
if (needs_rn) {
|
|
1783
|
+
sb_appendf(&sb, " _numbered AS (SELECT *, ROW_NUMBER() OVER () AS _rn FROM %s)", input_source);
|
|
1784
|
+
prev = "_numbered";
|
|
1785
|
+
n_ctes++;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1188
1788
|
for (size_t i = first_transform; i < last_transform; i++) {
|
|
1189
1789
|
const tf_ir_node *node = &plan->nodes[i];
|
|
1190
1790
|
char *cte_name = name_bufs[n_ctes % 2];
|
|
@@ -1199,13 +1799,24 @@ char *tf_ir_to_sql(const tf_ir_plan *plan, char **error) {
|
|
|
1199
1799
|
else free(err);
|
|
1200
1800
|
return NULL;
|
|
1201
1801
|
}
|
|
1802
|
+
if (sb.failed) {
|
|
1803
|
+
sb_free(&sb);
|
|
1804
|
+
if (error) *error = strdup("sql: out of memory");
|
|
1805
|
+
return NULL;
|
|
1806
|
+
}
|
|
1202
1807
|
|
|
1203
1808
|
prev = cte_name;
|
|
1204
1809
|
n_ctes++;
|
|
1205
1810
|
}
|
|
1206
1811
|
|
|
1207
|
-
/* Final SELECT from last CTE */
|
|
1208
|
-
|
|
1812
|
+
/* Final SELECT from last CTE — exclude internal _rn column */
|
|
1813
|
+
if (needs_rn) {
|
|
1814
|
+
sb_appendf(&sb, "\nSELECT * EXCLUDE (_rn) FROM %s", prev);
|
|
1815
|
+
} else {
|
|
1816
|
+
sb_appendf(&sb, "\nSELECT * FROM %s", prev);
|
|
1817
|
+
}
|
|
1209
1818
|
|
|
1210
|
-
|
|
1819
|
+
char *sql = sb_detach(&sb);
|
|
1820
|
+
if (!sql && error && !*error) *error = strdup("sql: out of memory");
|
|
1821
|
+
return sql;
|
|
1211
1822
|
}
|