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.
Files changed (151) hide show
  1. package/LICENSE +177 -21
  2. package/NOTICE +8 -0
  3. package/README.md +627 -0
  4. package/app/assets/index-6quYZ5Ap.css +5 -0
  5. package/app/assets/index-BIAIKnrp.js +160 -0
  6. package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  7. package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  8. package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  9. package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  10. package/app/index.html +13 -0
  11. package/binding.gyp +121 -0
  12. package/csrc/arena.c +93 -0
  13. package/csrc/batch.c +976 -0
  14. package/csrc/buffer.c +154 -0
  15. package/csrc/cJSON.c +3386 -0
  16. package/csrc/cJSON.h +316 -0
  17. package/csrc/codec_csv.c +1951 -0
  18. package/csrc/codec_jsonl.c +1086 -0
  19. package/csrc/codec_table.c +248 -0
  20. package/csrc/codec_text.c +447 -0
  21. package/csrc/compiler.c +130 -0
  22. package/csrc/config.h +21 -0
  23. package/csrc/date_utils.h +94 -0
  24. package/csrc/dsl.c +5417 -0
  25. package/csrc/dsl.h +22 -0
  26. package/csrc/expr.c +1553 -0
  27. package/csrc/expr.h +58 -0
  28. package/csrc/internal.h +539 -0
  29. package/csrc/ir.c +166 -0
  30. package/csrc/ir.h +208 -0
  31. package/csrc/ir_schema.c +75 -0
  32. package/csrc/ir_serialize.c +166 -0
  33. package/csrc/ir_sql.c +1822 -0
  34. package/csrc/ir_validate.c +576 -0
  35. package/csrc/json_path.c +210 -0
  36. package/csrc/main.c +1241 -0
  37. package/csrc/memory_estimate.c +477 -0
  38. package/csrc/op_acf.c +283 -0
  39. package/csrc/op_across.c +477 -0
  40. package/csrc/op_anomaly.c +255 -0
  41. package/csrc/op_assert.c +761 -0
  42. package/csrc/op_bin.c +248 -0
  43. package/csrc/op_cast.c +523 -0
  44. package/csrc/op_clip.c +99 -0
  45. package/csrc/op_date_trunc.c +355 -0
  46. package/csrc/op_datetime.c +394 -0
  47. package/csrc/op_derive.c +216 -0
  48. package/csrc/op_diff.c +250 -0
  49. package/csrc/op_ewma.c +222 -0
  50. package/csrc/op_explode.c +206 -0
  51. package/csrc/op_fill_down.c +235 -0
  52. package/csrc/op_fill_null.c +268 -0
  53. package/csrc/op_filter.c +181 -0
  54. package/csrc/op_frequency.c +721 -0
  55. package/csrc/op_grep.c +181 -0
  56. package/csrc/op_group_agg.c +1956 -0
  57. package/csrc/op_hash.c +159 -0
  58. package/csrc/op_head.c +84 -0
  59. package/csrc/op_interpolate.c +445 -0
  60. package/csrc/op_join.c +2902 -0
  61. package/csrc/op_json_extract.c +227 -0
  62. package/csrc/op_json_filter.c +384 -0
  63. package/csrc/op_json_flatten.c +293 -0
  64. package/csrc/op_json_schema.c +503 -0
  65. package/csrc/op_label_encode.c +419 -0
  66. package/csrc/op_lag.c +181 -0
  67. package/csrc/op_lead.c +242 -0
  68. package/csrc/op_normalize.c +510 -0
  69. package/csrc/op_onehot.c +457 -0
  70. package/csrc/op_pivot.c +1754 -0
  71. package/csrc/op_quarantine.c +189 -0
  72. package/csrc/op_registry.c +3044 -0
  73. package/csrc/op_rename.c +129 -0
  74. package/csrc/op_replace.c +354 -0
  75. package/csrc/op_rleid.c +297 -0
  76. package/csrc/op_rowid.c +559 -0
  77. package/csrc/op_sample.c +158 -0
  78. package/csrc/op_schema.c +1341 -0
  79. package/csrc/op_schema_infer.c +252 -0
  80. package/csrc/op_select.c +340 -0
  81. package/csrc/op_set.c +3449 -0
  82. package/csrc/op_skip.c +95 -0
  83. package/csrc/op_sort.c +819 -0
  84. package/csrc/op_source_name.c +120 -0
  85. package/csrc/op_split.c +151 -0
  86. package/csrc/op_split_data.c +119 -0
  87. package/csrc/op_stack.c +271 -0
  88. package/csrc/op_stats.c +875 -0
  89. package/csrc/op_step.c +333 -0
  90. package/csrc/op_tail.c +105 -0
  91. package/csrc/op_tee.c +338 -0
  92. package/csrc/op_top.c +357 -0
  93. package/csrc/op_trim.c +138 -0
  94. package/csrc/op_unique.c +1343 -0
  95. package/csrc/op_unpivot.c +193 -0
  96. package/csrc/op_validate.c +648 -0
  97. package/csrc/op_window.c +591 -0
  98. package/csrc/path_policy.c +85 -0
  99. package/csrc/pipeline.c +1088 -0
  100. package/csrc/recipes.c +104 -0
  101. package/csrc/recipes.h +27 -0
  102. package/csrc/report.c +506 -0
  103. package/csrc/report.h +22 -0
  104. package/csrc/selector.c +1097 -0
  105. package/csrc/size_utils.c +348 -0
  106. package/csrc/spill.c +317 -0
  107. package/csrc/spill.h +21 -0
  108. package/csrc/tranfi.h +291 -0
  109. package/csrc/transform.h +209 -0
  110. package/csrc/transform_api.c +2237 -0
  111. package/csrc/transform_categorical.c +923 -0
  112. package/csrc/transform_internal.h +472 -0
  113. package/csrc/transform_json.c +3812 -0
  114. package/csrc/transform_numeric.c +1966 -0
  115. package/csrc/transform_sha256.c +154 -0
  116. package/csrc/transform_wasm.h +162 -0
  117. package/csrc/transform_wasm_api.c +1373 -0
  118. package/csrc/wasm_api.c +218 -0
  119. package/napi_api.c +534 -0
  120. package/napi_transform.c +1648 -0
  121. package/napi_transform.h +8 -0
  122. package/package.json +64 -59
  123. package/scripts/install-native.js +76 -0
  124. package/scripts/prepack.js +64 -0
  125. package/scripts/sync-csrc.js +23 -0
  126. package/src/cli.js +190 -0
  127. package/src/engines/duckdb.js +142 -0
  128. package/src/index.js +925 -0
  129. package/src/memory_policy.js +411 -0
  130. package/src/native.js +18 -0
  131. package/src/pipeline.js +709 -0
  132. package/src/recipe_json.js +80 -0
  133. package/src/server.js +279 -0
  134. package/src/transform.js +403 -0
  135. package/src/transform_error.js +10 -0
  136. package/src/wasm.js +21 -0
  137. package/wasm/index.js +732 -0
  138. package/wasm/package.json +1 -0
  139. package/wasm/tranfi_core.js +0 -0
  140. package/wasm/transform.js +1156 -0
  141. package/wasm/worker.js +786 -0
  142. package/dist/bundle.js +0 -1
  143. package/index.html +0 -18
  144. package/src/app.css +0 -169
  145. package/src/app.js +0 -203
  146. package/src/app.vue +0 -250
  147. package/src/bulma-input.vue +0 -110
  148. package/src/common-inputs.js +0 -28
  149. package/src/main.js +0 -20
  150. package/src/transforms.js +0 -166
  151. package/webpack.config.js +0 -108
