spindb 0.17.2 → 0.18.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/README.md +43 -2
- package/bin/cli.js +15 -8
- package/cli/commands/create.ts +1 -0
- package/cli/commands/engines.ts +62 -1
- package/cli/commands/menu/engine-handlers.ts +17 -0
- package/cli/commands/menu/shell-handlers.ts +14 -0
- package/cli/constants.ts +1 -0
- package/cli/helpers.ts +70 -0
- package/config/backup-formats.ts +16 -0
- package/config/engine-defaults.ts +14 -0
- package/config/engines.json +16 -0
- package/config/os-dependencies.ts +36 -0
- package/core/platform-service.ts +68 -0
- package/engines/base-engine.ts +9 -0
- package/engines/clickhouse/backup.ts +321 -0
- package/engines/clickhouse/binary-manager.ts +424 -0
- package/engines/clickhouse/binary-urls.ts +142 -0
- package/engines/clickhouse/cli-utils.ts +176 -0
- package/engines/clickhouse/hostdb-releases.ts +187 -0
- package/engines/clickhouse/index.ts +868 -0
- package/engines/clickhouse/restore.ts +410 -0
- package/engines/clickhouse/version-maps.ts +92 -0
- package/engines/clickhouse/version-validator.ts +153 -0
- package/engines/index.ts +4 -0
- package/package.json +3 -2
- package/types/index.ts +6 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
**The first npm CLI for running local databases without Docker.**
|
|
9
9
|
|
|
10
|
-
Spin up PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, Redis, and
|
|
10
|
+
Spin up PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, Redis, Valkey, and ClickHouse instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -343,6 +343,46 @@ spindb connect myvalkey --iredis
|
|
|
343
343
|
|
|
344
344
|
**Note:** Valkey uses `redis://` connection scheme for client compatibility since it's wire-compatible with Redis.
|
|
345
345
|
|
|
346
|
+
#### ClickHouse
|
|
347
|
+
|
|
348
|
+
| | |
|
|
349
|
+
|---|---|
|
|
350
|
+
| Versions | 25.12 |
|
|
351
|
+
| Default port | 9000 (native TCP), 8123 (HTTP) |
|
|
352
|
+
| Default user | `default` |
|
|
353
|
+
| Binary source | [hostdb](https://github.com/robertjbass/hostdb) |
|
|
354
|
+
|
|
355
|
+
ClickHouse is a column-oriented OLAP database designed for fast analytics on large datasets. SpinDB downloads ClickHouse server binaries automatically from [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases.
|
|
356
|
+
|
|
357
|
+
**Note:** ClickHouse is only available on macOS and Linux. Windows is not supported.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Create a ClickHouse container (downloads binaries automatically)
|
|
361
|
+
spindb create mydb --engine clickhouse
|
|
362
|
+
|
|
363
|
+
# Create with specific version
|
|
364
|
+
spindb create mydb --engine clickhouse --version 25.12
|
|
365
|
+
|
|
366
|
+
# Check what's available
|
|
367
|
+
spindb deps check --engine clickhouse
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
ClickHouse uses SQL (with ClickHouse-specific extensions):
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Create a table
|
|
374
|
+
spindb run mych -c "CREATE TABLE users (id UInt64, name String) ENGINE = MergeTree() ORDER BY id"
|
|
375
|
+
|
|
376
|
+
# Insert data
|
|
377
|
+
spindb run mych -c "INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob')"
|
|
378
|
+
|
|
379
|
+
# Query data
|
|
380
|
+
spindb run mych -c "SELECT * FROM users"
|
|
381
|
+
|
|
382
|
+
# Run a SQL file
|
|
383
|
+
spindb run mych --file ./scripts/seed.sql
|
|
384
|
+
```
|
|
385
|
+
|
|
346
386
|
### hostdb Platform Coverage
|
|
347
387
|
|
|
348
388
|
SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/hostdb), a repository of pre-built database binaries for all major platforms. The following table shows current platform support and integration status:
|
|
@@ -363,6 +403,7 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
|
|
|
363
403
|
| MongoDB* | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
364
404
|
| Redis* | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
365
405
|
| Valkey | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
406
|
+
| ClickHouse* | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
366
407
|
| **Planned for hostdb** |||||
|
|
367
408
|
| CockroachDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
368
409
|
| TimescaleDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
@@ -375,7 +416,6 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
|
|
|
375
416
|
| ArangoDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
376
417
|
| Qdrant | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
377
418
|
| Apache Cassandra | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
378
|
-
| ClickHouse | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
379
419
|
| InfluxDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
380
420
|
| CouchDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
381
421
|
| KeyDB | 🟪 | 🟪 | 🟪 | 🟪 | 🟪 |
|
|
@@ -386,6 +426,7 @@ SpinDB downloads database binaries from [hostdb](https://github.com/robertjbass/
|
|
|
386
426
|
**Notes:**
|
|
387
427
|
- **\*** Licensing considerations for commercial use — consider Valkey (Redis) or FerretDB (MongoDB) as alternatives
|
|
388
428
|
- **PostgreSQL** uses [EDB](https://www.enterprisedb.com/) binaries on Windows instead of hostdb
|
|
429
|
+
- **ClickHouse** Windows binaries are not available on hostdb (macOS and Linux only)
|
|
389
430
|
- **Valkey** is a Redis-compatible drop-in replacement with permissive licensing
|
|
390
431
|
- **CockroachDB** is planned for both hostdb and SpinDB (see [roadmap](TODO.md))
|
|
391
432
|
- All databases under "Planned for hostdb" have permissive open-source licenses (Apache 2.0, MIT, or BSD)
|
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
7
8
|
|
|
8
9
|
// Get the directory of this file
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -20,18 +21,24 @@ const mainScript = join(packageRoot, 'cli', 'bin.ts')
|
|
|
20
21
|
// 3. Arguments pass through without shell interpretation (shell: false)
|
|
21
22
|
// 4. Works on Windows without needing to spawn .cmd files
|
|
22
23
|
|
|
23
|
-
// Find tsx ESM loader
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
join(packageRoot, 'node_modules', 'tsx', 'dist', 'loader.mjs'),
|
|
27
|
-
]
|
|
24
|
+
// Find tsx ESM loader using Node's module resolution
|
|
25
|
+
// This works with npm, pnpm, yarn regardless of hoisting/symlink structure
|
|
26
|
+
let tsxLoader = null
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
try {
|
|
29
|
+
const require = createRequire(import.meta.url)
|
|
30
|
+
const tsxDir = dirname(require.resolve('tsx/package.json'))
|
|
31
|
+
const loaderPaths = [
|
|
32
|
+
join(tsxDir, 'dist', 'esm', 'index.mjs'),
|
|
33
|
+
join(tsxDir, 'dist', 'loader.mjs'),
|
|
34
|
+
]
|
|
35
|
+
tsxLoader = loaderPaths.find((p) => existsSync(p))
|
|
36
|
+
} catch {
|
|
37
|
+
// tsx not found via module resolution
|
|
38
|
+
}
|
|
30
39
|
|
|
31
40
|
if (!tsxLoader) {
|
|
32
41
|
console.error('Error: tsx loader not found.')
|
|
33
|
-
console.error('Searched paths:')
|
|
34
|
-
tsxLoaderPaths.forEach((p) => console.error(` - ${p}`))
|
|
35
42
|
console.error('\nTry running: pnpm install')
|
|
36
43
|
process.exit(1)
|
|
37
44
|
}
|
package/cli/commands/create.ts
CHANGED
|
@@ -515,6 +515,7 @@ export const createCommand = new Command('create')
|
|
|
515
515
|
|
|
516
516
|
try {
|
|
517
517
|
await dbEngine.initDataDir(containerName, version, {
|
|
518
|
+
port,
|
|
518
519
|
superuser: engineDefaults.superuser,
|
|
519
520
|
maxConnections: options.maxConnections
|
|
520
521
|
? parseInt(options.maxConnections, 10)
|
package/cli/commands/engines.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { mongodbBinaryManager } from '../../engines/mongodb/binary-manager'
|
|
|
51
51
|
import { redisBinaryManager } from '../../engines/redis/binary-manager'
|
|
52
52
|
import { valkeyBinaryManager } from '../../engines/valkey/binary-manager'
|
|
53
53
|
import { sqliteBinaryManager } from '../../engines/sqlite/binary-manager'
|
|
54
|
+
import { clickhouseBinaryManager } from '../../engines/clickhouse/binary-manager'
|
|
54
55
|
|
|
55
56
|
// Pad string to width, accounting for emoji taking 2 display columns
|
|
56
57
|
function padWithEmoji(str: string, width: number): string {
|
|
@@ -1218,9 +1219,69 @@ enginesCommand
|
|
|
1218
1219
|
return
|
|
1219
1220
|
}
|
|
1220
1221
|
|
|
1222
|
+
if (['clickhouse', 'ch'].includes(normalizedEngine)) {
|
|
1223
|
+
// Check platform support
|
|
1224
|
+
const { platform } = platformService.getPlatformInfo()
|
|
1225
|
+
if (platform === 'win32') {
|
|
1226
|
+
console.error(
|
|
1227
|
+
uiError('ClickHouse is not supported on Windows via hostdb'),
|
|
1228
|
+
)
|
|
1229
|
+
console.log(
|
|
1230
|
+
chalk.gray(
|
|
1231
|
+
' ClickHouse binaries are only available for macOS and Linux.',
|
|
1232
|
+
),
|
|
1233
|
+
)
|
|
1234
|
+
process.exit(1)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (!version) {
|
|
1238
|
+
console.error(uiError('ClickHouse requires a version (e.g., 25.12)'))
|
|
1239
|
+
process.exit(1)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const engine = getEngine(Engine.ClickHouse)
|
|
1243
|
+
|
|
1244
|
+
const spinner = createSpinner(
|
|
1245
|
+
`Checking ClickHouse ${version} binaries...`,
|
|
1246
|
+
)
|
|
1247
|
+
spinner.start()
|
|
1248
|
+
|
|
1249
|
+
let wasCached = false
|
|
1250
|
+
await engine.ensureBinaries(version, ({ stage, message }) => {
|
|
1251
|
+
if (stage === 'cached') {
|
|
1252
|
+
wasCached = true
|
|
1253
|
+
spinner.text = `ClickHouse ${version} binaries ready (cached)`
|
|
1254
|
+
} else {
|
|
1255
|
+
spinner.text = message
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
if (wasCached) {
|
|
1260
|
+
spinner.succeed(`ClickHouse ${version} binaries already installed`)
|
|
1261
|
+
} else {
|
|
1262
|
+
spinner.succeed(`ClickHouse ${version} binaries downloaded`)
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Show the path for reference
|
|
1266
|
+
const { platform: chPlatform, arch: chArch } =
|
|
1267
|
+
platformService.getPlatformInfo()
|
|
1268
|
+
const chFullVersion = clickhouseBinaryManager.getFullVersion(version)
|
|
1269
|
+
const binPath = paths.getBinaryPath({
|
|
1270
|
+
engine: 'clickhouse',
|
|
1271
|
+
version: chFullVersion,
|
|
1272
|
+
platform: chPlatform,
|
|
1273
|
+
arch: chArch,
|
|
1274
|
+
})
|
|
1275
|
+
console.log(chalk.gray(` Location: ${binPath}`))
|
|
1276
|
+
|
|
1277
|
+
// Check for bundled client tools and install missing ones
|
|
1278
|
+
await checkAndInstallClientTools('clickhouse', binPath)
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1221
1282
|
console.error(
|
|
1222
1283
|
uiError(
|
|
1223
|
-
`Unknown engine "${engineName}". Supported: postgresql, mysql, sqlite, mongodb, redis, valkey`,
|
|
1284
|
+
`Unknown engine "${engineName}". Supported: postgresql, mysql, sqlite, mongodb, redis, valkey, clickhouse`,
|
|
1224
1285
|
),
|
|
1225
1286
|
)
|
|
1226
1287
|
process.exit(1)
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type InstalledMongodbEngine,
|
|
17
17
|
type InstalledRedisEngine,
|
|
18
18
|
type InstalledValkeyEngine,
|
|
19
|
+
type InstalledClickHouseEngine,
|
|
19
20
|
} from '../../helpers'
|
|
20
21
|
|
|
21
22
|
import { type MenuChoice } from './shared'
|
|
@@ -55,6 +56,7 @@ export async function handleEngines(): Promise<void> {
|
|
|
55
56
|
mongodbEngines,
|
|
56
57
|
redisEngines,
|
|
57
58
|
valkeyEngines,
|
|
59
|
+
clickhouseEngines,
|
|
58
60
|
] = [
|
|
59
61
|
engines.filter(
|
|
60
62
|
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
@@ -65,6 +67,9 @@ export async function handleEngines(): Promise<void> {
|
|
|
65
67
|
engines.filter((e): e is InstalledMongodbEngine => e.engine === 'mongodb'),
|
|
66
68
|
engines.filter((e): e is InstalledRedisEngine => e.engine === 'redis'),
|
|
67
69
|
engines.filter((e): e is InstalledValkeyEngine => e.engine === 'valkey'),
|
|
70
|
+
engines.filter(
|
|
71
|
+
(e): e is InstalledClickHouseEngine => e.engine === 'clickhouse',
|
|
72
|
+
),
|
|
68
73
|
]
|
|
69
74
|
|
|
70
75
|
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
@@ -80,6 +85,10 @@ export async function handleEngines(): Promise<void> {
|
|
|
80
85
|
)
|
|
81
86
|
const totalRedisSize = redisEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
82
87
|
const totalValkeySize = valkeyEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
88
|
+
const totalClickhouseSize = clickhouseEngines.reduce(
|
|
89
|
+
(acc, e) => acc + e.sizeBytes,
|
|
90
|
+
0,
|
|
91
|
+
)
|
|
83
92
|
|
|
84
93
|
const COL_ENGINE = 14
|
|
85
94
|
const COL_VERSION = 12
|
|
@@ -104,6 +113,7 @@ export async function handleEngines(): Promise<void> {
|
|
|
104
113
|
...mongodbEngines,
|
|
105
114
|
...redisEngines,
|
|
106
115
|
...valkeyEngines,
|
|
116
|
+
...clickhouseEngines,
|
|
107
117
|
]
|
|
108
118
|
|
|
109
119
|
for (const engine of allEnginesSorted) {
|
|
@@ -172,6 +182,13 @@ export async function handleEngines(): Promise<void> {
|
|
|
172
182
|
),
|
|
173
183
|
)
|
|
174
184
|
}
|
|
185
|
+
if (clickhouseEngines.length > 0) {
|
|
186
|
+
console.log(
|
|
187
|
+
chalk.gray(
|
|
188
|
+
` ClickHouse: ${clickhouseEngines.length} version(s), ${formatBytes(totalClickhouseSize)}`,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
175
192
|
console.log()
|
|
176
193
|
|
|
177
194
|
const choices: MenuChoice[] = allEnginesSorted.map((e) => ({
|
|
@@ -507,6 +507,20 @@ async function launchShell(
|
|
|
507
507
|
config.database,
|
|
508
508
|
]
|
|
509
509
|
installHint = 'spindb engines download valkey'
|
|
510
|
+
} else if (config.engine === 'clickhouse') {
|
|
511
|
+
// ClickHouse uses a unified binary with subcommands
|
|
512
|
+
const clickhousePath = await configManager.getBinaryPath('clickhouse')
|
|
513
|
+
shellCmd = clickhousePath || 'clickhouse'
|
|
514
|
+
shellArgs = [
|
|
515
|
+
'client',
|
|
516
|
+
'--host',
|
|
517
|
+
'127.0.0.1',
|
|
518
|
+
'--port',
|
|
519
|
+
String(config.port),
|
|
520
|
+
'--database',
|
|
521
|
+
config.database,
|
|
522
|
+
]
|
|
523
|
+
installHint = 'spindb engines download clickhouse'
|
|
510
524
|
} else {
|
|
511
525
|
shellCmd = 'psql'
|
|
512
526
|
shellArgs = [connectionString]
|
package/cli/constants.ts
CHANGED
package/cli/helpers.ts
CHANGED
|
@@ -134,6 +134,16 @@ export type InstalledValkeyEngine = {
|
|
|
134
134
|
source: 'downloaded'
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
export type InstalledClickHouseEngine = {
|
|
138
|
+
engine: 'clickhouse'
|
|
139
|
+
version: string
|
|
140
|
+
platform: string
|
|
141
|
+
arch: string
|
|
142
|
+
path: string
|
|
143
|
+
sizeBytes: number
|
|
144
|
+
source: 'downloaded'
|
|
145
|
+
}
|
|
146
|
+
|
|
137
147
|
export type InstalledEngine =
|
|
138
148
|
| InstalledPostgresEngine
|
|
139
149
|
| InstalledMariadbEngine
|
|
@@ -142,6 +152,7 @@ export type InstalledEngine =
|
|
|
142
152
|
| InstalledMongodbEngine
|
|
143
153
|
| InstalledRedisEngine
|
|
144
154
|
| InstalledValkeyEngine
|
|
155
|
+
| InstalledClickHouseEngine
|
|
145
156
|
|
|
146
157
|
async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
147
158
|
const ext = platformService.getExecutableExtension()
|
|
@@ -535,6 +546,61 @@ async function getInstalledValkeyEngines(): Promise<InstalledValkeyEngine[]> {
|
|
|
535
546
|
return engines
|
|
536
547
|
}
|
|
537
548
|
|
|
549
|
+
// Get ClickHouse version from binary path
|
|
550
|
+
async function getClickHouseVersion(binPath: string): Promise<string | null> {
|
|
551
|
+
const clickhousePath = join(binPath, 'bin', 'clickhouse')
|
|
552
|
+
if (!existsSync(clickhousePath)) {
|
|
553
|
+
return null
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const { stdout } = await execFileAsync(clickhousePath, ['client', '--version'])
|
|
558
|
+
// Parse output like "ClickHouse client version 25.12.3.21 (official build)"
|
|
559
|
+
const match = stdout.match(/version\s+([\d.]+)/)
|
|
560
|
+
return match ? match[1] : null
|
|
561
|
+
} catch {
|
|
562
|
+
return null
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Get installed ClickHouse engines from downloaded binaries
|
|
567
|
+
async function getInstalledClickHouseEngines(): Promise<InstalledClickHouseEngine[]> {
|
|
568
|
+
const binDir = paths.bin
|
|
569
|
+
|
|
570
|
+
if (!existsSync(binDir)) {
|
|
571
|
+
return []
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const entries = await readdir(binDir, { withFileTypes: true })
|
|
575
|
+
const engines: InstalledClickHouseEngine[] = []
|
|
576
|
+
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
if (!entry.isDirectory()) continue
|
|
579
|
+
if (!entry.name.startsWith('clickhouse-')) continue
|
|
580
|
+
|
|
581
|
+
const parsed = parseEngineDirectory(entry.name, 'clickhouse-', binDir)
|
|
582
|
+
if (!parsed) continue
|
|
583
|
+
|
|
584
|
+
const actualVersion =
|
|
585
|
+
(await getClickHouseVersion(parsed.path)) || parsed.version
|
|
586
|
+
const sizeBytes = await calculateDirectorySize(parsed.path)
|
|
587
|
+
|
|
588
|
+
engines.push({
|
|
589
|
+
engine: 'clickhouse',
|
|
590
|
+
version: actualVersion,
|
|
591
|
+
platform: parsed.platform,
|
|
592
|
+
arch: parsed.arch,
|
|
593
|
+
path: parsed.path,
|
|
594
|
+
sizeBytes,
|
|
595
|
+
source: 'downloaded',
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
engines.sort((a, b) => compareVersions(b.version, a.version))
|
|
600
|
+
|
|
601
|
+
return engines
|
|
602
|
+
}
|
|
603
|
+
|
|
538
604
|
export function compareVersions(a: string, b: string): number {
|
|
539
605
|
const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
|
|
540
606
|
const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
|
|
@@ -571,6 +637,9 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
|
571
637
|
const valkeyEngines = await getInstalledValkeyEngines()
|
|
572
638
|
engines.push(...valkeyEngines)
|
|
573
639
|
|
|
640
|
+
const clickhouseEngines = await getInstalledClickHouseEngines()
|
|
641
|
+
engines.push(...clickhouseEngines)
|
|
642
|
+
|
|
574
643
|
return engines
|
|
575
644
|
}
|
|
576
645
|
|
|
@@ -581,4 +650,5 @@ export {
|
|
|
581
650
|
getInstalledMongodbEngines,
|
|
582
651
|
getInstalledRedisEngines,
|
|
583
652
|
getInstalledValkeyEngines,
|
|
653
|
+
getInstalledClickHouseEngines,
|
|
584
654
|
}
|
package/config/backup-formats.ts
CHANGED
|
@@ -122,6 +122,22 @@ export const BACKUP_FORMATS: Record<string, EngineBackupFormats> = {
|
|
|
122
122
|
supportsFormatChoice: true,
|
|
123
123
|
defaultFormat: 'dump',
|
|
124
124
|
},
|
|
125
|
+
clickhouse: {
|
|
126
|
+
sql: {
|
|
127
|
+
extension: '.sql',
|
|
128
|
+
label: '.sql',
|
|
129
|
+
description: 'SQL dump - DDL + INSERT statements',
|
|
130
|
+
spinnerLabel: 'SQL',
|
|
131
|
+
},
|
|
132
|
+
dump: {
|
|
133
|
+
extension: '.sql',
|
|
134
|
+
label: '.sql',
|
|
135
|
+
description: 'SQL dump - DDL + INSERT statements',
|
|
136
|
+
spinnerLabel: 'SQL',
|
|
137
|
+
},
|
|
138
|
+
supportsFormatChoice: false, // Only SQL format supported
|
|
139
|
+
defaultFormat: 'sql',
|
|
140
|
+
},
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
// Get backup format info for an engine
|
|
@@ -129,6 +129,20 @@ export const engineDefaults: Record<string, EngineDefaults> = {
|
|
|
129
129
|
clientTools: ['valkey-cli'],
|
|
130
130
|
maxConnections: 0, // Not applicable for Valkey
|
|
131
131
|
},
|
|
132
|
+
clickhouse: {
|
|
133
|
+
defaultVersion: '25.12',
|
|
134
|
+
defaultPort: 9000, // Native TCP port (HTTP is 8123)
|
|
135
|
+
portRange: { start: 9000, end: 9100 },
|
|
136
|
+
supportedVersions: ['25.12'], // Keep in sync with engines/clickhouse/version-maps.ts
|
|
137
|
+
latestVersion: '25.12',
|
|
138
|
+
superuser: 'default', // Default user in ClickHouse
|
|
139
|
+
connectionScheme: 'clickhouse',
|
|
140
|
+
logFileName: 'clickhouse-server.log',
|
|
141
|
+
pidFileName: 'clickhouse.pid',
|
|
142
|
+
dataSubdir: 'data',
|
|
143
|
+
clientTools: ['clickhouse'],
|
|
144
|
+
maxConnections: 0, // Not applicable
|
|
145
|
+
},
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/**
|
package/config/engines.json
CHANGED
|
@@ -104,6 +104,22 @@
|
|
|
104
104
|
"clientTools": ["valkey-server", "valkey-cli"],
|
|
105
105
|
"licensing": "BSD-3-Clause",
|
|
106
106
|
"notes": "Redis fork with permissive licensing. Shares default port 6379 with Redis; port conflicts are resolved automatically."
|
|
107
|
+
},
|
|
108
|
+
"clickhouse": {
|
|
109
|
+
"displayName": "ClickHouse",
|
|
110
|
+
"icon": "🏠",
|
|
111
|
+
"status": "integrated",
|
|
112
|
+
"binarySource": "hostdb",
|
|
113
|
+
"supportedVersions": ["25.12.3.21"],
|
|
114
|
+
"defaultVersion": "25.12.3.21",
|
|
115
|
+
"defaultPort": 9000,
|
|
116
|
+
"runtime": "server",
|
|
117
|
+
"queryLanguage": "sql",
|
|
118
|
+
"connectionScheme": "clickhouse",
|
|
119
|
+
"superuser": "default",
|
|
120
|
+
"clientTools": ["clickhouse"],
|
|
121
|
+
"licensing": "Apache-2.0",
|
|
122
|
+
"notes": "Column-oriented OLAP database. Uses YY.MM.X.build versioning. Native port 9000, HTTP port 8123."
|
|
107
123
|
}
|
|
108
124
|
}
|
|
109
125
|
}
|
|
@@ -651,6 +651,41 @@ const valkeyDependencies: EngineDependencies = {
|
|
|
651
651
|
],
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// =============================================================================
|
|
655
|
+
// ClickHouse Dependencies
|
|
656
|
+
// =============================================================================
|
|
657
|
+
|
|
658
|
+
const clickhouseDependencies: EngineDependencies = {
|
|
659
|
+
engine: 'clickhouse',
|
|
660
|
+
displayName: 'ClickHouse',
|
|
661
|
+
dependencies: [
|
|
662
|
+
{
|
|
663
|
+
name: 'clickhouse',
|
|
664
|
+
binary: 'clickhouse',
|
|
665
|
+
description: 'ClickHouse server and client (unified binary)',
|
|
666
|
+
packages: {
|
|
667
|
+
brew: { package: 'clickhouse' },
|
|
668
|
+
// ClickHouse requires their own apt repository
|
|
669
|
+
},
|
|
670
|
+
manualInstall: {
|
|
671
|
+
darwin: [
|
|
672
|
+
'Install with Homebrew: brew install clickhouse',
|
|
673
|
+
'Or use SpinDB: spindb engines download clickhouse 25.12',
|
|
674
|
+
],
|
|
675
|
+
linux: [
|
|
676
|
+
'ClickHouse provides official packages.',
|
|
677
|
+
'Add their apt repository: https://clickhouse.com/docs/en/install#install-from-deb-packages',
|
|
678
|
+
'Or use SpinDB: spindb engines download clickhouse 25.12',
|
|
679
|
+
],
|
|
680
|
+
win32: [
|
|
681
|
+
'ClickHouse does not officially support Windows.',
|
|
682
|
+
'Use WSL2 with Linux installation instructions.',
|
|
683
|
+
],
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
}
|
|
688
|
+
|
|
654
689
|
// =============================================================================
|
|
655
690
|
// Optional Tools (engine-agnostic)
|
|
656
691
|
// =============================================================================
|
|
@@ -807,6 +842,7 @@ export const engineDependencies: EngineDependencies[] = [
|
|
|
807
842
|
mongodbDependencies,
|
|
808
843
|
redisDependencies,
|
|
809
844
|
valkeyDependencies,
|
|
845
|
+
clickhouseDependencies,
|
|
810
846
|
]
|
|
811
847
|
|
|
812
848
|
// Get dependencies for a specific engine
|
package/core/platform-service.ts
CHANGED
|
@@ -120,6 +120,13 @@ export abstract class BasePlatformService {
|
|
|
120
120
|
// Check if a process is running by PID
|
|
121
121
|
abstract isProcessRunning(pid: number): boolean
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Find process PIDs listening on a specific port
|
|
125
|
+
* @param port - Port number to check
|
|
126
|
+
* @returns Array of PIDs listening on the port (empty if none found)
|
|
127
|
+
*/
|
|
128
|
+
abstract findProcessByPort(port: number): Promise<number[]>
|
|
129
|
+
|
|
123
130
|
// Copy text to clipboard
|
|
124
131
|
async copyToClipboard(text: string): Promise<boolean> {
|
|
125
132
|
const config = this.getClipboardConfig()
|
|
@@ -344,6 +351,21 @@ class DarwinPlatformService extends BasePlatformService {
|
|
|
344
351
|
}
|
|
345
352
|
}
|
|
346
353
|
|
|
354
|
+
async findProcessByPort(port: number): Promise<number[]> {
|
|
355
|
+
try {
|
|
356
|
+
const { stdout } = await execAsync(`lsof -ti tcp:${port} 2>/dev/null || true`)
|
|
357
|
+
const pids = stdout
|
|
358
|
+
.trim()
|
|
359
|
+
.split('\n')
|
|
360
|
+
.filter(Boolean)
|
|
361
|
+
.map((pid) => parseInt(pid, 10))
|
|
362
|
+
.filter((pid) => !isNaN(pid))
|
|
363
|
+
return pids
|
|
364
|
+
} catch {
|
|
365
|
+
return []
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
347
369
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
348
370
|
return `${dir}/${toolName}`
|
|
349
371
|
}
|
|
@@ -544,6 +566,21 @@ class LinuxPlatformService extends BasePlatformService {
|
|
|
544
566
|
}
|
|
545
567
|
}
|
|
546
568
|
|
|
569
|
+
async findProcessByPort(port: number): Promise<number[]> {
|
|
570
|
+
try {
|
|
571
|
+
const { stdout } = await execAsync(`lsof -ti tcp:${port} 2>/dev/null || true`)
|
|
572
|
+
const pids = stdout
|
|
573
|
+
.trim()
|
|
574
|
+
.split('\n')
|
|
575
|
+
.filter(Boolean)
|
|
576
|
+
.map((pid) => parseInt(pid, 10))
|
|
577
|
+
.filter((pid) => !isNaN(pid))
|
|
578
|
+
return pids
|
|
579
|
+
} catch {
|
|
580
|
+
return []
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
547
584
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
548
585
|
return `${dir}/${toolName}`
|
|
549
586
|
}
|
|
@@ -700,6 +737,37 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
700
737
|
}
|
|
701
738
|
}
|
|
702
739
|
|
|
740
|
+
async findProcessByPort(port: number): Promise<number[]> {
|
|
741
|
+
try {
|
|
742
|
+
// Use netstat to find PIDs listening on the port
|
|
743
|
+
// -a = all connections, -n = numeric, -o = owner PID
|
|
744
|
+
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`)
|
|
745
|
+
const pids: number[] = []
|
|
746
|
+
|
|
747
|
+
// Parse netstat output to find LISTENING processes on this exact port
|
|
748
|
+
// Format: TCP 0.0.0.0:PORT 0.0.0.0:0 LISTENING PID
|
|
749
|
+
const lines = stdout.trim().split('\n')
|
|
750
|
+
for (const line of lines) {
|
|
751
|
+
// Match lines with LISTENING state on the specific port
|
|
752
|
+
const parts = line.trim().split(/\s+/)
|
|
753
|
+
if (parts.length >= 5 && parts[3] === 'LISTENING') {
|
|
754
|
+
const localAddress = parts[1]
|
|
755
|
+
// Check if this is the exact port (not just containing the port number)
|
|
756
|
+
if (localAddress.endsWith(`:${port}`)) {
|
|
757
|
+
const pid = parseInt(parts[4], 10)
|
|
758
|
+
if (!isNaN(pid) && !pids.includes(pid)) {
|
|
759
|
+
pids.push(pid)
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return pids
|
|
766
|
+
} catch {
|
|
767
|
+
return []
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
703
771
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
704
772
|
return `${dir}\\${toolName}.exe`
|
|
705
773
|
}
|
package/engines/base-engine.ts
CHANGED
|
@@ -122,6 +122,15 @@ export abstract class BaseEngine {
|
|
|
122
122
|
throw new Error('valkey-cli not found')
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Get the path to the clickhouse client if available
|
|
127
|
+
* Default implementation throws; engines that can provide a bundled or
|
|
128
|
+
* configured clickhouse should override this method.
|
|
129
|
+
*/
|
|
130
|
+
async getClickHouseClientPath(): Promise<string> {
|
|
131
|
+
throw new Error('clickhouse client not found')
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
/**
|
|
126
135
|
* Get the path to the sqlite3 client if available
|
|
127
136
|
* Default implementation returns null; SQLite engine overrides this method.
|