Commit 1351cb42 authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[1068] update (write) support in the SQLite3 accessor.

It includes refactoring/cleanups on managing SQLite3 statements.
parent 575bd3ec
......@@ -14,36 +14,104 @@
#include <sqlite3.h>
#include <string>
#include <vector>
#include <boost/foreach.hpp>
#include <datasrc/sqlite3_accessor.h>
#include <datasrc/logger.h>
#include <datasrc/data_source.h>
#include <util/filename.h>
using namespace std;
namespace isc {
namespace datasrc {
// The following enum and char* array define the SQL statements commonly
// used in this implementation. Corresponding prepared statements (of
// type sqlite3_stmt*) are maintained in the statements_ array of the
// SQLite3Parameters structure.
enum StatementID {
ZONE = 0,
ANY = 1,
BEGIN = 2,
COMMIT = 3,
ROLLBACK = 4,
DEL_ZONE_RECORDS = 5,
ADD_RECORD = 6,
DEL_RECORD = 7,
NUM_STATEMENTS = 8
};
const char* const text_statements[NUM_STATEMENTS] = {
"SELECT id FROM zones WHERE name=?1 AND rdclass = ?2", // ZONE
"SELECT rdtype, ttl, sigtype, rdata FROM records " // ANY
"WHERE zone_id=?1 AND name=?2",
"BEGIN", // BEGIN
"COMMIT", // COMMIT
"ROLLBACK", // ROLLBACK
"DELETE FROM records WHERE zone_id=?1", // DEL_ZONE_RECORDS
"INSERT INTO records " // ADD_RECORD
"(zone_id, name, rname, ttl, rdtype, sigtype, rdata) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
"DELETE FROM records WHERE zone_id=?1 AND name=?2 " // DEL_RECORD
"AND rdtype=?3 AND rdata=?4"
};
struct SQLite3Parameters {
SQLite3Parameters() :
db_(NULL), version_(-1),
q_zone_(NULL), q_any_(NULL)
/*q_record_(NULL), q_addrs_(NULL), q_referral_(NULL),
q_count_(NULL), q_previous_(NULL), q_nsec3_(NULL),
q_prevnsec3_(NULL) */
{}
db_(NULL), version_(-1), updating_zone(false), updated_zone_id(-1)
{
for (int i = 0; i < NUM_STATEMENTS; ++i) {
statements_[i] = NULL;
}
}
sqlite3* db_;
int version_;
sqlite3_stmt* q_zone_;
sqlite3_stmt* q_any_;
/*
TODO: Yet unneeded statements
sqlite3_stmt* q_record_;
sqlite3_stmt* q_addrs_;
sqlite3_stmt* q_referral_;
sqlite3_stmt* q_count_;
sqlite3_stmt* q_previous_;
sqlite3_stmt* q_nsec3_;
sqlite3_stmt* q_prevnsec3_;
*/
sqlite3_stmt* statements_[NUM_STATEMENTS];
bool updating_zone; // whether or not updating the zone
int updated_zone_id; // valid only when updating_zone is true
};
// This is a helper class to encapsulate the code logic of executing
// a specific SQLite3 statement, ensuring the corresponding prepared
// statement is always reset whether the execution is completed successfully
// or it results in an exception.
// Note that an object of this class is intended to be used for "ephemeral"
// statement, which is completed with a single "step" (normally within a
// single call to an SQLite3Database method). In particular, it cannot be
// used for "SELECT" variants, which generally expect multiple matching rows.
class StatementExecuter {
public:
// desc will be used on failure in the what() message of the resulting
// DataSourceError exception.
StatementExecuter(SQLite3Parameters* dbparameters, StatementID stmt_id,
const char* desc) :
dbparameters_(dbparameters), stmt_id_(stmt_id), desc_(desc)
{
sqlite3_clear_bindings(dbparameters_->statements_[stmt_id_]);
}
~StatementExecuter() {
sqlite3_reset(dbparameters_->statements_[stmt_id_]);
}
void exec() {
if (sqlite3_step(dbparameters_->statements_[stmt_id_]) !=
SQLITE_DONE) {
sqlite3_reset(dbparameters_->statements_[stmt_id_]);
isc_throw(DataSourceError, "failed to " << desc_ << ": " <<
sqlite3_errmsg(dbparameters_->db_));
}
}
private:
SQLite3Parameters* dbparameters_;
const StatementID stmt_id_;
const char* const desc_;
};
SQLite3Database::SQLite3Database(const std::string& filename,
......@@ -70,35 +138,10 @@ namespace {
class Initializer {
public:
~Initializer() {
if (params_.q_zone_ != NULL) {
sqlite3_finalize(params_.q_zone_);
}
if (params_.q_any_ != NULL) {
sqlite3_finalize(params_.q_any_);
}
/*
if (params_.q_record_ != NULL) {
sqlite3_finalize(params_.q_record_);
}
if (params_.q_addrs_ != NULL) {
sqlite3_finalize(params_.q_addrs_);
}
if (params_.q_referral_ != NULL) {
sqlite3_finalize(params_.q_referral_);
}
if (params_.q_count_ != NULL) {
sqlite3_finalize(params_.q_count_);
}
if (params_.q_previous_ != NULL) {
sqlite3_finalize(params_.q_previous_);
for (int i = 0; i < NUM_STATEMENTS; ++i) {
sqlite3_finalize(params_.statements_[i]);
}
if (params_.q_nsec3_ != NULL) {
sqlite3_finalize(params_.q_nsec3_);
}
if (params_.q_prevnsec3_ != NULL) {
sqlite3_finalize(params_.q_prevnsec3_);
}
*/
if (params_.db_ != NULL) {
sqlite3_close(params_.db_);
}
......@@ -134,41 +177,6 @@ const char* const SCHEMA_LIST[] = {
NULL
};
const char* const q_zone_str = "SELECT id FROM zones WHERE name=?1 AND rdclass = ?2";
const char* const q_any_str = "SELECT rdtype, ttl, sigtype, rdata "
"FROM records WHERE zone_id=?1 AND name=?2";
/* TODO: Prune the statements, not everything will be needed maybe?
const char* const q_record_str = "SELECT rdtype, ttl, sigtype, rdata "
"FROM records WHERE zone_id=?1 AND name=?2 AND "
"((rdtype=?3 OR sigtype=?3) OR "
"(rdtype='CNAME' OR sigtype='CNAME') OR "
"(rdtype='NS' OR sigtype='NS'))";
const char* const q_addrs_str = "SELECT rdtype, ttl, sigtype, rdata "
"FROM records WHERE zone_id=?1 AND name=?2 AND "
"(rdtype='A' OR sigtype='A' OR rdtype='AAAA' OR sigtype='AAAA')";
const char* const q_referral_str = "SELECT rdtype, ttl, sigtype, rdata FROM "
"records WHERE zone_id=?1 AND name=?2 AND"
"(rdtype='NS' OR sigtype='NS' OR rdtype='DS' OR sigtype='DS' OR "
"rdtype='DNAME' OR sigtype='DNAME')";
const char* const q_count_str = "SELECT COUNT(*) FROM records "
"WHERE zone_id=?1 AND rname LIKE (?2 || '%');";
const char* const q_previous_str = "SELECT name FROM records "
"WHERE zone_id=?1 AND rdtype = 'NSEC' AND "
"rname < $2 ORDER BY rname DESC LIMIT 1";
const char* const q_nsec3_str = "SELECT rdtype, ttl, rdata FROM nsec3 "
"WHERE zone_id = ?1 AND hash = $2";
const char* const q_prevnsec3_str = "SELECT hash FROM nsec3 "
"WHERE zone_id = ?1 AND hash <= $2 ORDER BY hash DESC LIMIT 1";
*/
sqlite3_stmt*
prepare(sqlite3* const db, const char* const statement) {
sqlite3_stmt* prepared = NULL;
......@@ -203,17 +211,9 @@ checkAndSetupSchema(Initializer* initializer) {
}
}
initializer->params_.q_zone_ = prepare(db, q_zone_str);
initializer->params_.q_any_ = prepare(db, q_any_str);
/* TODO: Yet unneeded statements
initializer->params_.q_record_ = prepare(db, q_record_str);
initializer->params_.q_addrs_ = prepare(db, q_addrs_str);
initializer->params_.q_referral_ = prepare(db, q_referral_str);
initializer->params_.q_count_ = prepare(db, q_count_str);
initializer->params_.q_previous_ = prepare(db, q_previous_str);
initializer->params_.q_nsec3_ = prepare(db, q_nsec3_str);
initializer->params_.q_prevnsec3_ = prepare(db, q_prevnsec3_str);
*/
for (int i = 0; i < NUM_STATEMENTS; ++i) {
initializer->params_.statements_[i] = prepare(db, text_statements[i]);
}
}
}
......@@ -253,34 +253,10 @@ SQLite3Database::close(void) {
}
// XXX: sqlite3_finalize() could fail. What should we do in that case?
sqlite3_finalize(dbparameters_->q_zone_);
dbparameters_->q_zone_ = NULL;
sqlite3_finalize(dbparameters_->q_any_);
dbparameters_->q_any_ = NULL;
/* TODO: Once they are needed or not, uncomment or drop
sqlite3_finalize(dbparameters->q_record_);
dbparameters->q_record_ = NULL;
sqlite3_finalize(dbparameters->q_addrs_);
dbparameters->q_addrs_ = NULL;
sqlite3_finalize(dbparameters->q_referral_);
dbparameters->q_referral_ = NULL;
sqlite3_finalize(dbparameters->q_count_);
dbparameters->q_count_ = NULL;
sqlite3_finalize(dbparameters->q_previous_);
dbparameters->q_previous_ = NULL;
sqlite3_finalize(dbparameters->q_prevnsec3_);
dbparameters->q_prevnsec3_ = NULL;
sqlite3_finalize(dbparameters->q_nsec3_);
dbparameters->q_nsec3_ = NULL;
*/
for (int i = 0; i < NUM_STATEMENTS; ++i) {
sqlite3_finalize(dbparameters_->statements_[i]);
dbparameters_->statements_[i] = NULL;
}
sqlite3_close(dbparameters_->db_);
dbparameters_->db_ = NULL;
......@@ -288,36 +264,38 @@ SQLite3Database::close(void) {
std::pair<bool, int>
SQLite3Database::getZone(const isc::dns::Name& name) const {
return (getZone(name.toText()));
}
std::pair<bool, int>
SQLite3Database::getZone(const string& name) const {
int rc;
sqlite3_stmt* const stmt = dbparameters_->statements_[ZONE];
// Take the statement (simple SELECT id FROM zones WHERE...)
// and prepare it (bind the parameters to it)
sqlite3_reset(dbparameters_->q_zone_);
rc = sqlite3_bind_text(dbparameters_->q_zone_, 1, name.toText().c_str(),
-1, SQLITE_TRANSIENT);
sqlite3_reset(stmt);
rc = sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_STATIC);
if (rc != SQLITE_OK) {
isc_throw(SQLite3Error, "Could not bind " << name <<
" to SQL statement (zone)");
}
rc = sqlite3_bind_text(dbparameters_->q_zone_, 2, class_.c_str(), -1,
SQLITE_STATIC);
rc = sqlite3_bind_text(stmt, 2, class_.c_str(), -1, SQLITE_STATIC);
if (rc != SQLITE_OK) {
isc_throw(SQLite3Error, "Could not bind " << class_ <<
" to SQL statement (zone)");
}
// Get the data there and see if it found anything
rc = sqlite3_step(dbparameters_->q_zone_);
rc = sqlite3_step(stmt);
std::pair<bool, int> result;
if (rc == SQLITE_ROW) {
result = std::pair<bool, int>(true,
sqlite3_column_int(dbparameters_->
q_zone_, 0));
result = std::pair<bool, int>(true, sqlite3_column_int(stmt, 0));
} else {
result = std::pair<bool, int>(false, 0);
}
// Free resources
sqlite3_reset(dbparameters_->q_zone_);
sqlite3_reset(stmt);
return (result);
}
......@@ -325,14 +303,16 @@ SQLite3Database::getZone(const isc::dns::Name& name) const {
void
SQLite3Database::searchForRecords(int zone_id, const std::string& name) {
resetSearch();
if (sqlite3_bind_int(dbparameters_->q_any_, 1, zone_id) != SQLITE_OK) {
sqlite3_stmt* const stmt = dbparameters_->statements_[ANY];
if (sqlite3_bind_int(stmt, 1, zone_id) != SQLITE_OK) {
isc_throw(DataSourceError,
"Error in sqlite3_bind_int() for zone_id " <<
zone_id << ": " << sqlite3_errmsg(dbparameters_->db_));
}
// use transient since name is a ref and may disappear
if (sqlite3_bind_text(dbparameters_->q_any_, 2, name.c_str(), -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
if (sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT) !=
SQLITE_OK) {
isc_throw(DataSourceError,
"Error in sqlite3_bind_text() for name " <<
name << ": " << sqlite3_errmsg(dbparameters_->db_));
......@@ -376,7 +356,7 @@ SQLite3Database::getNextRecord(std::string columns[], size_t column_count) {
"of size " << COLUMN_COUNT << " to getNextRecord()");
}
sqlite3_stmt* current_stmt = dbparameters_->q_any_;
sqlite3_stmt* current_stmt = dbparameters_->statements_[ANY];
const int rc = sqlite3_step(current_stmt);
if (rc == SQLITE_ROW) {
......@@ -404,8 +384,127 @@ SQLite3Database::getNextRecord(std::string columns[], size_t column_count) {
void
SQLite3Database::resetSearch() {
sqlite3_reset(dbparameters_->q_any_);
sqlite3_clear_bindings(dbparameters_->q_any_);
sqlite3_reset(dbparameters_->statements_[ANY]);
sqlite3_clear_bindings(dbparameters_->statements_[ANY]);
}
pair<bool, int>
SQLite3Database::startUpdateZone(const string& zone_name, const bool replace) {
if (dbparameters_->updating_zone) {
isc_throw(DataSourceError,
"duplicate zone update on SQLite3 data source");
}
const pair<bool, int> zone_info(getZone(zone_name));
if (!zone_info.first) {
return (zone_info);
}
dbparameters_->updating_zone = true;
dbparameters_->updated_zone_id = zone_info.second;
StatementExecuter(dbparameters_, BEGIN,
"start an SQLite3 transaction").exec();
if (replace) {
StatementExecuter delzone_exec(dbparameters_, DEL_ZONE_RECORDS,
"delete zone records");
sqlite3_clear_bindings(dbparameters_->statements_[DEL_ZONE_RECORDS]);
if (sqlite3_bind_int(dbparameters_->statements_[DEL_ZONE_RECORDS],
1, zone_info.second) != SQLITE_OK) {
isc_throw(DataSourceError,
"failed to bind SQLite3 parameter: " <<
sqlite3_errmsg(dbparameters_->db_));
}
delzone_exec.exec();
}
return (zone_info);
}
void
SQLite3Database::commitUpdateZone() {
if (!dbparameters_->updating_zone) {
isc_throw(DataSourceError, "committing zone update on SQLite3 "
"data source without transaction");
}
StatementExecuter(dbparameters_, COMMIT,
"commit an SQLite3 transaction").exec();
dbparameters_->updating_zone = false;
dbparameters_->updated_zone_id = -1;
}
void
SQLite3Database::rollbackUpdateZone() {
if (!dbparameters_->updating_zone) {
isc_throw(DataSourceError, "rolling back zone update on SQLite3 "
"data source without transaction");
}
// We expect that ROLLBACK always succeeds. But could that fail?
// If so, what should we do? Is it okay to simply propagate the
// DataSourceError exception?
StatementExecuter(dbparameters_, ROLLBACK,
"commit an SQLite3 transaction").exec();
dbparameters_->updating_zone = false;
dbparameters_->updated_zone_id = -1;
}
namespace {
// Commonly used code sequence for adding/deleting record
void
doUpdate(SQLite3Parameters* dbparams, StatementID stmt_id,
const vector<string>& update_params, const char* exec_desc)
{
sqlite3_stmt* const stmt = dbparams->statements_[stmt_id];
StatementExecuter executer(dbparams, stmt_id, exec_desc);
int param_id = 0;
if (sqlite3_bind_int(stmt, ++param_id, dbparams->updated_zone_id)
!= SQLITE_OK) {
isc_throw(DataSourceError, "failed to bind SQLite3 parameter: " <<
sqlite3_errmsg(dbparams->db_));
}
BOOST_FOREACH(const string& column, update_params) {
if (sqlite3_bind_text(stmt, ++param_id, column.c_str(), -1,
SQLITE_TRANSIENT) != SQLITE_OK) {
isc_throw(DataSourceError, "failed to bind SQLite3 parameter: " <<
sqlite3_errmsg(dbparams->db_));
}
}
executer.exec();
}
}
void
SQLite3Database::addRecordToZone(const vector<string>& columns) {
if (!dbparameters_->updating_zone) {
isc_throw(DataSourceError, "adding record to SQLite3 "
"data source without transaction");
}
if (columns.size() != ADD_COLUMN_COUNT) {
isc_throw(DataSourceError, "adding incompatible number of columns "
"to SQLite3 data source: " << columns.size());
}
doUpdate(dbparameters_, ADD_RECORD, columns, "add record to zone");
}
void
SQLite3Database::deleteRecordInZone(const vector<string>& params) {
if (!dbparameters_->updating_zone) {
isc_throw(DataSourceError, "deleting record in SQLite3 "
"data source without transaction");
}
if (params.size() != DEL_PARAM_COUNT) {
isc_throw(DataSourceError, "incompatible # of parameters for "
"deleting in SQLite3 data source: " << params.size());
}
doUpdate(dbparameters_, DEL_RECORD, params, "delete record from zone");
}
}
......
......@@ -135,6 +135,30 @@ public:
*/
virtual void resetSearch();
/// TBD
/// This cannot be nested.
virtual std::pair<bool, int> startUpdateZone(const std::string& zone_name,
bool replace);
/// TBD
/// Note: we are quite impatient here: it's quite possible that the COMMIT
/// fails due to other process performing SELECT on the same database
/// (consider the case where COMMIT is done by xfrin or dynamic update
/// server while an authoritative server is busy reading the DB).
/// In a future version we should probably need to introduce some retry
/// attempt and/or increase timeout before giving up the COMMIT, even
/// if it still doesn't guarantee 100% success.
virtual void commitUpdateZone();
/// TBD
virtual void rollbackUpdateZone();
/// TBD
virtual void addRecordToZone(const std::vector<std::string>& columns);
/// TBD
virtual void deleteRecordInZone(const std::vector<std::string>& params);
/// The SQLite3 implementation of this method returns a string starting
/// with a fixed prefix of "sqlite3_" followed by the DB file name
/// removing any path name. For example, for the DB file
......@@ -143,6 +167,11 @@ public:
virtual const std::string& getDBName() const { return (database_name_); }
private:
// same as the public version except it takes name as a string
// (actually this is the intended interface. this should replace the
// current public version).
std::pair<bool, int> getZone(const std::string& name) const;
/// \brief Private database data
SQLite3Parameters* dbparameters_;
/// \brief The class for which the queries are done
......@@ -157,4 +186,8 @@ private:
}
}
#endif
#endif // __DATASRC_SQLITE3_CONNECTION_H
// Local Variables:
// mode: c++
// End:
SUBDIRS = . testdata
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += -I$(top_builddir)/src/lib/dns -I$(top_srcdir)/src/lib/dns
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CPPFLAGS += $(SQLITE_CFLAGS)
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(srcdir)/testdata\"
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_srcdir)/testdata\"
AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_builddir)/testdata\"
AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
AM_CXXFLAGS = $(B10_CXXFLAGS)
......
......@@ -11,6 +11,9 @@
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <vector>
#include <datasrc/sqlite3_accessor.h>
#include <datasrc/data_source.h>
......@@ -20,7 +23,9 @@
#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
using namespace std;
using namespace isc::datasrc;
using boost::shared_ptr;
using isc::data::ConstElementPtr;
using isc::data::Element;
using isc::dns::RRClass;
......@@ -242,4 +247,287 @@ TEST_F(SQLite3Access, getRecords) {
"33495 example.com. FAKEFAKEFAKEFAKE");
}
//
// Commonly used data for update tests
//
const char* const common_expected_data[] = {
// Test record already stored in the tested sqlite3 DB file.
"foo.bar.example.com.", "com.example.bar.foo.", "3600", "A", "",
"192.0.2.1"
};
const char* const new_data[] = {
// Newly added data commonly used by some of the tests below
"newdata.example.com.", "com.example.newdata.", "3600", "A", "",
"192.0.2.1"
};
const char* const deleted_data[] = {
// Existing data to be removed commonly used by some of the tests below
"foo.bar.example.com.", "A", "192.0.2.1"
};
class SQLite3Update : public SQLite3Access {
protected:
SQLite3Update() {
ASSERT_EQ(0, system(INSTALL_PROG " " TEST_DATA_DIR
"/test.sqlite3 "
TEST_DATA_BUILDDIR "/test.sqlite3.copied"));
initAccessor(TEST_DATA_BUILDDIR "/test.sqlite3.copied", RRClass::IN());
zone_id = db->getZone(Name("example.com")).second;
another_db.reset(new SQLite3Database(
TEST_DATA_BUILDDIR "/test.sqlite3.copied",
RRClass::IN()));
expected_stored.push_back(common_expected_data);
}
int zone_id;
std::string get_columns[DatabaseAccessor::COLUMN_COUNT];
std::vector<std::string> update_columns;
vector<const char* const*> expected_stored; // placeholder for checkRecords
vector<const char* const*> empty_stored; // indicate no corresponding data
// Another accessor, emulating one running on a different process/thread
shared_ptr<SQLite3Database> another_db;
};
void
checkRecords(SQLite3Database& db, int zone_id, const std::string& name,
vector<const char* const*> expected_rows)
{
db.searchForRecords(zone_id, name);
std::string columns[DatabaseAccessor::COLUMN_COUNT];
vector<const char* const*>::const_iterator it = expected_rows.begin();
while (db.getNextRecord(columns, DatabaseAccessor::COLUMN_COUNT)) {
ASSERT_TRUE(it != expected_rows.end());
checkRecordRow(columns, (*it)[3], (*it)[2], (*it)[4], (*it)[5]);
++it;
}
EXPECT_TRUE(it == expected_rows.end());