@@ -0,0 +1,709 @@
1
+ /**
2
+ * pipeline.js — Pipeline builder and runner.
3
+ *
4
+ * Tries native N-API addon first, falls back to WASM.
5
+ */
6
+
7
+ const { readFile, writeFile } = require('fs/promises')
8
+ const { createReadStream } = require('fs')
9
+ const { Readable } = require('stream')
10
+ const { createGunzip } = require('zlib')
11
+ const { finished } = require('stream/promises')
12
+ const nativeBinding = require('./native.js')
13
+ const { prepareNativePlan, validateNativeMemoryPolicy } = require('./memory_policy.js')
14
+
15
+ // Channel IDs (match tranfi.h)
16
+ const CHAN_MAIN = 0
17
+ const CHAN_ERRORS = 1
18
+ const CHAN_STATS = 2
19
+ const CHAN_SAMPLES = 3
20
+
21
+ const CHUNK_SIZE = 64 * 1024
22
+
23
+ const HOSTED_AUDIT_PRIVACY_OPS = new Set([
24
+ 'codec.csv.decode',
25
+ 'filter',
26
+ 'validate',
27
+ 'assert',
28
+ 'schema',
29
+ 'json-schema',
30
+ 'fill-null',
31
+ 'replace',
32
+ 'cast',
33
+ 'normalize',
34
+ 'frequency',
35
+ 'tee',
36
+ 'quarantine'
37
+ ])
38
+
39
+ function normalizeSqlDialect(options) {
40
+ let dialect = 'duckdb'
41
+ if (typeof options === 'string') {
42
+ dialect = options
43
+ } else if (options && typeof options === 'object') {
44
+ dialect = options.dialect || options.sqlDialect || dialect
45
+ } else if (options !== undefined && options !== null) {
46
+ throw new TypeError('compileToSql options must be an object or dialect string')
47
+ }
48
+ if (typeof dialect !== 'string') throw new TypeError('SQL dialect must be a string')
49
+ const name = dialect.toLowerCase()
50
+ if (name === 'duckdb') return name
51
+ if (name === 'sqlite' || name === 'postgres') {
52
+ throw new Error(`SQL dialect '${name}' is recognized but not implemented yet; only duckdb lowering is available`)
53
+ }
54
+ throw new Error(`unknown SQL dialect '${dialect}' (expected duckdb, sqlite, or postgres)`)
55
+ }
56
+
57
+ function applyHostedAuditDefaults(planJson) {
58
+ const plan = JSON.parse(planJson)
59
+ if (!plan || !Array.isArray(plan.steps)) return planJson
60
+ let changed = false
61
+ for (const step of plan.steps) {
62
+ if (!step || typeof step !== 'object' || !HOSTED_AUDIT_PRIVACY_OPS.has(step.op)) continue
63
+ if (!step.args || typeof step.args !== 'object' || Array.isArray(step.args)) step.args = {}
64
+ if (!Object.prototype.hasOwnProperty.call(step.args, 'audit_include_row')) {
65
+ step.args.audit_include_row = false
66
+ changed = true
67
+ }
68
+ }
69
+ return changed ? JSON.stringify(plan) : planJson
70
+ }
71
+
72
+ /**
73
+ * Get the backend (native or WASM).
74
+ * Returns an object with: createPipeline, push, finish, pull, free, version
75
+ */
76
+ let _backend = null
77
+
78
+ function effectiveHostPolicy({ allowFs, allowRulesFile = false, workspaceRoot, spillDir, allowBlocking = true } = {}) {
79
+ return {
80
+ allowFs: allowFs === undefined ? Boolean(spillDir) : Boolean(allowFs),
81
+ allowSpill: Boolean(spillDir),
82
+ allowRulesFile: Boolean(allowRulesFile),
83
+ allowBlocking: Boolean(allowBlocking),
84
+ workspaceRoot
85
+ }
86
+ }
87
+
88
+ function planUsesHostPaths(planJson) {
89
+ try {
90
+ const plan = JSON.parse(planJson)
91
+ const steps = Array.isArray(plan.steps) ? plan.steps : []
92
+ return steps.some((step) => {
93
+ const args = step && step.args
94
+ if (!args || typeof args !== 'object') return false
95
+ return ['file', 'rules_file', 'rulesFile', 'spill_dir', 'spillDir'].some((key) =>
96
+ typeof args[key] === 'string' && args[key].length > 0
97
+ )
98
+ })
99
+ } catch (_) {
100
+ return false
101
+ }
102
+ }
103
+
104
+ async function getBackend() {
105
+ if (_backend) return _backend
106
+
107
+ // Try native first
108
+ if (nativeBinding) {
109
+ _backend = nativeBinding
110
+ return _backend
111
+ }
112
+
113
+ // Fall back to WASM
114
+ const { loadWasm } = require('./wasm.js')
115
+ const wasm = await loadWasm()
116
+
117
+ _backend = {
118
+ isWasm: true,
119
+ compileDsl(dsl) {
120
+ const len = wasm.lengthBytesUTF8(dsl)
121
+ const ptr = wasm._malloc(len + 1)
122
+ wasm.stringToUTF8(dsl, ptr, len + 1)
123
+ const resultPtr = wasm.ccall('wasm_compile_dsl', 'number', ['number', 'number'], [ptr, len])
124
+ wasm._free(ptr)
125
+ if (!resultPtr) {
126
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [-1])
127
+ throw new Error(`DSL parse failed: ${err || 'unknown error'}`)
128
+ }
129
+ const json = wasm.UTF8ToString(resultPtr)
130
+ wasm._free(resultPtr)
131
+ return json
132
+ },
133
+ createPipeline(planJson, options = {}) {
134
+ const len = wasm.lengthBytesUTF8(planJson)
135
+ const ptr = wasm._malloc(len + 1)
136
+ wasm.stringToUTF8(planJson, ptr, len + 1)
137
+ let rootPtr = 0
138
+ if (options.workspaceRoot) {
139
+ const root = String(options.workspaceRoot)
140
+ const rootLen = wasm.lengthBytesUTF8(root)
141
+ rootPtr = wasm._malloc(rootLen + 1)
142
+ wasm.stringToUTF8(root, rootPtr, rootLen + 1)
143
+ }
144
+ const hasPolicyCreate = typeof wasm._wasm_pipeline_create_with_policy === 'function'
145
+ const needsPolicyCreate = options.allowFs || options.allowSpill || options.allowRulesFile || rootPtr || planUsesHostPaths(planJson)
146
+ let handle = -1
147
+ if (hasPolicyCreate) {
148
+ handle = wasm.ccall('wasm_pipeline_create_with_policy', 'number',
149
+ ['number', 'number', 'number', 'number', 'number', 'number'],
150
+ [ptr, len, options.allowFs ? 1 : 0, options.allowSpill ? 1 : 0,
151
+ options.allowRulesFile ? 1 : 0, rootPtr])
152
+ } else if (!needsPolicyCreate) {
153
+ handle = wasm.ccall('wasm_pipeline_create', 'number', ['number', 'number'], [ptr, len])
154
+ }
155
+ wasm._free(ptr)
156
+ if (rootPtr) wasm._free(rootPtr)
157
+ if (!hasPolicyCreate && needsPolicyCreate) {
158
+ throw new Error('Failed to create pipeline: WASM host policy export unavailable for this plan; rebuild tranfi_core.js')
159
+ }
160
+ if (handle < 0) {
161
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
162
+ throw new Error(`Failed to create pipeline: ${err || 'unknown error'}`)
163
+ }
164
+ return handle
165
+ },
166
+ push(handle, buffer) {
167
+ const len = buffer.length
168
+ const ptr = wasm._malloc(len)
169
+ wasm.HEAPU8.set(buffer, ptr)
170
+ const rc = wasm.ccall('wasm_pipeline_push', 'number', ['number', 'number', 'number'], [handle, ptr, len])
171
+ wasm._free(ptr)
172
+ if (rc !== 0) {
173
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
174
+ throw new Error(`Push failed: ${err || 'unknown error'}`)
175
+ }
176
+ },
177
+ flushInput(handle) {
178
+ const rc = wasm.ccall('wasm_pipeline_flush_input', 'number', ['number'], [handle])
179
+ if (rc !== 0) {
180
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
181
+ throw new Error(`Input flush failed: ${err || 'unknown error'}`)
182
+ }
183
+ },
184
+ setSourceName(handle, name) {
185
+ const source = name == null ? '' : String(name)
186
+ const len = wasm.lengthBytesUTF8(source)
187
+ const ptr = wasm._malloc(len + 1)
188
+ wasm.stringToUTF8(source, ptr, len + 1)
189
+ const rc = wasm.ccall('wasm_pipeline_set_source_name', 'number', ['number', 'number'], [handle, ptr])
190
+ wasm._free(ptr)
191
+ if (rc !== 0) {
192
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
193
+ throw new Error(`Set source name failed: ${err || 'unknown error'}`)
194
+ }
195
+ },
196
+ finish(handle) {
197
+ const rc = wasm.ccall('wasm_pipeline_finish', 'number', ['number'], [handle])
198
+ if (rc !== 0) {
199
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
200
+ throw new Error(`Finish failed: ${err || 'unknown error'}`)
201
+ }
202
+ },
203
+ finishStep(handle) {
204
+ const rc = wasm.ccall('wasm_pipeline_finish_step', 'number', ['number'], [handle])
205
+ if (rc < 0) {
206
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [handle])
207
+ throw new Error(`Finish failed: ${err || 'unknown error'}`)
208
+ }
209
+ return rc === 1
210
+ },
211
+ pull(handle, channel) {
212
+ const bufSize = 65536
213
+ const ptr = wasm._malloc(bufSize)
214
+ const chunks = []
215
+ for (;;) {
216
+ const n = wasm.ccall('wasm_pipeline_pull', 'number',
217
+ ['number', 'number', 'number', 'number'], [handle, channel, ptr, bufSize])
218
+ if (n === 0) break
219
+ chunks.push(new Uint8Array(wasm.HEAPU8.buffer, ptr, n).slice())
220
+ }
221
+ wasm._free(ptr)
222
+ // Concatenate
223
+ const total = chunks.reduce((s, c) => s + c.length, 0)
224
+ const result = Buffer.alloc(total)
225
+ let offset = 0
226
+ for (const c of chunks) {
227
+ result.set(c, offset)
228
+ offset += c.length
229
+ }
230
+ return result
231
+ },
232
+ pullChunk(handle, channel, chunkSize = 65536) {
233
+ if (chunkSize <= 0) throw new Error('chunkSize must be positive')
234
+ const ptr = wasm._malloc(chunkSize)
235
+ const n = wasm.ccall('wasm_pipeline_pull', 'number',
236
+ ['number', 'number', 'number', 'number'], [handle, channel, ptr, chunkSize])
237
+ const result = Buffer.from(new Uint8Array(wasm.HEAPU8.buffer, ptr, n))
238
+ wasm._free(ptr)
239
+ return result
240
+ },
241
+ free(handle) {
242
+ wasm.ccall('wasm_pipeline_free', null, ['number'], [handle])
243
+ },
244
+ version() {
245
+ return wasm.ccall('wasm_version', 'string', [], [])
246
+ },
247
+ recipeCount() {
248
+ return wasm.ccall('wasm_recipe_count', 'number', [], [])
249
+ },
250
+ recipeName(index) {
251
+ return wasm.ccall('wasm_recipe_name', 'string', ['number'], [index])
252
+ },
253
+ recipeDsl(index) {
254
+ return wasm.ccall('wasm_recipe_dsl', 'string', ['number'], [index])
255
+ },
256
+ recipeDescription(index) {
257
+ return wasm.ccall('wasm_recipe_description', 'string', ['number'], [index])
258
+ },
259
+ recipeFindDsl(name) {
260
+ const len = wasm.lengthBytesUTF8(name)
261
+ const ptr = wasm._malloc(len + 1)
262
+ wasm.stringToUTF8(name, ptr, len + 1)
263
+ const result = wasm.ccall('wasm_recipe_find_dsl', 'string', ['number'], [ptr])
264
+ wasm._free(ptr)
265
+ return result || null
266
+ },
267
+ compileToSql(dsl) {
268
+ const len = wasm.lengthBytesUTF8(dsl)
269
+ const ptr = wasm._malloc(len + 1)
270
+ wasm.stringToUTF8(dsl, ptr, len + 1)
271
+ const resultPtr = wasm.ccall('wasm_compile_to_sql', 'number', ['number', 'number'], [ptr, len])
272
+ wasm._free(ptr)
273
+ if (!resultPtr) {
274
+ const err = wasm.ccall('wasm_pipeline_error', 'string', ['number'], [-1])
275
+ throw new Error(`SQL compile failed: ${err || 'unknown error'}`)
276
+ }
277
+ const sql = wasm.UTF8ToString(resultPtr)
278
+ wasm._free(resultPtr)
279
+ return sql
280
+ }
281
+ }
282
+
283
+ return _backend
284
+ }
285
+
286
+
287
+ function asInputBuffer(input) {
288
+ if (Buffer.isBuffer(input)) return input
289
+ if (typeof input === 'string') return Buffer.from(input, 'utf-8')
290
+ if (input instanceof ArrayBuffer) return Buffer.from(new Uint8Array(input))
291
+ return Buffer.from(input)
292
+ }
293
+
294
+ function hasInput(value) {
295
+ return value !== undefined && value !== null
296
+ }
297
+
298
+ function assertSingleInputSource({ input, inputFile, inputFiles, inputStream }) {
299
+ const count = (hasInput(input) ? 1 : 0) + (hasInput(inputFile) ? 1 : 0) + (hasInput(inputFiles) ? 1 : 0) + (hasInput(inputStream) ? 1 : 0)
300
+ if (count > 1) throw new Error('pass only one of input, inputFile, inputFiles, or inputStream')
301
+ }
302
+
303
+ function normalizeInputFiles(inputFiles) {
304
+ if (!hasInput(inputFiles)) return null
305
+ if (typeof inputFiles === 'string' || Buffer.isBuffer(inputFiles)) {
306
+ throw new TypeError('inputFiles must be an iterable of paths, not a single path')
307
+ }
308
+ if (typeof inputFiles[Symbol.iterator] !== 'function') {
309
+ throw new TypeError('inputFiles must be an iterable of paths')
310
+ }
311
+ const files = Array.from(inputFiles)
312
+ if (files.length === 0) throw new Error('inputFiles must not be empty')
313
+ return files
314
+ }
315
+
316
+ function injectSourceNameStep(planJson, sourceColumn) {
317
+ if (sourceColumn === undefined || sourceColumn === null) return planJson
318
+ const result = String(sourceColumn)
319
+ if (!result) throw new Error('sourceColumn must be a non-empty column name')
320
+ const plan = JSON.parse(planJson)
321
+ if (!plan || !Array.isArray(plan.steps)) throw new Error('plan must contain a steps array')
322
+ let insertAt = 0
323
+ if (plan.steps.length > 0) {
324
+ const op = plan.steps[0] && plan.steps[0].op
325
+ if (typeof op === 'string' && op.startsWith('codec.') && op.endsWith('.decode')) insertAt = 1
326
+ }
327
+ plan.steps.splice(insertAt, 0, { op: 'source-name', args: { result } })
328
+ return JSON.stringify(plan)
329
+ }
330
+
331
+ function normalizeCompression(compression = 'auto') {
332
+ const mode = compression == null ? 'auto' : String(compression).toLowerCase()
333
+ if (!['auto', 'none', 'gzip'].includes(mode)) {
334
+ throw new Error("compression must be 'auto', 'none', or 'gzip'")
335
+ }
336
+ return mode
337
+ }
338
+
339
+ function resolveFileCompression(inputFile, compression) {
340
+ const mode = normalizeCompression(compression)
341
+ if (mode === 'auto') {
342
+ return String(inputFile).toLowerCase().endsWith('.gz') ? 'gzip' : 'none'
343
+ }
344
+ return mode
345
+ }
346
+
347
+ async function *yieldChunkItems(chunk, chunkSize, sourceName) {
348
+ const buf = asInputBuffer(chunk)
349
+ for (let i = 0; i < buf.length; i += chunkSize) {
350
+ yield { chunk: buf.subarray(i, Math.min(i + chunkSize, buf.length)), sourceName }
351
+ }
352
+ }
353
+
354
+ async function *yieldFileItems(inputFile, chunkSize, requestedCompression) {
355
+ const sourceName = String(inputFile)
356
+ const fileCompression = resolveFileCompression(inputFile, requestedCompression)
357
+ let stream = createReadStream(inputFile, { highWaterMark: chunkSize })
358
+ if (fileCompression === 'gzip') stream = stream.pipe(createGunzip())
359
+ for await (const chunk of stream) {
360
+ yield * yieldChunkItems(chunk, chunkSize, sourceName)
361
+ }
362
+ yield { flushInput: true, sourceName }
363
+ }
364
+
365
+ async function *iterInputChunks({ input, inputFile, inputFiles, inputStream, chunkSize, compression = 'auto' }) {
366
+ const files = normalizeInputFiles(inputFiles)
367
+ assertSingleInputSource({ input, inputFile, inputFiles: files, inputStream })
368
+ const requestedCompression = normalizeCompression(compression)
369
+
370
+ if (files) {
371
+ for (const file of files) yield * yieldFileItems(file, chunkSize, requestedCompression)
372
+ return
373
+ }
374
+
375
+ if (hasInput(inputFile)) {
376
+ yield * yieldFileItems(inputFile, chunkSize, requestedCompression)
377
+ return
378
+ }
379
+
380
+ if (hasInput(inputStream)) {
381
+ const isIterable = typeof inputStream[Symbol.asyncIterator] === 'function' || typeof inputStream[Symbol.iterator] === 'function'
382
+ if (!isIterable || typeof inputStream.pipe !== 'function' && requestedCompression === 'gzip') {
383
+ throw new TypeError('inputStream must be a Node.js stream or an iterable of chunks')
384
+ }
385
+ let stream = inputStream
386
+ if (requestedCompression === 'gzip') stream = inputStream.pipe(createGunzip())
387
+ for await (const chunk of stream) {
388
+ yield * yieldChunkItems(chunk, chunkSize)
389
+ }
390
+ return
391
+ }
392
+
393
+ if (requestedCompression === 'gzip') throw new Error('compression=gzip requires inputFile, inputFiles, or inputStream')
394
+
395
+ if (hasInput(input)) {
396
+ const buf = asInputBuffer(input)
397
+ for (let i = 0; i < buf.length; i += chunkSize) {
398
+ yield { chunk: buf.subarray(i, Math.min(i + chunkSize, buf.length)) }
399
+ }
400
+ }
401
+ }
402
+
403
+ function concatBuffers(chunks) {
404
+ if (chunks.length === 0) return Buffer.alloc(0)
405
+ return Buffer.concat(chunks)
406
+ }
407
+
408
+ function waitForWritableDrain(writable) {
409
+ return new Promise((resolve, reject) => {
410
+ function cleanup() {
411
+ writable.off('drain', onDrain)
412
+ writable.off('error', onError)
413
+ }
414
+ function onDrain() {
415
+ cleanup()
416
+ resolve()
417
+ }
418
+ function onError(err) {
419
+ cleanup()
420
+ reject(err)
421
+ }
422
+ writable.once('drain', onDrain)
423
+ writable.once('error', onError)
424
+ })
425
+ }
426
+
427
+ async function writeWritableChunk(writable, chunk) {
428
+ if (chunk.length === 0) return
429
+ if (!writable.write(chunk)) await waitForWritableDrain(writable)
430
+ }
431
+
432
+ function pullOneChunk(backend, handle, channel, chunkSize) {
433
+ if (typeof backend.pullChunk === 'function') {
434
+ return Buffer.from(backend.pullChunk(handle, channel, chunkSize))
435
+ }
436
+ return Buffer.from(backend.pull(handle, channel))
437
+ }
438
+
439
+ async function drainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput) {
440
+ for (;;) {
441
+ const chunk = pullOneChunk(backend, handle, CHAN_MAIN, chunkSize)
442
+ if (chunk.length === 0) break
443
+ if (onOutput) await onOutput(chunk)
444
+ if (collectOutput) outputChunks.push(chunk)
445
+ }
446
+ }
447
+
448
+ async function finishAndDrainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput) {
449
+ if (typeof backend.finishStep === 'function') {
450
+ for (;;) {
451
+ const done = backend.finishStep(handle)
452
+ await drainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput)
453
+ if (done) break
454
+ }
455
+ } else {
456
+ backend.finish(handle)
457
+ await drainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput)
458
+ }
459
+ }
460
+
461
+ class PipelineResult {
462
+ constructor(output, errors, stats, samples) {
463
+ this.output = output
464
+ this.errors = errors
465
+ this.stats = stats
466
+ this.samples = samples
467
+ }
468
+
469
+ get outputText() {
470
+ return this.output.toString('utf-8')
471
+ }
472
+
473
+ get statsText() {
474
+ return this.stats.toString('utf-8')
475
+ }
476
+
477
+ toString() {
478
+ return `PipelineResult(output=${this.output.length} bytes, errors=${this.errors.length} bytes, stats=${this.stats.length} bytes)`
479
+ }
480
+ }
481
+
482
+
483
+ class Pipeline {
484
+ constructor(steps, { planJson, engine } = {}) {
485
+ this._engine = engine || null
486
+ if (planJson) {
487
+ this._planJson = planJson
488
+ this._dsl = null
489
+ this._steps = null
490
+ } else if (typeof steps === 'string') {
491
+ this._dsl = steps
492
+ this._steps = null
493
+ this._planJson = null
494
+ } else {
495
+ this._dsl = null
496
+ this._steps = steps
497
+ this._planJson = null
498
+ }
499
+ }
500
+
501
+ async _toPlanJson(backend) {
502
+ if (this._planJson) return this._planJson
503
+ if (this._dsl) {
504
+ // Check if it's a built-in recipe name (no pipes, no braces)
505
+ const s = this._dsl.trim()
506
+ if (!s.includes('|') && !s.startsWith('{')) {
507
+ const recipeDsl = backend.recipeFindDsl(s)
508
+ if (recipeDsl) return backend.compileDsl(recipeDsl)
509
+ }
510
+ return backend.compileDsl(this._dsl)
511
+ }
512
+ return JSON.stringify({ steps: this._steps })
513
+ }
514
+
515
+ async run({ input, inputFile, inputFiles, inputStream, sourceColumn, onOutput, collectOutput = true, chunkSize = CHUNK_SIZE, allowBlocking = false, memory, spillDir, allowFs, allowRulesFile = false, workspaceRoot, compression = 'auto', hostedAuditDefaults = false } = {}) {
516
+ if (chunkSize <= 0) throw new Error('chunkSize must be positive')
517
+ if (sourceColumn !== undefined && sourceColumn !== null && !hasInput(inputFile) && !hasInput(inputFiles)) {
518
+ throw new Error('sourceColumn requires inputFile or inputFiles')
519
+ }
520
+ if (onOutput !== undefined && typeof onOutput !== 'function') {
521
+ throw new TypeError('onOutput must be a function')
522
+ }
523
+ if (this._engine && this._engine !== 'native') {
524
+ if (onOutput || collectOutput === false || hasInput(inputStream) || hasInput(inputFiles) || sourceColumn !== undefined && sourceColumn !== null) {
525
+ throw new Error('streaming input/output, inputFiles, and sourceColumn require the native or WASM backend')
526
+ }
527
+ return this._runEngine({ input, inputFile, memory, spillDir })
528
+ }
529
+
530
+ const backend = await getBackend()
531
+ if (spillDir && backend.isWasm) {
532
+ throw new Error('spillDir is not supported by WASM native execution because browser/WASM spill storage would occupy WASM memory; use the Node native addon, CLI/direct C, or DuckDB')
533
+ }
534
+ let planJson = await this._toPlanJson(backend)
535
+ planJson = injectSourceNameStep(planJson, sourceColumn)
536
+ if (hostedAuditDefaults) planJson = applyHostedAuditDefaults(planJson)
537
+ planJson = prepareNativePlan(planJson, { allowBlocking, memory, spillDir })
538
+ const handle = backend.createPipeline(planJson, effectiveHostPolicy({ allowFs, allowRulesFile, workspaceRoot, spillDir, allowBlocking: true }))
539
+ const outputChunks = []
540
+
541
+ try {
542
+ for await (const item of iterInputChunks({ input, inputFile, inputFiles, inputStream, chunkSize, compression })) {
543
+ if (item.sourceName !== undefined && typeof backend.setSourceName === 'function') backend.setSourceName(handle, item.sourceName)
544
+ if (item.flushInput) {
545
+ if (typeof backend.flushInput !== 'function') throw new Error('inputFiles require backend.flushInput')
546
+ backend.flushInput(handle)
547
+ await drainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput)
548
+ continue
549
+ }
550
+ backend.push(handle, item.chunk)
551
+ await drainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput)
552
+ }
553
+
554
+ await finishAndDrainMain(backend, handle, chunkSize, outputChunks, onOutput, collectOutput)
555
+
556
+ const output = collectOutput ? concatBuffers(outputChunks) : Buffer.alloc(0)
557
+ const errors = backend.pull(handle, CHAN_ERRORS)
558
+ const stats = backend.pull(handle, CHAN_STATS)
559
+ const samples = backend.pull(handle, CHAN_SAMPLES)
560
+
561
+ return new PipelineResult(output, errors, stats, samples)
562
+ } finally {
563
+ backend.free(handle)
564
+ }
565
+ }
566
+
567
+ async *iterChunks({ input, inputFile, inputFiles, inputStream, sourceColumn, chunkSize = CHUNK_SIZE, allowBlocking = false, memory, spillDir, allowFs, allowRulesFile = false, workspaceRoot, compression = 'auto' } = {}) {
568
+ if (chunkSize <= 0) throw new Error('chunkSize must be positive')
569
+ if (sourceColumn !== undefined && sourceColumn !== null && !hasInput(inputFile) && !hasInput(inputFiles)) {
570
+ throw new Error('sourceColumn requires inputFile or inputFiles')
571
+ }
572
+ if (this._engine && this._engine !== 'native') {
573
+ throw new Error('streaming chunk iteration requires the native or WASM backend')
574
+ }
575
+
576
+ const backend = await getBackend()
577
+ if (spillDir && backend.isWasm) {
578
+ throw new Error('spillDir is not supported by WASM native execution because browser/WASM spill storage would occupy WASM memory; use the Node native addon, CLI/direct C, or DuckDB')
579
+ }
580
+ let planJson = await this._toPlanJson(backend)
581
+ planJson = injectSourceNameStep(planJson, sourceColumn)
582
+ planJson = prepareNativePlan(planJson, { allowBlocking, memory, spillDir })
583
+ const handle = backend.createPipeline(planJson, effectiveHostPolicy({ allowFs, allowRulesFile, workspaceRoot, spillDir, allowBlocking: true }))
584
+
585
+ try {
586
+ for await (const item of iterInputChunks({ input, inputFile, inputFiles, inputStream, chunkSize, compression })) {
587
+ if (item.sourceName !== undefined && typeof backend.setSourceName === 'function') backend.setSourceName(handle, item.sourceName)
588
+ if (item.flushInput) {
589
+ if (typeof backend.flushInput !== 'function') throw new Error('inputFiles require backend.flushInput')
590
+ backend.flushInput(handle)
591
+ } else {
592
+ backend.push(handle, item.chunk)
593
+ }
594
+ for (;;) {
595
+ const out = pullOneChunk(backend, handle, CHAN_MAIN, chunkSize)
596
+ if (out.length === 0) break
597
+ yield out
598
+ }
599
+ }
600
+
601
+ if (typeof backend.finishStep === 'function') {
602
+ for (;;) {
603
+ const done = backend.finishStep(handle)
604
+ for (;;) {
605
+ const out = pullOneChunk(backend, handle, CHAN_MAIN, chunkSize)
606
+ if (out.length === 0) break
607
+ yield out
608
+ }
609
+ if (done) break
610
+ }
611
+ } else {
612
+ backend.finish(handle)
613
+ for (;;) {
614
+ const out = pullOneChunk(backend, handle, CHAN_MAIN, chunkSize)
615
+ if (out.length === 0) break
616
+ yield out
617
+ }
618
+ }
619
+ } finally {
620
+ backend.free(handle)
621
+ }
622
+ }
623
+
624
+ toReadable({ highWaterMark, ...options } = {}) {
625
+ const streamOptions = { objectMode: false }
626
+ if (highWaterMark !== undefined) streamOptions.highWaterMark = highWaterMark
627
+ return Readable.from(this.iterChunks(options), streamOptions)
628
+ }
629
+
630
+ async writeTo(writable, { end = true, ...options } = {}) {
631
+ if (!writable || typeof writable.write !== 'function') {
632
+ throw new TypeError('writable must be a Node.js Writable stream')
633
+ }
634
+ const result = await this.run({
635
+ ...options,
636
+ collectOutput: false,
637
+ onOutput: async chunk => {
638
+ await writeWritableChunk(writable, chunk)
639
+ }
640
+ })
641
+ if (end && typeof writable.end === 'function') {
642
+ writable.end()
643
+ await finished(writable)
644
+ }
645
+ return result
646
+ }
647
+
648
+ async _runEngine({ input, inputFile, inputFiles, sourceColumn, memory, spillDir } = {}) {
649
+ if (hasInput(inputFiles) || sourceColumn !== undefined && sourceColumn !== null) {
650
+ throw new Error('inputFiles and sourceColumn require the native or WASM backend')
651
+ }
652
+ const { getEngine } = require('./engines/duckdb.js')
653
+ const engine = getEngine(this._engine)
654
+ const dsl = this._dsl
655
+ if (!dsl) throw new Error('DuckDB engine requires a DSL string pipeline')
656
+ return engine.run(dsl, { input, inputFile, memory, spillDir })
657
+ }
658
+ }
659
+
660
+ async function compileDsl(dsl) {
661
+ const backend = await getBackend()
662
+ return backend.compileDsl(dsl)
663
+ }
664
+
665
+ async function compileToSql(dsl, options) {
666
+ normalizeSqlDialect(options)
667
+ const backend = await getBackend()
668
+ return backend.compileToSql(dsl)
669
+ }
670
+
671
+ async function loadRecipe(source) {
672
+ let planJson
673
+ if (typeof source === 'object' && source.steps) {
674
+ planJson = JSON.stringify(source)
675
+ } else if (typeof source === 'string' && source.trimStart().startsWith('{')) {
676
+ planJson = source
677
+ } else {
678
+ planJson = await readFile(source, 'utf-8')
679
+ }
680
+ return new Pipeline(null, { planJson })
681
+ }
682
+
683
+ async function saveRecipe(steps, path) {
684
+ await writeFile(path, JSON.stringify({ steps }))
685
+ }
686
+
687
+ async function recipes() {
688
+ const backend = await getBackend()
689
+ const n = backend.recipeCount()
690
+ const result = []
691
+ for (let i = 0; i < n; i++) {
692
+ result.push({
693
+ name: backend.recipeName(i),
694
+ dsl: backend.recipeDsl(i),
695
+ description: backend.recipeDescription(i)
696
+ })
697
+ }
698
+ return result
699
+ }
700
+
701
+ module.exports = {
702
+ Pipeline,
703
+ PipelineResult,
704
+ compileDsl,
705
+ compileToSql,
706
+ loadRecipe,
707
+ saveRecipe,
708
+ recipes
709
+ }