Skip to content

Commit d55b155

Browse files
committed
feat(cli): add database migrate and backup commands
1 parent 64ba78b commit d55b155

5 files changed

Lines changed: 462 additions & 21 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
*
3+
* @file DbBackup.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_DB_BACKUP_HPP
14+
#define VIX_DB_BACKUP_HPP
15+
16+
#include <vix/cli/commands/db/DbTypes.hpp>
17+
18+
namespace vix::commands::db::backup
19+
{
20+
/**
21+
* @brief Create a SQLite database backup.
22+
*
23+
* This function creates a timestamped backup copy of the configured
24+
* SQLite database file. If SQLite WAL and SHM files are present, they
25+
* are copied beside the database backup as well.
26+
*
27+
* Current MVP behavior:
28+
* - supports SQLite only
29+
* - requires the database file to exist
30+
* - creates the backups directory if missing
31+
* - copies `.db`, `.db-wal`, and `.db-shm` when present
32+
*
33+
* @param cfg Effective database configuration.
34+
* @param options Runtime database options.
35+
* @return Process exit code.
36+
*/
37+
int create_backup(
38+
const DbConfig &cfg,
39+
const DbOptions &options);
40+
}
41+
42+
#endif
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
*
3+
* @file DbMigrator.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_DB_MIGRATOR_HPP
14+
#define VIX_DB_MIGRATOR_HPP
15+
16+
#include <vix/cli/commands/db/DbTypes.hpp>
17+
18+
namespace vix::commands::db::migrator
19+
{
20+
/**
21+
* @brief Apply pending database migrations.
22+
*
23+
* This function applies file-based SQL migrations from the configured
24+
* migrations directory.
25+
*
26+
* Current MVP behavior:
27+
* - supports SQLite only
28+
* - requires the storage directory to exist
29+
* - requires the migrations directory to exist
30+
* - creates the SQLite database file if it does not exist yet
31+
*
32+
* @param cfg Effective database configuration.
33+
* @param options Runtime database options.
34+
* @return Process exit code.
35+
*/
36+
int migrate(
37+
const DbConfig &cfg,
38+
const DbOptions &options);
39+
}
40+
41+
#endif

src/commands/DbCommand.cpp

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
* Vix.cpp
1212
*/
1313
#include <vix/cli/commands/DbCommand.hpp>
14+
#include <vix/cli/commands/db/DbBackup.hpp>
1415
#include <vix/cli/commands/db/DbChecker.hpp>
1516
#include <vix/cli/commands/db/DbConfig.hpp>
17+
#include <vix/cli/commands/db/DbMigrator.hpp>
1618
#include <vix/cli/commands/db/DbOutput.hpp>
1719
#include <vix/cli/commands/db/DbTypes.hpp>
1820

@@ -155,33 +157,47 @@ namespace vix::commands
155157
return 1;
156158
}
157159

158-
if (action != "status")
160+
const db::DbConfig cfg =
161+
db::apply_db_options(
162+
db::load_db_config(),
163+
options);
164+
165+
if (action == "status")
159166
{
160-
db::output::error(
161-
std::cerr,
162-
"unknown db action: " + action);
167+
const db::DbCheckResult result =
168+
db::checker::check_status(cfg);
163169

164-
db::output::fix(
165-
std::cerr,
166-
"vix db --help");
170+
db::output::print_status(
171+
std::cout,
172+
result,
173+
options);
167174

168-
return 1;
175+
return exit_code_for_status(result.status);
169176
}
170177

171-
const db::DbConfig cfg =
172-
db::apply_db_options(
173-
db::load_db_config(),
174-
options);
178+
if (action == "migrate")
179+
{
180+
return db::migrator::migrate(
181+
cfg,
182+
options);
183+
}
184+
185+
if (action == "backup")
186+
{
187+
return db::backup::create_backup(
188+
cfg,
189+
options);
190+
}
175191

176-
const db::DbCheckResult result =
177-
db::checker::check_status(cfg);
192+
db::output::error(
193+
std::cerr,
194+
"unknown db action: " + action);
178195

179-
db::output::print_status(
180-
std::cout,
181-
result,
182-
options);
196+
db::output::fix(
197+
std::cerr,
198+
"vix db --help");
183199

184-
return exit_code_for_status(result.status);
200+
return 1;
185201
}
186202

187203
int DbCommand::help()
@@ -190,7 +206,9 @@ namespace vix::commands
190206
<< "Usage:\n"
191207
<< " vix db [action] [options]\n\n"
192208
<< "Actions:\n"
193-
<< " status Inspect SQLite database and storage status\n\n"
209+
<< " status Inspect SQLite database and storage status\n"
210+
<< " migrate Apply pending file-based SQL migrations\n"
211+
<< " backup Create a SQLite database backup\n\n"
194212
<< "Options:\n"
195213
<< " --json Print supported output as JSON\n"
196214
<< " --verbose Show verbose diagnostic output\n"
@@ -199,7 +217,9 @@ namespace vix::commands
199217
<< "Examples:\n"
200218
<< " vix db\n"
201219
<< " vix db status\n"
202-
<< " vix db status --json\n\n"
220+
<< " vix db status --json\n"
221+
<< " vix db migrate\n"
222+
<< " vix db backup\n\n"
203223
<< "Config:\n"
204224
<< " database.engine\n"
205225
<< " database.sqlite.path\n"

