spindb 0.31.2 → 0.31.3
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/cli/commands/databases.ts +84 -0
- package/cli/commands/start.ts +14 -2
- package/core/container-manager.ts +68 -2
- package/core/error-handler.ts +21 -0
- package/core/pull-manager.ts +28 -13
- package/engines/base-engine.ts +18 -0
- package/engines/clickhouse/index.ts +59 -0
- package/engines/cockroachdb/index.ts +53 -0
- package/engines/couchdb/index.ts +23 -0
- package/engines/duckdb/index.ts +10 -0
- package/engines/ferretdb/index.ts +57 -0
- package/engines/mariadb/index.ts +56 -0
- package/engines/meilisearch/index.ts +25 -0
- package/engines/mongodb/index.ts +48 -0
- package/engines/mysql/index.ts +56 -0
- package/engines/postgresql/index.ts +60 -0
- package/engines/qdrant/index.ts +10 -0
- package/engines/questdb/index.ts +10 -0
- package/engines/redis/index.ts +11 -0
- package/engines/sqlite/index.ts +10 -0
- package/engines/surrealdb/index.ts +71 -0
- package/engines/valkey/index.ts +11 -0
- package/package.json +1 -1
|
@@ -437,6 +437,90 @@ databasesCommand
|
|
|
437
437
|
},
|
|
438
438
|
)
|
|
439
439
|
|
|
440
|
+
// Refresh databases from server - queries the actual database server
|
|
441
|
+
databasesCommand
|
|
442
|
+
.command('refresh')
|
|
443
|
+
.description(
|
|
444
|
+
'Refresh tracking by querying the database server for actual databases',
|
|
445
|
+
)
|
|
446
|
+
.argument('<container>', 'Container name')
|
|
447
|
+
.option('-j, --json', 'Output as JSON')
|
|
448
|
+
.action(async (container: string, options: { json?: boolean }) => {
|
|
449
|
+
try {
|
|
450
|
+
const config = await containerManager.getConfig(container)
|
|
451
|
+
if (!config) {
|
|
452
|
+
if (options.json) {
|
|
453
|
+
console.log(
|
|
454
|
+
JSON.stringify(
|
|
455
|
+
{ error: `Container "${container}" not found` },
|
|
456
|
+
null,
|
|
457
|
+
2,
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
} else {
|
|
461
|
+
console.error(uiError(`Container "${container}" not found`))
|
|
462
|
+
}
|
|
463
|
+
process.exit(1)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const beforeDatabases = config.databases || [config.database]
|
|
467
|
+
const afterDatabases = await containerManager.syncDatabases(container)
|
|
468
|
+
|
|
469
|
+
// Calculate changes
|
|
470
|
+
const added = afterDatabases.filter((db) => !beforeDatabases.includes(db))
|
|
471
|
+
const removed = beforeDatabases.filter(
|
|
472
|
+
(db) => !afterDatabases.includes(db),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if (options.json) {
|
|
476
|
+
console.log(
|
|
477
|
+
JSON.stringify(
|
|
478
|
+
{
|
|
479
|
+
success: true,
|
|
480
|
+
container,
|
|
481
|
+
databases: afterDatabases,
|
|
482
|
+
changes: {
|
|
483
|
+
added: added.length > 0 ? added : undefined,
|
|
484
|
+
removed: removed.length > 0 ? removed : undefined,
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
null,
|
|
488
|
+
2,
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
} else {
|
|
492
|
+
if (added.length === 0 && removed.length === 0) {
|
|
493
|
+
console.log(chalk.gray(`Registry already in sync for "${container}"`))
|
|
494
|
+
} else {
|
|
495
|
+
console.log(
|
|
496
|
+
uiSuccess(`Refreshed database tracking for "${container}"`),
|
|
497
|
+
)
|
|
498
|
+
if (added.length > 0) {
|
|
499
|
+
console.log(chalk.green(` Added: ${added.join(', ')}`))
|
|
500
|
+
}
|
|
501
|
+
if (removed.length > 0) {
|
|
502
|
+
console.log(chalk.yellow(` Removed: ${removed.join(', ')}`))
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
console.log()
|
|
506
|
+
console.log(chalk.bold('Current databases:'))
|
|
507
|
+
for (const db of afterDatabases) {
|
|
508
|
+
const isPrimary = db === config.database
|
|
509
|
+
const label = isPrimary ? chalk.gray(' (primary)') : ''
|
|
510
|
+
console.log(` ${chalk.cyan(db)}${label}`)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const e = error as Error
|
|
515
|
+
if (options.json) {
|
|
516
|
+
console.log(JSON.stringify({ error: e.message }, null, 2))
|
|
517
|
+
} else {
|
|
518
|
+
console.error(uiError(e.message))
|
|
519
|
+
}
|
|
520
|
+
process.exit(1)
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
|
|
440
524
|
// Set the default/primary database for a container
|
|
441
525
|
databasesCommand
|
|
442
526
|
.command('set-default')
|
package/cli/commands/start.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { getEngineDefaults } from '../../config/defaults'
|
|
|
9
9
|
import { promptContainerSelect, promptConfirm } from '../ui/prompts'
|
|
10
10
|
import { createSpinner } from '../ui/spinner'
|
|
11
11
|
import { uiWarning } from '../ui/theme'
|
|
12
|
-
import { Engine } from '../../types'
|
|
13
|
-
import { exitWithError } from '../../core/error-handler'
|
|
12
|
+
import { Engine, isFileBasedEngine } from '../../types'
|
|
13
|
+
import { exitWithError, logDebug } from '../../core/error-handler'
|
|
14
14
|
|
|
15
15
|
export const startCommand = new Command('start')
|
|
16
16
|
.description('Start a container')
|
|
@@ -174,6 +174,18 @@ export const startCommand = new Command('start')
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// Sync database registry with actual server state (silent, non-blocking)
|
|
178
|
+
if (!isFileBasedEngine(config.engine)) {
|
|
179
|
+
try {
|
|
180
|
+
await containerManager.syncDatabases(containerName)
|
|
181
|
+
} catch (syncError) {
|
|
182
|
+
// Don't fail start if sync fails - just log for debugging
|
|
183
|
+
logDebug(
|
|
184
|
+
`Failed to sync databases for ${containerName}: ${syncError}`,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
const connectionString = engine.getConnectionString(config)
|
|
178
190
|
|
|
179
191
|
if (options.json) {
|
|
@@ -13,13 +13,13 @@ import { paths } from '../config/paths'
|
|
|
13
13
|
import { processManager } from './process-manager'
|
|
14
14
|
import { portManager } from './port-manager'
|
|
15
15
|
import { isWindows } from './platform-service'
|
|
16
|
-
import { logDebug } from './error-handler'
|
|
16
|
+
import { logDebug, UnsupportedOperationError } from './error-handler'
|
|
17
17
|
import { getEngineDefaults, getSupportedEngines } from '../config/defaults'
|
|
18
18
|
import { getEngine } from '../engines'
|
|
19
19
|
import { sqliteRegistry } from '../engines/sqlite/registry'
|
|
20
20
|
import { duckdbRegistry } from '../engines/duckdb/registry'
|
|
21
21
|
import type { ContainerConfig } from '../types'
|
|
22
|
-
import { Engine } from '../types'
|
|
22
|
+
import { Engine, isFileBasedEngine } from '../types'
|
|
23
23
|
|
|
24
24
|
export type CreateOptions = {
|
|
25
25
|
engine: Engine
|
|
@@ -712,6 +712,72 @@ export class ContainerManager {
|
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Sync the databases array with the actual databases on the server.
|
|
717
|
+
* Queries the database server for all user databases and updates the registry.
|
|
718
|
+
*
|
|
719
|
+
* @param containerName - The container to sync
|
|
720
|
+
* @returns The updated list of databases
|
|
721
|
+
* @throws Error if the container is not running or doesn't support listing databases
|
|
722
|
+
*/
|
|
723
|
+
async syncDatabases(containerName: string): Promise<string[]> {
|
|
724
|
+
const config = await this.getConfig(containerName)
|
|
725
|
+
if (!config) {
|
|
726
|
+
throw new Error(`Container "${containerName}" not found`)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// File-based engines don't have multiple databases to sync
|
|
730
|
+
if (isFileBasedEngine(config.engine)) {
|
|
731
|
+
return config.databases || [config.database]
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Container must be running to query databases
|
|
735
|
+
const running = await processManager.isRunning(containerName, {
|
|
736
|
+
engine: config.engine,
|
|
737
|
+
})
|
|
738
|
+
if (!running) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Container "${containerName}" is not running. Start it first to sync databases.`,
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const engine = getEngine(config.engine)
|
|
745
|
+
|
|
746
|
+
// Query the actual database server for all databases
|
|
747
|
+
let actualDatabases: string[]
|
|
748
|
+
try {
|
|
749
|
+
actualDatabases = await engine.listDatabases(config)
|
|
750
|
+
} catch (error) {
|
|
751
|
+
// If the engine doesn't support listDatabases, return current registry
|
|
752
|
+
if (error instanceof UnsupportedOperationError) {
|
|
753
|
+
logDebug(
|
|
754
|
+
`listDatabases not supported for ${config.engine}, skipping sync`,
|
|
755
|
+
)
|
|
756
|
+
return config.databases || [config.database]
|
|
757
|
+
}
|
|
758
|
+
throw error
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Ensure primary database is always included
|
|
762
|
+
if (!actualDatabases.includes(config.database)) {
|
|
763
|
+
actualDatabases = [config.database, ...actualDatabases]
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Sort for consistent ordering (primary database first, then alphabetical)
|
|
767
|
+
const sortedDatabases = [
|
|
768
|
+
config.database,
|
|
769
|
+
...actualDatabases
|
|
770
|
+
.filter((db) => db !== config.database)
|
|
771
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
772
|
+
]
|
|
773
|
+
|
|
774
|
+
// Update the registry
|
|
775
|
+
config.databases = sortedDatabases
|
|
776
|
+
await this.saveConfig(containerName, { engine: config.engine }, config)
|
|
777
|
+
|
|
778
|
+
return sortedDatabases
|
|
779
|
+
}
|
|
780
|
+
|
|
715
781
|
getConnectionString(config: ContainerConfig, database?: string): string {
|
|
716
782
|
const engine = getEngine(config.engine)
|
|
717
783
|
return engine.getConnectionString(config, database)
|
package/core/error-handler.ts
CHANGED
|
@@ -136,6 +136,27 @@ export class MissingToolError extends Error {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Error thrown when an operation is not supported by an engine.
|
|
141
|
+
* For example, listDatabases on engines that don't have the concept of databases.
|
|
142
|
+
*/
|
|
143
|
+
export class UnsupportedOperationError extends Error {
|
|
144
|
+
public readonly operation: string
|
|
145
|
+
public readonly engine: string
|
|
146
|
+
|
|
147
|
+
constructor(operation: string, engine: string, message?: string) {
|
|
148
|
+
super(
|
|
149
|
+
message ??
|
|
150
|
+
`${operation} is not supported for ${engine}. ` +
|
|
151
|
+
`This engine may use a different concept (collections, indexes, etc.).`,
|
|
152
|
+
)
|
|
153
|
+
this.name = 'UnsupportedOperationError'
|
|
154
|
+
this.operation = operation
|
|
155
|
+
this.engine = engine
|
|
156
|
+
Error.captureStackTrace(this, UnsupportedOperationError)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
function getLogPath(): string {
|
|
140
161
|
return join(getSpinDBRoot(), 'spindb.log')
|
|
141
162
|
}
|
package/core/pull-manager.ts
CHANGED
|
@@ -123,7 +123,7 @@ export class PullManager {
|
|
|
123
123
|
// Track whether to keep backup in final result (user didn't specify --no-backup)
|
|
124
124
|
const keepBackup = !options.noBackup
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const result = await withTransaction(async (tx) => {
|
|
127
127
|
// --- BACKUP ORIGINAL (always if post-script, otherwise if not --no-backup) ---
|
|
128
128
|
if (needsBackup) {
|
|
129
129
|
// Step 1: Create backup database
|
|
@@ -217,12 +217,7 @@ export class PullManager {
|
|
|
217
217
|
createDatabase: false,
|
|
218
218
|
})
|
|
219
219
|
|
|
220
|
-
// Step 9:
|
|
221
|
-
if (keepBackup) {
|
|
222
|
-
await containerManager.addDatabase(config.name, backupDatabase)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Step 10: Cleanup temp files
|
|
220
|
+
// Step 9: Cleanup temp files
|
|
226
221
|
try {
|
|
227
222
|
await unlink(tempOriginalDump)
|
|
228
223
|
} catch {
|
|
@@ -234,7 +229,7 @@ export class PullManager {
|
|
|
234
229
|
// Ignore errors
|
|
235
230
|
}
|
|
236
231
|
|
|
237
|
-
// Step
|
|
232
|
+
// Step 10: Run post-script if provided
|
|
238
233
|
if (options.postScript) {
|
|
239
234
|
const context: PullContext = {
|
|
240
235
|
container: config.name,
|
|
@@ -278,6 +273,19 @@ export class PullManager {
|
|
|
278
273
|
: `Pulled remote data into "${targetDatabase}"`,
|
|
279
274
|
}
|
|
280
275
|
})
|
|
276
|
+
|
|
277
|
+
// Sync registry with actual databases on server after transaction commits
|
|
278
|
+
// This captures the backup database (if kept) and any other databases
|
|
279
|
+
// Wrapped in try/catch to avoid affecting the main pull result on transient failures
|
|
280
|
+
try {
|
|
281
|
+
await containerManager.syncDatabases(config.name)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logDebug(
|
|
284
|
+
`Failed to sync databases for "${config.name}": ${error instanceof Error ? error.message : error}`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
private async executeCloneMode(
|
|
@@ -335,17 +343,14 @@ export class PullManager {
|
|
|
335
343
|
createDatabase: false,
|
|
336
344
|
})
|
|
337
345
|
|
|
338
|
-
// Step 5:
|
|
339
|
-
await containerManager.addDatabase(config.name, targetDatabase)
|
|
340
|
-
|
|
341
|
-
// Step 6: Cleanup
|
|
346
|
+
// Step 5: Cleanup
|
|
342
347
|
try {
|
|
343
348
|
await unlink(tempRemoteDump)
|
|
344
349
|
} catch {
|
|
345
350
|
// Ignore errors
|
|
346
351
|
}
|
|
347
352
|
|
|
348
|
-
// Step
|
|
353
|
+
// Step 6: Run post-script if provided
|
|
349
354
|
if (options.postScript) {
|
|
350
355
|
const context: PullContext = {
|
|
351
356
|
container: config.name,
|
|
@@ -360,6 +365,16 @@ export class PullManager {
|
|
|
360
365
|
await this.runPostScript(options.postScript, context)
|
|
361
366
|
}
|
|
362
367
|
|
|
368
|
+
// Step 7: Sync registry with actual databases on server
|
|
369
|
+
// Wrapped in try/catch to avoid rolling back a successful clone on transient failures
|
|
370
|
+
try {
|
|
371
|
+
await containerManager.syncDatabases(config.name)
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logDebug(
|
|
374
|
+
`Failed to sync databases for "${config.name}": ${error instanceof Error ? error.message : error}`,
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
363
378
|
return {
|
|
364
379
|
success: true,
|
|
365
380
|
mode: 'clone' as const,
|
package/engines/base-engine.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
QueryResult,
|
|
11
11
|
QueryOptions,
|
|
12
12
|
} from '../types'
|
|
13
|
+
import { UnsupportedOperationError } from '../core/error-handler'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Base class for database engines
|
|
@@ -263,4 +264,21 @@ export abstract class BaseEngine {
|
|
|
263
264
|
query: string,
|
|
264
265
|
options?: QueryOptions,
|
|
265
266
|
): Promise<QueryResult>
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List all user databases on the server, excluding system databases.
|
|
270
|
+
* Used to sync the registry with actual databases on the server.
|
|
271
|
+
*
|
|
272
|
+
* System databases excluded by default:
|
|
273
|
+
* - PostgreSQL: template0, template1, postgres
|
|
274
|
+
* - MySQL/MariaDB: information_schema, mysql, performance_schema, sys
|
|
275
|
+
* - CockroachDB: defaultdb, postgres, system
|
|
276
|
+
*
|
|
277
|
+
* @param container - The container configuration
|
|
278
|
+
* @returns Array of database names (excluding system databases)
|
|
279
|
+
* @throws Error if the engine doesn't support multiple databases or listing
|
|
280
|
+
*/
|
|
281
|
+
async listDatabases(_container: ContainerConfig): Promise<string[]> {
|
|
282
|
+
throw new UnsupportedOperationError('listDatabases', this.displayName)
|
|
283
|
+
}
|
|
266
284
|
}
|
|
@@ -1185,6 +1185,65 @@ export class ClickHouseEngine extends BaseEngine {
|
|
|
1185
1185
|
})
|
|
1186
1186
|
})
|
|
1187
1187
|
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* List all user databases, excluding system databases (system, information_schema, INFORMATION_SCHEMA).
|
|
1191
|
+
*/
|
|
1192
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1193
|
+
const { port, version } = container
|
|
1194
|
+
const clickhouse = await this.getClickHouseClientPath(version)
|
|
1195
|
+
|
|
1196
|
+
logDebug(`Listing databases on port ${port} with version ${version}`)
|
|
1197
|
+
|
|
1198
|
+
return new Promise((resolve, reject) => {
|
|
1199
|
+
const args = [
|
|
1200
|
+
'client',
|
|
1201
|
+
'--host',
|
|
1202
|
+
'127.0.0.1',
|
|
1203
|
+
'--port',
|
|
1204
|
+
String(port),
|
|
1205
|
+
'--query',
|
|
1206
|
+
'SHOW DATABASES',
|
|
1207
|
+
]
|
|
1208
|
+
|
|
1209
|
+
const proc = spawn(clickhouse, args, {
|
|
1210
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
let stdout = ''
|
|
1214
|
+
let stderr = ''
|
|
1215
|
+
|
|
1216
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1217
|
+
stdout += data.toString()
|
|
1218
|
+
})
|
|
1219
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1220
|
+
stderr += data.toString()
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
proc.on('error', reject)
|
|
1224
|
+
|
|
1225
|
+
proc.on('close', (code) => {
|
|
1226
|
+
if (code !== 0) {
|
|
1227
|
+
reject(new Error(stderr || `clickhouse exited with code ${code}`))
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Parse output (one database per line)
|
|
1232
|
+
const systemDatabases = [
|
|
1233
|
+
'system',
|
|
1234
|
+
'information_schema',
|
|
1235
|
+
'INFORMATION_SCHEMA',
|
|
1236
|
+
]
|
|
1237
|
+
const databases = stdout
|
|
1238
|
+
.trim()
|
|
1239
|
+
.split('\n')
|
|
1240
|
+
.map((db) => db.trim())
|
|
1241
|
+
.filter((db) => db.length > 0 && !systemDatabases.includes(db))
|
|
1242
|
+
|
|
1243
|
+
resolve(databases)
|
|
1244
|
+
})
|
|
1245
|
+
})
|
|
1246
|
+
}
|
|
1188
1247
|
}
|
|
1189
1248
|
|
|
1190
1249
|
export const clickhouseEngine = new ClickHouseEngine()
|
|
@@ -1148,6 +1148,59 @@ export class CockroachDBEngine extends BaseEngine {
|
|
|
1148
1148
|
})
|
|
1149
1149
|
})
|
|
1150
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* List all user databases, excluding system databases (defaultdb, postgres, system).
|
|
1154
|
+
*/
|
|
1155
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1156
|
+
const { port, version } = container
|
|
1157
|
+
const cockroach = await this.getCockroachPath(version)
|
|
1158
|
+
|
|
1159
|
+
return new Promise((resolve, reject) => {
|
|
1160
|
+
const args = [
|
|
1161
|
+
'sql',
|
|
1162
|
+
'--insecure',
|
|
1163
|
+
'--host',
|
|
1164
|
+
`127.0.0.1:${port}`,
|
|
1165
|
+
'--execute',
|
|
1166
|
+
`SHOW DATABASES`,
|
|
1167
|
+
'--format=csv',
|
|
1168
|
+
]
|
|
1169
|
+
|
|
1170
|
+
const proc = spawn(cockroach, args, {
|
|
1171
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
let stdout = ''
|
|
1175
|
+
let stderr = ''
|
|
1176
|
+
|
|
1177
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1178
|
+
stdout += data.toString()
|
|
1179
|
+
})
|
|
1180
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1181
|
+
stderr += data.toString()
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
proc.on('error', reject)
|
|
1185
|
+
|
|
1186
|
+
proc.on('close', (code) => {
|
|
1187
|
+
if (code !== 0) {
|
|
1188
|
+
reject(new Error(stderr || `cockroach sql exited with code ${code}`))
|
|
1189
|
+
return
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Parse CSV output (first column is database_name, skip header)
|
|
1193
|
+
const systemDatabases = ['defaultdb', 'postgres', 'system']
|
|
1194
|
+
const lines = stdout.trim().split('\n')
|
|
1195
|
+
const databases = lines
|
|
1196
|
+
.slice(1) // Skip header
|
|
1197
|
+
.map((line) => line.split(',')[0].trim())
|
|
1198
|
+
.filter((db) => db.length > 0 && !systemDatabases.includes(db))
|
|
1199
|
+
|
|
1200
|
+
resolve(databases)
|
|
1201
|
+
})
|
|
1202
|
+
})
|
|
1203
|
+
}
|
|
1151
1204
|
}
|
|
1152
1205
|
|
|
1153
1206
|
export const cockroachdbEngine = new CockroachDBEngine()
|
package/engines/couchdb/index.ts
CHANGED
|
@@ -1253,6 +1253,29 @@ export class CouchDBEngine extends BaseEngine {
|
|
|
1253
1253
|
|
|
1254
1254
|
return parseRESTAPIResult(JSON.stringify(response.data))
|
|
1255
1255
|
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* List all user databases, excluding system databases (_users, _replicator, _global_changes).
|
|
1259
|
+
*/
|
|
1260
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1261
|
+
const { port } = container
|
|
1262
|
+
|
|
1263
|
+
const response = await couchdbApiRequest(port, 'GET', '/_all_dbs')
|
|
1264
|
+
|
|
1265
|
+
if (response.status >= 400) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`CouchDB API error (${response.status}): ${JSON.stringify(response.data)}`,
|
|
1268
|
+
)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const allDatabases = response.data as string[]
|
|
1272
|
+
const systemDatabases = ['_users', '_replicator', '_global_changes']
|
|
1273
|
+
const databases = allDatabases.filter(
|
|
1274
|
+
(db) => !systemDatabases.includes(db) && !db.startsWith('_'),
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
return databases
|
|
1278
|
+
}
|
|
1256
1279
|
}
|
|
1257
1280
|
|
|
1258
1281
|
export const couchdbEngine = new CouchDBEngine()
|
package/engines/duckdb/index.ts
CHANGED
|
@@ -804,6 +804,16 @@ export class DuckDBEngine extends BaseEngine {
|
|
|
804
804
|
})
|
|
805
805
|
})
|
|
806
806
|
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* List databases for DuckDB.
|
|
810
|
+
* DuckDB is file-based with one database per file.
|
|
811
|
+
* Returns the configured database (file path) as a single-item array.
|
|
812
|
+
*/
|
|
813
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
814
|
+
// DuckDB is file-based, one database per file
|
|
815
|
+
return [container.database]
|
|
816
|
+
}
|
|
807
817
|
}
|
|
808
818
|
|
|
809
819
|
export const duckdbEngine = new DuckDBEngine()
|
|
@@ -1283,6 +1283,63 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1283
1283
|
})
|
|
1284
1284
|
})
|
|
1285
1285
|
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* List all user databases, excluding system databases (admin, config, local).
|
|
1289
|
+
* FerretDB uses MongoDB protocol, so same approach as MongoDB.
|
|
1290
|
+
*/
|
|
1291
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1292
|
+
const { port } = container
|
|
1293
|
+
const mongosh = await this.getMongoshPath()
|
|
1294
|
+
|
|
1295
|
+
return new Promise((resolve, reject) => {
|
|
1296
|
+
// Use JSON output for reliable parsing
|
|
1297
|
+
const script = `JSON.stringify(db.adminCommand({listDatabases: 1}).databases.map(d => d.name))`
|
|
1298
|
+
const args = [
|
|
1299
|
+
'--quiet',
|
|
1300
|
+
'--host',
|
|
1301
|
+
'127.0.0.1',
|
|
1302
|
+
'--port',
|
|
1303
|
+
String(port),
|
|
1304
|
+
'--eval',
|
|
1305
|
+
script,
|
|
1306
|
+
]
|
|
1307
|
+
|
|
1308
|
+
const proc = spawn(mongosh, args, {
|
|
1309
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
let stdout = ''
|
|
1313
|
+
let stderr = ''
|
|
1314
|
+
|
|
1315
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1316
|
+
stdout += data.toString()
|
|
1317
|
+
})
|
|
1318
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1319
|
+
stderr += data.toString()
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
proc.on('error', reject)
|
|
1323
|
+
|
|
1324
|
+
proc.on('close', (code) => {
|
|
1325
|
+
if (code !== 0) {
|
|
1326
|
+
reject(new Error(stderr || `mongosh exited with code ${code}`))
|
|
1327
|
+
return
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
try {
|
|
1331
|
+
const allDatabases = JSON.parse(stdout.trim()) as string[]
|
|
1332
|
+
const systemDatabases = ['admin', 'config', 'local']
|
|
1333
|
+
const databases = allDatabases.filter(
|
|
1334
|
+
(db) => !systemDatabases.includes(db),
|
|
1335
|
+
)
|
|
1336
|
+
resolve(databases)
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
reject(new Error(`Failed to parse database list: ${error}`))
|
|
1339
|
+
}
|
|
1340
|
+
})
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1286
1343
|
}
|
|
1287
1344
|
|
|
1288
1345
|
export const ferretdbEngine = new FerretDBEngine()
|
package/engines/mariadb/index.ts
CHANGED
|
@@ -1112,6 +1112,62 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
1112
1112
|
})
|
|
1113
1113
|
})
|
|
1114
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* List all user databases, excluding system databases
|
|
1118
|
+
* (information_schema, mysql, performance_schema, sys).
|
|
1119
|
+
*/
|
|
1120
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1121
|
+
const { port } = container
|
|
1122
|
+
const mariadb = await this.getMariadbClientPath()
|
|
1123
|
+
|
|
1124
|
+
// Query for all non-system databases
|
|
1125
|
+
const sql = `SHOW DATABASES WHERE \`Database\` NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')`
|
|
1126
|
+
|
|
1127
|
+
const args = [
|
|
1128
|
+
'-h',
|
|
1129
|
+
'127.0.0.1',
|
|
1130
|
+
'-P',
|
|
1131
|
+
String(port),
|
|
1132
|
+
'-u',
|
|
1133
|
+
engineDef.superuser,
|
|
1134
|
+
'-N', // Skip column names
|
|
1135
|
+
'-B', // Batch mode (no formatting)
|
|
1136
|
+
'-e',
|
|
1137
|
+
sql,
|
|
1138
|
+
]
|
|
1139
|
+
|
|
1140
|
+
return new Promise((resolve, reject) => {
|
|
1141
|
+
const proc = spawn(mariadb, args, {
|
|
1142
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
let stdout = ''
|
|
1146
|
+
let stderr = ''
|
|
1147
|
+
|
|
1148
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1149
|
+
stdout += data.toString()
|
|
1150
|
+
})
|
|
1151
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1152
|
+
stderr += data.toString()
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
proc.on('error', reject)
|
|
1156
|
+
|
|
1157
|
+
proc.on('close', (code) => {
|
|
1158
|
+
if (code === 0) {
|
|
1159
|
+
const databases = stdout
|
|
1160
|
+
.trim()
|
|
1161
|
+
.split('\n')
|
|
1162
|
+
.map((db) => db.trim())
|
|
1163
|
+
.filter((db) => db.length > 0)
|
|
1164
|
+
resolve(databases)
|
|
1165
|
+
} else {
|
|
1166
|
+
reject(new Error(stderr || `mariadb exited with code ${code}`))
|
|
1167
|
+
}
|
|
1168
|
+
})
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1115
1171
|
}
|
|
1116
1172
|
|
|
1117
1173
|
export const mariadbEngine = new MariaDBEngine()
|
|
@@ -1166,6 +1166,31 @@ export class MeilisearchEngine extends BaseEngine {
|
|
|
1166
1166
|
|
|
1167
1167
|
return parseRESTAPIResult(JSON.stringify(response.data))
|
|
1168
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* List databases for Meilisearch.
|
|
1172
|
+
* Meilisearch uses indexes, so we query the /indexes endpoint.
|
|
1173
|
+
*/
|
|
1174
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1175
|
+
const { port } = container
|
|
1176
|
+
|
|
1177
|
+
try {
|
|
1178
|
+
const response = await meilisearchApiRequest(port, 'GET', '/indexes')
|
|
1179
|
+
if (response.status === 200 && response.data) {
|
|
1180
|
+
// Response is { results: [{ uid: "index_name", ... }, ...], ... }
|
|
1181
|
+
const data = response.data as { results?: Array<{ uid: string }> }
|
|
1182
|
+
if (Array.isArray(data.results)) {
|
|
1183
|
+
const indexes = data.results.map((index) => index.uid)
|
|
1184
|
+
return indexes.length > 0 ? indexes : [container.database]
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// Fall back to configured database
|
|
1188
|
+
return [container.database]
|
|
1189
|
+
} catch {
|
|
1190
|
+
// On error, fall back to configured database
|
|
1191
|
+
return [container.database]
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1169
1194
|
}
|
|
1170
1195
|
|
|
1171
1196
|
export const meilisearchEngine = new MeilisearchEngine()
|
package/engines/mongodb/index.ts
CHANGED
|
@@ -983,6 +983,54 @@ export class MongoDBEngine extends BaseEngine {
|
|
|
983
983
|
|
|
984
984
|
return parseMongoDBResult(jsonMatch[0])
|
|
985
985
|
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* List all user databases, excluding system databases (admin, config, local).
|
|
989
|
+
*/
|
|
990
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
991
|
+
const { port } = container
|
|
992
|
+
const mongosh = await this.getMongoshPath()
|
|
993
|
+
|
|
994
|
+
return new Promise((resolve, reject) => {
|
|
995
|
+
// Use JSON output for reliable parsing
|
|
996
|
+
const script = `JSON.stringify(db.adminCommand({listDatabases: 1}).databases.map(d => d.name))`
|
|
997
|
+
const args = ['--quiet', '--host', `127.0.0.1:${port}`, '--eval', script]
|
|
998
|
+
|
|
999
|
+
const proc = spawn(mongosh, args, {
|
|
1000
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
let stdout = ''
|
|
1004
|
+
let stderr = ''
|
|
1005
|
+
|
|
1006
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1007
|
+
stdout += data.toString()
|
|
1008
|
+
})
|
|
1009
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1010
|
+
stderr += data.toString()
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
proc.on('error', reject)
|
|
1014
|
+
|
|
1015
|
+
proc.on('close', (code) => {
|
|
1016
|
+
if (code !== 0) {
|
|
1017
|
+
reject(new Error(stderr || `mongosh exited with code ${code}`))
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
try {
|
|
1022
|
+
const allDatabases = JSON.parse(stdout.trim()) as string[]
|
|
1023
|
+
const systemDatabases = ['admin', 'config', 'local']
|
|
1024
|
+
const databases = allDatabases.filter(
|
|
1025
|
+
(db) => !systemDatabases.includes(db),
|
|
1026
|
+
)
|
|
1027
|
+
resolve(databases)
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
reject(new Error(`Failed to parse database list: ${error}`))
|
|
1030
|
+
}
|
|
1031
|
+
})
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
986
1034
|
}
|
|
987
1035
|
|
|
988
1036
|
export const mongodbEngine = new MongoDBEngine()
|
package/engines/mysql/index.ts
CHANGED
|
@@ -1196,6 +1196,62 @@ export class MySQLEngine extends BaseEngine {
|
|
|
1196
1196
|
})
|
|
1197
1197
|
})
|
|
1198
1198
|
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* List all user databases, excluding system databases
|
|
1202
|
+
* (information_schema, mysql, performance_schema, sys).
|
|
1203
|
+
*/
|
|
1204
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1205
|
+
const { port } = container
|
|
1206
|
+
const mysql = await this.getMysqlClientPath()
|
|
1207
|
+
|
|
1208
|
+
// Query for all non-system databases
|
|
1209
|
+
const sql = `SHOW DATABASES WHERE \`Database\` NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')`
|
|
1210
|
+
|
|
1211
|
+
const args = [
|
|
1212
|
+
'-h',
|
|
1213
|
+
'127.0.0.1',
|
|
1214
|
+
'-P',
|
|
1215
|
+
String(port),
|
|
1216
|
+
'-u',
|
|
1217
|
+
engineDef.superuser,
|
|
1218
|
+
'-N', // Skip column names
|
|
1219
|
+
'-B', // Batch mode (no formatting)
|
|
1220
|
+
'-e',
|
|
1221
|
+
sql,
|
|
1222
|
+
]
|
|
1223
|
+
|
|
1224
|
+
return new Promise((resolve, reject) => {
|
|
1225
|
+
const proc = spawn(mysql, args, {
|
|
1226
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
let stdout = ''
|
|
1230
|
+
let stderr = ''
|
|
1231
|
+
|
|
1232
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1233
|
+
stdout += data.toString()
|
|
1234
|
+
})
|
|
1235
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1236
|
+
stderr += data.toString()
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
proc.on('error', reject)
|
|
1240
|
+
|
|
1241
|
+
proc.on('close', (code) => {
|
|
1242
|
+
if (code === 0) {
|
|
1243
|
+
const databases = stdout
|
|
1244
|
+
.trim()
|
|
1245
|
+
.split('\n')
|
|
1246
|
+
.map((db) => db.trim())
|
|
1247
|
+
.filter((db) => db.length > 0)
|
|
1248
|
+
resolve(databases)
|
|
1249
|
+
} else {
|
|
1250
|
+
reject(new Error(stderr || `mysql exited with code ${code}`))
|
|
1251
|
+
}
|
|
1252
|
+
})
|
|
1253
|
+
})
|
|
1254
|
+
}
|
|
1199
1255
|
}
|
|
1200
1256
|
|
|
1201
1257
|
export const mysqlEngine = new MySQLEngine()
|
|
@@ -983,6 +983,66 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
983
983
|
})
|
|
984
984
|
})
|
|
985
985
|
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* List all user databases, excluding system databases (template0, template1, postgres).
|
|
989
|
+
*/
|
|
990
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
991
|
+
const { port } = container
|
|
992
|
+
const psqlPath = await this.getPsqlPath()
|
|
993
|
+
|
|
994
|
+
// Query pg_database for all non-system databases
|
|
995
|
+
const sql = `SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'postgres') AND datistemplate = false ORDER BY datname`
|
|
996
|
+
|
|
997
|
+
const args = [
|
|
998
|
+
'-X', // Skip ~/.psqlrc
|
|
999
|
+
'-h',
|
|
1000
|
+
'127.0.0.1',
|
|
1001
|
+
'-p',
|
|
1002
|
+
String(port),
|
|
1003
|
+
'-U',
|
|
1004
|
+
defaults.superuser,
|
|
1005
|
+
'-d',
|
|
1006
|
+
'postgres',
|
|
1007
|
+
'-t', // Tuples only (no headers)
|
|
1008
|
+
'-A', // Unaligned output
|
|
1009
|
+
'-c',
|
|
1010
|
+
sql,
|
|
1011
|
+
]
|
|
1012
|
+
|
|
1013
|
+
return new Promise((resolve, reject) => {
|
|
1014
|
+
const proc = spawn(psqlPath, args, {
|
|
1015
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
let stdout = ''
|
|
1019
|
+
let stderr = ''
|
|
1020
|
+
|
|
1021
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1022
|
+
stdout += data.toString()
|
|
1023
|
+
})
|
|
1024
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1025
|
+
stderr += data.toString()
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
1029
|
+
reject(err)
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
proc.on('close', (code) => {
|
|
1033
|
+
if (code === 0) {
|
|
1034
|
+
const databases = stdout
|
|
1035
|
+
.trim()
|
|
1036
|
+
.split('\n')
|
|
1037
|
+
.map((db) => db.trim())
|
|
1038
|
+
.filter((db) => db.length > 0)
|
|
1039
|
+
resolve(databases)
|
|
1040
|
+
} else {
|
|
1041
|
+
reject(new Error(stderr || `psql exited with code ${code}`))
|
|
1042
|
+
}
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
986
1046
|
}
|
|
987
1047
|
|
|
988
1048
|
export const postgresqlEngine = new PostgreSQLEngine()
|
package/engines/qdrant/index.ts
CHANGED
|
@@ -1192,6 +1192,16 @@ export class QdrantEngine extends BaseEngine {
|
|
|
1192
1192
|
|
|
1193
1193
|
return parseRESTAPIResult(JSON.stringify(response.data))
|
|
1194
1194
|
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* List databases for Qdrant.
|
|
1198
|
+
* Qdrant uses collections, not databases. Returns the configured database.
|
|
1199
|
+
*/
|
|
1200
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1201
|
+
// Qdrant uses collections, not databases
|
|
1202
|
+
// Return the container's configured database
|
|
1203
|
+
return [container.database]
|
|
1204
|
+
}
|
|
1195
1205
|
}
|
|
1196
1206
|
|
|
1197
1207
|
export const qdrantEngine = new QdrantEngine()
|
package/engines/questdb/index.ts
CHANGED
|
@@ -1008,6 +1008,16 @@ export class QuestDBEngine extends BaseEngine {
|
|
|
1008
1008
|
})
|
|
1009
1009
|
})
|
|
1010
1010
|
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* List databases for QuestDB.
|
|
1014
|
+
* QuestDB has a single database 'qdb'. Returns the configured database.
|
|
1015
|
+
*/
|
|
1016
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1017
|
+
// QuestDB has a single database 'qdb'
|
|
1018
|
+
// Return the container's configured database
|
|
1019
|
+
return [container.database]
|
|
1020
|
+
}
|
|
1011
1021
|
}
|
|
1012
1022
|
|
|
1013
1023
|
export const questdbEngine = new QuestDBEngine()
|
package/engines/redis/index.ts
CHANGED
|
@@ -1423,6 +1423,17 @@ export class RedisEngine extends BaseEngine {
|
|
|
1423
1423
|
proc.stdin?.end()
|
|
1424
1424
|
})
|
|
1425
1425
|
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* List databases for Redis.
|
|
1429
|
+
* Redis uses numbered databases (0-15 by default), not named databases.
|
|
1430
|
+
* Returns the configured database number as a single-item array.
|
|
1431
|
+
*/
|
|
1432
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1433
|
+
// Redis has numbered databases, not named ones
|
|
1434
|
+
// Return the container's configured database
|
|
1435
|
+
return [container.database]
|
|
1436
|
+
}
|
|
1426
1437
|
}
|
|
1427
1438
|
|
|
1428
1439
|
export const redisEngine = new RedisEngine()
|
package/engines/sqlite/index.ts
CHANGED
|
@@ -696,6 +696,16 @@ export class SQLiteEngine extends BaseEngine {
|
|
|
696
696
|
})
|
|
697
697
|
})
|
|
698
698
|
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* List databases for SQLite.
|
|
702
|
+
* SQLite is file-based with one database per file.
|
|
703
|
+
* Returns the configured database (file path) as a single-item array.
|
|
704
|
+
*/
|
|
705
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
706
|
+
// SQLite is file-based, one database per file
|
|
707
|
+
return [container.database]
|
|
708
|
+
}
|
|
699
709
|
}
|
|
700
710
|
|
|
701
711
|
export const sqliteEngine = new SQLiteEngine()
|
|
@@ -1070,6 +1070,77 @@ export class SurrealDBEngine extends BaseEngine {
|
|
|
1070
1070
|
})
|
|
1071
1071
|
})
|
|
1072
1072
|
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* List all databases in the container's namespace.
|
|
1076
|
+
* SurrealDB has a namespace > database hierarchy.
|
|
1077
|
+
*/
|
|
1078
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1079
|
+
const { port, version, name } = container
|
|
1080
|
+
const surreal = await this.getSurrealPath(version)
|
|
1081
|
+
const namespace = name.replace(/-/g, '_')
|
|
1082
|
+
|
|
1083
|
+
return new Promise((resolve, reject) => {
|
|
1084
|
+
const args = [
|
|
1085
|
+
'sql',
|
|
1086
|
+
'--endpoint',
|
|
1087
|
+
`ws://127.0.0.1:${port}`,
|
|
1088
|
+
'--user',
|
|
1089
|
+
'root',
|
|
1090
|
+
'--pass',
|
|
1091
|
+
'root',
|
|
1092
|
+
'--ns',
|
|
1093
|
+
namespace,
|
|
1094
|
+
'--db',
|
|
1095
|
+
container.database,
|
|
1096
|
+
'--hide-welcome',
|
|
1097
|
+
'--json',
|
|
1098
|
+
]
|
|
1099
|
+
|
|
1100
|
+
const proc = spawn(surreal, args, {
|
|
1101
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1102
|
+
cwd: paths.getContainerPath(name, { engine: ENGINE }),
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
let stdout = ''
|
|
1106
|
+
let stderr = ''
|
|
1107
|
+
|
|
1108
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
1109
|
+
stdout += data.toString()
|
|
1110
|
+
})
|
|
1111
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1112
|
+
stderr += data.toString()
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
proc.on('error', reject)
|
|
1116
|
+
|
|
1117
|
+
// Send the INFO FOR NS query to list databases
|
|
1118
|
+
proc.stdin?.write('INFO FOR NS;\n')
|
|
1119
|
+
proc.stdin?.end()
|
|
1120
|
+
|
|
1121
|
+
proc.on('close', (code) => {
|
|
1122
|
+
if (code !== 0) {
|
|
1123
|
+
reject(new Error(stderr || `surreal sql exited with code ${code}`))
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
try {
|
|
1128
|
+
// Parse JSON output - INFO FOR NS returns database info
|
|
1129
|
+
const results = JSON.parse(stdout)
|
|
1130
|
+
if (Array.isArray(results) && results[0]?.result?.databases) {
|
|
1131
|
+
const databases = Object.keys(results[0].result.databases)
|
|
1132
|
+
resolve(databases)
|
|
1133
|
+
} else {
|
|
1134
|
+
// No databases found or different format
|
|
1135
|
+
resolve([container.database])
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
// If parsing fails, return the configured database
|
|
1139
|
+
resolve([container.database])
|
|
1140
|
+
}
|
|
1141
|
+
})
|
|
1142
|
+
})
|
|
1143
|
+
}
|
|
1073
1144
|
}
|
|
1074
1145
|
|
|
1075
1146
|
export const surrealdbEngine = new SurrealDBEngine()
|
package/engines/valkey/index.ts
CHANGED
|
@@ -1450,6 +1450,17 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
1450
1450
|
proc.stdin?.end()
|
|
1451
1451
|
})
|
|
1452
1452
|
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* List databases for Valkey.
|
|
1456
|
+
* Valkey uses numbered databases (0-15 by default), not named databases.
|
|
1457
|
+
* Returns the configured database number as a single-item array.
|
|
1458
|
+
*/
|
|
1459
|
+
async listDatabases(container: ContainerConfig): Promise<string[]> {
|
|
1460
|
+
// Valkey has numbered databases, not named ones
|
|
1461
|
+
// Return the container's configured database
|
|
1462
|
+
return [container.database]
|
|
1463
|
+
}
|
|
1453
1464
|
}
|
|
1454
1465
|
|
|
1455
1466
|
export const valkeyEngine = new ValkeyEngine()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.3",
|
|
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.",
|