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.
Files changed (37) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commands/databases.js +647 -12
  3. package/dist/cli/commands/databases.js.map +1 -1
  4. package/dist/cli/commands/menu/container-handlers.js +788 -175
  5. package/dist/cli/commands/menu/container-handlers.js.map +1 -1
  6. package/dist/cli/commands/query.js +2 -2
  7. package/dist/cli/commands/query.js.map +1 -1
  8. package/dist/config/paths.js +2 -0
  9. package/dist/config/paths.js.map +1 -1
  10. package/dist/config/version.js +1 -1
  11. package/dist/core/container-manager.js +21 -0
  12. package/dist/core/container-manager.js.map +1 -1
  13. package/dist/core/database-capabilities.js +156 -0
  14. package/dist/core/database-capabilities.js.map +1 -0
  15. package/dist/engines/base-engine.js +8 -0
  16. package/dist/engines/base-engine.js.map +1 -1
  17. package/dist/engines/clickhouse/index.js +45 -0
  18. package/dist/engines/clickhouse/index.js.map +1 -1
  19. package/dist/engines/cockroachdb/index.js +45 -0
  20. package/dist/engines/cockroachdb/index.js.map +1 -1
  21. package/dist/engines/mariadb/index.js +1 -3
  22. package/dist/engines/mariadb/index.js.map +1 -1
  23. package/dist/engines/meilisearch/index.js +50 -1
  24. package/dist/engines/meilisearch/index.js.map +1 -1
  25. package/dist/engines/mongodb/index.js +42 -13
  26. package/dist/engines/mongodb/index.js.map +1 -1
  27. package/dist/engines/mysql/index.js +1 -3
  28. package/dist/engines/mysql/index.js.map +1 -1
  29. package/dist/engines/postgresql/index.js +50 -0
  30. package/dist/engines/postgresql/index.js.map +1 -1
  31. package/dist/engines/qdrant/index.js +2 -2
  32. package/dist/engines/qdrant/index.js.map +1 -1
  33. package/dist/engines/redis/index.js.map +1 -1
  34. package/dist/engines/valkey/index.js.map +1 -1
  35. package/dist/engines/weaviate/index.js +1 -1
  36. package/dist/engines/weaviate/index.js.map +1 -1
  37. 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 { containerManager } from '../../core/container-manager.js';
4
- import { uiError, uiSuccess } from '../ui/theme.js';
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 database tracking within containers.
17
+ * CLI command for managing databases within containers.
8
18
  *
9
- * SpinDB tracks which databases exist within a container for informational purposes.
10
- * This command allows adding/removing databases from tracking without actually
11
- * creating/dropping them in the database server.
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 database tracking within a container');
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