spindb 0.36.2 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/cli/commands/create.ts +7 -0
- package/cli/commands/databases.ts +17 -12
- package/cli/commands/delete.ts +3 -0
- package/cli/commands/engines.ts +59 -3
- 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/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/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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TigerBeetle restore module
|
|
3
|
+
* Supports restoring from a TigerBeetle data file backup.
|
|
4
|
+
*
|
|
5
|
+
* Restore copies the backup data file into the container's data directory.
|
|
6
|
+
* The server must be stopped before restore.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { copyFile, mkdir, stat } from 'fs/promises'
|
|
10
|
+
import { existsSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
import { logDebug } from '../../core/error-handler'
|
|
13
|
+
import type { BackupFormat, RestoreResult } from '../../types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect backup format from a file path.
|
|
17
|
+
* TigerBeetle backups are single binary data files.
|
|
18
|
+
*/
|
|
19
|
+
export async function detectBackupFormat(
|
|
20
|
+
filePath: string,
|
|
21
|
+
): Promise<BackupFormat> {
|
|
22
|
+
if (!existsSync(filePath)) {
|
|
23
|
+
throw new Error(`Backup file not found: ${filePath}`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const stats = await stat(filePath)
|
|
27
|
+
|
|
28
|
+
// TigerBeetle data files are regular files (not directories)
|
|
29
|
+
if (stats.isFile()) {
|
|
30
|
+
if (filePath.endsWith('.tigerbeetle')) {
|
|
31
|
+
return {
|
|
32
|
+
format: 'binary',
|
|
33
|
+
description: 'TigerBeetle data file',
|
|
34
|
+
restoreCommand: 'Copy to data directory (spindb restore handles this)',
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for common backup naming patterns
|
|
39
|
+
return {
|
|
40
|
+
format: 'binary',
|
|
41
|
+
description: 'TigerBeetle data file (assumed from file)',
|
|
42
|
+
restoreCommand: 'Copy to data directory (spindb restore handles this)',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
format: 'unknown',
|
|
48
|
+
description: 'Unknown backup format',
|
|
49
|
+
restoreCommand: 'Use a TigerBeetle data file (.tigerbeetle) for restore',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Restore options for TigerBeetle
|
|
54
|
+
export type RestoreOptions = {
|
|
55
|
+
containerName: string
|
|
56
|
+
dataDir: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Restore from a TigerBeetle data file backup.
|
|
61
|
+
* Copies the backup file into the container's data directory.
|
|
62
|
+
*/
|
|
63
|
+
export async function restoreBackup(
|
|
64
|
+
backupPath: string,
|
|
65
|
+
options: RestoreOptions,
|
|
66
|
+
): Promise<RestoreResult> {
|
|
67
|
+
const { dataDir } = options
|
|
68
|
+
|
|
69
|
+
if (!existsSync(backupPath)) {
|
|
70
|
+
throw new Error(`Backup not found: ${backupPath}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const format = await detectBackupFormat(backupPath)
|
|
74
|
+
logDebug(`Detected backup format: ${format.format}`)
|
|
75
|
+
|
|
76
|
+
// Ensure data directory exists
|
|
77
|
+
if (!existsSync(dataDir)) {
|
|
78
|
+
await mkdir(dataDir, { recursive: true })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const targetPath = join(dataDir, '0_0.tigerbeetle')
|
|
82
|
+
|
|
83
|
+
logDebug(`Restoring TigerBeetle data file to: ${targetPath}`)
|
|
84
|
+
await copyFile(backupPath, targetPath)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
format: 'binary',
|
|
88
|
+
stdout: `Restored TigerBeetle data file to ${targetPath}`,
|
|
89
|
+
code: 0,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse TigerBeetle connection string
|
|
95
|
+
* Format: host:port or 127.0.0.1:port
|
|
96
|
+
*
|
|
97
|
+
* TigerBeetle uses a custom binary protocol (no URI scheme).
|
|
98
|
+
*/
|
|
99
|
+
export function parseConnectionString(connectionString: string): {
|
|
100
|
+
host: string
|
|
101
|
+
port: number
|
|
102
|
+
} {
|
|
103
|
+
if (!connectionString || typeof connectionString !== 'string') {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'Invalid TigerBeetle connection string: expected a non-empty string',
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const trimmed = connectionString.trim()
|
|
110
|
+
|
|
111
|
+
// Parse host:port format
|
|
112
|
+
const parts = trimmed.split(':')
|
|
113
|
+
if (parts.length !== 2) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Invalid TigerBeetle connection string: "${trimmed}". ` +
|
|
116
|
+
'Expected format: host:port (e.g., 127.0.0.1:3000)',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const host = parts[0] || '127.0.0.1'
|
|
121
|
+
const port = parseInt(parts[1], 10)
|
|
122
|
+
|
|
123
|
+
if (isNaN(port) || port <= 0 || port > 65535) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Invalid TigerBeetle port: "${parts[1]}". Expected a number between 1 and 65535.`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { host, port }
|
|
130
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TigerBeetle Version Maps
|
|
3
|
+
*
|
|
4
|
+
* TEMPORARY: This version map will be replaced by the hostdb npm package once published.
|
|
5
|
+
* Until then, manually keep this in sync with robertjbass/hostdb releases.json:
|
|
6
|
+
* https://github.com/robertjbass/hostdb/blob/main/releases.json
|
|
7
|
+
*
|
|
8
|
+
* When updating versions:
|
|
9
|
+
* 1. Check hostdb releases.json for available versions
|
|
10
|
+
* 2. Update TIGERBEETLE_VERSION_MAP to match
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { logDebug } from '../../core/error-handler'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map of major TigerBeetle versions to their latest stable patch versions.
|
|
17
|
+
* Must match versions available in hostdb releases.json.
|
|
18
|
+
*
|
|
19
|
+
* TigerBeetle uses xy-format grouping (like MariaDB/ClickHouse):
|
|
20
|
+
* 0.16.70 groups as "0.16"
|
|
21
|
+
*/
|
|
22
|
+
export const TIGERBEETLE_VERSION_MAP: Record<string, string> = {
|
|
23
|
+
// 1-part: major version → latest
|
|
24
|
+
'0': '0.16.70',
|
|
25
|
+
// 2-part: major.minor → latest patch
|
|
26
|
+
'0.16': '0.16.70',
|
|
27
|
+
// 3-part: exact version (identity mapping)
|
|
28
|
+
'0.16.70': '0.16.70',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Supported major TigerBeetle versions (2-part format).
|
|
33
|
+
* Derived from TIGERBEETLE_VERSION_MAP keys to avoid duplication.
|
|
34
|
+
* Used for grouping and display purposes.
|
|
35
|
+
*/
|
|
36
|
+
export const SUPPORTED_MAJOR_VERSIONS = Object.keys(
|
|
37
|
+
TIGERBEETLE_VERSION_MAP,
|
|
38
|
+
).filter((key) => key.split('.').length === 2)
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the full version string for a major version.
|
|
42
|
+
*
|
|
43
|
+
* @param majorVersion - Major version (e.g., '0.16')
|
|
44
|
+
* @returns Full version string (e.g., '0.16.70') or null if not supported
|
|
45
|
+
*/
|
|
46
|
+
export function getFullVersion(majorVersion: string): string | null {
|
|
47
|
+
return TIGERBEETLE_VERSION_MAP[majorVersion] || null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalize a version string to X.Y.Z format.
|
|
52
|
+
*
|
|
53
|
+
* @param version - Version string (e.g., '0', '0.16', '0.16.70')
|
|
54
|
+
* @returns Normalized version (e.g., '0.16.70')
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeVersion(version: string): string {
|
|
57
|
+
// If it's a version key in the map (major, major.minor, or full), return the mapped version
|
|
58
|
+
const fullVersion = TIGERBEETLE_VERSION_MAP[version]
|
|
59
|
+
if (fullVersion) {
|
|
60
|
+
return fullVersion
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Unknown version - log debug and return as-is
|
|
64
|
+
logDebug(
|
|
65
|
+
`TigerBeetle version '${version}' not in version map, may not be available in hostdb`,
|
|
66
|
+
)
|
|
67
|
+
return version
|
|
68
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TigerBeetle version validation utilities
|
|
3
|
+
* Handles version parsing, comparison, and compatibility checking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a TigerBeetle version string into components
|
|
8
|
+
* Handles formats like "0.16.70", "0.16", "v0.16.70"
|
|
9
|
+
*/
|
|
10
|
+
export function parseVersion(versionString: string): {
|
|
11
|
+
major: number
|
|
12
|
+
minor: number
|
|
13
|
+
patch: number
|
|
14
|
+
raw: string
|
|
15
|
+
} | null {
|
|
16
|
+
const cleaned = versionString.replace(/^v/, '').trim()
|
|
17
|
+
if (!cleaned) return null
|
|
18
|
+
|
|
19
|
+
const parts = cleaned.split('.')
|
|
20
|
+
|
|
21
|
+
const major = parseInt(parts[0], 10)
|
|
22
|
+
const minor = parts[1] ? parseInt(parts[1], 10) : 0
|
|
23
|
+
const patch = parts[2] ? parseInt(parts[2], 10) : 0
|
|
24
|
+
|
|
25
|
+
if (isNaN(major)) return null
|
|
26
|
+
if (parts[1] && isNaN(minor)) return null
|
|
27
|
+
if (parts[2] && isNaN(patch)) return null
|
|
28
|
+
|
|
29
|
+
return { major, minor, patch, raw: cleaned }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a TigerBeetle version is supported by SpinDB
|
|
34
|
+
* Minimum supported version: 0.16.0
|
|
35
|
+
*/
|
|
36
|
+
export function isVersionSupported(version: string): boolean {
|
|
37
|
+
const parsed = parseVersion(version)
|
|
38
|
+
if (!parsed) return false
|
|
39
|
+
|
|
40
|
+
// Support 0.16+
|
|
41
|
+
if (parsed.major === 0) {
|
|
42
|
+
return parsed.minor >= 16
|
|
43
|
+
}
|
|
44
|
+
return parsed.major >= 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get major version from full version string (xy-format: 2-part)
|
|
49
|
+
* e.g., "0.16.70" -> "0.16"
|
|
50
|
+
*/
|
|
51
|
+
export function getMajorVersion(version: string): string {
|
|
52
|
+
const parsed = parseVersion(version)
|
|
53
|
+
if (!parsed) return version
|
|
54
|
+
return `${parsed.major}.${parsed.minor}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get major.minor version from full version string.
|
|
59
|
+
* Intentional alias for getMajorVersion — both return the 2-part xy-format
|
|
60
|
+
* version (e.g., "0.16"). Kept as a separate export for API consistency
|
|
61
|
+
* with other engine version validators.
|
|
62
|
+
*/
|
|
63
|
+
export function getMajorMinorVersion(version: string): string {
|
|
64
|
+
return getMajorVersion(version)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compare two TigerBeetle versions
|
|
69
|
+
* Returns: -1 if a < b, 0 if a == b, 1 if a > b, null if either version cannot be parsed
|
|
70
|
+
*/
|
|
71
|
+
export function compareVersions(a: string, b: string): number | null {
|
|
72
|
+
const parsedA = parseVersion(a)
|
|
73
|
+
const parsedB = parseVersion(b)
|
|
74
|
+
|
|
75
|
+
if (!parsedA || !parsedB) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (parsedA.major !== parsedB.major) {
|
|
80
|
+
return parsedA.major < parsedB.major ? -1 : 1
|
|
81
|
+
}
|
|
82
|
+
if (parsedA.minor !== parsedB.minor) {
|
|
83
|
+
return parsedA.minor < parsedB.minor ? -1 : 1
|
|
84
|
+
}
|
|
85
|
+
if (parsedA.patch !== parsedB.patch) {
|
|
86
|
+
return parsedA.patch < parsedB.patch ? -1 : 1
|
|
87
|
+
}
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if a backup version is compatible with the restore version
|
|
93
|
+
* TigerBeetle data files are generally compatible within minor versions
|
|
94
|
+
*/
|
|
95
|
+
export function isVersionCompatible(
|
|
96
|
+
backupVersion: string,
|
|
97
|
+
restoreVersion: string,
|
|
98
|
+
): { compatible: boolean; warning?: string } {
|
|
99
|
+
const backup = parseVersion(backupVersion)
|
|
100
|
+
const restore = parseVersion(restoreVersion)
|
|
101
|
+
|
|
102
|
+
if (!backup || !restore) {
|
|
103
|
+
return {
|
|
104
|
+
compatible: true,
|
|
105
|
+
warning: 'Could not parse versions, proceeding with restore',
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Must be same major.minor for TigerBeetle
|
|
110
|
+
if (backup.major !== restore.major || backup.minor !== restore.minor) {
|
|
111
|
+
return {
|
|
112
|
+
compatible: false,
|
|
113
|
+
warning: `Cannot restore TigerBeetle ${backupVersion} data to ${restoreVersion} server. Major.minor versions must match.`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { compatible: true }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate that a version string matches supported format
|
|
122
|
+
*/
|
|
123
|
+
export function isValidVersionFormat(version: string): boolean {
|
|
124
|
+
const parsed = parseVersion(version)
|
|
125
|
+
return parsed !== null
|
|
126
|
+
}
|
package/engines/valkey/index.ts
CHANGED
|
@@ -601,6 +601,20 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
601
601
|
if (settled) return
|
|
602
602
|
|
|
603
603
|
if (ready) {
|
|
604
|
+
// On Windows, Cygwin binaries may fork internally, making proc.pid stale.
|
|
605
|
+
// Find the actual PID by port and update the PID file (same pattern as QuestDB).
|
|
606
|
+
try {
|
|
607
|
+
const pids = await platformService.findProcessByPort(port)
|
|
608
|
+
if (pids.length > 0 && pids[0] !== proc.pid) {
|
|
609
|
+
logDebug(
|
|
610
|
+
`Valkey actual PID ${pids[0]} differs from spawn PID ${proc.pid}, updating PID file`,
|
|
611
|
+
)
|
|
612
|
+
await writeFile(pidFile, String(pids[0]))
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
// Non-fatal - PID file already has proc.pid from earlier write
|
|
616
|
+
}
|
|
617
|
+
|
|
604
618
|
settled = true
|
|
605
619
|
resolve({
|
|
606
620
|
port,
|
|
@@ -703,10 +717,7 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
703
717
|
} catch {
|
|
704
718
|
logContent = ''
|
|
705
719
|
}
|
|
706
|
-
const libError = detectLibraryError(
|
|
707
|
-
stderr + logContent,
|
|
708
|
-
'Valkey',
|
|
709
|
-
)
|
|
720
|
+
const libError = detectLibraryError(stderr + logContent, 'Valkey')
|
|
710
721
|
if (libError) {
|
|
711
722
|
reject(new Error(libError))
|
|
712
723
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
4
4
|
"author": "Bob Bass <bob@bbass.co>",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"typedb",
|
|
58
58
|
"influxdb",
|
|
59
59
|
"weaviate",
|
|
60
|
+
"tigerbeetle",
|
|
60
61
|
"layerbase",
|
|
61
62
|
"docker-free",
|
|
62
63
|
"tui",
|
package/types/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export enum Engine {
|
|
|
40
40
|
TypeDB = 'typedb',
|
|
41
41
|
InfluxDB = 'influxdb',
|
|
42
42
|
Weaviate = 'weaviate',
|
|
43
|
+
TigerBeetle = 'tigerbeetle',
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// Icon display mode for engine icons in CLI output
|
|
@@ -82,6 +83,7 @@ export const ALL_ENGINES = [
|
|
|
82
83
|
Engine.TypeDB,
|
|
83
84
|
Engine.InfluxDB,
|
|
84
85
|
Engine.Weaviate,
|
|
86
|
+
Engine.TigerBeetle,
|
|
85
87
|
] as const
|
|
86
88
|
|
|
87
89
|
// File-based engines (no server process, data stored in user project directories)
|
|
@@ -214,6 +216,7 @@ export type QuestDBFormat = 'sql'
|
|
|
214
216
|
export type TypeDBFormat = 'typeql'
|
|
215
217
|
export type InfluxDBFormat = 'sql'
|
|
216
218
|
export type WeaviateFormat = 'snapshot'
|
|
219
|
+
export type TigerBeetleFormat = 'binary'
|
|
217
220
|
|
|
218
221
|
// Query command types
|
|
219
222
|
export type QueryResultRow = Record<string, unknown>
|
|
@@ -294,6 +297,7 @@ export type BackupFormatType =
|
|
|
294
297
|
| TypeDBFormat
|
|
295
298
|
| InfluxDBFormat
|
|
296
299
|
| WeaviateFormat
|
|
300
|
+
| TigerBeetleFormat
|
|
297
301
|
|
|
298
302
|
// Mapping from Engine to its corresponding backup format type
|
|
299
303
|
type EngineFormatMap = {
|
|
@@ -316,6 +320,7 @@ type EngineFormatMap = {
|
|
|
316
320
|
[Engine.TypeDB]: TypeDBFormat
|
|
317
321
|
[Engine.InfluxDB]: InfluxDBFormat
|
|
318
322
|
[Engine.Weaviate]: WeaviateFormat
|
|
323
|
+
[Engine.TigerBeetle]: TigerBeetleFormat
|
|
319
324
|
}
|
|
320
325
|
|
|
321
326
|
// Helper type to get format type for a specific engine
|
|
@@ -434,6 +439,8 @@ export type BinaryTool =
|
|
|
434
439
|
| 'influxdb3'
|
|
435
440
|
// Weaviate tools
|
|
436
441
|
| 'weaviate'
|
|
442
|
+
// TigerBeetle tools
|
|
443
|
+
| 'tigerbeetle'
|
|
437
444
|
// Web panels
|
|
438
445
|
| 'pgweb'
|
|
439
446
|
// TUI tools
|
|
@@ -532,6 +539,8 @@ export type SpinDBConfig = {
|
|
|
532
539
|
influxdb3?: BinaryConfig
|
|
533
540
|
// Weaviate tools
|
|
534
541
|
weaviate?: BinaryConfig
|
|
542
|
+
// TigerBeetle tools
|
|
543
|
+
tigerbeetle?: BinaryConfig
|
|
535
544
|
// Web panels
|
|
536
545
|
pgweb?: BinaryConfig
|
|
537
546
|
// TUI tools
|