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/wasm/index.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
|
|
2
|
+
var createTransformAPI = require('./transform.js')
|
|
3
|
+
|
|
4
|
+
var BLOCKING_OPS = {
|
|
5
|
+
'sort': true,
|
|
6
|
+
'pivot': true,
|
|
7
|
+
'interpolate': true,
|
|
8
|
+
'normalize': true,
|
|
9
|
+
'acf': true,
|
|
10
|
+
'codec.table.encode': true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var KEY_STATE_OPS = {
|
|
14
|
+
'rowid': true,
|
|
15
|
+
'unique': true,
|
|
16
|
+
'dedup': true,
|
|
17
|
+
'group-agg': true,
|
|
18
|
+
'frequency': true,
|
|
19
|
+
'join': true,
|
|
20
|
+
'semi-join': true,
|
|
21
|
+
'anti-join': true,
|
|
22
|
+
'onehot': true,
|
|
23
|
+
'label-encode': true,
|
|
24
|
+
'intersect': true,
|
|
25
|
+
'setdiff': true,
|
|
26
|
+
'union': true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var SIZE_UNITS = {
|
|
30
|
+
'': 1,
|
|
31
|
+
b: 1,
|
|
32
|
+
k: 1024,
|
|
33
|
+
kb: 1024,
|
|
34
|
+
kib: 1024,
|
|
35
|
+
m: 1024 * 1024,
|
|
36
|
+
mb: 1024 * 1024,
|
|
37
|
+
mib: 1024 * 1024,
|
|
38
|
+
g: 1024 * 1024 * 1024,
|
|
39
|
+
gb: 1024 * 1024 * 1024,
|
|
40
|
+
gib: 1024 * 1024 * 1024
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var UNIQUE_DEFAULT_BLOOM_BYTES = 1024 * 1024
|
|
44
|
+
|
|
45
|
+
function normalizeSqlDialect(options) {
|
|
46
|
+
var dialect = 'duckdb'
|
|
47
|
+
if (typeof options === 'string') {
|
|
48
|
+
dialect = options
|
|
49
|
+
} else if (options && typeof options === 'object') {
|
|
50
|
+
dialect = options.dialect || options.sqlDialect || dialect
|
|
51
|
+
} else if (options !== undefined && options !== null) {
|
|
52
|
+
throw new TypeError('compileToSql options must be an object or dialect string')
|
|
53
|
+
}
|
|
54
|
+
if (typeof dialect !== 'string') throw new TypeError('SQL dialect must be a string')
|
|
55
|
+
var name = dialect.toLowerCase()
|
|
56
|
+
if (name === 'duckdb') return name
|
|
57
|
+
if (name === 'sqlite' || name === 'postgres') {
|
|
58
|
+
throw new Error("SQL dialect '" + name + "' is recognized but not implemented yet; only duckdb lowering is available")
|
|
59
|
+
}
|
|
60
|
+
throw new Error("unknown SQL dialect '" + dialect + "' (expected duckdb, sqlite, or postgres)")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseMemorySize(value) {
|
|
64
|
+
if (value === undefined || value === null) return null
|
|
65
|
+
if (typeof value === 'number') {
|
|
66
|
+
if (!isFinite(value) || value <= 0 || Math.floor(value) !== value) throw new Error('memory must be a positive integer byte count')
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
if (typeof value !== 'string') throw new TypeError('memory must be a positive byte count or size string')
|
|
70
|
+
var text = value.trim()
|
|
71
|
+
var lower = text.toLowerCase()
|
|
72
|
+
if (lower.indexOf('max:') === 0 || lower.indexOf('max=') === 0) text = text.slice(4).trim()
|
|
73
|
+
var match = /^(\d+)\s*([a-zA-Z]*)$/.exec(text)
|
|
74
|
+
if (!match) throw new Error('memory must look like 64MB or max:64MB')
|
|
75
|
+
var amount = Number(match[1])
|
|
76
|
+
var suffix = match[2].toLowerCase()
|
|
77
|
+
if (!SIZE_UNITS.hasOwnProperty(suffix)) throw new Error("unsupported memory size suffix '" + suffix + "'")
|
|
78
|
+
return amount * SIZE_UNITS[suffix]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatBytes(n) {
|
|
82
|
+
if (n < 1024) return String(n) + 'B'
|
|
83
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'KB'
|
|
84
|
+
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + 'MB'
|
|
85
|
+
return (n / (1024 * 1024 * 1024)).toFixed(1) + 'GB'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function memoryClass(step) {
|
|
89
|
+
if (typeof step.memory_class === 'string') return step.memory_class
|
|
90
|
+
if (BLOCKING_OPS[step.op]) return 'blocking'
|
|
91
|
+
if (KEY_STATE_OPS[step.op]) return 'key_state'
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function argsOf(step) {
|
|
96
|
+
return step && step.args && typeof step.args === 'object' ? step.args : {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function positiveInt(value) {
|
|
100
|
+
return typeof value === 'number' && isFinite(value) && value > 0 && Math.floor(value) === value ? value : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function arrayLen(value) {
|
|
104
|
+
return Array.isArray(value) ? value.length : 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stringArrayBytes(value) {
|
|
108
|
+
if (!Array.isArray(value)) return 0
|
|
109
|
+
var enc = new TextEncoder()
|
|
110
|
+
var total = 0
|
|
111
|
+
for (var i = 0; i < value.length; i++) {
|
|
112
|
+
if (typeof value[i] === 'string') total += enc.encode(value[i]).length + 1
|
|
113
|
+
}
|
|
114
|
+
return total
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function categoryCap(step) {
|
|
118
|
+
var args = argsOf(step)
|
|
119
|
+
var maxCategories = positiveInt(args.max_categories)
|
|
120
|
+
var nCategories = arrayLen(args.categories)
|
|
121
|
+
var literalBytes = stringArrayBytes(args.categories)
|
|
122
|
+
if (args.unknown === 'other') {
|
|
123
|
+
nCategories += 1
|
|
124
|
+
literalBytes += 'other'.length + 1
|
|
125
|
+
}
|
|
126
|
+
if (maxCategories !== null) return [maxCategories, literalBytes]
|
|
127
|
+
if (nCategories > 0) return [nCategories, literalBytes]
|
|
128
|
+
return [null, literalBytes]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function estimateKeyStateStepBytes(step) {
|
|
132
|
+
var args = argsOf(step)
|
|
133
|
+
var op = step.op
|
|
134
|
+
if (op === 'rowid') {
|
|
135
|
+
var nCols = arrayLen(args.columns)
|
|
136
|
+
if (nCols === 0 || args.sorted === true) return 2048 + nCols * 128
|
|
137
|
+
var maxStateBytes = positiveInt(args.max_state_bytes)
|
|
138
|
+
if (maxStateBytes !== null) return maxStateBytes
|
|
139
|
+
var maxKeys = positiveInt(args.max_keys)
|
|
140
|
+
if (maxKeys === null) throw new Error("step 'rowid' needs max_keys, max_state_bytes, or sorted=true")
|
|
141
|
+
return 1024 + maxKeys * 224 + nCols * 128
|
|
142
|
+
}
|
|
143
|
+
if (op === 'unique' || op === 'dedup') {
|
|
144
|
+
var ukNCols = arrayLen(args.columns)
|
|
145
|
+
if (args.sorted === true) return 2048 + ukNCols * 128
|
|
146
|
+
if (args.approx === true || args.mode === 'approx') {
|
|
147
|
+
var ukBloomBytes = positiveInt(args.bloom_bytes)
|
|
148
|
+
return ukBloomBytes !== null ? ukBloomBytes : UNIQUE_DEFAULT_BLOOM_BYTES
|
|
149
|
+
}
|
|
150
|
+
var ukMaxStateBytes = positiveInt(args.max_state_bytes)
|
|
151
|
+
if (ukMaxStateBytes !== null) return ukMaxStateBytes
|
|
152
|
+
var ukMaxKeys = positiveInt(args.max_keys)
|
|
153
|
+
if (ukMaxKeys === null) throw new Error("step '" + op + "' needs max_keys, max_state_bytes, or sorted=true for byte-bounded native execution")
|
|
154
|
+
return 1024 + ukMaxKeys * (256 + ukNCols * 32)
|
|
155
|
+
}
|
|
156
|
+
if (op === 'group-agg') {
|
|
157
|
+
var nGroup = arrayLen(args.group_by)
|
|
158
|
+
var nAggs = arrayLen(args.aggs)
|
|
159
|
+
var maxStateBytes = positiveInt(args.max_state_bytes)
|
|
160
|
+
if (maxStateBytes !== null) return maxStateBytes
|
|
161
|
+
if (args.sorted === true) return 4096 + nGroup * 128 + nAggs * 128
|
|
162
|
+
var maxGroups = positiveInt(args.max_groups)
|
|
163
|
+
if (maxGroups === null) throw new Error("step 'group-agg' needs max_groups, max_state_bytes, or sorted=true for byte-bounded native execution")
|
|
164
|
+
return 2048 + maxGroups * (384 + nGroup * 64 + nAggs * 96)
|
|
165
|
+
}
|
|
166
|
+
if (op === 'frequency') {
|
|
167
|
+
var maxStateBytes = positiveInt(args.max_state_bytes)
|
|
168
|
+
if (maxStateBytes !== null) return maxStateBytes
|
|
169
|
+
var maxValues = positiveInt(args.max_values)
|
|
170
|
+
if (maxValues === null) throw new Error("step 'frequency' needs max_values or max_state_bytes for byte-bounded native execution")
|
|
171
|
+
var cappedValues = args.overflow === 'other' ? maxValues + 1 : maxValues
|
|
172
|
+
return 1024 + cappedValues * (224 + arrayLen(args.columns) * 32)
|
|
173
|
+
}
|
|
174
|
+
if (op === 'onehot' || op === 'label-encode') {
|
|
175
|
+
var catMaxStateBytes = positiveInt(args.max_state_bytes)
|
|
176
|
+
if (catMaxStateBytes !== null) return catMaxStateBytes
|
|
177
|
+
var cap = categoryCap(step)
|
|
178
|
+
if (cap[0] === null) throw new Error("step '" + op + "' needs max_categories, max_state_bytes, or declared categories")
|
|
179
|
+
return 1024 + cap[1] + cap[0] * (op === 'onehot' ? 320 : 224)
|
|
180
|
+
}
|
|
181
|
+
if (op === 'intersect' || op === 'setdiff') {
|
|
182
|
+
var setNCols = arrayLen(args.columns)
|
|
183
|
+
if (args.sorted === true) return 4096 + setNCols * 256
|
|
184
|
+
var setMaxLookupBytes = positiveInt(args.max_lookup_bytes)
|
|
185
|
+
if (setMaxLookupBytes === null) throw new Error("step '" + op + "' needs max_lookup_bytes or sorted=true for byte-bounded native execution")
|
|
186
|
+
var setMaxStateBytes = positiveInt(args.max_state_bytes)
|
|
187
|
+
if (setMaxStateBytes !== null) return 4096 + setMaxLookupBytes * 3 + setMaxStateBytes
|
|
188
|
+
var setMaxOutputKeys = positiveInt(args.max_output_keys)
|
|
189
|
+
if (setMaxOutputKeys === null) throw new Error("step '" + op + "' needs max_output_keys or max_state_bytes for duplicate-eliminating set semantics")
|
|
190
|
+
var setEst = 4096 + setMaxLookupBytes * 3
|
|
191
|
+
var setMaxLookupKeys = positiveInt(args.max_lookup_keys)
|
|
192
|
+
if (setMaxLookupKeys !== null) setEst += setMaxLookupKeys * (192 + setNCols * 32)
|
|
193
|
+
setEst += setMaxOutputKeys * (192 + setNCols * 32)
|
|
194
|
+
return setEst
|
|
195
|
+
}
|
|
196
|
+
if (op === 'union') {
|
|
197
|
+
var unionMaxStateBytes = positiveInt(args.max_state_bytes)
|
|
198
|
+
if (unionMaxStateBytes !== null) return unionMaxStateBytes
|
|
199
|
+
var unionNCols = arrayLen(args.columns)
|
|
200
|
+
var unionMaxOutputKeys = positiveInt(args.max_output_keys)
|
|
201
|
+
if (unionMaxOutputKeys === null) throw new Error("step 'union' needs max_output_keys or max_state_bytes for duplicate-eliminating set semantics")
|
|
202
|
+
return 4096 + unionMaxOutputKeys * (192 + unionNCols * 32)
|
|
203
|
+
}
|
|
204
|
+
if (op === 'join' || op === 'semi-join' || op === 'anti-join') {
|
|
205
|
+
if (args.sorted === true) {
|
|
206
|
+
var filtering = op === 'semi-join' || op === 'anti-join' || args.how === 'semi' || args.how === 'anti'
|
|
207
|
+
if (filtering) return 4096
|
|
208
|
+
var maxMatches = positiveInt(args.max_matches_per_row)
|
|
209
|
+
if (maxMatches === null) throw new Error("step '" + op + "' sorted=true needs max_matches_per_row for byte-bounded mutating join")
|
|
210
|
+
return 4096 + maxMatches * 512
|
|
211
|
+
}
|
|
212
|
+
var maxLookupBytes = positiveInt(args.max_lookup_bytes)
|
|
213
|
+
if (maxLookupBytes === null) throw new Error("step '" + op + "' needs max_lookup_bytes or sorted=true for byte-bounded native execution")
|
|
214
|
+
var est = 4096 + maxLookupBytes * 4
|
|
215
|
+
var maxStateBytes = positiveInt(args.max_state_bytes)
|
|
216
|
+
if (maxStateBytes !== null) return est + maxStateBytes
|
|
217
|
+
var maxLookupRows = positiveInt(args.max_lookup_rows)
|
|
218
|
+
var maxLookupKeys = positiveInt(args.max_lookup_keys)
|
|
219
|
+
if (maxLookupRows !== null) est += maxLookupRows * 96
|
|
220
|
+
if (maxLookupKeys !== null) est += maxLookupKeys * 192
|
|
221
|
+
return est
|
|
222
|
+
}
|
|
223
|
+
throw new Error("step '" + op + "' has no native byte estimator")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function loadPlan(planJson) {
|
|
227
|
+
var plan = typeof planJson === 'string' ? JSON.parse(planJson) : planJson
|
|
228
|
+
if (!plan || !Array.isArray(plan.steps)) throw new Error('plan must contain a steps array')
|
|
229
|
+
return plan
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function blockingSteps(steps) {
|
|
233
|
+
var out = []
|
|
234
|
+
for (var i = 0; i < steps.length; i++) if (memoryClass(steps[i]) === 'blocking') out.push(steps[i])
|
|
235
|
+
return out
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function keyStateSteps(steps) {
|
|
239
|
+
var out = []
|
|
240
|
+
for (var i = 0; i < steps.length; i++) if (memoryClass(steps[i]) === 'key_state') out.push(steps[i])
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function blockingPlanCanUseNativeSpill(steps) {
|
|
245
|
+
if (steps.length === 0) return false
|
|
246
|
+
for (var i = 0; i < steps.length; i++) if (steps[i].op !== 'sort') return false
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function injectSortSpill(plan, spillDir, memoryBytes) {
|
|
251
|
+
for (var i = 0; i < plan.steps.length; i++) {
|
|
252
|
+
var step = plan.steps[i]
|
|
253
|
+
if (step.op !== 'sort') continue
|
|
254
|
+
if (!step.args || typeof step.args !== 'object') step.args = {}
|
|
255
|
+
step.args.spill_dir = spillDir
|
|
256
|
+
if (memoryBytes !== null) step.args.spill_memory_bytes = memoryBytes
|
|
257
|
+
}
|
|
258
|
+
return JSON.stringify(plan)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function prepareNativePlan(planJson, options) {
|
|
262
|
+
options = options || {}
|
|
263
|
+
var plan = loadPlan(planJson)
|
|
264
|
+
var blocking = blockingSteps(plan.steps)
|
|
265
|
+
var firstBlocking = blocking.length ? blocking[0] : null
|
|
266
|
+
var keySteps = keyStateSteps(plan.steps)
|
|
267
|
+
var memoryBytes = parseMemorySize(options.memory)
|
|
268
|
+
var canSpillBlocking = false
|
|
269
|
+
|
|
270
|
+
if (options.spillDir) {
|
|
271
|
+
throw new Error('spillDir is not supported by standalone WASM native execution because browser/WASM spill storage would occupy WASM memory; use the Node native addon, CLI/direct C, or DuckDB')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (options.spillDir) {
|
|
275
|
+
if (keySteps.length > 0) {
|
|
276
|
+
throw new Error("spillDir was requested, but native spill is not implemented yet for key-state step '" + keySteps[0].op + "'")
|
|
277
|
+
}
|
|
278
|
+
for (var i = 0; i < blocking.length; i++) {
|
|
279
|
+
if (blocking[i].op !== 'sort') {
|
|
280
|
+
throw new Error("spillDir was requested, but native spill is not implemented yet for blocking step '" + blocking[i].op + "'")
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (memoryBytes !== null && firstBlocking && !canSpillBlocking) {
|
|
286
|
+
throw new Error("memory " + formatBytes(memoryBytes) + " was requested, but native byte caps are not implemented for blocking step '" + firstBlocking.op + "'")
|
|
287
|
+
}
|
|
288
|
+
if (memoryBytes !== null && keySteps.length > 0) {
|
|
289
|
+
var total = 0
|
|
290
|
+
for (var j = 0; j < keySteps.length; j++) total += estimateKeyStateStepBytes(keySteps[j])
|
|
291
|
+
if (total > memoryBytes) throw new Error('estimated native key-state memory ' + formatBytes(total) + ' exceeds memory ' + formatBytes(memoryBytes))
|
|
292
|
+
}
|
|
293
|
+
if (firstBlocking && !options.allowBlocking && !canSpillBlocking) {
|
|
294
|
+
throw new Error("blocking step '" + firstBlocking.op + "' requires full input in native mode; pass allowBlocking: true for known-small data or use an external engine")
|
|
295
|
+
}
|
|
296
|
+
if (canSpillBlocking) return injectSortSpill(plan, options.spillDir, memoryBytes)
|
|
297
|
+
return typeof planJson === 'string' ? planJson : JSON.stringify(plan)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validateNativeMemoryPolicy(planJson, options) {
|
|
301
|
+
options = options || {}
|
|
302
|
+
var plan = loadPlan(planJson)
|
|
303
|
+
var keySteps = keyStateSteps(plan.steps)
|
|
304
|
+
var memoryBytes = parseMemorySize(options.memory)
|
|
305
|
+
prepareNativePlan(plan, options)
|
|
306
|
+
if (memoryBytes === null || keySteps.length === 0) return null
|
|
307
|
+
var total = 0
|
|
308
|
+
for (var i = 0; i < keySteps.length; i++) total += estimateKeyStateStepBytes(keySteps[i])
|
|
309
|
+
return total
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* tranfi/wasm — Browser-friendly WASM wrapper.
|
|
314
|
+
*
|
|
315
|
+
* Usage:
|
|
316
|
+
* import createTranfi from 'tranfi/wasm'
|
|
317
|
+
* const tf = await createTranfi()
|
|
318
|
+
*
|
|
319
|
+
* // Compile DSL to SQL
|
|
320
|
+
* const sql = tf.compileToSql('csv | filter "age > 25" | csv', { dialect: 'duckdb' })
|
|
321
|
+
*
|
|
322
|
+
* // Run with DuckDB-WASM
|
|
323
|
+
* import * as duckdb from '@duckdb/duckdb-wasm'
|
|
324
|
+
* const db = new duckdb.AsyncDuckDB(...)
|
|
325
|
+
* const result = await tf.runDuckDB(db, 'csv | filter "age > 25" | csv', csvBlob)
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
async function createTranfi() {
|
|
329
|
+
// Dynamic import to handle CJS/ESM interop
|
|
330
|
+
var mod = await import('./tranfi_core.js')
|
|
331
|
+
var createModule = mod.default || mod
|
|
332
|
+
var wasm = await createModule()
|
|
333
|
+
var transformAPI = createTransformAPI(wasm)
|
|
334
|
+
|
|
335
|
+
function allocString(str) {
|
|
336
|
+
var len = wasm.lengthBytesUTF8(str)
|
|
337
|
+
var ptr = wasm._malloc(len + 1)
|
|
338
|
+
wasm.stringToUTF8(str, ptr, len + 1)
|
|
339
|
+
return { ptr, len }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function toBytes(data) {
|
|
343
|
+
return data instanceof Uint8Array ? data : new TextEncoder().encode(data)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function concatUint8(chunks) {
|
|
347
|
+
var total = chunks.reduce(function(s, c) { return s + c.length }, 0)
|
|
348
|
+
var result = new Uint8Array(total)
|
|
349
|
+
var offset = 0
|
|
350
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
351
|
+
result.set(chunks[i], offset)
|
|
352
|
+
offset += chunks[i].length
|
|
353
|
+
}
|
|
354
|
+
return result
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function hostPolicyOptions(options) {
|
|
358
|
+
options = options || {}
|
|
359
|
+
return {
|
|
360
|
+
allowFs: options.allowFs === undefined ? Boolean(options.spillDir) : Boolean(options.allowFs),
|
|
361
|
+
allowSpill: Boolean(options.spillDir),
|
|
362
|
+
allowRulesFile: Boolean(options.allowRulesFile),
|
|
363
|
+
workspaceRoot: options.workspaceRoot
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function planUsesHostPaths(planJson) {
|
|
368
|
+
try {
|
|
369
|
+
var plan = JSON.parse(planJson)
|
|
370
|
+
var steps = Array.isArray(plan.steps) ? plan.steps : []
|
|
371
|
+
return steps.some(function(step) {
|
|
372
|
+
var args = step && step.args
|
|
373
|
+
if (!args || typeof args !== 'object') return false
|
|
374
|
+
return ['file', 'rules_file', 'rulesFile', 'spill_dir', 'spillDir'].some(function(key) {
|
|
375
|
+
return typeof args[key] === 'string' && args[key].length > 0
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
} catch (_) {
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
/** Raw WASM module (for advanced use) */
|
|
385
|
+
_wasm: wasm,
|
|
386
|
+
|
|
387
|
+
TranfiTransformError: transformAPI.TranfiTransformError,
|
|
388
|
+
TransformRecipe: transformAPI.TransformRecipe,
|
|
389
|
+
TransformAnalyzer: transformAPI.TransformAnalyzer,
|
|
390
|
+
TransformPlan: transformAPI.TransformPlan,
|
|
391
|
+
TransformApply: transformAPI.TransformApply,
|
|
392
|
+
TransformCancelToken: transformAPI.TransformCancelToken,
|
|
393
|
+
createTransformCancelToken: transformAPI.createTransformCancelToken,
|
|
394
|
+
safeTransformLimits: transformAPI.safeTransformLimits,
|
|
395
|
+
|
|
396
|
+
/** Library version */
|
|
397
|
+
version() {
|
|
398
|
+
return wasm.ccall('wasm_version', 'string', [], [])
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/** Compile DSL string to plan JSON */
|
|
402
|
+
compileDsl(dsl) {
|
|
403
|
+
var s = allocString(dsl)
|
|
404
|
+
var resultPtr = wasm.ccall('wasm_compile_dsl', 'number', ['number', 'number'], [s.ptr, s.len])
|
|
405
|
+
wasm._free(s.ptr)
|
|
406
|
+
if (!resultPtr) {
|
|
407
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [-1])
|
|
408
|
+
throw new Error('DSL parse failed: ' + (err || 'unknown error'))
|
|
409
|
+
}
|
|
410
|
+
var json = wasm.UTF8ToString(resultPtr)
|
|
411
|
+
wasm._free(resultPtr)
|
|
412
|
+
return json
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
/** Apply standalone WASM native memory policy to a compiled plan */
|
|
416
|
+
prepareNativePlan(planJson, options) {
|
|
417
|
+
return prepareNativePlan(planJson, options || {})
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/** Compile DSL string to SQL query */
|
|
421
|
+
compileToSql(dsl, options) {
|
|
422
|
+
normalizeSqlDialect(options)
|
|
423
|
+
var s = allocString(dsl)
|
|
424
|
+
var resultPtr = wasm.ccall('wasm_compile_to_sql', 'number', ['number', 'number'], [s.ptr, s.len])
|
|
425
|
+
wasm._free(s.ptr)
|
|
426
|
+
if (!resultPtr) {
|
|
427
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [-1])
|
|
428
|
+
throw new Error('SQL compile failed: ' + (err || 'unknown error'))
|
|
429
|
+
}
|
|
430
|
+
var sql = wasm.UTF8ToString(resultPtr)
|
|
431
|
+
wasm._free(resultPtr)
|
|
432
|
+
return sql
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/** Create a native pipeline (push/pull streaming) */
|
|
436
|
+
createPipeline(planJson, options) {
|
|
437
|
+
options = options || {}
|
|
438
|
+
var s = allocString(planJson)
|
|
439
|
+
var root = options.workspaceRoot ? allocString(String(options.workspaceRoot)) : { ptr: 0 }
|
|
440
|
+
var hasPolicyCreate = typeof wasm._wasm_pipeline_create_with_policy === 'function'
|
|
441
|
+
var needsPolicyCreate = options.allowFs || options.allowSpill || options.allowRulesFile || root.ptr || planUsesHostPaths(planJson)
|
|
442
|
+
var handle = -1
|
|
443
|
+
if (hasPolicyCreate) {
|
|
444
|
+
handle = wasm.ccall('wasm_pipeline_create_with_policy', 'number',
|
|
445
|
+
['number', 'number', 'number', 'number', 'number', 'number'],
|
|
446
|
+
[s.ptr, s.len, options.allowFs ? 1 : 0, options.allowSpill ? 1 : 0,
|
|
447
|
+
options.allowRulesFile ? 1 : 0, root.ptr])
|
|
448
|
+
} else if (!needsPolicyCreate) {
|
|
449
|
+
handle = wasm.ccall('wasm_pipeline_create', 'number', ['number', 'number'], [s.ptr, s.len])
|
|
450
|
+
}
|
|
451
|
+
wasm._free(s.ptr)
|
|
452
|
+
if (root.ptr) wasm._free(root.ptr)
|
|
453
|
+
if (!hasPolicyCreate && needsPolicyCreate) {
|
|
454
|
+
throw new Error('Failed to create pipeline: WASM host policy export unavailable for this plan; rebuild tranfi_core.js')
|
|
455
|
+
}
|
|
456
|
+
if (handle < 0) {
|
|
457
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
458
|
+
throw new Error('Failed to create pipeline: ' + (err || 'unknown error'))
|
|
459
|
+
}
|
|
460
|
+
return handle
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/** Push data into pipeline */
|
|
464
|
+
push(handle, data) {
|
|
465
|
+
var buf = data instanceof Uint8Array ? data : new TextEncoder().encode(data)
|
|
466
|
+
var ptr = wasm._malloc(buf.length)
|
|
467
|
+
wasm.HEAPU8.set(buf, ptr)
|
|
468
|
+
var rc = wasm.ccall('wasm_pipeline_push', 'number', ['number', 'number', 'number'], [handle, ptr, buf.length])
|
|
469
|
+
wasm._free(ptr)
|
|
470
|
+
if (rc !== 0) {
|
|
471
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
472
|
+
throw new Error('Push failed: ' + (err || 'unknown error'))
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/** Flush the decoder input boundary without finishing transforms */
|
|
477
|
+
flushInput(handle) {
|
|
478
|
+
var rc = wasm.ccall('wasm_pipeline_flush_input', 'number', ['number'], [handle])
|
|
479
|
+
if (rc !== 0) {
|
|
480
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
481
|
+
throw new Error('Input flush failed: ' + (err || 'unknown error'))
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
/** Set or clear the current host source name */
|
|
486
|
+
setSourceName(handle, name) {
|
|
487
|
+
var source = name == null ? '' : String(name)
|
|
488
|
+
var s = allocString(source)
|
|
489
|
+
var rc = wasm.ccall('wasm_pipeline_set_source_name', 'number', ['number', 'number'], [handle, s.ptr])
|
|
490
|
+
wasm._free(s.ptr)
|
|
491
|
+
if (rc !== 0) {
|
|
492
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
493
|
+
throw new Error('Set source name failed: ' + (err || 'unknown error'))
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/** Signal end of input */
|
|
498
|
+
finish(handle) {
|
|
499
|
+
var rc = wasm.ccall('wasm_pipeline_finish', 'number', ['number'], [handle])
|
|
500
|
+
if (rc !== 0) {
|
|
501
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
502
|
+
throw new Error('Finish failed: ' + (err || 'unknown error'))
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
/** Advance finish by one flush boundary. Returns true when complete. */
|
|
507
|
+
finishStep(handle) {
|
|
508
|
+
var rc = wasm.ccall('wasm_pipeline_finish_step', 'number', ['number'], [handle])
|
|
509
|
+
if (rc < 0) {
|
|
510
|
+
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
511
|
+
throw new Error('Finish failed: ' + (err || 'unknown error'))
|
|
512
|
+
}
|
|
513
|
+
return rc === 1
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/** Pull output from pipeline */
|
|
517
|
+
pull(handle, channel) {
|
|
518
|
+
var bufSize = 65536
|
|
519
|
+
var ptr = wasm._malloc(bufSize)
|
|
520
|
+
var chunks = []
|
|
521
|
+
for (;;) {
|
|
522
|
+
var n = wasm.ccall('wasm_pipeline_pull', 'number',
|
|
523
|
+
['number', 'number', 'number', 'number'], [handle, channel, ptr, bufSize])
|
|
524
|
+
if (n === 0) break
|
|
525
|
+
chunks.push(new Uint8Array(wasm.HEAPU8.buffer, ptr, n).slice())
|
|
526
|
+
}
|
|
527
|
+
wasm._free(ptr)
|
|
528
|
+
var total = chunks.reduce(function(s, c) { return s + c.length }, 0)
|
|
529
|
+
var result = new Uint8Array(total)
|
|
530
|
+
var offset = 0
|
|
531
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
532
|
+
result.set(chunks[i], offset)
|
|
533
|
+
offset += chunks[i].length
|
|
534
|
+
}
|
|
535
|
+
return result
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/** Pull at most bufSize bytes from pipeline */
|
|
539
|
+
pullChunk(handle, channel, bufSize) {
|
|
540
|
+
bufSize = bufSize || 65536
|
|
541
|
+
if (bufSize <= 0) throw new Error('bufSize must be positive')
|
|
542
|
+
var ptr = wasm._malloc(bufSize)
|
|
543
|
+
var n = wasm.ccall('wasm_pipeline_pull', 'number',
|
|
544
|
+
['number', 'number', 'number', 'number'], [handle, channel, ptr, bufSize])
|
|
545
|
+
var result = new Uint8Array(wasm.HEAPU8.buffer, ptr, n).slice()
|
|
546
|
+
wasm._free(ptr)
|
|
547
|
+
return result
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/** Free pipeline */
|
|
551
|
+
free(handle) {
|
|
552
|
+
wasm.ccall('wasm_pipeline_free', null, ['number'], [handle])
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
/** Run pipeline with native streaming engine */
|
|
556
|
+
run(dsl, data, options) {
|
|
557
|
+
options = options || {}
|
|
558
|
+
var chunkSize = options.chunkSize || 65536
|
|
559
|
+
if (chunkSize <= 0) throw new Error('chunkSize must be positive')
|
|
560
|
+
var onOutput = options.onOutput
|
|
561
|
+
if (onOutput && typeof onOutput !== 'function') throw new TypeError('onOutput must be a function')
|
|
562
|
+
var collectOutput = options.collectOutput !== false
|
|
563
|
+
var allowBlocking = options.allowBlocking === true
|
|
564
|
+
|
|
565
|
+
var planJson = this.compileDsl(dsl)
|
|
566
|
+
planJson = prepareNativePlan(planJson, { allowBlocking: allowBlocking, memory: options.memory, spillDir: options.spillDir })
|
|
567
|
+
var handle = this.createPipeline(planJson, hostPolicyOptions(options))
|
|
568
|
+
var outputChunks = []
|
|
569
|
+
var self = this
|
|
570
|
+
function drainMain() {
|
|
571
|
+
for (;;) {
|
|
572
|
+
var chunk = self.pullChunk(handle, 0, chunkSize)
|
|
573
|
+
if (chunk.length === 0) break
|
|
574
|
+
if (onOutput) onOutput(chunk)
|
|
575
|
+
if (collectOutput) outputChunks.push(chunk)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
var buf = toBytes(data || '')
|
|
581
|
+
for (var i = 0; i < buf.length; i += chunkSize) {
|
|
582
|
+
this.push(handle, buf.subarray(i, Math.min(i + chunkSize, buf.length)))
|
|
583
|
+
drainMain()
|
|
584
|
+
}
|
|
585
|
+
for (;;) {
|
|
586
|
+
var done = this.finishStep(handle)
|
|
587
|
+
drainMain()
|
|
588
|
+
if (done) break
|
|
589
|
+
}
|
|
590
|
+
var output = collectOutput ? concatUint8(outputChunks) : new Uint8Array(0)
|
|
591
|
+
return {
|
|
592
|
+
output: output,
|
|
593
|
+
errors: this.pull(handle, 1),
|
|
594
|
+
stats: this.pull(handle, 2),
|
|
595
|
+
samples: this.pull(handle, 3),
|
|
596
|
+
get outputText() { return new TextDecoder().decode(this.output) },
|
|
597
|
+
get statsText() { return new TextDecoder().decode(this.stats) }
|
|
598
|
+
}
|
|
599
|
+
} finally {
|
|
600
|
+
this.free(handle)
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
/** Yield native main-output chunks while a WASM pipeline runs */
|
|
605
|
+
*iterChunks(dsl, data, options) {
|
|
606
|
+
options = options || {}
|
|
607
|
+
var chunkSize = options.chunkSize || 65536
|
|
608
|
+
if (chunkSize <= 0) throw new Error('chunkSize must be positive')
|
|
609
|
+
var allowBlocking = options.allowBlocking === true
|
|
610
|
+
var planJson = this.compileDsl(dsl)
|
|
611
|
+
planJson = prepareNativePlan(planJson, { allowBlocking: allowBlocking, memory: options.memory, spillDir: options.spillDir })
|
|
612
|
+
var handle = this.createPipeline(planJson, hostPolicyOptions(options))
|
|
613
|
+
try {
|
|
614
|
+
var buf = toBytes(data || '')
|
|
615
|
+
for (var i = 0; i < buf.length; i += chunkSize) {
|
|
616
|
+
this.push(handle, buf.subarray(i, Math.min(i + chunkSize, buf.length)))
|
|
617
|
+
for (;;) {
|
|
618
|
+
var chunk = this.pullChunk(handle, 0, chunkSize)
|
|
619
|
+
if (chunk.length === 0) break
|
|
620
|
+
yield chunk
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (;;) {
|
|
624
|
+
var done = this.finishStep(handle)
|
|
625
|
+
for (;;) {
|
|
626
|
+
var out = this.pullChunk(handle, 0, chunkSize)
|
|
627
|
+
if (out.length === 0) break
|
|
628
|
+
yield out
|
|
629
|
+
}
|
|
630
|
+
if (done) break
|
|
631
|
+
}
|
|
632
|
+
} finally {
|
|
633
|
+
this.free(handle)
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Run pipeline via DuckDB-WASM.
|
|
639
|
+
*
|
|
640
|
+
* @param {object} db — An initialized @duckdb/duckdb-wasm AsyncDuckDB or DuckDB instance
|
|
641
|
+
* @param {string} dsl — Tranfi DSL string
|
|
642
|
+
* @param {string|Uint8Array|File} data — CSV input data
|
|
643
|
+
* @returns {Promise<{output: Uint8Array, outputText: string, rows: Array}>}
|
|
644
|
+
*/
|
|
645
|
+
async runDuckDB(db, dsl, data) {
|
|
646
|
+
var sql = this.compileToSql(dsl)
|
|
647
|
+
var conn = await db.connect()
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
// Register input data as a table
|
|
651
|
+
if (typeof data === 'string' || data instanceof Uint8Array) {
|
|
652
|
+
var buf = data instanceof Uint8Array ? data : new TextEncoder().encode(data)
|
|
653
|
+
await db.registerFileBuffer('input.csv', buf)
|
|
654
|
+
sql = sql.replaceAll('input_data', "read_csv('input.csv')")
|
|
655
|
+
} else if (data && data.name) {
|
|
656
|
+
// File object
|
|
657
|
+
await db.registerFileHandle(data.name, data)
|
|
658
|
+
sql = sql.replaceAll('input_data', "read_csv('" + data.name + "')")
|
|
659
|
+
} else {
|
|
660
|
+
throw new Error('data must be a string, Uint8Array, or File')
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
var result = await conn.query(sql)
|
|
664
|
+
var rows = result.toArray().map(function(row) { return row.toJSON() })
|
|
665
|
+
|
|
666
|
+
// Convert to CSV
|
|
667
|
+
var output = ''
|
|
668
|
+
if (rows.length > 0) {
|
|
669
|
+
var cols = Object.keys(rows[0])
|
|
670
|
+
output = cols.join(',') + '\n'
|
|
671
|
+
for (var i = 0; i < rows.length; i++) {
|
|
672
|
+
var vals = cols.map(function(c) {
|
|
673
|
+
var v = rows[i][c]
|
|
674
|
+
if (v === null || v === undefined) return ''
|
|
675
|
+
var s = String(v)
|
|
676
|
+
if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0) {
|
|
677
|
+
return '"' + s.replace(/"/g, '""') + '"'
|
|
678
|
+
}
|
|
679
|
+
return s
|
|
680
|
+
})
|
|
681
|
+
output += vals.join(',') + '\n'
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
var outputBytes = new TextEncoder().encode(output)
|
|
686
|
+
return {
|
|
687
|
+
output: outputBytes,
|
|
688
|
+
rows: rows,
|
|
689
|
+
get outputText() { return output }
|
|
690
|
+
}
|
|
691
|
+
} finally {
|
|
692
|
+
await conn.close()
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
/** Recipe helpers */
|
|
697
|
+
recipeCount() {
|
|
698
|
+
return wasm.ccall('wasm_recipe_count', 'number', [], [])
|
|
699
|
+
},
|
|
700
|
+
recipeName(index) {
|
|
701
|
+
return wasm.ccall('wasm_recipe_name', 'string', ['number'], [index])
|
|
702
|
+
},
|
|
703
|
+
recipeDsl(index) {
|
|
704
|
+
return wasm.ccall('wasm_recipe_dsl', 'string', ['number'], [index])
|
|
705
|
+
},
|
|
706
|
+
recipeDescription(index) {
|
|
707
|
+
return wasm.ccall('wasm_recipe_description', 'string', ['number'], [index])
|
|
708
|
+
},
|
|
709
|
+
recipeFindDsl(name) {
|
|
710
|
+
var s = allocString(name)
|
|
711
|
+
var result = wasm.ccall('wasm_recipe_find_dsl', 'string', ['number'], [s.ptr])
|
|
712
|
+
wasm._free(s.ptr)
|
|
713
|
+
return result || null
|
|
714
|
+
},
|
|
715
|
+
recipes() {
|
|
716
|
+
var n = this.recipeCount()
|
|
717
|
+
var result = []
|
|
718
|
+
for (var i = 0; i < n; i++) {
|
|
719
|
+
result.push({
|
|
720
|
+
name: this.recipeName(i),
|
|
721
|
+
dsl: this.recipeDsl(i),
|
|
722
|
+
description: this.recipeDescription(i)
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
return result
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Support both CJS and ESM
|
|
731
|
+
if (typeof module !== 'undefined') module.exports = createTranfi
|
|
732
|
+
if (typeof exports !== 'undefined') exports.default = createTranfi
|