tranfi 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -0
- package/NOTICE +8 -0
- package/README.md +272 -40
- package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
- package/app/index.html +1 -1
- package/binding.gyp +55 -3
- package/csrc/arena.c +7 -5
- package/csrc/batch.c +818 -71
- package/csrc/buffer.c +84 -8
- package/csrc/cJSON.c +262 -19
- package/csrc/cJSON.h +17 -1
- package/csrc/codec_csv.c +1074 -181
- package/csrc/codec_jsonl.c +830 -118
- package/csrc/codec_table.c +108 -78
- package/csrc/codec_text.c +286 -68
- package/csrc/compiler.c +31 -3
- package/csrc/config.h +21 -0
- package/csrc/dsl.c +4722 -485
- package/csrc/expr.c +363 -55
- package/csrc/expr.h +2 -0
- package/csrc/internal.h +316 -27
- package/csrc/ir.c +65 -18
- package/csrc/ir.h +41 -0
- package/csrc/ir_schema.c +20 -5
- package/csrc/ir_serialize.c +68 -6
- package/csrc/ir_sql.c +796 -185
- package/csrc/ir_validate.c +462 -6
- package/csrc/json_path.c +210 -0
- package/csrc/main.c +879 -30
- package/csrc/memory_estimate.c +477 -0
- package/csrc/op_acf.c +171 -21
- package/csrc/op_across.c +477 -0
- package/csrc/op_anomaly.c +167 -32
- package/csrc/op_assert.c +761 -0
- package/csrc/op_bin.c +168 -29
- package/csrc/op_cast.c +383 -55
- package/csrc/op_clip.c +30 -19
- package/csrc/op_date_trunc.c +208 -34
- package/csrc/op_datetime.c +259 -77
- package/csrc/op_derive.c +65 -97
- package/csrc/op_diff.c +146 -30
- package/csrc/op_ewma.c +149 -30
- package/csrc/op_explode.c +124 -26
- package/csrc/op_fill_down.c +125 -53
- package/csrc/op_fill_null.c +176 -31
- package/csrc/op_filter.c +89 -40
- package/csrc/op_frequency.c +571 -43
- package/csrc/op_grep.c +36 -18
- package/csrc/op_group_agg.c +1790 -119
- package/csrc/op_hash.c +48 -15
- package/csrc/op_head.c +21 -86
- package/csrc/op_interpolate.c +268 -62
- package/csrc/op_join.c +2700 -182
- package/csrc/op_json_extract.c +227 -0
- package/csrc/op_json_filter.c +384 -0
- package/csrc/op_json_flatten.c +293 -0
- package/csrc/op_json_schema.c +503 -0
- package/csrc/op_label_encode.c +328 -53
- package/csrc/op_lag.c +181 -0
- package/csrc/op_lead.c +141 -89
- package/csrc/op_normalize.c +363 -79
- package/csrc/op_onehot.c +345 -73
- package/csrc/op_pivot.c +1546 -162
- package/csrc/op_quarantine.c +189 -0
- package/csrc/op_registry.c +2062 -166
- package/csrc/op_rename.c +41 -50
- package/csrc/op_replace.c +270 -118
- package/csrc/op_rleid.c +297 -0
- package/csrc/op_rowid.c +559 -0
- package/csrc/op_sample.c +80 -23
- package/csrc/op_schema.c +1341 -0
- package/csrc/op_schema_infer.c +252 -0
- package/csrc/op_select.c +265 -65
- package/csrc/op_set.c +3449 -0
- package/csrc/op_skip.c +30 -87
- package/csrc/op_sort.c +670 -124
- package/csrc/op_source_name.c +120 -0
- package/csrc/op_split.c +65 -28
- package/csrc/op_split_data.c +41 -9
- package/csrc/op_stack.c +178 -222
- package/csrc/op_stats.c +206 -110
- package/csrc/op_step.c +217 -55
- package/csrc/op_tail.c +21 -12
- package/csrc/op_tee.c +338 -0
- package/csrc/op_top.c +260 -53
- package/csrc/op_trim.c +48 -19
- package/csrc/op_unique.c +1193 -150
- package/csrc/op_unpivot.c +100 -66
- package/csrc/op_validate.c +601 -24
- package/csrc/op_window.c +492 -51
- package/csrc/path_policy.c +85 -0
- package/csrc/pipeline.c +872 -99
- package/csrc/recipes.c +3 -1
- package/csrc/report.c +73 -30
- package/csrc/selector.c +1097 -0
- package/csrc/size_utils.c +348 -0
- package/csrc/spill.c +317 -0
- package/csrc/spill.h +21 -0
- package/csrc/tranfi.h +169 -1
- package/csrc/transform.h +209 -0
- package/csrc/transform_api.c +2237 -0
- package/csrc/transform_categorical.c +923 -0
- package/csrc/transform_internal.h +472 -0
- package/csrc/transform_json.c +3812 -0
- package/csrc/transform_numeric.c +1966 -0
- package/csrc/transform_sha256.c +154 -0
- package/csrc/transform_wasm.h +162 -0
- package/csrc/transform_wasm_api.c +1373 -0
- package/csrc/wasm_api.c +70 -9
- package/napi_api.c +219 -11
- package/napi_transform.c +1648 -0
- package/napi_transform.h +8 -0
- package/package.json +27 -11
- package/scripts/install-native.js +76 -0
- package/scripts/prepack.js +64 -0
- package/scripts/sync-csrc.js +23 -0
- package/src/cli.js +8 -11
- package/src/engines/duckdb.js +45 -12
- package/src/index.js +661 -42
- package/src/memory_policy.js +411 -0
- package/src/native.js +1 -5
- package/src/pipeline.js +454 -31
- package/src/recipe_json.js +80 -0
- package/src/server.js +10 -8
- package/src/transform.js +403 -0
- package/src/transform_error.js +10 -0
- package/src/wasm.js +6 -4
- package/wasm/index.js +498 -10
- package/wasm/tranfi_core.js +0 -0
- package/wasm/transform.js +1156 -0
- package/wasm/worker.js +786 -0
- package/csrc/plan.c +0 -206
package/napi_transform.h
ADDED
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tranfi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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 && 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, recipes } = require('./index.js')
|
|
13
|
+
const { readFileSync, writeFileSync, existsSync } = 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
|
|
|
@@ -47,8 +44,8 @@ Options:
|
|
|
47
44
|
function findAppDir () {
|
|
48
45
|
// Try known locations relative to this file
|
|
49
46
|
const candidates = [
|
|
50
|
-
resolve(__dirname, '../../app/dist'), // dev: js/src/
|
|
51
|
-
resolve(__dirname, '../app'), // npm package: src/
|
|
47
|
+
resolve(__dirname, '../../app/dist'), // dev: js/src/ -> ../../app/dist
|
|
48
|
+
resolve(__dirname, '../app'), // npm package: src/ -> ../app/
|
|
52
49
|
]
|
|
53
50
|
for (const dir of candidates) {
|
|
54
51
|
if (existsSync(resolve(dir, 'index.html'))) return dir
|
|
@@ -57,7 +54,7 @@ function findAppDir () {
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
async function serveCommand (argv) {
|
|
60
|
-
const { startServer } =
|
|
57
|
+
const { startServer } = require('./server.js')
|
|
61
58
|
let dataDir = '.'
|
|
62
59
|
let appDir = null
|
|
63
60
|
let port = 3000
|
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 }
|