spindb 0.5.2 → 0.5.4

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 (38) hide show
  1. package/README.md +188 -9
  2. package/cli/commands/connect.ts +334 -105
  3. package/cli/commands/create.ts +106 -67
  4. package/cli/commands/deps.ts +19 -4
  5. package/cli/commands/edit.ts +245 -0
  6. package/cli/commands/engines.ts +434 -0
  7. package/cli/commands/info.ts +279 -0
  8. package/cli/commands/list.ts +1 -1
  9. package/cli/commands/menu.ts +664 -167
  10. package/cli/commands/restore.ts +11 -25
  11. package/cli/commands/start.ts +25 -20
  12. package/cli/commands/url.ts +79 -0
  13. package/cli/index.ts +9 -3
  14. package/cli/ui/prompts.ts +20 -12
  15. package/cli/ui/theme.ts +1 -1
  16. package/config/engine-defaults.ts +24 -1
  17. package/config/os-dependencies.ts +151 -113
  18. package/config/paths.ts +7 -36
  19. package/core/binary-manager.ts +12 -6
  20. package/core/config-manager.ts +17 -5
  21. package/core/dependency-manager.ts +144 -15
  22. package/core/error-handler.ts +336 -0
  23. package/core/platform-service.ts +634 -0
  24. package/core/port-manager.ts +11 -3
  25. package/core/process-manager.ts +12 -2
  26. package/core/start-with-retry.ts +167 -0
  27. package/core/transaction-manager.ts +170 -0
  28. package/engines/mysql/binary-detection.ts +177 -100
  29. package/engines/mysql/index.ts +240 -131
  30. package/engines/mysql/restore.ts +257 -0
  31. package/engines/mysql/version-validator.ts +373 -0
  32. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  33. package/engines/postgresql/binary-urls.ts +5 -3
  34. package/engines/postgresql/index.ts +35 -4
  35. package/engines/postgresql/restore.ts +54 -5
  36. package/engines/postgresql/version-validator.ts +262 -0
  37. package/package.json +6 -2
  38. package/cli/commands/postgres-tools.ts +0 -216
