diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java new file mode 100644 index 000000000000..cc518cddc8d6 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLDialect.java @@ -0,0 +1,226 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import org.hibernate.community.dialect.sequence.SpannerPostgreSQLSequenceSupport; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SimpleDatabaseVersion; +import org.hibernate.dialect.sequence.SequenceSupport; +import org.hibernate.dialect.unique.AlterTableUniqueIndexDelegate; +import org.hibernate.dialect.unique.UniqueDelegate; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.procedure.internal.StandardCallableStatementSupport; +import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.tool.schema.internal.StandardTableExporter; + +public class SpannerPostgreSQLDialect extends PostgreSQLDialect { + + private final UniqueDelegate SPANNER_UNIQUE_DELEGATE = new AlterTableUniqueIndexDelegate( this ); + private final StandardTableExporter SPANNER_TABLE_EXPORTER = new SpannerPostgreSQLTableExporter( this ); + + public SpannerPostgreSQLDialect() { + super(); + } + + public SpannerPostgreSQLDialect(DialectResolutionInfo info) { + super( info ); + } + + public SpannerPostgreSQLDialect(DatabaseVersion version) { + super( version ); + } + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return SimpleDatabaseVersion.ZERO_VERSION; + } + + @Override + public StandardTableExporter getTableExporter() { + return SPANNER_TABLE_EXPORTER; + } + + @Override + public UniqueDelegate getUniqueDelegate() { + return SPANNER_UNIQUE_DELEGATE; + } + + @Override + public SequenceSupport getSequenceSupport() { + return SpannerPostgreSQLSequenceSupport.INSTANCE; + } + + @Override + public boolean supportsUserDefinedTypes() { + return false; + } + + @Override + public boolean supportsFilterClause() { + return false; + } + + @Override + public boolean supportsRecursiveCycleUsingClause() { + return false; + } + + @Override + public boolean supportsRecursiveSearchClause() { + return false; + } + + @Override + public boolean supportsUniqueConstraints() { + return false; + } + + @Override + public boolean supportsRowValueConstructorGtLtSyntax() { + return false; + } + + // ALL subqueries with operators other than <>/!= are not supported + @Override + public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + public boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return false; + } + + @Override + public boolean supportsCaseInsensitiveLike() { + return false; + } + + @Override + public String currentTimestamp() { + return currentTimestampWithTimeZone(); + } + + @Override + public String currentTime() { + return currentTimestampWithTimeZone(); + } + + @Override + public boolean supportsLateral() { + return false; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return false; + } + + @Override + public int getMaxVarcharLength() { + return 2_621_440; + } + + @Override + public int getMaxVarbinaryLength() { + //max is equivalent 10 MiB + return 10_485_760; + } + + @Override + public String getCurrentSchemaCommand() { + return ""; + } + + @Override + public boolean supportsCommentOn() { + return false; + } + + @Override + public boolean supportsWindowFunctions() { + return false; + } + + @Override + public String getAddForeignKeyConstraintString( + String constraintName, + String[] foreignKey, + String referencedTable, + String[] primaryKey, + boolean referencesPrimaryKey) { + if ( !referencesPrimaryKey ) { + throw new UnsupportedOperationException( + "Cannot add foreign keys without reference table's primary key" ); + } + return super.getAddForeignKeyConstraintString( constraintName, foreignKey, referencedTable, primaryKey, referencesPrimaryKey ); + } + + @Override + public boolean canBatchTruncate() { + return false; + } + + @Override + public String rowId(String rowId) { + return null; + } + + @Override + public boolean supportsRowConstructor() { + return false; + } + + @Override + public String getTruncateTableStatement(String tableName) { + return "delete from " + tableName; + } + + @Override + public String getBeforeDropStatement() { + return null; + } + + @Override + public String getCascadeConstraintsString() { + return ""; + } + + @Override + public boolean supportsIfExistsBeforeConstraintName() { + return false; + } + + @Override + public boolean supportsIfExistsAfterAlterTable() { + return false; + } + + @Override + public boolean supportsDistinctFromPredicate() { + return false; + } + + @Override + public boolean supportsPartitionBy() { + return false; + } + + @Override + public boolean supportsNonQueryWithCTE() { + return false; + } + + @Override + public boolean supportsRecursiveCTE() { + return false; + } + + @Override + public CallableStatementSupport getCallableStatementSupport() { + return StandardCallableStatementSupport.NO_REF_CURSOR_INSTANCE; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java new file mode 100644 index 000000000000..6c9350509450 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SpannerPostgreSQLTableExporter.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.mapping.Column; +import org.hibernate.tool.schema.internal.ColumnValue; +import org.hibernate.mapping.Index; +import org.hibernate.mapping.PrimaryKey; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.tool.schema.internal.StandardTableExporter; + +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class SpannerPostgreSQLTableExporter extends StandardTableExporter { + + public SpannerPostgreSQLTableExporter(Dialect dialect) { + super( dialect ); + } + + @Override + public String[] getSqlCreateStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { + // Spanner mandates that primary key should be present in all the tables. For element collection tables, + // there will be no primary key. In order to fix the problem, we randomly generate the ID column + // with BIT_REVERSED_POSITIVE sequence + if ( !table.hasPrimaryKey() && !table.getForeignKeyCollection().isEmpty() ) { + Column column = getAutoGeneratedPrimaryKeyColumn( table, metadata ); + table.addColumn( column ); + + PrimaryKey primaryKey = new PrimaryKey( table ); + primaryKey.addColumn( column ); + + table.setPrimaryKey( primaryKey ); + } + + return super.getSqlCreateStrings( table, metadata, context ); + } + + @Override + public String[] getSqlDropStrings(Table table, Metadata metadata, SqlStringGenerationContext context) { + // Spanner requires the indexes to be dropped before dropping the table + List sqlDropIndexStrings = new ArrayList<>(); + for ( Index index : table.getIndexes().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString(index.getName()) ); + } + // Spanner requires all the unique indexes to be dropped before dropping the tables + for ( UniqueKey uniqueKey : table.getUniqueKeys().values() ) { + sqlDropIndexStrings.add( sqlDropIndexString(uniqueKey.getName()) ); + } + for ( Column column : table.getColumns() ) { + if ( column.isUnique() ) { + sqlDropIndexStrings.add( sqlDropIndexString(column.getUniqueKeyName()) ); + } + } + String[] sqlDropStrings = super.getSqlDropStrings( table, metadata, context ); + return Stream.concat( sqlDropIndexStrings.stream(), Stream.of( sqlDropStrings ) ) + .toArray( String[]::new ); + } + + private String sqlDropIndexString(String indexName) { + return "drop index if exists " + indexName; + } + + private Column getAutoGeneratedPrimaryKeyColumn(Table table, Metadata metadata) { + Column column = new Column( "rowid" ); + column.setSqlTypeCode( Types.BIGINT ); + column.setNullable( false ); + column.setSqlType( "bigint" ); + column.setOptions( "hidden" ); + column.setIdentity( true ); + column.setValue( new ColumnValue( metadata.getDatabase(), table, column, metadata.getDatabase().getTypeConfiguration().getBasicTypeForJavaType( Long.class ) ) ); + return column; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java new file mode 100644 index 000000000000..ae2dcc6de88e --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/SpannerPostgreSQLSequenceSupport.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.sequence; + +import org.hibernate.MappingException; +import org.hibernate.dialect.sequence.PostgreSQLSequenceSupport; +import org.hibernate.dialect.sequence.SequenceSupport; + +public class SpannerPostgreSQLSequenceSupport extends PostgreSQLSequenceSupport { + + public static final SequenceSupport INSTANCE = new SpannerPostgreSQLSequenceSupport(); + + @Override + public String getCreateSequenceString(String sequenceName, int initialValue, int incrementSize) throws MappingException { + if ( incrementSize == 0 ) { + throw new MappingException( "Unable to create the sequence [" + sequenceName + "]: the increment size must not be 0" ); + } + return getCreateSequenceString( sequenceName ) + + startingValue( initialValue, incrementSize ) + + " start counter with " + initialValue; + } + + @Override + public String getRestartSequenceString(String sequenceName, long startWith) { + return "alter sequence " + sequenceName + " restart counter with " + startWith; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java index b206caffaf9d..6d89eac5f3c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java @@ -231,7 +231,7 @@ public Dialect createDialect(DialectResolutionInfo info) { } @Override public boolean productNameMatches(String databaseName) { - return databaseName.startsWith( "Google Cloud Spanner" ); + return databaseName.equals( "Google Cloud Spanner" ); } @Override public String getDriverClassName(String jdbcUrl) { diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/ExportableColumnHelper.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/ExportableColumnHelper.java index d275408a15e3..f5f4091c29a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/ExportableColumnHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/ExportableColumnHelper.java @@ -4,156 +4,18 @@ */ package org.hibernate.id.enhanced; -import org.hibernate.FetchMode; -import org.hibernate.MappingException; import org.hibernate.boot.model.relational.Database; -import org.hibernate.boot.spi.MetadataBuildingContext; -import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.tool.schema.internal.ColumnValue; import org.hibernate.mapping.Column; -import org.hibernate.mapping.Selectable; import org.hibernate.mapping.Table; -import org.hibernate.mapping.Value; -import org.hibernate.mapping.ValueVisitor; -import org.hibernate.service.ServiceRegistry; import org.hibernate.type.BasicType; -import org.hibernate.type.MappingContext; -import org.hibernate.type.Type; - -import java.util.List; class ExportableColumnHelper { static Column column(Database database, Table table, String segmentColumnName, BasicType type, String typeName) { final var column = new Column( segmentColumnName ); column.setSqlType( typeName ); - column.setValue( new Value() { - @Override - public Value copy() { - return this; - } - - @Override - public int getColumnSpan() { - return 1; - } - - @Override - public List getSelectables() { - return List.of( column ); - } - - @Override - public List getColumns() { - return List.of( column ); - } - - @Override - public boolean hasColumns() { - return true; - } - - @Override - public Type getType() { - return type; - } - - @Override - public FetchMode getFetchMode() { - return null; - } - - @Override - public Table getTable() { - return table; - } - - @Override - public boolean hasFormula() { - return false; - } - - @Override - public boolean isAlternateUniqueKey() { - return false; - } - - @Override - public boolean isNullable() { - return false; - } - - @Override - public boolean[] getColumnInsertability() { - return ArrayHelper.TRUE; - } - - @Override - public boolean hasAnyInsertableColumns() { - return true; - } - - @Override - public boolean[] getColumnUpdateability() { - return ArrayHelper.TRUE; - } - - @Override - public boolean hasAnyUpdatableColumns() { - return true; - } - - @Override - public void createForeignKey() { - } - - @Override - public void createUniqueKey(MetadataBuildingContext context) { - } - - @Override - public boolean isSimpleValue() { - return true; - } - - @Override - public boolean isValid(MappingContext mappingContext) throws MappingException { - return false; - } - - @Override - public void setTypeUsingReflection(String className, String propertyName) { - } - - @Override - public Object accept(ValueVisitor visitor) { - return null; - } - - @Override - public boolean isSame(Value value) { - return this == value; - } - - @Override - public ServiceRegistry getServiceRegistry() { - return database.getServiceRegistry(); - } - - @Override - public boolean isColumnInsertable(int index) { - return true; - } - - @Override - public boolean isColumnUpdateable(int index) { - return true; - } - - @Override - public boolean isPartitionKey() { - return false; - } - } ); + column.setValue( new ColumnValue( database, table, column, type ) ); return column; } } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnValue.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnValue.java new file mode 100644 index 000000000000..9ebf794fc057 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/ColumnValue.java @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.tool.schema.internal; + +import java.util.List; + +import org.hibernate.FetchMode; +import org.hibernate.MappingException; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Selectable; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.Value; +import org.hibernate.mapping.ValueVisitor; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.MappingContext; +import org.hibernate.type.Type; + +public class ColumnValue implements Value { + + private final Database database; + private final Table table; + private final Column column; + private final Type type; + + public ColumnValue(Database database, Table table, Column column, Type type) { + this.database = database; + this.table = table; + this.column = column; + this.type = type; + } + + @Override + public Value copy() { + return this; + } + + @Override + public int getColumnSpan() { + return 1; + } + + @Override + public List getSelectables() { + return List.of( column ); + } + + @Override + public List getColumns() { + return List.of( column ); + } + + @Override + public boolean hasColumns() { + return true; + } + + @Override + public Type getType() { + return type; + } + + @Override + public FetchMode getFetchMode() { + return null; + } + + @Override + public Table getTable() { + return table; + } + + @Override + public boolean hasFormula() { + return false; + } + + @Override + public boolean isAlternateUniqueKey() { + return false; + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public boolean[] getColumnInsertability() { + return ArrayHelper.TRUE; + } + + @Override + public boolean hasAnyInsertableColumns() { + return true; + } + + @Override + public boolean[] getColumnUpdateability() { + return ArrayHelper.TRUE; + } + + @Override + public boolean hasAnyUpdatableColumns() { + return true; + } + + @Override + public void createForeignKey() { + } + + @Override + public void createUniqueKey(MetadataBuildingContext context) { + } + + @Override + public boolean isSimpleValue() { + return true; + } + + @Override + public boolean isValid(MappingContext mappingContext) throws MappingException { + return false; + } + + @Override + public void setTypeUsingReflection(String className, String propertyName) { + } + + @Override + public Object accept(ValueVisitor visitor) { + return null; + } + + @Override + public boolean isSame(Value value) { + return this == value; + } + + @Override + public ServiceRegistry getServiceRegistry() { + return database.getServiceRegistry(); + } + + @Override + public boolean isColumnInsertable(int index) { + return true; + } + + @Override + public boolean isColumnUpdateable(int index) { + return true; + } + + @Override + public boolean isPartitionKey() { + return false; + } +}