# # # patch "ChangeLog" # from [70a93f32459f008e2055e300dd0a3b98793fdebc] # to [c987ea8784a09997591ff5625b42c39886dace67] # # patch "database.cc" # from [c0b7bcc73596dbe49ceb18c98141c360b0cabf64] # to [e72a2c3e9de3c9d3e1ece2ecd9f6939f18ee8ed7] # # patch "database.hh" # from [ff098459a66c8681188f7d66c1be55cf0ac109d1] # to [9713f6e00494fbf2ca1a4b3bdff0d3cbe9b7a545] # # patch "schema_migration.cc" # from [d55940e2585c1813bfa193fdd6d302c0219d1eca] # to [04a62f69aacaa7b4bbce7a3eb8759895c68b9205] # # patch "schema_migration.hh" # from [d17dda6f74de9d6cc6a84c8faf222e5c0bb8013e] # to [62bbd4fe56ec69ab000f76a88f3cf4219cc88034] # ============================================================ --- ChangeLog 70a93f32459f008e2055e300dd0a3b98793fdebc +++ ChangeLog c987ea8784a09997591ff5625b42c39886dace67 @@ -1,3 +1,14 @@ +2007-01-20 Zack Weinberg + + * database.cc: Make database::info work on arbitrarily screwed up + databases. Also make its output consistently formatted and easier + to read. Check for SQLite 3 database in more places. + * database.hh: Tweaks to database private member declarations. + * schema_migration.cc: Restructure diagnostics of mismatched + schemas so we can provide a classification in database::info. + * schema_migration.hh: New interface, describe_sql_schema. + calculate_schema_id is no longer visible. + 2007-01-14 Nathaniel Smith * m4/boost.m4: Check for another possible library suffix used on ============================================================ --- database.cc c0b7bcc73596dbe49ceb18c98141c360b0cabf64 +++ database.cc e72a2c3e9de3c9d3e1ece2ecd9f6939f18ee8ed7 @@ -447,6 +447,7 @@ database::dump(ostream & out) // don't care about schema checking etc. check_filename(); check_db_exists(); + check_sqlite_format_version(filename); open(); { transaction_guard guard(*this); @@ -524,107 +525,174 @@ database::debug(string const & sql, ostr } } +// Subroutine of info(). This compares strings that might either be numbers +// or error messages surrounded by square brackets. We want the longest +// number, even if there's an error message that's longer than that. +static bool longest_number(string a, string b) +{ + if(a.length() > 0 && a[0] == '[') + return true; // b is longer + if(b.length() > 0 && b[0] == '[') + return false; // a is longer -namespace + return a.length() < b.length(); +} + +// Subroutine of info() and some things it calls. +// Given an informative_failure which is believed to represent an SQLite +// error, either return a string version of the error message (if it was an +// SQLite error) or rethrow the execption (if it wasn't). +static string +format_sqlite_error_for_info(informative_failure const & e) { - unsigned long - add(unsigned long count, unsigned long & total) - { - total += count; - return count; - } + string err(e.what()); + string prefix = _("error: "); + prefix.append("sqlite error: "); + if (err.find(prefix) != 0) + throw; + + err.replace(0, prefix.length(), "["); + string::size_type nl = err.find('\n'); + if (nl != string::npos) + err.erase(nl); + + err.append("]"); + return err; } + void database::info(ostream & out) { - string id; - calculate_schema_id(sql(), id); + // don't check the schema + check_filename(); + check_db_exists(); + check_sqlite_format_version(filename); + open(); + + vector counts; + counts.push_back(count("rosters")); + counts.push_back(count("roster_deltas")); + counts.push_back(count("files")); + counts.push_back(count("file_deltas")); + counts.push_back(count("revisions")); + counts.push_back(count("revision_ancestry")); + counts.push_back(count("revision_certs")); - unsigned long total = 0UL; - - u64 num_nodes; { results res; - fetch(res, one_col, any_rows, query("SELECT node FROM next_roster_node_number")); - if (res.empty()) - num_nodes = 0; - else + try { - I(res.size() == 1); - num_nodes = lexical_cast(res[0][0]) - 1; + fetch(res, one_col, any_rows, + query("SELECT node FROM next_roster_node_number")); + if (res.empty()) + counts.push_back("0"); + else + { + I(res.size() == 1); + counts.push_back((F("%u") + % (lexical_cast(res[0][0]) - 1)).str()); + } } + catch (informative_failure const & e) + { + counts.push_back(format_sqlite_error_for_info(e)); + } } -#define SPACE_USAGE(TABLE, COLS) add(space_usage(TABLE, COLS), total) + vector bytes; + { + u64 total = 0; + bytes.push_back(space("rosters", + "length(id) + length(checksum) + length(data)", + total)); + bytes.push_back(space("roster_deltas", + "length(id) + length(checksum)" + "+ length(base) + length(delta)", total)); + bytes.push_back(space("files", "length(id) + length(data)", total)); + bytes.push_back(space("file_deltas", + "length(id) + length(base) + length(delta)", total)); + bytes.push_back(space("revisions", "length(id) + length(data)", total)); + bytes.push_back(space("revision_ancestry", + "length(parent) + length(child)", total)); + bytes.push_back(space("revision_certs", + "length(hash) + length(id) + length(name)" + "+ length(value) + length(keypair)" + "+ length(signature)", total)); + bytes.push_back(space("heights", "length(revision) + length(height)", + total)); + bytes.push_back((F("%u") % total).str()); + } - out << ( \ + // pad each vector's strings on the left with spaces to make them all the + // same length + { + string::size_type width + = max_element(counts.begin(), counts.end(), longest_number)->length(); + for(vector::iterator i = counts.begin(); i != counts.end(); i++) + if (width > i->length() && (*i)[0] != '[') + i->insert(0, width - i->length(), ' '); + + width = max_element(bytes.begin(), bytes.end(), longest_number)->length(); + for(vector::iterator i = bytes.begin(); i != bytes.end(); i++) + if (width > i->length() && (*i)[0] != '[') + i->insert(0, width - i->length(), ' '); + } + + i18n_format form = F("schema version : %s\n" "counts:\n" - " full rosters : %u\n" - " roster deltas : %u\n" - " full files : %u\n" - " file deltas : %u\n" - " revisions : %u\n" - " ancestry edges : %u\n" - " certs : %u\n" - " logical files : %u\n" + " full rosters : %s\n" + " roster deltas : %s\n" + " full files : %s\n" + " file deltas : %s\n" + " revisions : %s\n" + " ancestry edges : %s\n" + " certs : %s\n" + " logical files : %s\n" "bytes:\n" - " full rosters : %u\n" - " roster deltas : %u\n" - " full files : %u\n" - " file deltas : %u\n" - " revisions : %u\n" - " cached ancestry : %u\n" - " certs : %u\n" - " heights : %u\n" - " total : %u\n" + " full rosters : %s\n" + " roster deltas : %s\n" + " full files : %s\n" + " file deltas : %s\n" + " revisions : %s\n" + " cached ancestry : %s\n" + " certs : %s\n" + " heights : %s\n" + " total : %s\n" "database:\n" - " page size : %u\n" - " cache size : %u" - ) - % id - // counts - % count("rosters") - % count("roster_deltas") - % count("files") - % count("file_deltas") - % count("revisions") - % count("revision_ancestry") - % count("revision_certs") - % num_nodes - // bytes - % SPACE_USAGE("rosters", "length(id) + length(checksum) + length(data)") - % SPACE_USAGE("roster_deltas", "length(id) + length(checksum) + length(base) + length(delta)") - % SPACE_USAGE("files", "length(id) + length(data)") - % SPACE_USAGE("file_deltas", "length(id) + length(base) + length(delta)") - % SPACE_USAGE("revisions", "length(id) + length(data)") - % SPACE_USAGE("revision_ancestry", "length(parent) + length(child)") - % SPACE_USAGE("revision_certs", "length(hash) + length(id) + length(name)" - " + length(value) + length(keypair) + length(signature)") - % SPACE_USAGE("heights","length(revision) + length(height)") - % total - % page_size() - % cache_size() - ) << "\n"; // final newline is kept out of the translation + " page size : %s\n" + " cache size : %s" + ); -#undef SPACE_USAGE + form = form % describe_sql_schema(__sql); + + for (vector::iterator i = counts.begin(); i != counts.end(); i++) + form = form % *i; + + for (vector::iterator i = bytes.begin(); i != bytes.end(); i++) + form = form % *i; + + form = form % page_size(); + form = form % cache_size(); + + out << form.str() << "\n"; // final newline is kept out of the translation + + close(); } void database::version(ostream & out) { - string id; - check_filename(); check_db_exists(); + check_sqlite_format_version(filename); open(); - calculate_schema_id(__sql, id); + out << (F("database schema version: %s") % describe_sql_schema(__sql)).str() + << "\n"; close(); - - out << F("database schema version: %s") % id << endl; } void @@ -632,6 +700,7 @@ database::migrate() { check_filename(); check_db_exists(); + check_sqlite_format_version(filename); open(); migrate_sql_schema(__sql, *__app); @@ -644,6 +713,7 @@ database::test_migration_step(string con { check_filename(); check_db_exists(); + check_sqlite_format_version(filename); open(); ::test_migration_step(__sql, *__app, schema); @@ -995,24 +1065,40 @@ database::delta_exists(string const & id return table_has_entry(ident, "id", table); } -unsigned long +string database::count(string const & table) { - results res; - query q("SELECT COUNT(*) FROM " + table); - fetch(res, one_col, one_row, q); - return lexical_cast(res[0][0]); + try + { + results res; + query q("SELECT COUNT(*) FROM " + table); + fetch(res, one_col, one_row, q); + return (F("%u") % lexical_cast(res[0][0])).str(); + } + catch (informative_failure const & e) + { + return format_sqlite_error_for_info(e); + } + } -unsigned long -database::space_usage(string const & table, string const & rowspace) +string +database::space(string const & table, string const & rowspace, u64 & total) { - results res; - // COALESCE is required since SUM({empty set}) is NULL. - // the sqlite docs for SUM suggest this as a workaround - query q("SELECT COALESCE(SUM(" + rowspace + "), 0) FROM " + table); - fetch(res, one_col, one_row, q); - return lexical_cast(res[0][0]); + try + { + results res; + // SUM({empty set}) is NULL; TOTAL({empty set}) is 0.0 + query q("SELECT TOTAL(" + rowspace + ") FROM " + table); + fetch(res, one_col, one_row, q); + u64 bytes = static_cast(lexical_cast(res[0][0])); + total += bytes; + return (F("%u") % bytes).str(); + } + catch (informative_failure & e) + { + return format_sqlite_error_for_info(e); + } } unsigned int ============================================================ --- database.hh ff098459a66c8681188f7d66c1be55cf0ac109d1 +++ database.hh 9713f6e00494fbf2ca1a4b3bdff0d3cbe9b7a545 @@ -138,9 +138,10 @@ private: // --== Generic database metadata gathering ==-- // private: - unsigned long count(std::string const & table); - unsigned long space_usage(std::string const & table, - std::string const & concatenated_columns); + std::string count(std::string const & table); + std::string space(std::string const & table, + std::string const & concatenated_columns, + u64 & total); unsigned int page_size(); unsigned int cache_size(); ============================================================ --- schema_migration.cc d55940e2585c1813bfa193fdd6d302c0219d1eca +++ schema_migration.cc 04a62f69aacaa7b4bbce7a3eb8759895c68b9205 @@ -648,7 +648,7 @@ const size_t n_migration_events = (sizeo const size_t n_migration_events = (sizeof migration_events / sizeof migration_events[0]); -void +static void calculate_schema_id(sqlite3 *db, string & ident) { sql stmt(db, 1, @@ -698,37 +698,96 @@ schema_to_migration(string const & id) return 0; } +// This enumerates the possible mismatches between the monotone executable +// and its database. +enum schema_mismatch_case + { + SCHEMA_MATCHES = 0, + SCHEMA_MIGRATION_NEEDED, + SCHEMA_TOO_NEW, + SCHEMA_NOT_MONOTONE, + SCHEMA_EMPTY + }; + +static schema_mismatch_case +classify_schema(sqlite3 * db, string const & id, migration_event const * m) +{ + if (m) + { + if (m->migrator_sql || m->migrator_func) + return SCHEMA_MIGRATION_NEEDED; + else + return SCHEMA_MATCHES; + } + else + { + // Distinguish an utterly empty database, such as is created by + // "mtn db load < /dev/null", or by the sqlite3 command line utility + // if you don't give it anything to do. + if (id == "da39a3ee5e6b4b0d3255bfef95601890afd80709") + return SCHEMA_EMPTY; + + // Every version of the schema has included tables named 'files', + // 'file_deltas', 'manifests', and 'manifest_deltas'. + // FIXME: Instead, use PRAGMA user_version to record an additional + // magic number in monotone databases. + int n = sql::value(db, + "SELECT COUNT(*) FROM sqlite_master " + "WHERE type = 'table' AND sql IS NOT NULL AND (" + " name = 'files' OR name = 'file_deltas'" + "OR name = 'manifests' OR name = 'manifest_deltas')"); + if (n != 4) + return SCHEMA_NOT_MONOTONE; + + return SCHEMA_TOO_NEW; + } +} + +string +describe_sql_schema(sqlite3 * db) +{ + I(db != NULL); + + string id; + calculate_schema_id(db, id); + migration_event const *m = schema_to_migration(id); + schema_mismatch_case cat = classify_schema(db, id, m); + + switch (cat) + { + case SCHEMA_MATCHES: + return (F("%s (usable)") % id).str(); + case SCHEMA_MIGRATION_NEEDED: + return (F("%s (migration needed)") % id).str(); + case SCHEMA_TOO_NEW: + return (F("%s (too new, cannot use)") % id).str(); + case SCHEMA_NOT_MONOTONE: + return (F("%s (not a monotone database)") % id).str(); + case SCHEMA_EMPTY: + return (F("%s (database has no tables!)") % id).str(); + default: + I(false); + } +} + // Provide sensible diagnostics for a database schema whose hash we do not -// recognize. -static void NORETURN -diagnose_unrecognized_schema(sqlite3 * db, system_path const & filename, +// recognize. (Shared between check_sql_schema and migrate_sql_schema.) +static void +diagnose_unrecognized_schema(schema_mismatch_case cat, + system_path const & filename, string const & id) { - // Give a special message for an utterly empty sqlite3 database, such as - // is created by "mtn db load < /dev/null", or by the sqlite3 command line - // utility if you don't give it anything to do. - N(id != "da39a3ee5e6b4b0d3255bfef95601890afd80709", + N(cat != SCHEMA_EMPTY, F("cannot use the empty sqlite database %s\n" "(monotone databases must be created with '%s db init')") % filename % ui.prog_name); - // Do a sanity check to make sure we are actually looking at a monotone - // database, not some other sqlite3 database. Every version of the schema - // has included tables named 'files', 'file_deltas', 'manifests', and - // 'manifest_deltas'. - // ??? Use PRAGMA user_version to record an additional magic number in - // monotone databases. - int n = sql::value(db, - "SELECT COUNT(*) FROM sqlite_master " - "WHERE type = 'table' AND sql IS NOT NULL " - "AND (name = 'files' OR name = 'file_deltas'" - " OR name = 'manifests' OR name = 'manifest_deltas')"); - N(n == 4, + N(cat != SCHEMA_NOT_MONOTONE, F("%s does not appear to be a monotone database\n" "(schema %s, core tables missing)") % filename % id); - N(false, + N(cat != SCHEMA_TOO_NEW, F("%s appears to be a monotone database, but this version of\n" "monotone does not recognize its schema (%s).\n" "you probably need a newer version of monotone.") @@ -745,13 +804,12 @@ check_sql_schema(sqlite3 * db, system_pa string id; calculate_schema_id(db, id); - migration_event const *m = schema_to_migration(id); + schema_mismatch_case cat = classify_schema(db, id, m); - if (m == 0) - diagnose_unrecognized_schema(db, filename, id); + diagnose_unrecognized_schema(cat, filename, id); - N(m->migrator_sql == 0 && m->migrator_func == 0, + N(cat != SCHEMA_MIGRATION_NEEDED, F("database %s is laid out according to an old schema, %s\n" "try '%s db migrate' to upgrade\n" "(this is irreversible; you may want to make a backup copy first)") @@ -782,15 +840,15 @@ migrate_sql_schema(sqlite3 * db, app_sta P(F("calculating migration for schema %s") % init); migration_event const *m = schema_to_migration(init); + schema_mismatch_case cat = classify_schema(db, init, m); - if (m == 0) - diagnose_unrecognized_schema(db, app.db.get_filename(), init); + diagnose_unrecognized_schema(cat, app.db.get_filename(), init); // We really want 'db migrate' on an up-to-date schema to be a no-op // (no vacuum or anything, even), so that automated scripts can fire // one off optimistically and not have to worry about getting their // administrators to do it by hand. - if (m->migrator_func == 0 && m->migrator_sql == 0) + if (cat == SCHEMA_MATCHES) { P(F("no migration performed; database schema already up-to-date")); return; ============================================================ --- schema_migration.hh d17dda6f74de9d6cc6a84c8faf222e5c0bb8013e +++ schema_migration.hh 62bbd4fe56ec69ab000f76a88f3cf4219cc88034 @@ -23,9 +23,10 @@ class system_path; class app_state; class system_path; -void calculate_schema_id(sqlite3 * db, std::string & id); void migrate_sql_schema(sqlite3 * db, app_state & app); void check_sql_schema(sqlite3 * db, system_path const & filename); +std::string describe_sql_schema(sqlite3 * db); + void test_migration_step(sqlite3 * db, app_state & app, std::string const & schema);