diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java index 79d2902b66..12a79b0a43 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java @@ -67,11 +67,11 @@ public class DatabaseUtils public static void main(String[] argv) { // Usage checks - if (argv.length != 1) + if (argv.length < 1) { System.out.println("\nDatabase action argument is missing."); - System.out.println("Valid actions include: 'test', 'info', 'migrate', 'migrate-ignored, 'repair' or 'clean'"); - System.out.println("Or, type 'database help' for more information.\n"); + System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair' or 'clean'"); + System.out.println("\nOr, type 'database help' for more information.\n"); System.exit(1); } @@ -84,7 +84,7 @@ public class DatabaseUtils // Get configured DB URL for reporting below String url = ConfigurationManager.getProperty("db.url"); - + // Point Flyway API to our database Flyway flyway = setupFlyway(dataSource); @@ -92,7 +92,7 @@ public class DatabaseUtils if(argv[0].equalsIgnoreCase("test")) { // Try to connect to the database - System.out.println("\nAttempting to connect to database: "); + System.out.println("\nAttempting to connect to database using these configurations: "); System.out.println(" - URL: " + url); System.out.println(" - Driver: " + ConfigurationManager.getProperty("db.driver")); System.out.println(" - Username: " + ConfigurationManager.getProperty("db.username")); @@ -124,6 +124,7 @@ public class DatabaseUtils Connection connection = dataSource.getConnection(); DatabaseMetaData meta = connection.getMetaData(); System.out.println("\nDatabase URL: " + url); + System.out.println("Database Schema: " + getSchemaName(connection)); System.out.println("Database Software: " + meta.getDatabaseProductName() + " version " + meta.getDatabaseProductVersion()); System.out.println("Database Driver: " + meta.getDriverName() + " version " + meta.getDriverVersion()); @@ -140,8 +141,9 @@ public class DatabaseUtils String dbVersion = determineDBVersion(connection); if (dbVersion!=null) { - System.out.println("Your database looks to be compatible with DSpace version " + dbVersion); - System.out.println("All upgrades *after* version " + dbVersion + " will be automatically run during the next migration."); + System.out.println("\nYour database looks to be compatible with DSpace version " + dbVersion); + System.out.println("All upgrades *after* version " + dbVersion + " will be run during the next migration."); + System.out.println("\nIf you'd like to upgrade now, simply run 'dspace database migrate'."); } } connection.close(); @@ -150,23 +152,44 @@ public class DatabaseUtils else if(argv[0].equalsIgnoreCase("migrate")) { System.out.println("\nDatabase URL: " + url); - System.out.println("Migrating database to latest version... (Check logs for details)"); - // NOTE: This looks odd, but all we really need to do is ensure the - // DatabaseManager auto-initializes. It'll take care of the migration itself. - // Asking for our DB Name will ensure DatabaseManager.initialize() is called. - DatabaseManager.getDbName(); - System.out.println("Done."); - } - // "migrate-ignored" = Manually run any "ignored" Database migrations (if any) - else if(argv[0].equalsIgnoreCase("migrate-ignored")) - { - System.out.println("\nDatabase URL: " + url); - System.out.println("Migrating database to latest version AND running previously \"Ignored\" migrations... (Check logs for details)"); - Connection connection = dataSource.getConnection(); - // Update the database, but set "outOfOrder=true" - updateDatabase(dataSource, connection, true); - connection.close(); + // "migrate" allows for an OPTIONAL second argument: + // - "ignored" = Also run any previously "ignored" migrations during the migration + // - [version] = ONLY run migrations up to a specific DSpace version (ONLY FOR TESTING) + if(argv.length==2) + { + if(argv[1].equalsIgnoreCase("ignored")) + { + System.out.println("Migrating database to latest version AND running previously \"Ignored\" migrations... (Check logs for details)"); + Connection connection = dataSource.getConnection(); + // Update the database to latest version, but set "outOfOrder=true" + // This will ensure any old migrations in the "ignored" state are now run + updateDatabase(dataSource, connection, null, true); + connection.close(); + } + else + { + // Otherwise, we assume "argv[1]" is a valid migration version number + // This is only for testing! Never specify for Production! + System.out.println("Migrating database ONLY to version " + argv[1] + " ... (Check logs for details)"); + System.out.println("\nWARNING: It is highly likely you will see errors in your logs when the Metadata"); + System.out.println("or Bitstream Format Registry auto-update. This is because you are attempting to"); + System.out.println("use an OLD version " + argv[1] + " Database with a newer DSpace API. NEVER do this in a"); + System.out.println("PRODUCTION scenario. The resulting old DB is only useful for migration testing.\n"); + Connection connection = dataSource.getConnection(); + // Update the database, to the version specified. + updateDatabase(dataSource, connection, argv[1], false); + connection.close(); + } + } + else + { + System.out.println("Migrating database to latest version... (Check logs for details)"); + // NOTE: This looks odd, but all we really need to do is ensure the + // DatabaseManager auto-initializes. It'll take care of the migration itself. + // Asking for our DB Name will ensure DatabaseManager.initialize() is called. + DatabaseManager.getDbName(); + } System.out.println("Done."); } // "repair" = Run Flyway repair script @@ -182,10 +205,10 @@ public class DatabaseUtils { BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); System.out.println("\nDatabase URL: " + url); - System.out.println("\nIf you continue, ALL DATA AND TABLES IN YOUR DATABASE WILL BE PERMANENTLY DELETED.\n"); - System.out.println("There is NO turning back from this action. You should backup your database before continuing."); + System.out.println("\nWARNING: ALL DATA AND TABLES IN YOUR DATABASE WILL BE PERMANENTLY DELETED.\n"); + System.out.println("There is NO turning back from this action. Backup your DB before continuing."); System.out.println("If you are using Oracle, your RECYCLEBIN will also be PURGED.\n"); - System.out.print("Are you sure you want to PERMANENTLY DELETE everything from your database? [y/n]: "); + System.out.print("Do you want to PERMANENTLY DELETE everything from your database? [y/n]: "); String choiceString = input.readLine(); input.close(); @@ -199,13 +222,13 @@ public class DatabaseUtils else { System.out.println("\nUsage: database [action]"); - System.out.println("Valid actions include: 'test', 'info', 'migrate', 'migrate-ignored, 'repair' or 'clean'"); - System.out.println(" - test = Test database connection is OK"); - System.out.println(" - info = Describe basic info about database (type, version, driver, migrations run)"); - System.out.println(" - migrate = Migrate the Database to the latest version"); - System.out.println(" - migrate-ignored = If any migrations are \"Ignored\", run them AND migrate to the latest version"); - System.out.println(" - repair = Attempt to repair any previously failed database migrations"); - System.out.println(" - clean = Destroy all data and tables in Database (WARNING there is no going back!)"); + System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair' or 'clean'"); + System.out.println(" - test = Test database connection is OK"); + System.out.println(" - info = Describe basic info about database, including migrations run"); + System.out.println(" - migrate = Migrate the Database to the latest version"); + System.out.println(" Optionally, specify \"ignored\" to also run \"Ignored\" migrations"); + System.out.println(" - repair = Attempt to repair any previously failed database migrations"); + System.out.println(" - clean = DESTROY all data and tables in Database (WARNING there is no going back!)"); System.out.println(""); } @@ -303,8 +326,8 @@ public class DatabaseUtils protected static synchronized void updateDatabase(DataSource datasource, Connection connection) throws SQLException { - // By default, never run migrations out of order - updateDatabase(datasource, connection, false); + // By default, upgrade to the *latest* version and never run migrations out-of-order + updateDatabase(datasource, connection, null, false); } /** @@ -320,13 +343,16 @@ public class DatabaseUtils * DataSource object (retrieved from DatabaseManager()) * @param connection * Database connection + * @param targetVersion + * If specified, only migrate the database to a particular *version* of DSpace. This is mostly just useful for testing. + * If null, the database is migrated to the latest version. * @param outOfOrder * If true, Flyway will run any lower version migrations that were previously "ignored". * If false, Flyway will only run new migrations with a higher version number. * @throws SQLException * If database cannot be upgraded. */ - protected static synchronized void updateDatabase(DataSource datasource, Connection connection, boolean outOfOrder) + protected static synchronized void updateDatabase(DataSource datasource, Connection connection, String targetVersion, boolean outOfOrder) throws SQLException { try @@ -338,6 +364,13 @@ public class DatabaseUtils // and Flyway ONLY runs migrations that have a higher version number. flyway.setOutOfOrder(outOfOrder); + // If a target version was specified, tell Flyway to ONLY migrate to that version + // (i.e. all later migrations are left as "pending"). By default we always migrate to latest version. + if(!StringUtils.isBlank(targetVersion)) + { + flyway.setTarget(targetVersion); + } + // Does the necessary Flyway table ("schema_version") exist in this database? // If not, then this is the first time Flyway has run, and we need to initialize // NOTE: search is case sensitive, as flyway table name is ALWAYS lowercase, @@ -389,7 +422,7 @@ public class DatabaseUtils throw new SQLException("Flyway migration error occurred", fe); } } - + /** * Clean the existing database, permanently removing all data and tables *

@@ -585,36 +618,24 @@ public class DatabaseUtils */ public static boolean tableExists(Connection connection, String tableName, boolean caseSensitive) { - // Get the name of the Schema that the DSpace Database is using - // (That way we can search the right schema for this table) - String schema = ConfigurationManager.getProperty("db.schema"); - if(StringUtils.isBlank(schema)){ - schema = null; - } - boolean exists = false; ResultSet results = null; try { + // Get the name of the Schema that the DSpace Database is using + // (That way we can search the right schema) + String schema = getSchemaName(connection); + // Get information about our database. DatabaseMetaData meta = connection.getMetaData(); // If this is not a case sensitive search if(!caseSensitive) { - // Check how this database stores its table names, etc. - // i.e. lowercase vs uppercase (by default we assume mixed case) - if(meta.storesLowerCaseIdentifiers()) - { - schema = (schema == null) ? null : StringUtils.lowerCase(schema); - tableName = StringUtils.lowerCase(tableName); - } - else if(meta.storesUpperCaseIdentifiers()) - { - schema = (schema == null) ? null : StringUtils.upperCase(schema); - tableName = StringUtils.upperCase(tableName); - } + // Canonicalize everything to the proper case based on DB type + schema = canonicalize(connection, schema); + tableName = canonicalize(connection, tableName); } // Search for a table of the given name in our current schema @@ -658,36 +679,23 @@ public class DatabaseUtils */ public static boolean tableColumnExists(Connection connection, String tableName, String columnName) { - // Get the name of the Schema that the DSpace Database is using - // (That way we can search the right schema for this table) - String schema = ConfigurationManager.getProperty("db.schema"); - if(StringUtils.isBlank(schema)){ - schema = null; - } - boolean exists = false; ResultSet results = null; try { + // Get the name of the Schema that the DSpace Database is using + // (That way we can search the right schema) + String schema = getSchemaName(connection); + + // Canonicalize everything to the proper case based on DB type + schema = canonicalize(connection, schema); + tableName = canonicalize(connection, tableName); + columnName = canonicalize(connection, columnName); + // Get information about our database. DatabaseMetaData meta = connection.getMetaData(); - // Check how this database stores its table names, etc. - // i.e. lowercase vs uppercase (by default we assume mixed case) - if(meta.storesLowerCaseIdentifiers()) - { - schema = (schema == null) ? null : StringUtils.lowerCase(schema); - tableName = StringUtils.lowerCase(tableName); - columnName = StringUtils.lowerCase(columnName); - } - else if(meta.storesUpperCaseIdentifiers()) - { - schema = (schema == null) ? null : StringUtils.upperCase(schema); - tableName = StringUtils.upperCase(tableName); - columnName = StringUtils.upperCase(columnName); - } - // Search for a column of that name in the specified table & schema results = meta.getColumns(null, schema, tableName, columnName); if (results!=null && results.next()) @@ -727,13 +735,6 @@ public class DatabaseUtils */ public static boolean sequenceExists(Connection connection, String sequenceName) { - // Get the name of the Schema that the DSpace Database is using - // (That way we can search the right schema) - String schema = ConfigurationManager.getProperty("db.schema"); - if(StringUtils.isBlank(schema)){ - schema = null; - } - boolean exists = false; PreparedStatement statement = null; ResultSet results = null; @@ -742,6 +743,14 @@ public class DatabaseUtils try { + // Get the name of the Schema that the DSpace Database is using + // (That way we can search the right schema) + String schema = getSchemaName(connection); + + // Canonicalize everything to the proper case based on DB type + schema = canonicalize(connection, schema); + sequenceName = canonicalize(connection, sequenceName); + // Different database types store sequence information in different tables String dbtype = DatabaseManager.findDbKeyword(connection.getMetaData()); String sequenceSQL = null; @@ -783,17 +792,17 @@ public class DatabaseUtils { // Run the query, passing it our parameters statement = connection.prepareStatement(sequenceSQL); - statement.setString(1, StringUtils.upperCase(sequenceName)); + statement.setString(1, sequenceName); if(schemaFilter) { - statement.setString(2, StringUtils.upperCase(schema)); + statement.setString(2, schema); } results = statement.executeQuery(); // If results are non-zero, then this sequence exists! - if(results!=null && results.next()) + if(results!=null && results.next() && results.getInt(1)>0) { - exists = true; + exists = true; } } } @@ -852,6 +861,84 @@ public class DatabaseUtils } } + /** + * Get the Database Schema Name in use by this Connection, so that it can + * be used to limit queries in other methods (e.g. tableExists()). + *

+ * For PostgreSQL, schema is simply what is configured in db.schema or "public" + * For Oracle, schema is actually the database *USER* or owner. + * + * @param connection + * Current Database Connection + * @return Schema name as a string, or "null" if cannot be determined or unspecified + */ + public static String getSchemaName(Connection connection) + throws SQLException + { + String schema = null; + DatabaseMetaData meta = connection.getMetaData(); + + // Determine our DB type + String dbType = DatabaseManager.findDbKeyword(meta); + + if(dbType.equals(DatabaseManager.DBMS_POSTGRES)) + { + // Get the schema name from "db.schema" + schema = ConfigurationManager.getProperty("db.schema"); + + // If unspecified, default schema is "public" + if(StringUtils.isBlank(schema)){ + schema = "public"; + } + } + else if (dbType.equals(DatabaseManager.DBMS_ORACLE)) + { + // Schema is actually the user account + // See: http://stackoverflow.com/a/13341390 + schema = meta.getUserName(); + } + else + schema = null; + + return schema; + } + + /** + * Return the canonical name for a database identifier based on whether this + * database defaults to storing identifiers in uppercase or lowercase. + * + * @param connection + * Current Database Connection + * @param dbIdentifier + * Identifier to canonicalize (may be a table name, column name, etc) + * @return The canonical name of the identifier. + */ + public static String canonicalize(Connection connection, String dbIdentifier) + throws SQLException + { + // Avoid any null pointers + if(dbIdentifier==null) + return null; + + DatabaseMetaData meta = connection.getMetaData(); + + // Check how this database stores its identifiers, etc. + // i.e. lowercase vs uppercase (by default we assume mixed case) + if(meta.storesLowerCaseIdentifiers()) + { + return StringUtils.lowerCase(dbIdentifier); + + } + else if(meta.storesUpperCaseIdentifiers()) + { + return StringUtils.upperCase(dbIdentifier); + } + else // Otherwise DB doesn't care about case + { + return dbIdentifier; + } + } + /** * Whether or not to tell Discovery to reindex itself based on the updated * database. diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/MigrationUtils.java similarity index 92% rename from dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java rename to dspace-api/src/main/java/org/dspace/storage/rdbms/MigrationUtils.java index 86e29fc770..4f78d6ea83 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/MigrationUtils.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/MigrationUtils.java @@ -5,14 +5,14 @@ * * http://www.dspace.org/license/ */ -package org.dspace.storage.rdbms.migration; +package org.dspace.storage.rdbms; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import org.apache.commons.lang3.StringUtils; -import org.dspace.storage.rdbms.DatabaseManager; +import org.apache.commons.lang.StringUtils; /** * This Utility class offers utility methods which may be of use to perform @@ -42,7 +42,10 @@ public class MigrationUtils // First, in order to drop the appropriate Database constraint, we // must determine the unique name of the constraint. As constraint // naming is DB specific, this is dependent on our DB Type - String dbtype = DatabaseManager.getDbKeyword(); + DatabaseMetaData meta = connection.getMetaData(); + // NOTE: We use "findDbKeyword()" here as it won't cause + // DatabaseManager.initialize() to be called (which in turn re-calls Flyway) + String dbtype = DatabaseManager.findDbKeyword(meta); String constraintName = null; String constraintNameSQL = null; switch(dbtype) diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_3_9__Drop_constraint_for_DSpace_1_4_schema.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_3_9__Drop_constraint_for_DSpace_1_4_schema.java index 21eba04caf..4bd219327d 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_3_9__Drop_constraint_for_DSpace_1_4_schema.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_3_9__Drop_constraint_for_DSpace_1_4_schema.java @@ -10,7 +10,7 @@ package org.dspace.storage.rdbms.migration; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; -import org.dspace.storage.rdbms.migration.MigrationUtils; +import org.dspace.storage.rdbms.MigrationUtils; import org.flywaydb.core.api.migration.MigrationChecksumProvider; import org.flywaydb.core.api.migration.jdbc.JdbcMigration; import org.slf4j.Logger; diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_5_9__Drop_constraint_for_DSpace_1_6_schema.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_5_9__Drop_constraint_for_DSpace_1_6_schema.java index ffec0fddc0..5e03e7ecdf 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_5_9__Drop_constraint_for_DSpace_1_6_schema.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V1_5_9__Drop_constraint_for_DSpace_1_6_schema.java @@ -10,7 +10,7 @@ package org.dspace.storage.rdbms.migration; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; -import org.dspace.storage.rdbms.migration.MigrationUtils; +import org.dspace.storage.rdbms.MigrationUtils; import org.flywaydb.core.api.migration.MigrationChecksumProvider; import org.flywaydb.core.api.migration.jdbc.JdbcMigration; import org.slf4j.Logger; diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V5_0_2014_09_25__DS_1582_Metadata_For_All_Objects_drop_constraint.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V5_0_2014_09_25__DS_1582_Metadata_For_All_Objects_drop_constraint.java index 7493ff7d19..83b0914e29 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V5_0_2014_09_25__DS_1582_Metadata_For_All_Objects_drop_constraint.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/migration/V5_0_2014_09_25__DS_1582_Metadata_For_All_Objects_drop_constraint.java @@ -10,6 +10,7 @@ package org.dspace.storage.rdbms.migration; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; +import org.dspace.storage.rdbms.MigrationUtils; import org.flywaydb.core.api.migration.MigrationChecksumProvider; import org.flywaydb.core.api.migration.jdbc.JdbcMigration; import org.slf4j.Logger;