spindb 0.40.1 → 0.42.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.
- package/README.md +5 -0
- package/dist/cli/commands/databases.js +647 -12
- package/dist/cli/commands/databases.js.map +1 -1
- package/dist/cli/commands/menu/container-handlers.js +788 -175
- package/dist/cli/commands/menu/container-handlers.js.map +1 -1
- package/dist/cli/commands/query.js +2 -2
- package/dist/cli/commands/query.js.map +1 -1
- package/dist/config/paths.js +2 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/version.js +1 -1
- package/dist/core/container-manager.js +21 -0
- package/dist/core/container-manager.js.map +1 -1
- package/dist/core/database-capabilities.js +156 -0
- package/dist/core/database-capabilities.js.map +1 -0
- package/dist/engines/base-engine.js +8 -0
- package/dist/engines/base-engine.js.map +1 -1
- package/dist/engines/clickhouse/index.js +45 -0
- package/dist/engines/clickhouse/index.js.map +1 -1
- package/dist/engines/cockroachdb/index.js +45 -0
- package/dist/engines/cockroachdb/index.js.map +1 -1
- package/dist/engines/mariadb/index.js +1 -3
- package/dist/engines/mariadb/index.js.map +1 -1
- package/dist/engines/meilisearch/index.js +50 -1
- package/dist/engines/meilisearch/index.js.map +1 -1
- package/dist/engines/mongodb/index.js +42 -13
- package/dist/engines/mongodb/index.js.map +1 -1
- package/dist/engines/mysql/index.js +1 -3
- package/dist/engines/mysql/index.js.map +1 -1
- package/dist/engines/postgresql/index.js +50 -0
- package/dist/engines/postgresql/index.js.map +1 -1
- package/dist/engines/qdrant/index.js +2 -2
- package/dist/engines/qdrant/index.js.map +1 -1
- package/dist/engines/redis/index.js.map +1 -1
- package/dist/engines/valkey/index.js.map +1 -1
- package/dist/engines/weaviate/index.js +1 -1
- package/dist/engines/weaviate/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,6 +157,11 @@ spindb backup mydb
|
|
|
157
157
|
spindb restore mydb backup.dump
|
|
158
158
|
spindb clone mydb mydb-copy
|
|
159
159
|
spindb delete mydb -f
|
|
160
|
+
|
|
161
|
+
# Database management within containers
|
|
162
|
+
spindb databases create mydb analytics # Create a new database
|
|
163
|
+
spindb databases rename mydb old_name new_name # Rename (backup/restore or native)
|
|
164
|
+
spindb databases drop mydb analytics --force # Drop a database
|
|
160
165
|
```
|
|
161
166
|
|
|
162
167
|
Every engine works the same way. Learn one, use them all.
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { mkdir, stat, unlink } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { containerManager, updateRenameTracking, } from '../../core/container-manager.js';
|
|
7
|
+
import { uiError, uiSuccess, uiWarning } from '../ui/theme.js';
|
|
5
8
|
import { getEngineMetadata } from '../helpers.js';
|
|
9
|
+
import { getEngine } from '../../engines/index.js';
|
|
10
|
+
import { isRemoteContainer } from '../../types/index.js';
|
|
11
|
+
import { canCreateDatabase, canDropDatabase, canRenameDatabase, getDatabaseCapabilities, getUnsupportedCreateMessage, getUnsupportedDropMessage, getUnsupportedRenameMessage, } from '../../core/database-capabilities.js';
|
|
12
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
13
|
+
import { paths } from '../../config/paths.js';
|
|
14
|
+
import { getDefaultFormat, getBackupExtension, } from '../../config/backup-formats.js';
|
|
15
|
+
import { isInteractiveMode, isValidDatabaseName, } from '../../core/error-handler.js';
|
|
6
16
|
/**
|
|
7
|
-
* CLI command for managing
|
|
17
|
+
* CLI command for managing databases within containers.
|
|
8
18
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Use cases:
|
|
14
|
-
* - After renaming databases via SQL, update SpinDB's tracking to match
|
|
15
|
-
* - After external scripts create/drop databases, sync the tracking
|
|
16
|
-
* - Clean up stale database entries from tracking
|
|
19
|
+
* Includes:
|
|
20
|
+
* - create/drop/rename: Perform real database operations on running containers
|
|
21
|
+
* - list/add/remove/sync/refresh/set-default: Manage database tracking metadata
|
|
17
22
|
*/
|
|
18
|
-
export const databasesCommand = new Command('databases').description('Manage
|
|
23
|
+
export const databasesCommand = new Command('databases').description('Manage databases within a container');
|
|
19
24
|
// List databases in a container (or all containers if none specified)
|
|
20
25
|
databasesCommand
|
|
21
26
|
.command('list')
|
|
@@ -482,4 +487,634 @@ databasesCommand
|
|
|
482
487
|
process.exit(1);
|
|
483
488
|
}
|
|
484
489
|
});
|
|
490
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
491
|
+
// Real database operations (create, drop, rename)
|
|
492
|
+
// These perform actual database operations on running containers
|
|
493
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
494
|
+
/**
|
|
495
|
+
* Helper: output an error in the appropriate format and exit
|
|
496
|
+
*/
|
|
497
|
+
function outputError(message, json) {
|
|
498
|
+
if (json) {
|
|
499
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.error(uiError(message));
|
|
503
|
+
}
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Helper: validate database name format for CLI args
|
|
508
|
+
*/
|
|
509
|
+
function validateDbName(name, json) {
|
|
510
|
+
if (!isValidDatabaseName(name)) {
|
|
511
|
+
outputError(`Invalid database name: "${name}". Names must start with a letter and contain only letters, numbers, and underscores.`, json);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Helper: validate common preconditions for database operations
|
|
516
|
+
*/
|
|
517
|
+
async function validateContainer(containerName, options) {
|
|
518
|
+
const config = await containerManager.getConfig(containerName);
|
|
519
|
+
if (!config) {
|
|
520
|
+
outputError(`Container "${containerName}" not found`, options.json);
|
|
521
|
+
}
|
|
522
|
+
if (isRemoteContainer(config)) {
|
|
523
|
+
outputError(`Database operations are not supported for linked/remote containers. Use your database provider's tools instead.`, options.json);
|
|
524
|
+
}
|
|
525
|
+
if (options.requireRunning && config.status !== 'running') {
|
|
526
|
+
outputError(`Container "${containerName}" is not running. Start it first with: spindb start ${containerName}`, options.json);
|
|
527
|
+
}
|
|
528
|
+
return config;
|
|
529
|
+
}
|
|
530
|
+
// Create a new database within a running container
|
|
531
|
+
databasesCommand
|
|
532
|
+
.command('create')
|
|
533
|
+
.description('Create a new database within a running container')
|
|
534
|
+
.argument('<container>', 'Container name')
|
|
535
|
+
.argument('[database]', 'Database name (prompted interactively if omitted)')
|
|
536
|
+
.option('-j, --json', 'Output as JSON')
|
|
537
|
+
.action(async (containerName, database, options) => {
|
|
538
|
+
try {
|
|
539
|
+
const config = await validateContainer(containerName, {
|
|
540
|
+
json: options.json,
|
|
541
|
+
requireRunning: true,
|
|
542
|
+
});
|
|
543
|
+
if (!canCreateDatabase(config.engine)) {
|
|
544
|
+
outputError(getUnsupportedCreateMessage(config.engine), options.json);
|
|
545
|
+
}
|
|
546
|
+
// Require database arg in JSON mode (no interactive prompts)
|
|
547
|
+
if (!database && options.json) {
|
|
548
|
+
outputError('Database name is required in --json mode', options.json);
|
|
549
|
+
}
|
|
550
|
+
// Prompt for database name if not provided
|
|
551
|
+
if (!database) {
|
|
552
|
+
if (!isInteractiveMode()) {
|
|
553
|
+
outputError('Database name is required in non-interactive mode. Usage: spindb databases create <container> <database>', options.json);
|
|
554
|
+
}
|
|
555
|
+
const { dbName } = await inquirer.prompt([
|
|
556
|
+
{
|
|
557
|
+
type: 'input',
|
|
558
|
+
name: 'dbName',
|
|
559
|
+
message: 'Database name:',
|
|
560
|
+
validate: (input) => {
|
|
561
|
+
if (!input.trim())
|
|
562
|
+
return 'Database name is required';
|
|
563
|
+
if (/\s/.test(input))
|
|
564
|
+
return 'Database name cannot contain spaces';
|
|
565
|
+
return true;
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
database = dbName;
|
|
570
|
+
}
|
|
571
|
+
validateDbName(database, options.json);
|
|
572
|
+
// Check if database already exists in tracking
|
|
573
|
+
const rawDatabases = config.databases || [];
|
|
574
|
+
const trackedDatabases = [
|
|
575
|
+
...new Set([config.database, ...rawDatabases]),
|
|
576
|
+
];
|
|
577
|
+
if (trackedDatabases.includes(database)) {
|
|
578
|
+
outputError(`Database "${database}" already exists in "${containerName}"`, options.json);
|
|
579
|
+
}
|
|
580
|
+
// Check if database exists on the server
|
|
581
|
+
const engine = getEngine(config.engine);
|
|
582
|
+
try {
|
|
583
|
+
const serverDatabases = await engine.listDatabases(config);
|
|
584
|
+
if (serverDatabases.includes(database)) {
|
|
585
|
+
outputError(`Database "${database}" already exists on the server. Use "spindb databases add ${containerName} ${database}" to track it.`, options.json);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// listDatabases may not be supported; proceed anyway
|
|
590
|
+
}
|
|
591
|
+
// Create the database
|
|
592
|
+
if (!options.json) {
|
|
593
|
+
const spinner = createSpinner(`Creating database "${database}" in "${containerName}"...`);
|
|
594
|
+
spinner.start();
|
|
595
|
+
try {
|
|
596
|
+
await engine.createDatabase(config, database);
|
|
597
|
+
spinner.succeed(`Created database "${database}" in "${containerName}"`);
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
spinner.fail(`Failed to create database "${database}"`);
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
await engine.createDatabase(config, database);
|
|
606
|
+
}
|
|
607
|
+
// Track the new database
|
|
608
|
+
await containerManager.addDatabase(containerName, database);
|
|
609
|
+
// Get connection string
|
|
610
|
+
const connectionString = engine.getConnectionString(config, database);
|
|
611
|
+
if (options.json) {
|
|
612
|
+
console.log(JSON.stringify({
|
|
613
|
+
success: true,
|
|
614
|
+
container: containerName,
|
|
615
|
+
engine: config.engine,
|
|
616
|
+
database,
|
|
617
|
+
connectionString,
|
|
618
|
+
}, null, 2));
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
console.log(chalk.gray(` Connection: ${chalk.white(connectionString)}`));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
const e = error;
|
|
626
|
+
if (options.json) {
|
|
627
|
+
console.log(JSON.stringify({ error: e.message }, null, 2));
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
console.error(uiError(e.message));
|
|
631
|
+
}
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
// Drop a database from a running container
|
|
636
|
+
databasesCommand
|
|
637
|
+
.command('drop')
|
|
638
|
+
.description('Drop a database from a running container (with confirmation)')
|
|
639
|
+
.argument('<container>', 'Container name')
|
|
640
|
+
.argument('[database]', 'Database name (selected interactively if omitted)')
|
|
641
|
+
.option('-j, --json', 'Output as JSON')
|
|
642
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
643
|
+
.action(async (containerName, database, options) => {
|
|
644
|
+
try {
|
|
645
|
+
const config = await validateContainer(containerName, {
|
|
646
|
+
json: options.json,
|
|
647
|
+
requireRunning: true,
|
|
648
|
+
});
|
|
649
|
+
if (!canDropDatabase(config.engine)) {
|
|
650
|
+
outputError(getUnsupportedDropMessage(config.engine), options.json);
|
|
651
|
+
}
|
|
652
|
+
// Require database arg in JSON mode
|
|
653
|
+
if (!database && options.json) {
|
|
654
|
+
outputError('Database name is required in --json mode', options.json);
|
|
655
|
+
}
|
|
656
|
+
// Build list of droppable databases (exclude primary)
|
|
657
|
+
const rawDatabases = config.databases || [];
|
|
658
|
+
const trackedDatabases = [
|
|
659
|
+
...new Set([config.database, ...rawDatabases]),
|
|
660
|
+
];
|
|
661
|
+
const droppable = trackedDatabases.filter((db) => db !== config.database);
|
|
662
|
+
// Prompt for database name if not provided
|
|
663
|
+
if (!database) {
|
|
664
|
+
if (droppable.length === 0) {
|
|
665
|
+
outputError(`No databases to drop in "${containerName}". The primary database cannot be dropped.`, options.json);
|
|
666
|
+
}
|
|
667
|
+
if (!isInteractiveMode()) {
|
|
668
|
+
outputError('Database name is required in non-interactive mode. Usage: spindb databases drop <container> <database>', options.json);
|
|
669
|
+
}
|
|
670
|
+
const { dbName } = await inquirer.prompt([
|
|
671
|
+
{
|
|
672
|
+
type: 'list',
|
|
673
|
+
name: 'dbName',
|
|
674
|
+
message: 'Select database to drop:',
|
|
675
|
+
choices: droppable.map((db) => ({ name: db, value: db })),
|
|
676
|
+
},
|
|
677
|
+
]);
|
|
678
|
+
database = dbName;
|
|
679
|
+
}
|
|
680
|
+
validateDbName(database, options.json);
|
|
681
|
+
// Block dropping the primary database
|
|
682
|
+
if (database === config.database) {
|
|
683
|
+
outputError(`Cannot drop the primary database "${database}". Use "spindb delete ${containerName}" to remove the entire container.`, options.json);
|
|
684
|
+
}
|
|
685
|
+
// Confirm unless --force
|
|
686
|
+
if (!options.force && !options.json) {
|
|
687
|
+
if (!isInteractiveMode()) {
|
|
688
|
+
outputError(`Dropping a database is destructive. Use --force to skip confirmation in non-interactive mode.`, options.json);
|
|
689
|
+
}
|
|
690
|
+
const { confirm } = await inquirer.prompt([
|
|
691
|
+
{
|
|
692
|
+
type: 'confirm',
|
|
693
|
+
name: 'confirm',
|
|
694
|
+
message: `Drop database "${database}" from ${config.engine} container "${containerName}"? This cannot be undone.`,
|
|
695
|
+
default: false,
|
|
696
|
+
},
|
|
697
|
+
]);
|
|
698
|
+
if (!confirm) {
|
|
699
|
+
console.log(chalk.gray('Cancelled.'));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const engine = getEngine(config.engine);
|
|
704
|
+
// Terminate connections and drop
|
|
705
|
+
if (!options.json) {
|
|
706
|
+
const spinner = createSpinner(`Dropping database "${database}" from "${containerName}"...`);
|
|
707
|
+
spinner.start();
|
|
708
|
+
try {
|
|
709
|
+
await engine.terminateConnections(config, database);
|
|
710
|
+
await engine.dropDatabase(config, database);
|
|
711
|
+
spinner.succeed(`Dropped database "${database}" from "${containerName}"`);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
spinner.fail(`Failed to drop database "${database}"`);
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
await engine.terminateConnections(config, database);
|
|
720
|
+
await engine.dropDatabase(config, database);
|
|
721
|
+
}
|
|
722
|
+
// Remove from tracking
|
|
723
|
+
await containerManager.removeDatabase(containerName, database);
|
|
724
|
+
if (options.json) {
|
|
725
|
+
console.log(JSON.stringify({
|
|
726
|
+
success: true,
|
|
727
|
+
container: containerName,
|
|
728
|
+
engine: config.engine,
|
|
729
|
+
dropped: database,
|
|
730
|
+
}, null, 2));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
const e = error;
|
|
735
|
+
if (options.json) {
|
|
736
|
+
console.log(JSON.stringify({ error: e.message }, null, 2));
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
console.error(uiError(e.message));
|
|
740
|
+
}
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
// Rename a database within a running container
|
|
745
|
+
databasesCommand
|
|
746
|
+
.command('rename')
|
|
747
|
+
.description('Rename a database within a running container')
|
|
748
|
+
.argument('<container>', 'Container name')
|
|
749
|
+
.argument('[old-name]', 'Current database name')
|
|
750
|
+
.argument('[new-name]', 'New database name')
|
|
751
|
+
.option('-j, --json', 'Output as JSON')
|
|
752
|
+
.option('--backup', 'Force backup/restore path even for native-rename engines')
|
|
753
|
+
.option('--no-drop', 'Keep the old database after copying data to new name')
|
|
754
|
+
.action(async (containerName, oldName, newName, options) => {
|
|
755
|
+
try {
|
|
756
|
+
const config = await validateContainer(containerName, {
|
|
757
|
+
json: options.json,
|
|
758
|
+
requireRunning: true,
|
|
759
|
+
});
|
|
760
|
+
if (!canRenameDatabase(config.engine)) {
|
|
761
|
+
outputError(getUnsupportedRenameMessage(config.engine), options.json);
|
|
762
|
+
}
|
|
763
|
+
// Require both args in JSON mode
|
|
764
|
+
if ((!oldName || !newName) && options.json) {
|
|
765
|
+
outputError('Both old-name and new-name are required in --json mode', options.json);
|
|
766
|
+
}
|
|
767
|
+
// Build list of renameable databases
|
|
768
|
+
const rawDatabases = config.databases || [];
|
|
769
|
+
const trackedDatabases = [
|
|
770
|
+
...new Set([config.database, ...rawDatabases]),
|
|
771
|
+
];
|
|
772
|
+
// Prompt for old name if not provided
|
|
773
|
+
if (!oldName) {
|
|
774
|
+
if (trackedDatabases.length === 0) {
|
|
775
|
+
outputError(`No databases to rename in "${containerName}".`, options.json);
|
|
776
|
+
}
|
|
777
|
+
if (!isInteractiveMode()) {
|
|
778
|
+
outputError('Database names are required in non-interactive mode. Usage: spindb databases rename <container> <old> <new>', options.json);
|
|
779
|
+
}
|
|
780
|
+
const { dbName } = await inquirer.prompt([
|
|
781
|
+
{
|
|
782
|
+
type: 'list',
|
|
783
|
+
name: 'dbName',
|
|
784
|
+
message: 'Select database to rename:',
|
|
785
|
+
choices: trackedDatabases.map((db) => {
|
|
786
|
+
const isPrimary = db === config.database;
|
|
787
|
+
return {
|
|
788
|
+
name: isPrimary ? `${db} (primary)` : db,
|
|
789
|
+
value: db,
|
|
790
|
+
};
|
|
791
|
+
}),
|
|
792
|
+
},
|
|
793
|
+
]);
|
|
794
|
+
oldName = dbName;
|
|
795
|
+
}
|
|
796
|
+
// Prompt for new name if not provided
|
|
797
|
+
if (!newName) {
|
|
798
|
+
if (!isInteractiveMode()) {
|
|
799
|
+
outputError('New database name is required in non-interactive mode. Usage: spindb databases rename <container> <old> <new>', options.json);
|
|
800
|
+
}
|
|
801
|
+
const { dbName } = await inquirer.prompt([
|
|
802
|
+
{
|
|
803
|
+
type: 'input',
|
|
804
|
+
name: 'dbName',
|
|
805
|
+
message: `New name for "${oldName}":`,
|
|
806
|
+
validate: (input) => {
|
|
807
|
+
if (!input.trim())
|
|
808
|
+
return 'Database name is required';
|
|
809
|
+
if (/\s/.test(input))
|
|
810
|
+
return 'Database name cannot contain spaces';
|
|
811
|
+
if (input === oldName)
|
|
812
|
+
return 'New name must be different';
|
|
813
|
+
return true;
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
newName = dbName;
|
|
818
|
+
}
|
|
819
|
+
validateDbName(oldName, options.json);
|
|
820
|
+
validateDbName(newName, options.json);
|
|
821
|
+
// Validate old != new
|
|
822
|
+
if (oldName === newName) {
|
|
823
|
+
outputError(`Old and new database names are the same: "${oldName}"`, options.json);
|
|
824
|
+
}
|
|
825
|
+
// Validate old name exists
|
|
826
|
+
if (!trackedDatabases.includes(oldName)) {
|
|
827
|
+
outputError(`Database "${oldName}" is not tracked in "${containerName}". Use "spindb databases remove ${containerName} ${oldName}" to clean up stale entries.`, options.json);
|
|
828
|
+
}
|
|
829
|
+
// Validate new name doesn't already exist
|
|
830
|
+
if (trackedDatabases.includes(newName)) {
|
|
831
|
+
outputError(`Database "${newName}" already exists in "${containerName}"`, options.json);
|
|
832
|
+
}
|
|
833
|
+
// Check server for new name too
|
|
834
|
+
const engine = getEngine(config.engine);
|
|
835
|
+
try {
|
|
836
|
+
const serverDatabases = await engine.listDatabases(config);
|
|
837
|
+
if (!serverDatabases.includes(oldName)) {
|
|
838
|
+
outputError(`Database "${oldName}" does not exist on the server. Use "spindb databases remove ${containerName} ${oldName}" to clean up tracking.`, options.json);
|
|
839
|
+
}
|
|
840
|
+
if (serverDatabases.includes(newName)) {
|
|
841
|
+
outputError(`Database "${newName}" already exists on the server`, options.json);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// listDatabases may not be supported; proceed anyway
|
|
846
|
+
}
|
|
847
|
+
const caps = getDatabaseCapabilities(config.engine);
|
|
848
|
+
const useNativeRename = caps.supportsRename === 'native' &&
|
|
849
|
+
!options.backup &&
|
|
850
|
+
options.drop !== false;
|
|
851
|
+
const isPrimaryRename = oldName === config.database;
|
|
852
|
+
if (isPrimaryRename && !options.json) {
|
|
853
|
+
console.log(uiWarning(`Renaming the primary database. The primary will be updated to "${newName}".`));
|
|
854
|
+
}
|
|
855
|
+
if (useNativeRename) {
|
|
856
|
+
// Native rename path (PostgreSQL, ClickHouse, CockroachDB, Meilisearch)
|
|
857
|
+
if (!options.json) {
|
|
858
|
+
const spinner = createSpinner(`Renaming "${oldName}" to "${newName}" in "${containerName}"...`);
|
|
859
|
+
spinner.start();
|
|
860
|
+
try {
|
|
861
|
+
await engine.renameDatabase(config, oldName, newName);
|
|
862
|
+
spinner.succeed(`Renamed "${oldName}" to "${newName}"`);
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
spinner.fail(`Failed to rename database`);
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
await engine.renameDatabase(config, oldName, newName);
|
|
871
|
+
}
|
|
872
|
+
// Update tracking
|
|
873
|
+
await updateRenameTracking(containerName, oldName, newName, {
|
|
874
|
+
shouldDrop: true,
|
|
875
|
+
isPrimaryRename,
|
|
876
|
+
});
|
|
877
|
+
const connectionString = engine.getConnectionString({ ...config, database: newName }, newName);
|
|
878
|
+
if (options.json) {
|
|
879
|
+
console.log(JSON.stringify({
|
|
880
|
+
success: true,
|
|
881
|
+
container: containerName,
|
|
882
|
+
engine: config.engine,
|
|
883
|
+
oldName,
|
|
884
|
+
newName,
|
|
885
|
+
method: 'native',
|
|
886
|
+
connectionString,
|
|
887
|
+
primaryChanged: isPrimaryRename,
|
|
888
|
+
}, null, 2));
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
console.log(chalk.gray(` Connection: ${chalk.white(connectionString)}`));
|
|
892
|
+
if (isPrimaryRename) {
|
|
893
|
+
console.log(chalk.gray(` Note: The primary database has been changed to "${newName}".`));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
// Backup/restore rename path
|
|
899
|
+
if (!options.json) {
|
|
900
|
+
if (caps.supportsRename === 'native') {
|
|
901
|
+
console.log(`\nUsing backup/restore (native rename bypassed via flags).`);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
console.log(`\n${engine.displayName} does not support native database renaming.`);
|
|
905
|
+
}
|
|
906
|
+
console.log(`Cloning "${oldName}" to "${newName}" in "${containerName}" via backup/restore...\n`);
|
|
907
|
+
}
|
|
908
|
+
await mkdir(paths.renameBackups, { recursive: true });
|
|
909
|
+
const format = getDefaultFormat(config.engine);
|
|
910
|
+
const extension = getBackupExtension(config.engine, format);
|
|
911
|
+
const timestamp = new Date()
|
|
912
|
+
.toISOString()
|
|
913
|
+
.replace(/[:.]/g, '')
|
|
914
|
+
.slice(0, 15);
|
|
915
|
+
const backupFileName = `${containerName}-${oldName}-rename-${timestamp}${extension}`;
|
|
916
|
+
const backupPath = join(paths.renameBackups, backupFileName);
|
|
917
|
+
let backupSize = 0;
|
|
918
|
+
// Step 1: Backup old database
|
|
919
|
+
if (!options.json) {
|
|
920
|
+
const spinner = createSpinner(`Backing up "${oldName}"...`);
|
|
921
|
+
spinner.start();
|
|
922
|
+
try {
|
|
923
|
+
const result = await engine.backup(config, backupPath, {
|
|
924
|
+
database: oldName,
|
|
925
|
+
format,
|
|
926
|
+
});
|
|
927
|
+
backupSize = result.size;
|
|
928
|
+
const sizeStr = backupSize > 0 ? ` (${formatBackupSize(backupSize)})` : '';
|
|
929
|
+
spinner.succeed(`Backed up "${oldName}"${sizeStr}`);
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
spinner.fail(`Failed to backup "${oldName}"`);
|
|
933
|
+
throw error;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
const result = await engine.backup(config, backupPath, {
|
|
938
|
+
database: oldName,
|
|
939
|
+
format,
|
|
940
|
+
});
|
|
941
|
+
backupSize = result.size;
|
|
942
|
+
}
|
|
943
|
+
// Step 2: Create new database
|
|
944
|
+
let newDbCreated = false;
|
|
945
|
+
try {
|
|
946
|
+
if (!options.json) {
|
|
947
|
+
const spinner = createSpinner(`Creating database "${newName}"...`);
|
|
948
|
+
spinner.start();
|
|
949
|
+
try {
|
|
950
|
+
await engine.createDatabase(config, newName);
|
|
951
|
+
newDbCreated = true;
|
|
952
|
+
spinner.succeed(`Created database "${newName}"`);
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
spinner.fail(`Failed to create database "${newName}"`);
|
|
956
|
+
throw error;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
await engine.createDatabase(config, newName);
|
|
961
|
+
newDbCreated = true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
// Rollback: delete backup file
|
|
966
|
+
try {
|
|
967
|
+
await unlink(backupPath);
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
// Backup file may not exist
|
|
971
|
+
}
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
// Step 3: Restore data to new database
|
|
975
|
+
try {
|
|
976
|
+
if (!options.json) {
|
|
977
|
+
const spinner = createSpinner(`Restoring data to "${newName}"...`);
|
|
978
|
+
spinner.start();
|
|
979
|
+
try {
|
|
980
|
+
await engine.restore({ ...config, database: newName }, backupPath, { database: newName });
|
|
981
|
+
spinner.succeed(`Restored data to "${newName}"`);
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
spinner.fail(`Failed to restore data to "${newName}"`);
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
await engine.restore({ ...config, database: newName }, backupPath, { database: newName });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
// Rollback: drop newly created database, keep backup
|
|
994
|
+
if (newDbCreated) {
|
|
995
|
+
try {
|
|
996
|
+
await engine.dropDatabase(config, newName);
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// Best-effort cleanup
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const e = error;
|
|
1003
|
+
const msg = `Restore failed: ${e.message}\nSafety backup retained at: ${backupPath}`;
|
|
1004
|
+
outputError(msg, options.json);
|
|
1005
|
+
}
|
|
1006
|
+
// Step 4: Verify new database exists
|
|
1007
|
+
if (!options.json) {
|
|
1008
|
+
const spinner = createSpinner(`Verifying "${newName}" exists...`);
|
|
1009
|
+
spinner.start();
|
|
1010
|
+
try {
|
|
1011
|
+
const serverDbs = await engine.listDatabases(config);
|
|
1012
|
+
if (serverDbs.includes(newName)) {
|
|
1013
|
+
spinner.succeed(`Verified "${newName}" exists`);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
spinner.warn(`Could not verify "${newName}" via listDatabases`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
spinner.warn(`Verification skipped (listDatabases not supported)`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Step 5: Drop old database (unless --no-drop)
|
|
1024
|
+
// options.drop is false when --no-drop is passed (commander inverts it)
|
|
1025
|
+
const shouldDrop = options.drop !== false;
|
|
1026
|
+
let dropSucceeded = false;
|
|
1027
|
+
let oldDropError;
|
|
1028
|
+
if (shouldDrop) {
|
|
1029
|
+
if (!options.json) {
|
|
1030
|
+
const spinner = createSpinner(`Dropping old database "${oldName}"...`);
|
|
1031
|
+
spinner.start();
|
|
1032
|
+
try {
|
|
1033
|
+
await engine.terminateConnections(config, oldName);
|
|
1034
|
+
await engine.dropDatabase(config, oldName);
|
|
1035
|
+
spinner.succeed(`Dropped old database "${oldName}"`);
|
|
1036
|
+
dropSucceeded = true;
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
const e = error;
|
|
1040
|
+
oldDropError = e.message;
|
|
1041
|
+
spinner.warn(`Could not drop old database "${oldName}": ${e.message}`);
|
|
1042
|
+
// Non-fatal — data is safely in new database
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
try {
|
|
1047
|
+
await engine.terminateConnections(config, oldName);
|
|
1048
|
+
await engine.dropDatabase(config, oldName);
|
|
1049
|
+
dropSucceeded = true;
|
|
1050
|
+
}
|
|
1051
|
+
catch (error) {
|
|
1052
|
+
oldDropError = error.message;
|
|
1053
|
+
// Non-fatal — reported in JSON output
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Update tracking — only remove old DB from tracking if drop actually succeeded
|
|
1058
|
+
await updateRenameTracking(containerName, oldName, newName, {
|
|
1059
|
+
shouldDrop: dropSucceeded,
|
|
1060
|
+
isPrimaryRename,
|
|
1061
|
+
});
|
|
1062
|
+
const connectionString = engine.getConnectionString({ ...config, database: newName }, newName);
|
|
1063
|
+
// Get backup file size
|
|
1064
|
+
try {
|
|
1065
|
+
const backupStat = await stat(backupPath);
|
|
1066
|
+
backupSize = backupStat.size;
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
// Use previously captured size
|
|
1070
|
+
}
|
|
1071
|
+
if (options.json) {
|
|
1072
|
+
console.log(JSON.stringify({
|
|
1073
|
+
success: true,
|
|
1074
|
+
container: containerName,
|
|
1075
|
+
engine: config.engine,
|
|
1076
|
+
oldName,
|
|
1077
|
+
newName,
|
|
1078
|
+
method: 'backup-restore',
|
|
1079
|
+
backup: {
|
|
1080
|
+
path: backupPath,
|
|
1081
|
+
size: backupSize,
|
|
1082
|
+
format: String(format),
|
|
1083
|
+
},
|
|
1084
|
+
connectionString,
|
|
1085
|
+
primaryChanged: isPrimaryRename,
|
|
1086
|
+
oldDatabaseDropped: dropSucceeded,
|
|
1087
|
+
...(oldDropError && { oldDropError }),
|
|
1088
|
+
}, null, 2));
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
console.log(`\nRename complete.`);
|
|
1092
|
+
console.log(chalk.gray(` Safety backup: ${chalk.white(backupPath)}`));
|
|
1093
|
+
console.log(chalk.gray(` Connection: ${chalk.white(connectionString)}`));
|
|
1094
|
+
if (isPrimaryRename) {
|
|
1095
|
+
console.log(chalk.gray(`\n Note: The primary database has been changed to "${newName}".`));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
const e = error;
|
|
1102
|
+
if (options.json) {
|
|
1103
|
+
console.log(JSON.stringify({ error: e.message }, null, 2));
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
console.error(uiError(e.message));
|
|
1107
|
+
}
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
function formatBackupSize(bytes) {
|
|
1112
|
+
if (bytes < 1024)
|
|
1113
|
+
return `${bytes} B`;
|
|
1114
|
+
if (bytes < 1024 * 1024)
|
|
1115
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1116
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
1117
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1118
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1119
|
+
}
|
|
485
1120
|
//# sourceMappingURL=databases.js.map
|