From 1381bc96cd87326768bf9e63addf8d0035579a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Sat, 5 Oct 2024 09:39:12 -0400 Subject: [PATCH] Generate Liquibase Database Migrations --- extensions/liquibase/runtime/pom.xml | 4 + .../liquibase/database/QuarkusDatabase.java | 182 +++++ .../connection/HibernateConnection.java | 343 ++++++++ .../HibernateConnectionMetadata.java | 737 ++++++++++++++++++ .../database/connection/HibernateDriver.java | 55 ++ .../diff/ChangedColumnChangeGenerator.java | 83 ++ .../ChangedForeignKeyChangeGenerator.java | 39 + .../ChangedPrimaryKeyChangeGenerator.java | 38 + .../diff/ChangedSequenceChangeGenerator.java | 92 +++ ...hangedUniqueConstraintChangeGenerator.java | 39 + .../diff/MissingSequenceChangeGenerator.java | 32 + .../diff/UnexpectedIndexChangeGenerator.java | 34 + .../snapshot/CatalogSnapshotGenerator.java | 30 + .../snapshot/ColumnSnapshotGenerator.java | 215 +++++ .../snapshot/ForeignKeySnapshotGenerator.java | 94 +++ .../snapshot/HibernateSnapshotGenerator.java | 111 +++ .../snapshot/IndexSnapshotGenerator.java | 101 +++ .../snapshot/PrimaryKeySnapshotGenerator.java | 73 ++ .../snapshot/SchemaSnapshotGenerator.java | 32 + .../snapshot/SequenceSnapshotGenerator.java | 52 ++ .../snapshot/TableSnapshotGenerator.java | 127 +++ .../UniqueConstraintSnapshotGenerator.java | 119 +++ .../snapshot/ViewSnapshotGenerator.java | 33 + .../extension/ExtendedSnapshotGenerator.java | 9 + .../TableGeneratorSnapshotGenerator.java | 52 ++ .../services/liquibase.database.Database | 1 + .../liquibase.snapshot.SnapshotGenerator | 10 + .../org.hibernate.integrator.spi.Integrator | 1 + 28 files changed, 2738 insertions(+) create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/QuarkusDatabase.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnection.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnectionMetadata.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateDriver.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedColumnChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedForeignKeyChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedPrimaryKeyChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedSequenceChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedUniqueConstraintChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/MissingSequenceChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/UnexpectedIndexChangeGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/CatalogSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ColumnSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ForeignKeySnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/HibernateSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/IndexSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/PrimaryKeySnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SchemaSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SequenceSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/TableSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/UniqueConstraintSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ViewSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/ExtendedSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/TableGeneratorSnapshotGenerator.java create mode 100644 extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.database.Database create mode 100644 extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator create mode 100644 extensions/liquibase/runtime/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator diff --git a/extensions/liquibase/runtime/pom.xml b/extensions/liquibase/runtime/pom.xml index 2e094d6c9d4e3..06b241ce1413f 100644 --- a/extensions/liquibase/runtime/pom.xml +++ b/extensions/liquibase/runtime/pom.xml @@ -21,6 +21,10 @@ org.liquibase liquibase-core + + org.hibernate.orm + hibernate-core + org.osgi osgi.core diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/QuarkusDatabase.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/QuarkusDatabase.java new file mode 100644 index 0000000000000..ea9238ebbbd1e --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/QuarkusDatabase.java @@ -0,0 +1,182 @@ +package io.quarkus.liquibase.database; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +import io.quarkus.liquibase.database.connection.HibernateConnection; +import io.quarkus.liquibase.database.connection.HibernateDriver; +import liquibase.database.AbstractJdbcDatabase; +import liquibase.database.DatabaseConnection; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.DatabaseException; + +/** + * Base class for all Hibernate Databases. This extension interacts with Hibernate by creating standard + * liquibase.database.Database implementations that + * bridge what Liquibase expects and the Hibernate APIs. + */ +public class QuarkusDatabase extends AbstractJdbcDatabase implements Integrator { + + private static Metadata metadata; + private static Dialect dialect; + + private boolean indexesForForeignKeys = false; + public static final String DEFAULT_SCHEMA = "HIBERNATE"; + + public QuarkusDatabase() { + setDefaultCatalogName(DEFAULT_SCHEMA); + setDefaultSchemaName(DEFAULT_SCHEMA); + } + + @Override + public void integrate( + Metadata metadata, + BootstrapContext bootstrapContext, + SessionFactoryImplementor sessionFactory) { + QuarkusDatabase.metadata = metadata; + QuarkusDatabase.dialect = sessionFactory.getJdbcServices().getDialect(); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + } + + @Override + public String getShortName() { + return "hibernateQuarkus"; + } + + @Override + protected String getDefaultDatabaseProductName() { + return "Quarkus Hibernate"; + } + + @Override + public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException { + return conn.getURL().startsWith("hibernate:quarkus:"); + } + + public boolean requiresPassword() { + return false; + } + + public boolean requiresUsername() { + return false; + } + + public String getDefaultDriver(String url) { + if (url.startsWith("hibernate")) { + return HibernateDriver.class.getName(); + } + return null; + } + + public int getPriority() { + return PRIORITY_DEFAULT; + } + + /** Returns the dialect determined during database initialization. */ + public Dialect getDialect() { + return QuarkusDatabase.dialect; + } + + /** + * Return the hibernate {@link Metadata} used by this database. + */ + public Metadata getMetadata() throws DatabaseException { + return metadata; + } + + /** + * Convenience method to return the underlying HibernateConnection in the JdbcConnection returned by + * {@link #getConnection()} + */ + protected HibernateConnection getHibernateConnection() { + return ((HibernateConnection) ((JdbcConnection) getConnection()).getUnderlyingConnection()); + } + + /** + * Perform any post-configuration setting logic. + */ + protected void afterSetup() { + if (dialect instanceof MySQLDialect) { + indexesForForeignKeys = true; + } + } + + /** + * Returns the value of the given property. Should return the value given as a connection URL first, then fall back to + * configuration-specific values. + */ + public String getProperty(String name) { + return getHibernateConnection().getProperties().getProperty(name); + } + + @Override + public boolean createsIndexesForForeignKeys() { + return indexesForForeignKeys; + } + + @Override + public Integer getDefaultPort() { + return 0; + } + + @Override + public boolean supportsInitiallyDeferrableColumns() { + return false; + } + + @Override + public boolean supportsTablespaces() { + return false; + } + + @Override + protected String getConnectionCatalogName() throws DatabaseException { + return getDefaultCatalogName(); + } + + @Override + protected String getConnectionSchemaName() { + return getDefaultSchemaName(); + } + + @Override + public String getDefaultSchemaName() { + return DEFAULT_SCHEMA; + } + + @Override + public String getDefaultCatalogName() { + return DEFAULT_SCHEMA; + } + + @Override + public boolean isSafeToRunUpdate() throws DatabaseException { + return true; + } + + @Override + public boolean isCaseSensitive() { + return false; + } + + @Override + public boolean supportsSchemas() { + return true; + } + + @Override + public boolean supportsCatalogs() { + return false; + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnection.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnection.java new file mode 100644 index 0000000000000..637d86c83668f --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnection.java @@ -0,0 +1,343 @@ +package io.quarkus.liquibase.database.connection; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URLDecoder; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +import liquibase.resource.ResourceAccessor; + +/** + * Implements java.sql.Connection in order to pretend a hibernate configuration is a database in order to fit into the Liquibase + * framework. + * Beyond standard Connection methods, this class exposes {@link #getPrefix()}, {@link #getPath()} and {@link #getProperties()} + * to access the setting passed in the JDBC URL. + */ +public class HibernateConnection implements Connection { + private String prefix; + private String url; + + private String path; + private ResourceAccessor resourceAccessor; + private Properties properties; + + public HibernateConnection(String url, ResourceAccessor resourceAccessor) { + this.url = url; + + this.prefix = url.replaceFirst(":[^:]+$", ""); + + // Trim the prefix off the URL for the path + path = url.substring(prefix.length() + 1); + this.resourceAccessor = resourceAccessor; + + // Check if there is a parameter/query string value. + properties = new Properties(); + + int queryIndex = path.indexOf('?'); + if (queryIndex >= 0) { + // Convert the query string into properties + properties.putAll(readProperties(path.substring(queryIndex + 1))); + + if (properties.containsKey("dialect") && !properties.containsKey("hibernate.dialect")) { + properties.put("hibernate.dialect", properties.getProperty("dialect")); + } + + // Remove the query string + path = path.substring(0, queryIndex); + } + } + + /** + * Creates properties to attach to this connection based on the passed query string. + */ + protected Properties readProperties(String queryString) { + Properties properties = new Properties(); + queryString = queryString.replaceAll("&", System.getProperty("line.separator")); + try { + queryString = URLDecoder.decode(queryString, "UTF-8"); + properties.load(new StringReader(queryString)); + } catch (IOException ioe) { + throw new IllegalStateException("Failed to read properties from url", ioe); + } + + return properties; + } + + /** + * Returns the entire connection URL + */ + public String getUrl() { + return url; + } + + /** + * Returns the 'protocol' of the URL. For example, "hibernate:classic" or "hibernate:ejb3" + */ + public String getPrefix() { + return prefix; + } + + /** + * The portion of the url between the path and the query string. Normally a filename or a class name. + */ + public String getPath() { + return path; + } + + /** + * The set of properties provided by the URL. Eg: + *

+ * hibernate:classic:/path/to/hibernate.cfg.xml?foo=bar + *

+ * This will have a property called 'foo' with a value of 'bar'. + */ + public Properties getProperties() { + return properties; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// JDBC METHODS + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public Statement createStatement() throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql) throws SQLException { + return null; + } + + public CallableStatement prepareCall(String sql) throws SQLException { + return null; + } + + public String nativeSQL(String sql) throws SQLException { + return null; + } + + public void setAutoCommit(boolean autoCommit) throws SQLException { + + } + + public boolean getAutoCommit() throws SQLException { + return false; + } + + public void commit() throws SQLException { + + } + + public void rollback() throws SQLException { + + } + + public void close() throws SQLException { + + } + + public boolean isClosed() throws SQLException { + return false; + } + + public DatabaseMetaData getMetaData() throws SQLException { + return new HibernateConnectionMetadata(url); + } + + public void setReadOnly(boolean readOnly) throws SQLException { + + } + + public boolean isReadOnly() throws SQLException { + return true; + } + + public void setCatalog(String catalog) throws SQLException { + + } + + public String getCatalog() throws SQLException { + return "HIBERNATE"; + } + + public void setTransactionIsolation(int level) throws SQLException { + + } + + public int getTransactionIsolation() throws SQLException { + return 0; + } + + public SQLWarning getWarnings() throws SQLException { + return null; + } + + public void clearWarnings() throws SQLException { + + } + + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return null; + } + + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return null; + } + + public Map> getTypeMap() throws SQLException { + return null; + } + + public void setTypeMap(Map> map) throws SQLException { + + } + + public void setHoldability(int holdability) throws SQLException { + + } + + public int getHoldability() throws SQLException { + return 0; + } + + public Savepoint setSavepoint() throws SQLException { + return null; + } + + public Savepoint setSavepoint(String name) throws SQLException { + return null; + } + + public void rollback(Savepoint savepoint) throws SQLException { + + } + + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + + } + + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return null; + } + + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return null; + } + + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return null; + } + + public Clob createClob() throws SQLException { + return null; + } + + public Blob createBlob() throws SQLException { + return null; + } + + public NClob createNClob() throws SQLException { + return null; + } + + public SQLXML createSQLXML() throws SQLException { + return null; + } + + public boolean isValid(int timeout) throws SQLException { + return false; + } + + public void setClientInfo(String name, String value) throws SQLClientInfoException { + + } + + public void setClientInfo(Properties properties) throws SQLClientInfoException { + + } + + public String getClientInfo(String name) throws SQLException { + return null; + } + + public Properties getClientInfo() throws SQLException { + return null; + } + + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return null; + } + + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return null; + } + + public T unwrap(Class iface) throws SQLException { + return null; + } + + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + //@Override only in java 1.7 + public void abort(Executor arg0) throws SQLException { + } + + //@Override only in java 1.7 + public int getNetworkTimeout() throws SQLException { + return 0; + } + + //@Override only in java 1.7 + public String getSchema() throws SQLException { + return "HIBERNATE"; + } + + //@Override only in java 1.7 + public void setNetworkTimeout(Executor arg0, int arg1) throws SQLException { + } + + //@Override only in java 1.7 + public void setSchema(String arg0) throws SQLException { + } + + public ResourceAccessor getResourceAccessor() { + return resourceAccessor; + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnectionMetadata.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnectionMetadata.java new file mode 100644 index 0000000000000..9c35fa8a2ac6f --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateConnectionMetadata.java @@ -0,0 +1,737 @@ +package io.quarkus.liquibase.database.connection; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; + +import org.hibernate.Version; + +/** + * Implements the standard java.sql.DatabaseMetaData interface to allow the Hibernate integration to better fit into + * what Liquibase expects. + */ +public class HibernateConnectionMetadata implements DatabaseMetaData { + + private String url; + + public HibernateConnectionMetadata(String url) { + this.url = url; + } + + public boolean allProceduresAreCallable() throws SQLException { + return false; + } + + public boolean allTablesAreSelectable() throws SQLException { + return false; + } + + public String getURL() throws SQLException { + return url; + } + + public String getUserName() throws SQLException { + return null; + } + + public boolean isReadOnly() throws SQLException { + return true; + } + + public boolean nullsAreSortedHigh() throws SQLException { + return false; + } + + public boolean nullsAreSortedLow() throws SQLException { + return false; + } + + public boolean nullsAreSortedAtStart() throws SQLException { + return false; + } + + public boolean nullsAreSortedAtEnd() throws SQLException { + return false; + } + + public String getDatabaseProductName() throws SQLException { + return "Hibernate"; + } + + public String getDatabaseProductVersion() throws SQLException { + return Version.getVersionString(); + } + + public String getDriverName() throws SQLException { + return null; + } + + public String getDriverVersion() throws SQLException { + return "0"; + } + + public int getDriverMajorVersion() { + return 0; + } + + public int getDriverMinorVersion() { + return 0; + } + + public boolean usesLocalFiles() throws SQLException { + return false; + } + + public boolean usesLocalFilePerTable() throws SQLException { + return false; + } + + public boolean supportsMixedCaseIdentifiers() throws SQLException { + return false; + } + + public boolean storesUpperCaseIdentifiers() throws SQLException { + return false; + } + + public boolean storesLowerCaseIdentifiers() throws SQLException { + return false; + } + + public boolean storesMixedCaseIdentifiers() throws SQLException { + return false; + } + + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return false; + } + + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return false; + } + + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return false; + } + + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return false; + } + + public String getIdentifierQuoteString() throws SQLException { + return null; + } + + public String getSQLKeywords() throws SQLException { + return ""; // do not return null here due to liquibase.database.jvm.JdbcConnection:30 to avoid NPE's there + } + + public String getNumericFunctions() throws SQLException { + return null; + } + + public String getStringFunctions() throws SQLException { + return null; + } + + public String getSystemFunctions() throws SQLException { + return null; + } + + public String getTimeDateFunctions() throws SQLException { + return null; + } + + public String getSearchStringEscape() throws SQLException { + return null; + } + + public String getExtraNameCharacters() throws SQLException { + return null; + } + + public boolean supportsAlterTableWithAddColumn() throws SQLException { + return false; + } + + public boolean supportsAlterTableWithDropColumn() throws SQLException { + return false; + } + + public boolean supportsColumnAliasing() throws SQLException { + return false; + } + + public boolean nullPlusNonNullIsNull() throws SQLException { + return false; + } + + public boolean supportsConvert() throws SQLException { + return false; + } + + public boolean supportsConvert(int fromType, int toType) throws SQLException { + return false; + } + + public boolean supportsTableCorrelationNames() throws SQLException { + return false; + } + + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + return false; + } + + public boolean supportsExpressionsInOrderBy() throws SQLException { + return false; + } + + public boolean supportsOrderByUnrelated() throws SQLException { + return false; + } + + public boolean supportsGroupBy() throws SQLException { + return false; + } + + public boolean supportsGroupByUnrelated() throws SQLException { + return false; + } + + public boolean supportsGroupByBeyondSelect() throws SQLException { + return false; + } + + public boolean supportsLikeEscapeClause() throws SQLException { + return false; + } + + public boolean supportsMultipleResultSets() throws SQLException { + return false; + } + + public boolean supportsMultipleTransactions() throws SQLException { + return false; + } + + public boolean supportsNonNullableColumns() throws SQLException { + return false; + } + + public boolean supportsMinimumSQLGrammar() throws SQLException { + return false; + } + + public boolean supportsCoreSQLGrammar() throws SQLException { + return false; + } + + public boolean supportsExtendedSQLGrammar() throws SQLException { + return false; + } + + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + return false; + } + + public boolean supportsANSI92IntermediateSQL() throws SQLException { + return false; + } + + public boolean supportsANSI92FullSQL() throws SQLException { + return false; + } + + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + return false; + } + + public boolean supportsOuterJoins() throws SQLException { + return false; + } + + public boolean supportsFullOuterJoins() throws SQLException { + return false; + } + + public boolean supportsLimitedOuterJoins() throws SQLException { + return false; + } + + public String getSchemaTerm() throws SQLException { + return null; + } + + public String getProcedureTerm() throws SQLException { + return null; + } + + public String getCatalogTerm() throws SQLException { + return null; + } + + public boolean isCatalogAtStart() throws SQLException { + return false; + } + + public String getCatalogSeparator() throws SQLException { + return null; + } + + public boolean supportsSchemasInDataManipulation() throws SQLException { + return false; + } + + public boolean supportsSchemasInProcedureCalls() throws SQLException { + return false; + } + + public boolean supportsSchemasInTableDefinitions() throws SQLException { + return false; + } + + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + return false; + } + + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return false; + } + + public boolean supportsCatalogsInDataManipulation() throws SQLException { + return false; + } + + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + return false; + } + + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + return false; + } + + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return false; + } + + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return false; + } + + public boolean supportsPositionedDelete() throws SQLException { + return false; + } + + public boolean supportsPositionedUpdate() throws SQLException { + return false; + } + + public boolean supportsSelectForUpdate() throws SQLException { + return false; + } + + public boolean supportsStoredProcedures() throws SQLException { + return false; + } + + public boolean supportsSubqueriesInComparisons() throws SQLException { + return false; + } + + public boolean supportsSubqueriesInExists() throws SQLException { + return false; + } + + public boolean supportsSubqueriesInIns() throws SQLException { + return false; + } + + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + return false; + } + + public boolean supportsCorrelatedSubqueries() throws SQLException { + return false; + } + + public boolean supportsUnion() throws SQLException { + return false; + } + + public boolean supportsUnionAll() throws SQLException { + return false; + } + + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return false; + } + + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return false; + } + + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return false; + } + + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return false; + } + + public int getMaxBinaryLiteralLength() throws SQLException { + return 0; + } + + public int getMaxCharLiteralLength() throws SQLException { + return 0; + } + + public int getMaxColumnNameLength() throws SQLException { + return 0; + } + + public int getMaxColumnsInGroupBy() throws SQLException { + return 0; + } + + public int getMaxColumnsInIndex() throws SQLException { + return 0; + } + + public int getMaxColumnsInOrderBy() throws SQLException { + return 0; + } + + public int getMaxColumnsInSelect() throws SQLException { + return 0; + } + + public int getMaxColumnsInTable() throws SQLException { + return 0; + } + + public int getMaxConnections() throws SQLException { + return 0; + } + + public int getMaxCursorNameLength() throws SQLException { + return 0; + } + + public int getMaxIndexLength() throws SQLException { + return 0; + } + + public int getMaxSchemaNameLength() throws SQLException { + return 0; + } + + public int getMaxProcedureNameLength() throws SQLException { + return 0; + } + + public int getMaxCatalogNameLength() throws SQLException { + return 0; + } + + public int getMaxRowSize() throws SQLException { + return 0; + } + + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return false; + } + + public int getMaxStatementLength() throws SQLException { + return 0; + } + + public int getMaxStatements() throws SQLException { + return 0; + } + + public int getMaxTableNameLength() throws SQLException { + return 0; + } + + public int getMaxTablesInSelect() throws SQLException { + return 0; + } + + public int getMaxUserNameLength() throws SQLException { + return 0; + } + + public int getDefaultTransactionIsolation() throws SQLException { + return 0; + } + + public boolean supportsTransactions() throws SQLException { + return false; + } + + public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return false; + } + + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return false; + } + + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return false; + } + + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return false; + } + + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return false; + } + + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + return null; + } + + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, + String columnNamePattern) throws SQLException { + return null; + } + + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) + throws SQLException { + return null; + } + + public ResultSet getSchemas() throws SQLException { + return null; + } + + public ResultSet getCatalogs() throws SQLException { + return null; + } + + public ResultSet getTableTypes() throws SQLException { + return null; + } + + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + return null; + } + + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) + throws SQLException { + return null; + } + + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null; + } + + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { + return null; + } + + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return null; + } + + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + return null; + } + + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return null; + } + + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + return null; + } + + public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, + String foreignSchema, String foreignTable) throws SQLException { + return null; + } + + public ResultSet getTypeInfo() throws SQLException { + return null; + } + + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException { + return null; + } + + public boolean supportsResultSetType(int type) throws SQLException { + return false; + } + + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return false; + } + + public boolean ownUpdatesAreVisible(int type) throws SQLException { + return false; + } + + public boolean ownDeletesAreVisible(int type) throws SQLException { + return false; + } + + public boolean ownInsertsAreVisible(int type) throws SQLException { + return false; + } + + public boolean othersUpdatesAreVisible(int type) throws SQLException { + return false; + } + + public boolean othersDeletesAreVisible(int type) throws SQLException { + return false; + } + + public boolean othersInsertsAreVisible(int type) throws SQLException { + return false; + } + + public boolean updatesAreDetected(int type) throws SQLException { + return false; + } + + public boolean deletesAreDetected(int type) throws SQLException { + return false; + } + + public boolean insertsAreDetected(int type) throws SQLException { + return false; + } + + public boolean supportsBatchUpdates() throws SQLException { + return false; + } + + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return null; + } + + public Connection getConnection() throws SQLException { + return null; + } + + public boolean supportsSavepoints() throws SQLException { + return false; + } + + public boolean supportsNamedParameters() throws SQLException { + return false; + } + + public boolean supportsMultipleOpenResults() throws SQLException { + return false; + } + + public boolean supportsGetGeneratedKeys() throws SQLException { + return false; + } + + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return null; + } + + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null; + } + + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) + throws SQLException { + return null; + } + + public boolean supportsResultSetHoldability(int holdability) throws SQLException { + return false; + } + + public int getResultSetHoldability() throws SQLException { + return 0; + } + + public int getDatabaseMajorVersion() throws SQLException { + return 0; + } + + public int getDatabaseMinorVersion() throws SQLException { + return 0; + } + + public int getJDBCMajorVersion() throws SQLException { + return 0; + } + + public int getJDBCMinorVersion() throws SQLException { + return 0; + } + + public int getSQLStateType() throws SQLException { + return 0; + } + + public boolean locatorsUpdateCopy() throws SQLException { + return false; + } + + public boolean supportsStatementPooling() throws SQLException { + return false; + } + + public RowIdLifetime getRowIdLifetime() throws SQLException { + return null; + } + + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return null; + } + + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return false; + } + + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return false; + } + + public ResultSet getClientInfoProperties() throws SQLException { + return null; + } + + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { + return null; + } + + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, + String columnNamePattern) throws SQLException { + return null; + } + + public T unwrap(Class iface) throws SQLException { + return null; + } + + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + //@Override only override for java 1.7 + public boolean generatedKeyAlwaysReturned() throws SQLException { + return false; + } + + //@Override only override for java 1.7 + public ResultSet getPseudoColumns(String arg0, String arg1, String arg2, String arg3) throws SQLException { + return null; + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateDriver.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateDriver.java new file mode 100644 index 0000000000000..4098293b8214d --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/database/connection/HibernateDriver.java @@ -0,0 +1,55 @@ +package io.quarkus.liquibase.database.connection; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.logging.Logger; + +import liquibase.database.LiquibaseExtDriver; +import liquibase.resource.ResourceAccessor; + +/** + * Implements the standard java.sql.Driver interface to allow the Hibernate integration to better fit into + * what Liquibase expects. + */ +public class HibernateDriver implements Driver, LiquibaseExtDriver { + + private ResourceAccessor resourceAccessor; + + public Connection connect(String url, Properties info) throws SQLException { + return new HibernateConnection(url, resourceAccessor); + } + + public boolean acceptsURL(String url) throws SQLException { + return url.startsWith("hibernate:"); + } + + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return new DriverPropertyInfo[0]; + } + + public int getMajorVersion() { + return 0; + } + + public int getMinorVersion() { + return 0; + } + + public boolean jdbcCompliant() { + return false; + } + + //@Override only override for java 1.7 + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setResourceAccessor(ResourceAccessor accessor) { + this.resourceAccessor = accessor; + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedColumnChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedColumnChangeGenerator.java new file mode 100644 index 0000000000000..3777e41e1546c --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedColumnChangeGenerator.java @@ -0,0 +1,83 @@ +package io.quarkus.liquibase.diff; + +import java.util.List; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.Difference; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.output.DiffOutputControl; +import liquibase.statement.DatabaseFunction; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.DataType; + +/** + * Hibernate and database types tend to look different even though they are not. + * The only change that we are handling it size change, and even for this one there are exceptions. + */ +public class ChangedColumnChangeGenerator extends liquibase.diff.output.changelog.core.ChangedColumnChangeGenerator { + + private static final List TYPES_TO_IGNORE_SIZE = List.of("TIMESTAMP", "TIME"); + + @Override + public int getPriority(Class objectType, Database database) { + if (Column.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + protected void handleTypeDifferences(Column column, ObjectDifferences differences, DiffOutputControl control, + List changes, Database referenceDatabase, Database comparisonDatabase) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + handleSizeChange(column, differences, control, changes, referenceDatabase, comparisonDatabase); + } else { + super.handleTypeDifferences(column, differences, control, changes, referenceDatabase, comparisonDatabase); + } + } + + private void handleSizeChange(Column column, ObjectDifferences differences, DiffOutputControl control, List changes, + Database referenceDatabase, Database comparisonDatabase) { + if (TYPES_TO_IGNORE_SIZE.stream().anyMatch(s -> s.equalsIgnoreCase(column.getType().getTypeName()))) { + return; + } + Difference difference = differences.getDifference("type"); + if (difference != null) { + for (Difference d : differences.getDifferences()) { + if (!(d.getReferenceValue() instanceof DataType)) { + differences.removeDifference(d.getField()); + continue; + } + Integer originalSize = ((DataType) d.getReferenceValue()).getColumnSize(); + Integer newSize = ((DataType) d.getComparedValue()).getColumnSize(); + if (newSize == null || originalSize == null || newSize.equals(originalSize)) { + differences.removeDifference(d.getField()); + } + } + super.handleTypeDifferences(column, differences, control, changes, referenceDatabase, comparisonDatabase); + } + } + + @Override + protected void handleDefaultValueDifferences(Column column, ObjectDifferences differences, DiffOutputControl control, + List changes, Database referenceDatabase, Database comparisonDatabase) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + Difference difference = differences.getDifference("defaultValue"); + if (difference != null && difference.getReferenceValue() == null + && difference.getComparedValue() instanceof DatabaseFunction) { + //database sometimes adds a function default value, like for timestamp columns + return; + } + difference = differences.getDifference("defaultValue"); + if (difference != null) { + super.handleDefaultValueDifferences(column, differences, control, changes, referenceDatabase, + comparisonDatabase); + } + // do nothing, types tend to not match with hibernate + } + super.handleDefaultValueDifferences(column, differences, control, changes, referenceDatabase, comparisonDatabase); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedForeignKeyChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedForeignKeyChangeGenerator.java new file mode 100644 index 0000000000000..32f83e3aef513 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedForeignKeyChangeGenerator.java @@ -0,0 +1,39 @@ +package io.quarkus.liquibase.diff; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.ForeignKey; + +/** + * Hibernate doesn't know about all the variations that occur with foreign keys but just whether the FK exists or not. + * To prevent changing customized foreign keys, we suppress all foreign key changes from hibernate. + */ +public class ChangedForeignKeyChangeGenerator extends liquibase.diff.output.changelog.core.ChangedForeignKeyChangeGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (ForeignKey.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixChanged(DatabaseObject changedObject, ObjectDifferences differences, DiffOutputControl control, + Database referenceDatabase, Database comparisonDatabase, ChangeGeneratorChain chain) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + differences.removeDifference("deleteRule"); + differences.removeDifference("updateRule"); + if (!differences.hasDifferences()) { + return null; + } + } + + return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedPrimaryKeyChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedPrimaryKeyChangeGenerator.java new file mode 100644 index 0000000000000..93cfdefa8e540 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedPrimaryKeyChangeGenerator.java @@ -0,0 +1,38 @@ +package io.quarkus.liquibase.diff; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.PrimaryKey; + +/** + * Hibernate doesn't know about all the variations that occur with primary keys, especially backing index stuff. + * To prevent changing customized primary keys, we suppress this kind of changes from hibernate side. + */ +public class ChangedPrimaryKeyChangeGenerator extends liquibase.diff.output.changelog.core.ChangedPrimaryKeyChangeGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (PrimaryKey.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixChanged(DatabaseObject changedObject, ObjectDifferences differences, DiffOutputControl control, + Database referenceDatabase, Database comparisonDatabase, ChangeGeneratorChain chain) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + differences.removeDifference("unique"); + if (!differences.hasDifferences()) { + return null; + } + } + + return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedSequenceChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedSequenceChangeGenerator.java new file mode 100644 index 0000000000000..9dfb74992365c --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedSequenceChangeGenerator.java @@ -0,0 +1,92 @@ +package io.quarkus.liquibase.diff; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.Difference; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Sequence; + +/** + * Hibernate manages sequences only by the name, startValue and incrementBy fields. + * However, non-hibernate databases might return default values for other fields triggering false positives. + */ +public class ChangedSequenceChangeGenerator extends liquibase.diff.output.changelog.core.ChangedSequenceChangeGenerator { + + private static final Set HIBERNATE_SEQUENCE_FIELDS; + + static { + HashSet hibernateSequenceFields = new HashSet<>(); + hibernateSequenceFields.add("name"); + hibernateSequenceFields.add("startValue"); + hibernateSequenceFields.add("incrementBy"); + HIBERNATE_SEQUENCE_FIELDS = Collections.unmodifiableSet(hibernateSequenceFields); + } + + @Override + public int getPriority(Class objectType, Database database) { + if (Sequence.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixChanged(DatabaseObject changedObject, ObjectDifferences differences, DiffOutputControl control, + Database referenceDatabase, Database comparisonDatabase, ChangeGeneratorChain chain) { + if (!(referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase)) { + return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain); + } + + // if any of the databases is a hibernate database, remove all differences that affect a field not managed by hibernate + Set ignoredDifferenceFields = differences.getDifferences().stream() + .map(Difference::getField) + .filter(differenceField -> !HIBERNATE_SEQUENCE_FIELDS.contains(differenceField)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + ignoredDifferenceFields.forEach(differences::removeDifference); + this.advancedIgnoredDifferenceFields(differences, referenceDatabase, comparisonDatabase); + return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain); + } + + /** + * In some cases a value that was 1 can be null in the database, or the name field can be different only by case. + * This method removes these differences from the list of differences so we don't generate a change for them. + */ + private void advancedIgnoredDifferenceFields(ObjectDifferences differences, Database referenceDatabase, + Database comparisonDatabase) { + Set ignoredDifferenceFields = new HashSet<>(); + for (Difference difference : differences.getDifferences()) { + String field = difference.getField(); + String refValue = difference.getReferenceValue() != null ? difference.getReferenceValue().toString() : null; + String comparedValue = difference.getComparedValue() != null ? difference.getComparedValue().toString() : null; + + // if the name field case is different and the databases are case-insensitive, we can ignore the difference + boolean isNameField = field.equals("name"); + boolean isCaseInsensitive = !referenceDatabase.isCaseSensitive() || !comparisonDatabase.isCaseSensitive(); + + // if the startValue or incrementBy fields are 1 and the other is null, we can ignore the difference + // Or 50, as it is the default value for hibernate for allocationSize: + // https://github.com/hibernate/hibernate-orm/blob/bda95dfbe75c68f5c1b77a2f21c403cbe08548a2/hibernate-core/src/main/java/org/hibernate/boot/model/IdentifierGeneratorDefinition.java#L252 + boolean isStartOrIncrementField = field.equals("startValue") || field.equals("incrementBy"); + boolean isOneOrFiftyAndNull = "1".equals(refValue) && comparedValue == null + || refValue == null && "1".equals(comparedValue) || + "50".equals(refValue) && comparedValue == null || refValue == null && "50".equals(comparedValue); + + if ((isNameField && isCaseInsensitive && refValue != null && refValue.equalsIgnoreCase(comparedValue)) || + (isStartOrIncrementField && isOneOrFiftyAndNull)) { + ignoredDifferenceFields.add(field); + } + } + ignoredDifferenceFields.forEach(differences::removeDifference); + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedUniqueConstraintChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedUniqueConstraintChangeGenerator.java new file mode 100644 index 0000000000000..77291bf25d669 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/ChangedUniqueConstraintChangeGenerator.java @@ -0,0 +1,39 @@ +package io.quarkus.liquibase.diff; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.ObjectDifferences; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.UniqueConstraint; + +/** + * Unique attribute for unique constraints backing index can have different values dependending on the database implementation, + * so we suppress all unique constraint changes based on unique constraints. + * + */ +public class ChangedUniqueConstraintChangeGenerator + extends liquibase.diff.output.changelog.core.ChangedUniqueConstraintChangeGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (UniqueConstraint.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixChanged(DatabaseObject changedObject, ObjectDifferences differences, DiffOutputControl control, + Database referenceDatabase, Database comparisonDatabase, ChangeGeneratorChain chain) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + differences.removeDifference("unique"); + if (!differences.hasDifferences()) { + return null; + } + } + return super.fixChanged(changedObject, differences, control, referenceDatabase, comparisonDatabase, chain); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/MissingSequenceChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/MissingSequenceChangeGenerator.java new file mode 100644 index 0000000000000..a76e4fe81d6ec --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/MissingSequenceChangeGenerator.java @@ -0,0 +1,32 @@ +package io.quarkus.liquibase.diff; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Sequence; + +public class MissingSequenceChangeGenerator extends liquibase.diff.output.changelog.core.MissingSequenceChangeGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (Sequence.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixMissing(DatabaseObject missingObject, DiffOutputControl control, Database referenceDatabase, + Database comparisonDatabase, ChangeGeneratorChain chain) { + if (referenceDatabase instanceof QuarkusDatabase && !comparisonDatabase.supportsSequences()) { + return null; + } else if (comparisonDatabase instanceof QuarkusDatabase && !referenceDatabase.supportsSequences()) { + return null; + } else { + return super.fixMissing(missingObject, control, referenceDatabase, comparisonDatabase, chain); + } + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/UnexpectedIndexChangeGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/UnexpectedIndexChangeGenerator.java new file mode 100644 index 0000000000000..11a7225c859b8 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/diff/UnexpectedIndexChangeGenerator.java @@ -0,0 +1,34 @@ +package io.quarkus.liquibase.diff; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.change.Change; +import liquibase.database.Database; +import liquibase.diff.output.DiffOutputControl; +import liquibase.diff.output.changelog.ChangeGeneratorChain; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Index; + +/** + * Indexes tend to be added in the database that don't correspond to what is in Hibernate, so we suppress all dropIndex changes + * based on indexes defined in the database but not in hibernate. + */ +public class UnexpectedIndexChangeGenerator extends liquibase.diff.output.changelog.core.UnexpectedIndexChangeGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (Index.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public Change[] fixUnexpected(DatabaseObject unexpectedObject, DiffOutputControl control, Database referenceDatabase, + Database comparisonDatabase, ChangeGeneratorChain chain) { + if (referenceDatabase instanceof QuarkusDatabase || comparisonDatabase instanceof QuarkusDatabase) { + return null; + } else { + return super.fixUnexpected(unexpectedObject, control, referenceDatabase, comparisonDatabase, chain); + } + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/CatalogSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/CatalogSnapshotGenerator.java new file mode 100644 index 0000000000000..eed7bf35f425f --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/CatalogSnapshotGenerator.java @@ -0,0 +1,30 @@ +package io.quarkus.liquibase.snapshot; + +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Catalog; + +/** + * Hibernate doesn't really support Catalogs, so just return the passed example back as if it had all the info it needed. + */ +public class CatalogSnapshotGenerator extends HibernateSnapshotGenerator { + + public CatalogSnapshotGenerator() { + super(Catalog.class); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return new Catalog(snapshot.getDatabase().getDefaultCatalogName()).setDefault(true); + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + // Nothing to add to + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ColumnSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ColumnSnapshotGenerator.java new file mode 100644 index 0000000000000..05060249549be --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ColumnSnapshotGenerator.java @@ -0,0 +1,215 @@ +package io.quarkus.liquibase.snapshot; + +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.id.ExportableColumn; +import org.hibernate.mapping.SimpleValue; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.Scope; +import liquibase.datatype.DataTypeFactory; +import liquibase.datatype.core.UnknownType; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.statement.DatabaseFunction; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.DataType; +import liquibase.structure.core.Relation; +import liquibase.structure.core.Table; +import liquibase.util.SqlUtil; +import liquibase.util.StringUtil; + +/** + * Columns are snapshotted along with with Tables in {@link TableSnapshotGenerator} but this class needs to be here to keep the + * default ColumnSnapshotGenerator from running. + * Ideally the column logic would be moved out of the TableSnapshotGenerator to better work in situations where the object types + * to snapshot are being controlled, but that is not the case yet. + */ +public class ColumnSnapshotGenerator extends HibernateSnapshotGenerator { + + private final static Pattern pattern = Pattern.compile("([^\\(]*)\\s*\\(?\\s*(\\d*)?\\s*,?\\s*(\\d*)?\\s*([^\\(]*?)\\)?"); + + public ColumnSnapshotGenerator() { + super(Column.class, new Class[] { Table.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + Column column = (Column) example; + if (column.getType() == null) { //not the actual full version found with the table + if (column.getRelation() == null) { + throw new InvalidExampleException("No relation set on " + column); + } + Relation relation = snapshot.get(column.getRelation()); + if (relation != null) { + for (Column columnSnapshot : relation.getColumns()) { + if (columnSnapshot.getName().equalsIgnoreCase(column.getName())) { + return columnSnapshot; + } + } + } + snapshotColumn((Column) example, snapshot); + return example; //did not find it + } else { + return example; + } + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (foundObject instanceof Table) { + org.hibernate.mapping.Table hibernateTable = findHibernateTable(foundObject, snapshot); + if (hibernateTable == null) { + return; + } + + for (org.hibernate.mapping.Column hibernateColumn : hibernateTable.getColumns()) { + Column column = new Column(); + column.setName(hibernateColumn.getName()); + column.setRelation((Table) foundObject); + + snapshotColumn(column, snapshot); + + ((Table) foundObject).getColumns().add(column); + + } + } + } + + protected void snapshotColumn(Column column, DatabaseSnapshot snapshot) throws DatabaseException { + QuarkusDatabase database = (QuarkusDatabase) snapshot.getDatabase(); + + org.hibernate.mapping.Table hibernateTable = findHibernateTable(column.getRelation(), snapshot); + if (hibernateTable == null) { + return; + } + + Dialect dialect = database.getDialect(); + MetadataImplementor metadata = (MetadataImplementor) database.getMetadata(); + + for (org.hibernate.mapping.Column hibernateColumn : hibernateTable.getColumns()) { + if (hibernateColumn.getName().equalsIgnoreCase(column.getName())) { + + String defaultValue = null; + String hibernateType = hibernateColumn.getSqlType(metadata.getTypeConfiguration(), dialect, metadata); + Matcher defaultValueMatcher = Pattern.compile("(?i) DEFAULT\\s+(.*)").matcher(hibernateType); + if (defaultValueMatcher.find()) { + defaultValue = defaultValueMatcher.group(1); + hibernateType = hibernateType.replace(defaultValueMatcher.group(0), ""); + } + + DataType dataType = toDataType(hibernateType, hibernateColumn.getSqlTypeCode()); + if (dataType == null) { + throw new DatabaseException("Unable to find column data type for column " + hibernateColumn.getName()); + } + + column.setType(dataType); + Scope.getCurrentScope().getLog(getClass()) + .info("Found column " + column.getName() + " " + column.getType().toString()); + + column.setRemarks(hibernateColumn.getComment()); + if (hibernateColumn.getValue() instanceof SimpleValue) { + DataType parseType; + if (DataTypeFactory.getInstance().from(dataType, database) instanceof UnknownType) { + parseType = new DataType(((SimpleValue) hibernateColumn.getValue()).getTypeName()); + } else { + parseType = dataType; + } + + if (defaultValue == null) { + defaultValue = hibernateColumn.getDefaultValue(); + } + + column.setDefaultValue(SqlUtil.parseValue( + snapshot.getDatabase(), + defaultValue, + parseType)); + } else { + column.setDefaultValue(hibernateColumn.getDefaultValue()); + } + column.setNullable(hibernateColumn.isNullable()); + column.setCertainDataType(false); + + org.hibernate.mapping.PrimaryKey hibernatePrimaryKey = hibernateTable.getPrimaryKey(); + if (hibernatePrimaryKey != null) { + boolean isPrimaryKeyColumn = false; + for (org.hibernate.mapping.Column pkColumn : (List) hibernatePrimaryKey + .getColumns()) { + if (pkColumn.getName().equalsIgnoreCase(hibernateColumn.getName())) { + isPrimaryKeyColumn = true; + break; + } + } + + if (isPrimaryKeyColumn) { + String identifierGeneratorStrategy; + + if (hibernateColumn instanceof ExportableColumn) { + //nothing + } else { + + identifierGeneratorStrategy = hibernateColumn.getValue().isSimpleValue() + ? ((SimpleValue) hibernateColumn.getValue()).getIdentifierGeneratorStrategy() + : null; + + if (("native".equalsIgnoreCase(identifierGeneratorStrategy) + || "identity".equalsIgnoreCase(identifierGeneratorStrategy))) { + if (PostgreSQLDialect.class.isAssignableFrom(dialect.getClass())) { + column.setAutoIncrementInformation(new Column.AutoIncrementInformation()); + String sequenceName = (column.getRelation().getName() + "_" + column.getName() + "_seq") + .toLowerCase(); + column.setDefaultValue(new DatabaseFunction("nextval('" + sequenceName + "'::regclass)")); + } else if (database.supportsAutoIncrement()) { + column.setAutoIncrementInformation(new Column.AutoIncrementInformation()); + } + } else if ("org.hibernate.id.enhanced.SequenceStyleGenerator".equals(identifierGeneratorStrategy)) { + Properties prop = ((SimpleValue) hibernateColumn.getValue()).getIdentifierGeneratorProperties(); + if (prop.get("sequence_name") == null) + column.setAutoIncrementInformation(new Column.AutoIncrementInformation()); + } + } + column.setNullable(false); + } + } + return; + } + } + } + + protected DataType toDataType(String hibernateType, Integer sqlTypeCode) throws DatabaseException { + Matcher matcher = pattern.matcher(hibernateType); + if (!matcher.matches()) { + return null; + } + DataType dataType = new DataType(matcher.group(1)); + if (matcher.group(3).isEmpty()) { + if (!matcher.group(2).isEmpty()) { + dataType.setColumnSize(Integer.parseInt(matcher.group(2))); + } + } else { + dataType.setColumnSize(Integer.parseInt(matcher.group(2))); + dataType.setDecimalDigits(Integer.parseInt(matcher.group(3))); + } + + String extra = StringUtil.trimToNull(matcher.group(4)); + if (extra != null) { + if (extra.equalsIgnoreCase("char")) { + dataType.setColumnSizeUnit(DataType.ColumnSizeUnit.CHAR); + } + } + + dataType.setDataTypeId(sqlTypeCode); + return dataType; + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ForeignKeySnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ForeignKeySnapshotGenerator.java new file mode 100644 index 0000000000000..6e2e5a793470a --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ForeignKeySnapshotGenerator.java @@ -0,0 +1,94 @@ +package io.quarkus.liquibase.snapshot; + +import java.util.Collection; +import java.util.Iterator; + +import org.hibernate.boot.spi.MetadataImplementor; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.diff.compare.DatabaseObjectComparatorFactory; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.ForeignKey; +import liquibase.structure.core.Table; + +public class ForeignKeySnapshotGenerator extends HibernateSnapshotGenerator { + + public ForeignKeySnapshotGenerator() { + super(ForeignKey.class, new Class[] { Table.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return example; + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(ForeignKey.class)) { + return; + } + if (foundObject instanceof Table) { + Table table = (Table) foundObject; + QuarkusDatabase database = (QuarkusDatabase) snapshot.getDatabase(); + MetadataImplementor metadata = (MetadataImplementor) database.getMetadata(); + + Collection tmapp = metadata.collectTableMappings(); + Iterator tableMappings = tmapp.iterator(); + while (tableMappings.hasNext()) { + org.hibernate.mapping.Table hibernateTable = (org.hibernate.mapping.Table) tableMappings.next(); + Iterator fkIterator = hibernateTable.getForeignKeyIterator(); + while (fkIterator.hasNext()) { + org.hibernate.mapping.ForeignKey hibernateForeignKey = (org.hibernate.mapping.ForeignKey) fkIterator.next(); + Table currentTable = new Table().setName(hibernateTable.getName()); + currentTable.setSchema(hibernateTable.getCatalog(), hibernateTable.getSchema()); + + org.hibernate.mapping.Table hibernateReferencedTable = hibernateForeignKey.getReferencedTable(); + Table referencedTable = new Table().setName(hibernateReferencedTable.getName()); + referencedTable.setSchema(hibernateReferencedTable.getCatalog(), hibernateReferencedTable.getSchema()); + + if (hibernateForeignKey.isCreationEnabled() && hibernateForeignKey.isPhysicalConstraint()) { + ForeignKey fk = new ForeignKey(); + fk.setName(hibernateForeignKey.getName()); + fk.setPrimaryKeyTable(referencedTable); + fk.setForeignKeyTable(currentTable); + for (Object column : hibernateForeignKey.getColumns()) { + fk.addForeignKeyColumn( + new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName())); + } + for (Object column : hibernateForeignKey.getReferencedColumns()) { + fk.addPrimaryKeyColumn( + new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName())); + } + if (fk.getPrimaryKeyColumns() == null || fk.getPrimaryKeyColumns().isEmpty()) { + for (Object column : hibernateReferencedTable.getPrimaryKey().getColumns()) { + fk.addPrimaryKeyColumn( + new liquibase.structure.core.Column(((org.hibernate.mapping.Column) column).getName())); + } + } + + fk.setDeferrable(false); + fk.setInitiallyDeferred(false); + + // Index index = new Index(); + // index.setName("IX_" + fk.getName()); + // index.setTable(fk.getForeignKeyTable()); + // index.setColumns(fk.getForeignKeyColumns()); + // fk.setBackingIndex(index); + // table.getIndexes().add(index); + + if (DatabaseObjectComparatorFactory.getInstance().isSameObject(currentTable, table, null, database)) { + table.getOutgoingForeignKeys().add(fk); + table.getSchema().addDatabaseObject(fk); + } + } + } + } + } + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/HibernateSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/HibernateSnapshotGenerator.java new file mode 100644 index 0000000000000..1bcef15867a5b --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/HibernateSnapshotGenerator.java @@ -0,0 +1,111 @@ +package io.quarkus.liquibase.snapshot; + +import java.util.Collection; +import java.util.Iterator; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.mapping.Table; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.database.Database; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.snapshot.SnapshotGenerator; +import liquibase.snapshot.SnapshotGeneratorChain; +import liquibase.structure.DatabaseObject; + +/** + * Base class for all Hibernate SnapshotGenerators + */ +public abstract class HibernateSnapshotGenerator implements SnapshotGenerator { + + private static final int PRIORITY_HIBERNATE_ADDITIONAL = 200; + private static final int PRIORITY_HIBERNATE_DEFAULT = 100; + + private Class defaultFor = null; + private Class[] addsTo = null; + + protected HibernateSnapshotGenerator(Class defaultFor) { + this.defaultFor = defaultFor; + } + + protected HibernateSnapshotGenerator(Class defaultFor, Class[] addsTo) { + this.defaultFor = defaultFor; + this.addsTo = addsTo; + } + + @Override + public Class[] replaces() { + return null; + } + + @Override + public final int getPriority(Class objectType, Database database) { + if (database instanceof QuarkusDatabase) { + if (defaultFor != null && defaultFor.isAssignableFrom(objectType)) { + return PRIORITY_HIBERNATE_DEFAULT; + } + if (addsTo() != null) { + for (Class type : addsTo()) { + if (type.isAssignableFrom(objectType)) { + return PRIORITY_HIBERNATE_ADDITIONAL; + } + } + } + } + return PRIORITY_NONE; + + } + + @Override + public final Class[] addsTo() { + return addsTo; + } + + @Override + public final DatabaseObject snapshot(DatabaseObject example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) + throws DatabaseException, InvalidExampleException { + if (defaultFor != null && defaultFor.isAssignableFrom(example.getClass())) { + DatabaseObject result = snapshotObject(example, snapshot); + return result; + } + DatabaseObject chainResponse = chain.snapshot(example, snapshot); + if (chainResponse == null) { + return null; + } + if (addsTo() != null) { + for (Class addType : addsTo()) { + if (addType.isAssignableFrom(example.getClass())) { + if (chainResponse != null) { + addTo(chainResponse, snapshot); + } + } + } + } + return chainResponse; + + } + + protected abstract DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException; + + protected abstract void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException; + + protected Table findHibernateTable(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException { + QuarkusDatabase database = (QuarkusDatabase) snapshot.getDatabase(); + MetadataImplementor metadata = (MetadataImplementor) database.getMetadata(); + + Collection tmapp = metadata.collectTableMappings(); + Iterator
tableMappings = tmapp.iterator(); + + while (tableMappings.hasNext()) { + Table hibernateTable = tableMappings.next(); + if (hibernateTable.getName().equalsIgnoreCase(example.getName())) { + return hibernateTable; + } + } + return null; + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/IndexSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/IndexSnapshotGenerator.java new file mode 100644 index 0000000000000..57e3238f707a9 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/IndexSnapshotGenerator.java @@ -0,0 +1,101 @@ +package io.quarkus.liquibase.snapshot; + +import liquibase.Scope; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.ForeignKey; +import liquibase.structure.core.Index; +import liquibase.structure.core.Relation; +import liquibase.structure.core.Table; +import liquibase.structure.core.UniqueConstraint; + +public class IndexSnapshotGenerator extends HibernateSnapshotGenerator { + + private static final String HIBERNATE_ORDER_ASC = "asc"; + private static final String HIBERNATE_ORDER_DESC = "desc"; + + @SuppressWarnings("unchecked") + public IndexSnapshotGenerator() { + super(Index.class, new Class[] { Table.class, ForeignKey.class, UniqueConstraint.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (example.getSnapshotId() != null) { + return example; + } + Relation table = ((Index) example).getRelation(); + var hibernateTable = findHibernateTable(table, snapshot); + if (hibernateTable == null) { + return example; + } + for (var hibernateIndex : hibernateTable.getIndexes().values()) { + Index index = handleHibernateIndex(table, hibernateIndex); + if (index.getColumnNames().equalsIgnoreCase(((Index) example).getColumnNames())) { + Scope.getCurrentScope().getLog(getClass()).info("Found index " + index.getName()); + table.getIndexes().add(index); + return index; + } + } + return example; + + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(Index.class)) { + return; + } + if (foundObject instanceof Table) { + Table table = (Table) foundObject; + org.hibernate.mapping.Table hibernateTable = findHibernateTable(table, snapshot); + if (hibernateTable == null) { + return; + } + for (var hibernateIndex : hibernateTable.getIndexes().values()) { + Index index = handleHibernateIndex(table, hibernateIndex); + Scope.getCurrentScope().getLog(getClass()).info("Found index " + index.getName()); + table.getIndexes().add(index); + } + } + } + + private Index handleHibernateIndex(Relation table, org.hibernate.mapping.Index hibernateIndex) { + Index index = new Index(); + index.setRelation(table); + index.setName(hibernateIndex.getName()); + index.setUnique(isUniqueIndex(hibernateIndex)); + for (var hibernateColumn : hibernateIndex.getColumns()) { + String hibernateOrder = hibernateIndex.getColumnOrderMap().get(hibernateColumn); + Boolean descending = HIBERNATE_ORDER_ASC.equals(hibernateOrder) + ? Boolean.FALSE + : (HIBERNATE_ORDER_DESC.equals(hibernateOrder) ? Boolean.TRUE : null); + index.getColumns().add(new Column(hibernateColumn.getName()).setRelation(table).setDescending(descending)); + } + return index; + } + + private Boolean isUniqueIndex(org.hibernate.mapping.Index hibernateIndex) { + /* + * This seems to be necessary to explicitly tell liquibase that there's no + * actual diff in certain non-unique indexes + */ + if (hibernateIndex.getColumnSpan() == 1) { + var col = hibernateIndex.getColumns().get(0); + return col.isUnique(); + } else { + /* + * It seems that because Hibernate does not implement the unique property of the Jpa composite index, + * the diff command appears 'diffence', because the unique property of the entity index is 'null', + * and the value read from the database is 'false', resulting in the generated changeSet after the Drop and + * Recreate Index. + */ + return false; + } + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/PrimaryKeySnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/PrimaryKeySnapshotGenerator.java new file mode 100644 index 0000000000000..fac1affab8367 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/PrimaryKeySnapshotGenerator.java @@ -0,0 +1,73 @@ +package io.quarkus.liquibase.snapshot; + +import org.hibernate.sql.Alias; + +import liquibase.Scope; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.Index; +import liquibase.structure.core.PrimaryKey; +import liquibase.structure.core.Table; + +public class PrimaryKeySnapshotGenerator extends HibernateSnapshotGenerator { + + private static final int PK_NAME_LENGTH = 63; + private static final String PK_NAME_SUFFIX = "PK"; + private static final Alias PK_NAME_ALIAS = new Alias(PK_NAME_LENGTH, PK_NAME_SUFFIX); + + public PrimaryKeySnapshotGenerator() { + super(PrimaryKey.class, new Class[] { Table.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return example; + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(PrimaryKey.class)) { + return; + } + if (foundObject instanceof Table) { + Table table = (Table) foundObject; + org.hibernate.mapping.Table hibernateTable = findHibernateTable(table, snapshot); + if (hibernateTable == null) { + return; + } + org.hibernate.mapping.PrimaryKey hibernatePrimaryKey = hibernateTable.getPrimaryKey(); + if (hibernatePrimaryKey != null) { + PrimaryKey pk = new PrimaryKey(); + String hbnTableName = hibernateTable.getName(); + + String pkName = PK_NAME_ALIAS.toAliasString(hbnTableName); + if (pkName.length() == PK_NAME_LENGTH) { + String suffix = "_" + Integer.toHexString(hbnTableName.hashCode()).toUpperCase() + "_" + PK_NAME_SUFFIX; + pkName = pkName.substring(0, PK_NAME_LENGTH - suffix.length()) + suffix; + } + pk.setName(pkName); + + pk.setTable(table); + for (org.hibernate.mapping.Column hibernateColumn : hibernatePrimaryKey.getColumns()) { + pk.getColumns().add(new Column(hibernateColumn.getName()).setRelation(table)); + } + + Scope.getCurrentScope().getLog(getClass()).info("Found primary key " + pk.getName()); + table.setPrimaryKey(pk); + Index index = new Index(); + index.setName("IX_" + pk.getName()); + index.setRelation(table); + index.setColumns(pk.getColumns()); + index.setUnique(true); + pk.setBackingIndex(index); + table.getIndexes().add(index); + } + } + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SchemaSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SchemaSnapshotGenerator.java new file mode 100644 index 0000000000000..46451325af07c --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SchemaSnapshotGenerator.java @@ -0,0 +1,32 @@ +package io.quarkus.liquibase.snapshot; + +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Catalog; +import liquibase.structure.core.Schema; + +/** + * Hibernate doesn't really support Schemas, so just return the passed example back as if it had all the info it needed. + */ +public class SchemaSnapshotGenerator extends HibernateSnapshotGenerator { + + public SchemaSnapshotGenerator() { + super(Schema.class, new Class[] { Catalog.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return new Schema(snapshot.getDatabase().getDefaultCatalogName(), snapshot.getDatabase().getDefaultSchemaName()) + .setDefault(true); + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + // Nothing to do + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SequenceSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SequenceSnapshotGenerator.java new file mode 100644 index 0000000000000..1efd3bce55abf --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/SequenceSnapshotGenerator.java @@ -0,0 +1,52 @@ +package io.quarkus.liquibase.snapshot; + +import java.math.BigInteger; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Schema; +import liquibase.structure.core.Sequence; + +/** + * Sequence snapshots are not yet supported, but this class needs to be implemented in order to prevent the default + * SequenceSnapshotGenerator from running. + */ +public class SequenceSnapshotGenerator extends HibernateSnapshotGenerator { + + public SequenceSnapshotGenerator() { + super(Sequence.class, new Class[] { Schema.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return example; + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(Sequence.class)) { + return; + } + + if (foundObject instanceof Schema) { + Schema schema = (Schema) foundObject; + QuarkusDatabase database = (QuarkusDatabase) snapshot.getDatabase(); + for (org.hibernate.boot.model.relational.Namespace namespace : database.getMetadata().getDatabase() + .getNamespaces()) { + for (org.hibernate.boot.model.relational.Sequence sequence : namespace.getSequences()) { + schema.addDatabaseObject(new Sequence() + .setName(sequence.getName().getSequenceName().getText()) + .setSchema(schema) + .setStartValue(BigInteger.valueOf(sequence.getInitialValue())) + .setIncrementBy(BigInteger.valueOf(sequence.getIncrementSize()))); + } + } + } + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/TableSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/TableSnapshotGenerator.java new file mode 100644 index 0000000000000..e7e0e8a5fac0a --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/TableSnapshotGenerator.java @@ -0,0 +1,127 @@ +package io.quarkus.liquibase.snapshot; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.generator.Generator; +import org.hibernate.mapping.Join; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.RootClass; +import org.hibernate.mapping.SimpleValue; + +import io.quarkus.liquibase.database.QuarkusDatabase; +import io.quarkus.liquibase.snapshot.extension.ExtendedSnapshotGenerator; +import io.quarkus.liquibase.snapshot.extension.TableGeneratorSnapshotGenerator; +import liquibase.Scope; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Schema; +import liquibase.structure.core.Table; + +public class TableSnapshotGenerator extends HibernateSnapshotGenerator { + + private List> tableIdGenerators = new ArrayList<>(); + + public TableSnapshotGenerator() { + super(Table.class, new Class[] { Schema.class }); + tableIdGenerators.add(new TableGeneratorSnapshotGenerator()); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (example.getSnapshotId() != null) { + return example; + } + org.hibernate.mapping.Table hibernateTable = findHibernateTable(example, snapshot); + if (hibernateTable == null) { + return example; + } + + Table table = new Table().setName(hibernateTable.getName()); + Scope.getCurrentScope().getLog(getClass()).info("Found table " + table.getName()); + table.setSchema(example.getSchema()); + if (hibernateTable.getComment() != null && !hibernateTable.getComment().isEmpty()) { + table.setRemarks(hibernateTable.getComment()); + } + + return table; + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(Table.class)) { + return; + } + + if (foundObject instanceof Schema) { + + Schema schema = (Schema) foundObject; + QuarkusDatabase database = (QuarkusDatabase) snapshot.getDatabase(); + MetadataImplementor metadata = (MetadataImplementor) database.getMetadata(); + + Collection entityBindings = metadata.getEntityBindings(); + Iterator tableMappings = entityBindings.iterator(); + + while (tableMappings.hasNext()) { + PersistentClass pc = tableMappings.next(); + + org.hibernate.mapping.Table hibernateTable = pc.getTable(); + if (hibernateTable.isPhysicalTable()) { + addDatabaseObjectToSchema(hibernateTable, schema, snapshot); + + Collection joins = pc.getJoins(); + Iterator joinMappings = joins.iterator(); + while (joinMappings.hasNext()) { + Join join = joinMappings.next(); + addDatabaseObjectToSchema(join.getTable(), schema, snapshot); + } + } + } + + Iterator classMappings = entityBindings.iterator(); + while (classMappings.hasNext()) { + PersistentClass persistentClass = classMappings.next(); + if (!persistentClass.isInherited() && persistentClass.getIdentifier() instanceof SimpleValue) { + var simpleValue = (SimpleValue) persistentClass.getIdentifier(); + Generator ig = simpleValue.createGenerator( + metadata.getMetadataBuildingOptions().getIdentifierGeneratorFactory(), + database.getDialect(), + (RootClass) persistentClass); + for (ExtendedSnapshotGenerator tableIdGenerator : tableIdGenerators) { + if (tableIdGenerator.supports(ig)) { + Table idTable = tableIdGenerator.snapshot(ig); + idTable.setSchema(schema); + schema.addDatabaseObject(snapshotObject(idTable, snapshot)); + break; + } + } + } + } + + Collection collectionBindings = metadata.getCollectionBindings(); + Iterator collIter = collectionBindings.iterator(); + while (collIter.hasNext()) { + org.hibernate.mapping.Collection coll = collIter.next(); + org.hibernate.mapping.Table hTable = coll.getCollectionTable(); + if (hTable.isPhysicalTable()) { + addDatabaseObjectToSchema(hTable, schema, snapshot); + } + } + } + } + + private void addDatabaseObjectToSchema(org.hibernate.mapping.Table join, Schema schema, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + Table joinTable = new Table().setName(join.getName()); + joinTable.setSchema(schema); + Scope.getCurrentScope().getLog(getClass()).info("Found table " + joinTable.getName()); + schema.addDatabaseObject(snapshotObject(joinTable, snapshot)); + } +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/UniqueConstraintSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/UniqueConstraintSnapshotGenerator.java new file mode 100644 index 0000000000000..e48565459a406 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/UniqueConstraintSnapshotGenerator.java @@ -0,0 +1,119 @@ +package io.quarkus.liquibase.snapshot; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.hibernate.HibernateException; + +import liquibase.Scope; +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Column; +import liquibase.structure.core.Index; +import liquibase.structure.core.Table; +import liquibase.structure.core.UniqueConstraint; +import liquibase.util.StringUtil; + +public class UniqueConstraintSnapshotGenerator extends HibernateSnapshotGenerator { + + public UniqueConstraintSnapshotGenerator() { + super(UniqueConstraint.class, new Class[] { Table.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + return example; + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + if (!snapshot.getSnapshotControl().shouldInclude(UniqueConstraint.class)) { + return; + } + + if (foundObject instanceof Table) { + Table table = (Table) foundObject; + org.hibernate.mapping.Table hibernateTable = findHibernateTable(table, snapshot); + if (hibernateTable == null) { + return; + } + for (var hibernateUnique : hibernateTable.getUniqueKeys().values()) { + UniqueConstraint uniqueConstraint = new UniqueConstraint(); + uniqueConstraint.setName(hibernateUnique.getName()); + uniqueConstraint.setRelation(table); + uniqueConstraint.setClustered(false); // No way to set true via Hibernate + + int i = 0; + for (var hibernateColumn : hibernateUnique.getColumns()) { + uniqueConstraint.addColumn(i++, new Column(hibernateColumn.getName()).setRelation(table)); + } + + Index index = getBackingIndex(uniqueConstraint, hibernateTable, snapshot); + uniqueConstraint.setBackingIndex(index); + + Scope.getCurrentScope().getLog(getClass()).info("Found unique constraint " + uniqueConstraint); + table.getUniqueConstraints().add(uniqueConstraint); + } + for (var column : hibernateTable.getColumns()) { + if (column.isUnique()) { + UniqueConstraint uniqueConstraint = new UniqueConstraint(); + uniqueConstraint.setRelation(table); + uniqueConstraint.setClustered(false); // No way to set true via Hibernate + String name = "UC_" + table.getName().toUpperCase() + column.getName().toUpperCase() + "_COL"; + if (name.length() > 64) { + name = name.substring(0, 63); + } + uniqueConstraint.addColumn(0, new Column(column.getName()).setRelation(table)); + uniqueConstraint.setName(name); + Scope.getCurrentScope().getLog(getClass()).info("Found unique constraint " + uniqueConstraint); + table.getUniqueConstraints().add(uniqueConstraint); + + Index index = getBackingIndex(uniqueConstraint, hibernateTable, snapshot); + uniqueConstraint.setBackingIndex(index); + + } + } + + for (UniqueConstraint uc : table.getUniqueConstraints()) { + if (uc.getName() == null || uc.getName().isEmpty()) { + String name = table.getName() + uc.getColumnNames(); + name = "UCIDX" + hashedName(name); + uc.setName(name); + } + } + } + } + + private String hashedName(String s) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.reset(); + md.update(s.getBytes()); + byte[] digest = md.digest(); + BigInteger bigInt = new BigInteger(1, digest); + // By converting to base 35 (full alphanumeric), we guarantee + // that the length of the name will always be smaller than the 30 + // character identifier restriction enforced by a few dialects. + return bigInt.toString(35); + } catch (NoSuchAlgorithmException e) { + throw new HibernateException("Unable to generate a hashed name!", e); + } + } + + protected Index getBackingIndex(UniqueConstraint uniqueConstraint, org.hibernate.mapping.Table hibernateTable, + DatabaseSnapshot snapshot) { + Index index = new Index(); + index.setRelation(uniqueConstraint.getRelation()); + index.setColumns(uniqueConstraint.getColumns()); + index.setUnique(true); + index.setName(String.format("%s_%s_IX", hibernateTable.getName(), StringUtil.randomIdentifer(4))); + + return index; + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ViewSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ViewSnapshotGenerator.java new file mode 100644 index 0000000000000..02de4770fce99 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/ViewSnapshotGenerator.java @@ -0,0 +1,33 @@ +package io.quarkus.liquibase.snapshot; + +import liquibase.exception.DatabaseException; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Schema; +import liquibase.structure.core.View; + +/** + * View snapshots are not supported from hibernate, but this class needs to be implemented in order to prevent the default + * ViewSnapshotGenerator from running. + */ +public class ViewSnapshotGenerator extends HibernateSnapshotGenerator { + + public ViewSnapshotGenerator() { + super(View.class, new Class[] { Schema.class }); + } + + @Override + protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + throw new DatabaseException("No views in Hibernate mapping"); + } + + @Override + protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) + throws DatabaseException, InvalidExampleException { + // No views in Hibernate mapping + + } + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/ExtendedSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/ExtendedSnapshotGenerator.java new file mode 100644 index 0000000000000..8ec7f4052325d --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/ExtendedSnapshotGenerator.java @@ -0,0 +1,9 @@ +package io.quarkus.liquibase.snapshot.extension; + +public interface ExtendedSnapshotGenerator { + + U snapshot(T object); + + boolean supports(T object); + +} diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/TableGeneratorSnapshotGenerator.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/TableGeneratorSnapshotGenerator.java new file mode 100644 index 0000000000000..afd75d43d2b4e --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/snapshot/extension/TableGeneratorSnapshotGenerator.java @@ -0,0 +1,52 @@ +package io.quarkus.liquibase.snapshot.extension; + +import org.hibernate.generator.Generator; +import org.hibernate.id.enhanced.TableGenerator; + +import liquibase.structure.core.Column; +import liquibase.structure.core.DataType; +import liquibase.structure.core.PrimaryKey; +import liquibase.structure.core.Table; + +public class TableGeneratorSnapshotGenerator implements ExtendedSnapshotGenerator { + + private static final String PK_DATA_TYPE = "varchar"; + private static final String VALUE_DATA_TYPE = "bigint"; + + @Override + public Table snapshot(Generator ig) { + TableGenerator tableGenerator = (TableGenerator) ig; + Table table = new Table().setName(tableGenerator.getTableName()); + + Column pkColumn = new Column(); + pkColumn.setName(tableGenerator.getSegmentColumnName()); + DataType pkDataType = new DataType(PK_DATA_TYPE); + pkDataType.setColumnSize(tableGenerator.getSegmentValueLength()); + pkColumn.setType(pkDataType); + pkColumn.setCertainDataType(false); + pkColumn.setRelation(table); + table.getColumns().add(pkColumn); + + PrimaryKey primaryKey = new PrimaryKey(); + primaryKey.setName(tableGenerator.getTableName() + "PK"); + primaryKey.addColumn(0, new Column(pkColumn.getName()).setRelation(table)); + primaryKey.setTable(table); + table.setPrimaryKey(primaryKey); + + Column valueColumn = new Column(); + valueColumn.setName(tableGenerator.getValueColumnName()); + valueColumn.setType(new DataType(VALUE_DATA_TYPE)); + valueColumn.setNullable(false); + valueColumn.setCertainDataType(false); + valueColumn.setRelation(table); + table.getColumns().add(valueColumn); + + return table; + } + + @Override + public boolean supports(Generator ig) { + return ig instanceof TableGenerator; + } + +} diff --git a/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.database.Database b/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.database.Database new file mode 100644 index 0000000000000..7402ef367db50 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.database.Database @@ -0,0 +1 @@ +io.quarkus.liquibase.database.QuarkusDatabase \ No newline at end of file diff --git a/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator b/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator new file mode 100644 index 0000000000000..0069f209b3183 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/resources/META-INF/services/liquibase.snapshot.SnapshotGenerator @@ -0,0 +1,10 @@ +io.quarkus.liquibase.snapshot.CatalogSnapshotGenerator +io.quarkus.liquibase.snapshot.ColumnSnapshotGenerator +io.quarkus.liquibase.snapshot.ForeignKeySnapshotGenerator +io.quarkus.liquibase.snapshot.IndexSnapshotGenerator +io.quarkus.liquibase.snapshot.PrimaryKeySnapshotGenerator +io.quarkus.liquibase.snapshot.SchemaSnapshotGenerator +io.quarkus.liquibase.snapshot.SequenceSnapshotGenerator +io.quarkus.liquibase.snapshot.TableSnapshotGenerator +io.quarkus.liquibase.snapshot.UniqueConstraintSnapshotGenerator +io.quarkus.liquibase.snapshot.ViewSnapshotGenerator diff --git a/extensions/liquibase/runtime/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator b/extensions/liquibase/runtime/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator new file mode 100644 index 0000000000000..7402ef367db50 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator @@ -0,0 +1 @@ +io.quarkus.liquibase.database.QuarkusDatabase \ No newline at end of file