Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added schema migration #1

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions include/sqnice/database.hh
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,49 @@ namespace sqnice {
/// @warning NEVER pass an untrusted string; it can enable SQL injection attacks!
status pragma(const char* name, std::string_view value);

#pragma mark - SCHEMA MIGRATION:

/// Returns the database's "user version", an app-defined version number. Initially 0.
[[nodiscard]] int64_t user_version() const;

/// Sets the database's "user version", an app-defined version number.
status set_user_version(int64_t);

/// Simple schema upgrades. If the database's user version is equal to `old_version`,
/// executes the SQL command in `sql_command` and then sets the version to `new_version`.
/// The SQL will presumably change the database's schema by adding tables or indexes,
/// altering tables, etc.
/// It's recommended that you wrap all your migrations in a single transaction.
status migrate_from(int64_t old_version,
int64_t new_version,
std::string_view sql_command);

/// Simple schema upgrades. If the database's user version is less than `new_version`,
/// executes the SQL command(s) in `sql_command` and then sets the user version to
/// `new_version`.
/// The SQL will presumably change the database's schema by adding tables or indexes,
/// altering tables, etc.
/// It's recommended that you wrap all your migrations in a single transaction.
status migrate_to(int64_t new_version,
std::string_view sql_command);

/// Simple schema upgrades. If the database's user version is equal to `old_version`,
/// calls `fn` and on success sets the user version to `new_version`.
/// The function will presumably change the database's schema by adding tables or indexes,
/// altering tables, etc.
/// It's recommended that you wrap all your migrations in a single transaction.
status migrate_from(int64_t old_version,
int64_t new_version,
std::function<status(database&)> fn);

/// Simple schema upgrades. If the database's user version is less than `new_version`,
/// calls `fn` and on success sets the user version to `new_version`.
/// The function will presumably change the database's schema by adding tables or indexes,
/// altering tables, etc.
/// It's recommended that you wrap all your migrations in a single transaction.
status migrate_to(int64_t new_version,
std::function<status(database&)> fn);

#pragma mark - STATUS:

/// The status of the last operation on this database or its queries/commands/blob_handles.
Expand Down
10 changes: 8 additions & 2 deletions include/sqnice/pool.hh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ namespace sqnice {
/// since it will be called multiple times.
void on_open(std::function<void(database&)>);

/// The number of databases open, both borrowed and available.
unsigned open_count() const;

/// The number of databases currently borrowed. Ranges from 0 up to `capacity`.
unsigned borrowed_count() const;

Expand Down Expand Up @@ -125,16 +128,19 @@ namespace sqnice {
private:
pool(pool&&) = delete;
pool& operator=(pool&&) = delete;
unsigned _borrowed_count() const;
unsigned _open_count() const {return _ro_total + _rw_total;}
borrowed_database borrow(bool);
borrowed_writeable_database borrow_writeable(bool);
std::unique_ptr<database> new_db(bool writeable);
void _close_unused();

using db_ptr = std::unique_ptr<const database>;

std::string const _dbname, _vfs; // Path & vfs to open
open_flags _flags; // Flags to open with
std::mutex _mutex; // Magic thread-safety voodoo
std::condition_variable _cond; // Magic thread-safety voodoo
std::mutex mutable _mutex; // Magic thread-safety voodoo
std::condition_variable mutable _cond; // Magic thread-safety voodoo
std::function<void(database&)> _initializer; // Init fn called on each new `database`
unsigned _ro_capacity =4;// Current capacity (of read-only dbs)
unsigned _ro_total = 0; // Number of read-only DBs I created
Expand Down
36 changes: 36 additions & 0 deletions src/database.cc
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,42 @@ namespace sqnice {
}


int64_t database::user_version() const {
return const_cast<database*>(this)->pragma("user_version");
}

status database::set_user_version(int64_t v) {
return pragma("user_version", v);
}

status database::migrate_from(int64_t old, int64_t nuu, function<status (database &)> fn) {
assert(old < nuu);
if (user_version() == old) {
if (auto rc = fn(*this); !ok(rc))
return rc;
set_user_version(nuu);
}
return status::ok;
}

status database::migrate_to(int64_t nuu, function<status (database &)> fn) {
if (user_version() < nuu) {
if (auto rc = fn(*this); !ok(rc))
return rc;
set_user_version(nuu);
}
return status::ok;
}

status database::migrate_from(int64_t old, int64_t nuu, string_view sql) {
return migrate_from(old, nuu, [sql](database& db) {return db.execute(sql);});
}

status database::migrate_to(int64_t nuu, string_view sql) {
return migrate_to(nuu, [sql](database& db) {return db.execute(sql);});
}


int64_t database::pragma(const char* pragma) {
return sqnice::query(*this, string("PRAGMA \"") + pragma + "\"").single_value_or<int>(0);
}
Expand Down
34 changes: 27 additions & 7 deletions src/pool.cc
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,39 @@ namespace sqnice {
}


unsigned pool::open_count() const {
unique_lock lock(_mutex);
return _ro_total + _rw_total;
}


unsigned pool::borrowed_count() const {
unique_lock lock(_mutex);
return _borrowed_count();
}


unsigned pool::_borrowed_count() const {
return unsigned(_ro_total - _readonly.size()) + (_rw_total - !!_readwrite);
}


void pool::close_all() {
unique_lock lock(_mutex);
_cond.wait(lock, [&] { return borrowed_count() == 0; });
_close_unused();
_cond.wait(lock, [&] { return _borrowed_count() == 0; });
_close_unused();
assert(_open_count() == 0);
}


void pool::close_unused() {
unique_lock lock(_mutex);
_close_unused();
}


void pool::_close_unused() {
_ro_total -= _readonly.size();
_readonly.clear();
if (_readwrite) {
Expand All @@ -96,11 +121,6 @@ namespace sqnice {
}


unsigned pool::borrowed_count() const {
return unsigned(_ro_total - _readonly.size()) + (_rw_total - !!_readwrite);
}


// Allocates a new database.
unique_ptr<database> pool::new_db(bool writeable) {
using enum open_flags;
Expand Down Expand Up @@ -170,7 +190,7 @@ namespace sqnice {
unique_lock lock(_mutex);
assert(!dbp->is_writeable());
assert(_readonly.size() < _ro_total);
if (_ro_total < _ro_capacity) {
if (_ro_total <= _ro_capacity) {
_readonly.emplace_back(dbp);
_cond.notify_all();
} else {
Expand Down
135 changes: 108 additions & 27 deletions test/testdb.cc
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ TEST_CASE_METHOD(sqnice_test, "SQNice callbacks", "[sqnice]") {
}

TEST_CASE("SQNice pool", "[sqnice]") {
sqnice::pool p("sqnice_test.sqlite3",
sqnice::open_flags::delete_first | sqnice::open_flags::readwrite);
static constexpr string_view kDBPath = "sqnice_test.sqlite3";
sqnice::pool pool(kDBPath, sqnice::open_flags::delete_first | sqnice::open_flags::readwrite);
{
auto db = p.borrow_writeable();
CHECK(p.borrowed_count() == 1);
auto db = pool.borrow_writeable();
CHECK(pool.borrowed_count() == 1);
db->execute(R"""(
CREATE TABLE contacts (
id INTEGER PRIMARY KEY,
Expand All @@ -169,33 +169,114 @@ TEST_CASE("SQNice pool", "[sqnice]") {
auto cmd = db->command("INSERT INTO contacts (name, phone) VALUES (?1, ?2)");
cmd.execute("Bob", "555-1212");

CHECK(p.try_borrow_writeable() == nullptr);
CHECK(pool.try_borrow_writeable() == nullptr);
}

CHECK(p.borrowed_count() == 0);
CHECK(pool.borrowed_count() == 0);
CHECK(pool.open_count() == 1);

auto db1 = p.borrow();
CHECK(p.borrowed_count() == 1);
string name = db1->query("SELECT name FROM contacts").single_value_or<string>("");
CHECK(name == "Bob");
{
auto db1 = pool.borrow();
CHECK(pool.borrowed_count() == 1);
CHECK(pool.open_count() == 2);
string name = db1->query("SELECT name FROM contacts").single_value_or<string>("");
CHECK(name == "Bob");

auto db2 = pool.borrow();
CHECK(pool.borrowed_count() == 2);
CHECK(pool.open_count() == 3);
auto db3 = pool.borrow();
CHECK(pool.borrowed_count() == 3);
CHECK(pool.open_count() == 4);
auto db4 = pool.borrow();
CHECK(pool.borrowed_count() == 4);
CHECK(pool.open_count() == 5);

CHECK(pool.try_borrow() == nullptr);
db1.reset();

CHECK(pool.borrowed_count() == 3);
CHECK(pool.open_count() == 5);

auto db5 = pool.borrow();
CHECK(pool.borrowed_count() == 4);
CHECK(pool.open_count() == 5);

{
sqnice::transaction txn(pool);
CHECK(pool.borrowed_count() == 5);
CHECK(pool.try_borrow_writeable() == nullptr);
}

auto db2 = p.borrow();
CHECK(p.borrowed_count() == 2);
auto db3 = p.borrow();
CHECK(p.borrowed_count() == 3);
auto db4 = p.borrow();
CHECK(p.borrowed_count() == 4);
CHECK(pool.borrowed_count() == 4);
CHECK(pool.open_count() == 5);
}

CHECK(p.try_borrow() == nullptr);
db1.reset();
CHECK(p.borrowed_count() == 3);
auto db5 = p.borrow();
CHECK(p.borrowed_count() == 4);
CHECK(pool.borrowed_count() == 0);
CHECK(pool.open_count() == 5);

{
sqnice::transaction txn(p);
CHECK(p.borrowed_count() == 5);
CHECK(p.try_borrow_writeable() == nullptr);
}
CHECK(p.borrowed_count() == 4);
pool.close_all();

CHECK(pool.borrowed_count() == 0);
CHECK(pool.open_count() == 0);

sqnice::database::delete_file(kDBPath);
}


TEST_CASE("SQNice schema migration", "[sqnice]") {
static constexpr string_view kDBPath = "sqnice_test.sqlite3";
sqnice::database::delete_file(kDBPath);
auto open_v1 = [] {
sqnice::database db(kDBPath);
db.setup();
sqnice::transaction txn(db);
db.migrate_from(0, 1, R"""(
CREATE TABLE contacts (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL,
address TEXT,
UNIQUE(name, phone)
);
)""");
txn.commit();
CHECK(db.user_version() == 1);
};

open_v1();

open_v1();

auto open_v2 = [] (int64_t expectedVersion) {
sqnice::database db(kDBPath);
sqnice::transaction txn(db);
CHECK(db.user_version() == expectedVersion);

// Migration for a newly created database, leaving it at version 2:
db.migrate_from(0, 2, R"""(
CREATE TABLE contacts (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT NOT NULL,
address TEXT,
age INTEGER,
UNIQUE(name, phone)
);
)""");

// Migration to upgrade version 1 (above) to version 2:
db.migrate_to(2, "ALTER TABLE contacts ADD COLUMN age INTEGER");
txn.commit();

CHECK(db.user_version() == 2);

db.execute("INSERT INTO contacts (name, phone, age) VALUES ('Alice', '555-1919', 39)");
};

open_v2(1);

sqnice::database::delete_file(kDBPath);

open_v2(0);
}