From 21095565bc36e1c4876e7edb76b3813e96c26b0d Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 20 Nov 2024 16:31:42 -0600 Subject: [PATCH] HHH-7913 - Schema replacement in / @Subselect --- .../userguide/chapters/domain/entity.adoc | 6 + .../SqlStringGenerationContext.java | 65 +++++++- .../SqlStringGenerationContextImpl.java | 37 ----- .../entity/AbstractEntityPersister.java | 14 +- .../ExplicitSqlStringGenerationContext.java | 100 +++++++++++ .../query/sql/internal/SQLQueryParser.java | 24 ++- .../orm/test/sql/SQLQueryParserUnitTests.java | 101 +++++++++++ .../SubselectWithPlaceholdersUnitTests.java | 157 ++++++++++++++++++ 8 files changed, 451 insertions(+), 53 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/persister/entity/ExplicitSqlStringGenerationContext.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/subselect/SubselectWithPlaceholdersUnitTests.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc index d0f18fb0d5..3bc1d4f69b 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc @@ -432,6 +432,12 @@ include::{example-dir-mapping}/basic/SubselectTest.java[tag=mapping-Subselect-en ---- ==== +[NOTE] +==== +The underlying `@Subselect` SQL query supports standard Hibernate placeholders ( see <> ). +Global settings can be overridden using the `schema` or `catalog` attributes of the `@Table` annotation. +==== + If we add a new `AccountTransaction` entity and refresh the `AccountSummary` entity, the balance is updated accordingly: [[mapping-Subselect-refresh-find-example]] diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/SqlStringGenerationContext.java b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/SqlStringGenerationContext.java index 50c40acdfa..c0ad0d6c86 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/SqlStringGenerationContext.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/SqlStringGenerationContext.java @@ -40,10 +40,14 @@ public interface SqlStringGenerationContext { Identifier getDefaultCatalog(); /** - * @param explicitCatalogOrNull An explicitly configured catalog, or {@code null}. - * @return The given identifier if non-{@code null}, or the default catalog otherwise. + * Interpret the incoming catalog, returning the incoming value if it is non-null. + * Otherwise, returns the current {@linkplain #getDefaultCatalog() default catalog}. + * + * @apiNote May return {@code null} if {@linkplain #getDefaultCatalog() default catalog} is {@code null}. */ - Identifier catalogWithDefault(Identifier explicitCatalogOrNull); + default Identifier catalogWithDefault(Identifier explicitCatalogOrNull) { + return explicitCatalogOrNull != null ? explicitCatalogOrNull : getDefaultCatalog(); + } /** * @return The default schema, used for table/sequence names that do not explicitly mention a schema. @@ -54,10 +58,14 @@ public interface SqlStringGenerationContext { Identifier getDefaultSchema(); /** - * @param explicitSchemaOrNull An explicitly configured schema, or {@code null}. - * @return The given identifier if non-{@code null}, or the default schema otherwise. + * Interpret the incoming schema, returning the incoming value if it is non-null. + * Otherwise, returns the current {@linkplain #getDefaultSchema() default schema}. + * + * @apiNote May return {@code null} if {@linkplain #getDefaultSchema() default schema} is {@code null}. */ - Identifier schemaWithDefault(Identifier explicitSchemaOrNull); + default Identifier schemaWithDefault(Identifier explicitSchemaOrNull) { + return explicitSchemaOrNull != null ? explicitSchemaOrNull : getDefaultSchema(); + } /** * Render a formatted a table name @@ -101,4 +109,49 @@ public interface SqlStringGenerationContext { * @return {@code true} if and only if this is a migration */ boolean isMigration(); + + /** + * Apply default catalog and schema, if necessary, to the given name. May return a new reference. + */ + default QualifiedTableName withDefaults(QualifiedTableName name) { + if ( name.getCatalogName() == null && getDefaultCatalog() != null + || name.getSchemaName() == null && getDefaultSchema() != null ) { + return new QualifiedTableName( + catalogWithDefault( name.getCatalogName() ), + schemaWithDefault( name.getSchemaName() ), + name.getTableName() + ); + } + return name; + } + + /** + * Apply default catalog and schema, if necessary, to the given name. May return a new reference. + */ + default QualifiedSequenceName withDefaults(QualifiedSequenceName name) { + if ( name.getCatalogName() == null && getDefaultCatalog() != null + || name.getSchemaName() == null && getDefaultSchema() != null ) { + return new QualifiedSequenceName( + catalogWithDefault( name.getCatalogName() ), + schemaWithDefault( name.getSchemaName() ), + name.getSequenceName() + ); + } + return name; + } + + /** + * Apply default catalog and schema, if necessary, to the given name. May return a new reference. + */ + default QualifiedName withDefaults(QualifiedName name) { + if ( name.getCatalogName() == null && getDefaultCatalog() != null + || name.getSchemaName() == null && getDefaultSchema() != null ) { + return new QualifiedNameImpl( + catalogWithDefault( name.getCatalogName() ), + schemaWithDefault( name.getSchemaName() ), + name.getObjectName() + ); + } + return name; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/internal/SqlStringGenerationContextImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/internal/SqlStringGenerationContextImpl.java index 828682d3ce..c4f0332236 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/relational/internal/SqlStringGenerationContextImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/relational/internal/SqlStringGenerationContextImpl.java @@ -146,48 +146,11 @@ public class SqlStringGenerationContextImpl return defaultCatalog; } - @Override - public Identifier catalogWithDefault(Identifier explicitCatalogOrNull) { - return explicitCatalogOrNull != null ? explicitCatalogOrNull : defaultCatalog; - } - @Override public Identifier getDefaultSchema() { return defaultSchema; } - @Override - public Identifier schemaWithDefault(Identifier explicitSchemaOrNull) { - return explicitSchemaOrNull != null ? explicitSchemaOrNull : defaultSchema; - } - - private QualifiedTableName withDefaults(QualifiedTableName name) { - if ( name.getCatalogName() == null && defaultCatalog != null - || name.getSchemaName() == null && defaultSchema != null ) { - return new QualifiedTableName( catalogWithDefault( name.getCatalogName() ), - schemaWithDefault( name.getSchemaName() ), name.getTableName() ); - } - return name; - } - - private QualifiedSequenceName withDefaults(QualifiedSequenceName name) { - if ( name.getCatalogName() == null && defaultCatalog != null - || name.getSchemaName() == null && defaultSchema != null ) { - return new QualifiedSequenceName( catalogWithDefault( name.getCatalogName() ), - schemaWithDefault( name.getSchemaName() ), name.getSequenceName() ); - } - return name; - } - - private QualifiedName withDefaults(QualifiedName name) { - if ( name.getCatalogName() == null && defaultCatalog != null - || name.getSchemaName() == null && defaultSchema != null ) { - return new QualifiedSequenceName( catalogWithDefault( name.getCatalogName() ), - schemaWithDefault( name.getSchemaName() ), name.getObjectName() ); - } - return name; - } - @Override public String format(QualifiedTableName qualifiedName) { return qualifiedObjectNameFormatter.format( withDefaults( qualifiedName ), dialect ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 877ae92f6f..a68b8ad10f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -4620,9 +4620,21 @@ public abstract class AbstractEntityPersister } protected String determineTableName(Table table) { - return MappingModelCreationHelper.getTableIdentifierExpression( table, factory ); + if ( table.getSubselect() != null ) { + final SQLQueryParser sqlQueryParser = new SQLQueryParser( + table.getSubselect(), + null, + // NOTE : this allows finer control over catalog and schema used for placeholder + // handling (`{h-catalog}`, `{h-schema}`, `{h-domain}`) + new ExplicitSqlStringGenerationContext( table.getCatalog(), table.getSchema(), factory ) + ); + return "( " + sqlQueryParser.process() + " )"; + } + + return factory.getSqlStringGenerationContext().format( table.getQualifiedTableName() ); } + @Override public EntityEntryFactory getEntityEntryFactory() { return this.entityEntryFactory; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/ExplicitSqlStringGenerationContext.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/ExplicitSqlStringGenerationContext.java new file mode 100644 index 0000000000..670dd334f0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/ExplicitSqlStringGenerationContext.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.QualifiedName; +import org.hibernate.boot.model.relational.QualifiedSequenceName; +import org.hibernate.boot.model.relational.QualifiedTableName; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.env.spi.QualifiedObjectNameFormatter; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +/** + * SqlStringGenerationContext implementation with support for overriding the + * default catalog and schema + * + * @author Steve Ebersole + */ +public class ExplicitSqlStringGenerationContext implements SqlStringGenerationContext { + private final SessionFactoryImplementor factory; + private final Identifier defaultCatalog; + private final Identifier defaultSchema; + + public ExplicitSqlStringGenerationContext( + String defaultCatalog, + String defaultSchema, + SessionFactoryImplementor factory) { + this.factory = factory; + this.defaultCatalog = defaultCatalog != null + ? toIdentifier( defaultCatalog ) + : toIdentifier( factory.getSessionFactoryOptions().getDefaultCatalog() ); + this.defaultSchema = defaultSchema != null + ? toIdentifier( defaultSchema ) + : toIdentifier( factory.getSessionFactoryOptions().getDefaultSchema() ); + } + + @Override + public Dialect getDialect() { + return factory.getJdbcServices().getDialect(); + } + + @Override + public Identifier toIdentifier(String text) { + return factory.getJdbcServices().getJdbcEnvironment().getIdentifierHelper().toIdentifier( text ); + } + + @Override + public Identifier getDefaultCatalog() { + return defaultCatalog; + } + + @Override + public Identifier getDefaultSchema() { + return defaultSchema; + } + + @Override + public String format(QualifiedTableName qualifiedName) { + return nameFormater().format( withDefaults( qualifiedName ), getDialect() ); + } + + private QualifiedObjectNameFormatter nameFormater() { + final JdbcEnvironment jdbcEnvironment = factory.getJdbcServices().getJdbcEnvironment(); + //noinspection deprecation + return jdbcEnvironment.getQualifiedObjectNameFormatter(); + } + + @Override + public String format(QualifiedSequenceName qualifiedName) { + return nameFormater().format( withDefaults( qualifiedName ), getDialect() ); + } + + @Override + public String format(QualifiedName qualifiedName) { + return nameFormater().format( withDefaults( qualifiedName ), getDialect() ); + } + + @Override + public String formatWithoutCatalog(QualifiedSequenceName qualifiedName) { + QualifiedSequenceName nameToFormat; + if ( qualifiedName.getCatalogName() != null + || qualifiedName.getSchemaName() == null && defaultSchema != null ) { + nameToFormat = new QualifiedSequenceName( null, + schemaWithDefault( qualifiedName.getSchemaName() ), qualifiedName.getSequenceName() ); + } + else { + nameToFormat = qualifiedName; + } + return nameFormater().format( nameToFormat, getDialect() ); + } + + @Override + public boolean isMigration() { + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java index f9f0e825f4..e203128df3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java @@ -4,8 +4,6 @@ */ package org.hibernate.query.sql.internal; -import java.util.Map; - import org.hibernate.QueryException; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.SqlStringGenerationContext; @@ -14,6 +12,8 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; +import java.util.Map; + /** * Substitutes escape sequences of form {@code {alias}}, * {@code {alias.field}}, and {@code {alias.*}} in a @@ -25,11 +25,11 @@ import org.hibernate.persister.entity.EntityPersister; * @author Paul Benedict */ public class SQLQueryParser { - - private final SessionFactoryImplementor factory; private final String originalQueryString; private final ParserContext context; + private final SqlStringGenerationContext sqlStringGenerationContext; + private long aliasesFound; public interface ParserContext { @@ -43,9 +43,16 @@ public class SQLQueryParser { } public SQLQueryParser(String queryString, ParserContext context, SessionFactoryImplementor factory) { + this( queryString, context, factory.getSqlStringGenerationContext() ); + } + + public SQLQueryParser( + String queryString, + ParserContext context, + SqlStringGenerationContext sqlStringGenerationContext) { this.originalQueryString = queryString; this.context = context; - this.factory = factory; + this.sqlStringGenerationContext = sqlStringGenerationContext; } public boolean queryHasAliases() { @@ -169,10 +176,9 @@ public class SQLQueryParser { } private void handlePlaceholder(String token, StringBuilder result) { - final SqlStringGenerationContext context = factory.getSqlStringGenerationContext(); - final Identifier defaultCatalog = context.getDefaultCatalog(); - final Identifier defaultSchema = context.getDefaultSchema(); - final Dialect dialect = context.getDialect(); + final Identifier defaultCatalog = sqlStringGenerationContext.getDefaultCatalog(); + final Identifier defaultSchema = sqlStringGenerationContext.getDefaultSchema(); + final Dialect dialect = sqlStringGenerationContext.getDialect(); switch (token) { case "h-domain": if ( defaultCatalog != null ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java new file mode 100644 index 0000000000..2509d730e8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.sql; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.ExplicitSqlStringGenerationContext; +import org.hibernate.query.sql.internal.SQLQueryParser; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SQLQueryParser} + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class SQLQueryParserUnitTests { + + @Test + @DomainModel + @SessionFactory + @RequiresDialect(H2Dialect.class) + void testDomainParsing(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final String sqlQuery = "select id, name from {h-domain}the_table"; + + final String full = processSqlString( sqlQuery, "my_catalog", "my_schema", sessionFactory ); + assertThat( full ).endsWith( " my_catalog.my_schema.the_table" ); + + final String catalogOnly = processSqlString( sqlQuery, "my_catalog", null, sessionFactory ); + assertThat( catalogOnly ).endsWith( " my_catalog.the_table" ); + + final String schemaOnly = processSqlString( sqlQuery, null, "my_schema", sessionFactory ); + assertThat( schemaOnly ).endsWith( " my_schema.the_table" ); + + final String none = processSqlString( sqlQuery, null, null, sessionFactory ); + assertThat( none ).endsWith( " the_table" ); + } + + @Test + @DomainModel + @SessionFactory + @RequiresDialect(H2Dialect.class) + void testCatalogParsing(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final String sqlQuery = "select id, name from {h-catalog}the_table"; + + final String full = processSqlString( sqlQuery, "my_catalog", "my_schema", sessionFactory ); + assertThat( full ).endsWith( " my_catalog.the_table" ); + + final String catalogOnly = processSqlString( sqlQuery, "my_catalog", null, sessionFactory ); + assertThat( catalogOnly ).endsWith( " my_catalog.the_table" ); + + final String schemaOnly = processSqlString( sqlQuery, null, "my_schema", sessionFactory ); + assertThat( schemaOnly ).endsWith( " the_table" ); + + final String none = processSqlString( sqlQuery, null, null, sessionFactory ); + assertThat( none ).endsWith( " the_table" ); + } + + @Test + @DomainModel + @SessionFactory + @RequiresDialect(H2Dialect.class) + void testSchemaParsing(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final String sqlQuery = "select id, name from {h-schema}the_table"; + + final String full = processSqlString( sqlQuery, "my_catalog", "my_schema", sessionFactory ); + assertThat( full ).endsWith( " my_schema.the_table" ); + + final String catalogOnly = processSqlString( sqlQuery, "my_catalog", null, sessionFactory ); + assertThat( catalogOnly ).endsWith( " the_table" ); + + final String schemaOnly = processSqlString( sqlQuery, null, "my_schema", sessionFactory ); + assertThat( schemaOnly ).endsWith( " my_schema.the_table" ); + + final String none = processSqlString( sqlQuery, null, null, sessionFactory ); + assertThat( none ).endsWith( " the_table" ); + } + + private static String processSqlString( + String sqlQuery, + String catalogName, + String schemaName, + SessionFactoryImplementor sessionFactory) { + // Use a custom SqlStringGenerationContext to integrate the catalog and schema + final ExplicitSqlStringGenerationContext stringGenerationContext + = new ExplicitSqlStringGenerationContext( catalogName, schemaName, sessionFactory ); + return new SQLQueryParser( sqlQuery, null, stringGenerationContext ).process(); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/subselect/SubselectWithPlaceholdersUnitTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/subselect/SubselectWithPlaceholdersUnitTests.java new file mode 100644 index 0000000000..5a266e1b7f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/subselect/SubselectWithPlaceholdersUnitTests.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.subselect; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Subselect; +import org.hibernate.cfg.MappingSettings; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.metamodel.spi.RuntimeMetamodelsImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Simple tests that catalog/schema placeholders in {@link org.hibernate.annotations.Subselect} + * mappings are handled correctly. + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class SubselectWithPlaceholdersUnitTests { + @Test + @DomainModel(annotatedClasses = {EntityInCatalog.class, EntityInCatalogAndSchema.class, EntityInSchema.class, EntityInLocalCatalog.class}) + @SessionFactory(exportSchema = false) + void testPlaceholdersWithNone(SessionFactoryScope scope) { + final RuntimeMetamodelsImplementor runtimeMetamodels = scope.getSessionFactory().getRuntimeMetamodels(); + final MappingMetamodelImplementor mappingMetamodel = runtimeMetamodels.getMappingMetamodel(); + + final EntityPersister entityInCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalog.class ); + assertThat( entityInCatalogDescriptor.getTableName() ).endsWith( " from table_in_catalog )" ); + + final EntityPersister entityInSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInSchema.class ); + assertThat( entityInSchemaDescriptor.getTableName() ).endsWith( " from table_in_schema )" ); + + final EntityPersister entityInCatalogAndSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalogAndSchema.class ); + assertThat( entityInCatalogAndSchemaDescriptor.getTableName() ).endsWith( " from table_in_both )" ); + } + + @Test + @ServiceRegistry(settings = { + @Setting(name= MappingSettings.DEFAULT_CATALOG, value = "the_catalog") + }) + @DomainModel(annotatedClasses = {EntityInCatalog.class, EntityInCatalogAndSchema.class, EntityInSchema.class, EntityInLocalCatalog.class}) + @SessionFactory(exportSchema = false) + void testPlaceholdersWithCatalog(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final RuntimeMetamodelsImplementor runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); + final MappingMetamodelImplementor mappingMetamodel = runtimeMetamodels.getMappingMetamodel(); + + final EntityPersister entityInCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalog.class ); + assertThat( entityInCatalogDescriptor.getTableName() ).endsWith( " from the_catalog.table_in_catalog )" ); + + final EntityPersister entityInSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInSchema.class ); + assertThat( entityInSchemaDescriptor.getTableName() ).endsWith( " from table_in_schema )" ); + + final EntityPersister entityInCatalogAndSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalogAndSchema.class ); + assertThat( entityInCatalogAndSchemaDescriptor.getTableName() ).endsWith( " from the_catalog.table_in_both )" ); + + final EntityPersister entityInLocalCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInLocalCatalog.class ); + assertThat( entityInLocalCatalogDescriptor.getTableName() ).endsWith( " from local_catalog.table_in_local_catalog )" ); + } + + @Test + @ServiceRegistry(settings = { + @Setting(name= MappingSettings.DEFAULT_SCHEMA, value = "the_schema") + }) + @DomainModel(annotatedClasses = {EntityInCatalog.class, EntityInCatalogAndSchema.class, EntityInSchema.class, EntityInLocalCatalog.class}) + @SessionFactory(exportSchema = false) + void testPlaceholdersWithSchema(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final RuntimeMetamodelsImplementor runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); + final MappingMetamodelImplementor mappingMetamodel = runtimeMetamodels.getMappingMetamodel(); + + + final EntityPersister entityInCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalog.class ); + assertThat( entityInCatalogDescriptor.getTableName() ).endsWith( " from table_in_catalog )" ); + + final EntityPersister entityInSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInSchema.class ); + assertThat( entityInSchemaDescriptor.getTableName() ).endsWith( " from the_schema.table_in_schema )" ); + + final EntityPersister entityInCatalogAndSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalogAndSchema.class ); + assertThat( entityInCatalogAndSchemaDescriptor.getTableName() ).endsWith( " from the_schema.table_in_both )" ); + + final EntityPersister entityInLocalCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInLocalCatalog.class ); + assertThat( entityInLocalCatalogDescriptor.getTableName() ).endsWith( " from local_catalog.table_in_local_catalog )" ); + } + + @Test + @ServiceRegistry(settings = { + @Setting(name= MappingSettings.DEFAULT_CATALOG, value = "the_catalog"), + @Setting(name= MappingSettings.DEFAULT_SCHEMA, value = "the_schema") + }) + @DomainModel(annotatedClasses = {EntityInCatalog.class, EntityInCatalogAndSchema.class, EntityInSchema.class, EntityInLocalCatalog.class}) + @SessionFactory(exportSchema = false) + void testPlaceholdersWithCatalogAndSchema(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final RuntimeMetamodelsImplementor runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); + final MappingMetamodelImplementor mappingMetamodel = runtimeMetamodels.getMappingMetamodel(); + + final EntityPersister entityInCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalog.class ); + assertThat( entityInCatalogDescriptor.getTableName() ).endsWith( " from the_catalog.table_in_catalog )" ); + + final EntityPersister entityInSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInSchema.class ); + assertThat( entityInSchemaDescriptor.getTableName() ).endsWith( " from the_schema.table_in_schema )" ); + + final EntityPersister entityInCatalogAndSchemaDescriptor = mappingMetamodel.getEntityDescriptor( EntityInCatalogAndSchema.class ); + assertThat( entityInCatalogAndSchemaDescriptor.getTableName() ).endsWith( " from the_catalog.the_schema.table_in_both )" ); + + final EntityPersister entityInLocalCatalogDescriptor = mappingMetamodel.getEntityDescriptor( EntityInLocalCatalog.class ); + assertThat( entityInLocalCatalogDescriptor.getTableName() ).endsWith( " from local_catalog.table_in_local_catalog )" ); + } + + @Entity + @Subselect("select id, name from {h-catalog}table_in_catalog") + public static class EntityInCatalog { + @Id + private Integer id; + private String name; + } + + @Entity + @Subselect("select id, name from {h-catalog}table_in_local_catalog") + @Table(catalog = "local_catalog") + public static class EntityInLocalCatalog { + @Id + private Integer id; + private String name; + } + + @Entity(name = "EntityInCatalogAndSchema") + @Subselect("select id, name from {h-domain}table_in_both") + public static class EntityInCatalogAndSchema { + @Id + private Integer id; + private String name; + } + + @Entity(name = "EntityInSchema") + @Subselect("select id, name from {h-schema}table_in_schema") + public static class EntityInSchema { + @Id + private Integer id; + private String name; + } +}