@@ -0,0 +1,257 @@
1
+ /**
2
+ * MySQL/MariaDB Backup Detection and Restore
3
+ *
4
+ * Handles detecting backup formats and restoring MySQL dumps.
5
+ */
6
+
7
+ import { spawn } from 'child_process'
8
+ import { createReadStream } from 'fs'
9
+ import { open } from 'fs/promises'
10
+ import { getMysqlClientPath } from './binary-detection'
11
+ import { validateRestoreCompatibility } from './version-validator'
12
+ import { getEngineDefaults } from '../../config/defaults'
13
+ import { logDebug, SpinDBError, ErrorCodes } from '../../core/error-handler'
14
+ import type { BackupFormat, RestoreResult } from '../../types'
15
+
16
+ const engineDef = getEngineDefaults('mysql')
17
+
18
+ // =============================================================================
19
+ // Backup Format Detection
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Detect the format of a MySQL backup file
24
+ *
25
+ * MySQL primarily uses SQL dumps (unlike PostgreSQL which has multiple formats).
26
+ * We detect:
27
+ * - MySQL SQL dump (mysqldump output)
28
+ * - MariaDB SQL dump
29
+ * - PostgreSQL dumps (to provide helpful error)
30
+ * - Generic SQL files
31
+ * - Compressed files (gzip)
32
+ */
33
+ export async function detectBackupFormat(
34
+ filePath: string,
35
+ ): Promise<BackupFormat> {
36
+ const buffer = Buffer.alloc(128)
37
+ const file = await open(filePath, 'r')
38
+ await file.read(buffer, 0, 128, 0)
39
+ await file.close()
40
+
41
+ const header = buffer.toString('utf8')
42
+
43
+ // Check for PostgreSQL custom format (PGDMP magic bytes)
44
+ if (buffer.toString('ascii', 0, 5) === 'PGDMP') {
45
+ return {
46
+ format: 'postgresql_custom',
47
+ description: 'PostgreSQL custom format dump (incompatible with MySQL)',
48
+ restoreCommand: 'pg_restore',
49
+ }
50
+ }
51
+
52
+ // Check for PostgreSQL SQL dump markers
53
+ if (
54
+ header.includes('-- PostgreSQL database dump') ||
55
+ header.includes('pg_dump') ||
56
+ header.includes('Dumped from database version')
57
+ ) {
58
+ return {
59
+ format: 'postgresql_sql',
60
+ description: 'PostgreSQL SQL dump (incompatible with MySQL)',
61
+ restoreCommand: 'psql',
62
+ }
63
+ }
64
+
65
+ // Check for gzip compression
66
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
67
+ return {
68
+ format: 'compressed',
69
+ description: 'Gzip compressed SQL dump',
70
+ restoreCommand: 'mysql',
71
+ }
72
+ }
73
+
74
+ // Check for MySQL dump markers
75
+ if (header.includes('-- MySQL dump')) {
76
+ return {
77
+ format: 'sql',
78
+ description: 'MySQL SQL dump (mysqldump)',
79
+ restoreCommand: 'mysql',
80
+ }
81
+ }
82
+
83
+ // Check for MariaDB dump markers
84
+ if (header.includes('-- MariaDB dump')) {
85
+ return {
86
+ format: 'sql',
87
+ description: 'MariaDB SQL dump (mysqldump)',
88
+ restoreCommand: 'mysql',
89
+ }
90
+ }
91
+
92
+ // Check if it looks like SQL (starts with common SQL statements)
93
+ const textStart = header.toLowerCase()
94
+ if (
95
+ textStart.startsWith('--') ||
96
+ textStart.startsWith('/*') ||
97
+ textStart.startsWith('set ') ||
98
+ textStart.startsWith('create') ||
99
+ textStart.startsWith('drop') ||
100
+ textStart.startsWith('begin') ||
101
+ textStart.startsWith('use ')
102
+ ) {
103
+ return {
104
+ format: 'sql',
105
+ description: 'SQL file',
106
+ restoreCommand: 'mysql',
107
+ }
108
+ }
109
+
110
+ // Default to SQL format
111
+ return {
112
+ format: 'unknown',
113
+ description: 'Unknown format - will attempt as SQL',
114
+ restoreCommand: 'mysql',
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if the backup file is from the wrong engine and throw helpful error
120
+ */
121
+ export function assertCompatibleFormat(format: BackupFormat): void {
122
+ if (
123
+ format.format === 'postgresql_custom' ||
124
+ format.format === 'postgresql_sql'
125
+ ) {
126
+ throw new SpinDBError(
127
+ ErrorCodes.WRONG_ENGINE_DUMP,
128
+ `This appears to be a PostgreSQL dump file, but you're trying to restore it to MySQL.`,
129
+ 'fatal',
130
+ `Create a PostgreSQL container instead:\n spindb create mydb --engine postgresql --from <dump-file>`,
131
+ {
132
+ detectedFormat: format.format,
133
+ expectedEngine: 'mysql',
134
+ detectedEngine: 'postgresql',
135
+ },
136
+ )
137
+ }
138
+ }
139
+
140
+ // =============================================================================
141
+ // Restore Options
142
+ // =============================================================================
143
+
144
+ export type RestoreOptions = {
145
+ port: number
146
+ database: string
147
+ user?: string
148
+ createDatabase?: boolean
149
+ validateVersion?: boolean
150
+ }
151
+
152
+ // =============================================================================
153
+ // Restore Functions
154
+ // =============================================================================
155
+
156
+ /**
157
+ * Restore a MySQL backup to a database
158
+ *
159
+ * CLI equivalent: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
160
+ */
161
+ export async function restoreBackup(
162
+ backupPath: string,
163
+ options: RestoreOptions,
164
+ ): Promise<RestoreResult> {
165
+ const {
166
+ port,
167
+ database,
168
+ user = engineDef.superuser,
169
+ validateVersion = true,
170
+ } = options
171
+
172
+ // Validate version compatibility if requested
173
+ if (validateVersion) {
174
+ try {
175
+ await validateRestoreCompatibility({ dumpPath: backupPath })
176
+ } catch (err) {
177
+ // Re-throw SpinDBError, log and continue for other errors
178
+ if (err instanceof Error && err.name === 'SpinDBError') {
179
+ throw err
180
+ }
181
+ logDebug('Version validation failed, proceeding anyway', {
182
+ error: err instanceof Error ? err.message : String(err),
183
+ })
184
+ }
185
+ }
186
+
187
+ const mysql = await getMysqlClientPath()
188
+ if (!mysql) {
189
+ throw new Error(
190
+ 'mysql client not found. Install MySQL client tools:\n' +
191
+ ' macOS: brew install mysql-client\n' +
192
+ ' Ubuntu/Debian: sudo apt install mysql-client',
193
+ )
194
+ }
195
+
196
+ // Detect format and check for wrong engine
197
+ const format = await detectBackupFormat(backupPath)
198
+ logDebug('Detected backup format', { format: format.format })
199
+ assertCompatibleFormat(format)
200
+
201
+ // Restore using mysql client
202
+ // CLI: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
203
+ return new Promise((resolve, reject) => {
204
+ const args = ['-h', '127.0.0.1', '-P', String(port), '-u', user, database]
205
+
206
+ const proc = spawn(mysql, args, {
207
+ stdio: ['pipe', 'pipe', 'pipe'],
208
+ })
209
+
210
+ // Pipe backup file to stdin
211
+ const fileStream = createReadStream(backupPath)
212
+ fileStream.pipe(proc.stdin)
213
+
214
+ let stdout = ''
215
+ let stderr = ''
216
+
217
+ proc.stdout.on('data', (data: Buffer) => {
218
+ stdout += data.toString()
219
+ })
220
+ proc.stderr.on('data', (data: Buffer) => {
221
+ stderr += data.toString()
222
+ })
223
+
224
+ proc.on('close', (code) => {
225
+ resolve({
226
+ format: format.format,
227
+ stdout,
228
+ stderr,
229
+ code: code ?? undefined,
230
+ })
231
+ })
232
+
233
+ proc.on('error', reject)
234
+ })
235
+ }
236
+
237
+ /**
238
+ * Parse a MySQL connection string
239
+ *
240
+ * Format: mysql://user:pass@host:port/database
241
+ */
242
+ export function parseConnectionString(connectionString: string): {
243
+ host: string
244
+ port: string
245
+ user: string
246
+ password: string
247
+ database: string
248
+ } {
249
+ const url = new URL(connectionString)
250
+ return {
251
+ host: url.hostname,
252
+ port: url.port || '3306',
253
+ user: url.username || 'root',
254
+ password: url.password || '',
255
+ database: url.pathname.slice(1), // Remove leading /
256
+ }
257
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * MySQL/MariaDB Version Validator
3
+ *
4
+ * Validates compatibility between mysql client version and dump file version.
5
+ * MySQL is generally more lenient than PostgreSQL, but we still warn about:
6
+ * - MariaDB dumps being restored to MySQL (and vice versa)
7
+ * - Newer dumps being restored to older clients
8
+ */
9
+
10
+ import { exec } from 'child_process'
11
+ import { promisify } from 'util'
12
+ import { createReadStream } from 'fs'
13
+ import { createInterface } from 'readline'
14
+ import {
15
+ SpinDBError,
16
+ ErrorCodes,
17
+ logWarning,
18
+ logDebug,
19
+ } from '../../core/error-handler'
20
+ import { getMysqlClientPath } from './binary-detection'
21
+
22
+ const execAsync = promisify(exec)
23
+
24
+ // =============================================================================
25
+ // Types
26
+ // =============================================================================
27
+
28
+ export type VersionInfo = {
29
+ major: number
30
+ minor: number
31
+ patch: number
32
+ full: string
33
+ }
34
+
35
+ export type MySQLVariant = 'mysql' | 'mariadb' | 'unknown'
36
+
37
+ export type DumpInfo = {
38
+ version: VersionInfo | null
39
+ variant: MySQLVariant
40
+ serverVersion?: string
41
+ }
42
+
43
+ export type CompatibilityResult = {
44
+ compatible: boolean
45
+ dumpInfo: DumpInfo
46
+ toolVersion: VersionInfo
47
+ toolVariant: MySQLVariant
48
+ warning?: string
49
+ error?: string
50
+ }
51
+
52
+ // =============================================================================
53
+ // Version Parsing
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Parse version from mysql --version output
58
+ * Examples:
59
+ * "mysql Ver 8.0.35 for macos14.0 on arm64 (Homebrew)"
60
+ * "mysql Ver 14.14 Distrib 5.7.44, for Linux (x86_64)" (MySQL 5.7)
61
+ * "mysql Ver 15.1 Distrib 10.11.6-MariaDB, for osx10.19 (arm64)"
62
+ * "mysql from 11.4.3-MariaDB, client 15.2 for osx10.20 (arm64)"
63
+ */
64
+ export function parseToolVersion(output: string): {
65
+ version: VersionInfo
66
+ variant: MySQLVariant
67
+ } {
68
+ // Check for MariaDB - must explicitly contain "mariadb" in the string
69
+ // Note: Both MySQL 5.7 and MariaDB use "Distrib", but only MariaDB includes "-MariaDB"
70
+ const isMariaDB = output.toLowerCase().includes('mariadb')
71
+
72
+ let match: RegExpMatchArray | null = null
73
+
74
+ if (isMariaDB) {
75
+ // MariaDB: "Distrib 10.11.6-MariaDB" or "from 11.4.3-MariaDB"
76
+ match = output.match(/(?:Distrib|from)\s+(\d+)\.(\d+)\.(\d+)/)
77
+ }
78
+
79
+ if (!match) {
80
+ // MySQL with Distrib: "Distrib 5.7.44" (MySQL 5.7 style)
81
+ match = output.match(/Distrib\s+(\d+)\.(\d+)\.(\d+)/)
82
+ }
83
+
84
+ if (!match) {
85
+ // MySQL: "Ver 8.0.35"
86
+ match = output.match(/Ver\s+(\d+)\.(\d+)(?:\.(\d+))?/)
87
+ }
88
+
89
+ if (!match) {
90
+ // Generic fallback
91
+ match = output.match(/(\d+)\.(\d+)(?:\.(\d+))?/)
92
+ }
93
+
94
+ if (!match) {
95
+ throw new Error(`Cannot parse version from: ${output}`)
96
+ }
97
+
98
+ return {
99
+ version: {
100
+ major: parseInt(match[1], 10),
101
+ minor: parseInt(match[2], 10),
102
+ patch: parseInt(match[3] || '0', 10),
103
+ full: match[0].replace(/^(Ver|Distrib|from)\s+/, ''),
104
+ },
105
+ variant: isMariaDB ? 'mariadb' : 'mysql',
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Read the first N lines of a file
111
+ */
112
+ async function readFirstLines(
113
+ filePath: string,
114
+ lineCount: number,
115
+ ): Promise<string> {
116
+ return new Promise((resolve, reject) => {
117
+ const lines: string[] = []
118
+ const stream = createReadStream(filePath, { encoding: 'utf8' })
119
+ const rl = createInterface({ input: stream })
120
+
121
+ rl.on('line', (line) => {
122
+ lines.push(line)
123
+ if (lines.length >= lineCount) {
124
+ rl.close()
125
+ stream.destroy()
126
+ }
127
+ })
128
+
129
+ rl.on('close', () => {
130
+ resolve(lines.join('\n'))
131
+ })
132
+
133
+ rl.on('error', reject)
134
+ stream.on('error', reject)
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Parse version from dump file header
140
+ *
141
+ * MySQL dump header:
142
+ * -- MySQL dump 10.13 Distrib 8.0.35, for macos14.0 (arm64)
143
+ * -- Server version 8.0.35
144
+ *
145
+ * MariaDB dump header:
146
+ * -- MariaDB dump 10.19-11.4.3-MariaDB, for osx10.20 (arm64)
147
+ * -- Server version 11.4.3-MariaDB
148
+ */
149
+ export async function parseDumpVersion(dumpPath: string): Promise<DumpInfo> {
150
+ try {
151
+ const header = await readFirstLines(dumpPath, 30)
152
+
153
+ // Detect variant
154
+ let variant: MySQLVariant = 'unknown'
155
+ if (header.includes('MariaDB dump') || header.includes('-MariaDB')) {
156
+ variant = 'mariadb'
157
+ } else if (header.includes('MySQL dump')) {
158
+ variant = 'mysql'
159
+ }
160
+
161
+ // Try to get server version (more accurate than dump tool version)
162
+ // "-- Server version 8.0.35" or "-- Server version 11.4.3-MariaDB"
163
+ const serverMatch = header.match(
164
+ /--\s*Server version\s+(\d+)\.(\d+)(?:\.(\d+))?/,
165
+ )
166
+ if (serverMatch) {
167
+ return {
168
+ version: {
169
+ major: parseInt(serverMatch[1], 10),
170
+ minor: parseInt(serverMatch[2], 10),
171
+ patch: parseInt(serverMatch[3] || '0', 10),
172
+ full: `${serverMatch[1]}.${serverMatch[2]}${serverMatch[3] ? `.${serverMatch[3]}` : ''}`,
173
+ },
174
+ variant,
175
+ serverVersion: header.match(/--\s*Server version\s+([^\n]+)/)?.[1],
176
+ }
177
+ }
178
+
179
+ // Fall back to Distrib version in header
180
+ // "Distrib 8.0.35" or "10.19-11.4.3-MariaDB"
181
+ let distribMatch = header.match(/Distrib\s+(\d+)\.(\d+)(?:\.(\d+))?/)
182
+ if (!distribMatch && variant === 'mariadb') {
183
+ // MariaDB format: "dump 10.19-11.4.3-MariaDB"
184
+ distribMatch = header.match(/dump\s+[\d.]+-(\d+)\.(\d+)\.(\d+)/)
185
+ }
186
+
187
+ if (distribMatch) {
188
+ return {
189
+ version: {
190
+ major: parseInt(distribMatch[1], 10),
191
+ minor: parseInt(distribMatch[2], 10),
192
+ patch: parseInt(distribMatch[3] || '0', 10),
193
+ full: `${distribMatch[1]}.${distribMatch[2]}${distribMatch[3] ? `.${distribMatch[3]}` : ''}`,
194
+ },
195
+ variant,
196
+ }
197
+ }
198
+
199
+ return { version: null, variant }
200
+ } catch (err) {
201
+ logDebug('Failed to parse dump version', {
202
+ dumpPath,
203
+ error: err instanceof Error ? err.message : String(err),
204
+ })
205
+ return { version: null, variant: 'unknown' }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Get the version of the mysql client
211
+ */
212
+ export async function getMysqlClientVersion(): Promise<{
213
+ version: VersionInfo
214
+ variant: MySQLVariant
215
+ }> {
216
+ const mysqlPath = await getMysqlClientPath()
217
+ if (!mysqlPath) {
218
+ throw new Error('mysql client not found')
219
+ }
220
+
221
+ const { stdout } = await execAsync(`"${mysqlPath}" --version`)
222
+ return parseToolVersion(stdout)
223
+ }
224
+
225
+ // =============================================================================
226
+ // Compatibility Checking
227
+ // =============================================================================
228
+
229
+ /**
230
+ * Check version compatibility
231
+ *
232
+ * MySQL/MariaDB compatibility matrix:
233
+ * | Scenario | Result |
234
+ * |----------|--------|
235
+ * | MySQL 8 client + MySQL 8 dump | ✅ Works |
236
+ * | MySQL 8 client + MySQL 5.7 dump | ✅ Works (backwards compatible) |
237
+ * | MySQL 5.7 client + MySQL 8 dump | ⚠️ May have issues |
238
+ * | MariaDB client + MySQL dump | ⚠️ Warning (mostly compatible) |
239
+ * | MySQL client + MariaDB dump | ⚠️ Warning (mostly compatible) |
240
+ */
241
+ export function checkVersionCompatibility(
242
+ dumpInfo: DumpInfo,
243
+ toolVersion: VersionInfo,
244
+ toolVariant: MySQLVariant,
245
+ ): CompatibilityResult {
246
+ const result: CompatibilityResult = {
247
+ compatible: true,
248
+ dumpInfo,
249
+ toolVersion,
250
+ toolVariant,
251
+ }
252
+
253
+ // If we couldn't parse dump version, proceed with warning
254
+ if (!dumpInfo.version) {
255
+ result.warning = 'Could not detect dump version. Proceeding anyway.'
256
+ return result
257
+ }
258
+
259
+ // Check for variant mismatch (MySQL vs MariaDB)
260
+ if (
261
+ dumpInfo.variant !== 'unknown' &&
262
+ toolVariant !== 'unknown' &&
263
+ dumpInfo.variant !== toolVariant
264
+ ) {
265
+ result.warning =
266
+ `Dump was created with ${dumpInfo.variant === 'mariadb' ? 'MariaDB' : 'MySQL'}, ` +
267
+ `but restoring with ${toolVariant === 'mariadb' ? 'MariaDB' : 'MySQL'}. ` +
268
+ `This usually works, but some features may not be compatible.`
269
+ return result
270
+ }
271
+
272
+ // MySQL 8 introduced significant changes
273
+ // Restoring MySQL 8+ dump with MySQL 5.x client may fail
274
+ if (dumpInfo.version.major >= 8 && toolVersion.major < 8) {
275
+ result.compatible = false
276
+ result.error =
277
+ `Dump was created with MySQL ${dumpInfo.version.major}, ` +
278
+ `but your mysql client is version ${toolVersion.major}. ` +
279
+ `MySQL 8 dumps may contain syntax not supported by older clients.`
280
+ return result
281
+ }
282
+
283
+ // MariaDB 10.x to MySQL may have issues with specific features
284
+ if (
285
+ dumpInfo.variant === 'mariadb' &&
286
+ toolVariant === 'mysql' &&
287
+ dumpInfo.version.major >= 10
288
+ ) {
289
+ result.warning =
290
+ `Dump was created with MariaDB ${dumpInfo.version.full}. ` +
291
+ `Some MariaDB-specific features may not restore correctly to MySQL.`
292
+ return result
293
+ }
294
+
295
+ // Warn if dump is newer than tool (any variant)
296
+ if (dumpInfo.version.major > toolVersion.major) {
297
+ result.warning =
298
+ `Dump was created with version ${dumpInfo.version.full}, ` +
299
+ `but your client is version ${toolVersion.full}. ` +
300
+ `Some features may not restore correctly.`
301
+ return result
302
+ }
303
+
304
+ // Warn if dump is very old (5+ years)
305
+ if (
306
+ dumpInfo.version.major < 5 ||
307
+ (dumpInfo.version.major === 5 && dumpInfo.version.minor < 7)
308
+ ) {
309
+ result.warning =
310
+ `Dump was created with MySQL ${dumpInfo.version.full}. ` +
311
+ `This is a very old version; some data types may not import correctly.`
312
+ return result
313
+ }
314
+
315
+ return result
316
+ }
317
+
318
+ // =============================================================================
319
+ // Main Validation Function
320
+ // =============================================================================
321
+
322
+ /**
323
+ * Validate that a dump file can be restored with the available mysql client
324
+ *
325
+ * @throws SpinDBError if versions are incompatible
326
+ */
327
+ export async function validateRestoreCompatibility(options: {
328
+ dumpPath: string
329
+ }): Promise<{
330
+ dumpInfo: DumpInfo
331
+ toolVersion: VersionInfo
332
+ toolVariant: MySQLVariant
333
+ }> {
334
+ const { dumpPath } = options
335
+
336
+ // Get tool version
337
+ const { version: toolVersion, variant: toolVariant } =
338
+ await getMysqlClientVersion()
339
+ logDebug('mysql client version detected', {
340
+ version: toolVersion.full,
341
+ variant: toolVariant,
342
+ })
343
+
344
+ // Get dump version
345
+ const dumpInfo = await parseDumpVersion(dumpPath)
346
+ if (dumpInfo.version) {
347
+ logDebug('Dump version detected', {
348
+ version: dumpInfo.version.full,
349
+ variant: dumpInfo.variant,
350
+ })
351
+ } else {
352
+ logDebug('Could not detect dump version')
353
+ }
354
+
355
+ // Check compatibility
356
+ const result = checkVersionCompatibility(dumpInfo, toolVersion, toolVariant)
357
+
358
+ if (!result.compatible) {
359
+ throw new SpinDBError(
360
+ ErrorCodes.VERSION_MISMATCH,
361
+ result.error!,
362
+ 'fatal',
363
+ 'Install a newer version of MySQL client tools',
364
+ { dumpInfo, toolVersion, toolVariant },
365
+ )
366
+ }
367
+
368
+ if (result.warning) {
369
+ logWarning(result.warning)
370
+ }
371
+
372
+ return { dumpInfo, toolVersion, toolVariant }
373
+ }