diff --git a/jOOQ/src/main/java/org/jooq/Migration.java b/jOOQ/src/main/java/org/jooq/Migration.java index f454abd27b..125cfc32c6 100644 --- a/jOOQ/src/main/java/org/jooq/Migration.java +++ b/jOOQ/src/main/java/org/jooq/Migration.java @@ -37,7 +37,8 @@ */ package org.jooq; -import org.jooq.exception.DataDefinitionException; +import org.jooq.exception.DataMigrationException; +import org.jooq.exception.DataMigrationValidationException; /** * An executable migration between two {@link Version} instances. @@ -61,11 +62,26 @@ public interface Migration extends Scope { */ Queries queries(); + /** + * Validate a migration. + * + * @throws DataMigrationValidationException When something went wrong during + * the validation of the migration. + */ + void validate() throws DataMigrationValidationException; + /** * Apply the migration. * - * @throws DataDefinitionException When something went wrong during the + * @throws DataMigrationException When something went wrong during the * application of the migration. */ - MigrationResult execute() throws DataDefinitionException; + MigrationResult execute() throws DataMigrationException; + + /** + * The result of a {@link Migration} execution. + */ + public interface MigrationResult { + + } } diff --git a/jOOQ/src/main/java/org/jooq/conf/InterpreterSearchSchema.java b/jOOQ/src/main/java/org/jooq/conf/InterpreterSearchSchema.java new file mode 100644 index 0000000000..91de1cc69a --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/conf/InterpreterSearchSchema.java @@ -0,0 +1,141 @@ + +package org.jooq.conf; + +import java.io.Serializable; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import org.jooq.util.jaxb.tools.XMLAppendable; +import org.jooq.util.jaxb.tools.XMLBuilder; + + +/** + * A schema that is on the search path. + * + * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "InterpreterSearchSchema", propOrder = { + +}) +@SuppressWarnings({ + "all" +}) +public class InterpreterSearchSchema + extends SettingsBase + implements Serializable, Cloneable, XMLAppendable +{ + + private final static long serialVersionUID = 31200L; + protected String catalog; + @XmlElement(required = true) + protected String schema; + + /** + * The catalog qualifier of the schema, if applicable. + * + */ + public String getCatalog() { + return catalog; + } + + /** + * The catalog qualifier of the schema, if applicable. + * + */ + public void setCatalog(String value) { + this.catalog = value; + } + + /** + * The schema qualifier whose elements can be found from the search path. + * + */ + public String getSchema() { + return schema; + } + + /** + * The schema qualifier whose elements can be found from the search path. + * + */ + public void setSchema(String value) { + this.schema = value; + } + + /** + * The catalog qualifier of the schema, if applicable. + * + */ + public InterpreterSearchSchema withCatalog(String value) { + setCatalog(value); + return this; + } + + /** + * The schema qualifier whose elements can be found from the search path. + * + */ + public InterpreterSearchSchema withSchema(String value) { + setSchema(value); + return this; + } + + @Override + public final void appendTo(XMLBuilder builder) { + builder.append("catalog", catalog); + builder.append("schema", schema); + } + + @Override + public String toString() { + XMLBuilder builder = XMLBuilder.nonFormatting(); + appendTo(builder); + return builder.toString(); + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + if (that == null) { + return false; + } + if (getClass()!= that.getClass()) { + return false; + } + InterpreterSearchSchema other = ((InterpreterSearchSchema) that); + if (catalog == null) { + if (other.catalog!= null) { + return false; + } + } else { + if (!catalog.equals(other.catalog)) { + return false; + } + } + if (schema == null) { + if (other.schema!= null) { + return false; + } + } else { + if (!schema.equals(other.schema)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = ((prime*result)+((catalog == null)? 0 :catalog.hashCode())); + result = ((prime*result)+((schema == null)? 0 :schema.hashCode())); + return result; + } + +} diff --git a/jOOQ/src/main/java/org/jooq/conf/ObjectFactory.java b/jOOQ/src/main/java/org/jooq/conf/ObjectFactory.java index e835a25a8f..3229b8378f 100644 --- a/jOOQ/src/main/java/org/jooq/conf/ObjectFactory.java +++ b/jOOQ/src/main/java/org/jooq/conf/ObjectFactory.java @@ -49,6 +49,14 @@ public class ObjectFactory { return new ParseSearchSchema(); } + /** + * Create an instance of {@link InterpreterSearchSchema } + * + */ + public InterpreterSearchSchema createInterpreterSearchSchema() { + return new InterpreterSearchSchema(); + } + /** * Create an instance of {@link RenderMapping } * diff --git a/jOOQ/src/main/java/org/jooq/conf/Settings.java b/jOOQ/src/main/java/org/jooq/conf/Settings.java index 9b95380a7e..321082a8c4 100644 --- a/jOOQ/src/main/java/org/jooq/conf/Settings.java +++ b/jOOQ/src/main/java/org/jooq/conf/Settings.java @@ -196,6 +196,8 @@ public class Settings @XmlElement(type = String.class) @XmlJavaTypeAdapter(LocaleAdapter.class) protected Locale interpreterLocale; + @XmlElement(defaultValue = "true") + protected Boolean migrationAutoValidation = true; @XmlElement(type = String.class) @XmlJavaTypeAdapter(LocaleAdapter.class) protected Locale locale; @@ -223,6 +225,9 @@ public class Settings protected String parseIgnoreCommentStart = "[jooq ignore start]"; @XmlElement(defaultValue = "[jooq ignore stop]") protected String parseIgnoreCommentStop = "[jooq ignore stop]"; + @XmlElementWrapper(name = "interpreterSearchPath") + @XmlElement(name = "schema") + protected List interpreterSearchPath; @XmlElementWrapper(name = "parseSearchPath") @XmlElement(name = "schema") protected List parseSearchPath; @@ -1654,6 +1659,30 @@ public class Settings this.interpreterLocale = value; } + /** + * Whether a migration automatically runs a validation first. + * + * @return + * possible object is + * {@link Boolean } + * + */ + public Boolean isMigrationAutoValidation() { + return migrationAutoValidation; + } + + /** + * Sets the value of the migrationAutoValidation property. + * + * @param value + * allowed object is + * {@link Boolean } + * + */ + public void setMigrationAutoValidation(Boolean value) { + this.migrationAutoValidation = value; + } + /** * The Locale to be used with any locale dependent logic if there is not a more specific locale available. More specific locales include e.g. {@link #getRenderLocale()}, {@link #getParseLocale()}, or {@link #getInterpreterLocale()}. * @@ -1822,6 +1851,17 @@ public class Settings this.parseIgnoreCommentStop = value; } + public List getInterpreterSearchPath() { + if (interpreterSearchPath == null) { + interpreterSearchPath = new ArrayList(); + } + return interpreterSearchPath; + } + + public void setInterpreterSearchPath(List interpreterSearchPath) { + this.interpreterSearchPath = interpreterSearchPath; + } + public List getParseSearchPath() { if (parseSearchPath == null) { parseSearchPath = new ArrayList(); @@ -2367,6 +2407,11 @@ public class Settings return this; } + public Settings withMigrationAutoValidation(Boolean value) { + setMigrationAutoValidation(value); + return this; + } + /** * The Locale to be used with any locale dependent logic if there is not a more specific locale available. More specific locales include e.g. {@link #getRenderLocale()}, {@link #getParseLocale()}, or {@link #getInterpreterLocale()}. * @@ -2453,6 +2498,27 @@ public class Settings return this; } + public Settings withInterpreterSearchPath(InterpreterSearchSchema... values) { + if (values!= null) { + for (InterpreterSearchSchema value: values) { + getInterpreterSearchPath().add(value); + } + } + return this; + } + + public Settings withInterpreterSearchPath(Collection values) { + if (values!= null) { + getInterpreterSearchPath().addAll(values); + } + return this; + } + + public Settings withInterpreterSearchPath(List interpreterSearchPath) { + setInterpreterSearchPath(interpreterSearchPath); + return this; + } + public Settings withParseSearchPath(ParseSearchSchema... values) { if (values!= null) { for (ParseSearchSchema value: values) { @@ -2542,6 +2608,7 @@ public class Settings builder.append("interpreterDialect", interpreterDialect); builder.append("interpreterNameLookupCaseSensitivity", interpreterNameLookupCaseSensitivity); builder.append("interpreterLocale", interpreterLocale); + builder.append("migrationAutoValidation", migrationAutoValidation); builder.append("locale", locale); builder.append("parseDialect", parseDialect); builder.append("parseLocale", parseLocale); @@ -2552,6 +2619,7 @@ public class Settings builder.append("parseIgnoreComments", parseIgnoreComments); builder.append("parseIgnoreCommentStart", parseIgnoreCommentStart); builder.append("parseIgnoreCommentStop", parseIgnoreCommentStop); + builder.append("interpreterSearchPath", "schema", interpreterSearchPath); builder.append("parseSearchPath", "schema", parseSearchPath); } @@ -3168,6 +3236,15 @@ public class Settings return false; } } + if (migrationAutoValidation == null) { + if (other.migrationAutoValidation!= null) { + return false; + } + } else { + if (!migrationAutoValidation.equals(other.migrationAutoValidation)) { + return false; + } + } if (locale == null) { if (other.locale!= null) { return false; @@ -3258,6 +3335,15 @@ public class Settings return false; } } + if (interpreterSearchPath == null) { + if (other.interpreterSearchPath!= null) { + return false; + } + } else { + if (!interpreterSearchPath.equals(other.interpreterSearchPath)) { + return false; + } + } if (parseSearchPath == null) { if (other.parseSearchPath!= null) { return false; @@ -3340,6 +3426,7 @@ public class Settings result = ((prime*result)+((interpreterDialect == null)? 0 :interpreterDialect.hashCode())); result = ((prime*result)+((interpreterNameLookupCaseSensitivity == null)? 0 :interpreterNameLookupCaseSensitivity.hashCode())); result = ((prime*result)+((interpreterLocale == null)? 0 :interpreterLocale.hashCode())); + result = ((prime*result)+((migrationAutoValidation == null)? 0 :migrationAutoValidation.hashCode())); result = ((prime*result)+((locale == null)? 0 :locale.hashCode())); result = ((prime*result)+((parseDialect == null)? 0 :parseDialect.hashCode())); result = ((prime*result)+((parseLocale == null)? 0 :parseLocale.hashCode())); @@ -3350,6 +3437,7 @@ public class Settings result = ((prime*result)+((parseIgnoreComments == null)? 0 :parseIgnoreComments.hashCode())); result = ((prime*result)+((parseIgnoreCommentStart == null)? 0 :parseIgnoreCommentStart.hashCode())); result = ((prime*result)+((parseIgnoreCommentStop == null)? 0 :parseIgnoreCommentStop.hashCode())); + result = ((prime*result)+((interpreterSearchPath == null)? 0 :interpreterSearchPath.hashCode())); result = ((prime*result)+((parseSearchPath == null)? 0 :parseSearchPath.hashCode())); return result; } diff --git a/jOOQ/src/main/java/org/jooq/MigrationResult.java b/jOOQ/src/main/java/org/jooq/exception/DataMigrationException.java similarity index 57% rename from jOOQ/src/main/java/org/jooq/MigrationResult.java rename to jOOQ/src/main/java/org/jooq/exception/DataMigrationException.java index bda6d6797a..1b74c8b060 100644 --- a/jOOQ/src/main/java/org/jooq/MigrationResult.java +++ b/jOOQ/src/main/java/org/jooq/exception/DataMigrationException.java @@ -35,13 +35,38 @@ * * */ -package org.jooq; +package org.jooq.exception; + +import org.jooq.Migration; /** - * The result of a {@link Migration}. + * An error occurred while running a {@link Migration}. * * @author Lukas Eder */ -public interface MigrationResult { +public class DataMigrationException extends DataAccessException { + /** + * Generated UID + */ + private static final long serialVersionUID = -6460945824599280420L; + + /** + * Constructor for DataMigrationException. + * + * @param message the detail message + */ + public DataMigrationException(String message) { + super(message); + } + + /** + * Constructor for DataMigrationException. + * + * @param message the detail message + * @param cause the cause + */ + public DataMigrationException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java b/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java new file mode 100644 index 0000000000..251b124eac --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: http://www.jooq.org/licenses + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.exception; + +import org.jooq.Migration; + +/** + * An error occurred while running {@link Migration#validate()}. + * + * @author Lukas Eder + */ +public class DataMigrationValidationException extends DataAccessException { + + /** + * Generated UID + */ + private static final long serialVersionUID = -6460945824599280420L; + + /** + * Constructor for DataMigrationValidationException. + * + * @param message the detail message + */ + public DataMigrationValidationException(String message) { + super(message); + } + + /** + * Constructor for DataMigrationValidationException. + * + * @param message the detail message + * @param cause the cause + */ + public DataMigrationValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/DDLInterpreter.java b/jOOQ/src/main/java/org/jooq/impl/DDLInterpreter.java index 1fd54ac4ef..d65e1af00e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DDLInterpreter.java +++ b/jOOQ/src/main/java/org/jooq/impl/DDLInterpreter.java @@ -44,6 +44,8 @@ import static org.jooq.impl.Cascade.CASCADE; import static org.jooq.impl.Cascade.RESTRICT; import static org.jooq.impl.ConstraintType.FOREIGN_KEY; import static org.jooq.impl.ConstraintType.PRIMARY_KEY; +import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.schema; import static org.jooq.impl.SQLDataType.BIGINT; import static org.jooq.impl.Tools.EMPTY_FIELD; import static org.jooq.impl.Tools.intersect; @@ -93,6 +95,7 @@ import org.jooq.TableOptions.TableType; import org.jooq.UniqueKey; import org.jooq.Update; import org.jooq.conf.InterpreterNameLookupCaseSensitivity; +import org.jooq.conf.InterpreterSearchSchema; import org.jooq.exception.DataAccessException; import org.jooq.exception.DataDefinitionException; import org.jooq.impl.ConstraintImpl.Action; @@ -109,7 +112,6 @@ final class DDLInterpreter { private final Map catalogs = new LinkedHashMap<>(); private final MutableCatalog defaultCatalog; private final MutableSchema defaultSchema; - private MutableSchema currentSchema; // Caches private final Map interpretedCatalogs = new HashMap<>(); @@ -127,7 +129,6 @@ final class DDLInterpreter { this.defaultCatalog = new MutableCatalog(NO_NAME); this.catalogs.put(defaultCatalog.name(), defaultCatalog); this.defaultSchema = new MutableSchema(NO_NAME, defaultCatalog); - this.currentSchema = defaultSchema; } final Meta meta() { @@ -273,10 +274,6 @@ final class DDLInterpreter { mutableSchema.catalog.schemas.remove(mutableSchema); else throw schemaNotEmpty(schema); - - // TODO: Is this needed? - if (mutableSchema.equals(currentSchema)) - currentSchema = null; } private final void accept0(CreateTableImpl query) { @@ -1061,10 +1058,8 @@ final class DDLInterpreter { } private final MutableSchema getSchema(Schema input, boolean create) { - - // TODO It does not appear we should auto-create schema in the interpreter. Why is this being done? if (input == null) - return currentSchema; + return getInterpreterSearchPathSchema(create); MutableCatalog catalog = defaultCatalog; if (input.getCatalog() != null) { @@ -1084,6 +1079,16 @@ final class DDLInterpreter { return schema; } + private final MutableSchema getInterpreterSearchPathSchema(boolean create) { + List searchPath = configuration.settings().getInterpreterSearchPath(); + + if (searchPath.isEmpty()) + return defaultSchema; + + InterpreterSearchSchema schema = searchPath.get(0); + return getSchema(schema(name(schema.getCatalog(), schema.getSchema())), create); + } + private final MutableTable newTable( Table table, MutableSchema schema, diff --git a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java index 3b18c155a7..797af796e5 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java @@ -37,6 +37,9 @@ */ package org.jooq.impl; +import static java.lang.Boolean.FALSE; +import static org.jooq.impl.DSL.createSchemaIfNotExists; + import java.sql.Timestamp; import java.util.Arrays; import java.util.HashMap; @@ -48,9 +51,9 @@ import org.jooq.Constants; import org.jooq.ContextTransactionalCallable; import org.jooq.Field; import org.jooq.Identity; +import org.jooq.Meta; import org.jooq.Migration; import org.jooq.MigrationListener; -import org.jooq.MigrationResult; import org.jooq.Name; import org.jooq.Queries; import org.jooq.Query; @@ -61,7 +64,8 @@ import org.jooq.TableField; import org.jooq.UniqueKey; import org.jooq.Version; import org.jooq.exception.DataAccessException; -import org.jooq.exception.DataDefinitionException; +import org.jooq.exception.DataMigrationException; +import org.jooq.exception.DataMigrationValidationException; import org.jooq.tools.JooqLogger; import org.jooq.tools.StopWatch; @@ -87,17 +91,10 @@ final class MigrationImpl extends AbstractScope implements Migration { @Override public final Version from() { - if (from == null) { + if (from == null) // TODO: Use pessimistic locking so no one else can migrate in between - JooqMigrationsChangelogRecord currentRecord = - dsl().selectFrom(CHANGELOG) - .orderBy(CHANGELOG.MIGRATED_AT.desc(), CHANGELOG.ID.desc()) - .limit(1) - .fetchOne(); - - from = currentRecord == null ? to().root() : versions().get(currentRecord.getMigratedTo()); - } + from = currentVersion(); return from; } @@ -126,16 +123,45 @@ final class MigrationImpl extends AbstractScope implements Migration { return versions; } + @Override + public final void validate() { + JooqMigrationsChangelogRecord currentRecord = currentChangelogRecord(); + + if (currentRecord != null) { + Version currentVersion = versions().get(currentRecord.getMigratedTo()); + + if (currentVersion == null) + throw new DataMigrationValidationException("Version trying to migrate to is not available from VersionProvider: " + currentRecord.getMigratedTo()); + } + + validateUnexpectedObjects(); + } + + private final void validateUnexpectedObjects() { + Version currentVersion = currentVersion(); + Meta currentMeta = currentVersion.meta(); + + Meta existingMeta = dsl().meta(); + + for (Schema schema : existingMeta.getSchemas()) + currentMeta = currentMeta.apply(createSchemaIfNotExists(schema)); + + System.out.println(existingMeta.migrateTo(currentMeta)); + } + private static final MigrationResult MIGRATION_RESULT = new MigrationResult() {}; @Override - public final MigrationResult execute() throws DataDefinitionException { + public final MigrationResult execute() { // TODO: Transactions don't really make sense in most dialects. In some, they do // e.g. PostgreSQL supports transactional DDL. Check if we're getting this right. return run(new ContextTransactionalCallable() { @Override public MigrationResult run() { + if (!FALSE.equals(dsl().settings().isMigrationAutoValidation())) + validate(); + DefaultMigrationContext ctx = new DefaultMigrationContext(configuration(), from(), to(), queries()); MigrationListener listener = new MigrationListeners(configuration); @@ -147,6 +173,7 @@ final class MigrationImpl extends AbstractScope implements Migration { return MIGRATION_RESULT; } + // TODO: What to do if we're about to install things on a non-empty schema // TODO: Implement preconditions // TODO: Implement a listener with a variety of pro / oss features // TODO: Implement additional out-of-the-box sanity checks @@ -230,7 +257,7 @@ final class MigrationImpl extends AbstractScope implements Migration { public final void init() { // TODO: What to do when initialising jOOQ-migrations on an existing database? - // - Should there be init() commands that can be run explicitl by the user? + // - Should there be init() commands that can be run explicitly by the user? // - Will we reverse engineer the production Meta snapshot first? if (!existsChangelog()) dsl().meta(CHANGELOG).ddl().executeBatch(); @@ -248,9 +275,31 @@ final class MigrationImpl extends AbstractScope implements Migration { return false; } + private final JooqMigrationsChangelogRecord currentChangelogRecord() { + return existsChangelog() + ? dsl().selectFrom(CHANGELOG) + .orderBy(CHANGELOG.MIGRATED_AT.desc(), CHANGELOG.ID.desc()) + .limit(1) + .fetchOne() + : null; + } + + private final Version currentVersion() { + JooqMigrationsChangelogRecord currentRecord = currentChangelogRecord(); + return currentRecord == null ? to().root() : versions().get(currentRecord.getMigratedTo()); + } + private final T run(final ContextTransactionalCallable runnable) { - init(); - return dsl().transactionResult(runnable); + try { + init(); + return dsl().transactionResult(runnable); + } + catch (DataMigrationException e) { + throw e; + } + catch (Exception e) { + throw new DataMigrationException("Exception during migration", e); + } } // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/impl/QualifiedName.java b/jOOQ/src/main/java/org/jooq/impl/QualifiedName.java index 2ab5256a86..a079846e2b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/QualifiedName.java +++ b/jOOQ/src/main/java/org/jooq/impl/QualifiedName.java @@ -206,7 +206,7 @@ final class QualifiedName extends AbstractName { @Override public final Name unqualifiedName() { - if (qualifiedName.length <= 1) + if (qualifiedName.length == 0) return this; else return qualifiedName[qualifiedName.length - 1]; diff --git a/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd b/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd index 8802d6d244..669916849f 100644 --- a/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd +++ b/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd @@ -399,6 +399,14 @@ jOOQ queries, for which no specific fetchSize value was specified.]]> + + + + + + + + @@ -463,6 +471,24 @@ jOOQ queries, for which no specific fetchSize value was specified.]]> + + + + + + + + + + + + + + + + + +