Parcourir la source

[master] Merge branch 'trac1068review1'
with fixing conflicts:
src/lib/datasrc/database.cc
src/lib/datasrc/database.h
src/lib/datasrc/sqlite3_accessor.cc
src/lib/datasrc/tests/Makefile.am
src/lib/datasrc/tests/sqlite3_accessor_unittest.cc

JINMEI Tatuya il y a 13 ans
Parent
commit
e2ada81cd2

+ 1 - 0
configure.ac

@@ -856,6 +856,7 @@ AC_CONFIG_FILES([Makefile
                  src/lib/exceptions/tests/Makefile
                  src/lib/datasrc/Makefile
                  src/lib/datasrc/tests/Makefile
+                 src/lib/datasrc/tests/testdata/Makefile
                  src/lib/xfr/Makefile
                  src/lib/log/Makefile
                  src/lib/log/compiler/Makefile

+ 30 - 31
src/lib/datasrc/database.cc

@@ -38,10 +38,10 @@ namespace isc {
 namespace datasrc {
 
 DatabaseClient::DatabaseClient(boost::shared_ptr<DatabaseAccessor>
-                               database) :
-    database_(database)
+                               accessor) :
+    accessor_(accessor)
 {
-    if (database_.get() == NULL) {
+    if (!accessor_) {
         isc_throw(isc::InvalidParameter,
                   "No database provided to DatabaseClient");
     }
@@ -49,21 +49,21 @@ DatabaseClient::DatabaseClient(boost::shared_ptr<DatabaseAccessor>
 
 DataSourceClient::FindResult
 DatabaseClient::findZone(const Name& name) const {
-    std::pair<bool, int> zone(database_->getZone(name.toText()));
+    std::pair<bool, int> zone(accessor_->getZone(name.toText()));
     // Try exact first
     if (zone.first) {
         return (FindResult(result::SUCCESS,
-                           ZoneFinderPtr(new Finder(database_,
+                           ZoneFinderPtr(new Finder(accessor_,
                                                     zone.second, name))));
     }
     // Then super domains
     // Start from 1, as 0 is covered above
     for (size_t i(1); i < name.getLabelCount(); ++i) {
         isc::dns::Name superdomain(name.split(i));
-        zone = database_->getZone(superdomain.toText());
+        zone = accessor_->getZone(superdomain.toText());
         if (zone.first) {
             return (FindResult(result::PARTIALMATCH,
-                               ZoneFinderPtr(new Finder(database_,
+                               ZoneFinderPtr(new Finder(accessor_,
                                                         zone.second,
                                                         superdomain))));
         }
@@ -72,10 +72,9 @@ DatabaseClient::findZone(const Name& name) const {
     return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
 }
 
-DatabaseClient::Finder::Finder(boost::shared_ptr<DatabaseAccessor>
-                               database, int zone_id,
-                               const isc::dns::Name& origin) :
-    database_(database),
+DatabaseClient::Finder::Finder(boost::shared_ptr<DatabaseAccessor> accessor,
+                               int zone_id, const isc::dns::Name& origin) :
+    accessor_(accessor),
     zone_id_(zone_id),
     origin_(origin)
 { }
@@ -184,7 +183,7 @@ DatabaseClient::Finder::getRRset(const isc::dns::Name& name,
 
     // Request the context
     DatabaseAccessor::IteratorContextPtr
-        context(database_->getRecords(name.toText(), zone_id_));
+        context(accessor_->getRecords(name.toText(), zone_id_));
     // It must not return NULL, that's a bug of the implementation
     if (!context) {
         isc_throw(isc::Unexpected, "Iterator context null at " +
@@ -232,7 +231,7 @@ DatabaseClient::Finder::getRRset(const isc::dns::Name& name,
                 addOrCreate(result_rrset, *construct_name, getClass(),
                             cur_type, cur_ttl,
                             columns[DatabaseAccessor::RDATA_COLUMN],
-                            *database_);
+                            *accessor_);
             } else if (type != NULL && cur_type == *type) {
                 if (result_rrset &&
                     result_rrset->getType() == isc::dns::RRType::CNAME()) {
@@ -246,7 +245,7 @@ DatabaseClient::Finder::getRRset(const isc::dns::Name& name,
                 addOrCreate(result_rrset, *construct_name, getClass(),
                             cur_type, cur_ttl,
                             columns[DatabaseAccessor::RDATA_COLUMN],
-                            *database_);
+                            *accessor_);
             } else if (want_cname && cur_type == isc::dns::RRType::CNAME()) {
                 // There should be no other data, so result_rrset should
                 // be empty.
@@ -257,7 +256,7 @@ DatabaseClient::Finder::getRRset(const isc::dns::Name& name,
                 addOrCreate(result_rrset, *construct_name, getClass(),
                             cur_type, cur_ttl,
                             columns[DatabaseAccessor::RDATA_COLUMN],
-                            *database_);
+                            *accessor_);
             } else if (want_dname && cur_type == isc::dns::RRType::DNAME()) {
                 // There should be max one RR of DNAME present
                 if (result_rrset &&
@@ -268,7 +267,7 @@ DatabaseClient::Finder::getRRset(const isc::dns::Name& name,
                 addOrCreate(result_rrset, *construct_name, getClass(),
                             cur_type, cur_ttl,
                             columns[DatabaseAccessor::RDATA_COLUMN],
-                            *database_);
+                            *accessor_);
             } else if (cur_type == isc::dns::RRType::RRSIG()) {
                 // If we get signatures before we get the actual data, we
                 // can't know which ones to keep and which to drop...
@@ -304,7 +303,7 @@ bool
 DatabaseClient::Finder::hasSubdomains(const std::string& name) {
     // Request the context
     DatabaseAccessor::IteratorContextPtr
-        context(database_->getRecords(name, zone_id_, true));
+        context(accessor_->getRecords(name, zone_id_, true));
     // It must not return NULL, that's a bug of the implementation
     if (!context) {
         isc_throw(isc::Unexpected, "Iterator context null at " + name);
@@ -328,7 +327,7 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
     ZoneFinder::Result result_status = SUCCESS;
     std::pair<bool, isc::dns::RRsetPtr> found;
     logger.debug(DBG_TRACE_DETAILED, DATASRC_DATABASE_FIND_RECORDS)
-        .arg(database_->getDBName()).arg(name).arg(type);
+        .arg(accessor_->getDBName()).arg(name).arg(type);
     // In case we are in GLUE_OK mode and start matching wildcards,
     // we can't do it under NS, so we store it here to check
     isc::dns::RRsetPtr first_ns;
@@ -365,12 +364,12 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
             if (result_rrset->getType() == isc::dns::RRType::NS()) {
                 LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                           DATASRC_DATABASE_FOUND_DELEGATION).
-                    arg(database_->getDBName()).arg(superdomain);
+                    arg(accessor_->getDBName()).arg(superdomain);
                 result_status = DELEGATION;
             } else {
                 LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                           DATASRC_DATABASE_FOUND_DNAME).
-                    arg(database_->getDBName()).arg(superdomain);
+                    arg(accessor_->getDBName()).arg(superdomain);
                 result_status = DNAME;
             }
             // Don't search more
@@ -389,7 +388,7 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
             result_rrset->getType() == isc::dns::RRType::NS()) {
             LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                       DATASRC_DATABASE_FOUND_DELEGATION_EXACT).
-                arg(database_->getDBName()).arg(name);
+                arg(accessor_->getDBName()).arg(name);
             result_status = DELEGATION;
         } else if (result_rrset && type != isc::dns::RRType::CNAME() &&
                    result_rrset->getType() == isc::dns::RRType::CNAME()) {
@@ -403,7 +402,7 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
             if (hasSubdomains(name.toText())) {
                 LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                           DATASRC_DATABASE_FOUND_EMPTY_NONTERMINAL).
-                    arg(database_->getDBName()).arg(name);
+                    arg(accessor_->getDBName()).arg(name);
                 records_found = true;
             } else {
                 // It's not empty non-terminal. So check for wildcards.
@@ -433,7 +432,7 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
                             glue_ok = false;
                             LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                                       DATASRC_DATABASE_WILDCARD_CANCEL_NS).
-                                arg(database_->getDBName()).arg(wildcard).
+                                arg(accessor_->getDBName()).arg(wildcard).
                                 arg(first_ns->getName());
                         } else if (!hasSubdomains(name.split(i - 1).toText()))
                         {
@@ -445,12 +444,12 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
                             result_rrset = found.second;
                             LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                                       DATASRC_DATABASE_WILDCARD).
-                                arg(database_->getDBName()).arg(wildcard).
+                                arg(accessor_->getDBName()).arg(wildcard).
                                 arg(name);
                         } else {
                             LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                                       DATASRC_DATABASE_WILDCARD_CANCEL_SUB).
-                                arg(database_->getDBName()).arg(wildcard).
+                                arg(accessor_->getDBName()).arg(wildcard).
                                 arg(name).arg(superdomain);
                         }
                         break;
@@ -459,7 +458,7 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
                         records_found = true;
                         LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                                   DATASRC_DATABASE_WILDCARD_EMPTY).
-                            arg(database_->getDBName()).arg(wildcard).
+                            arg(accessor_->getDBName()).arg(wildcard).
                             arg(name);
                         break;
                     }
@@ -472,20 +471,20 @@ DatabaseClient::Finder::find(const isc::dns::Name& name,
         if (records_found) {
             logger.debug(DBG_TRACE_DETAILED,
                          DATASRC_DATABASE_FOUND_NXRRSET)
-                        .arg(database_->getDBName()).arg(name)
+                        .arg(accessor_->getDBName()).arg(name)
                         .arg(getClass()).arg(type);
             result_status = NXRRSET;
         } else {
             logger.debug(DBG_TRACE_DETAILED,
                          DATASRC_DATABASE_FOUND_NXDOMAIN)
-                        .arg(database_->getDBName()).arg(name)
+                        .arg(accessor_->getDBName()).arg(name)
                         .arg(getClass()).arg(type);
             result_status = NXDOMAIN;
         }
     } else {
         logger.debug(DBG_TRACE_DETAILED,
                      DATASRC_DATABASE_FOUND_RRSET)
-                    .arg(database_->getDBName()).arg(*result_rrset);
+                    .arg(accessor_->getDBName()).arg(*result_rrset);
     }
     return (FindResult(result_status, result_rrset));
 }
@@ -580,7 +579,7 @@ private:
 ZoneIteratorPtr
 DatabaseClient::getIterator(const isc::dns::Name& name) const {
     // Get the zone
-    std::pair<bool, int> zone(database_->getZone(name.toText()));
+    std::pair<bool, int> zone(accessor_->getZone(name.toText()));
     if (!zone.first) {
         // No such zone, can't continue
         isc_throw(DataSourceError, "Zone " + name.toText() +
@@ -589,7 +588,7 @@ DatabaseClient::getIterator(const isc::dns::Name& name) const {
     }
     // Request the context
     DatabaseAccessor::IteratorContextPtr
-        context(database_->getAllRecords(zone.second));
+        context(accessor_->getAllRecords(zone.second));
     // It must not return NULL, that's a bug of the implementation
     if (context == DatabaseAccessor::IteratorContextPtr()) {
         isc_throw(isc::Unexpected, "Iterator context null at " +

+ 224 - 7
src/lib/datasrc/database.h

@@ -71,6 +71,38 @@ public:
     };
 
     /**
+     * Definitions of the fields to be passed to addRecordToZone().
+     *
+     * Each derived implementation of addRecordToZone() should expect
+     * the "columns" vector to be filled with the values as described in this
+     * enumeration, in this order.
+     */
+    enum AddRecordColumns {
+        ADD_NAME = 0, ///< The owner name of the record (a domain name)
+        ADD_REV_NAME = 1, ///< Reversed name of NAME (used for DNSSEC)
+        ADD_TTL = 2,     ///< The TTL of the record (in numeric form)
+        ADD_TYPE = 3,    ///< The RRType of the record (A/NS/TXT etc.)
+        ADD_SIGTYPE = 4, ///< For RRSIG records, this contains the RRTYPE
+                            ///< the RRSIG covers.
+        ADD_RDATA = 5,    ///< Full text representation of the record's RDATA
+        ADD_COLUMN_COUNT = 6 ///< Number of columns
+    };
+
+    /**
+     * Definitions of the fields to be passed to deleteRecordInZone().
+     *
+     * Each derived implementation of deleteRecordInZone() should expect
+     * the "params" vector to be filled with the values as described in this
+     * enumeration, in this order.
+     */
+    enum DeleteRecordParams {
+        DEL_NAME = 0, ///< The owner name of the record (a domain name)
+        DEL_TYPE = 1, ///< The RRType of the record (A/NS/TXT etc.)
+        DEL_RDATA = 2, ///< Full text representation of the record's RDATA
+        DEL_PARAM_COUNT = 3 ///< Number of parameters
+    };
+
+    /**
      * \brief Destructor
      *
      * It is empty, but needs a virtual one, since we will use the derived
@@ -205,6 +237,189 @@ public:
      */
     virtual IteratorContextPtr getAllRecords(int id) const = 0;
 
+    /// Start a transaction for updating a zone.
+    ///
+    /// Each derived class version of this method starts a database
+    /// transaction to make updates to the given name of zone (whose class was
+    /// specified at the construction of the class).
+    ///
+    /// If \c replace is true, any existing records of the zone will be
+    /// deleted on successful completion of updates (after
+    /// \c commitUpdateZone()); if it's false, the existing records will be
+    /// intact unless explicitly deleted by \c deleteRecordInZone().
+    ///
+    /// A single \c DatabaseAccessor instance can perform at most one update
+    /// transaction; a duplicate call to this method before
+    /// \c commitUpdateZone() or \c rollbackUpdateZone() will result in
+    /// a \c DataSourceError exception.  If multiple update attempts need
+    /// to be performed concurrently (and if the underlying database allows
+    /// such operation), separate \c DatabaseAccessor instance must be
+    /// created.
+    ///
+    /// \note The underlying database may not allow concurrent updates to
+    /// the same database instance even if different "connections" (or
+    /// something similar specific to the database implementation) are used
+    /// for different sets of updates.  For example, it doesn't seem to be
+    /// possible for SQLite3 unless different databases are used.  MySQL
+    /// allows concurrent updates to different tables of the same database,
+    /// but a specific operation may block others.  As such, this interface
+    /// doesn't require derived classes to allow concurrent updates with
+    /// multiple \c DatabaseAccessor instances; however, the implementation
+    /// is encouraged to do the best for making it more likely to succeed
+    /// as long as the underlying database system allows concurrent updates.
+    ///
+    /// This method returns a pair of \c bool and \c int.  Its first element
+    /// indicates whether the given name of zone is found.  If it's false,
+    /// the transaction isn't considered to be started; a subsequent call to
+    /// this method with an existing zone name should succeed.  Likewise,
+    /// if a call to this method results in an exception, the transaction
+    /// isn't considered to be started.  Note also that if the zone is not
+    /// found this method doesn't try to create a new one in the database.
+    /// It must have been created by some other means beforehand.
+    ///
+    /// The second element is the internal zone ID used for subsequent
+    /// updates.  Depending on implementation details of the actual derived
+    /// class method, it may be different from the one returned by
+    /// \c getZone(); for example, a specific implementation may use a
+    /// completely new zone ID when \c replace is true.
+    ///
+    /// \exception DataSourceError Duplicate call to this method, or some
+    /// internal database related error.
+    ///
+    /// \param zone_name A string representation of the zone name to be updated
+    /// \param replace Whether to replace the entire zone (see above)
+    ///
+    /// \return A pair of bool and int, indicating whether the specified zone
+    /// exists and (if so) the zone ID to be used for the update, respectively.
+    virtual std::pair<bool, int> startUpdateZone(const std::string& zone_name,
+                                                 bool replace) = 0;
+
+    /// Add a single record to the zone to be updated.
+    ///
+    /// This method provides a simple interface to insert a new record
+    /// (a database "row") to the zone in the update context started by
+    /// \c startUpdateZone().  The zone to which the record to be added
+    /// is the one specified at the time of the \c startUpdateZone() call.
+    ///
+    /// A successful call to \c startUpdateZone() must have preceded to
+    /// this call; otherwise a \c DataSourceError exception will be thrown.
+    ///
+    /// The row is defined as a vector of strings that has exactly
+    /// ADD_COLUMN_COUNT number of elements.  See AddRecordColumns for
+    /// the semantics of each element.
+    ///
+    /// Derived class methods are not required to check whether the given
+    /// values in \c columns are valid in terms of the expected semantics;
+    /// in general, it's the caller's responsibility.
+    /// For example, TTLs would normally be expected to be a textual
+    /// representation of decimal numbers, but this interface doesn't require
+    /// the implementation to perform this level of validation.  It may check
+    /// the values, however, and in that case if it detects an error it
+    /// should throw a \c DataSourceError exception.
+    ///
+    /// Likewise, derived class methods are not required to detect any
+    /// duplicate record that is already in the zone.
+    ///
+    /// \note The underlying database schema may not have a trivial mapping
+    /// from this style of definition of rows to actual database records.
+    /// It's the implementation's responsibility to implement the mapping
+    /// in the actual derived method.
+    ///
+    /// \exception DataSourceError Invalid call without starting a transaction,
+    /// or other internal database error.
+    ///
+    /// \param columns An array of strings that defines a record to be added
+    /// to the zone.
+    virtual void addRecordToZone(
+        const std::string (&columns)[ADD_COLUMN_COUNT]) = 0;
+
+    /// Delete a single record from the zone to be updated.
+    ///
+    /// This method provides a simple interface to delete a record
+    /// (a database "row") from the zone in the update context started by
+    /// \c startUpdateZone().  The zone from which the record to be deleted
+    /// is the one specified at the time of the \c startUpdateZone() call.
+    ///
+    /// A successful call to \c startUpdateZone() must have preceded to
+    /// this call; otherwise a \c DataSourceError exception will be thrown.
+    ///
+    /// The record to be deleted is specified by a vector of strings that has
+    /// exactly DEL_PARAM_COUNT number of elements.  See DeleteRecordParams
+    /// for the semantics of each element.
+    ///
+    /// \note In IXFR, TTL may also be specified, but we intentionally
+    /// ignore that in this interface, because it's not guaranteed
+    /// that all records have the same TTL (unlike the RRset
+    /// assumption) and there can even be multiple records for the
+    /// same name, type and rdata with different TTLs.  If we only
+    /// delete one of them, subsequent lookup will still return a
+    /// positive answer, which would be confusing.  It's a higher
+    /// layer's responsibility to check if there is at least one
+    /// record in the database that has the given TTL.
+    ///
+    /// Like \c addRecordToZone, derived class methods are not required to
+    /// validate the semantics of the given parameters or to check if there
+    /// is a record that matches the specified parameter; if there isn't
+    /// it simply ignores the result.
+    ///
+    /// \exception DataSourceError Invalid call without starting a transaction,
+    /// or other internal database error.
+    ///
+    /// \param params An array of strings that defines a record to be deleted
+    /// from the zone.
+    virtual void deleteRecordInZone(
+        const std::string (&params)[DEL_PARAM_COUNT]) = 0;
+
+    /// Commit updates to the zone.
+    ///
+    /// This method completes a transaction of making updates to the zone
+    /// in the context started by startUpdateZone.
+    ///
+    /// A successful call to \c startUpdateZone() must have preceded to
+    /// this call; otherwise a \c DataSourceError exception will be thrown.
+    /// Once this method successfully completes, the transaction isn't
+    /// considered to exist any more.  So a new transaction can now be
+    /// started.  On the other hand, a duplicate call to this method after
+    /// a successful completion of it is invalid and should result in
+    /// a \c DataSourceError exception.
+    ///
+    /// If some internal database error happens, a \c DataSourceError
+    /// exception must be thrown.  In that case the transaction is still
+    /// considered to be valid; the caller must explicitly rollback it
+    /// or (if it's confident that the error is temporary) try to commit it
+    /// again.
+    ///
+    /// \exception DataSourceError Call without a transaction, duplicate call
+    /// to the method or internal database error.
+    virtual void commitUpdateZone() = 0;
+
+    /// Rollback updates to the zone made so far.
+    ///
+    /// This method rollbacks a transaction of making updates to the zone
+    /// in the context started by startUpdateZone.  When it succeeds
+    /// (it normally should, but see below), the underlying database should
+    /// be reverted to the point before performing the corresponding
+    /// \c startUpdateZone().
+    ///
+    /// A successful call to \c startUpdateZone() must have preceded to
+    /// this call; otherwise a \c DataSourceError exception will be thrown.
+    /// Once this method successfully completes, the transaction isn't
+    /// considered to exist any more.  So a new transaction can now be
+    /// started.  On the other hand, a duplicate call to this method after
+    /// a successful completion of it is invalid and should result in
+    /// a \c DataSourceError exception.
+    ///
+    /// Normally this method should not fail.  But it may not always be
+    /// possible to guarantee it depending on the characteristics of the
+    /// underlying database system.  So this interface doesn't require the
+    /// actual implementation for the error free property.  But if a specific
+    /// implementation of this method can fail, it is encouraged to document
+    /// when that can happen with its implication.
+    ///
+    /// \exception DataSourceError Call without a transaction, duplicate call
+    /// to the method or internal database error.
+    virtual void rollbackUpdateZone() = 0;
+
     /**
      * \brief Returns a string identifying this dabase backend
      *
@@ -338,18 +553,19 @@ public:
          * applications shouldn't need it.
          */
         int zone_id() const { return (zone_id_); }
+
         /**
-         * \brief The database.
+         * \brief The database accessor.
          *
-         * This function provides the database stored inside as
+         * This function provides the database accessor stored inside as
          * passed to the constructor. This is meant for testing purposes and
          * normal applications shouldn't need it.
          */
-        const DatabaseAccessor& database() const {
-            return (*database_);
+        const DatabaseAccessor& getAccessor() const {
+            return (*accessor_);
         }
     private:
-        boost::shared_ptr<DatabaseAccessor> database_;
+        boost::shared_ptr<DatabaseAccessor> accessor_;
         const int zone_id_;
         const isc::dns::Name origin_;
         /**
@@ -408,6 +624,7 @@ public:
          */
         bool hasSubdomains(const std::string& name);
     };
+
     /**
      * \brief Find a zone in the database
      *
@@ -444,8 +661,8 @@ public:
     virtual ZoneIteratorPtr getIterator(const isc::dns::Name& name) const;
 
 private:
-    /// \brief Our database.
-    const boost::shared_ptr<DatabaseAccessor> database_;
+    /// \brief The accessor to our database.
+    const boost::shared_ptr<DatabaseAccessor> accessor_;
 };
 
 }

+ 269 - 158
src/lib/datasrc/sqlite3_accessor.cc

@@ -14,29 +14,118 @@
 
 #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;
 
 #define SQLITE_SCHEMA_VERSION 1
 
 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,
+    ANY_SUB = 2,
+    BEGIN = 3,
+    COMMIT = 4,
+    ROLLBACK = 5,
+    DEL_ZONE_RECORDS = 6,
+    ADD_RECORD = 7,
+    DEL_RECORD = 8,
+    ITERATE = 9,
+    NUM_STATEMENTS = 10
+};
+
+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",
+    "SELECT rdtype, ttl, sigtype, rdata " // ANY_SUB
+    "FROM records WHERE zone_id=?1 AND name LIKE (\"%.\" || ?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)
-    {}
+        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* 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 StatementProcessor {
+public:
+    // desc will be used on failure in the what() message of the resulting
+    // DataSourceError exception.
+    StatementProcessor(SQLite3Parameters& dbparameters, StatementID stmt_id,
+                       const char* desc) :
+        dbparameters_(dbparameters), stmt_id_(stmt_id), desc_(desc)
+    {
+        sqlite3_clear_bindings(dbparameters_.statements_[stmt_id_]);
+    }
+
+    ~StatementProcessor() {
+        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,
+SQLite3Accessor::SQLite3Accessor(const std::string& filename,
                                  const isc::dns::RRClass& rrclass) :
     dbparameters_(new SQLite3Parameters),
     class_(rrclass.toText()),
@@ -60,34 +149,10 @@ namespace {
 class Initializer {
 public:
     ~Initializer() {
-        if (params_.q_zone_ != NULL) {
-            sqlite3_finalize(params_.q_zone_);
-        }
-        // we do NOT finalize q_current_ - that is just a pointer to one of
-        // the other statements, not a separate one.
-        /*
-        if (params_.q_record_ != NULL) {
-            sqlite3_finalize(params_.q_record_);
-        }
-        if (params_.q_addrs_ != NULL) {
-            sqlite3_finalize(params_.q_addrs_);
+        for (int i = 0; i < NUM_STATEMENTS; ++i) {
+            sqlite3_finalize(params_.statements_[i]);
         }
-        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_);
-        }
-        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_);
         }
@@ -123,54 +188,6 @@ const char* const SCHEMA_LIST[] = {
     NULL
 };
 
-const char* const q_version_str = "SELECT version FROM schema_version";
-
-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";
-
-const char* const q_any_sub_str = "SELECT rdtype, ttl, sigtype, rdata "
-    "FROM records WHERE zone_id=?1 AND name LIKE (\"%.\" || ?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;
@@ -184,7 +201,7 @@ prepare(sqlite3* const db, const char* const statement) {
 // small function to sleep for 0.1 seconds, needed when waiting for
 // exclusive database locks (which should only occur on startup, and only
 // when the database has not been created yet)
-void do_sleep() {
+void doSleep() {
     struct timespec req;
     req.tv_sec = 0;
     req.tv_nsec = 100000000;
@@ -193,13 +210,14 @@ void do_sleep() {
 
 // returns the schema version if the schema version table exists
 // returns -1 if it does not
-int check_schema_version(sqlite3* db) {
+int checkSchemaVersion(sqlite3* db) {
     sqlite3_stmt* prepared = NULL;
     // At this point in time, the database might be exclusively locked, in
     // which case even prepare() will return BUSY, so we may need to try a
     // few times
     for (size_t i = 0; i < 50; ++i) {
-        int rc = sqlite3_prepare_v2(db, q_version_str, -1, &prepared, NULL);
+        int rc = sqlite3_prepare_v2(db, "SELECT version FROM schema_version",
+                                    -1, &prepared, NULL);
         if (rc == SQLITE_ERROR) {
             // this is the error that is returned when the table does not
             // exist
@@ -210,7 +228,7 @@ int check_schema_version(sqlite3* db) {
             isc_throw(SQLite3Error, "Unable to prepare version query: "
                         << rc << " " << sqlite3_errmsg(db));
         }
-        do_sleep();
+        doSleep();
     }
     if (sqlite3_step(prepared) != SQLITE_ROW) {
         isc_throw(SQLite3Error,
@@ -238,9 +256,9 @@ int create_database(sqlite3* db) {
             isc_throw(SQLite3Error, "Unable to acquire exclusive lock "
                         "for database creation: " << sqlite3_errmsg(db));
         }
-        do_sleep();
+        doSleep();
     }
-    int schema_version = check_schema_version(db);
+    int schema_version = checkSchemaVersion(db);
     if (schema_version == -1) {
         for (int i = 0; SCHEMA_LIST[i] != NULL; ++i) {
             if (sqlite3_exec(db, SCHEMA_LIST[i], NULL, NULL, NULL) !=
@@ -260,28 +278,21 @@ void
 checkAndSetupSchema(Initializer* initializer) {
     sqlite3* const db = initializer->params_.db_;
 
-    int schema_version = check_schema_version(db);
+    int schema_version = checkSchemaVersion(db);
     if (schema_version != SQLITE_SCHEMA_VERSION) {
         schema_version = create_database(db);
     }
     initializer->params_.version_ = schema_version;
 
-    initializer->params_.q_zone_ = prepare(db, q_zone_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]);
+    }
 }
 
 }
 
 void
-SQLite3Database::open(const std::string& name) {
+SQLite3Accessor::open(const std::string& name) {
     LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_SQLITE_CONNOPEN).arg(name);
     if (dbparameters_->db_ != NULL) {
         // There shouldn't be a way to trigger this anyway
@@ -298,7 +309,7 @@ SQLite3Database::open(const std::string& name) {
     initializer.move(dbparameters_.get());
 }
 
-SQLite3Database::~SQLite3Database() {
+SQLite3Accessor::~SQLite3Accessor() {
     LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_SQLITE_DROPCONN);
     if (dbparameters_->db_ != NULL) {
         close();
@@ -306,7 +317,7 @@ SQLite3Database::~SQLite3Database() {
 }
 
 void
-SQLite3Database::close(void) {
+SQLite3Accessor::close(void) {
     LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_SQLITE_CONNCLOSE);
     if (dbparameters_->db_ == NULL) {
         isc_throw(DataSourceError,
@@ -314,104 +325,83 @@ SQLite3Database::close(void) {
     }
 
     // XXX: sqlite3_finalize() could fail.  What should we do in that case?
-    sqlite3_finalize(dbparameters_->q_zone_);
-    dbparameters_->q_zone_ = 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;
 }
 
 std::pair<bool, int>
-SQLite3Database::getZone(const std::string& name) const {
+SQLite3Accessor::getZone(const std::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.c_str(),
-                           -1, SQLITE_STATIC);
+    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));
 }
 
-class SQLite3Database::Context : public DatabaseAccessor::IteratorContext {
+class SQLite3Accessor::Context : public DatabaseAccessor::IteratorContext {
 public:
     // Construct an iterator for all records. When constructed this
     // way, the getNext() call will copy all fields
-    Context(const boost::shared_ptr<const SQLite3Database>& database, int id) :
+    Context(const boost::shared_ptr<const SQLite3Accessor>& accessor, int id) :
         iterator_type_(ITT_ALL),
-        database_(database),
+        accessor_(accessor),
         statement_(NULL),
         name_("")
     {
         // We create the statement now and then just keep getting data from it
-        statement_ = prepare(database->dbparameters_->db_, q_iterate_str);
+        statement_ = prepare(accessor->dbparameters_->db_,
+                             text_statements[ITERATE]);
         bindZoneId(id);
     }
 
     // Construct an iterator for records with a specific name. When constructed
     // this way, the getNext() call will copy all fields except name
-    Context(const boost::shared_ptr<const SQLite3Database>& database, int id,
+    Context(const boost::shared_ptr<const SQLite3Accessor>& accessor, int id,
             const std::string& name, bool subdomains) :
         iterator_type_(ITT_NAME),
-        database_(database),
+        accessor_(accessor),
         statement_(NULL),
         name_(name)
+
     {
         // We create the statement now and then just keep getting data from it
-        statement_ = prepare(database->dbparameters_->db_,
-                             subdomains ? q_any_sub_str : q_any_str);
+        statement_ = prepare(accessor->dbparameters_->db_,
+                             subdomains ? text_statements[ANY_SUB] :
+                             text_statements[ANY]);
         bindZoneId(id);
         bindName(name_);
     }
@@ -438,7 +428,7 @@ public:
         } else if (rc != SQLITE_DONE) {
             isc_throw(DataSourceError,
                       "Unexpected failure in sqlite3_step: " <<
-                      sqlite3_errmsg(database_->dbparameters_->db_));
+                      sqlite3_errmsg(accessor_->dbparameters_->db_));
         }
         finalize();
         return (false);
@@ -467,14 +457,14 @@ private:
             finalize();
             isc_throw(SQLite3Error, "Could not bind int " << zone_id <<
                       " to SQL statement: " <<
-                      sqlite3_errmsg(database_->dbparameters_->db_));
+                      sqlite3_errmsg(accessor_->dbparameters_->db_));
         }
     }
 
     void bindName(const std::string& name) {
         if (sqlite3_bind_text(statement_, 2, name.c_str(), -1,
-                              SQLITE_STATIC) != SQLITE_OK) {
-            const char* errmsg = sqlite3_errmsg(database_->dbparameters_->db_);
+                              SQLITE_TRANSIENT) != SQLITE_OK) {
+            const char* errmsg = sqlite3_errmsg(accessor_->dbparameters_->db_);
             finalize();
             isc_throw(SQLite3Error, "Could not bind text '" << name <<
                       "' to SQL statement: " << errmsg);
@@ -496,7 +486,7 @@ private:
             // The field can really be NULL, in which case we return an
             // empty string, or sqlite may have run out of memory, in
             // which case we raise an error
-            if (sqlite3_errcode(database_->dbparameters_->db_)
+            if (sqlite3_errcode(accessor_->dbparameters_->db_)
                                 == SQLITE_NOMEM) {
                 isc_throw(DataSourceError,
                         "Sqlite3 backend encountered a memory allocation "
@@ -510,13 +500,13 @@ private:
     }
 
     const IteratorType iterator_type_;
-    boost::shared_ptr<const SQLite3Database> database_;
+    boost::shared_ptr<const SQLite3Accessor> accessor_;
     sqlite3_stmt *statement_;
     const std::string name_;
 };
 
 DatabaseAccessor::IteratorContextPtr
-SQLite3Database::getRecords(const std::string& name, int id,
+SQLite3Accessor::getRecords(const std::string& name, int id,
                             bool subdomains) const
 {
     return (IteratorContextPtr(new Context(shared_from_this(), id, name,
@@ -524,9 +514,130 @@ SQLite3Database::getRecords(const std::string& name, int id,
 }
 
 DatabaseAccessor::IteratorContextPtr
-SQLite3Database::getAllRecords(int id) const {
+SQLite3Accessor::getAllRecords(int id) const {
     return (IteratorContextPtr(new Context(shared_from_this(), id)));
 }
 
+pair<bool, int>
+SQLite3Accessor::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);
+    }
+
+    StatementProcessor(*dbparameters_, BEGIN,
+                       "start an SQLite3 transaction").exec();
+
+    if (replace) {
+        try {
+            StatementProcessor 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();
+        } catch (const DataSourceError&) {
+            // Once we start a transaction, if something unexpected happens
+            // we need to rollback the transaction so that a subsequent update
+            // is still possible with this accessor.
+            StatementProcessor(*dbparameters_, ROLLBACK,
+                               "rollback an SQLite3 transaction").exec();
+            throw;
+        }
+    }
+
+    dbparameters_->updating_zone = true;
+    dbparameters_->updated_zone_id = zone_info.second;
+
+    return (zone_info);
+}
+
+void
+SQLite3Accessor::commitUpdateZone() {
+    if (!dbparameters_->updating_zone) {
+        isc_throw(DataSourceError, "committing zone update on SQLite3 "
+                  "data source without transaction");
+    }
+
+    StatementProcessor(*dbparameters_, COMMIT,
+                       "commit an SQLite3 transaction").exec();
+    dbparameters_->updating_zone = false;
+    dbparameters_->updated_zone_id = -1;
+}
+
+void
+SQLite3Accessor::rollbackUpdateZone() {
+    if (!dbparameters_->updating_zone) {
+        isc_throw(DataSourceError, "rolling back zone update on SQLite3 "
+                  "data source without transaction");
+    }
+
+    StatementProcessor(*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
+template <typename COLUMNS_TYPE>
+void
+doUpdate(SQLite3Parameters& dbparams, StatementID stmt_id,
+         COLUMNS_TYPE update_params, const char* exec_desc)
+{
+    sqlite3_stmt* const stmt = dbparams.statements_[stmt_id];
+    StatementProcessor 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_));
+    }
+    const size_t column_count =
+        sizeof(update_params) / sizeof(update_params[0]);
+    for (int i = 0; i < column_count; ++i) {
+        if (sqlite3_bind_text(stmt, ++param_id, update_params[i].c_str(), -1,
+                              SQLITE_TRANSIENT) != SQLITE_OK) {
+            isc_throw(DataSourceError, "failed to bind SQLite3 parameter: " <<
+                      sqlite3_errmsg(dbparams.db_));
+        }
+    }
+    executer.exec();
+}
+}
+
+void
+SQLite3Accessor::addRecordToZone(const string (&columns)[ADD_COLUMN_COUNT]) {
+    if (!dbparameters_->updating_zone) {
+        isc_throw(DataSourceError, "adding record to SQLite3 "
+                  "data source without transaction");
+    }
+    doUpdate<const string (&)[DatabaseAccessor::ADD_COLUMN_COUNT]>(
+        *dbparameters_, ADD_RECORD, columns, "add record to zone");
+}
+
+void
+SQLite3Accessor::deleteRecordInZone(const string (&params)[DEL_PARAM_COUNT]) {
+    if (!dbparameters_->updating_zone) {
+        isc_throw(DataSourceError, "deleting record in SQLite3 "
+                  "data source without transaction");
+    }
+    doUpdate<const string (&)[DatabaseAccessor::DEL_PARAM_COUNT]>(
+        *dbparameters_, DEL_RECORD, params, "delete record from zone");
+}
+
 }
 }

+ 36 - 5
src/lib/datasrc/sqlite3_accessor.h

@@ -53,8 +53,8 @@ struct SQLite3Parameters;
  * According to the design, it doesn't interpret the data in any way, it just
  * provides unified access to the DB.
  */
-class SQLite3Database : public DatabaseAccessor,
-    public boost::enable_shared_from_this<SQLite3Database> {
+class SQLite3Accessor : public DatabaseAccessor,
+    public boost::enable_shared_from_this<SQLite3Accessor> {
 public:
     /**
      * \brief Constructor
@@ -69,14 +69,14 @@ public:
      *     file can contain multiple classes of data, single database can
      *     provide only one class).
      */
-    SQLite3Database(const std::string& filename,
+    SQLite3Accessor(const std::string& filename,
                     const isc::dns::RRClass& rrclass);
     /**
      * \brief Destructor
      *
      * Closes the database.
      */
-    ~SQLite3Database();
+    ~SQLite3Accessor();
 
     /**
      * \brief Look up a zone
@@ -121,6 +121,33 @@ public:
      */
     virtual IteratorContextPtr getAllRecords(int id) const;
 
+    virtual std::pair<bool, int> startUpdateZone(const std::string& zone_name,
+                                                 bool replace);
+
+    /// \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.  Right now this
+    /// implementation throws a \c DataSourceError exception in such a case.
+    virtual void commitUpdateZone();
+
+    /// \note 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 a \c DataSourceError exception.  It should be
+    /// considered a bug of the higher level application program.
+    virtual void rollbackUpdateZone();
+
+    virtual void addRecordToZone(
+        const std::string (&columns)[ADD_COLUMN_COUNT]);
+
+    virtual void deleteRecordInZone(
+        const std::string (&params)[DEL_PARAM_COUNT]);
+
     /// 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
@@ -146,4 +173,8 @@ private:
 }
 }
 
-#endif
+#endif  // __DATASRC_SQLITE3_CONNECTION_H
+
+// Local Variables:
+// mode: c++
+// End:

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

@@ -1,9 +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_BUILD_DIR=\"$(builddir)/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)
 

+ 13 - 7
src/lib/datasrc/tests/database_unittest.cc

@@ -58,6 +58,15 @@ public:
         }
     }
 
+    virtual std::pair<bool, int> startUpdateZone(const std::string&, bool) {
+        // return dummy value.  unused anyway.
+        return (pair<bool, int>(true, 0));
+    }
+    virtual void commitUpdateZone() {}
+    virtual void rollbackUpdateZone() {}
+    virtual void addRecordToZone(const string (&)[ADD_COLUMN_COUNT]) {}
+    virtual void deleteRecordInZone(const string (&)[DEL_PARAM_COUNT]) {}
+
     virtual const std::string& getDBName() const {
         return (database_name_);
     }
@@ -498,12 +507,12 @@ public:
      * times per test.
      */
     void createClient() {
-        current_database_ = new MockAccessor();
+        current_accessor_ = new MockAccessor();
         client_.reset(new DatabaseClient(shared_ptr<DatabaseAccessor>(
-             current_database_)));
+             current_accessor_)));
     }
     // Will be deleted by client_, just keep the current value for comparison.
-    MockAccessor* current_database_;
+    MockAccessor* current_accessor_;
     shared_ptr<DatabaseClient> client_;
     const std::string database_name_;
 
@@ -518,7 +527,7 @@ public:
         ASSERT_NE(shared_ptr<DatabaseClient::Finder>(), finder) <<
             "Wrong type of finder";
         EXPECT_EQ(42, finder->zone_id());
-        EXPECT_EQ(current_database_, &finder->database());
+        EXPECT_EQ(current_accessor_, &finder->getAccessor());
     }
 
     shared_ptr<DatabaseClient::Finder> getFinder() {
@@ -848,7 +857,6 @@ TEST_F(DatabaseClientTest, find) {
                ZoneFinder::CNAME,
                expected_rdatas_, expected_sig_rdatas_);
 
-
     expected_rdatas_.clear();
     expected_sig_rdatas_.clear();
     expected_rdatas_.push_back("192.0.2.1");
@@ -899,7 +907,6 @@ TEST_F(DatabaseClientTest, find) {
                ZoneFinder::SUCCESS,
                expected_rdatas_, expected_sig_rdatas_);
 
-
     EXPECT_THROW(finder->find(isc::dns::Name("badcname1.example.org."),
                                               isc::dns::RRType::A(),
                                               NULL, ZoneFinder::FIND_DEFAULT),
@@ -942,7 +949,6 @@ TEST_F(DatabaseClientTest, find) {
                                               isc::dns::RRType::A(),
                                               NULL, ZoneFinder::FIND_DEFAULT),
                  std::exception);
-
     EXPECT_THROW(finder->find(isc::dns::Name("dsexception.in.getnext."),
                                               isc::dns::RRType::A(),
                                               NULL, ZoneFinder::FIND_DEFAULT),

+ 336 - 43
src/lib/datasrc/tests/sqlite3_accessor_unittest.cc

@@ -11,6 +11,10 @@
 // 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 <algorithm>
+#include <vector>
+
 #include <datasrc/sqlite3_accessor.h>
 
 #include <datasrc/data_source.h>
@@ -22,7 +26,9 @@
 #include <fstream>
 #include <sqlite3.h>
 
+using namespace std;
 using namespace isc::datasrc;
+using boost::shared_ptr;
 using isc::data::ConstElementPtr;
 using isc::data::Element;
 using isc::dns::RRClass;
@@ -47,82 +53,81 @@ std::string SQLITE_DBFILE_NOTEXIST = TEST_DATA_DIR "/nodir/notexist";
 
 // new db file, we don't need this to be a std::string, and given the
 // raw calls we use it in a const char* is more convenient
-const char* SQLITE_NEW_DBFILE = TEST_DATA_BUILD_DIR "/newdb.sqlite3";
+const char* SQLITE_NEW_DBFILE = TEST_DATA_BUILDDIR "/newdb.sqlite3";
 
 // Opening works (the content is tested in different tests)
 TEST(SQLite3Open, common) {
-    EXPECT_NO_THROW(SQLite3Database db(SQLITE_DBFILE_EXAMPLE,
-                                       RRClass::IN()));
+    EXPECT_NO_THROW(SQLite3Accessor accessor(SQLITE_DBFILE_EXAMPLE,
+                                             RRClass::IN()));
 }
 
 // The file can't be opened
 TEST(SQLite3Open, notExist) {
-    EXPECT_THROW(SQLite3Database db(SQLITE_DBFILE_NOTEXIST,
-                                    RRClass::IN()), SQLite3Error);
+    EXPECT_THROW(SQLite3Accessor accessor(SQLITE_DBFILE_NOTEXIST,
+                                          RRClass::IN()), SQLite3Error);
 }
 
 // It rejects broken DB
 TEST(SQLite3Open, brokenDB) {
-    EXPECT_THROW(SQLite3Database db(SQLITE_DBFILE_BROKENDB,
-                                    RRClass::IN()), SQLite3Error);
+    EXPECT_THROW(SQLite3Accessor accessor(SQLITE_DBFILE_BROKENDB,
+                                          RRClass::IN()), SQLite3Error);
 }
 
 // Test we can create the schema on the fly
 TEST(SQLite3Open, memoryDB) {
-    EXPECT_NO_THROW(SQLite3Database db(SQLITE_DBFILE_MEMORY,
-                                       RRClass::IN()));
+    EXPECT_NO_THROW(SQLite3Accessor accessor(SQLITE_DBFILE_MEMORY,
+                                             RRClass::IN()));
 }
 
 // Test fixture for querying the db
-class SQLite3Access : public ::testing::Test {
+class SQLite3AccessorTest : public ::testing::Test {
 public:
-    SQLite3Access() {
+    SQLite3AccessorTest() {
         initAccessor(SQLITE_DBFILE_EXAMPLE, RRClass::IN());
     }
     // So it can be re-created with different data
     void initAccessor(const std::string& filename, const RRClass& rrclass) {
-        db.reset(new SQLite3Database(filename, rrclass));
+        accessor.reset(new SQLite3Accessor(filename, rrclass));
     }
-    // The tested db
-    boost::shared_ptr<SQLite3Database> db;
+    // The tested accessor
+    boost::shared_ptr<SQLite3Accessor> accessor;
 };
 
 // This zone exists in the data, so it should be found
-TEST_F(SQLite3Access, getZone) {
-    std::pair<bool, int> result(db->getZone("example.com."));
+TEST_F(SQLite3AccessorTest, getZone) {
+    std::pair<bool, int> result(accessor->getZone("example.com."));
     EXPECT_TRUE(result.first);
     EXPECT_EQ(1, result.second);
 }
 
 // But it should find only the zone, nothing below it
-TEST_F(SQLite3Access, subZone) {
-    EXPECT_FALSE(db->getZone("sub.example.com.").first);
+TEST_F(SQLite3AccessorTest, subZone) {
+    EXPECT_FALSE(accessor->getZone("sub.example.com.").first);
 }
 
 // This zone is not there at all
-TEST_F(SQLite3Access, noZone) {
-    EXPECT_FALSE(db->getZone("example.org.").first);
+TEST_F(SQLite3AccessorTest, noZone) {
+    EXPECT_FALSE(accessor->getZone("example.org.").first);
 }
 
 // This zone is there, but in different class
-TEST_F(SQLite3Access, noClass) {
+TEST_F(SQLite3AccessorTest, noClass) {
     initAccessor(SQLITE_DBFILE_EXAMPLE, RRClass::CH());
-    EXPECT_FALSE(db->getZone("example.com.").first);
+    EXPECT_FALSE(accessor->getZone("example.com.").first);
 }
 
 // This tests the iterator context
-TEST_F(SQLite3Access, iterator) {
+TEST_F(SQLite3AccessorTest, iterator) {
     // Our test zone is conveniently small, but not empty
     initAccessor(SQLITE_DBFILE_EXAMPLE_ORG, RRClass::IN());
 
-    const std::pair<bool, int> zone_info(db->getZone("example.org."));
+    const std::pair<bool, int> zone_info(accessor->getZone("example.org."));
     ASSERT_TRUE(zone_info.first);
 
     // Get the iterator context
     DatabaseAccessor::IteratorContextPtr
-        context(db->getAllRecords(zone_info.second));
-    ASSERT_NE(DatabaseAccessor::IteratorContextPtr(),
-              context);
+        context(accessor->getAllRecords(zone_info.second));
+    ASSERT_NE(DatabaseAccessor::IteratorContextPtr(), context);
 
     std::string data[DatabaseAccessor::COLUMN_COUNT];
     // Get and check the first and only record
@@ -202,13 +207,13 @@ TEST_F(SQLite3Access, iterator) {
 }
 
 TEST(SQLite3Open, getDBNameExample2) {
-    SQLite3Database db(SQLITE_DBFILE_EXAMPLE2, RRClass::IN());
-    EXPECT_EQ(SQLITE_DBNAME_EXAMPLE2, db.getDBName());
+    SQLite3Accessor accessor(SQLITE_DBFILE_EXAMPLE2, RRClass::IN());
+    EXPECT_EQ(SQLITE_DBNAME_EXAMPLE2, accessor.getDBName());
 }
 
 TEST(SQLite3Open, getDBNameExampleROOT) {
-    SQLite3Database db(SQLITE_DBFILE_EXAMPLE_ROOT, RRClass::IN());
-    EXPECT_EQ(SQLITE_DBNAME_EXAMPLE_ROOT, db.getDBName());
+    SQLite3Accessor accessor(SQLITE_DBFILE_EXAMPLE_ROOT, RRClass::IN());
+    EXPECT_EQ(SQLITE_DBNAME_EXAMPLE_ROOT, accessor.getDBName());
 }
 
 // Simple function to cound the number of records for
@@ -228,8 +233,8 @@ checkRecordRow(const std::string columns[],
     EXPECT_EQ(field4, columns[DatabaseAccessor::NAME_COLUMN]);
 }
 
-TEST_F(SQLite3Access, getRecords) {
-    const std::pair<bool, int> zone_info(db->getZone("example.com."));
+TEST_F(SQLite3AccessorTest, getRecords) {
+    const std::pair<bool, int> zone_info(accessor->getZone("example.com."));
     ASSERT_TRUE(zone_info.first);
 
     const int zone_id = zone_info.second;
@@ -238,14 +243,14 @@ TEST_F(SQLite3Access, getRecords) {
     std::string columns[DatabaseAccessor::COLUMN_COUNT];
 
     DatabaseAccessor::IteratorContextPtr
-        context(db->getRecords("foo.bar", 1));
+        context(accessor->getRecords("foo.bar", 1));
     ASSERT_NE(DatabaseAccessor::IteratorContextPtr(),
               context);
     EXPECT_FALSE(context->getNext(columns));
     checkRecordRow(columns, "", "", "", "", "");
 
     // now try some real searches
-    context = db->getRecords("foo.example.com.", zone_id);
+    context = accessor->getRecords("foo.example.com.", zone_id);
     ASSERT_TRUE(context->getNext(columns));
     checkRecordRow(columns, "CNAME", "3600", "",
                    "cnametest.example.org.", "");
@@ -261,12 +266,13 @@ TEST_F(SQLite3Access, getRecords) {
                    "NSEC 5 3 7200 20100322084538 20100220084538 33495 "
                    "example.com. FAKEFAKEFAKEFAKE", "");
     EXPECT_FALSE(context->getNext(columns));
+
     // with no more records, the array should not have been modified
     checkRecordRow(columns, "RRSIG", "7200", "NSEC",
                    "NSEC 5 3 7200 20100322084538 20100220084538 33495 "
                    "example.com. FAKEFAKEFAKEFAKE", "");
 
-    context = db->getRecords("example.com.", zone_id);
+    context = accessor->getRecords("example.com.", zone_id);
     ASSERT_TRUE(context->getNext(columns));
     checkRecordRow(columns, "SOA", "3600", "",
                    "master.example.com. admin.example.com. "
@@ -336,12 +342,12 @@ TEST_F(SQLite3Access, getRecords) {
 
     // Try searching for subdomain
     // There's foo.bar.example.com in the data
-    context = db->getRecords("bar.example.com.", zone_id, true);
+    context = accessor->getRecords("bar.example.com.", zone_id, true);
     ASSERT_TRUE(context->getNext(columns));
     checkRecordRow(columns, "A", "3600", "", "192.0.2.1", "");
     EXPECT_FALSE(context->getNext(columns));
     // But we shouldn't match mix.example.com here
-    context = db->getRecords("ix.example.com.", zone_id, true);
+    context = accessor->getRecords("ix.example.com.", zone_id, true);
     EXPECT_FALSE(context->getNext(columns));
 }
 
@@ -366,7 +372,7 @@ bool exists(const char* filename) {
 TEST_F(SQLite3Create, creationtest) {
     ASSERT_FALSE(exists(SQLITE_NEW_DBFILE));
     // Should simply be created
-    SQLite3Database db(SQLITE_NEW_DBFILE, RRClass::IN());
+    SQLite3Accessor accessor(SQLITE_NEW_DBFILE, RRClass::IN());
     ASSERT_TRUE(exists(SQLITE_NEW_DBFILE));
 }
 
@@ -378,12 +384,12 @@ TEST_F(SQLite3Create, emptytest) {
     ASSERT_EQ(SQLITE_OK, sqlite3_open(SQLITE_NEW_DBFILE, &db));
 
     // empty, but not locked, so creating it now should work
-    SQLite3Database db2(SQLITE_NEW_DBFILE, RRClass::IN());
+    SQLite3Accessor accessor2(SQLITE_NEW_DBFILE, RRClass::IN());
 
     sqlite3_close(db);
 
     // should work now that we closed it
-    SQLite3Database db3(SQLITE_NEW_DBFILE, RRClass::IN());
+    SQLite3Accessor accessor3(SQLITE_NEW_DBFILE, RRClass::IN());
 }
 
 TEST_F(SQLite3Create, lockedtest) {
@@ -395,13 +401,300 @@ TEST_F(SQLite3Create, lockedtest) {
     sqlite3_exec(db, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL, NULL);
 
     // should not be able to open it
-    EXPECT_THROW(SQLite3Database db2(SQLITE_NEW_DBFILE, RRClass::IN()),
+    EXPECT_THROW(SQLite3Accessor accessor2(SQLITE_NEW_DBFILE, RRClass::IN()),
                  SQLite3Error);
 
     sqlite3_exec(db, "ROLLBACK TRANSACTION", NULL, NULL, NULL);
 
     // should work now that we closed it
-    SQLite3Database db3(SQLITE_NEW_DBFILE, RRClass::IN());
+    SQLite3Accessor accessor3(SQLITE_NEW_DBFILE, RRClass::IN());
 }
 
+//
+// 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 SQLite3AccessorTest {
+protected:
+    SQLite3Update() {
+        // Note: if "installing" the test file fails some of the subsequent
+        // tests will fail and we should be able to notice that.
+        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 = accessor->getZone("example.com.").second;
+        another_accessor.reset(new SQLite3Accessor(
+                                   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::string add_columns[DatabaseAccessor::ADD_COLUMN_COUNT];
+    std::string del_params[DatabaseAccessor::DEL_PARAM_COUNT];
+
+    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<SQLite3Accessor> another_accessor;
+    DatabaseAccessor::IteratorContextPtr iterator;
+};
+
+void
+checkRecords(SQLite3Accessor& accessor, int zone_id, const std::string& name,
+             vector<const char* const*> expected_rows)
+{
+    DatabaseAccessor::IteratorContextPtr iterator =
+        accessor.getRecords(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(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+    accessor->commitUpdateZone();
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, flushZone) {
+    // With 'replace' being true startUpdateZone() will flush the existing
+    // zone content.
+
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+    zone_id = accessor->startUpdateZone("example.com.", true).second;
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+    accessor->commitUpdateZone();
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, readWhileUpdate) {
+    zone_id = accessor->startUpdateZone("example.com.", true).second;
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Until commit is done, the other accessor should see the old data
+    checkRecords(*another_accessor, zone_id, "foo.bar.example.com.",
+                 expected_stored);
+
+    // Once the changes are committed, the other accessor will see the new
+    // data.
+    accessor->commitUpdateZone();
+    checkRecords(*another_accessor, zone_id, "foo.bar.example.com.",
+                 empty_stored);
+}
+
+TEST_F(SQLite3Update, rollback) {
+    zone_id = accessor->startUpdateZone("example.com.", true).second;
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Rollback will revert the change made by startUpdateZone(, true).
+    accessor->rollbackUpdateZone();
+    checkRecords(*accessor, 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 = accessor->getRecords("example.com.", zone_id);
+    EXPECT_TRUE(iterator->getNext(columns));
+
+    accessor->startUpdateZone("example.com.", true);
+    EXPECT_THROW(accessor->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_accessor->getRecords("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 = accessor->startUpdateZone("example.com.", true).second;
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+    EXPECT_THROW(accessor->commitUpdateZone(), DataSourceError);
+    accessor->rollbackUpdateZone();   // rollback should still succeed
+
+    checkRecords(*accessor, 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_accessor->startUpdateZone("sql1.example.com.",
+                                                  true).first);
+    EXPECT_THROW(accessor->startUpdateZone("example.com.", true),
+                 DataSourceError);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+
+    // Once we rollback the other attempt of change, we should be able to
+    // start and commit the transaction using the main accessor.
+    another_accessor->rollbackUpdateZone();
+    accessor->startUpdateZone("example.com.", true);
+    accessor->commitUpdateZone();
+}
+
+TEST_F(SQLite3Update, duplicateUpdate) {
+    accessor->startUpdateZone("example.com.", false);
+    EXPECT_THROW(accessor->startUpdateZone("example.com.", false),
+                 DataSourceError);
+}
+
+TEST_F(SQLite3Update, commitWithoutTransaction) {
+    EXPECT_THROW(accessor->commitUpdateZone(), DataSourceError);
+}
+
+TEST_F(SQLite3Update, rollbackWithoutTransaction) {
+    EXPECT_THROW(accessor->rollbackUpdateZone(), DataSourceError);
+}
+
+TEST_F(SQLite3Update, addRecord) {
+    // Before update, there should be no record for this name
+    checkRecords(*accessor, zone_id, "newdata.example.com.", empty_stored);
+
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+    copy(new_data, new_data + DatabaseAccessor::ADD_COLUMN_COUNT,
+         add_columns);
+    accessor->addRecordToZone(add_columns);
+
+    expected_stored.clear();
+    expected_stored.push_back(new_data);
+    checkRecords(*accessor, zone_id, "newdata.example.com.", expected_stored);
+
+    // Commit the change, and confirm the new data is still there.
+    accessor->commitUpdateZone();
+    checkRecords(*accessor, zone_id, "newdata.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, addThenRollback) {
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+    copy(new_data, new_data + DatabaseAccessor::ADD_COLUMN_COUNT,
+         add_columns);
+    accessor->addRecordToZone(add_columns);
+
+    expected_stored.clear();
+    expected_stored.push_back(new_data);
+    checkRecords(*accessor, zone_id, "newdata.example.com.", expected_stored);
+
+    accessor->rollbackUpdateZone();
+    checkRecords(*accessor, 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(*accessor, 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.
+    copy(dup_data, dup_data + DatabaseAccessor::ADD_COLUMN_COUNT,
+         add_columns);
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+    accessor->addRecordToZone(add_columns);
+    expected_stored.push_back(dup_data);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, invalidAdd) {
+    // An attempt of add before an explicit start of transaction
+    EXPECT_THROW(accessor->addRecordToZone(add_columns), DataSourceError);
+}
+
+TEST_F(SQLite3Update, deleteRecord) {
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+
+    copy(deleted_data, deleted_data + DatabaseAccessor::DEL_PARAM_COUNT,
+         del_params);
+    accessor->deleteRecordInZone(del_params);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Commit the change, and confirm the deleted data still isn't there.
+    accessor->commitUpdateZone();
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+}
+
+TEST_F(SQLite3Update, deleteThenRollback) {
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+
+    copy(deleted_data, deleted_data + DatabaseAccessor::DEL_PARAM_COUNT,
+         del_params);
+    accessor->deleteRecordInZone(del_params);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", empty_stored);
+
+    // Rollback the change, and confirm the data still exists.
+    accessor->rollbackUpdateZone();
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, deleteNonexistent) {
+    zone_id = accessor->startUpdateZone("example.com.", false).second;
+    copy(deleted_data, deleted_data + DatabaseAccessor::DEL_PARAM_COUNT,
+         del_params);
+
+    // Replace the name with a non existent one, then try to delete it.
+    // nothing should happen.
+    del_params[DatabaseAccessor::DEL_NAME] = "no-such-name.example.com.";
+    checkRecords(*accessor, zone_id, "no-such-name.example.com.",
+                 empty_stored);
+    accessor->deleteRecordInZone(del_params);
+    checkRecords(*accessor, 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.
+    copy(deleted_data, deleted_data + DatabaseAccessor::DEL_PARAM_COUNT,
+         del_params);
+    del_params[DatabaseAccessor::DEL_TYPE] = "AAAA";
+    accessor->deleteRecordInZone(del_params);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+
+    // Similar to the previous case, but RDATA is different.
+    copy(deleted_data, deleted_data + DatabaseAccessor::DEL_PARAM_COUNT,
+         del_params);
+    del_params[DatabaseAccessor::DEL_RDATA] = "192.0.2.2";
+    accessor->deleteRecordInZone(del_params);
+    checkRecords(*accessor, zone_id, "foo.bar.example.com.", expected_stored);
+}
+
+TEST_F(SQLite3Update, invalidDelete) {
+    // An attempt of delete before an explicit start of transaction
+    EXPECT_THROW(accessor->deleteRecordInZone(del_params), DataSourceError);
+}
 } // end anonymous namespace

+ 1 - 0
src/lib/datasrc/tests/testdata/Makefile.am

@@ -0,0 +1 @@
+CLEANFILES = *.copied