Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1a06c5f

Browse files
committedMay 2, 2024·
Added schema migration
1 parent 99586c9 commit 1a06c5f

File tree

3 files changed

+140
-2
lines changed

3 files changed

+140
-2
lines changed
 

‎include/sqnice/database.hh

+43
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,49 @@ namespace sqnice {
256256
/// @warning NEVER pass an untrusted string; it can enable SQL injection attacks!
257257
status pragma(const char* name, std::string_view value);
258258

259+
#pragma mark - SCHEMA MIGRATION:
260+
261+
/// Returns the database's "user version", an app-defined version number. Initially 0.
262+
[[nodiscard]] int64_t user_version() const;
263+
264+
/// Sets the database's "user version", an app-defined version number.
265+
status set_user_version(int64_t);
266+
267+
/// Simple schema upgrades. If the database's user version is equal to `old_version`,
268+
/// executes the SQL command in `sql_command` and then sets the version to `new_version`.
269+
/// The SQL will presumably change the database's schema by adding tables or indexes,
270+
/// altering tables, etc.
271+
/// It's recommended that you wrap all your migrations in a single transaction.
272+
status migrate_from(int64_t old_version,
273+
int64_t new_version,
274+
std::string_view sql_command);
275+
276+
/// Simple schema upgrades. If the database's user version is less than `new_version`,
277+
/// executes the SQL command(s) in `sql_command` and then sets the user version to
278+
/// `new_version`.
279+
/// The SQL will presumably change the database's schema by adding tables or indexes,
280+
/// altering tables, etc.
281+
/// It's recommended that you wrap all your migrations in a single transaction.
282+
status migrate_to(int64_t new_version,
283+
std::string_view sql_command);
284+
285+
/// Simple schema upgrades. If the database's user version is equal to `old_version`,
286+
/// calls `fn` and on success sets the user version to `new_version`.
287+
/// The function will presumably change the database's schema by adding tables or indexes,
288+
/// altering tables, etc.
289+
/// It's recommended that you wrap all your migrations in a single transaction.
290+
status migrate_from(int64_t old_version,
291+
int64_t new_version,
292+
std::function<status(database&)> fn);
293+
294+
/// Simple schema upgrades. If the database's user version is less than `new_version`,
295+
/// calls `fn` and on success sets the user version to `new_version`.
296+
/// The function will presumably change the database's schema by adding tables or indexes,
297+
/// altering tables, etc.
298+
/// It's recommended that you wrap all your migrations in a single transaction.
299+
status migrate_to(int64_t new_version,
300+
std::function<status(database&)> fn);
301+
259302
#pragma mark - STATUS:
260303

261304
/// The status of the last operation on this database or its queries/commands/blob_handles.

‎src/database.cc

+36
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,42 @@ namespace sqnice {
312312
}
313313

314314

315+
int64_t database::user_version() const {
316+
return const_cast<database*>(this)->pragma("user_version");
317+
}
318+
319+
status database::set_user_version(int64_t v) {
320+
return pragma("user_version", v);
321+
}
322+
323+
status database::migrate_from(int64_t old, int64_t nuu, function<status (database &)> fn) {
324+
assert(old < nuu);
325+
if (user_version() == old) {
326+
if (auto rc = fn(*this); !ok(rc))
327+
return rc;
328+
set_user_version(nuu);
329+
}
330+
return status::ok;
331+
}
332+
333+
status database::migrate_to(int64_t nuu, function<status (database &)> fn) {
334+
if (user_version() < nuu) {
335+
if (auto rc = fn(*this); !ok(rc))
336+
return rc;
337+
set_user_version(nuu);
338+
}
339+
return status::ok;
340+
}
341+
342+
status database::migrate_from(int64_t old, int64_t nuu, string_view sql) {
343+
return migrate_from(old, nuu, [sql](database& db) {return db.execute(sql);});
344+
}
345+
346+
status database::migrate_to(int64_t nuu, string_view sql) {
347+
return migrate_to(nuu, [sql](database& db) {return db.execute(sql);});
348+
}
349+
350+
315351
int64_t database::pragma(const char* pragma) {
316352
return sqnice::query(*this, string("PRAGMA \"") + pragma + "\"").single_value_or<int>(0);
317353
}

‎test/testdb.cc

+61-2
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ TEST_CASE_METHOD(sqnice_test, "SQNice callbacks", "[sqnice]") {
151151
}
152152

153153
TEST_CASE("SQNice pool", "[sqnice]") {
154-
sqnice::pool p("sqnice_test.sqlite3",
155-
sqnice::open_flags::delete_first | sqnice::open_flags::readwrite);
154+
static constexpr string_view kDBPath = "sqnice_test.sqlite3";
155+
sqnice::pool p(kDBPath, sqnice::open_flags::delete_first | sqnice::open_flags::readwrite);
156156
{
157157
auto db = p.borrow_writeable();
158158
CHECK(p.borrowed_count() == 1);
@@ -198,4 +198,63 @@ TEST_CASE("SQNice pool", "[sqnice]") {
198198
CHECK(p.try_borrow_writeable() == nullptr);
199199
}
200200
CHECK(p.borrowed_count() == 4);
201+
sqnice::database::delete_file(kDBPath);
202+
}
203+
204+
205+
TEST_CASE("SQNice schema migration", "[sqnice]") {
206+
static constexpr string_view kDBPath = "sqnice_test.sqlite3";
207+
sqnice::database::delete_file(kDBPath);
208+
auto open_v1 = [] {
209+
sqnice::database db(kDBPath);
210+
db.setup();
211+
sqnice::transaction txn(db);
212+
db.migrate_from(0, 1, R"""(
213+
CREATE TABLE contacts (
214+
id INTEGER PRIMARY KEY,
215+
name TEXT NOT NULL,
216+
phone TEXT NOT NULL,
217+
address TEXT,
218+
UNIQUE(name, phone)
219+
);
220+
)""");
221+
txn.commit();
222+
CHECK(db.user_version() == 1);
223+
};
224+
225+
open_v1();
226+
227+
open_v1();
228+
229+
auto open_v2 = [] (int64_t expectedVersion) {
230+
sqnice::database db(kDBPath);
231+
sqnice::transaction txn(db);
232+
CHECK(db.user_version() == expectedVersion);
233+
234+
// Migration for a newly created database, leaving it at version 2:
235+
db.migrate_from(0, 2, R"""(
236+
CREATE TABLE contacts (
237+
id INTEGER PRIMARY KEY,
238+
name TEXT NOT NULL,
239+
phone TEXT NOT NULL,
240+
address TEXT,
241+
age INTEGER,
242+
UNIQUE(name, phone)
243+
);
244+
)""");
245+
246+
// Migration to upgrade version 1 (above) to version 2:
247+
db.migrate_to(2, "ALTER TABLE contacts ADD COLUMN age INTEGER");
248+
txn.commit();
249+
250+
CHECK(db.user_version() == 2);
251+
252+
db.execute("INSERT INTO contacts (name, phone, age) VALUES ('Alice', '555-1919', 39)");
253+
};
254+
255+
open_v2(1);
256+
257+
sqnice::database::delete_file(kDBPath);
258+
259+
open_v2(0);
201260
}

0 commit comments

Comments
 (0)
Please sign in to comment.