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.
Files changed (132) hide show
  1. package/LICENSE +177 -0
  2. package/NOTICE +8 -0
  3. package/README.md +272 -40
  4. package/app/assets/{index-pDFMluyz.js → index-BIAIKnrp.js} +1 -1
  5. package/app/index.html +1 -1
  6. package/binding.gyp +55 -3
  7. package/csrc/arena.c +7 -5
  8. package/csrc/batch.c +818 -71
  9. package/csrc/buffer.c +84 -8
  10. package/csrc/cJSON.c +262 -19
  11. package/csrc/cJSON.h +17 -1
  12. package/csrc/codec_csv.c +1074 -181
  13. package/csrc/codec_jsonl.c +830 -118
  14. package/csrc/codec_table.c +108 -78
  15. package/csrc/codec_text.c +286 -68
  16. package/csrc/compiler.c +31 -3
  17. package/csrc/config.h +21 -0
  18. package/csrc/dsl.c +4722 -485
  19. package/csrc/expr.c +363 -55
  20. package/csrc/expr.h +2 -0
  21. package/csrc/internal.h +316 -27
  22. package/csrc/ir.c +65 -18
  23. package/csrc/ir.h +41 -0
  24. package/csrc/ir_schema.c +20 -5
  25. package/csrc/ir_serialize.c +68 -6
  26. package/csrc/ir_sql.c +796 -185
  27. package/csrc/ir_validate.c +462 -6
  28. package/csrc/json_path.c +210 -0
  29. package/csrc/main.c +879 -30
  30. package/csrc/memory_estimate.c +477 -0
  31. package/csrc/op_acf.c +171 -21
  32. package/csrc/op_across.c +477 -0
  33. package/csrc/op_anomaly.c +167 -32
  34. package/csrc/op_assert.c +761 -0
  35. package/csrc/op_bin.c +168 -29
  36. package/csrc/op_cast.c +383 -55
  37. package/csrc/op_clip.c +30 -19
  38. package/csrc/op_date_trunc.c +208 -34
  39. package/csrc/op_datetime.c +259 -77
  40. package/csrc/op_derive.c +65 -97
  41. package/csrc/op_diff.c +146 -30
  42. package/csrc/op_ewma.c +149 -30
  43. package/csrc/op_explode.c +124 -26
  44. package/csrc/op_fill_down.c +125 -53
  45. package/csrc/op_fill_null.c +176 -31
  46. package/csrc/op_filter.c +89 -40
  47. package/csrc/op_frequency.c +571 -43
  48. package/csrc/op_grep.c +36 -18
  49. package/csrc/op_group_agg.c +1790 -119
  50. package/csrc/op_hash.c +48 -15
  51. package/csrc/op_head.c +21 -86
  52. package/csrc/op_interpolate.c +268 -62
  53. package/csrc/op_join.c +2700 -182
  54. package/csrc/op_json_extract.c +227 -0
  55. package/csrc/op_json_filter.c +384 -0
  56. package/csrc/op_json_flatten.c +293 -0
  57. package/csrc/op_json_schema.c +503 -0
  58. package/csrc/op_label_encode.c +328 -53
  59. package/csrc/op_lag.c +181 -0
  60. package/csrc/op_lead.c +141 -89
  61. package/csrc/op_normalize.c +363 -79
  62. package/csrc/op_onehot.c +345 -73
  63. package/csrc/op_pivot.c +1546 -162
  64. package/csrc/op_quarantine.c +189 -0
  65. package/csrc/op_registry.c +2062 -166
  66. package/csrc/op_rename.c +41 -50
  67. package/csrc/op_replace.c +270 -118
  68. package/csrc/op_rleid.c +297 -0
  69. package/csrc/op_rowid.c +559 -0
  70. package/csrc/op_sample.c +80 -23
  71. package/csrc/op_schema.c +1341 -0
  72. package/csrc/op_schema_infer.c +252 -0
  73. package/csrc/op_select.c +265 -65
  74. package/csrc/op_set.c +3449 -0
  75. package/csrc/op_skip.c +30 -87
  76. package/csrc/op_sort.c +670 -124
  77. package/csrc/op_source_name.c +120 -0
  78. package/csrc/op_split.c +65 -28
  79. package/csrc/op_split_data.c +41 -9
  80. package/csrc/op_stack.c +178 -222
  81. package/csrc/op_stats.c +206 -110
  82. package/csrc/op_step.c +217 -55
  83. package/csrc/op_tail.c +21 -12
  84. package/csrc/op_tee.c +338 -0
  85. package/csrc/op_top.c +260 -53
  86. package/csrc/op_trim.c +48 -19
  87. package/csrc/op_unique.c +1193 -150
  88. package/csrc/op_unpivot.c +100 -66
  89. package/csrc/op_validate.c +601 -24
  90. package/csrc/op_window.c +492 -51
  91. package/csrc/path_policy.c +85 -0
  92. package/csrc/pipeline.c +872 -99
  93. package/csrc/recipes.c +3 -1
  94. package/csrc/report.c +73 -30
  95. package/csrc/selector.c +1097 -0
  96. package/csrc/size_utils.c +348 -0
  97. package/csrc/spill.c +317 -0
  98. package/csrc/spill.h +21 -0
  99. package/csrc/tranfi.h +169 -1
  100. package/csrc/transform.h +209 -0
  101. package/csrc/transform_api.c +2237 -0
  102. package/csrc/transform_categorical.c +923 -0
  103. package/csrc/transform_internal.h +472 -0
  104. package/csrc/transform_json.c +3812 -0
  105. package/csrc/transform_numeric.c +1966 -0
  106. package/csrc/transform_sha256.c +154 -0
  107. package/csrc/transform_wasm.h +162 -0
  108. package/csrc/transform_wasm_api.c +1373 -0
  109. package/csrc/wasm_api.c +70 -9
  110. package/napi_api.c +219 -11
  111. package/napi_transform.c +1648 -0
  112. package/napi_transform.h +8 -0
  113. package/package.json +27 -11
  114. package/scripts/install-native.js +76 -0
  115. package/scripts/prepack.js +64 -0
  116. package/scripts/sync-csrc.js +23 -0
  117. package/src/cli.js +8 -11
  118. package/src/engines/duckdb.js +45 -12
  119. package/src/index.js +661 -42
  120. package/src/memory_policy.js +411 -0
  121. package/src/native.js +1 -5
  122. package/src/pipeline.js +454 -31
  123. package/src/recipe_json.js +80 -0
  124. package/src/server.js +10 -8
  125. package/src/transform.js +403 -0
  126. package/src/transform_error.js +10 -0
  127. package/src/wasm.js +6 -4
  128. package/wasm/index.js +498 -10
  129. package/wasm/tranfi_core.js +0 -0
  130. package/wasm/transform.js +1156 -0
  131. package/wasm/worker.js +786 -0
  132. package/csrc/plan.c +0 -206
