Browse Source

[1068] update (write) support in the SQLite3 accessor.
It includes refactoring/cleanups on managing SQLite3 statements.

JINMEI Tatuya 13 years ago
parent
commit
1351cb42c9

+ 235 - 136
src/lib/datasrc/sqlite3_accessor.cc

@@ -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");
 }
 
 }

+ 34 - 1
src/lib/datasrc/sqlite3_accessor.h

@@ -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:

+ 5 - 1
src/lib/datasrc/tests/Makefile.am

@@ -1,8 +1,12 @@
+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)
 

+ 288 - 0
src/lib/datasrc/tests/sqlite3_accessor_unittest.cc

@@ -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());
+}
+
+TEST_F(SQLite3Update, emptyUpdate) {
+    // If we do nothing between start and commit, the zone content
+    // should be intact.
+
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+    db->commitUpdateZone();
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, flushZone) {
+    // With 'replace' being true startUpdateZone() will flush the existing
+    // zone content.
+
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+    zone_id = db->startUpdateZone("example.com.", true).second;
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+    db->commitUpdateZone();
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, readWhileUpdate) {
+    zone_id = db->startUpdateZone("example.com.", true).second;
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Until commit is done, the other accessor should see the old data
+    another_db->searchForRecords(zone_id, "foo.bar.example.com.");
+    checkRecords(*another_db, zone_id, "foo.bar.example.com.",
+                 expected_stored);
+
+    // Once the changes are committed, the other accessor will see the new
+    // data.
+    db->commitUpdateZone();
+    another_db->searchForRecords(zone_id, "foo.bar.example.com.");
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, rollback) {
+    zone_id = db->startUpdateZone("example.com.", true).second;
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Rollback will revert the change made by startUpdateZone(, true).
+    db->rollbackUpdateZone();
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, commitConflict) {
+    // Start reading the DB by another accessor.  We should stop at a single
+    // call to getNextRecord() to keep holding the lock.
+    another_db->searchForRecords(zone_id, "foo.example.com.");
+    EXPECT_TRUE(another_db->getNextRecord(get_columns,
+                                          DatabaseAccessor::COLUMN_COUNT));
+
+    // Due to getNextRecord() above, the other accessor holds a DB lock,
+    // which will prevent commit.
+    zone_id = db->startUpdateZone("example.com.", true).second;
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+    EXPECT_THROW(db->commitUpdateZone(), DataSourceError);
+    db->rollbackUpdateZone();   // rollback should still succeed
+
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, updateConflict) {
+    // Similar to the previous case, but this is a conflict with another
+    // update attempt.  Note that these two accessors modify disjoint sets
+    // of data; sqlite3 only has a coarse-grained lock so we cannot allow
+    // these updates to run concurrently.
+    EXPECT_TRUE(another_db->startUpdateZone("sql1.example.com.", true).first);
+    EXPECT_THROW(db->startUpdateZone("example.com.", true), DataSourceError);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, duplicateUpdate) {
+    db->startUpdateZone("example.com.", false);
+    EXPECT_THROW(db->startUpdateZone("example.com.", false), DataSourceError);
+}
+
+TEST_F(SQLite3Update, commitWithoutTransaction) {
+    EXPECT_THROW(db->commitUpdateZone(), DataSourceError);
+}
+
+TEST_F(SQLite3Update, rollbackWithoutTransaction) {
+    EXPECT_THROW(db->rollbackUpdateZone(), DataSourceError);
+}
+
+TEST_F(SQLite3Update, addRecord) {
+    // Before update, there should be no record for this name
+    checkRecords(*db, zone_id, "newdata.example.com.", empty_stored);
+
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    update_columns.assign(new_data,
+                          new_data + DatabaseAccessor::ADD_COLUMN_COUNT);
+    db->addRecordToZone(update_columns);
+
+    expected_stored.clear();
+    expected_stored.push_back(new_data);
+    checkRecords(*db, zone_id, "newdata.example.com.", expected_stored);
+
+    // Commit the change, and confirm the new data is still there.
+    db->commitUpdateZone();
+    checkRecords(*db, zone_id, "newdata.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, addThenRollback) {
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    update_columns.assign(new_data,
+                          new_data + DatabaseAccessor::ADD_COLUMN_COUNT);
+    db->addRecordToZone(update_columns);
+
+    expected_stored.clear();
+    expected_stored.push_back(new_data);
+    checkRecords(*db, zone_id, "newdata.example.com.", expected_stored);
+
+    db->rollbackUpdateZone();
+    checkRecords(*db, zone_id, "newdata.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, duplicateAdd) {
+    const char* const dup_data[] = {
+        "foo.bar.example.com.", "com.example.bar.foo.", "3600", "A", "",
+        "192.0.2.1"
+    };
+    expected_stored.clear();
+    expected_stored.push_back(dup_data);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+
+    // Adding exactly the same data.  As this backend is "dumb", another
+    // row of the same content will be inserted.
+    update_columns.assign(dup_data,
+                          dup_data + DatabaseAccessor::ADD_COLUMN_COUNT);
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    db->addRecordToZone(update_columns);
+    expected_stored.push_back(dup_data);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, invalidAdd) {
+    // An attempt of add before an explicit start of transaction
+    EXPECT_THROW(db->addRecordToZone(update_columns), DataSourceError);
+
+    // Short column vector
+    update_columns.clear();
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    EXPECT_THROW(db->addRecordToZone(update_columns), DataSourceError);
+
+    // Too many columns
+    for (int i = 0; i < DatabaseAccessor::ADD_COLUMN_COUNT + 1; ++i) {
+        update_columns.push_back("");
+    }
+    EXPECT_THROW(db->addRecordToZone(update_columns), DataSourceError);
+}
+
+TEST_F(SQLite3Update, deleteRecord) {
+    zone_id = db->startUpdateZone("example.com.", false).second;
+
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+
+    update_columns.assign(deleted_data, deleted_data +
+                          DatabaseAccessor::DEL_PARAM_COUNT);
+    db->deleteRecordInZone(update_columns);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Commit the change, and confirm the deleted data still isn't there.
+    db->commitUpdateZone();
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, deleteThenRollback) {
+    zone_id = db->startUpdateZone("example.com.", false).second;
+
+    update_columns.assign(deleted_data, deleted_data +
+                          DatabaseAccessor::DEL_PARAM_COUNT);
+    db->deleteRecordInZone(update_columns);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Rollback the change, and confirm the data still exists.
+    db->rollbackUpdateZone();
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, deleteNonexistent) {
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    update_columns.assign(deleted_data, deleted_data +
+                          DatabaseAccessor::DEL_PARAM_COUNT);
+
+    // Replace the name with a non existent one, then try to delete it.
+    // nothing should happen.
+    update_columns[0] = "no-such-name.example.com.";
+    checkRecords(*db, zone_id, "no-such-name.example.com.", empty_stored);
+    db->deleteRecordInZone(update_columns);
+    checkRecords(*db, zone_id, "no-such-name.example.com.", empty_stored);
+
+    // Name exists but the RR type is different.  Delete attempt shouldn't
+    // delete only by name.
+    update_columns.assign(deleted_data, deleted_data +
+                          DatabaseAccessor::DEL_PARAM_COUNT);
+    update_columns[1] = "AAAA";
+    db->deleteRecordInZone(update_columns);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+
+    // Similar to the previous case, but RDATA is different.
+    update_columns.assign(deleted_data, deleted_data +
+                          DatabaseAccessor::DEL_PARAM_COUNT);
+    update_columns[2] = "192.0.2.2";
+    db->deleteRecordInZone(update_columns);
+    checkRecords(*db, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, invalidDelete) {
+    // An attempt of delete before an explicit start of transaction
+    EXPECT_THROW(db->deleteRecordInZone(update_columns), DataSourceError);
+
+    // Short column vector
+    update_columns.clear();
+    zone_id = db->startUpdateZone("example.com.", false).second;
+    EXPECT_THROW(db->deleteRecordInZone(update_columns), DataSourceError);
+
+    // Too many parameters
+    for (int i = 0; i < DatabaseAccessor::DEL_PARAM_COUNT + 1; ++i) {
+        update_columns.push_back("");
+    }
+    EXPECT_THROW(db->deleteRecordInZone(update_columns), DataSourceError);
+}
 } // end anonymous namespace