Browse Source

[1068] Merge commit '1351cb4' into trac1068review1 with fixing conflicts.
Also fixed an unrelated bug in getZone() which left an unfinished statement.

JINMEI Tatuya 13 years ago
parent
commit
1c834de994

+ 9 - 0
src/lib/datasrc/database.h

@@ -51,6 +51,15 @@ public:
     /// The number of fields the columns array passed to getNext should have
     static const size_t COLUMN_COUNT = 5;
 
+    /// TBD
+    /// Compliant database should support the following columns:
+    /// name, rname, ttl, rdtype, sigtype, rdata
+    /// (even though their internal representation may be different).
+    static const size_t ADD_COLUMN_COUNT = 6;
+
+    /// TBD
+    static const size_t DEL_PARAM_COUNT = 3;
+
     /**
      * \brief Destructor
      *

+ 237 - 146
src/lib/datasrc/sqlite3_accessor.cc

@@ -14,38 +14,109 @@
 
 #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>
 
-#include <boost/lexical_cast.hpp>
+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,
+    ITERATE = 8,
+    NUM_STATEMENTS = 9
+};
+
+const char* const text_statements[NUM_STATEMENTS] = {
+    // note for ANY and ITERATE: the order of the SELECT values is
+    // specifically chosen to match the enum values in RecordColumns
+    "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",
+    "SELECT rdtype, ttl, sigtype, rdata, name FROM records " // ITERATE
+    "WHERE zone_id = ?1 ORDER BY name, rdtype"
+};
+
 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,
@@ -72,35 +143,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_);
+        for (int i = 0; i < NUM_STATEMENTS; ++i) {
+            sqlite3_finalize(params_.statements_[i]);
         }
-        if (params_.q_count_ != NULL) {
-            sqlite3_finalize(params_.q_count_);
-        }
-        if (params_.q_previous_ != NULL) {
-            sqlite3_finalize(params_.q_previous_);
-        }
-        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_);
         }
@@ -136,49 +182,6 @@ const char* const SCHEMA_LIST[] = {
     NULL
 };
 
-const char* const q_zone_str = "SELECT id FROM zones WHERE name=?1 AND rdclass = ?2";
-
-// note that the order of the SELECT values is specifically chosen to match
-// the enum values in RecordColumns
-const char* const q_any_str = "SELECT rdtype, ttl, sigtype, rdata "
-    "FROM records WHERE zone_id=?1 AND name=?2";
-
-// note that the order of the SELECT values is specifically chosen to match
-// the enum values in RecordColumns
-const char* const q_iterate_str = "SELECT rdtype, ttl, sigtype, rdata, name FROM records "
-                                  "WHERE zone_id = ?1 "
-                                  "ORDER BY name, rdtype";
-
-/* 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;
@@ -213,17 +216,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]);
+    }
 }
 
 }
@@ -262,34 +257,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;
@@ -297,41 +268,43 @@ 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_);
-    std::pair<bool, int> result;
+    rc = sqlite3_step(stmt);
     if (rc == SQLITE_ROW) {
-        result = std::pair<bool, int>(true,
-                                      sqlite3_column_int(dbparameters_->
-                                                         q_zone_, 0));
-        return (result);
+        const int zone_id = sqlite3_column_int(stmt, 0);
+        sqlite3_reset(stmt);
+        return (pair<bool, int>(true, zone_id));
     } else if (rc == SQLITE_DONE) {
-        result = std::pair<bool, int>(false, 0);
         // Free resources
-        sqlite3_reset(dbparameters_->q_zone_);
-        return (result);
+        sqlite3_reset(stmt);
+        return (pair<bool, int>(false, 0));
     }
 
+    sqlite3_reset(stmt);
     isc_throw(DataSourceError, "Unexpected failure in sqlite3_step: " <<
-                               sqlite3_errmsg(dbparameters_->db_));
+              sqlite3_errmsg(dbparameters_->db_));
     // Compilers might not realize isc_throw always throws
     return (std::pair<bool, int>(false, 0));
 }
@@ -375,7 +348,8 @@ public:
         statement_(NULL)
     {
         // We create the statement now and then just keep getting data from it
-        statement_ = prepare(database->dbparameters_->db_, q_iterate_str);
+        statement_ = prepare(database->dbparameters_->db_,
+                             text_statements[ITERATE]);
         bindZoneId(id);
     }
 
@@ -388,7 +362,8 @@ public:
         statement_(NULL)
     {
         // We create the statement now and then just keep getting data from it
-        statement_ = prepare(database->dbparameters_->db_, q_any_str);
+        statement_ = prepare(database->dbparameters_->db_,
+                             text_statements[ANY]);
         bindZoneId(id);
         bindName(name);
     }
@@ -467,5 +442,121 @@ SQLite3Database::getAllRecords(int id) const {
     return (IteratorContextPtr(new Context(shared_from_this(), id)));
 }
 
+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");
+    }
+
+    StatementExecuter(*dbparameters_, ROLLBACK,
+                      "rollback 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");
+}
+
 }
 }

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

@@ -119,6 +119,37 @@ public:
      */
     virtual IteratorContextPtr getAllRecords(int id) const;
 
+    /// 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
+    ///
+    /// In SQLite3 rollback can fail if there's another unfinished statement
+    /// is performed for the same database structure.  Although it's not
+    /// expected to happen in our expected usage, it's not guaranteed to be
+    /// prevented at the API level.  If it ever happens, this method throws
+    /// an \c DataSourceError exception.  It should be considered a bug of
+    /// the higher level application program.
+    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
@@ -127,6 +158,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
     boost::scoped_ptr<SQLite3Parameters> dbparameters_;
     /// \brief The class for which the queries are done
@@ -144,4 +180,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)
 

+ 300 - 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;
@@ -273,4 +278,299 @@ 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;
+    DatabaseAccessor::IteratorContextPtr iterator;
+};
+
+void
+checkRecords(SQLite3Database& db, int zone_id, const std::string& name,
+             vector<const char* const*> expected_rows)
+{
+    DatabaseAccessor::IteratorContextPtr iterator =
+        db.getRecords(Name(name), zone_id);
+    std::string columns[DatabaseAccessor::COLUMN_COUNT];
+    vector<const char* const*>::const_iterator it = expected_rows.begin();
+    while (iterator->getNext(columns)) {
+        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
+    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();
+    checkRecords(*another_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, rollbackFailure) {
+    // This test emulates a rare scenario of making rollback attempt fail.
+    // The iterator is paused in the middle of getting records, which prevents
+    // the rollback operation at the end of the test.
+
+    string columns[DatabaseAccessor::COLUMN_COUNT];
+    iterator = db->getRecords(Name("example.com"), zone_id);
+    EXPECT_TRUE(iterator->getNext(columns));
+
+    db->startUpdateZone("example.com.", true);
+    EXPECT_THROW(db->rollbackUpdateZone(), DataSourceError);
+}
+
+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.
+    iterator = another_db->getRecords(Name("foo.example.com"), zone_id);
+    EXPECT_TRUE(iterator->getNext(get_columns));
+
+    // 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