@@ -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,15 +1,25 @@
1
1
  {
2
2
  "name": "tranfi",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Streaming ETL language + runtime",
5
- "type": "module",
5
+ "type": "commonjs",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "tranfi": "src/cli.js"
9
9
  },
10
10
  "exports": {
11
- ".": "./src/index.js",
12
- "./wasm": "./wasm/index.js"
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
- "README.md"
33
+ "scripts/",
34
+ "README.md",
35
+ "LICENSE",
36
+ "NOTICE"
22
37
  ],
23
38
  "scripts": {
24
- "prepack": "rm -rf csrc && mkdir -p csrc && cp ../src/*.c ../src/*.h csrc/ && rm -rf app && cp -r ../app/dist app && rm -rf app/wasm app/lib",
25
- "install": "node-gyp rebuild 2>/dev/null || true",
26
- "build:native": "rm -rf csrc && mkdir -p csrc && cp ../src/*.c ../src/*.h csrc/ && node-gyp rebuild",
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": "MIT",
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
- import { pipeline, compileDsl, recipes } from './index.js'
13
- import { readFileSync, writeFileSync, existsSync } from 'fs'
14
- import { resolve, dirname } from 'path'
15
- import { fileURLToPath } from 'url'
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 | sort age | csv' # sort by column
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/ → ../../app/dist
51
- resolve(__dirname, '../app'), // npm package: src/ → ../app/
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 } = await import('./server.js')
57
+ const { startServer } = require('./server.js')
61
58
  let dataDir = '.'
62
59
  let appDir = null
63
60
  let port = 3000
@@ -5,26 +5,27 @@
5
5
  * Install: npm install duckdb
6
6
  */
7
7
 
8
- import { writeFile, unlink } from 'fs/promises'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { randomBytes } from 'crypto'
12
- import { PipelineResult } from '../pipeline.js'
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 } = await import('../pipeline.js')
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 = (await import('duckdb')).default || await import('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 = String(v)
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
- export function getEngine(name) {
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 }