From: JINMEI Tatuya Date: Thu, 25 Aug 2011 20:14:08 +0000 (-0700) Subject: [master] Merge branch 'trac1068review1' X-Git-Tag: perftcpdns_before_epoll~191^2~17 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e2ada81cd2a090f707147abdb73a90d44db2f2b0;p=thirdparty%2Fkea.git [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 --- e2ada81cd2a090f707147abdb73a90d44db2f2b0 diff --cc src/lib/datasrc/database.cc index 347e3abe47,1a073ed324..d9b5458506 --- a/src/lib/datasrc/database.cc +++ b/src/lib/datasrc/database.cc @@@ -49,7 -49,7 +49,7 @@@ DatabaseClient::DatabaseClient(boost::s DataSourceClient::FindResult DatabaseClient::findZone(const Name& name) const { - std::pair zone(database_->getZone(name.toText())); - std::pair zone(accessor_->getZone(name)); ++ std::pair zone(accessor_->getZone(name.toText())); // Try exact first if (zone.first) { return (FindResult(result::SUCCESS, @@@ -60,10 -60,10 +60,10 @@@ // 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); ++ zone = accessor_->getZone(superdomain.toText()); if (zone.first) { return (FindResult(result::PARTIALMATCH, - ZoneFinderPtr(new Finder(database_, + ZoneFinderPtr(new Finder(accessor_, zone.second, superdomain)))); } @@@ -184,7 -182,7 +183,7 @@@ DatabaseClient::Finder::getRRset(const // Request the context DatabaseAccessor::IteratorContextPtr - context(database_->getRecords(name.toText(), zone_id_)); - context(accessor_->getRecords(name, 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 " + @@@ -229,10 -224,9 +228,10 @@@ isc_throw(DataSourceError, "NS found together with data" " in non-apex domain " + name.toText()); } - addOrCreate(result_rrset, name, getClass(), cur_type, cur_ttl, + 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()) { @@@ -243,10 -237,9 +242,10 @@@ isc_throw(DataSourceError, "NS found together with data" " in non-apex domain " + name.toText()); } - addOrCreate(result_rrset, name, getClass(), cur_type, cur_ttl, + 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. @@@ -254,10 -247,9 +253,10 @@@ isc_throw(DataSourceError, "CNAME found but it is not " "the only record for " + name.toText()); } - addOrCreate(result_rrset, name, getClass(), cur_type, cur_ttl, + 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 && @@@ -265,10 -257,9 +264,10 @@@ isc_throw(DataSourceError, "DNAME with multiple RRs in " + name.toText()); } - addOrCreate(result_rrset, name, getClass(), cur_type, cur_ttl, + 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... @@@ -300,20 -291,6 +299,20 @@@ return (std::pair(records_found, result_rrset)); } +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); + } + + std::string columns[DatabaseAccessor::COLUMN_COUNT]; + return (context->getNext(columns)); +} + ZoneFinder::FindResult DatabaseClient::Finder::find(const isc::dns::Name& name, const isc::dns::RRType& type, @@@ -328,19 -305,14 +327,19 @@@ ZoneFinder::Result result_status = SUCCESS; std::pair 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; // First, do we have any kind of delegation (NS/DNAME) here? - const Name origin(getOrigin()); - const size_t origin_label_count(origin.getLabelCount()); - const size_t current_label_count(name.getLabelCount()); + Name origin(getOrigin()); + size_t origin_label_count(origin.getLabelCount()); + // Number of labels in the last known non-empty domain + size_t last_known(origin_label_count); + size_t current_label_count(name.getLabelCount()); // This is how many labels we remove to get origin - const size_t remove_labels(current_label_count - origin_label_count); + size_t remove_labels(current_label_count - origin_label_count); // Now go trough all superdomains from origin down for (int i(remove_labels); i > 0; --i) { @@@ -395,77 -357,6 +394,77 @@@ result_rrset->getType() == isc::dns::RRType::CNAME()) { result_status = CNAME; } + + if (!result_rrset && !records_found) { + // Nothing lives here. + // But check if something lives below this + // domain and if so, pretend something is here as well. + 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. + // We remove labels one by one and look for the wildcard there. + // Go up to first non-empty domain. + + remove_labels = current_label_count - last_known; + Name star("*"); + for (size_t i(1); i <= remove_labels; ++ i) { + // Construct the name with * + // TODO: Once the underlying DatabaseAccessor takes + // string, do the concatenation on strings, not + // Names + Name superdomain(name.split(i)); + Name wildcard(star.concatenate(superdomain)); + // TODO What do we do about DNAME here? + found = getRRset(wildcard, &type, true, false, true, + &name); + if (found.first) { + if (first_ns) { + // In case we are under NS, we don't + // wildcard-match, but return delegation + result_rrset = first_ns; + result_status = DELEGATION; + records_found = true; + // We pretend to switch to non-glue_ok mode + 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())) + { + // Nothing we added as part of the * can exist + // directly, as we go up only to first existing + // domain, but it could be empty non-terminal. In + // that case, we need to cancel the match. + records_found = true; + 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; + } else if (hasSubdomains(wildcard.toText())) { + // Empty non-terminal asterisk + 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; + } + } + } + } } if (!result_rrset) { @@@ -580,7 -471,7 +579,7 @@@ private ZoneIteratorPtr DatabaseClient::getIterator(const isc::dns::Name& name) const { // Get the zone - std::pair zone(database_->getZone(name.toText())); - std::pair zone(accessor_->getZone(name)); ++ std::pair zone(accessor_->getZone(name.toText())); if (!zone.first) { // No such zone, can't continue isc_throw(DataSourceError, "Zone " + name.toText() + diff --cc src/lib/datasrc/database.h index 1120a3e6a5,efa370663a..d9e23438ee --- a/src/lib/datasrc/database.h +++ b/src/lib/datasrc/database.h @@@ -65,11 -68,41 +65,43 @@@ public ///< the RRSIG covers. In the current implementation, ///< this field is ignored. RDATA_COLUMN = 3, ///< Full text representation of the record's RDATA - NAME_COLUMN = 4 ///< The domain name of this RR + NAME_COLUMN = 4, ///< The domain name of this RR + COLUMN_COUNT = 5 ///< The total number of columns, MUST be value of + ///< the largest other element in this enum plus 1. }; + /** + * 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 * @@@ -203,8 -238,203 +235,191 @@@ * \param id The ID of the zone, returned from getZone(). * \return Newly created iterator context. Must not be NULL. */ - virtual IteratorContextPtr getAllRecords(int id) const - { - /* - * This is a compromise. We need to document the parameters in doxygen, - * so they need a name, but then it complains about unused parameter. - * This is a NOP that "uses" the parameters. - */ - static_cast(id); - - isc_throw(isc::NotImplemented, - "This database datasource can't be iterated"); - } + 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 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 * @@@ -395,19 -623,9 +611,20 @@@ public type, bool want_cname, bool want_dname, - bool want_ns); + bool want_ns, const + isc::dns::Name* + construct_name = NULL); + /** + * \brief Checks if something lives below this domain. + * + * This looks if there's any subdomain of the given name. It can be + * used to test if domain is empty non-terminal. + * + * \param name The domain to check. + */ + bool hasSubdomains(const std::string& name); }; + /** * \brief Find a zone in the database * diff --cc src/lib/datasrc/sqlite3_accessor.cc index 3cbaeabdd0,b87ab2dc0e..ee81655cf2 --- a/src/lib/datasrc/sqlite3_accessor.cc +++ b/src/lib/datasrc/sqlite3_accessor.cc @@@ -19,24 -24,102 +24,108 @@@ #include #include - #include + 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, - BEGIN = 2, - COMMIT = 3, - ROLLBACK = 4, - DEL_ZONE_RECORDS = 5, - ADD_RECORD = 6, - DEL_RECORD = 7, - ITERATE = 8, - NUM_STATEMENTS = 9 ++ 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()), @@@ -181,101 -192,33 +198,95 @@@ prepare(sqlite3* const db, const char* return (prepared); } -void -checkAndSetupSchema(Initializer* initializer) { - sqlite3* const db = initializer->params_.db_; +// 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; + nanosleep(&req, NULL); +} +// 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; - if (sqlite3_prepare_v2(db, "SELECT version FROM schema_version", -1, - &prepared, NULL) == SQLITE_OK && - sqlite3_step(prepared) == SQLITE_ROW) { - initializer->params_.version_ = sqlite3_column_int(prepared, 0); - sqlite3_finalize(prepared); - } else { - logger.info(DATASRC_SQLITE_SETUP); - if (prepared != NULL) { - sqlite3_finalize(prepared); + // 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 + return (-1); + } else if (rc == SQLITE_OK) { + break; + } else if (rc != SQLITE_BUSY || i == 50) { + isc_throw(SQLite3Error, "Unable to prepare version query: " + << rc << " " << sqlite3_errmsg(db)); + } - do_sleep(); ++ doSleep(); + } + if (sqlite3_step(prepared) != SQLITE_ROW) { + isc_throw(SQLite3Error, + "Unable to query version: " << sqlite3_errmsg(db)); + } + int version = sqlite3_column_int(prepared, 0); + sqlite3_finalize(prepared); + return (version); +} + +// return db version +int create_database(sqlite3* db) { + // try to get an exclusive lock. Once that is obtained, do the version + // check *again*, just in case this process was racing another + // + // try for 5 secs (50*0.1) + int rc; + logger.info(DATASRC_SQLITE_SETUP); + for (size_t i = 0; i < 50; ++i) { + rc = sqlite3_exec(db, "BEGIN EXCLUSIVE TRANSACTION", NULL, NULL, + NULL); + if (rc == SQLITE_OK) { + break; + } else if (rc != SQLITE_BUSY || i == 50) { + 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) != SQLITE_OK) { isc_throw(SQLite3Error, - "Failed to set up schema " << SCHEMA_LIST[i]); + "Failed to set up schema " << SCHEMA_LIST[i]); } } + sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, NULL); + return (SQLITE_SCHEMA_VERSION); + } else { + return (schema_version); + } +} + +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]); + } } } @@@ -345,8 -267,14 +335,9 @@@ SQLite3Accessor::close(void) } std::pair - SQLite3Database::getZone(const std::string& name) const { -SQLite3Accessor::getZone(const isc::dns::Name& name) const { - return (getZone(name.toText())); -} - -std::pair -SQLite3Accessor::getZone(const 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) @@@ -385,35 -309,63 +372,38 @@@ return (std::pair(false, 0)); } - class SQLite3Database::Context : public DatabaseAccessor::IteratorContext { -namespace { -// This helper function converts from the unsigned char* type (used by -// sqlite3) to char* (wanted by std::string). Technically these types -// might not be directly convertable -// In case sqlite3_column_text() returns NULL, we just make it an -// empty string. -// The sqlite3parameters value is only used to check the error code if -// ucp == NULL -const char* -convertToPlainChar(const unsigned char* ucp, - SQLite3Parameters* dbparameters) { - if (ucp == NULL) { - // 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 (dbparameters != NULL && - sqlite3_errcode(dbparameters->db_) == SQLITE_NOMEM) { - isc_throw(DataSourceError, - "Sqlite3 backend encountered a memory allocation " - "error in sqlite3_column_text()"); - } else { - return (""); - } - } - const void* p = ucp; - return (static_cast(p)); -} -} - + 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& database, int id) : + Context(const boost::shared_ptr& accessor, int id) : iterator_type_(ITT_ALL), - database_(database), + accessor_(accessor), - statement_(NULL) + 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& database, int id, + Context(const boost::shared_ptr& accessor, int id, - const isc::dns::Name& name) : + const std::string& name, bool subdomains) : iterator_type_(ITT_NAME), - database_(database), + accessor_(accessor), - statement_(NULL) + 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); + bindName(name_); } bool getNext(std::string (&data)[COLUMN_COUNT]) { @@@ -438,9 -385,8 +428,9 @@@ } 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); } @@@ -464,63 -411,30 +454,63 @@@ private void bindZoneId(const int zone_id) { if (sqlite3_bind_int(statement_, 1, zone_id) != SQLITE_OK) { + 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 isc::dns::Name& name) { - if (sqlite3_bind_text(statement_, 2, name.toText().c_str(), -1, + 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_); - sqlite3_finalize(statement_); + finalize(); isc_throw(SQLite3Error, "Could not bind text '" << name << "' to SQL statement: " << errmsg); } } + void finalize() { + sqlite3_finalize(statement_); + statement_ = NULL; + } + + // This helper method converts from the unsigned char* type (used by + // sqlite3) to char* (wanted by std::string). Technically these types + // might not be directly convertable + // In case sqlite3_column_text() returns NULL, we just make it an + // empty string, unless it was caused by a memory error + const char* convertToPlainChar(const unsigned char* ucp) { + if (ucp == NULL) { + // 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 " + "error in sqlite3_column_text()"); + } else { + return (""); + } + } + const void* p = ucp; + return (static_cast(p)); + } + const IteratorType iterator_type_; - boost::shared_ptr database_; + boost::shared_ptr accessor_; sqlite3_stmt *statement_; + const std::string name_; }; DatabaseAccessor::IteratorContextPtr - SQLite3Database::getRecords(const std::string& name, int id, -SQLite3Accessor::getRecords(const isc::dns::Name& name, int id) const { - return (IteratorContextPtr(new Context(shared_from_this(), id, name))); ++SQLite3Accessor::getRecords(const std::string& name, int id, + bool subdomains) const +{ + return (IteratorContextPtr(new Context(shared_from_this(), id, name, + subdomains))); } DatabaseAccessor::IteratorContextPtr diff --cc src/lib/datasrc/tests/sqlite3_accessor_unittest.cc index b91f3aa337,5725c81219..c9db3f146f --- a/src/lib/datasrc/tests/sqlite3_accessor_unittest.cc +++ b/src/lib/datasrc/tests/sqlite3_accessor_unittest.cc @@@ -19,10 -23,10 +23,12 @@@ #include #include +#include +#include + using namespace std; using namespace isc::datasrc; + using boost::shared_ptr; using isc::data::ConstElementPtr; using isc::data::Element; using isc::dns::RRClass; @@@ -45,14 -48,10 +51,14 @@@ std::string SQLITE_DBFILE_EXAMPLE_ORG // The "nodir", a non existent directory, is inserted for this purpose. 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 @@@ -88,117 -87,62 +94,116 @@@ public }; // This zone exists in the data, so it should be found - TEST_F(SQLite3Access, getZone) { - std::pair result(db->getZone("example.com.")); + TEST_F(SQLite3AccessorTest, getZone) { - std::pair result(accessor->getZone(Name("example.com"))); ++ std::pair 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(Name("sub.example.com")).first); ++ 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(Name("example.org")).first); ++ 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(Name("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_EXAMPLE2, RRClass::IN()); + initAccessor(SQLITE_DBFILE_EXAMPLE_ORG, RRClass::IN()); + - const std::pair zone_info(db->getZone("example.org.")); ++ const std::pair zone_info(accessor->getZone("example.org.")); + ASSERT_TRUE(zone_info.first); // Get the iterator context - DatabaseAccessor::IteratorContextPtr context(accessor->getAllRecords(1)); + DatabaseAccessor::IteratorContextPtr - context(db->getAllRecords(zone_info.second)); - ASSERT_NE(DatabaseAccessor::IteratorContextPtr(), - context); ++ context(accessor->getAllRecords(zone_info.second)); + ASSERT_NE(DatabaseAccessor::IteratorContextPtr(), context); - const size_t size(5); - std::string data[size]; + std::string data[DatabaseAccessor::COLUMN_COUNT]; // Get and check the first and only record EXPECT_TRUE(context->getNext(data)); - EXPECT_EQ("example2.com.", data[4]); - EXPECT_EQ("SOA", data[0]); - EXPECT_EQ("master.example2.com. admin.example2.com. " - "1234 3600 1800 2419200 7200", data[3]); - EXPECT_EQ("3600", data[1]); - // Check there's no other - EXPECT_FALSE(context->getNext(data)); -} + EXPECT_EQ("DNAME", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("dname.example.info.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("dname.example.org.", data[DatabaseAccessor::NAME_COLUMN]); -TEST_F(SQLite3AccessorTest, iteratorColumnCount) { - // Our test zone is conveniently small, but not empty - initAccessor(SQLITE_DBFILE_EXAMPLE2, RRClass::IN()); + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("DNAME", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("dname2.example.info.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("dname2.foo.example.org.", data[DatabaseAccessor::NAME_COLUMN]); - // Get the iterator context - DatabaseAccessor::IteratorContextPtr - context(accessor->getAllRecords(1)); - ASSERT_NE(DatabaseAccessor::IteratorContextPtr(), - context); + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("MX", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("10 mail.example.org.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("example.org.", data[DatabaseAccessor::NAME_COLUMN]); - std::string data[DatabaseAccessor::COLUMN_COUNT]; - EXPECT_NO_THROW(context->getNext(data)); + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("NS", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("ns1.example.org.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("NS", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("ns2.example.org.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("NS", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("ns3.example.org.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("SOA", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("ns1.example.org. admin.example.org. " + "1234 3600 1800 2419200 7200", + data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("A", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("192.0.2.10", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("mail.example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("A", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("192.0.2.101", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("ns.sub.example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("NS", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("ns.sub.example.org.", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("sub.example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + EXPECT_TRUE(context->getNext(data)); + EXPECT_EQ("A", data[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ("3600", data[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ("192.0.2.1", data[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ("www.example.org.", data[DatabaseAccessor::NAME_COLUMN]); + + // Check there's no other + EXPECT_FALSE(context->getNext(data)); + + // And make sure calling it again won't cause problems. + EXPECT_FALSE(context->getNext(data)); } TEST(SQLite3Open, getDBNameExample2) { @@@ -221,31 -165,33 +226,31 @@@ checkRecordRow(const std::string column const std::string& field3, const std::string& field4) { - EXPECT_EQ(field0, columns[0]); - EXPECT_EQ(field1, columns[1]); - EXPECT_EQ(field2, columns[2]); - EXPECT_EQ(field3, columns[3]); - EXPECT_EQ(field4, columns[4]); + EXPECT_EQ(field0, columns[DatabaseAccessor::TYPE_COLUMN]); + EXPECT_EQ(field1, columns[DatabaseAccessor::TTL_COLUMN]); + EXPECT_EQ(field2, columns[DatabaseAccessor::SIGTYPE_COLUMN]); + EXPECT_EQ(field3, columns[DatabaseAccessor::RDATA_COLUMN]); + EXPECT_EQ(field4, columns[DatabaseAccessor::NAME_COLUMN]); } - TEST_F(SQLite3Access, getRecords) { - const std::pair zone_info(db->getZone("example.com.")); + TEST_F(SQLite3AccessorTest, getRecords) { - const std::pair zone_info( - accessor->getZone(Name("example.com"))); ++ const std::pair zone_info(accessor->getZone("example.com.")); ASSERT_TRUE(zone_info.first); const int zone_id = zone_info.second; ASSERT_EQ(1, zone_id); - const size_t column_count = DatabaseAccessor::COLUMN_COUNT; - std::string columns[column_count]; + std::string columns[DatabaseAccessor::COLUMN_COUNT]; DatabaseAccessor::IteratorContextPtr - context(db->getRecords("foo.bar", 1)); - context(accessor->getRecords(Name("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(Name("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.", ""); @@@ -266,7 -213,7 +272,7 @@@ "NSEC 5 3 7200 20100322084538 20100220084538 33495 " "example.com. FAKEFAKEFAKEFAKE", ""); - context = db->getRecords("example.com.", zone_id); - context = accessor->getRecords(Name("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. " @@@ -330,78 -277,293 +336,365 @@@ checkRecordRow(columns, "RRSIG", "3600", "DNSKEY", "DNSKEY 5 2 3600 20100322084538 20100220084538 " "33495 example.com. FAKEFAKEFAKEFAKE", ""); + + // check that another getNext does not cause problems + EXPECT_FALSE(context->getNext(columns)); + + // 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)); +} + +// Test fixture for creating a db that automatically deletes it before start, +// and when done +class SQLite3Create : public ::testing::Test { +public: + SQLite3Create() { + remove(SQLITE_NEW_DBFILE); + } + + ~SQLite3Create() { + remove(SQLITE_NEW_DBFILE); + } +}; + +bool exists(const char* filename) { + std::ifstream f(filename); + return (f != NULL); +} + +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)); +} + +TEST_F(SQLite3Create, emptytest) { + ASSERT_FALSE(exists(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)); + + // open one manually + sqlite3* db; + ASSERT_EQ(SQLITE_OK, sqlite3_open(SQLITE_NEW_DBFILE, &db)); + 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(Name("example.com")).second; ++ 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 expected_stored; // placeholder for checkRecords + vector empty_stored; // indicate no corresponding data + + // Another accessor, emulating one running on a different process/thread + shared_ptr another_accessor; + DatabaseAccessor::IteratorContextPtr iterator; + }; + + void + checkRecords(SQLite3Accessor& accessor, int zone_id, const std::string& name, + vector expected_rows) + { + DatabaseAccessor::IteratorContextPtr iterator = - accessor.getRecords(Name(name), zone_id); ++ accessor.getRecords(name, zone_id); + std::string columns[DatabaseAccessor::COLUMN_COUNT]; + vector::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(Name("example.com"), zone_id); ++ 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(Name("foo.example.com"), zone_id); ++ 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