Skip to content

Support arrays/collections mapping #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
221ec3d
HIBERNATE-58
stIncMale May 7, 2025
4ed1ce7
Remove custom array-handling code
stIncMale Jun 17, 2025
c4986fa
Hack-in `null` support to how the tests behave
stIncMale Jun 18, 2025
01a7365
Undo "Hack-in `null` support to how the tests behave"
stIncMale Jun 18, 2025
78d8892
Do changes identified during the code walkthrough
stIncMale Jun 19, 2025
2e9088a
Merge branch 'main' into HIBERNATE-58
stIncMale Jun 19, 2025
c13fda9
Get rid of the custom primitive types and arrays of them
stIncMale Jun 26, 2025
179f451
Simplify the invocation of `addDescriptorIfAbsent`
stIncMale Jun 26, 2025
68bea86
Remove the unused `ValueConversions.toCharDomainValue` method
stIncMale Jun 26, 2025
0b76dd3
Simplify `ValueConversions.toDomainValue(BsonString value)`
stIncMale Jun 26, 2025
f0899ef
Fix copyright comments
stIncMale Jun 26, 2025
1573746
Require presence of insertable columns in structs, which is stricter …
stIncMale Jun 26, 2025
8499612
Use `var` where possible
stIncMale Jun 26, 2025
1ca8201
Use `var` where possible
stIncMale Jun 26, 2025
865cf7e
Rename the parameter of the `assertCollectionContainsExactly` method
stIncMale Jun 26, 2025
dbb00b9
Update the code comment about Hibernate ORM reading `null` instead of…
stIncMale Jun 26, 2025
ed6fb2c
Replace bulky `instanceof` casts&assertions with a new more compact `…
stIncMale Jun 27, 2025
6de1bfe
`ValueConversions.toDomainValue` can never get `null` `value`
stIncMale Jun 27, 2025
20f8e83
Move setup methods to the top of test classes
stIncMale Jun 27, 2025
ec24448
Support missing fields in structs
stIncMale Jun 28, 2025
2998250
Use `var`
stIncMale Jul 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ class IdentifierIntegrationTests implements SessionFactoryScopeAware {

private SessionFactoryScope sessionFactoryScope;

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Test
void withSpaceAndDotAndMixedCase() {
var item = new WithSpaceAndDotAndMixedCase();
Expand Down Expand Up @@ -197,11 +202,6 @@ void endingWithRightSquareBracket() {
sessionFactoryScope.inTransaction(session -> session.find(EndingWithRightSquareBracket.class, item.id));
}

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Entity
@Table(name = WithSpaceAndDotAndMixedCase.COLLECTION_NAME)
static class WithSpaceAndDotAndMixedCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ public static void assertUsingRecursiveComparison(
@Nullable Object actual,
BiConsumer<RecursiveComparisonAssert<?>, Object> assertion) {
assertion.accept(
assertThat(expected)
assertThat(actual)
.usingRecursiveComparison()
.usingOverriddenEquals()
.withStrictTypeChecking(),
actual);
expected);
}

/**
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class ObjectIdAsIdIntegrationTests implements SessionFactoryScopeAware {

private SessionFactoryScope sessionFactoryScope;

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Test
void insert() {
var item = new Item();
Expand All @@ -70,11 +75,6 @@ void findById() {
assertEquals(item.id, loadedItem.id);
}

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Nested
class Generated {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,6 @@ abstract class AbstractSelectionQueryIntegrationTests implements SessionFactoryS

private TestCommandListener testCommandListener;

SessionFactoryScope getSessionFactoryScope() {
return sessionFactoryScope;
}

TestCommandListener getTestCommandListener() {
return testCommandListener;
}

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
Expand All @@ -60,6 +52,14 @@ public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope
this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class);
}

SessionFactoryScope getSessionFactoryScope() {
return sessionFactoryScope;
}

TestCommandListener getTestCommandListener() {
return testCommandListener;
}

<T> void assertSelectionQuery(
String hql,
Class<T> resultType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class SimpleSelectQueryIntegrationTests extends AbstractSelectionQueryIntegratio

@Nested
class QueryTests {
@BeforeEach
void beforeEach() {
getSessionFactoryScope().inTransaction(session -> testingContacts.forEach(session::persist));
getTestCommandListener().clear();
}

private static final List<Contact> testingContacts = List.of(
new Contact(1, "Bob", 18, Country.USA),
Expand All @@ -56,12 +61,6 @@ private static List<Contact> getTestingContacts(int... ids) {
.toList();
}

@BeforeEach
void beforeEach() {
getSessionFactoryScope().inTransaction(session -> testingContacts.forEach(session::persist));
getTestCommandListener().clear();
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void testComparisonByEq(boolean fieldAsLhs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@

@DomainModel(annotatedClasses = Book.class)
class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests {
@BeforeEach
void beforeEach() {
getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist));
getTestCommandListener().clear();
}

private static final List<Book> testingBooks = List.of(
new Book(1, "War and Peace", 1869, true),
Expand All @@ -55,12 +60,6 @@ private static List<Book> getBooksByIds(int... ids) {
.toList();
}

@BeforeEach
void beforeEach() {
getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist));
getTestCommandListener().clear();
}

@ParameterizedTest
@ValueSource(strings = {"ASC", "DESC"})
void testOrderBySingleFieldWithoutTies(String sortDirection) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class ObjectIdIntegrationTests implements SessionFactoryScopeAware {

private SessionFactoryScope sessionFactoryScope;

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Test
void insert() {
var item = new Item();
Expand Down Expand Up @@ -94,11 +99,6 @@ void findById() {
assertEq(item, loadedItem);
}

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Nested
class Generated {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static java.lang.String.format;

import com.mongodb.hibernate.internal.FeatureNotSupportedException;
import com.mongodb.hibernate.internal.type.MongoArrayJdbcType;
import com.mongodb.hibernate.internal.type.MongoStructJdbcType;
import org.hibernate.dialect.aggregate.AggregateSupportImpl;
import org.hibernate.mapping.AggregateColumn;
Expand All @@ -38,10 +39,11 @@ public String aggregateComponentCustomReadExpression(
AggregateColumn aggregateColumn,
Column column) {
var aggregateColumnType = aggregateColumn.getTypeCode();
if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) {
if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()
|| aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) {
return format(
"unused from %s.aggregateComponentCustomReadExpression",
MongoAggregateSupport.class.getSimpleName());
"unused from %s.aggregateComponentCustomReadExpression for SQL type code [%d]",
MongoAggregateSupport.class.getSimpleName(), aggregateColumnType);
}
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType));
}
Expand All @@ -53,17 +55,19 @@ public String aggregateComponentAssignmentExpression(
AggregateColumn aggregateColumn,
Column column) {
var aggregateColumnType = aggregateColumn.getTypeCode();
if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) {
if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()
|| aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) {
return format(
"unused from %s.aggregateComponentAssignmentExpression",
MongoAggregateSupport.class.getSimpleName());
"unused from %s.aggregateComponentAssignmentExpression for SQL type code [%d]",
MongoAggregateSupport.class.getSimpleName(), aggregateColumnType);
}
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType));
}

@Override
public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) {
if (aggregateSqlTypeCode == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) {
if (aggregateSqlTypeCode == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()
|| aggregateSqlTypeCode == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) {
return false;
}
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateSqlTypeCode));
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import static java.lang.String.format;

import com.mongodb.hibernate.internal.translate.MongoTranslatorFactory;
import com.mongodb.hibernate.internal.type.MongoArrayJdbcType;
import com.mongodb.hibernate.internal.type.MongoStructJdbcType;
import com.mongodb.hibernate.internal.type.MqlType;
import com.mongodb.hibernate.internal.type.ObjectIdJavaType;
import com.mongodb.hibernate.internal.type.ObjectIdJdbcType;
import com.mongodb.hibernate.jdbc.MongoConnectionProvider;
Expand All @@ -31,6 +33,7 @@
import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -91,9 +94,24 @@ public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
@Override
public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
super.contribute(typeContributions, serviceRegistry);
contributeObjectIdType(typeContributions);
typeContributions.contributeJdbcTypeConstructor(MongoArrayJdbcType.Constructor.INSTANCE);
typeContributions.contributeJdbcType(MongoStructJdbcType.INSTANCE);
}

private void contributeObjectIdType(TypeContributions typeContributions) {
typeContributions.contributeJavaType(ObjectIdJavaType.INSTANCE);
typeContributions.contributeJdbcType(ObjectIdJdbcType.INSTANCE);
typeContributions.contributeJdbcType(MongoStructJdbcType.INSTANCE);
var objectIdTypeCode = MqlType.OBJECT_ID.getVendorTypeNumber();
typeContributions
.getTypeConfiguration()
.getDdlTypeRegistry()
.addDescriptorIfAbsent(new DdlTypeImpl(
objectIdTypeCode,
format(
"unused from %s.contributeObjectIdType for SQL type code [%d]",
MongoDialect.class.getSimpleName(), objectIdTypeCode),
this));
}

@Override
Expand All @@ -105,4 +123,9 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv
public AggregateSupport getAggregateSupport() {
return MongoAggregateSupport.INSTANCE;
}

@Override
public boolean supportsStandardArrays() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private MongoAssertions() {}
*/
public static <T> T assertNotNull(@Nullable T value) throws AssertionError {
if (value == null) {
throw new AssertionError();
throw fail();
}
return value;
}
Expand Down Expand Up @@ -69,7 +69,7 @@ public static AssertionError fail() throws AssertionError {
*/
public static void assertNull(@Nullable Object value) throws AssertionError {
if (value != null) {
throw new AssertionError();
throw fail();
}
}

Expand All @@ -82,7 +82,7 @@ public static void assertNull(@Nullable Object value) throws AssertionError {
*/
public static boolean assertTrue(boolean value) throws AssertionError {
if (!value) {
throw new AssertionError();
throw fail();
}
return true;
}
Expand All @@ -96,8 +96,15 @@ public static boolean assertTrue(boolean value) throws AssertionError {
*/
public static boolean assertFalse(boolean value) throws AssertionError {
if (value) {
throw new AssertionError();
throw fail();
}
return false;
}

public static <T> T assertInstanceOf(@Nullable Object value, Class<? extends T> type) {
if (!type.isInstance(value)) {
throw fail();
}
return type.cast(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,13 @@ private static final class ConfigPropertiesParser {
var jdbcUrl = configurationValues.get(JAKARTA_JDBC_URL);
if (jdbcUrl == null) {
return null;
}
if (jdbcUrl instanceof String jdbcUrlText) {
} else if (jdbcUrl instanceof String jdbcUrlText) {
return parseConnectionString(JAKARTA_JDBC_URL, jdbcUrlText);
} else if (jdbcUrl instanceof ConnectionString jdbcUrlConnectionString) {
return jdbcUrlConnectionString;
} else {
throw MongoConfigurationBuilder.ConfigPropertiesParser.Exceptions.unsupportedType(
JAKARTA_JDBC_URL, jdbcUrl, String.class, ConnectionString.class);
}
throw MongoConfigurationBuilder.ConfigPropertiesParser.Exceptions.unsupportedType(
JAKARTA_JDBC_URL, jdbcUrl, String.class, ConnectionString.class);
}

private static ConnectionString parseConnectionString(String propertyName, String propertyValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public void contribute(
InFlightMetadataCollector metadata,
ResourceStreamLocator resourceStreamLocator,
MetadataBuildingContext buildingContext) {
forbidEmbeddablesWithoutPersistentAttributes(metadata);
metadata.getEntityBindings().forEach(persistentClass -> {
forbidDynamicInsert(persistentClass);
checkColumnNames(persistentClass);
Expand All @@ -67,7 +68,8 @@ public void contribute(

private static void forbidDynamicInsert(PersistentClass persistentClass) {
if (persistentClass.useDynamicInsert()) {
throw new FeatureNotSupportedException(format("%s is not supported", DynamicInsert.class.getSimpleName()));
throw new FeatureNotSupportedException(
format("%s: %s is not supported", persistentClass, DynamicInsert.class.getSimpleName()));
}
}

Expand Down Expand Up @@ -100,6 +102,15 @@ private static void forbidStructIdentifier(PersistentClass persistentClass) {
}
}

private static void forbidEmbeddablesWithoutPersistentAttributes(InFlightMetadataCollector metadata) {
metadata.visitRegisteredComponents(component -> {
if (!component.hasAnyInsertableColumns()) {
throw new FeatureNotSupportedException(
format("%s: must have at least one persistent attribute", component));
}
});
}

private static void setIdentifierColumnName(PersistentClass persistentClass) {
var identifier = persistentClass.getIdentifier();
assertFalse(identifier.hasFormula());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,7 @@ private static boolean isComparingFieldWithValue(ComparisonPredicate comparisonP
}

private static BsonValue toBsonValue(Object value) {
// TODO-HIBERNATE-74 decide if `value` is nullable
try {
return ValueConversions.toBsonValue(value);
} catch (SQLFeatureNotSupportedException e) {
Expand Down
Loading