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
package/csrc/recipes.c
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* recipes.c — Built-in named recipes for common ETL pipelines.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#include "recipes.h"
|
|
6
|
+
#include <string.h>
|
|
7
|
+
#include <ctype.h>
|
|
8
|
+
|
|
9
|
+
typedef struct {
|
|
10
|
+
const char *name;
|
|
11
|
+
const char *dsl;
|
|
12
|
+
const char *description;
|
|
13
|
+
} recipe_entry;
|
|
14
|
+
|
|
15
|
+
static const recipe_entry recipes[] = {
|
|
16
|
+
/* ---- Data Exploration ---- */
|
|
17
|
+
{"profile", "csv | stats | csv",
|
|
18
|
+
"Full data profiling (all statistics per column)"},
|
|
19
|
+
{"preview", "csv | head 10 | csv",
|
|
20
|
+
"Quick preview of first 10 rows"},
|
|
21
|
+
{"schema", "csv | head 0 | csv",
|
|
22
|
+
"Show column names only"},
|
|
23
|
+
{"sniff", "csv | schema infer rows=1000 | csv",
|
|
24
|
+
"Bounded-memory schema/type/nullability sniff (sample 1000 rows)"},
|
|
25
|
+
{"summary", "csv | stats count,min,max,avg,stddev | csv",
|
|
26
|
+
"Summary statistics"},
|
|
27
|
+
{"count", "csv | stats count | csv",
|
|
28
|
+
"Row count per column"},
|
|
29
|
+
{"cardinality", "csv | stats count,distinct | csv",
|
|
30
|
+
"Unique value counts per column"},
|
|
31
|
+
{"distro", "csv | stats min,p25,median,p75,max | csv",
|
|
32
|
+
"Five-number summary (quartiles)"},
|
|
33
|
+
|
|
34
|
+
/* ---- Data Quality ---- */
|
|
35
|
+
{"freq", "csv | frequency | csv",
|
|
36
|
+
"Value frequency distribution"},
|
|
37
|
+
{"dedup", "csv | dedup | csv",
|
|
38
|
+
"Remove duplicate rows"},
|
|
39
|
+
{"clean", "csv | trim | csv",
|
|
40
|
+
"Trim whitespace from all columns"},
|
|
41
|
+
|
|
42
|
+
/* ---- Data Sampling ---- */
|
|
43
|
+
{"sample", "csv | sample 100 | csv",
|
|
44
|
+
"Random sample of 100 rows"},
|
|
45
|
+
{"head", "csv | head 20 | csv",
|
|
46
|
+
"First 20 rows"},
|
|
47
|
+
{"tail", "csv | tail 20 | csv",
|
|
48
|
+
"Last 20 rows"},
|
|
49
|
+
|
|
50
|
+
/* ---- Format Conversion ---- */
|
|
51
|
+
{"csv2json", "csv | jsonl",
|
|
52
|
+
"Convert CSV to JSONL"},
|
|
53
|
+
{"json2csv", "jsonl | csv",
|
|
54
|
+
"Convert JSONL to CSV"},
|
|
55
|
+
{"tsv2csv", "csv delimiter=\"\t\" | csv",
|
|
56
|
+
"Convert TSV to CSV"},
|
|
57
|
+
{"csv2tsv", "csv | csv delimiter=\"\t\"",
|
|
58
|
+
"Convert CSV to TSV"},
|
|
59
|
+
|
|
60
|
+
/* ---- Display ---- */
|
|
61
|
+
{"look", "csv | table",
|
|
62
|
+
"Pretty-print as Markdown table"},
|
|
63
|
+
|
|
64
|
+
/* ---- Analysis ---- */
|
|
65
|
+
{"histogram", "csv | stats hist | csv",
|
|
66
|
+
"Distribution histograms"},
|
|
67
|
+
{"hash", "csv | hash | csv",
|
|
68
|
+
"Add row hash column for change detection"},
|
|
69
|
+
{"samples", "csv | stats sample | csv",
|
|
70
|
+
"Show sample values per column"},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
#define RECIPE_COUNT (sizeof(recipes) / sizeof(recipes[0]))
|
|
74
|
+
|
|
75
|
+
size_t tf_recipe_count(void) {
|
|
76
|
+
return RECIPE_COUNT;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const char *tf_recipe_name(size_t index) {
|
|
80
|
+
return index < RECIPE_COUNT ? recipes[index].name : NULL;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const char *tf_recipe_dsl(size_t index) {
|
|
84
|
+
return index < RECIPE_COUNT ? recipes[index].dsl : NULL;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const char *tf_recipe_description(size_t index) {
|
|
88
|
+
return index < RECIPE_COUNT ? recipes[index].description : NULL;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const char *tf_recipe_find_dsl(const char *name) {
|
|
92
|
+
if (!name) return NULL;
|
|
93
|
+
for (size_t i = 0; i < RECIPE_COUNT; i++) {
|
|
94
|
+
/* Case-insensitive comparison */
|
|
95
|
+
const char *a = name;
|
|
96
|
+
const char *b = recipes[i].name;
|
|
97
|
+
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) {
|
|
98
|
+
a++;
|
|
99
|
+
b++;
|
|
100
|
+
}
|
|
101
|
+
if (*a == '\0' && *b == '\0') return recipes[i].dsl;
|
|
102
|
+
}
|
|
103
|
+
return NULL;
|
|
104
|
+
}
|
package/csrc/recipes.h
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* recipes.h — Built-in named recipes.
|
|
3
|
+
*
|
|
4
|
+
* 20 pre-built DSL pipelines for common ETL operations:
|
|
5
|
+
* tranfi profile → full data profiling
|
|
6
|
+
* tranfi preview → first 10 rows
|
|
7
|
+
* tranfi csv2json → format conversion
|
|
8
|
+
* ...
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#ifndef TF_RECIPES_H
|
|
12
|
+
#define TF_RECIPES_H
|
|
13
|
+
|
|
14
|
+
#include <stddef.h>
|
|
15
|
+
|
|
16
|
+
/* Number of built-in recipes. */
|
|
17
|
+
size_t tf_recipe_count(void);
|
|
18
|
+
|
|
19
|
+
/* Accessors by index (0-based). Return NULL if index out of range. */
|
|
20
|
+
const char *tf_recipe_name(size_t index);
|
|
21
|
+
const char *tf_recipe_dsl(size_t index);
|
|
22
|
+
const char *tf_recipe_description(size_t index);
|
|
23
|
+
|
|
24
|
+
/* Lookup by name (case-insensitive). Returns DSL string or NULL. */
|
|
25
|
+
const char *tf_recipe_find_dsl(const char *name);
|
|
26
|
+
|
|
27
|
+
#endif /* TF_RECIPES_H */
|
package/csrc/report.c
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* report.c — Rich ANSI terminal report for stats output.
|
|
3
|
+
*
|
|
4
|
+
* Parses the CSV text produced by the stats channel and renders a
|
|
5
|
+
* compact per-column summary with Unicode sparkline histograms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "report.h"
|
|
9
|
+
#include "internal.h"
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
#include <stdio.h>
|
|
13
|
+
#include <stdarg.h>
|
|
14
|
+
#include <math.h>
|
|
15
|
+
|
|
16
|
+
/* ---- ANSI escape codes ---- */
|
|
17
|
+
|
|
18
|
+
#define ANSI_BOLD "\033[1m"
|
|
19
|
+
#define ANSI_DIM "\033[2m"
|
|
20
|
+
#define ANSI_CYAN "\033[36m"
|
|
21
|
+
#define ANSI_GREEN "\033[32m"
|
|
22
|
+
#define ANSI_YELLOW "\033[33m"
|
|
23
|
+
#define ANSI_RESET "\033[0m"
|
|
24
|
+
|
|
25
|
+
/* ---- Dynamic string buffer ---- */
|
|
26
|
+
|
|
27
|
+
typedef struct {
|
|
28
|
+
char *data;
|
|
29
|
+
size_t len;
|
|
30
|
+
size_t cap;
|
|
31
|
+
int failed;
|
|
32
|
+
} strbuf;
|
|
33
|
+
|
|
34
|
+
static void sb_init(strbuf *sb) {
|
|
35
|
+
sb->cap = 4096;
|
|
36
|
+
sb->len = 0;
|
|
37
|
+
sb->failed = 0;
|
|
38
|
+
sb->data = tf_mallocarray_checked(sb->cap, sizeof(char));
|
|
39
|
+
if (sb->data) {
|
|
40
|
+
sb->data[0] = '\0';
|
|
41
|
+
} else {
|
|
42
|
+
sb->cap = 0;
|
|
43
|
+
sb->failed = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static int sb_ensure(strbuf *sb, size_t extra) {
|
|
48
|
+
if (!sb || sb->failed) return -1;
|
|
49
|
+
size_t need;
|
|
50
|
+
if (tf_size_add(sb->len, extra, &need) != TF_OK ||
|
|
51
|
+
tf_size_add(need, 1, &need) != TF_OK) {
|
|
52
|
+
sb->failed = 1;
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
if (need <= sb->cap) return 0;
|
|
56
|
+
size_t new_cap;
|
|
57
|
+
if (tf_size_grow_pow2(sb->cap, need, 4096, &new_cap) != TF_OK) {
|
|
58
|
+
sb->failed = 1;
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
char *tmp = tf_reallocarray_checked(sb->data, new_cap, sizeof(char));
|
|
62
|
+
if (!tmp) {
|
|
63
|
+
sb->failed = 1;
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
sb->data = tmp;
|
|
67
|
+
sb->cap = new_cap;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static void sb_append(strbuf *sb, const char *s) {
|
|
72
|
+
if (!sb || sb->failed || !s) return;
|
|
73
|
+
size_t n = strlen(s);
|
|
74
|
+
if (sb_ensure(sb, n) != 0) return;
|
|
75
|
+
memcpy(sb->data + sb->len, s, n);
|
|
76
|
+
sb->len += n;
|
|
77
|
+
sb->data[sb->len] = '\0';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static void sb_printf(strbuf *sb, const char *fmt, ...) {
|
|
81
|
+
if (!sb || sb->failed || !fmt) return;
|
|
82
|
+
va_list ap;
|
|
83
|
+
va_start(ap, fmt);
|
|
84
|
+
int needed = vsnprintf(NULL, 0, fmt, ap);
|
|
85
|
+
va_end(ap);
|
|
86
|
+
if (needed < 0) { sb->failed = 1; return; }
|
|
87
|
+
if (sb_ensure(sb, (size_t)needed) != 0) return;
|
|
88
|
+
va_start(ap, fmt);
|
|
89
|
+
int written = vsnprintf(sb->data + sb->len, (size_t)needed + 1, fmt, ap);
|
|
90
|
+
va_end(ap);
|
|
91
|
+
if (written < 0 || written > needed) { sb->failed = 1; return; }
|
|
92
|
+
sb->len += (size_t)written;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ---- Sparkline ---- */
|
|
96
|
+
|
|
97
|
+
static const char *spark_chars[] = {
|
|
98
|
+
"\xe2\x96\x81", /* ▁ U+2581 */
|
|
99
|
+
"\xe2\x96\x82", /* ▂ U+2582 */
|
|
100
|
+
"\xe2\x96\x83", /* ▃ U+2583 */
|
|
101
|
+
"\xe2\x96\x84", /* ▄ U+2584 */
|
|
102
|
+
"\xe2\x96\x85", /* ▅ U+2585 */
|
|
103
|
+
"\xe2\x96\x86", /* ▆ U+2586 */
|
|
104
|
+
"\xe2\x96\x87", /* ▇ U+2587 */
|
|
105
|
+
"\xe2\x96\x88", /* █ U+2588 */
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/* ---- CSV parsing helpers ---- */
|
|
109
|
+
|
|
110
|
+
#define MAX_COLS 32
|
|
111
|
+
#define MAX_ROWS 256
|
|
112
|
+
#define MAX_FIELD 1024
|
|
113
|
+
|
|
114
|
+
typedef struct {
|
|
115
|
+
char *headers[MAX_COLS];
|
|
116
|
+
char *cells[MAX_ROWS][MAX_COLS];
|
|
117
|
+
size_t n_cols;
|
|
118
|
+
size_t n_rows;
|
|
119
|
+
} csv_table;
|
|
120
|
+
|
|
121
|
+
static void csv_free(csv_table *t);
|
|
122
|
+
|
|
123
|
+
static char *csv_field_copy(const char *start, size_t len) {
|
|
124
|
+
if (len > MAX_FIELD - 1) len = MAX_FIELD - 1;
|
|
125
|
+
char *field = tf_mallocarray_checked(len + 1, sizeof(char));
|
|
126
|
+
if (!field) return NULL;
|
|
127
|
+
if (len > 0) memcpy(field, start, len);
|
|
128
|
+
field[len] = '\0';
|
|
129
|
+
return field;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Find column index by name, -1 if not found */
|
|
133
|
+
static int csv_col(const csv_table *t, const char *name) {
|
|
134
|
+
for (size_t i = 0; i < t->n_cols; i++) {
|
|
135
|
+
if (t->headers[i] && strcmp(t->headers[i], name) == 0)
|
|
136
|
+
return (int)i;
|
|
137
|
+
}
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Parse the bounded stats CSV format, including quoted comma-containing fields. */
|
|
142
|
+
static csv_table *csv_parse(const char *csv, size_t len) {
|
|
143
|
+
csv_table *t = tf_callocarray_checked(1, sizeof(csv_table));
|
|
144
|
+
if (!t) return NULL;
|
|
145
|
+
|
|
146
|
+
const char *p = csv;
|
|
147
|
+
const char *end = csv + len;
|
|
148
|
+
|
|
149
|
+
/* Parse header line */
|
|
150
|
+
while (p < end && *p != '\n' && *p != '\r') {
|
|
151
|
+
const char *start = p;
|
|
152
|
+
while (p < end && *p != ',' && *p != '\n' && *p != '\r') p++;
|
|
153
|
+
size_t flen = (size_t)(p - start);
|
|
154
|
+
if (t->n_cols < MAX_COLS) {
|
|
155
|
+
char *field = csv_field_copy(start, flen);
|
|
156
|
+
if (!field) {
|
|
157
|
+
csv_free(t);
|
|
158
|
+
return NULL;
|
|
159
|
+
}
|
|
160
|
+
t->headers[t->n_cols] = field;
|
|
161
|
+
t->n_cols++;
|
|
162
|
+
}
|
|
163
|
+
if (p < end && *p == ',') p++;
|
|
164
|
+
}
|
|
165
|
+
/* Skip newline */
|
|
166
|
+
if (p < end && *p == '\r') p++;
|
|
167
|
+
if (p < end && *p == '\n') p++;
|
|
168
|
+
|
|
169
|
+
/* Parse data rows */
|
|
170
|
+
while (p < end && t->n_rows < MAX_ROWS) {
|
|
171
|
+
if (*p == '\n' || *p == '\r') { p++; continue; }
|
|
172
|
+
size_t ci = 0;
|
|
173
|
+
while (p < end && *p != '\n' && *p != '\r') {
|
|
174
|
+
const char *start = p;
|
|
175
|
+
/* Handle quoted fields (hist column has colons and commas inside quotes) */
|
|
176
|
+
if (*p == '"') {
|
|
177
|
+
p++; /* skip opening quote */
|
|
178
|
+
start = p;
|
|
179
|
+
while (p < end && *p != '"') p++;
|
|
180
|
+
size_t flen = (size_t)(p - start);
|
|
181
|
+
if (ci < MAX_COLS) {
|
|
182
|
+
char *field = csv_field_copy(start, flen);
|
|
183
|
+
if (!field) {
|
|
184
|
+
csv_free(t);
|
|
185
|
+
return NULL;
|
|
186
|
+
}
|
|
187
|
+
t->cells[t->n_rows][ci] = field;
|
|
188
|
+
}
|
|
189
|
+
if (p < end && *p == '"') p++; /* skip closing quote */
|
|
190
|
+
if (p < end && *p == ',') p++;
|
|
191
|
+
ci++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
while (p < end && *p != ',' && *p != '\n' && *p != '\r') p++;
|
|
195
|
+
size_t flen = (size_t)(p - start);
|
|
196
|
+
if (ci < MAX_COLS) {
|
|
197
|
+
char *field = csv_field_copy(start, flen);
|
|
198
|
+
if (!field) {
|
|
199
|
+
csv_free(t);
|
|
200
|
+
return NULL;
|
|
201
|
+
}
|
|
202
|
+
t->cells[t->n_rows][ci] = field;
|
|
203
|
+
}
|
|
204
|
+
if (p < end && *p == ',') p++;
|
|
205
|
+
ci++;
|
|
206
|
+
}
|
|
207
|
+
t->n_rows++;
|
|
208
|
+
if (p < end && *p == '\r') p++;
|
|
209
|
+
if (p < end && *p == '\n') p++;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return t;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static void csv_free(csv_table *t) {
|
|
216
|
+
if (!t) return;
|
|
217
|
+
for (size_t i = 0; i < t->n_cols; i++) free(t->headers[i]);
|
|
218
|
+
for (size_t r = 0; r < MAX_ROWS; r++)
|
|
219
|
+
for (size_t c = 0; c < t->n_cols; c++)
|
|
220
|
+
free(t->cells[r][c]);
|
|
221
|
+
free(t);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static const char *csv_get(const csv_table *t, size_t row, int col) {
|
|
225
|
+
if (col < 0 || (size_t)col >= t->n_cols || row >= t->n_rows) return NULL;
|
|
226
|
+
return t->cells[row][col];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static double csv_getf(const csv_table *t, size_t row, int col) {
|
|
230
|
+
const char *s = csv_get(t, row, col);
|
|
231
|
+
if (!s || *s == '\0') return NAN;
|
|
232
|
+
return atof(s);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static long long csv_getll(const csv_table *t, size_t row, int col) {
|
|
236
|
+
const char *s = csv_get(t, row, col);
|
|
237
|
+
if (!s || *s == '\0') return 0;
|
|
238
|
+
return atoll(s);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ---- Histogram parsing ---- */
|
|
242
|
+
|
|
243
|
+
#define HIST_BINS 32
|
|
244
|
+
|
|
245
|
+
typedef struct {
|
|
246
|
+
double lo, hi;
|
|
247
|
+
size_t counts[HIST_BINS];
|
|
248
|
+
size_t total;
|
|
249
|
+
} parsed_hist;
|
|
250
|
+
|
|
251
|
+
static int parse_hist(const char *s, parsed_hist *h) {
|
|
252
|
+
if (!s || !*s) return 0;
|
|
253
|
+
memset(h, 0, sizeof(*h));
|
|
254
|
+
|
|
255
|
+
/* Format: "lo:hi:c1,c2,...,c32" */
|
|
256
|
+
char *endp;
|
|
257
|
+
h->lo = strtod(s, &endp);
|
|
258
|
+
if (*endp != ':') return 0;
|
|
259
|
+
endp++;
|
|
260
|
+
h->hi = strtod(endp, &endp);
|
|
261
|
+
if (*endp != ':') return 0;
|
|
262
|
+
endp++;
|
|
263
|
+
|
|
264
|
+
for (int i = 0; i < HIST_BINS; i++) {
|
|
265
|
+
h->counts[i] = (size_t)strtoul(endp, &endp, 10);
|
|
266
|
+
h->total += h->counts[i];
|
|
267
|
+
if (*endp == ',') endp++;
|
|
268
|
+
}
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ---- Number formatting ---- */
|
|
273
|
+
|
|
274
|
+
static void fmt_num(char *buf, size_t bufsz, double v) {
|
|
275
|
+
if (isnan(v)) { snprintf(buf, bufsz, "-"); return; }
|
|
276
|
+
double av = fabs(v);
|
|
277
|
+
if (av == 0.0)
|
|
278
|
+
snprintf(buf, bufsz, "0");
|
|
279
|
+
else if (av >= 1e6)
|
|
280
|
+
snprintf(buf, bufsz, "%.3g", v);
|
|
281
|
+
else if (av >= 100.0)
|
|
282
|
+
snprintf(buf, bufsz, "%.1f", v);
|
|
283
|
+
else if (av >= 1.0)
|
|
284
|
+
snprintf(buf, bufsz, "%.2f", v);
|
|
285
|
+
else
|
|
286
|
+
snprintf(buf, bufsz, "%.4f", v);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static void fmt_int(char *buf, size_t bufsz, long long v) {
|
|
290
|
+
/* Add thousand separators */
|
|
291
|
+
char raw[32];
|
|
292
|
+
snprintf(raw, sizeof(raw), "%lld", v < 0 ? -v : v);
|
|
293
|
+
size_t rlen = strlen(raw);
|
|
294
|
+
size_t pos = 0;
|
|
295
|
+
if (v < 0 && pos < bufsz - 1) buf[pos++] = '-';
|
|
296
|
+
for (size_t i = 0; i < rlen && pos < bufsz - 1; i++) {
|
|
297
|
+
if (i > 0 && (rlen - i) % 3 == 0 && pos < bufsz - 1) buf[pos++] = ',';
|
|
298
|
+
buf[pos++] = raw[i];
|
|
299
|
+
}
|
|
300
|
+
buf[pos] = '\0';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ---- Main report formatter ---- */
|
|
304
|
+
|
|
305
|
+
char *tf_report_format(const char *stats_csv, size_t stats_len, int use_color) {
|
|
306
|
+
if (!stats_csv || stats_len == 0) return NULL;
|
|
307
|
+
|
|
308
|
+
csv_table *t = csv_parse(stats_csv, stats_len);
|
|
309
|
+
if (!t || t->n_rows == 0) { csv_free(t); return NULL; }
|
|
310
|
+
|
|
311
|
+
/* Find column indices */
|
|
312
|
+
int ci_name = csv_col(t, "column");
|
|
313
|
+
int ci_count = csv_col(t, "count");
|
|
314
|
+
int ci_avg = csv_col(t, "avg");
|
|
315
|
+
int ci_min = csv_col(t, "min");
|
|
316
|
+
int ci_max = csv_col(t, "max");
|
|
317
|
+
int ci_stddev = csv_col(t, "stddev");
|
|
318
|
+
int ci_median = csv_col(t, "median");
|
|
319
|
+
int ci_p25 = csv_col(t, "p25");
|
|
320
|
+
int ci_p75 = csv_col(t, "p75");
|
|
321
|
+
int ci_distinct = csv_col(t, "distinct");
|
|
322
|
+
int ci_hist = csv_col(t, "hist");
|
|
323
|
+
int ci_sample = csv_col(t, "sample");
|
|
324
|
+
|
|
325
|
+
if (ci_name < 0) { csv_free(t); return NULL; }
|
|
326
|
+
|
|
327
|
+
const char *bold = use_color ? ANSI_BOLD : "";
|
|
328
|
+
const char *dim = use_color ? ANSI_DIM : "";
|
|
329
|
+
const char *cyan = use_color ? ANSI_CYAN : "";
|
|
330
|
+
const char *green = use_color ? ANSI_GREEN : "";
|
|
331
|
+
const char *reset = use_color ? ANSI_RESET : "";
|
|
332
|
+
|
|
333
|
+
strbuf sb;
|
|
334
|
+
sb_init(&sb);
|
|
335
|
+
if (sb.failed) { csv_free(t); return NULL; }
|
|
336
|
+
|
|
337
|
+
/* Header */
|
|
338
|
+
long long total_count = 0;
|
|
339
|
+
if (ci_count >= 0 && t->n_rows > 0)
|
|
340
|
+
total_count = csv_getll(t, 0, ci_count);
|
|
341
|
+
|
|
342
|
+
char count_str[32];
|
|
343
|
+
fmt_int(count_str, sizeof(count_str), total_count);
|
|
344
|
+
|
|
345
|
+
sb_printf(&sb, "\n");
|
|
346
|
+
if (use_color) {
|
|
347
|
+
sb_printf(&sb, " %s%zu columns%s %s%s rows%s\n\n",
|
|
348
|
+
bold, t->n_rows, reset,
|
|
349
|
+
dim, count_str, reset);
|
|
350
|
+
} else {
|
|
351
|
+
sb_printf(&sb, " %zu columns %s rows\n\n", t->n_rows, count_str);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Per-column stats */
|
|
355
|
+
for (size_t r = 0; r < t->n_rows; r++) {
|
|
356
|
+
const char *name = csv_get(t, r, ci_name);
|
|
357
|
+
if (!name) continue;
|
|
358
|
+
|
|
359
|
+
/* Column name */
|
|
360
|
+
sb_printf(&sb, " %s%-20s%s", bold, name, reset);
|
|
361
|
+
|
|
362
|
+
/* Detect numeric vs string: if min/max are present and not empty */
|
|
363
|
+
const char *min_s = csv_get(t, r, ci_min);
|
|
364
|
+
int is_numeric = (min_s && *min_s != '\0' && strcmp(min_s, "") != 0);
|
|
365
|
+
|
|
366
|
+
if (is_numeric) {
|
|
367
|
+
/* Numeric column: show key stats inline */
|
|
368
|
+
char buf[32];
|
|
369
|
+
|
|
370
|
+
if (ci_min >= 0) {
|
|
371
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_min));
|
|
372
|
+
sb_printf(&sb, " %smin%s %-10s", dim, reset, buf);
|
|
373
|
+
}
|
|
374
|
+
if (ci_max >= 0) {
|
|
375
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_max));
|
|
376
|
+
sb_printf(&sb, " %smax%s %-10s", dim, reset, buf);
|
|
377
|
+
}
|
|
378
|
+
if (ci_avg >= 0) {
|
|
379
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_avg));
|
|
380
|
+
sb_printf(&sb, " %savg%s %-10s", dim, reset, buf);
|
|
381
|
+
}
|
|
382
|
+
if (ci_stddev >= 0) {
|
|
383
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_stddev));
|
|
384
|
+
sb_printf(&sb, " %sstd%s %-10s", dim, reset, buf);
|
|
385
|
+
}
|
|
386
|
+
sb_append(&sb, "\n");
|
|
387
|
+
|
|
388
|
+
/* Quantiles line */
|
|
389
|
+
if (ci_median >= 0 || ci_p25 >= 0 || ci_p75 >= 0) {
|
|
390
|
+
sb_printf(&sb, " %-20s", "");
|
|
391
|
+
if (ci_p25 >= 0) {
|
|
392
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_p25));
|
|
393
|
+
sb_printf(&sb, " %sp25%s %-10s", dim, reset, buf);
|
|
394
|
+
}
|
|
395
|
+
if (ci_median >= 0) {
|
|
396
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_median));
|
|
397
|
+
sb_printf(&sb, " %smed%s %-10s", dim, reset, buf);
|
|
398
|
+
}
|
|
399
|
+
if (ci_p75 >= 0) {
|
|
400
|
+
fmt_num(buf, sizeof(buf), csv_getf(t, r, ci_p75));
|
|
401
|
+
sb_printf(&sb, " %sp75%s %-10s", dim, reset, buf);
|
|
402
|
+
}
|
|
403
|
+
sb_append(&sb, "\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Histogram sparkline */
|
|
407
|
+
if (ci_hist >= 0) {
|
|
408
|
+
const char *hist_str = csv_get(t, r, ci_hist);
|
|
409
|
+
parsed_hist h;
|
|
410
|
+
if (parse_hist(hist_str, &h) && h.total > 0) {
|
|
411
|
+
/* Find max count for scaling */
|
|
412
|
+
size_t max_c = 0;
|
|
413
|
+
for (int i = 0; i < HIST_BINS; i++)
|
|
414
|
+
if (h.counts[i] > max_c) max_c = h.counts[i];
|
|
415
|
+
|
|
416
|
+
sb_printf(&sb, " %-20s %s", "", cyan);
|
|
417
|
+
for (int i = 0; i < HIST_BINS; i++) {
|
|
418
|
+
if (max_c == 0) {
|
|
419
|
+
sb_append(&sb, spark_chars[0]);
|
|
420
|
+
} else {
|
|
421
|
+
int level = (int)((double)h.counts[i] / (double)max_c * 7.0);
|
|
422
|
+
if (level < 0) level = 0;
|
|
423
|
+
if (level > 7) level = 7;
|
|
424
|
+
/* Use at least level 1 for non-zero bins */
|
|
425
|
+
if (h.counts[i] > 0 && level == 0) level = 1;
|
|
426
|
+
sb_append(&sb, spark_chars[level]);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
char lo_buf[16], hi_buf[16];
|
|
431
|
+
fmt_num(lo_buf, sizeof(lo_buf), h.lo);
|
|
432
|
+
fmt_num(hi_buf, sizeof(hi_buf), h.hi);
|
|
433
|
+
sb_printf(&sb, "%s %s%s — %s%s\n", reset, dim, lo_buf, hi_buf, reset);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* Sample dots */
|
|
438
|
+
if (ci_sample >= 0) {
|
|
439
|
+
const char *samp = csv_get(t, r, ci_sample);
|
|
440
|
+
if (samp && *samp) {
|
|
441
|
+
double vmin = csv_getf(t, r, ci_min);
|
|
442
|
+
double vmax = csv_getf(t, r, ci_max);
|
|
443
|
+
double range = vmax - vmin;
|
|
444
|
+
if (range > 0) {
|
|
445
|
+
sb_printf(&sb, " %-20s %s", "", green);
|
|
446
|
+
/* Parse sample values and place dots in a 32-char strip */
|
|
447
|
+
char strip[33];
|
|
448
|
+
memset(strip, ' ', 32);
|
|
449
|
+
strip[32] = '\0';
|
|
450
|
+
|
|
451
|
+
const char *p = samp;
|
|
452
|
+
while (*p) {
|
|
453
|
+
char *ep;
|
|
454
|
+
double v = strtod(p, &ep);
|
|
455
|
+
if (ep == p) break;
|
|
456
|
+
int pos = (int)((v - vmin) / range * 31.0);
|
|
457
|
+
if (pos < 0) pos = 0;
|
|
458
|
+
if (pos > 31) pos = 31;
|
|
459
|
+
strip[pos] = '.';
|
|
460
|
+
p = ep;
|
|
461
|
+
if (*p == ',') p++;
|
|
462
|
+
}
|
|
463
|
+
/* Print using unicode dots for better visibility */
|
|
464
|
+
for (int i = 0; i < 32; i++) {
|
|
465
|
+
if (strip[i] == '.')
|
|
466
|
+
sb_append(&sb, "\xc2\xb7"); /* · U+00B7 */
|
|
467
|
+
else
|
|
468
|
+
sb_append(&sb, " ");
|
|
469
|
+
}
|
|
470
|
+
sb_printf(&sb, "%s\n", reset);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
/* String/non-numeric column */
|
|
476
|
+
if (ci_count >= 0) {
|
|
477
|
+
char cnt_buf[32];
|
|
478
|
+
fmt_int(cnt_buf, sizeof(cnt_buf), csv_getll(t, r, ci_count));
|
|
479
|
+
sb_printf(&sb, " %sn%s %-10s", dim, reset, cnt_buf);
|
|
480
|
+
}
|
|
481
|
+
if (ci_distinct >= 0) {
|
|
482
|
+
long long dist = csv_getll(t, r, ci_distinct);
|
|
483
|
+
long long cnt = ci_count >= 0 ? csv_getll(t, r, ci_count) : 0;
|
|
484
|
+
char dist_buf[32];
|
|
485
|
+
fmt_int(dist_buf, sizeof(dist_buf), dist);
|
|
486
|
+
if (cnt > 0) {
|
|
487
|
+
double pct = 100.0 * (double)dist / (double)cnt;
|
|
488
|
+
sb_printf(&sb, " %suniq%s %s (%.1f%%)", dim, reset, dist_buf, pct);
|
|
489
|
+
} else {
|
|
490
|
+
sb_printf(&sb, " %suniq%s %s", dim, reset, dist_buf);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
sb_append(&sb, "\n");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
sb_append(&sb, "\n");
|
|
498
|
+
|
|
499
|
+
csv_free(t);
|
|
500
|
+
|
|
501
|
+
if (sb.failed) {
|
|
502
|
+
free(sb.data);
|
|
503
|
+
return NULL;
|
|
504
|
+
}
|
|
505
|
+
return sb.data;
|
|
506
|
+
}
|
package/csrc/report.h
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* report.h — Rich ANSI terminal report for stats output.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#ifndef TF_REPORT_H
|
|
6
|
+
#define TF_REPORT_H
|
|
7
|
+
|
|
8
|
+
#include <stddef.h>
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Format stats CSV into a rich terminal report with ANSI colors and
|
|
12
|
+
* Unicode sparkline histograms.
|
|
13
|
+
*
|
|
14
|
+
* stats_csv: the CSV output from the stats channel (column,count,avg,...)
|
|
15
|
+
* stats_len: byte length
|
|
16
|
+
* use_color: 1 = ANSI escape codes, 0 = plain text
|
|
17
|
+
*
|
|
18
|
+
* Returns a malloc'd string (caller frees), or NULL on error.
|
|
19
|
+
*/
|
|
20
|
+
char *tf_report_format(const char *stats_csv, size_t stats_len, int use_color);
|
|
21
|
+
|
|
22
|
+
#endif
|