|
@@ -14,19 +14,33 @@
|
|
|
|
|
|
#include <string>
|
|
|
#include <sstream>
|
|
|
+#include <utility>
|
|
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
|
#include <datasrc/sqlite3_datasrc.h>
|
|
|
#include <datasrc/logger.h>
|
|
|
-
|
|
|
+#include <exceptions/exceptions.h>
|
|
|
#include <dns/rrttl.h>
|
|
|
#include <dns/rdata.h>
|
|
|
#include <dns/rdataclass.h>
|
|
|
#include <dns/rrset.h>
|
|
|
#include <dns/rrsetlist.h>
|
|
|
|
|
|
-#define SQLITE_SCHEMA_VERSION 1
|
|
|
+namespace {
|
|
|
+// Expected schema. The major version must match else there is an error. If
|
|
|
+// the minor version of the database is less than this, a warning is output.
|
|
|
+//
|
|
|
+// It is assumed that a program written to run on m.n of the database will run
|
|
|
+// with a database version m.p, where p is any number. However, if p < n,
|
|
|
+// we assume that the database structure was upgraded for some reason, and that
|
|
|
+// some advantage may result if the database is upgraded. Conversely, if p > n,
|
|
|
+// The database is at a later version than the program was written for and the
|
|
|
+// program may not be taking advantage of features (possibly performance
|
|
|
+// improvements) added to the database.
|
|
|
+const int SQLITE_SCHEMA_MAJOR_VERSION = 2;
|
|
|
+const int SQLITE_SCHEMA_MINOR_VERSION = 0;
|
|
|
+}
|
|
|
|
|
|
using namespace std;
|
|
|
using namespace isc::dns;
|
|
@@ -36,13 +50,14 @@ namespace isc {
|
|
|
namespace datasrc {
|
|
|
|
|
|
struct Sqlite3Parameters {
|
|
|
- Sqlite3Parameters() : db_(NULL), version_(-1),
|
|
|
+ Sqlite3Parameters() : db_(NULL), major_version_(-1), minor_version_(-1),
|
|
|
q_zone_(NULL), q_record_(NULL), q_addrs_(NULL), q_referral_(NULL),
|
|
|
q_any_(NULL), q_count_(NULL), q_previous_(NULL), q_nsec3_(NULL),
|
|
|
q_prevnsec3_(NULL)
|
|
|
{}
|
|
|
sqlite3* db_;
|
|
|
- int version_;
|
|
|
+ int major_version_;
|
|
|
+ int minor_version_;
|
|
|
sqlite3_stmt* q_zone_;
|
|
|
sqlite3_stmt* q_record_;
|
|
|
sqlite3_stmt* q_addrs_;
|
|
@@ -56,38 +71,41 @@ struct Sqlite3Parameters {
|
|
|
|
|
|
namespace {
|
|
|
const char* const SCHEMA_LIST[] = {
|
|
|
- "CREATE TABLE schema_version (version INTEGER NOT NULL)",
|
|
|
- "INSERT INTO schema_version VALUES (1)",
|
|
|
+ "CREATE TABLE schema_version (version INTEGER NOT NULL, "
|
|
|
+ "minor INTEGER NOT NULL DEFAULT 0)",
|
|
|
+ "INSERT INTO schema_version VALUES (2, 0)",
|
|
|
"CREATE TABLE zones (id INTEGER PRIMARY KEY, "
|
|
|
- "name STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "rdclass STRING NOT NULL COLLATE NOCASE DEFAULT 'IN', "
|
|
|
+ "name TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "rdclass TEXT NOT NULL COLLATE NOCASE DEFAULT 'IN', "
|
|
|
"dnssec BOOLEAN NOT NULL DEFAULT 0)",
|
|
|
"CREATE INDEX zones_byname ON zones (name)",
|
|
|
"CREATE TABLE records (id INTEGER PRIMARY KEY, "
|
|
|
- "zone_id INTEGER NOT NULL, name STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "rname STRING NOT NULL COLLATE NOCASE, ttl INTEGER NOT NULL, "
|
|
|
- "rdtype STRING NOT NULL COLLATE NOCASE, sigtype STRING COLLATE NOCASE, "
|
|
|
- "rdata STRING NOT NULL)",
|
|
|
+ "zone_id INTEGER NOT NULL, name TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "rname TEXT NOT NULL COLLATE NOCASE, ttl INTEGER NOT NULL, "
|
|
|
+ "rdtype TEXT NOT NULL COLLATE NOCASE, sigtype TEXT COLLATE NOCASE, "
|
|
|
+ "rdata TEXT NOT NULL)",
|
|
|
"CREATE INDEX records_byname ON records (name)",
|
|
|
"CREATE INDEX records_byrname ON records (rname)",
|
|
|
+ "CREATE INDEX records_bytype_and_rname ON records (rdtype, rname)",
|
|
|
"CREATE TABLE nsec3 (id INTEGER PRIMARY KEY, zone_id INTEGER NOT NULL, "
|
|
|
- "hash STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "owner STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "ttl INTEGER NOT NULL, rdtype STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "rdata STRING NOT NULL)",
|
|
|
+ "hash TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "owner TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "ttl INTEGER NOT NULL, rdtype TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "rdata TEXT NOT NULL)",
|
|
|
"CREATE INDEX nsec3_byhash ON nsec3 (hash)",
|
|
|
"CREATE TABLE diffs (id INTEGER PRIMARY KEY, "
|
|
|
"zone_id INTEGER NOT NULL, "
|
|
|
"version INTEGER NOT NULL, "
|
|
|
"operation INTEGER NOT NULL, "
|
|
|
- "name STRING NOT NULL COLLATE NOCASE, "
|
|
|
- "rrtype STRING NOT NULL COLLATE NOCASE, "
|
|
|
+ "name TEXT NOT NULL COLLATE NOCASE, "
|
|
|
+ "rrtype TEXT NOT NULL COLLATE NOCASE, "
|
|
|
"ttl INTEGER NOT NULL, "
|
|
|
- "rdata STRING NOT NULL)",
|
|
|
+ "rdata TEXT NOT NULL)",
|
|
|
NULL
|
|
|
};
|
|
|
|
|
|
const char* const q_version_str = "SELECT version FROM schema_version";
|
|
|
+const char* const q_minor_str = "SELECT minor FROM schema_version";
|
|
|
|
|
|
const char* const q_zone_str = "SELECT id FROM zones WHERE name=?1";
|
|
|
|
|
@@ -109,12 +127,16 @@ const char* const q_referral_str = "SELECT rdtype, ttl, sigtype, rdata FROM "
|
|
|
const char* const q_any_str = "SELECT rdtype, ttl, sigtype, rdata "
|
|
|
"FROM records WHERE zone_id=?1 AND name=?2";
|
|
|
|
|
|
+// Note: the wildcard symbol '%' is expected to be added to the text
|
|
|
+// for the placeholder for LIKE given via sqlite3_bind_text(). We don't
|
|
|
+// use the expression such as (?2 || '%') because it would disable the use
|
|
|
+// of indices and could result in terrible performance.
|
|
|
const char* const q_count_str = "SELECT COUNT(*) FROM records "
|
|
|
- "WHERE zone_id=?1 AND rname LIKE (?2 || '%');";
|
|
|
+ "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";
|
|
|
+ "WHERE rname < ?2 AND zone_id=?1 AND rdtype = 'NSEC' "
|
|
|
+ "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";
|
|
@@ -314,8 +336,9 @@ Sqlite3DataSrc::findRecords(const Name& name, const RRType& rdtype,
|
|
|
" to SQL statement (qcount)");
|
|
|
}
|
|
|
|
|
|
- const string revname_text = name.reverse().toText();
|
|
|
- rc = sqlite3_bind_text(dbparameters->q_count_, 2, revname_text.c_str(),
|
|
|
+ const string revname_text = name.reverse().toText() + "%";
|
|
|
+ rc = sqlite3_bind_text(dbparameters->q_count_, 2,
|
|
|
+ revname_text.c_str(),
|
|
|
-1, SQLITE_STATIC);
|
|
|
if (rc != SQLITE_OK) {
|
|
|
isc_throw(Sqlite3Error, "Could not bind name " << name.reverse() <<
|
|
@@ -675,15 +698,15 @@ void do_sleep() {
|
|
|
nanosleep(&req, NULL);
|
|
|
}
|
|
|
|
|
|
-// returns the schema version if the schema version table exists
|
|
|
+// returns the schema version element if the schema version table exists
|
|
|
// returns -1 if it does not
|
|
|
-int check_schema_version(sqlite3* db) {
|
|
|
+int check_schema_version_element(sqlite3* db, const char* const version_query) {
|
|
|
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, version_query, -1, &prepared, NULL);
|
|
|
if (rc == SQLITE_ERROR) {
|
|
|
// this is the error that is returned when the table does not
|
|
|
// exist
|
|
@@ -705,27 +728,73 @@ int check_schema_version(sqlite3* db) {
|
|
|
return (version);
|
|
|
}
|
|
|
|
|
|
+// Returns the schema major and minor version numbers in a pair.
|
|
|
+// Returns (-1, -1) if the table does not exist, (1, 0) for a V1
|
|
|
+// database, and (n, m) for any other.
|
|
|
+pair<int, int> check_schema_version(sqlite3* db) {
|
|
|
+ int major = check_schema_version_element(db, q_version_str);
|
|
|
+ if (major == -1) {
|
|
|
+ return (make_pair(-1, -1));
|
|
|
+ } else if (major == 1) {
|
|
|
+ return (make_pair(1, 0));
|
|
|
+ } else {
|
|
|
+ int minor = check_schema_version_element(db, q_minor_str);
|
|
|
+ return (make_pair(major, minor));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// A helper class used in create_database() below so we manage the one shot
|
|
|
+// transaction safely.
|
|
|
+class ScopedTransaction {
|
|
|
+public:
|
|
|
+ ScopedTransaction(sqlite3* db) : db_(NULL) {
|
|
|
+ // try for 5 secs (50*0.1)
|
|
|
+ for (size_t i = 0; i < 50; ++i) {
|
|
|
+ const int 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();
|
|
|
+ }
|
|
|
+ // Hold the DB pointer once we have successfully acquired the lock.
|
|
|
+ db_ = db;
|
|
|
+ }
|
|
|
+ ~ScopedTransaction() {
|
|
|
+ if (db_ != NULL) {
|
|
|
+ // Note: even rollback could fail in theory, but in that case
|
|
|
+ // we cannot do much for safe recovery anyway. We could at least
|
|
|
+ // log the event, but for now don't even bother to do that, with
|
|
|
+ // the expectation that we'll soon stop creating the schema in this
|
|
|
+ // module.
|
|
|
+ sqlite3_exec(db_, "ROLLBACK", NULL, NULL, NULL);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ void commit() {
|
|
|
+ if (sqlite3_exec(db_, "COMMIT TRANSACTION", NULL, NULL, NULL) !=
|
|
|
+ SQLITE_OK) {
|
|
|
+ isc_throw(Sqlite3Error, "Unable to commit newly created database "
|
|
|
+ "schema: " << sqlite3_errmsg(db_));
|
|
|
+ }
|
|
|
+ db_ = NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ sqlite3* db_;
|
|
|
+};
|
|
|
+
|
|
|
// return db version
|
|
|
-int create_database(sqlite3* db) {
|
|
|
+pair<int, int> create_database(sqlite3* db) {
|
|
|
+ logger.info(DATASRC_SQLITE_SETUP);
|
|
|
+
|
|
|
// 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();
|
|
|
- }
|
|
|
- int schema_version = check_schema_version(db);
|
|
|
- if (schema_version == -1) {
|
|
|
+ ScopedTransaction transaction(db);
|
|
|
+ pair<int, int> schema_version = check_schema_version(db);
|
|
|
+ if (schema_version.first == -1) {
|
|
|
for (int i = 0; SCHEMA_LIST[i] != NULL; ++i) {
|
|
|
if (sqlite3_exec(db, SCHEMA_LIST[i], NULL, NULL, NULL) !=
|
|
|
SQLITE_OK) {
|
|
@@ -733,23 +802,40 @@ int create_database(sqlite3* db) {
|
|
|
"Failed to set up schema " << SCHEMA_LIST[i]);
|
|
|
}
|
|
|
}
|
|
|
- sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, NULL);
|
|
|
- return (SQLITE_SCHEMA_VERSION);
|
|
|
- } else {
|
|
|
- return (schema_version);
|
|
|
+ transaction.commit();
|
|
|
+
|
|
|
+ // Return the version. We query again to ensure that the only point
|
|
|
+ // in which the current schema version is defined is in the
|
|
|
+ // CREATE statements.
|
|
|
+ schema_version = check_schema_version(db);
|
|
|
}
|
|
|
+ return (schema_version);
|
|
|
}
|
|
|
|
|
|
void
|
|
|
checkAndSetupSchema(Sqlite3Initializer* initializer) {
|
|
|
sqlite3* const db = initializer->params_.db_;
|
|
|
|
|
|
- int schema_version = check_schema_version(db);
|
|
|
- if (schema_version != SQLITE_SCHEMA_VERSION) {
|
|
|
+ // Note: we use the same SCHEMA_xxx_VERSION log IDs here and in
|
|
|
+ // sqlite3_accessor.cc, which is against our policy of ID uniqueness.
|
|
|
+ // The assumption is that this file will soon be deprecated, and we don't
|
|
|
+ // bother to define separate IDs for the short period.
|
|
|
+ pair<int, int> schema_version = check_schema_version(db);
|
|
|
+ if (schema_version.first == -1) {
|
|
|
schema_version = create_database(db);
|
|
|
- }
|
|
|
- initializer->params_.version_ = schema_version;
|
|
|
-
|
|
|
+ } else if (schema_version.first != SQLITE_SCHEMA_MAJOR_VERSION) {
|
|
|
+ LOG_ERROR(logger, DATASRC_SQLITE_INCOMPATIBLE_VERSION)
|
|
|
+ .arg(schema_version.first).arg(schema_version.second)
|
|
|
+ .arg(SQLITE_SCHEMA_MAJOR_VERSION).arg(SQLITE_SCHEMA_MINOR_VERSION);
|
|
|
+ isc_throw(IncompatibleDbVersion, "Incompatible database version");
|
|
|
+ } else if (schema_version.second < SQLITE_SCHEMA_MINOR_VERSION) {
|
|
|
+ LOG_WARN(logger, DATASRC_SQLITE_COMPATIBLE_VERSION)
|
|
|
+ .arg(schema_version.first).arg(schema_version.second)
|
|
|
+ .arg(SQLITE_SCHEMA_MAJOR_VERSION).arg(SQLITE_SCHEMA_MINOR_VERSION);
|
|
|
+ }
|
|
|
+
|
|
|
+ initializer->params_.major_version_ = schema_version.first;
|
|
|
+ initializer->params_.minor_version_ = schema_version.second;
|
|
|
initializer->params_.q_zone_ = prepare(db, q_zone_str);
|
|
|
initializer->params_.q_record_ = prepare(db, q_record_str);
|
|
|
initializer->params_.q_addrs_ = prepare(db, q_addrs_str);
|