// file : migrate/migrate.cxx -*- C++ -*- // copyright : Copyright (c) 2014-2019 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file #include // strcasecmp() #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace odb::core; using namespace brep; // Operation failed, diagnostics has already been issued. // struct failed {}; static const char* help_info ( " info: run 'brep-migrate --help' for more information"); // Helper class that encapsulates both the ODB-generated schema and the // extra that comes from a .sql file (via xxd). // class schema { public: explicit schema (const char* extra, string name); void create (database&, bool extra_only = false) const; void drop (database&, bool extra_only = false) const; private: string name_; strings drop_statements_; strings create_statements_; }; schema:: schema (const char* s, string name) : name_ (move (name)) { // Remove comments, saving the cleaned SQL code into statements. // string statements; for (istringstream i (s); i; ) { // Skip leading spaces (including consequtive newlines). In case we // hit eof, keep c set to '\n'. // char c; static const string spaces (" \t\n\r"); for (c = '\n'; i.get (c) && spaces.find (c) != string::npos; c = '\n') ; // First non-space character (or '\n' for eof). See if this is a comment. // bool skip (c == '\n' || (c == '-' && i.peek () == '-')); // Read until newline (and don't forget the character we already have). // do { if (!skip) statements.push_back (c); } while (c != '\n' && i.get (c)); } istringstream i (move (statements)); // Build CREATE and DROP statement lists. // while (i) { string op; if (i >> op) // Get the first word. { string statement (op); auto read_until = [&i, &statement] (const char stop[2]) -> bool { for (char prev ('\0'), c; i.get (c); prev = c) { statement.push_back (c); if (stop[0] == prev && stop[1] == c) return true; } return false; }; if (strcasecmp (op.c_str (), "CREATE") == 0) { bool valid (true); string kw; i >> kw; statement += " " + kw; if (strcasecmp (kw.c_str (), "FUNCTION") == 0) { if (!read_until ("$$") || !read_until ("$$")) { cerr << "error: function body must be defined using $$-quoted " "strings" << endl; throw failed (); } } else if (strcasecmp (kw.c_str (), "TYPE") == 0) { // Fall through. } else if (strcasecmp (kw.c_str (), "FOREIGN") == 0) { i >> kw; statement += " " + kw; valid = strcasecmp (kw.c_str (), "TABLE") == 0; // Fall through. } else valid = false; if (!valid) { cerr << "error: unexpected CREATE statement" << endl; throw failed (); } if (!read_until (";\n")) { cerr << "error: expected ';\\n' at the end of CREATE statement" << endl; throw failed (); } assert (!statement.empty ()); create_statements_.emplace_back (move (statement)); } else if (strcasecmp (op.c_str (), "DROP") == 0) { if (!read_until (";\n")) { cerr << "error: expected ';\\n' at the end of DROP statement" << endl; throw failed (); } assert (!statement.empty ()); drop_statements_.emplace_back (move (statement)); } else { cerr << "error: unexpected statement starting with '" << op << "'" << endl; throw failed (); } } } } void schema:: drop (database& db, bool extra_only) const { for (const auto& s: drop_statements_) // If the statement execution fails, the corresponding source file line // number is not reported. The line number could be usefull for the // utility implementer only. The errors seen by the end-user should not be // statement-specific. // db.execute (s); if (!extra_only) schema_catalog::drop_schema (db, name_); } void schema:: create (database& db, bool extra_only) const { drop (db, extra_only); if (!extra_only) schema_catalog::create_schema (db, name_); for (const auto& s: create_statements_) db.execute (s); } // Register the data migration functions for the package database schema. // template using package_migration_entry_base = data_migration_entry; template struct package_migration_entry: package_migration_entry_base { package_migration_entry (void (*f) (database& db)) : package_migration_entry_base (f, "package") {} }; static const package_migration_entry<12> package_migrate_v12 ([] (database& db) { // Set the text_type::plain type for the present package descriptions. // db.execute ("UPDATE package SET description_type = 'text/plain' " "WHERE description IS NOT NULL"); }); static const package_migration_entry<14> package_migrate_v14 ([] (database& db) { // Save the package tags as the package keywords. // db.execute ("INSERT INTO package_keywords (" "tenant, " "name, " "version_epoch, " "version_canonical_upstream, " "version_canonical_release, " "version_revision, " "index, " "keyword) " "SELECT " "tenant, " "name, " "version_epoch, " "version_canonical_upstream, " "version_canonical_release, " "version_revision, " "index, " "tag " "FROM package_tags"); // Note that the package keywords are the second-strongest search keywords // and the package tags where the first-strongest search keywords. So, // strictly speaking, we should update the package table search_index column // to reflect the new ordering rules for the package search. But it feels // like it isn't really worth the trouble. // }); // main() function // int main (int argc, char* argv[]) try { cli::argv_scanner scan (argc, argv, true); options ops (scan); // Version. // if (ops.version ()) { cout << "brep-migrate " << BREP_VERSION_ID << endl << "libbrep " << LIBBREP_VERSION_ID << endl << "libbbot " << LIBBBOT_VERSION_ID << endl << "libbpkg " << LIBBPKG_VERSION_ID << endl << "libbutl " << LIBBUTL_VERSION_ID << endl << "Copyright (c) 2014-2019 Code Synthesis Ltd" << endl << "This is free software released under the MIT license." << endl; return 0; } // Help. // if (ops.help ()) { butl::pager p ("brep-migrate help", false, ops.pager_specified () ? &ops.pager () : nullptr, &ops.pager_option ()); print_usage (p.stream ()); // If the pager failed, assume it has issued some diagnostics. // return p.wait () ? 0 : 1; } if (!scan.more ()) { cerr << "error: no database schema specified" << endl << help_info << endl; return 1; } const string db_schema (scan.next ()); if (db_schema != "package" && db_schema != "build") throw cli::unknown_argument (db_schema); if (scan.more ()) { cerr << "error: unexpected argument encountered" << endl << help_info << endl; return 1; } if (ops.recreate () && ops.drop ()) { cerr << "error: inconsistent options specified" << endl << help_info << endl; return 1; } odb::pgsql::database db ( ops.db_user (), ops.db_password (), !ops.db_name ().empty () ? ops.db_name () : "brep_" + db_schema, ops.db_host (), ops.db_port (), "options='-c default_transaction_isolation=serializable'"); // Prevent several brep-migrate/load instances from updating DB // simultaneously. // database_lock l (db); // Currently we don't support data migration for the manual database scheme // migration. // if (db.schema_migration (db_schema)) { cerr << "error: manual database schema migration is not supported" << endl; throw failed (); } // Need to obtain schema version out of the transaction. If the // schema_version table does not exist, the SQL query fails, which makes the // transaction useless as all consequitive queries in that transaction will // be ignored by PostgreSQL. // schema_version schema_version (db.schema_version (db_schema)); odb::schema_version schema_current_version ( schema_catalog::current_version (db, db_schema)); // It is impossible to operate with the database which is out of the // [base_version, current_version] range due to the lack of the knowlege // required not just for migration, but for the database wiping as well. // if (schema_version > 0) { if (schema_version < schema_catalog::base_version (db, db_schema)) { cerr << "error: database schema is too old" << endl; throw failed (); } if (schema_version > schema_current_version) { cerr << "error: database schema is too new" << endl; throw failed (); } } bool drop (ops.drop ()); bool create (ops.recreate () || (schema_version == 0 && !drop)); assert (!create || !drop); // The database schema recreation requires dropping it initially, which is // impossible before the database is migrated to the current schema version. // Let the user decide if they want to migrate or just drop the entire // database (followed with the database creation for the --recreate option). // if ((create || drop) && schema_version != 0 && schema_version != schema_current_version) { cerr << "error: database schema requires migration" << endl << " info: either migrate the database first or drop the entire " "database using, for example, psql" << endl; throw failed (); } static const char package_extras[] = { #include , '\0'}; static const char build_extras[] = { #include , '\0'}; schema s (db_schema == "package" ? package_extras : build_extras, db_schema); transaction t (db.begin ()); if (create || drop) { if (create) s.create (db); else if (drop) s.drop (db); } else if (schema_version != schema_current_version) { // Drop the extras, migrate the database tables and data, and create the // extras afterwards. // // Note that here we assume that the latest extras drop SQL statements can // handle entities created by the create statements of the earlier schemas // (see libbrep/package-extra.sql for details). // s.drop (db, true /* extra_only */); schema_catalog::migrate (db, 0, db_schema); s.create (db, true /* extra_only */); } t.commit (); return 0; } catch (const database_locked&) { cerr << "brep-migrate or brep-load is running" << endl; return 2; } catch (const recoverable& e) { cerr << "recoverable database error: " << e << endl; return 3; } catch (const cli::exception& e) { cerr << "error: " << e << endl << help_info << endl; return 1; } catch (const failed&) { return 1; // Diagnostics has already been issued. } // Fully qualified to avoid ambiguity with odb exception. // catch (const std::exception& e) { cerr << "error: " << e << endl; return 1; }