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,8 @@
1
+ #ifndef TRANFI_NAPI_TRANSFORM_H
2
+ #define TRANFI_NAPI_TRANSFORM_H
3
+
4
+ #include <node_api.h>
5
+
6
+ napi_status tranfi_napi_define_transform(napi_env env, napi_value exports);
7
+
8
+ #endif
package/package.json CHANGED
@@ -1,68 +1,73 @@
1
1
  {
2
2
  "name": "tranfi",
3
- "version": "0.0.2",
4
- "description": "Stream-based tool for file processing",
5
- "main": "src/main.js",
3
+ "version": "0.2.0",
4
+ "description": "Streaming ETL language + runtime",
5
+ "type": "commonjs",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "tranfi": "src/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "require": "./src/index.js",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./wasm": {
16
+ "require": "./wasm/index.js",
17
+ "default": "./wasm/index.js"
18
+ },
19
+ "./wasm/worker": {
20
+ "require": "./wasm/worker.js",
21
+ "default": "./wasm/worker.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "src/",
26
+ "wasm/",
27
+ "csrc/",
28
+ "app/",
29
+ "napi_api.c",
30
+ "napi_transform.c",
31
+ "napi_transform.h",
32
+ "binding.gyp",
33
+ "scripts/",
34
+ "README.md",
35
+ "LICENSE",
36
+ "NOTICE"
37
+ ],
6
38
  "scripts": {
7
- "build-dev": "webpack --mode=development --progress --stats-children --env DEVELOPMENT",
8
- "build": "webpack --mode=production --progress && webpack --mode=production --progress --env RUNTIME",
9
- "watch": "nodemon --watch . --ignore dist --ext vue,js,css,html --exec 'npm run build-dev'",
10
- "prepublishOnly": "npm run build",
11
- "test": "jest test.js --detectOpenHandles",
12
- "test-head": "HEADLESS=false npm test"
39
+ "prepack": "node scripts/prepack.js",
40
+ "install": "node scripts/install-native.js",
41
+ "build:native": "node scripts/sync-csrc.js && node-gyp rebuild",
42
+ "test": "node --test ../test/test_install_native.js && node ../test/test_node.js && node ../test/test_transform_node.js && node ../test/test_transform_wasm_node.js && node ../test/test_transform_worker_node.js"
43
+ },
44
+ "peerDependencies": {
45
+ "duckdb": ">=1.0.0",
46
+ "@duckdb/duckdb-wasm": ">=1.0.0"
13
47
  },
14
- "author": "Anton Zemlyansky",
15
- "license": "MIT",
48
+ "peerDependenciesMeta": {
49
+ "duckdb": {
50
+ "optional": true
51
+ },
52
+ "@duckdb/duckdb-wasm": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "keywords": [
57
+ "etl",
58
+ "csv",
59
+ "streaming",
60
+ "data",
61
+ "pipeline",
62
+ "transform"
63
+ ],
64
+ "license": "Apache-2.0",
16
65
  "repository": {
17
66
  "type": "git",
18
- "url": "git+https://github.com/tranfi/tranfi"
19
- },
20
- "dependencies": {
21
- "@jseeio/jsee": "^0.2.8",
22
- "buffer": "^6.0.3",
23
- "bulma": "^0.9.3",
24
- "csv-parse": "^4.16.3",
25
- "element-plus": "^1.2.0-beta.6",
26
- "equal-vue": "^0.79.3",
27
- "file-saver": "^2.0.2",
28
- "filestream": "^5.0.0",
29
- "filtrex": "^2.2.3",
30
- "JSONStream": "^1.3.5",
31
- "notyf": "^3.10.0",
32
- "process": "^0.11.10",
33
- "stream-browserify": "^3.0.0",
34
- "stream-http": "^3.2.0",
35
- "through2": "^4.0.2",
36
- "unzip-stream": "^0.3.1",
37
- "url": "^0.11.0",
38
- "vue": "^3.2.23",
39
- "vue-style-loader": "^4.1.3",
40
- "vue3-json-viewer": "^1.0.4",
41
- "vuedraggable": "^4.1.0",
42
- "vuex": "^4.0.2"
67
+ "url": "https://github.com/tranfi/tranfi"
43
68
  },
44
69
  "devDependencies": {
45
- "@babel/core": "^7.16.5",
46
- "@babel/plugin-transform-runtime": "^7.4.4",
47
- "@babel/preset-env": "^7.16.5",
48
- "babel-loader": "^8.2.3",
49
- "css-loader": "^6.5.1",
50
- "http-server": "^14.0.0",
51
- "jest": "^27.4.5",
52
- "jest-puppeteer": "^6.0.3",
53
- "node-sass": "^7.0.0",
54
- "nodemon": "^2.0.15",
55
- "puppeteer": "^1.20.0",
56
- "sass-loader": "^12.4.0",
57
- "source-map-loader": "^3.0.0",
58
- "style-loader": "^3.3.1",
59
- "tape": "^4.9.1",
60
- "terser-webpack-plugin": "^5.3.0",
61
- "uglify-es": "^3.3.9",
62
- "vue-loader": "^17.0.0",
63
- "vue-template-compiler": "^2.6.14",
64
- "webpack": "^5.65.0",
65
- "webpack-cli": "^4.9.1",
66
- "worker-loader": "^3.0.8"
67
- }
70
+ "duckdb": "^1.4.4"
71
+ },
72
+ "author": "Anton Zemlyansky"
68
73
  }
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const { spawnSync } = require('child_process')
4
+ const path = require('path')
5
+
6
+ function describeFailure(label, result, logger) {
7
+ if (result && result.error) {
8
+ logger.error(`tranfi: ${label} failed: ${result.error.message}`)
9
+ } else if (result && result.signal) {
10
+ logger.error(`tranfi: ${label} terminated by signal ${result.signal}`)
11
+ } else {
12
+ logger.error(`tranfi: ${label} exited with status ${result ? result.status : 'unknown'}`)
13
+ }
14
+ }
15
+
16
+ function successful(result) {
17
+ return Boolean(result) && result.error === undefined && result.signal === null
18
+ && result.status === 0
19
+ }
20
+
21
+ function installNative({
22
+ env = process.env,
23
+ execPath = process.execPath,
24
+ platform = process.platform,
25
+ spawn = spawnSync,
26
+ logger = console,
27
+ packageRoot = path.resolve(__dirname, '..')
28
+ } = {}) {
29
+ if (env.TRANFI_SKIP_NATIVE_BUILD === '1') {
30
+ logger.warn(
31
+ 'tranfi: skipping the native addon because TRANFI_SKIP_NATIVE_BUILD=1; ' +
32
+ "use require('tranfi/wasm') explicitly because prepared transforms on " +
33
+ "require('tranfi') need the native addon"
34
+ )
35
+ return 0
36
+ }
37
+
38
+ const syncResult = spawn(execPath, [path.join(__dirname, 'sync-csrc.js')], {
39
+ cwd: packageRoot,
40
+ stdio: 'inherit'
41
+ })
42
+ if (!successful(syncResult)) {
43
+ describeFailure('C-source synchronization', syncResult, logger)
44
+ return Number.isInteger(syncResult && syncResult.status) && syncResult.status !== 0
45
+ ? syncResult.status
46
+ : 1
47
+ }
48
+
49
+ const nodeGyp = env.npm_config_node_gyp || 'node-gyp'
50
+ const nodeGypIsScript = /\.[cm]?js$/i.test(nodeGyp)
51
+ const buildCommand = nodeGypIsScript ? execPath : nodeGyp
52
+ const buildArgs = nodeGypIsScript ? [nodeGyp, 'rebuild'] : ['rebuild']
53
+ const buildResult = spawn(buildCommand, buildArgs, {
54
+ cwd: packageRoot,
55
+ stdio: 'inherit',
56
+ shell: platform === 'win32' && !nodeGypIsScript
57
+ })
58
+ if (!successful(buildResult)) {
59
+ describeFailure('native addon build', buildResult, logger)
60
+ logger.error(
61
+ 'tranfi: installation requires a working native toolchain for the default ' +
62
+ "Node entry; set TRANFI_SKIP_NATIVE_BUILD=1 only for explicit 'tranfi/wasm' use"
63
+ )
64
+ return Number.isInteger(buildResult && buildResult.status) && buildResult.status !== 0
65
+ ? buildResult.status
66
+ : 1
67
+ }
68
+
69
+ return 0
70
+ }
71
+
72
+ if (require.main === module) {
73
+ process.exitCode = installNative()
74
+ }
75
+
76
+ module.exports = { installNative }
@@ -0,0 +1,64 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { spawnSync } = require('child_process')
6
+
7
+ const scriptsDir = __dirname
8
+ const pkgDir = path.resolve(scriptsDir, '..')
9
+ const repoDir = path.resolve(pkgDir, '..')
10
+
11
+ function run(cmd, args, cwd, extraEnv = {}) {
12
+ const result = spawnSync(cmd, args, {
13
+ cwd,
14
+ stdio: 'inherit',
15
+ env: { ...process.env, ...extraEnv }
16
+ })
17
+ if (result.status !== 0) {
18
+ process.exit(result.status || 1)
19
+ }
20
+ }
21
+
22
+ run(process.execPath, [path.join(scriptsDir, 'sync-csrc.js')], pkgDir)
23
+
24
+ const wasmFile = path.join(pkgDir, 'wasm', 'tranfi_core.js')
25
+ if (!fs.existsSync(wasmFile)) {
26
+ const env = { EM_CACHE: process.env.EM_CACHE || path.join(repoDir, 'build-wasm', '.emcache') }
27
+ if (!process.env.EMSDK_PYTHON && fs.existsSync('/usr/bin/python3')) {
28
+ env.EMSDK_PYTHON = '/usr/bin/python3'
29
+ }
30
+ run('bash', [path.join(repoDir, 'scripts', 'build-wasm.sh')], repoDir, env)
31
+ }
32
+
33
+ const appDir = path.join(repoDir, 'app')
34
+ const appPublicWasmDir = path.join(appDir, 'public', 'wasm')
35
+ const appSource = path.join(appDir, 'dist')
36
+ const appDest = path.join(pkgDir, 'app')
37
+
38
+ fs.mkdirSync(appPublicWasmDir, { recursive: true })
39
+ fs.copyFileSync(wasmFile, path.join(appPublicWasmDir, 'tranfi_core.js'))
40
+
41
+ if (fs.existsSync(path.join(appDir, 'package.json'))) {
42
+ run('npm', ['run', 'build'], appDir)
43
+ }
44
+
45
+ if (fs.existsSync(appSource)) {
46
+ fs.rmSync(appDest, { recursive: true, force: true })
47
+ fs.cpSync(appSource, appDest, { recursive: true })
48
+ for (const extra of ['wasm', 'lib']) {
49
+ fs.rmSync(path.join(appDest, extra), { recursive: true, force: true })
50
+ }
51
+ }
52
+
53
+ const required = [
54
+ path.join(pkgDir, 'csrc'),
55
+ path.join(pkgDir, 'wasm', 'tranfi_core.js'),
56
+ path.join(pkgDir, 'app', 'index.html')
57
+ ]
58
+
59
+ for (const target of required) {
60
+ if (!fs.existsSync(target)) {
61
+ console.error('prepack: missing required publish artifact: ' + target)
62
+ process.exit(1)
63
+ }
64
+ }
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const pkgDir = path.resolve(__dirname, '..')
7
+ const repoDir = path.resolve(pkgDir, '..')
8
+ const srcDir = path.join(repoDir, 'src')
9
+ const outDir = path.join(pkgDir, 'csrc')
10
+
11
+ if (!fs.existsSync(srcDir)) {
12
+ console.log('sync-csrc: src/ not found (published package), skipping')
13
+ process.exit(0)
14
+ }
15
+
16
+ fs.rmSync(outDir, { recursive: true, force: true })
17
+ fs.mkdirSync(outDir, { recursive: true })
18
+
19
+ for (const name of fs.readdirSync(srcDir)) {
20
+ if (name.endsWith('.c') || name.endsWith('.h')) {
21
+ fs.copyFileSync(path.join(srcDir, name), path.join(outDir, name))
22
+ }
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * cli.js — Tranfi CLI for Node.js.
5
+ *
6
+ * Usage:
7
+ * tranfi 'csv | filter "age > 25" | csv' < in.csv
8
+ * tranfi -f pipeline.tf < in.csv > out.csv
9
+ * tranfi profile < data.csv
10
+ */
11
+
12
+ const { pipeline, compileDsl, recipes } = require('./index.js')
13
+ const { readFileSync, writeFileSync, existsSync } = require('fs')
14
+ const { resolve, dirname } = require('path')
15
+ const nativeBinding = require('./native.js')
16
+
17
+ function usage() {
18
+ process.stderr.write(`Usage: tranfi [OPTIONS] PIPELINE
19
+ tranfi [OPTIONS] -f FILE
20
+
21
+ Streaming ETL with a pipe-style DSL.
22
+
23
+ Examples:
24
+ tranfi 'csv | csv' # passthrough
25
+ tranfi 'csv | filter "age > 25" | csv' # filter rows
26
+ tranfi 'csv | select name,age | csv' # select columns
27
+ tranfi 'csv | head 10 | csv' # first N rows
28
+ tranfi 'csv | top-k 10 age | csv' # bounded top rows by column
29
+ tranfi 'csv | stats | jsonl' # aggregate stats
30
+ tranfi profile # built-in recipe
31
+
32
+ Options:
33
+ -f FILE Read pipeline from file
34
+ -i FILE Read input from file instead of stdin
35
+ -o FILE Write output to file instead of stdout
36
+ -j Compile only, output JSON plan
37
+ -q Quiet mode (suppress stats)
38
+ -v Show version
39
+ -R List built-in recipes
40
+ -h Show this help
41
+ `)
42
+ }
43
+
44
+ function findAppDir () {
45
+ // Try known locations relative to this file
46
+ const candidates = [
47
+ resolve(__dirname, '../../app/dist'), // dev: js/src/ -> ../../app/dist
48
+ resolve(__dirname, '../app'), // npm package: src/ -> ../app/
49
+ ]
50
+ for (const dir of candidates) {
51
+ if (existsSync(resolve(dir, 'index.html'))) return dir
52
+ }
53
+ return null
54
+ }
55
+
56
+ async function serveCommand (argv) {
57
+ const { startServer } = require('./server.js')
58
+ let dataDir = '.'
59
+ let appDir = null
60
+ let port = 3000
61
+
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const arg = argv[i]
64
+ if (arg === '-d' || arg === '--data') dataDir = argv[++i]
65
+ else if (arg === '-a' || arg === '--app') appDir = argv[++i]
66
+ else if (arg === '-p' || arg === '--port') port = parseInt(argv[++i], 10)
67
+ else if (arg === '-h' || arg === '--help') {
68
+ process.stderr.write(`Usage: tranfi serve [OPTIONS]
69
+
70
+ Serve the tranfi app with a native backend.
71
+
72
+ Options:
73
+ -d, --data DIR Data directory (default: .)
74
+ -a, --app DIR App dist directory (auto-detect)
75
+ -p, --port PORT Port (default: 3000)
76
+ -h, --help Show this help
77
+ `)
78
+ process.exit(0)
79
+ }
80
+ }
81
+
82
+ if (!appDir) appDir = findAppDir()
83
+ if (!appDir) {
84
+ process.stderr.write('error: app directory not found. Use --app to specify it.\n')
85
+ process.exit(1)
86
+ }
87
+
88
+ startServer({ port, dataDir: resolve(dataDir), appDir: resolve(appDir) })
89
+ }
90
+
91
+ async function main() {
92
+ const argv = process.argv.slice(2)
93
+
94
+ // Check for serve subcommand
95
+ if (argv[0] === 'serve') {
96
+ return serveCommand(argv.slice(1))
97
+ }
98
+
99
+ let pipelineFile = null
100
+ let pipelineText = null
101
+ let inputFile = null
102
+ let outputFile = null
103
+ let jsonMode = false
104
+ let quiet = false
105
+
106
+ // Parse args
107
+ for (let i = 0; i < argv.length; i++) {
108
+ const arg = argv[i]
109
+ if (arg === '-h' || arg === '--help') { usage(); process.exit(0) }
110
+ else if (arg === '-v' || arg === '--version') {
111
+ const v = nativeBinding ? nativeBinding.version() : 'unknown'
112
+ process.stdout.write(`tranfi ${v}\n`)
113
+ process.exit(0)
114
+ }
115
+ else if (arg === '-R' || arg === '--recipes') {
116
+ const list = await recipes()
117
+ process.stdout.write(`Built-in recipes (${list.length}):\n\n`)
118
+ for (const r of list) {
119
+ process.stdout.write(` ${r.name.padEnd(12)} ${r.description}\n`)
120
+ process.stdout.write(` ${''.padEnd(12)} ${r.dsl}\n\n`)
121
+ }
122
+ process.exit(0)
123
+ }
124
+ else if (arg === '-j') jsonMode = true
125
+ else if (arg === '-q') quiet = true
126
+ else if (arg === '-f') { pipelineFile = argv[++i] }
127
+ else if (arg === '-i') { inputFile = argv[++i] }
128
+ else if (arg === '-o') { outputFile = argv[++i] }
129
+ else if (!arg.startsWith('-')) { pipelineText = arg }
130
+ else {
131
+ process.stderr.write(`error: unknown option '${arg}'\n`)
132
+ process.exit(1)
133
+ }
134
+ }
135
+
136
+ // Get pipeline text
137
+ if (pipelineFile) {
138
+ pipelineText = readFileSync(pipelineFile, 'utf-8')
139
+ }
140
+ if (!pipelineText) {
141
+ process.stderr.write('error: no pipeline specified\n\n')
142
+ usage()
143
+ process.exit(1)
144
+ }
145
+
146
+ // JSON mode
147
+ if (jsonMode) {
148
+ const json = await compileDsl(pipelineText)
149
+ process.stdout.write(json + '\n')
150
+ process.exit(0)
151
+ }
152
+
153
+ // Create and run pipeline
154
+ const p = pipeline(pipelineText)
155
+ const input = inputFile ? undefined : await readStdin()
156
+ const result = await p.run({
157
+ inputFile: inputFile || undefined,
158
+ input
159
+ })
160
+
161
+ // Write output
162
+ if (outputFile) {
163
+ writeFileSync(outputFile, result.output)
164
+ } else {
165
+ process.stdout.write(result.output)
166
+ }
167
+
168
+ // Errors to stderr
169
+ if (result.errors.length > 0) {
170
+ process.stderr.write(result.errors)
171
+ }
172
+
173
+ // Stats to stderr (unless quiet)
174
+ if (!quiet && result.stats.length > 0) {
175
+ process.stderr.write(result.stats)
176
+ }
177
+ }
178
+
179
+ function readStdin() {
180
+ return new Promise((resolve) => {
181
+ const chunks = []
182
+ process.stdin.on('data', chunk => chunks.push(chunk))
183
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks)))
184
+ })
185
+ }
186
+
187
+ main().catch(err => {
188
+ process.stderr.write(`error: ${err.message}\n`)
189
+ process.exit(1)
190
+ })
@@ -0,0 +1,142 @@
1
+ /**
2
+ * duckdb.js — DuckDB engine for Node.js.
3
+ *
4
+ * Executes tranfi pipelines via SQL transpilation + duckdb.
5
+ * Install: npm install duckdb
6
+ */
7
+
8
+ const { writeFile, unlink } = require('fs/promises')
9
+ const { tmpdir } = require('os')
10
+ const { join } = require('path')
11
+ const { randomBytes } = require('crypto')
12
+ const { PipelineResult } = require('../pipeline.js')
13
+ const { memoryForEngine } = require('../memory_policy.js')
14
+
15
+ let _compileToSql = null
16
+
17
+ async function getCompileToSql() {
18
+ if (_compileToSql) return _compileToSql
19
+ const { compileToSql } = require('../pipeline.js')
20
+ _compileToSql = compileToSql
21
+ return _compileToSql
22
+ }
23
+
24
+ class DuckDBEngine {
25
+ async run(dsl, { input, inputFile, memory, spillDir } = {}) {
26
+ let duckdb
27
+ try {
28
+ duckdb = require('duckdb')
29
+ } catch {
30
+ throw new Error(
31
+ "DuckDB engine requires the 'duckdb' package. " +
32
+ 'Install it with: npm install duckdb'
33
+ )
34
+ }
35
+
36
+ const compileToSql = await getCompileToSql()
37
+ let sql = await compileToSql(dsl)
38
+
39
+ const db = new duckdb.Database(':memory:')
40
+ const conn = db.connect()
41
+
42
+ let tmpPath = null
43
+ try {
44
+ const memoryLimit = memoryForEngine(memory)
45
+ if (memoryLimit) await runSql(conn, `SET memory_limit = '${sqlString(memoryLimit)}'`)
46
+ if (spillDir) await runSql(conn, `SET temp_directory = '${sqlString(spillDir)}'`)
47
+
48
+ if (inputFile) {
49
+ sql = sql.replaceAll('input_data', `read_csv('${sqlString(inputFile)}')`)
50
+ } else if (input) {
51
+ const buf = typeof input === 'string' ? Buffer.from(input, 'utf-8') : input
52
+ tmpPath = join(tmpdir(), `tranfi_${randomBytes(8).toString('hex')}.csv`)
53
+ await writeFile(tmpPath, buf)
54
+ sql = sql.replaceAll('input_data', `read_csv('${sqlString(tmpPath)}')`)
55
+ } else {
56
+ throw new Error('Either input or inputFile must be provided')
57
+ }
58
+
59
+ const rows = await queryAll(conn, sql)
60
+ const output = rowsToCsv(rows)
61
+
62
+ return new PipelineResult(
63
+ Buffer.from(output, 'utf-8'),
64
+ Buffer.alloc(0),
65
+ Buffer.alloc(0),
66
+ Buffer.alloc(0)
67
+ )
68
+ } finally {
69
+ conn.close()
70
+ db.close()
71
+ if (tmpPath) {
72
+ await unlink(tmpPath).catch(() => {})
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ function sqlString(value) {
79
+ return String(value).replace(/'/g, "''")
80
+ }
81
+
82
+ function runSql(conn, sql) {
83
+ return new Promise((resolve, reject) => {
84
+ conn.run(sql, err => {
85
+ if (err) reject(err)
86
+ else resolve()
87
+ })
88
+ })
89
+ }
90
+
91
+ function queryAll(conn, sql) {
92
+ return new Promise((resolve, reject) => {
93
+ conn.all(sql, (err, rows) => {
94
+ if (err) reject(err)
95
+ else resolve(rows)
96
+ })
97
+ })
98
+ }
99
+
100
+ function rowsToCsv(rows) {
101
+ if (!rows || rows.length === 0) return ''
102
+ const cols = Object.keys(rows[0])
103
+ const lines = [cols.join(',')]
104
+ for (const row of rows) {
105
+ const vals = cols.map(c => {
106
+ const v = row[c]
107
+ if (v === null || v === undefined) return ''
108
+ const s = formatDuckValue(v)
109
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
110
+ return '"' + s.replace(/"/g, '""') + '"'
111
+ }
112
+ return s
113
+ })
114
+ lines.push(vals.join(','))
115
+ }
116
+ return lines.join('\n') + '\n'
117
+ }
118
+
119
+ function formatDuckValue(value) {
120
+ if (typeof value !== 'number') return String(value)
121
+ if (!Number.isFinite(value)) return String(value)
122
+ if (Number.isInteger(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER) {
123
+ return String(value)
124
+ }
125
+ let text = value.toPrecision(17)
126
+ if (!/[eE]/.test(text) && text.includes('.')) {
127
+ text = text.replace(/0+$/, '').replace(/\.$/, '')
128
+ }
129
+ return text
130
+ }
131
+
132
+ let _engine = null
133
+
134
+ function getEngine(name) {
135
+ if (name === 'duckdb') {
136
+ if (!_engine) _engine = new DuckDBEngine()
137
+ return _engine
138
+ }
139
+ throw new Error(`Unknown engine: '${name}'. Available: 'native', 'duckdb'`)
140
+ }
141
+
142
+ module.exports = { getEngine }