tranfi 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -21
- package/NOTICE +8 -0
- package/README.md +627 -0
- package/app/assets/index-6quYZ5Ap.css +5 -0
- package/app/assets/index-BIAIKnrp.js +160 -0
- package/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/app/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/app/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/app/index.html +13 -0
- package/binding.gyp +121 -0
- package/csrc/arena.c +93 -0
- package/csrc/batch.c +976 -0
- package/csrc/buffer.c +154 -0
- package/csrc/cJSON.c +3386 -0
- package/csrc/cJSON.h +316 -0
- package/csrc/codec_csv.c +1951 -0
- package/csrc/codec_jsonl.c +1086 -0
- package/csrc/codec_table.c +248 -0
- package/csrc/codec_text.c +447 -0
- package/csrc/compiler.c +130 -0
- package/csrc/config.h +21 -0
- package/csrc/date_utils.h +94 -0
- package/csrc/dsl.c +5417 -0
- package/csrc/dsl.h +22 -0
- package/csrc/expr.c +1553 -0
- package/csrc/expr.h +58 -0
- package/csrc/internal.h +539 -0
- package/csrc/ir.c +166 -0
- package/csrc/ir.h +208 -0
- package/csrc/ir_schema.c +75 -0
- package/csrc/ir_serialize.c +166 -0
- package/csrc/ir_sql.c +1822 -0
- package/csrc/ir_validate.c +576 -0
- package/csrc/json_path.c +210 -0
- package/csrc/main.c +1241 -0
- package/csrc/memory_estimate.c +477 -0
- package/csrc/op_acf.c +283 -0
- package/csrc/op_across.c +477 -0
- package/csrc/op_anomaly.c +255 -0
- package/csrc/op_assert.c +761 -0
- package/csrc/op_bin.c +248 -0
- package/csrc/op_cast.c +523 -0
- package/csrc/op_clip.c +99 -0
- package/csrc/op_date_trunc.c +355 -0
- package/csrc/op_datetime.c +394 -0
- package/csrc/op_derive.c +216 -0
- package/csrc/op_diff.c +250 -0
- package/csrc/op_ewma.c +222 -0
- package/csrc/op_explode.c +206 -0
- package/csrc/op_fill_down.c +235 -0
- package/csrc/op_fill_null.c +268 -0
- package/csrc/op_filter.c +181 -0
- package/csrc/op_frequency.c +721 -0
- package/csrc/op_grep.c +181 -0
- package/csrc/op_group_agg.c +1956 -0
- package/csrc/op_hash.c +159 -0
- package/csrc/op_head.c +84 -0
- package/csrc/op_interpolate.c +445 -0
- package/csrc/op_join.c +2902 -0
- package/csrc/op_json_extract.c +227 -0
- package/csrc/op_json_filter.c +384 -0
- package/csrc/op_json_flatten.c +293 -0
- package/csrc/op_json_schema.c +503 -0
- package/csrc/op_label_encode.c +419 -0
- package/csrc/op_lag.c +181 -0
- package/csrc/op_lead.c +242 -0
- package/csrc/op_normalize.c +510 -0
- package/csrc/op_onehot.c +457 -0
- package/csrc/op_pivot.c +1754 -0
- package/csrc/op_quarantine.c +189 -0
- package/csrc/op_registry.c +3044 -0
- package/csrc/op_rename.c +129 -0
- package/csrc/op_replace.c +354 -0
- package/csrc/op_rleid.c +297 -0
- package/csrc/op_rowid.c +559 -0
- package/csrc/op_sample.c +158 -0
- package/csrc/op_schema.c +1341 -0
- package/csrc/op_schema_infer.c +252 -0
- package/csrc/op_select.c +340 -0
- package/csrc/op_set.c +3449 -0
- package/csrc/op_skip.c +95 -0
- package/csrc/op_sort.c +819 -0
- package/csrc/op_source_name.c +120 -0
- package/csrc/op_split.c +151 -0
- package/csrc/op_split_data.c +119 -0
- package/csrc/op_stack.c +271 -0
- package/csrc/op_stats.c +875 -0
- package/csrc/op_step.c +333 -0
- package/csrc/op_tail.c +105 -0
- package/csrc/op_tee.c +338 -0
- package/csrc/op_top.c +357 -0
- package/csrc/op_trim.c +138 -0
- package/csrc/op_unique.c +1343 -0
- package/csrc/op_unpivot.c +193 -0
- package/csrc/op_validate.c +648 -0
- package/csrc/op_window.c +591 -0
- package/csrc/path_policy.c +85 -0
- package/csrc/pipeline.c +1088 -0
- package/csrc/recipes.c +104 -0
- package/csrc/recipes.h +27 -0
- package/csrc/report.c +506 -0
- package/csrc/report.h +22 -0
- package/csrc/selector.c +1097 -0
- package/csrc/size_utils.c +348 -0
- package/csrc/spill.c +317 -0
- package/csrc/spill.h +21 -0
- package/csrc/tranfi.h +291 -0
- package/csrc/transform.h +209 -0
- package/csrc/transform_api.c +2237 -0
- package/csrc/transform_categorical.c +923 -0
- package/csrc/transform_internal.h +472 -0
- package/csrc/transform_json.c +3812 -0
- package/csrc/transform_numeric.c +1966 -0
- package/csrc/transform_sha256.c +154 -0
- package/csrc/transform_wasm.h +162 -0
- package/csrc/transform_wasm_api.c +1373 -0
- package/csrc/wasm_api.c +218 -0
- package/napi_api.c +534 -0
- package/napi_transform.c +1648 -0
- package/napi_transform.h +8 -0
- package/package.json +64 -59
- package/scripts/install-native.js +76 -0
- package/scripts/prepack.js +64 -0
- package/scripts/sync-csrc.js +23 -0
- package/src/cli.js +190 -0
- package/src/engines/duckdb.js +142 -0
- package/src/index.js +925 -0
- package/src/memory_policy.js +411 -0
- package/src/native.js +18 -0
- package/src/pipeline.js +709 -0
- package/src/recipe_json.js +80 -0
- package/src/server.js +279 -0
- package/src/transform.js +403 -0
- package/src/transform_error.js +10 -0
- package/src/wasm.js +21 -0
- package/wasm/index.js +732 -0
- package/wasm/package.json +1 -0
- package/wasm/tranfi_core.js +0 -0
- package/wasm/transform.js +1156 -0
- package/wasm/worker.js +786 -0
- package/dist/bundle.js +0 -1
- package/index.html +0 -18
- package/src/app.css +0 -169
- package/src/app.js +0 -203
- package/src/app.vue +0 -250
- package/src/bulma-input.vue +0 -110
- package/src/common-inputs.js +0 -28
- package/src/main.js +0 -20
- package/src/transforms.js +0 -166
- package/webpack.config.js +0 -108
package/napi_transform.h
ADDED
package/package.json
CHANGED
|
@@ -1,68 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tranfi",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
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
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
"
|
|
15
|
-
|
|
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": "
|
|
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
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
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 }
|