tranfi 0.1.2 → 0.2.1
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 +443 -51
- 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 +352 -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 +81 -41
- 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/napi_transform.h
ADDED
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tranfi",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Streaming ETL language + runtime",
|
|
5
|
-
"type": "
|
|
5
|
+
"type": "commonjs",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"tranfi": "src/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
|
-
".":
|
|
12
|
-
|
|
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
|
+
}
|
|
13
23
|
},
|
|
14
24
|
"files": [
|
|
15
25
|
"src/",
|
|
@@ -17,14 +27,19 @@
|
|
|
17
27
|
"csrc/",
|
|
18
28
|
"app/",
|
|
19
29
|
"napi_api.c",
|
|
30
|
+
"napi_transform.c",
|
|
31
|
+
"napi_transform.h",
|
|
20
32
|
"binding.gyp",
|
|
21
|
-
"
|
|
33
|
+
"scripts/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"NOTICE"
|
|
22
37
|
],
|
|
23
38
|
"scripts": {
|
|
24
|
-
"prepack": "
|
|
25
|
-
"install": "node
|
|
26
|
-
"build:native": "
|
|
27
|
-
"test": "node ../test/test_node.js"
|
|
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 ../test/test_cli_node.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"
|
|
28
43
|
},
|
|
29
44
|
"peerDependencies": {
|
|
30
45
|
"duckdb": ">=1.0.0",
|
|
@@ -46,12 +61,13 @@
|
|
|
46
61
|
"pipeline",
|
|
47
62
|
"transform"
|
|
48
63
|
],
|
|
49
|
-
"license": "
|
|
64
|
+
"license": "Apache-2.0",
|
|
50
65
|
"repository": {
|
|
51
66
|
"type": "git",
|
|
52
67
|
"url": "https://github.com/tranfi/tranfi"
|
|
53
68
|
},
|
|
54
69
|
"devDependencies": {
|
|
55
70
|
"duckdb": "^1.4.4"
|
|
56
|
-
}
|
|
71
|
+
},
|
|
72
|
+
"author": "Anton Zemlyansky"
|
|
57
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
CHANGED
|
@@ -9,13 +9,10 @@
|
|
|
9
9
|
* tranfi profile < data.csv
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import nativeBinding from './native.js'
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const { pipeline, compileDsl, compileToSql, recipes } = require('./index.js')
|
|
13
|
+
const { readFileSync, writeFileSync, existsSync, openSync, closeSync } = require('fs')
|
|
14
|
+
const { resolve, dirname } = require('path')
|
|
15
|
+
const nativeBinding = require('./native.js')
|
|
19
16
|
|
|
20
17
|
function usage() {
|
|
21
18
|
process.stderr.write(`Usage: tranfi [OPTIONS] PIPELINE
|
|
@@ -28,7 +25,7 @@ Examples:
|
|
|
28
25
|
tranfi 'csv | filter "age > 25" | csv' # filter rows
|
|
29
26
|
tranfi 'csv | select name,age | csv' # select columns
|
|
30
27
|
tranfi 'csv | head 10 | csv' # first N rows
|
|
31
|
-
tranfi 'csv |
|
|
28
|
+
tranfi 'csv | top-k 10 age | csv' # bounded top rows by column
|
|
32
29
|
tranfi 'csv | stats | jsonl' # aggregate stats
|
|
33
30
|
tranfi profile # built-in recipe
|
|
34
31
|
|
|
@@ -37,6 +34,13 @@ Options:
|
|
|
37
34
|
-i FILE Read input from file instead of stdin
|
|
38
35
|
-o FILE Write output to file instead of stdout
|
|
39
36
|
-j Compile only, output JSON plan
|
|
37
|
+
--target native|json|sql Execute or compile a pipeline
|
|
38
|
+
--dialect NAME SQL dialect (duckdb)
|
|
39
|
+
--allow-blocking Permit full-input operations on known-small data
|
|
40
|
+
--fail-on-blocking Reject blocking operations (default)
|
|
41
|
+
--memory SIZE Native memory policy, e.g. max:64MB
|
|
42
|
+
--spill-dir DIR Existing private spill directory
|
|
43
|
+
--stats-json FILE Write stats NDJSON; - means stderr
|
|
40
44
|
-q Quiet mode (suppress stats)
|
|
41
45
|
-v Show version
|
|
42
46
|
-R List built-in recipes
|
|
@@ -47,8 +51,8 @@ Options:
|
|
|
47
51
|
function findAppDir () {
|
|
48
52
|
// Try known locations relative to this file
|
|
49
53
|
const candidates = [
|
|
50
|
-
resolve(__dirname, '../../app/dist'), // dev: js/src/
|
|
51
|
-
resolve(__dirname, '../app'), // npm package: src/
|
|
54
|
+
resolve(__dirname, '../../app/dist'), // dev: js/src/ -> ../../app/dist
|
|
55
|
+
resolve(__dirname, '../app'), // npm package: src/ -> ../app/
|
|
52
56
|
]
|
|
53
57
|
for (const dir of candidates) {
|
|
54
58
|
if (existsSync(resolve(dir, 'index.html'))) return dir
|
|
@@ -57,7 +61,7 @@ function findAppDir () {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
async function serveCommand (argv) {
|
|
60
|
-
const { startServer } =
|
|
64
|
+
const { startServer } = require('./server.js')
|
|
61
65
|
let dataDir = '.'
|
|
62
66
|
let appDir = null
|
|
63
67
|
let port = 3000
|
|
@@ -105,10 +109,21 @@ async function main() {
|
|
|
105
109
|
let outputFile = null
|
|
106
110
|
let jsonMode = false
|
|
107
111
|
let quiet = false
|
|
112
|
+
let allowBlocking = false
|
|
113
|
+
let failOnBlocking = false
|
|
114
|
+
let memory, spillDir, statsFile
|
|
115
|
+
let target = 'native'
|
|
116
|
+
let dialect = 'duckdb'
|
|
117
|
+
function valueAt(i, inline, flag) {
|
|
118
|
+
const value = inline === undefined ? argv[i + 1] : inline
|
|
119
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`)
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
108
122
|
|
|
109
123
|
// Parse args
|
|
110
124
|
for (let i = 0; i < argv.length; i++) {
|
|
111
|
-
const arg = argv[i]
|
|
125
|
+
const [arg, ...assigned] = argv[i].split('=')
|
|
126
|
+
const inline = assigned.length ? assigned.join('=') : undefined
|
|
112
127
|
if (arg === '-h' || arg === '--help') { usage(); process.exit(0) }
|
|
113
128
|
else if (arg === '-v' || arg === '--version') {
|
|
114
129
|
const v = nativeBinding ? nativeBinding.version() : 'unknown'
|
|
@@ -124,18 +139,35 @@ async function main() {
|
|
|
124
139
|
}
|
|
125
140
|
process.exit(0)
|
|
126
141
|
}
|
|
127
|
-
else if (arg === '-j') jsonMode = true
|
|
128
|
-
else if (arg === '-q') quiet = true
|
|
129
|
-
else if (arg === '-
|
|
130
|
-
else if (arg === '-
|
|
131
|
-
else if (
|
|
132
|
-
|
|
142
|
+
else if (arg === '-j' || arg === '--json') jsonMode = true
|
|
143
|
+
else if (arg === '-q' || arg === '--quiet') quiet = true
|
|
144
|
+
else if (arg === '--allow-blocking') allowBlocking = true
|
|
145
|
+
else if (arg === '--fail-on-blocking') failOnBlocking = true
|
|
146
|
+
else if (['-f', '--file', '-i', '--input', '-o', '--output', '--memory', '--spill-dir', '--stats-json', '--target', '--dialect'].includes(arg)) {
|
|
147
|
+
const value = valueAt(i, inline, arg)
|
|
148
|
+
if (inline === undefined) i++
|
|
149
|
+
if (arg === '-f' || arg === '--file') pipelineFile = value
|
|
150
|
+
else if (arg === '-i' || arg === '--input') inputFile = value
|
|
151
|
+
else if (arg === '-o' || arg === '--output') outputFile = value
|
|
152
|
+
else if (arg === '--memory') memory = value
|
|
153
|
+
else if (arg === '--spill-dir') spillDir = value
|
|
154
|
+
else if (arg === '--stats-json') statsFile = value
|
|
155
|
+
else if (arg === '--target') target = value
|
|
156
|
+
else if (arg === '--dialect') dialect = value
|
|
157
|
+
}
|
|
158
|
+
else if (!argv[i].startsWith('-')) {
|
|
159
|
+
if (pipelineText !== null) throw new Error('only one pipeline argument is allowed')
|
|
160
|
+
pipelineText = argv[i]
|
|
161
|
+
}
|
|
133
162
|
else {
|
|
134
163
|
process.stderr.write(`error: unknown option '${arg}'\n`)
|
|
135
164
|
process.exit(1)
|
|
136
165
|
}
|
|
137
166
|
}
|
|
138
167
|
|
|
168
|
+
if (allowBlocking && failOnBlocking) throw new Error('--allow-blocking conflicts with --fail-on-blocking')
|
|
169
|
+
if (!['native', 'json', 'sql'].includes(target)) throw new Error('unknown --target; expected native, json or sql')
|
|
170
|
+
|
|
139
171
|
// Get pipeline text
|
|
140
172
|
if (pipelineFile) {
|
|
141
173
|
pipelineText = readFileSync(pipelineFile, 'utf-8')
|
|
@@ -146,26 +178,44 @@ async function main() {
|
|
|
146
178
|
process.exit(1)
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
|
|
150
|
-
if (
|
|
181
|
+
const named = (await recipes()).find(recipe => recipe.name === pipelineText.trim())
|
|
182
|
+
if (named) pipelineText = named.dsl
|
|
183
|
+
if (target === 'sql') {
|
|
184
|
+
process.stdout.write(await compileToSql(pipelineText, { dialect }) + '\n')
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (jsonMode || target === 'json') {
|
|
151
189
|
const json = await compileDsl(pipelineText)
|
|
152
190
|
process.stdout.write(json + '\n')
|
|
153
191
|
process.exit(0)
|
|
154
192
|
}
|
|
155
193
|
|
|
156
|
-
// Create and run pipeline
|
|
157
194
|
const p = pipeline(pipelineText)
|
|
158
|
-
const
|
|
159
|
-
const result = await p.run({
|
|
195
|
+
const options = {
|
|
160
196
|
inputFile: inputFile || undefined,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
inputStream: inputFile ? undefined : process.stdin,
|
|
198
|
+
allowBlocking, memory, spillDir, allowFs: true, allowRulesFile: true
|
|
199
|
+
}
|
|
200
|
+
let result
|
|
165
201
|
if (outputFile) {
|
|
166
|
-
|
|
202
|
+
let fd
|
|
203
|
+
try {
|
|
204
|
+
result = await p.run({
|
|
205
|
+
...options,
|
|
206
|
+
collectOutput: false,
|
|
207
|
+
onOutput(chunk) {
|
|
208
|
+
// Open only after plan validation; a rejected plan must not truncate output.
|
|
209
|
+
if (fd === undefined) fd = openSync(outputFile, 'w')
|
|
210
|
+
writeFileSync(fd, chunk)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
if (fd === undefined) fd = openSync(outputFile, 'w')
|
|
214
|
+
} finally {
|
|
215
|
+
if (fd !== undefined) closeSync(fd)
|
|
216
|
+
}
|
|
167
217
|
} else {
|
|
168
|
-
process.stdout
|
|
218
|
+
result = await p.writeTo(process.stdout, { ...options, end: false })
|
|
169
219
|
}
|
|
170
220
|
|
|
171
221
|
// Errors to stderr
|
|
@@ -173,18 +223,8 @@ async function main() {
|
|
|
173
223
|
process.stderr.write(result.errors)
|
|
174
224
|
}
|
|
175
225
|
|
|
176
|
-
|
|
177
|
-
if (!quiet && result.stats.length > 0)
|
|
178
|
-
process.stderr.write(result.stats)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function readStdin() {
|
|
183
|
-
return new Promise((resolve) => {
|
|
184
|
-
const chunks = []
|
|
185
|
-
process.stdin.on('data', chunk => chunks.push(chunk))
|
|
186
|
-
process.stdin.on('end', () => resolve(Buffer.concat(chunks)))
|
|
187
|
-
})
|
|
226
|
+
if (statsFile && statsFile !== '-') writeFileSync(statsFile, result.stats)
|
|
227
|
+
else if ((statsFile || !quiet) && result.stats.length > 0) process.stderr.write(result.stats)
|
|
188
228
|
}
|
|
189
229
|
|
|
190
230
|
main().catch(err => {
|
package/src/engines/duckdb.js
CHANGED
|
@@ -5,26 +5,27 @@
|
|
|
5
5
|
* Install: npm install duckdb
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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')
|
|
13
14
|
|
|
14
15
|
let _compileToSql = null
|
|
15
16
|
|
|
16
17
|
async function getCompileToSql() {
|
|
17
18
|
if (_compileToSql) return _compileToSql
|
|
18
|
-
const { compileToSql } =
|
|
19
|
+
const { compileToSql } = require('../pipeline.js')
|
|
19
20
|
_compileToSql = compileToSql
|
|
20
21
|
return _compileToSql
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
class DuckDBEngine {
|
|
24
|
-
async run(dsl, { input, inputFile } = {}) {
|
|
25
|
+
async run(dsl, { input, inputFile, memory, spillDir } = {}) {
|
|
25
26
|
let duckdb
|
|
26
27
|
try {
|
|
27
|
-
duckdb = (
|
|
28
|
+
duckdb = require('duckdb')
|
|
28
29
|
} catch {
|
|
29
30
|
throw new Error(
|
|
30
31
|
"DuckDB engine requires the 'duckdb' package. " +
|
|
@@ -40,13 +41,17 @@ class DuckDBEngine {
|
|
|
40
41
|
|
|
41
42
|
let tmpPath = null
|
|
42
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
|
+
|
|
43
48
|
if (inputFile) {
|
|
44
|
-
sql = sql.replaceAll('input_data', `read_csv('${inputFile}')`)
|
|
49
|
+
sql = sql.replaceAll('input_data', `read_csv('${sqlString(inputFile)}')`)
|
|
45
50
|
} else if (input) {
|
|
46
51
|
const buf = typeof input === 'string' ? Buffer.from(input, 'utf-8') : input
|
|
47
52
|
tmpPath = join(tmpdir(), `tranfi_${randomBytes(8).toString('hex')}.csv`)
|
|
48
53
|
await writeFile(tmpPath, buf)
|
|
49
|
-
sql = sql.replaceAll('input_data', `read_csv('${tmpPath}')`)
|
|
54
|
+
sql = sql.replaceAll('input_data', `read_csv('${sqlString(tmpPath)}')`)
|
|
50
55
|
} else {
|
|
51
56
|
throw new Error('Either input or inputFile must be provided')
|
|
52
57
|
}
|
|
@@ -70,6 +75,19 @@ class DuckDBEngine {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
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
|
+
|
|
73
91
|
function queryAll(conn, sql) {
|
|
74
92
|
return new Promise((resolve, reject) => {
|
|
75
93
|
conn.all(sql, (err, rows) => {
|
|
@@ -87,7 +105,7 @@ function rowsToCsv(rows) {
|
|
|
87
105
|
const vals = cols.map(c => {
|
|
88
106
|
const v = row[c]
|
|
89
107
|
if (v === null || v === undefined) return ''
|
|
90
|
-
const s =
|
|
108
|
+
const s = formatDuckValue(v)
|
|
91
109
|
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
92
110
|
return '"' + s.replace(/"/g, '""') + '"'
|
|
93
111
|
}
|
|
@@ -98,12 +116,27 @@ function rowsToCsv(rows) {
|
|
|
98
116
|
return lines.join('\n') + '\n'
|
|
99
117
|
}
|
|
100
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
|
+
|
|
101
132
|
let _engine = null
|
|
102
133
|
|
|
103
|
-
|
|
134
|
+
function getEngine(name) {
|
|
104
135
|
if (name === 'duckdb') {
|
|
105
136
|
if (!_engine) _engine = new DuckDBEngine()
|
|
106
137
|
return _engine
|
|
107
138
|
}
|
|
108
139
|
throw new Error(`Unknown engine: '${name}'. Available: 'native', 'duckdb'`)
|
|
109
140
|
}
|
|
141
|
+
|
|
142
|
+
module.exports = { getEngine }
|