tranfi 0.1.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 -0
- package/NOTICE +8 -0
- package/README.md +272 -40
- 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 +348 -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 +8 -11
- 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/wasm/index.js
CHANGED
|
@@ -1,3 +1,314 @@
|
|
|
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
|
+
|
|
1
312
|
/**
|
|
2
313
|
* tranfi/wasm — Browser-friendly WASM wrapper.
|
|
3
314
|
*
|
|
@@ -6,7 +317,7 @@
|
|
|
6
317
|
* const tf = await createTranfi()
|
|
7
318
|
*
|
|
8
319
|
* // Compile DSL to SQL
|
|
9
|
-
* const sql = tf.compileToSql('csv | filter "age > 25" | csv')
|
|
320
|
+
* const sql = tf.compileToSql('csv | filter "age > 25" | csv', { dialect: 'duckdb' })
|
|
10
321
|
*
|
|
11
322
|
* // Run with DuckDB-WASM
|
|
12
323
|
* import * as duckdb from '@duckdb/duckdb-wasm'
|
|
@@ -19,6 +330,7 @@ async function createTranfi() {
|
|
|
19
330
|
var mod = await import('./tranfi_core.js')
|
|
20
331
|
var createModule = mod.default || mod
|
|
21
332
|
var wasm = await createModule()
|
|
333
|
+
var transformAPI = createTransformAPI(wasm)
|
|
22
334
|
|
|
23
335
|
function allocString(str) {
|
|
24
336
|
var len = wasm.lengthBytesUTF8(str)
|
|
@@ -27,10 +339,60 @@ async function createTranfi() {
|
|
|
27
339
|
return { ptr, len }
|
|
28
340
|
}
|
|
29
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
|
+
|
|
30
383
|
return {
|
|
31
384
|
/** Raw WASM module (for advanced use) */
|
|
32
385
|
_wasm: wasm,
|
|
33
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
|
+
|
|
34
396
|
/** Library version */
|
|
35
397
|
version() {
|
|
36
398
|
return wasm.ccall('wasm_version', 'string', [], [])
|
|
@@ -50,8 +412,14 @@ async function createTranfi() {
|
|
|
50
412
|
return json
|
|
51
413
|
},
|
|
52
414
|
|
|
415
|
+
/** Apply standalone WASM native memory policy to a compiled plan */
|
|
416
|
+
prepareNativePlan(planJson, options) {
|
|
417
|
+
return prepareNativePlan(planJson, options || {})
|
|
418
|
+
},
|
|
419
|
+
|
|
53
420
|
/** Compile DSL string to SQL query */
|
|
54
|
-
compileToSql(dsl) {
|
|
421
|
+
compileToSql(dsl, options) {
|
|
422
|
+
normalizeSqlDialect(options)
|
|
55
423
|
var s = allocString(dsl)
|
|
56
424
|
var resultPtr = wasm.ccall('wasm_compile_to_sql', 'number', ['number', 'number'], [s.ptr, s.len])
|
|
57
425
|
wasm._free(s.ptr)
|
|
@@ -65,10 +433,26 @@ async function createTranfi() {
|
|
|
65
433
|
},
|
|
66
434
|
|
|
67
435
|
/** Create a native pipeline (push/pull streaming) */
|
|
68
|
-
createPipeline(planJson) {
|
|
436
|
+
createPipeline(planJson, options) {
|
|
437
|
+
options = options || {}
|
|
69
438
|
var s = allocString(planJson)
|
|
70
|
-
var
|
|
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
|
+
}
|
|
71
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
|
+
}
|
|
72
456
|
if (handle < 0) {
|
|
73
457
|
var err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
|
|
74
458
|
throw new Error('Failed to create pipeline: ' + (err || 'unknown error'))
|
|
@@ -89,6 +473,27 @@ async function createTranfi() {
|
|
|
89
473
|
}
|
|
90
474
|
},
|
|
91
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
|
+
|
|
92
497
|
/** Signal end of input */
|
|
93
498
|
finish(handle) {
|
|
94
499
|
var rc = wasm.ccall('wasm_pipeline_finish', 'number', ['number'], [handle])
|
|
@@ -98,6 +503,16 @@ async function createTranfi() {
|
|
|
98
503
|
}
|
|
99
504
|
},
|
|
100
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
|
+
|
|
101
516
|
/** Pull output from pipeline */
|
|
102
517
|
pull(handle, channel) {
|
|
103
518
|
var bufSize = 65536
|
|
@@ -120,21 +535,61 @@ async function createTranfi() {
|
|
|
120
535
|
return result
|
|
121
536
|
},
|
|
122
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
|
+
|
|
123
550
|
/** Free pipeline */
|
|
124
551
|
free(handle) {
|
|
125
552
|
wasm.ccall('wasm_pipeline_free', null, ['number'], [handle])
|
|
126
553
|
},
|
|
127
554
|
|
|
128
555
|
/** Run pipeline with native streaming engine */
|
|
129
|
-
run(dsl, data) {
|
|
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
|
+
|
|
130
565
|
var planJson = this.compileDsl(dsl)
|
|
131
|
-
|
|
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
|
+
|
|
132
579
|
try {
|
|
133
|
-
var buf = data
|
|
134
|
-
|
|
135
|
-
|
|
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)
|
|
136
591
|
return {
|
|
137
|
-
output:
|
|
592
|
+
output: output,
|
|
138
593
|
errors: this.pull(handle, 1),
|
|
139
594
|
stats: this.pull(handle, 2),
|
|
140
595
|
samples: this.pull(handle, 3),
|
|
@@ -146,6 +601,39 @@ async function createTranfi() {
|
|
|
146
601
|
}
|
|
147
602
|
},
|
|
148
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
|
+
|
|
149
637
|
/**
|
|
150
638
|
* Run pipeline via DuckDB-WASM.
|
|
151
639
|
*
|
package/wasm/tranfi_core.js
CHANGED
|
Binary file
|