reapack

Package manager for REAPER
Log | Files | Refs | Submodules | README | LICENSE

commit 22817b0d0706d3910210d2139c62abd9858d6b8d
parent e7eade369c716f8e51744c091ba4c1f4e8d648cb
Author: cfillion <cfillion@users.noreply.github.com>
Date:   Sun, 17 Jan 2016 16:14:48 -0500

enhance database support and usage

Diffstat:
Asrc/database.cpp | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/database.hpp | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/registry.cpp | 42++++++++++++++++++++++++------------------
Msrc/registry.hpp | 10++++------
Dsrc/sqlite.cpp | 118-------------------------------------------------------------------------------
Dsrc/sqlite.hpp | 72------------------------------------------------------------------------
Atest/database.cpp | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/sqlite.cpp | 27---------------------------
8 files changed, 320 insertions(+), 241 deletions(-)

diff --git a/src/database.cpp b/src/database.cpp @@ -0,0 +1,124 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2016 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "database.hpp" + +#include "errors.hpp" + +#include <sqlite3.h> + +using namespace std; + +Database::Database(const string &filename) +{ + const char *file = ":memory:"; + + if(!filename.empty()) + file = filename.c_str(); + + if(sqlite3_open(file, &m_db)) { + const auto &error = lastError(); + sqlite3_close(m_db); + + throw error; + } +} + +Database::~Database() +{ + for(Statement *stmt : m_statements) + delete stmt; + + sqlite3_close(m_db); +} + +Statement *Database::prepare(const char *sql) +{ + Statement *stmt = new Statement(sql, this); + m_statements.push_back(stmt); + + return stmt; +} + +void Database::exec(const char *sql) +{ + if(sqlite3_exec(m_db, sql, nullptr, nullptr, nullptr) != SQLITE_OK) + throw lastError(); +} + +reapack_error Database::lastError() const +{ + return reapack_error(sqlite3_errmsg(m_db)); +} + +Statement::Statement(const char *sql, Database *db) + : m_db(db) +{ + if(sqlite3_prepare_v2(db->m_db, sql, -1, &m_stmt, nullptr) != SQLITE_OK) + throw m_db->lastError(); +} + +Statement::~Statement() +{ + sqlite3_finalize(m_stmt); +} + +void Statement::bind(const int index, const string &text) +{ + if(sqlite3_bind_text(m_stmt, index, text.c_str(), -1, SQLITE_STATIC)) + throw m_db->lastError(); +} + +void Statement::bind(const int index, const uint64_t integer) +{ + if(sqlite3_bind_int64(m_stmt, index, (sqlite3_int64)integer)) + throw m_db->lastError(); +} + +void Statement::exec() +{ + exec([=] { return false; }); +} + +void Statement::exec(const ExecCallback &callback) +{ + while(true) { + switch(sqlite3_step(m_stmt)) { + case SQLITE_ROW: + if(callback()) + break; + case SQLITE_DONE: + sqlite3_clear_bindings(m_stmt); + sqlite3_reset(m_stmt); + return; + default: + sqlite3_clear_bindings(m_stmt); + sqlite3_reset(m_stmt); + throw m_db->lastError(); + }; + } +} + +uint64_t Statement::uint64Column(const int index) const +{ + return (uint64_t)sqlite3_column_int64(m_stmt, index); +} + +string Statement::stringColumn(const int index) const +{ + return (char *)sqlite3_column_text(m_stmt, index); +} diff --git a/src/database.hpp b/src/database.hpp @@ -0,0 +1,72 @@ +/* ReaPack: Package manager for REAPER + * Copyright (C) 2015-2016 Christian Fillion + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef REAPACK_DATABASE_HPP +#define REAPACK_DATABASE_HPP + +#include <cstdint> +#include <functional> +#include <string> +#include <vector> + +class reapack_error; + +struct sqlite3; +struct sqlite3_stmt; + +class Statement; + +class Database { +public: + Database(const std::string &filename = std::string()); + ~Database(); + + Statement *prepare(const char *sql); + void exec(const char *sql); + +private: + friend Statement; + + reapack_error lastError() const; + + sqlite3 *m_db; + std::vector<Statement *> m_statements; +}; + +class Statement { +public: + typedef std::function<bool (void)> ExecCallback; + + void bind(const int index, const std::string &text); + void bind(const int index, const uint64_t integer); + void exec(); + void exec(const ExecCallback &); + + uint64_t uint64Column(const int index) const; + std::string stringColumn(const int index) const; + +private: + friend Database; + + Statement(const char *sql, Database *db); + ~Statement(); + + Database *m_db; + sqlite3_stmt *m_stmt; +}; + +#endif diff --git a/src/registry.cpp b/src/registry.cpp @@ -18,6 +18,7 @@ #include "registry.hpp" #include "errors.hpp" +#include "index.hpp" #include "package.hpp" #include "path.hpp" @@ -28,23 +29,26 @@ using namespace std; Registry::Registry(const Path &path) : m_db(path.join()) { - m_db.query( + m_db.exec( "PRAGMA foreign_keys = ON;" "CREATE TABLE IF NOT EXISTS entries (" - " package TEXT NOT NULL UNIQUE," - " version INTEGER NOT NULL" + " remote TEXT NOT NULL," + " category TEXT NOT NULL," + " package TEXT NOT NULL," + " version INTEGER NOT NULL," + " UNIQUE(remote, category, package)" ");" ); m_insertEntry = m_db.prepare( "INSERT OR REPLACE INTO entries " - "VALUES(?, ?);" + "VALUES(?, ?, ?, ?);" ); m_findEntry = m_db.prepare( - "SELECT rowid, version FROM entries " - "WHERE package = ? " + "SELECT version FROM entries " + "WHERE remote = ? AND category = ? AND package = ? " "LIMIT 1" ); } @@ -52,12 +56,13 @@ Registry::Registry(const Path &path) void Registry::push(Version *ver) { Package *pkg = ver->package(); + Category *cat = pkg->category(); + RemoteIndex *ri = cat->index(); - if(!pkg) - return; - - m_insertEntry->bind(1, hashPackage(pkg).c_str()); - m_insertEntry->bind(2, ver->code()); + m_insertEntry->bind(1, ri->name()); + m_insertEntry->bind(2, cat->name()); + m_insertEntry->bind(3, pkg->name()); + m_insertEntry->bind(4, ver->code()); m_insertEntry->exec(); } @@ -66,9 +71,15 @@ Registry::QueryResult Registry::query(Package *pkg) const bool exists = false; uint64_t version = 0; - m_findEntry->bind(1, hashPackage(pkg).c_str()); + Category *cat = pkg->category(); + RemoteIndex *ri = cat->index(); + + m_findEntry->bind(1, ri->name()); + m_findEntry->bind(2, cat->name()); + m_findEntry->bind(3, pkg->name()); + m_findEntry->exec([&] { - version = m_findEntry->uint64Column(1); + version = m_findEntry->uint64Column(0); exists = true; return false; }); @@ -100,8 +111,3 @@ bool Registry::addToREAPER(Version *ver, const Path &root) return id > 0; } - -string Registry::hashPackage(Package *pkg) const -{ - return (pkg->targetPath() + pkg->name()).join('\30'); -} diff --git a/src/registry.hpp b/src/registry.hpp @@ -22,7 +22,7 @@ #include <string> #include "path.hpp" -#include "sqlite.hpp" +#include "database.hpp" class Package; class Path; @@ -50,11 +50,9 @@ public: QueryResult query(Package *) const; private: - std::string hashPackage(Package *) const; - - SQLite::Database m_db; - SQLite::Statement *m_insertEntry; - SQLite::Statement *m_findEntry; + Database m_db; + Statement *m_insertEntry; + Statement *m_findEntry; }; #endif diff --git a/src/sqlite.cpp b/src/sqlite.cpp @@ -1,118 +0,0 @@ -/* ReaPack: Package manager for REAPER - * Copyright (C) 2015-2016 Christian Fillion - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include "sqlite.hpp" - -#include "errors.hpp" - -#include <sqlite3.h> - -using namespace std; -using namespace SQLite; - -Database::Database(const string &filename) -{ - const char *file = ":memory:"; - - if(!filename.empty()) - file = filename.c_str(); - - if(sqlite3_open(file, &m_db)) { - const auto &error = lastError(); - sqlite3_close(m_db); - - throw error; - } -} - -Database::~Database() -{ - for(Statement *stmt : m_statements) - delete stmt; - - sqlite3_close(m_db); -} - -SQLite::Statement *Database::prepare(const char *sql) -{ - Statement *stmt = new Statement(sql, this); - m_statements.push_back(stmt); - - return stmt; -} - -void Database::query(const char *sql) -{ - if(sqlite3_exec(m_db, sql, nullptr, nullptr, nullptr) != SQLITE_OK) - throw lastError(); -} - -reapack_error Database::lastError() const -{ - return reapack_error(sqlite3_errmsg(m_db)); -} - -Statement::Statement(const char *sql, Database *db) - : m_db(db) -{ - if(sqlite3_prepare_v2(db->m_db, sql, -1, &m_stmt, nullptr) != SQLITE_OK) - throw m_db->lastError(); -} - -Statement::~Statement() -{ - sqlite3_finalize(m_stmt); -} - -void Statement::bind(const int index, const char *text) -{ - if(sqlite3_bind_text(m_stmt, index, text, -1, SQLITE_TRANSIENT)) - throw m_db->lastError(); -} - -void Statement::bind(const int index, const uint64_t integer) -{ - if(sqlite3_bind_int64(m_stmt, index, (sqlite3_int64)integer)) - throw m_db->lastError(); -} - -void Statement::exec() -{ - exec([=] { return false; }); -} - -void Statement::exec(const ExecCallback &callback) -{ - while(true) { - switch(sqlite3_step(m_stmt)) { - case SQLITE_ROW: - if(callback()) - break; - case SQLITE_DONE: - sqlite3_reset(m_stmt); - return; - default: - sqlite3_reset(m_stmt); - throw m_db->lastError(); - }; - } -} - -uint64_t Statement::uint64Column(const int index) const -{ - return (uint64_t)sqlite3_column_int64(m_stmt, index); -} diff --git a/src/sqlite.hpp b/src/sqlite.hpp @@ -1,72 +0,0 @@ -/* ReaPack: Package manager for REAPER - * Copyright (C) 2015-2016 Christian Fillion - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#ifndef REAPACK_SQLITE_HPP -#define REAPACK_SQLITE_HPP - -#include <functional> -#include <string> -#include <vector> - -class reapack_error; - -struct sqlite3; -struct sqlite3_stmt; - -namespace SQLite { - class Statement; - - class Database { - public: - Database(const std::string &filename = std::string()); - ~Database(); - - Statement *prepare(const char *sql); - void query(const char *sql); - - private: - friend Statement; - - reapack_error lastError() const; - - sqlite3 *m_db; - std::vector<Statement *> m_statements; - }; - - class Statement { - public: - typedef std::function<bool (void)> ExecCallback; - - void bind(const int index, const char *text); - void bind(const int index, const uint64_t integer); - void exec(); - void exec(const ExecCallback &); - - uint64_t uint64Column(const int index) const; - - private: - friend Database; - - Statement(const char *sql, Database *db); - ~Statement(); - - Database *m_db; - sqlite3_stmt *m_stmt; - }; -}; - -#endif diff --git a/test/database.cpp b/test/database.cpp @@ -0,0 +1,96 @@ +#include <catch.hpp> + +#include <database.hpp> + +#include <errors.hpp> + +using namespace std; + +static const char *M = "[database]"; + +TEST_CASE("open bad sqlite file path", M) { + try { + Database db("/a\\"); + FAIL(); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "unable to open database file"); + } +} + +TEST_CASE("execute invalid sql", M) { + Database db; + + try { + db.exec("WHERE"); + FAIL(); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "near \"WHERE\": syntax error"); + } +} + +TEST_CASE("prepare invalid sql", M) { + Database db; + + try { + db.prepare("WHERE"); + FAIL(); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "near \"WHERE\": syntax error"); + } +} + +TEST_CASE("get rows from prepared statement", M) { + Database db; + db.exec( + "CREATE TABLE test (value TEXT NOT NULL);" + "INSERT INTO test VALUES (\"hello\");" + "INSERT INTO test VALUES (\"世界\");" + ); + + vector<string> values; + + Statement *stmt = db.prepare("SELECT value FROM test"); + + SECTION("continue") { + stmt->exec([&] { + values.push_back(stmt->stringColumn(0)); + return true; + }); + + REQUIRE(values.size() == 2); + REQUIRE(values[0] == "hello"); + REQUIRE(values[1] == "世界"); + } + + SECTION("abort") { + stmt->exec([&] { + values.push_back(stmt->stringColumn(0)); + return false; + }); + + REQUIRE(values.size() == 1); + REQUIRE(values[0] == "hello"); + } +} + +TEST_CASE("bing values and clear", M) { + Database db; + db.exec("CREATE TABLE test (value TEXT NOT NULL)"); + + vector<string> values; + + Statement *stmt = db.prepare("INSERT INTO test VALUES (?)"); + stmt->bind(1, "hello"); + stmt->exec(); + + try { + stmt->exec(); + FAIL("bindings not cleared"); + } + catch(const reapack_error &e) { + REQUIRE(string(e.what()) == "NOT NULL constraint failed: test.value"); + } +} diff --git a/test/sqlite.cpp b/test/sqlite.cpp @@ -1,27 +0,0 @@ -#include <catch.hpp> - -#include <sqlite.hpp> - -#include <errors.hpp> - -using namespace std; - -static const char *M = "[sqlite]"; - -TEST_CASE("open bad sqlite file path", M) { - try { - SQLite::Database db("/a\\"); - FAIL(); - } - catch(const reapack_error &e) {} -} - -TEST_CASE("execute invalid sql", M) { - SQLite::Database db; - - try { - db.query("WHERE"); - FAIL(); - } - catch(const reapack_error &e) {} -}