src/commands/db/DbBackup.cpp

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
*
3+
* @file DbBackup.cpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2026, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#include <vix/cli/commands/db/DbBackup.hpp>
14+
#include <vix/cli/commands/db/DbOutput.hpp>
15+
#include <vix/cli/util/Ui.hpp>
16+
17+
#include <chrono>
18+
#include <ctime>
19+
#include <exception>
20+
#include <filesystem>
21+
#include <iomanip>
22+
#include <iostream>
23+
#include <sstream>
24+
#include <string>
25+
26+
namespace fs = std::filesystem;
27+
28+
namespace vix::commands::db::backup
29+
{
30+
namespace
31+
{
32+
std::string timestamp()
33+
{
34+
using namespace std::chrono;
35+
36+
const auto now = system_clock::now();
37+
const std::time_t t = system_clock::to_time_t(now);
38+
39+
std::tm tm{};
40+
41+
#if defined(_WIN32)
42+
localtime_s(&tm, &t);
43+
#else
44+
localtime_r(&t, &tm);
45+
#endif
46+
47+
std::ostringstream out;
48+
out << std::put_time(&tm, "%Y%m%d-%H%M%S");
49+
return out.str();
50+
}
51+
52+
fs::path backup_database_path(
53+
const DbConfig &cfg,
54+
const std::string &stamp)
55+
{
56+
const std::string stem =
57+
cfg.databasePath.stem().string().empty()
58+
? cfg.appName
59+
: cfg.databasePath.stem().string();
60+
61+
const std::string ext =
62+
cfg.databasePath.extension().string().empty()
63+
? ".db"
64+
: cfg.databasePath.extension().string();
65+
66+
return fs::path("backups") / (stem + "-" + stamp + ext);
67+
}
68+
69+
bool is_sqlite(const DbConfig &cfg)
70+
{
71+
return cfg.engine == DbEngine::SQLite;
72+
}
73+
74+
bool copy_if_present(
75+
const fs::path &from,
76+
const fs::path &to)
77+
{
78+
if (!fs::exists(from) || !fs::is_regular_file(from))
79+
return false;
80+
81+
fs::copy_file(
82+
from,
83+
to,
84+
fs::copy_options::overwrite_existing);
85+
86+
return true;
87+
}
88+
89+
fs::path sidecar_backup_path(
90+
const fs::path &backupPath,
91+
const std::string &suffix)
92+
{
93+
return fs::path(backupPath.string() + suffix);
94+
}
95+
}
96+
97+
int create_backup(
98+
const DbConfig &cfg,
99+
const DbOptions &options)
100+
{
101+
(void)options;
102+
103+
if (!is_sqlite(cfg))
104+
{
105+
output::error(
106+
std::cerr,
107+
"vix db backup currently supports SQLite only.");
108+
109+
output::fix(
110+
std::cerr,
111+
"set database.engine to sqlite before using vix db backup");
112+
113+
return 1;
114+
}
115+
116+
if (!fs::exists(cfg.databasePath) ||
117+
!fs::is_regular_file(cfg.databasePath))
118+
{
119+
output::error(
120+
std::cerr,
121+
"database file not found: " + cfg.databasePath.string());
122+
123+
output::fix(
124+
std::cerr,
125+
"run vix db migrate or create the database before backing it up");
126+
127+
return 1;
128+
}
129+
130+
try
131+
{
132+
const fs::path backupDir = "backups";
133+
fs::create_directories(backupDir);
134+
135+
const std::string stamp = timestamp();
136+
const fs::path backupPath =
137+
backup_database_path(cfg, stamp);
138+
139+
fs::copy_file(
140+
cfg.databasePath,
141+
backupPath,
142+
fs::copy_options::overwrite_existing);
143+
144+
const bool copiedWal =
145+
copy_if_present(
146+
cfg.walPath,
147+
sidecar_backup_path(backupPath, "-wal"));
148+
149+
const bool copiedShm =
150+
copy_if_present(
151+
cfg.shmPath,
152+
sidecar_backup_path(backupPath, "-shm"));
153+
154+
output::step(std::cout, "Database Backup");
155+
156+
vix::cli::util::kv(
157+
std::cout,
158+
"Database",
159+
cfg.databasePath.string());
160+
161+
vix::cli::util::kv(
162+
std::cout,
163+
"Backup",
164+
backupPath.string());
165+
166+
vix::cli::util::kv(
167+
std::cout,
168+
"WAL copied",
169+
copiedWal ? "yes" : "no");
170+
171+
vix::cli::util::kv(
172+
std::cout,
173+
"SHM copied",
174+
copiedShm ? "yes" : "no");
175+
176+
output::ok(
177+
std::cout,
178+
"database backup created");
179+
180+
return 0;
181+
}
182+
catch (const std::exception &e)
183+
{
184+
output::error(
185+
std::cerr,
186+
"backup failed: " + std::string(e.what()));
187+
188+
return 1;
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)