spindb 0.36.2 → 0.37.2
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 +19 -8
- package/cli/commands/create.ts +9 -2
- package/cli/commands/databases.ts +17 -12
- package/cli/commands/delete.ts +3 -0
- package/cli/commands/engines.ts +60 -4
- package/cli/commands/info.ts +5 -0
- package/cli/commands/list.ts +2 -0
- package/cli/commands/menu/backup-handlers.ts +2 -0
- package/cli/commands/menu/settings-handlers.ts +3 -0
- package/cli/commands/menu/shell-handlers.ts +23 -0
- package/cli/commands/menu/update-handlers.ts +6 -2
- package/cli/commands/restore.ts +3 -0
- package/cli/commands/start.ts +3 -0
- package/cli/commands/url.ts +4 -0
- package/cli/constants.ts +4 -0
- package/cli/helpers.ts +93 -0
- package/config/backup-formats.ts +14 -0
- package/config/engine-defaults.ts +13 -0
- package/config/engines.json +17 -0
- package/core/config-manager.ts +5 -0
- package/core/dependency-manager.ts +2 -0
- package/core/docker-exporter.ts +17 -0
- package/core/library-env.ts +2 -4
- package/core/update-manager.ts +57 -35
- package/engines/base-engine.ts +8 -0
- package/engines/index.ts +4 -0
- package/engines/mariadb/index.ts +5 -4
- package/engines/redis/index.ts +15 -4
- package/engines/tigerbeetle/README.md +61 -0
- package/engines/tigerbeetle/backup.ts +49 -0
- package/engines/tigerbeetle/binary-manager.ts +95 -0
- package/engines/tigerbeetle/binary-urls.ts +62 -0
- package/engines/tigerbeetle/hostdb-releases.ts +26 -0
- package/engines/tigerbeetle/index.ts +746 -0
- package/engines/tigerbeetle/restore.ts +130 -0
- package/engines/tigerbeetle/version-maps.ts +68 -0
- package/engines/tigerbeetle/version-validator.ts +126 -0
- package/engines/valkey/index.ts +15 -4
- package/package.json +2 -1
- package/types/index.ts +9 -0
package/cli/helpers.ts
CHANGED
|
@@ -5,9 +5,28 @@ import { execFile } from 'child_process'
|
|
|
5
5
|
import { promisify } from 'util'
|
|
6
6
|
import { paths } from '../config/paths'
|
|
7
7
|
import { platformService } from '../core/platform-service'
|
|
8
|
+
import { type Engine } from '../types'
|
|
9
|
+
import { getEngineConfig } from '../config/engines-registry'
|
|
8
10
|
|
|
9
11
|
const execFileAsync = promisify(execFile)
|
|
10
12
|
|
|
13
|
+
export type EngineMetadata = {
|
|
14
|
+
queryLanguage: string
|
|
15
|
+
runtime: 'server' | 'embedded'
|
|
16
|
+
connectionScheme: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getEngineMetadata(
|
|
20
|
+
engine: string,
|
|
21
|
+
): Promise<EngineMetadata> {
|
|
22
|
+
const config = await getEngineConfig(engine as Engine)
|
|
23
|
+
return {
|
|
24
|
+
queryLanguage: config.queryLanguage,
|
|
25
|
+
runtime: config.runtime,
|
|
26
|
+
connectionScheme: config.connectionScheme,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
// Parsed engine directory info
|
|
12
31
|
type ParsedEngineDir = {
|
|
13
32
|
version: string
|
|
@@ -254,6 +273,16 @@ export type InstalledWeaviateEngine = {
|
|
|
254
273
|
source: 'downloaded'
|
|
255
274
|
}
|
|
256
275
|
|
|
276
|
+
export type InstalledTigerBeetleEngine = {
|
|
277
|
+
engine: 'tigerbeetle'
|
|
278
|
+
version: string
|
|
279
|
+
platform: string
|
|
280
|
+
arch: string
|
|
281
|
+
path: string
|
|
282
|
+
sizeBytes: number
|
|
283
|
+
source: 'downloaded'
|
|
284
|
+
}
|
|
285
|
+
|
|
257
286
|
export type InstalledEngine =
|
|
258
287
|
| InstalledPostgresEngine
|
|
259
288
|
| InstalledMariadbEngine
|
|
@@ -274,6 +303,7 @@ export type InstalledEngine =
|
|
|
274
303
|
| InstalledTypeDBEngine
|
|
275
304
|
| InstalledInfluxDBEngine
|
|
276
305
|
| InstalledWeaviateEngine
|
|
306
|
+
| InstalledTigerBeetleEngine
|
|
277
307
|
|
|
278
308
|
async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
279
309
|
const ext = platformService.getExecutableExtension()
|
|
@@ -1347,6 +1377,64 @@ async function getInstalledWeaviateEngines(): Promise<
|
|
|
1347
1377
|
return engines
|
|
1348
1378
|
}
|
|
1349
1379
|
|
|
1380
|
+
// Get TigerBeetle version from binary path
|
|
1381
|
+
async function getTigerBeetleVersion(binPath: string): Promise<string | null> {
|
|
1382
|
+
const ext = platformService.getExecutableExtension()
|
|
1383
|
+
const tigerbeetlePath = join(binPath, 'bin', `tigerbeetle${ext}`)
|
|
1384
|
+
if (!existsSync(tigerbeetlePath)) {
|
|
1385
|
+
return null
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
try {
|
|
1389
|
+
const { stdout } = await execFileAsync(tigerbeetlePath, ['version'])
|
|
1390
|
+
// Parse output like "TigerBeetle v0.16.70" or "0.16.70"
|
|
1391
|
+
const match = stdout.match(/(?:TigerBeetle\s+)?v?(\d+\.\d+\.\d+)/)
|
|
1392
|
+
return match ? match[1] : null
|
|
1393
|
+
} catch {
|
|
1394
|
+
return null
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Get installed TigerBeetle engines from downloaded binaries
|
|
1399
|
+
async function getInstalledTigerBeetleEngines(): Promise<
|
|
1400
|
+
InstalledTigerBeetleEngine[]
|
|
1401
|
+
> {
|
|
1402
|
+
const binDir = paths.bin
|
|
1403
|
+
|
|
1404
|
+
if (!existsSync(binDir)) {
|
|
1405
|
+
return []
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const entries = await readdir(binDir, { withFileTypes: true })
|
|
1409
|
+
const engines: InstalledTigerBeetleEngine[] = []
|
|
1410
|
+
|
|
1411
|
+
for (const entry of entries) {
|
|
1412
|
+
if (!entry.isDirectory()) continue
|
|
1413
|
+
if (!entry.name.startsWith('tigerbeetle-')) continue
|
|
1414
|
+
|
|
1415
|
+
const parsed = parseEngineDirectory(entry.name, 'tigerbeetle-', binDir)
|
|
1416
|
+
if (!parsed) continue
|
|
1417
|
+
|
|
1418
|
+
const actualVersion =
|
|
1419
|
+
(await getTigerBeetleVersion(parsed.path)) || parsed.version
|
|
1420
|
+
const sizeBytes = await calculateDirectorySize(parsed.path)
|
|
1421
|
+
|
|
1422
|
+
engines.push({
|
|
1423
|
+
engine: 'tigerbeetle',
|
|
1424
|
+
version: actualVersion,
|
|
1425
|
+
platform: parsed.platform,
|
|
1426
|
+
arch: parsed.arch,
|
|
1427
|
+
path: parsed.path,
|
|
1428
|
+
sizeBytes,
|
|
1429
|
+
source: 'downloaded',
|
|
1430
|
+
})
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
engines.sort((a, b) => compareVersions(b.version, a.version))
|
|
1434
|
+
|
|
1435
|
+
return engines
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1350
1438
|
export function compareVersions(a: string, b: string): number {
|
|
1351
1439
|
const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
|
|
1352
1440
|
const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
|
|
@@ -1384,6 +1472,7 @@ const ENGINE_PREFIXES = [
|
|
|
1384
1472
|
'typedb-',
|
|
1385
1473
|
'influxdb-',
|
|
1386
1474
|
'weaviate-',
|
|
1475
|
+
'tigerbeetle-',
|
|
1387
1476
|
] as const
|
|
1388
1477
|
|
|
1389
1478
|
/**
|
|
@@ -1433,6 +1522,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
|
1433
1522
|
typedbEngines,
|
|
1434
1523
|
influxdbEngines,
|
|
1435
1524
|
weaviateEngines,
|
|
1525
|
+
tigerbeetleEngines,
|
|
1436
1526
|
] = await Promise.all([
|
|
1437
1527
|
getInstalledPostgresEngines(),
|
|
1438
1528
|
getInstalledMariadbEngines(),
|
|
@@ -1453,6 +1543,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
|
1453
1543
|
getInstalledTypeDBEngines(),
|
|
1454
1544
|
getInstalledInfluxDBEngines(),
|
|
1455
1545
|
getInstalledWeaviateEngines(),
|
|
1546
|
+
getInstalledTigerBeetleEngines(),
|
|
1456
1547
|
])
|
|
1457
1548
|
|
|
1458
1549
|
return [
|
|
@@ -1475,6 +1566,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
|
1475
1566
|
...typedbEngines,
|
|
1476
1567
|
...influxdbEngines,
|
|
1477
1568
|
...weaviateEngines,
|
|
1569
|
+
...tigerbeetleEngines,
|
|
1478
1570
|
]
|
|
1479
1571
|
}
|
|
1480
1572
|
|
|
@@ -1497,4 +1589,5 @@ export {
|
|
|
1497
1589
|
getInstalledTypeDBEngines,
|
|
1498
1590
|
getInstalledInfluxDBEngines,
|
|
1499
1591
|
getInstalledWeaviateEngines,
|
|
1592
|
+
getInstalledTigerBeetleEngines,
|
|
1500
1593
|
}
|
package/config/backup-formats.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type TypeDBFormat,
|
|
30
30
|
type InfluxDBFormat,
|
|
31
31
|
type WeaviateFormat,
|
|
32
|
+
type TigerBeetleFormat,
|
|
32
33
|
type BackupFormatType,
|
|
33
34
|
} from '../types'
|
|
34
35
|
|
|
@@ -67,6 +68,7 @@ export const BACKUP_FORMATS: {
|
|
|
67
68
|
[Engine.TypeDB]: EngineBackupFormats<TypeDBFormat>
|
|
68
69
|
[Engine.InfluxDB]: EngineBackupFormats<InfluxDBFormat>
|
|
69
70
|
[Engine.Weaviate]: EngineBackupFormats<WeaviateFormat>
|
|
71
|
+
[Engine.TigerBeetle]: EngineBackupFormats<TigerBeetleFormat>
|
|
70
72
|
} = {
|
|
71
73
|
[Engine.PostgreSQL]: {
|
|
72
74
|
formats: {
|
|
@@ -350,6 +352,18 @@ export const BACKUP_FORMATS: {
|
|
|
350
352
|
supportsFormatChoice: false, // Only snapshot format supported
|
|
351
353
|
defaultFormat: 'snapshot',
|
|
352
354
|
},
|
|
355
|
+
[Engine.TigerBeetle]: {
|
|
356
|
+
formats: {
|
|
357
|
+
binary: {
|
|
358
|
+
extension: '.tigerbeetle',
|
|
359
|
+
label: '.tigerbeetle',
|
|
360
|
+
description: 'TigerBeetle data file - full database copy',
|
|
361
|
+
spinnerLabel: 'binary',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
supportsFormatChoice: false, // Only binary format supported
|
|
365
|
+
defaultFormat: 'binary',
|
|
366
|
+
},
|
|
353
367
|
}
|
|
354
368
|
|
|
355
369
|
/**
|
|
@@ -278,6 +278,19 @@ export const engineDefaults: Record<Engine, EngineDefaults> = {
|
|
|
278
278
|
clientTools: [], // Weaviate uses REST/GraphQL API, no separate CLI tools
|
|
279
279
|
maxConnections: 0, // Not applicable for vector DB
|
|
280
280
|
},
|
|
281
|
+
[Engine.TigerBeetle]: {
|
|
282
|
+
defaultVersion: '0.16',
|
|
283
|
+
defaultPort: 3000,
|
|
284
|
+
portRange: { start: 3000, end: 3100 },
|
|
285
|
+
latestVersion: '0.16',
|
|
286
|
+
superuser: '', // No auth
|
|
287
|
+
connectionScheme: '', // Custom binary protocol, no URI scheme
|
|
288
|
+
logFileName: 'tigerbeetle.log',
|
|
289
|
+
pidFileName: 'tigerbeetle.pid',
|
|
290
|
+
dataSubdir: 'data',
|
|
291
|
+
clientTools: ['tigerbeetle'], // Single binary serves as both server and REPL
|
|
292
|
+
maxConnections: 0, // Not applicable
|
|
293
|
+
},
|
|
281
294
|
}
|
|
282
295
|
|
|
283
296
|
/**
|
package/config/engines.json
CHANGED
|
@@ -326,6 +326,23 @@
|
|
|
326
326
|
"clientTools": ["weaviate"],
|
|
327
327
|
"licensing": "BSD-3-Clause",
|
|
328
328
|
"notes": "AI-native vector database. REST API on port 8080, gRPC on port+1. Uses classes/collections instead of databases."
|
|
329
|
+
},
|
|
330
|
+
"tigerbeetle": {
|
|
331
|
+
"displayName": "TigerBeetle",
|
|
332
|
+
"icon": "🐯",
|
|
333
|
+
"status": "integrated",
|
|
334
|
+
"binarySource": "hostdb",
|
|
335
|
+
"supportedVersions": ["0.16.70"],
|
|
336
|
+
"defaultVersion": "0.16.70",
|
|
337
|
+
"defaultPort": 3000,
|
|
338
|
+
"runtime": "server",
|
|
339
|
+
"queryLanguage": "custom",
|
|
340
|
+
"scriptFileLabel": null,
|
|
341
|
+
"connectionScheme": null,
|
|
342
|
+
"superuser": null,
|
|
343
|
+
"clientTools": ["tigerbeetle"],
|
|
344
|
+
"licensing": "Apache-2.0",
|
|
345
|
+
"notes": "High-performance financial ledger database. Custom binary protocol, REPL client. Uses --development flag for local dev."
|
|
329
346
|
}
|
|
330
347
|
}
|
|
331
348
|
}
|
package/core/config-manager.ts
CHANGED
|
@@ -84,6 +84,8 @@ const INFLUXDB_TOOLS: BinaryTool[] = ['influxdb3']
|
|
|
84
84
|
|
|
85
85
|
const WEAVIATE_TOOLS: BinaryTool[] = ['weaviate']
|
|
86
86
|
|
|
87
|
+
const TIGERBEETLE_TOOLS: BinaryTool[] = ['tigerbeetle']
|
|
88
|
+
|
|
87
89
|
const PGWEB_TOOLS: BinaryTool[] = ['pgweb']
|
|
88
90
|
|
|
89
91
|
const DBLAB_TOOLS: BinaryTool[] = ['dblab']
|
|
@@ -113,6 +115,7 @@ const ALL_TOOLS: BinaryTool[] = [
|
|
|
113
115
|
...TYPEDB_TOOLS,
|
|
114
116
|
...INFLUXDB_TOOLS,
|
|
115
117
|
...WEAVIATE_TOOLS,
|
|
118
|
+
...TIGERBEETLE_TOOLS,
|
|
116
119
|
...PGWEB_TOOLS,
|
|
117
120
|
...DBLAB_TOOLS,
|
|
118
121
|
...SQLITE_TOOLS,
|
|
@@ -139,6 +142,7 @@ const ENGINE_BINARY_MAP: Partial<Record<Engine, BinaryTool[]>> = {
|
|
|
139
142
|
[Engine.TypeDB]: TYPEDB_TOOLS,
|
|
140
143
|
[Engine.InfluxDB]: INFLUXDB_TOOLS,
|
|
141
144
|
[Engine.Weaviate]: WEAVIATE_TOOLS,
|
|
145
|
+
[Engine.TigerBeetle]: TIGERBEETLE_TOOLS,
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
export class ConfigManager {
|
|
@@ -637,6 +641,7 @@ export {
|
|
|
637
641
|
TYPEDB_TOOLS,
|
|
638
642
|
INFLUXDB_TOOLS,
|
|
639
643
|
WEAVIATE_TOOLS,
|
|
644
|
+
TIGERBEETLE_TOOLS,
|
|
640
645
|
PGWEB_TOOLS,
|
|
641
646
|
DBLAB_TOOLS,
|
|
642
647
|
SQLITE_TOOLS,
|
package/core/docker-exporter.ts
CHANGED
|
@@ -73,6 +73,7 @@ function getEngineDisplayName(engine: Engine): string {
|
|
|
73
73
|
[Engine.TypeDB]: 'TypeDB',
|
|
74
74
|
[Engine.InfluxDB]: 'InfluxDB',
|
|
75
75
|
[Engine.Weaviate]: 'Weaviate',
|
|
76
|
+
[Engine.TigerBeetle]: 'TigerBeetle',
|
|
76
77
|
}
|
|
77
78
|
return displayNames[engine] || engine
|
|
78
79
|
}
|
|
@@ -149,6 +150,9 @@ const _ENGINE_BINARY_CONFIG: Record<
|
|
|
149
150
|
[Engine.Weaviate]: {
|
|
150
151
|
primaryBinaries: [], // REST/GraphQL API only, no CLI tools
|
|
151
152
|
},
|
|
153
|
+
[Engine.TigerBeetle]: {
|
|
154
|
+
primaryBinaries: ['tigerbeetle'],
|
|
155
|
+
},
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
/**
|
|
@@ -218,6 +222,9 @@ function getConnectionStringTemplate(
|
|
|
218
222
|
case Engine.TypeDB:
|
|
219
223
|
return `typedb://<host>:${port}`
|
|
220
224
|
|
|
225
|
+
case Engine.TigerBeetle:
|
|
226
|
+
return `<host>:${port}`
|
|
227
|
+
|
|
221
228
|
case Engine.SQLite:
|
|
222
229
|
case Engine.DuckDB:
|
|
223
230
|
return `File-based database (no network connection)`
|
|
@@ -517,6 +524,13 @@ echo "No authentication required for local InfluxDB 3.x"
|
|
|
517
524
|
userCreationCommands = `
|
|
518
525
|
# TypeDB community edition does not support user management
|
|
519
526
|
echo "No authentication required"
|
|
527
|
+
`
|
|
528
|
+
break
|
|
529
|
+
|
|
530
|
+
case Engine.TigerBeetle:
|
|
531
|
+
userCreationCommands = `
|
|
532
|
+
# TigerBeetle has no authentication
|
|
533
|
+
echo "No authentication required"
|
|
520
534
|
`
|
|
521
535
|
break
|
|
522
536
|
|
|
@@ -1315,6 +1329,9 @@ export async function getDockerConnectionString(
|
|
|
1315
1329
|
case Engine.TypeDB:
|
|
1316
1330
|
return `typedb://${host}:${port}`
|
|
1317
1331
|
|
|
1332
|
+
case Engine.TigerBeetle:
|
|
1333
|
+
return `${host}:${port}`
|
|
1334
|
+
|
|
1318
1335
|
case Engine.SQLite:
|
|
1319
1336
|
case Engine.DuckDB:
|
|
1320
1337
|
return `File-based database (no network connection)`
|
package/core/library-env.ts
CHANGED
|
@@ -58,8 +58,7 @@ export function detectLibraryError(
|
|
|
58
58
|
lower.includes('dyld:') ||
|
|
59
59
|
lower.includes('dyld[')
|
|
60
60
|
) {
|
|
61
|
-
const needsOpenssl =
|
|
62
|
-
lower.includes('libssl') || lower.includes('libcrypto')
|
|
61
|
+
const needsOpenssl = lower.includes('libssl') || lower.includes('libcrypto')
|
|
63
62
|
|
|
64
63
|
if (needsOpenssl && plat === 'darwin') {
|
|
65
64
|
return (
|
|
@@ -97,8 +96,7 @@ export function detectLibraryError(
|
|
|
97
96
|
lower.includes('error while loading shared libraries') ||
|
|
98
97
|
lower.includes('cannot open shared object file')
|
|
99
98
|
) {
|
|
100
|
-
const needsOpenssl =
|
|
101
|
-
lower.includes('libssl') || lower.includes('libcrypto')
|
|
99
|
+
const needsOpenssl = lower.includes('libssl') || lower.includes('libcrypto')
|
|
102
100
|
|
|
103
101
|
if (needsOpenssl) {
|
|
104
102
|
return (
|
package/core/update-manager.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { exec } from 'child_process'
|
|
|
2
2
|
import { promisify } from 'util'
|
|
3
3
|
import { createRequire } from 'module'
|
|
4
4
|
import { configManager } from './config-manager'
|
|
5
|
+
import { logDebug } from './error-handler'
|
|
5
6
|
|
|
6
7
|
const execAsync = promisify(exec)
|
|
7
8
|
const require = createRequire(import.meta.url)
|
|
@@ -11,6 +12,17 @@ const CHECK_THROTTLE_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
|
11
12
|
|
|
12
13
|
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
13
14
|
|
|
15
|
+
const KNOWN_PACKAGE_MANAGERS: PackageManager[] = ['pnpm', 'yarn', 'bun', 'npm']
|
|
16
|
+
|
|
17
|
+
export function parseUserAgent(
|
|
18
|
+
userAgent: string | undefined,
|
|
19
|
+
): PackageManager | null {
|
|
20
|
+
if (!userAgent) return null
|
|
21
|
+
const firstToken = userAgent.split('/')[0]?.toLowerCase().trim()
|
|
22
|
+
if (!firstToken) return null
|
|
23
|
+
return KNOWN_PACKAGE_MANAGERS.find((pm) => pm === firstToken) ?? null
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export type UpdateCheckResult = {
|
|
15
27
|
currentVersion: string
|
|
16
28
|
latestVersion: string
|
|
@@ -75,49 +87,59 @@ export class UpdateManager {
|
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
// Checks
|
|
90
|
+
// Checks all PMs in parallel, falls back to npm_config_user_agent, then npm
|
|
79
91
|
async detectPackageManager(): Promise<PackageManager> {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const checks = await Promise.all([
|
|
93
|
+
this.checkGlobalInstall(
|
|
94
|
+
'pnpm',
|
|
95
|
+
'pnpm list -g spindb --json',
|
|
96
|
+
(stdout) => {
|
|
97
|
+
const data = JSON.parse(stdout) as Array<{
|
|
98
|
+
dependencies?: { spindb?: unknown }
|
|
99
|
+
}>
|
|
100
|
+
return !!data[0]?.dependencies?.spindb
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
this.checkGlobalInstall('yarn', 'yarn global list --json', (stdout) => {
|
|
104
|
+
return stdout.includes('"spindb@')
|
|
105
|
+
}),
|
|
106
|
+
this.checkGlobalInstall('bun', 'bun pm ls -g', (stdout) => {
|
|
107
|
+
return stdout.includes('spindb@')
|
|
108
|
+
}),
|
|
109
|
+
this.checkGlobalInstall('npm', 'npm list -g spindb --json', (stdout) => {
|
|
110
|
+
const data = JSON.parse(stdout) as {
|
|
111
|
+
dependencies?: { spindb?: unknown }
|
|
112
|
+
}
|
|
113
|
+
return !!data.dependencies?.spindb
|
|
114
|
+
}),
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const globalPm = checks.find((result) => result !== null)
|
|
118
|
+
if (globalPm) {
|
|
119
|
+
logDebug(`Detected global install via ${globalPm}`)
|
|
120
|
+
return globalPm
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})
|
|
100
|
-
// yarn outputs newline-delimited JSON, look for spindb in any line
|
|
101
|
-
if (stdout.includes('"spindb@')) {
|
|
102
|
-
return 'yarn'
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
// yarn not installed or spindb not found
|
|
123
|
+
const agentPm = parseUserAgent(process.env.npm_config_user_agent)
|
|
124
|
+
if (agentPm) {
|
|
125
|
+
logDebug(`Detected package manager from user agent: ${agentPm}`)
|
|
126
|
+
return agentPm
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
return 'npm'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async checkGlobalInstall(
|
|
133
|
+
pm: PackageManager,
|
|
134
|
+
command: string,
|
|
135
|
+
checkOutput: (stdout: string) => boolean,
|
|
136
|
+
): Promise<PackageManager | null> {
|
|
108
137
|
try {
|
|
109
|
-
const { stdout } = await execAsync(
|
|
110
|
-
|
|
111
|
-
cwd: '/',
|
|
112
|
-
})
|
|
113
|
-
if (stdout.includes('spindb@')) {
|
|
114
|
-
return 'bun'
|
|
115
|
-
}
|
|
138
|
+
const { stdout } = await execAsync(command, { timeout: 5000, cwd: '/' })
|
|
139
|
+
return checkOutput(stdout) ? pm : null
|
|
116
140
|
} catch {
|
|
117
|
-
|
|
141
|
+
return null
|
|
118
142
|
}
|
|
119
|
-
|
|
120
|
-
return 'npm'
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
getInstallCommand(pm: PackageManager): string {
|
package/engines/base-engine.ts
CHANGED
|
@@ -169,6 +169,14 @@ export abstract class BaseEngine {
|
|
|
169
169
|
throw new Error('influxdb3 not found')
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Get the path to the tigerbeetle binary if available
|
|
174
|
+
* Default implementation throws; TigerBeetle engine overrides this method.
|
|
175
|
+
*/
|
|
176
|
+
async getTigerBeetlePath(_version?: string): Promise<string> {
|
|
177
|
+
throw new Error('tigerbeetle not found')
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
/**
|
|
173
181
|
* Get the path to the sqlite3 client if available
|
|
174
182
|
* Default implementation returns null; SQLite engine overrides this method.
|
package/engines/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { questdbEngine } from './questdb'
|
|
|
17
17
|
import { typedbEngine } from './typedb'
|
|
18
18
|
import { influxdbEngine } from './influxdb'
|
|
19
19
|
import { weaviateEngine } from './weaviate'
|
|
20
|
+
import { tigerbeetleEngine } from './tigerbeetle'
|
|
20
21
|
import { platformService } from '../core/platform-service'
|
|
21
22
|
import { Engine, Platform } from '../types'
|
|
22
23
|
import type { BaseEngine } from './base-engine'
|
|
@@ -87,6 +88,9 @@ export const engines: Record<string, BaseEngine> = {
|
|
|
87
88
|
// Weaviate and aliases
|
|
88
89
|
[Engine.Weaviate]: weaviateEngine,
|
|
89
90
|
wv: weaviateEngine,
|
|
91
|
+
// TigerBeetle and aliases
|
|
92
|
+
[Engine.TigerBeetle]: tigerbeetleEngine,
|
|
93
|
+
tb: tigerbeetleEngine,
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
// Get an engine by name
|
package/engines/mariadb/index.ts
CHANGED
|
@@ -259,7 +259,10 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
259
259
|
return new Promise((resolve, reject) => {
|
|
260
260
|
exec(
|
|
261
261
|
cmd,
|
|
262
|
-
{
|
|
262
|
+
{
|
|
263
|
+
timeout: 120000,
|
|
264
|
+
env: { ...process.env, ...getLibraryEnv(binPath) },
|
|
265
|
+
},
|
|
263
266
|
async (error, stdout, stderr) => {
|
|
264
267
|
if (error) {
|
|
265
268
|
await cleanupOnFailure()
|
|
@@ -493,9 +496,7 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
493
496
|
}
|
|
494
497
|
|
|
495
498
|
reject(
|
|
496
|
-
new Error(
|
|
497
|
-
libError || 'MariaDB failed to start within timeout',
|
|
498
|
-
),
|
|
499
|
+
new Error(libError || 'MariaDB failed to start within timeout'),
|
|
499
500
|
)
|
|
500
501
|
}
|
|
501
502
|
}
|
package/engines/redis/index.ts
CHANGED
|
@@ -594,6 +594,20 @@ export class RedisEngine extends BaseEngine {
|
|
|
594
594
|
if (settled) return
|
|
595
595
|
|
|
596
596
|
if (ready) {
|
|
597
|
+
// On Windows, Cygwin binaries may fork internally, making proc.pid stale.
|
|
598
|
+
// Find the actual PID by port and update the PID file (same pattern as QuestDB).
|
|
599
|
+
try {
|
|
600
|
+
const pids = await platformService.findProcessByPort(port)
|
|
601
|
+
if (pids.length > 0 && pids[0] !== proc.pid) {
|
|
602
|
+
logDebug(
|
|
603
|
+
`Redis actual PID ${pids[0]} differs from spawn PID ${proc.pid}, updating PID file`,
|
|
604
|
+
)
|
|
605
|
+
await writeFile(pidFile, String(pids[0]))
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
// Non-fatal - PID file already has proc.pid from earlier write
|
|
609
|
+
}
|
|
610
|
+
|
|
597
611
|
settled = true
|
|
598
612
|
resolve({
|
|
599
613
|
port,
|
|
@@ -698,10 +712,7 @@ export class RedisEngine extends BaseEngine {
|
|
|
698
712
|
}
|
|
699
713
|
|
|
700
714
|
// Check for library loading errors
|
|
701
|
-
const libError = detectLibraryError(
|
|
702
|
-
stderr + logContent,
|
|
703
|
-
'Redis',
|
|
704
|
-
)
|
|
715
|
+
const libError = detectLibraryError(stderr + logContent, 'Redis')
|
|
705
716
|
if (libError) {
|
|
706
717
|
reject(new Error(libError))
|
|
707
718
|
return
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# TigerBeetle Engine
|
|
2
|
+
|
|
3
|
+
TigerBeetle is a high-performance financial ledger database written in Zig,
|
|
4
|
+
designed for mission-critical safety and performance.
|
|
5
|
+
|
|
6
|
+
## Platform Support
|
|
7
|
+
|
|
8
|
+
All 5 platforms:
|
|
9
|
+
- darwin-arm64
|
|
10
|
+
- darwin-x64
|
|
11
|
+
- linux-arm64
|
|
12
|
+
- linux-x64
|
|
13
|
+
- win32-x64
|
|
14
|
+
|
|
15
|
+
## Binary Structure
|
|
16
|
+
|
|
17
|
+
Single binary: `tigerbeetle` (handles both server and REPL client)
|
|
18
|
+
|
|
19
|
+
## Two-Step Initialization
|
|
20
|
+
|
|
21
|
+
TigerBeetle requires a format step before starting:
|
|
22
|
+
|
|
23
|
+
1. **Format**: `tigerbeetle format --cluster=0 --replica=0 --replica-count=1 --development <data-file>`
|
|
24
|
+
2. **Start**: `tigerbeetle start --addresses=127.0.0.1:<port> --development <data-file>`
|
|
25
|
+
|
|
26
|
+
The `--development` flag is always passed since SpinDB is a local dev tool
|
|
27
|
+
(relaxes Direct I/O requirements).
|
|
28
|
+
|
|
29
|
+
## REPL Usage
|
|
30
|
+
|
|
31
|
+
Connect to a running instance:
|
|
32
|
+
```bash
|
|
33
|
+
tigerbeetle repl --cluster=0 --addresses=127.0.0.1:<port>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Version Grouping
|
|
37
|
+
|
|
38
|
+
Uses xy-format: `0.16.70` groups as `0.16` (like MariaDB/ClickHouse).
|
|
39
|
+
|
|
40
|
+
## Backup/Restore
|
|
41
|
+
|
|
42
|
+
Stop-and-copy of the single `0_0.tigerbeetle` data file.
|
|
43
|
+
The server must be stopped before backup (the file is exclusively locked).
|
|
44
|
+
TigerBeetle is designed for abrupt shutdown (SIGTERM/SIGKILL are safe).
|
|
45
|
+
|
|
46
|
+
## Key Characteristics
|
|
47
|
+
|
|
48
|
+
- **Protocol**: Custom binary protocol (not REST, not SQL)
|
|
49
|
+
- **Auth**: None
|
|
50
|
+
- **Multi-database**: No (single ledger per instance)
|
|
51
|
+
- **Health check**: TCP port check + PID (no HTTP endpoint)
|
|
52
|
+
- **License**: Apache-2.0
|
|
53
|
+
- **Default port**: 3000
|
|
54
|
+
|
|
55
|
+
## Linux / Docker Note
|
|
56
|
+
|
|
57
|
+
TigerBeetle uses `io_uring` for I/O on Linux. This works on regular Linux systems
|
|
58
|
+
(bare metal, VMs, GitHub Actions runners) but Docker's default seccomp profile blocks
|
|
59
|
+
`io_uring_*` syscalls. When running TigerBeetle inside Docker, the container must be
|
|
60
|
+
started with `--security-opt seccomp=unconfined`. The `pnpm test:docker` wrapper
|
|
61
|
+
handles this automatically. This does not affect macOS or Windows.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TigerBeetle backup module
|
|
3
|
+
* Supports stop-and-copy backup of the single data file.
|
|
4
|
+
*
|
|
5
|
+
* TigerBeetle stores all data in a single file (e.g., 0_0.tigerbeetle).
|
|
6
|
+
* Backup requires the server to be stopped first since the data file
|
|
7
|
+
* is exclusively locked by the running process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { copyFile, mkdir, stat } from 'fs/promises'
|
|
11
|
+
import { existsSync } from 'fs'
|
|
12
|
+
import { dirname, join } from 'path'
|
|
13
|
+
import { logDebug } from '../../core/error-handler'
|
|
14
|
+
import type { BackupOptions, BackupResult } from '../../types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a backup by copying the TigerBeetle data file.
|
|
18
|
+
* The server MUST be stopped before calling this function.
|
|
19
|
+
*/
|
|
20
|
+
export async function createBackup(
|
|
21
|
+
dataDir: string,
|
|
22
|
+
outputPath: string,
|
|
23
|
+
_options: BackupOptions,
|
|
24
|
+
): Promise<BackupResult> {
|
|
25
|
+
const dataFile = join(dataDir, '0_0.tigerbeetle')
|
|
26
|
+
|
|
27
|
+
if (!existsSync(dataFile)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`TigerBeetle data file not found at ${dataFile}. Has the database been initialized?`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ensure output parent directory exists
|
|
34
|
+
const outputDir = dirname(outputPath)
|
|
35
|
+
if (!existsSync(outputDir)) {
|
|
36
|
+
await mkdir(outputDir, { recursive: true })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logDebug(`Copying TigerBeetle data file to ${outputPath}`)
|
|
40
|
+
await copyFile(dataFile, outputPath)
|
|
41
|
+
|
|
42
|
+
const stats = await stat(outputPath)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
path: outputPath,
|
|
46
|
+
format: 'binary',
|
|
47
|
+
size: stats.size,
|
|
48
|
+
}
|
|
49
|
+
}
|