BIND 10 master, updated. 255bf5b18e2b0e28a65062e87dc2d1212376bfc2 [master] rename exists() to isReadable() to better represent the intent. also use is_open() instead of != NULL (it's strange that the original code did even compile, but in any case this one should be better in clarity and possibly in portability). okayed on jabber, should be minor enough, directly pushing.
BIND 10 source code commits
bind10-changes at lists.isc.org
Thu Aug 25 20:17:47 UTC 2011
The branch, master has been updated
via 255bf5b18e2b0e28a65062e87dc2d1212376bfc2 (commit)
via e2ada81cd2a090f707147abdb73a90d44db2f2b0 (commit)
via 0953b3f5d7ed1b4a25362f9a2d1a41eeeda8efa6 (commit)
via df047c5ccb5c81f9a3d36f7fc38a19bc7c8f2ac2 (commit)
via a7346d50ae5389ce37e35a7131f0f218663b8c68 (commit)
via ad91831c938430b6d4a8fd7bfae517a0f1e327c1 (commit)
via ad36c799ff07d47ebd5c861c63e9feef50408e34 (commit)
via c5e0ebf85ef50e61457f3b99a05109a92b328573 (commit)
via 8216d5dbe1ef23d56ba589fe1de619a601bada4b (commit)
via 1c834de994f51a1fb98add648dad49abfea2c403 (commit)
via dc9318330acbd36e07ad5a4e8a68c9a6e2430543 (commit)
via e4a17a0630a6460090c5cdb562e02ba992a74fa8 (commit)
via 1351cb42c92cd415003adf6234d96507c8a6d2db (commit)
via 575bd3ec2fe918257cb448eee8ebbff269d85431 (commit)
from 30df43575158b0cb294ec49a8463fe8b49593e62 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 255bf5b18e2b0e28a65062e87dc2d1212376bfc2
Author: JINMEI Tatuya <jinmei at isc.org>
Date: Thu Aug 25 13:16:09 2011 -0700
[master] rename exists() to isReadable() to better represent the intent.
also use is_open() instead of != NULL (it's strange that the original code
did even compile, but in any case this one should be better in clarity and
possibly in portability).
okayed on jabber, should be minor enough, directly pushing.
commit e2ada81cd2a090f707147abdb73a90d44db2f2b0
Merge: 30df435 0953b3f
Author: JINMEI Tatuya <jinmei at isc.org>
Date: Thu Aug 25 13:14:08 2011 -0700
[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
-----------------------------------------------------------------------
Summary of changes:
configure.ac | 1 +
src/lib/datasrc/database.cc | 61 ++--
src/lib/datasrc/database.h | 231 ++++++++++-
src/lib/datasrc/sqlite3_accessor.cc | 427 ++++++++++++--------
src/lib/datasrc/sqlite3_accessor.h | 41 ++-
src/lib/datasrc/tests/Makefile.am | 7 +-
src/lib/datasrc/tests/database_unittest.cc | 20 +-
src/lib/datasrc/tests/sqlite3_accessor_unittest.cc | 392 ++++++++++++++++---
src/lib/datasrc/tests/testdata/Makefile.am | 1 +
9 files changed, 921 insertions(+), 260 deletions(-)
create mode 100644 src/lib/datasrc/tests/testdata/Makefile.am
-----------------------------------------------------------------------
diff --git a/configure.ac b/configure.ac
index 6e129b6..6999a21 100644
--- a/configure.ac
+++ b/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
diff --git a/src/lib/datasrc/database.cc b/src/lib/datasrc/database.cc
index 347e3ab..d9b5458 100644
--- a/src/lib/datasrc/database.cc
+++ b/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 " +
diff --git a/src/lib/datasrc/database.h b/src/lib/datasrc/database.h
index 1120a3e..d9e2343 100644
--- a/src/lib/datasrc/database.h
+++ b/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 (¶ms)[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_;
};
}
diff --git a/src/lib/datasrc/sqlite3_accessor.cc b/src/lib/datasrc/sqlite3_accessor.cc
index 3cbaeab..ee81655 100644
--- a/src/lib/datasrc/sqlite3_accessor.cc
+++ b/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 (¶ms)[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");
+}
+
}
}
diff --git a/src/lib/datasrc/sqlite3_accessor.h b/src/lib/datasrc/sqlite3_accessor.h
index bcbf358..8f993c1 100644
--- a/src/lib/datasrc/sqlite3_accessor.h
+++ b/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 (¶ms)[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:
diff --git a/src/lib/datasrc/tests/Makefile.am b/src/lib/datasrc/tests/Makefile.am
index eb0e3ad..a1ef054 100644
--- a/src/lib/datasrc/tests/Makefile.am
+++ b/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)
diff --git a/src/lib/datasrc/tests/database_unittest.cc b/src/lib/datasrc/tests/database_unittest.cc
index 13bda70..1fb79bc 100644
--- a/src/lib/datasrc/tests/database_unittest.cc
+++ b/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),
diff --git a/src/lib/datasrc/tests/sqlite3_accessor_unittest.cc b/src/lib/datasrc/tests/sqlite3_accessor_unittest.cc
index b91f3aa..427ee71 100644
--- a/src/lib/datasrc/tests/sqlite3_accessor_unittest.cc
+++ b/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));
}
@@ -358,36 +364,35 @@ public:
}
};
-bool exists(const char* filename) {
- std::ifstream f(filename);
- return (f != NULL);
+bool isReadable(const char* filename) {
+ return (std::ifstream(filename).is_open());
}
TEST_F(SQLite3Create, creationtest) {
- ASSERT_FALSE(exists(SQLITE_NEW_DBFILE));
+ ASSERT_FALSE(isReadable(SQLITE_NEW_DBFILE));
// Should simply be created
- SQLite3Database db(SQLITE_NEW_DBFILE, RRClass::IN());
- ASSERT_TRUE(exists(SQLITE_NEW_DBFILE));
+ SQLite3Accessor accessor(SQLITE_NEW_DBFILE, RRClass::IN());
+ ASSERT_TRUE(isReadable(SQLITE_NEW_DBFILE));
}
TEST_F(SQLite3Create, emptytest) {
- ASSERT_FALSE(exists(SQLITE_NEW_DBFILE));
+ ASSERT_FALSE(isReadable(SQLITE_NEW_DBFILE));
// open one manualle
sqlite3* db;
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) {
- ASSERT_FALSE(exists(SQLITE_NEW_DBFILE));
+ ASSERT_FALSE(isReadable(SQLITE_NEW_DBFILE));
// open one manually
sqlite3* db;
@@ -395,13 +400,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
diff --git a/src/lib/datasrc/tests/testdata/Makefile.am b/src/lib/datasrc/tests/testdata/Makefile.am
new file mode 100644
index 0000000..6a35fe3
--- /dev/null
+++ b/src/lib/datasrc/tests/testdata/Makefile.am
@@ -0,0 +1 @@
+CLEANFILES = *.copied
More information about the bind10-changes
mailing list