Skip to content

Commit 03b92d1

Browse files
committed
1 parent e72228b commit 03b92d1

File tree

11 files changed

+770
-18
lines changed

11 files changed

+770
-18
lines changed

src/integrationTest/java/com/mongodb/hibernate/CollectionIntegrationTests.java

Lines changed: 387 additions & 0 deletions
Large diffs are not rendered by default.

src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import jakarta.persistence.Entity;
3232
import jakarta.persistence.Id;
3333
import jakarta.persistence.Table;
34+
import java.util.Collection;
35+
import java.util.List;
3436
import org.bson.BsonDocument;
3537
import org.hibernate.annotations.Parent;
3638
import org.hibernate.boot.MetadataSources;
@@ -46,10 +48,11 @@
4648
@DomainModel(
4749
annotatedClasses = {
4850
EmbeddableIntegrationTests.ItemWithFlattenedValues.class,
49-
EmbeddableIntegrationTests.ItemWithOmittedEmptyValue.class
51+
EmbeddableIntegrationTests.ItemWithOmittedEmptyValue.class,
52+
EmbeddableIntegrationTests.ItemWithFlattenedValueHavingArraysAndCollections.class
5053
})
5154
@ExtendWith(MongoExtension.class)
52-
class EmbeddableIntegrationTests implements SessionFactoryScopeAware {
55+
public class EmbeddableIntegrationTests implements SessionFactoryScopeAware {
5356
@InjectMongoCollection("items")
5457
private static MongoCollection<BsonDocument> mongoCollection;
5558

@@ -139,6 +142,47 @@ void testFlattenedEmptyValue() {
139142
assertEquals(updatedItem, loadedItem);
140143
}
141144

145+
@Test
146+
void testFlattenedValueHavingArraysAndCollections() {
147+
var item = new ItemWithFlattenedValueHavingArraysAndCollections();
148+
{
149+
item.id = 1;
150+
item.flattened = new MultiValueWithArraysAndCollections();
151+
item.flattened.ints = new int[] {2, 3};
152+
item.flattened.intsCollection = List.of(4, 5);
153+
}
154+
sessionFactoryScope.inTransaction(session -> session.persist(item));
155+
assertCollectionContainsExactly(
156+
"""
157+
{
158+
_id: 1,
159+
flattened_ints: [2, 3]
160+
flattened_intsCollection: [4, 5]
161+
}
162+
""");
163+
var loadedItem = sessionFactoryScope.fromTransaction(
164+
session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id));
165+
assertEquals(item, loadedItem);
166+
var updatedItem = sessionFactoryScope.fromTransaction(session -> {
167+
var result = session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id);
168+
result.flattened.ints[0] = -result.flattened.ints[0];
169+
result.flattened.intsCollection.remove(5);
170+
result.flattened.intsCollection.add(-5);
171+
return result;
172+
});
173+
assertCollectionContainsExactly(
174+
"""
175+
{
176+
_id: 1,
177+
flattened_ints: [-2, 3]
178+
flattened_intsCollection: [4, -5]
179+
}
180+
""");
181+
loadedItem = sessionFactoryScope.fromTransaction(
182+
session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, updatedItem.id));
183+
assertEquals(updatedItem, loadedItem);
184+
}
185+
142186
@Override
143187
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
144188
this.sessionFactoryScope = sessionFactoryScope;
@@ -164,7 +208,7 @@ static class ItemWithFlattenedValues {
164208
}
165209

166210
@Embeddable
167-
static class SingleValue {
211+
public static class SingleValue {
168212
int a;
169213
}
170214

@@ -207,6 +251,23 @@ static class ItemWithOmittedEmptyValue {
207251
@Embeddable
208252
static class EmptyValue {}
209253

254+
@Entity
255+
@Table(name = "items")
256+
static class ItemWithFlattenedValueHavingArraysAndCollections {
257+
@Id
258+
int id;
259+
260+
@AttributeOverride(name = "ints", column = @Column(name = "flattened_ints"))
261+
@AttributeOverride(name = "intsCollection", column = @Column(name = "flattened_intsCollection"))
262+
MultiValueWithArraysAndCollections flattened;
263+
}
264+
265+
@Embeddable
266+
static class MultiValueWithArraysAndCollections {
267+
int[] ints;
268+
Collection<Integer> intsCollection;
269+
}
270+
210271
@Nested
211272
class Unsupported {
212273
@Test

src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import jakarta.persistence.Entity;
3131
import jakarta.persistence.Id;
3232
import jakarta.persistence.Table;
33+
import java.util.Collection;
34+
import java.util.List;
3335
import org.bson.BsonDocument;
3436
import org.hibernate.annotations.Parent;
3537
import org.hibernate.annotations.Struct;
@@ -47,12 +49,13 @@
4749
annotatedClasses = {
4850
StructAggregateEmbeddableIntegrationTests.ItemWithNestedValues.class,
4951
StructAggregateEmbeddableIntegrationTests.ItemWithOmittedEmptyValue.class,
52+
StructAggregateEmbeddableIntegrationTests.ItemWithNestedValueHavingCollections.class,
5053
StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingNonInsertable.class,
5154
StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingAllNonInsertable.class,
5255
StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingNonUpdatable.class
5356
})
5457
@ExtendWith(MongoExtension.class)
55-
class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware {
58+
public class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware {
5659
@InjectMongoCollection("items")
5760
private static MongoCollection<BsonDocument> mongoCollection;
5861

@@ -155,6 +158,47 @@ void testNestedEmptyValue() {
155158
assertEquals(updatedItem, loadedItem);
156159
}
157160

161+
@Test
162+
void testNestedValueHavingCollections() {
163+
var item = new ItemWithNestedValueHavingCollections();
164+
{
165+
item.id = 1;
166+
item.nested = new MultiValueWithCollections();
167+
item.nested.intsCollection = List.of(2, 3);
168+
}
169+
sessionFactoryScope.inTransaction(session -> session.persist(item));
170+
assertCollectionContainsExactly(
171+
"""
172+
{
173+
_id: 1,
174+
nested: {
175+
intsCollection: [2, 3]
176+
}
177+
}
178+
""");
179+
var loadedItem = sessionFactoryScope.fromTransaction(
180+
session -> session.find(ItemWithNestedValueHavingCollections.class, item.id));
181+
assertEquals(item, loadedItem);
182+
var updatedItem = sessionFactoryScope.fromTransaction(session -> {
183+
var result = session.find(ItemWithNestedValueHavingCollections.class, item.id);
184+
result.nested.intsCollection.remove(3);
185+
result.nested.intsCollection.add(-3);
186+
return result;
187+
});
188+
assertCollectionContainsExactly(
189+
"""
190+
{
191+
_id: 1,
192+
nested: {
193+
intsCollection: [2, -3]
194+
}
195+
}
196+
""");
197+
loadedItem = sessionFactoryScope.fromTransaction(
198+
session -> session.find(ItemWithNestedValueHavingCollections.class, updatedItem.id));
199+
assertEquals(updatedItem, loadedItem);
200+
}
201+
158202
@Override
159203
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
160204
this.sessionFactoryScope = sessionFactoryScope;
@@ -177,7 +221,7 @@ static class ItemWithNestedValues {
177221

178222
@Embeddable
179223
@Struct(name = "SingleValue")
180-
static class SingleValue {
224+
public static class SingleValue {
181225
int a;
182226
}
183227

@@ -223,6 +267,21 @@ static class ItemWithOmittedEmptyValue {
223267
@Struct(name = "EmptyValue")
224268
static class EmptyValue {}
225269

270+
@Entity
271+
@Table(name = "items")
272+
static class ItemWithNestedValueHavingCollections {
273+
@Id
274+
int id;
275+
276+
MultiValueWithCollections nested;
277+
}
278+
279+
@Embeddable
280+
@Struct(name = "MultiValueWithArraysAndCollections")
281+
static class MultiValueWithCollections {
282+
Collection<Integer> intsCollection;
283+
}
284+
226285
@Nested
227286
class Unsupported {
228287
@Test

src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.hibernate.dialect.aggregate.AggregateSupportImpl;
2424
import org.hibernate.mapping.AggregateColumn;
2525
import org.hibernate.mapping.Column;
26+
import org.hibernate.type.SqlTypes;
2627

2728
final class MongoAggregateSupport extends AggregateSupportImpl {
2829
static final MongoAggregateSupport INSTANCE = new MongoAggregateSupport();
@@ -38,10 +39,11 @@ public String aggregateComponentCustomReadExpression(
3839
AggregateColumn aggregateColumn,
3940
Column column) {
4041
var aggregateColumnType = aggregateColumn.getTypeCode();
41-
if (aggregateColumnType == JDBCType.STRUCT.getVendorTypeNumber()) {
42+
if (aggregateColumnType == JDBCType.STRUCT.getVendorTypeNumber()
43+
|| aggregateColumnType == SqlTypes.STRUCT_ARRAY) {
4244
return format(
43-
"unused from %s.aggregateComponentCustomReadExpression",
44-
MongoAggregateSupport.class.getSimpleName());
45+
"unused from %s.aggregateComponentCustomReadExpression for SQL type code [%d]",
46+
MongoAggregateSupport.class.getSimpleName(), aggregateColumnType);
4547
}
4648
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType));
4749
}
@@ -53,17 +55,19 @@ public String aggregateComponentAssignmentExpression(
5355
AggregateColumn aggregateColumn,
5456
Column column) {
5557
var aggregateColumnType = aggregateColumn.getTypeCode();
56-
if (aggregateColumnType == JDBCType.STRUCT.getVendorTypeNumber()) {
58+
if (aggregateColumnType == JDBCType.STRUCT.getVendorTypeNumber()
59+
|| aggregateColumnType == SqlTypes.STRUCT_ARRAY) {
5760
return format(
58-
"unused from %s.aggregateComponentAssignmentExpression",
59-
MongoAggregateSupport.class.getSimpleName());
61+
"unused from %s.aggregateComponentAssignmentExpression for SQL type code [%d]",
62+
MongoAggregateSupport.class.getSimpleName(), aggregateColumnType);
6063
}
6164
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType));
6265
}
6366

6467
@Override
6568
public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) {
66-
if (aggregateSqlTypeCode == JDBCType.STRUCT.getVendorTypeNumber()) {
69+
if (aggregateSqlTypeCode == JDBCType.STRUCT.getVendorTypeNumber()
70+
|| aggregateSqlTypeCode == SqlTypes.STRUCT_ARRAY) {
6771
return false;
6872
}
6973
throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateSqlTypeCode));

src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,9 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv
105105
public AggregateSupport getAggregateSupport() {
106106
return MongoAggregateSupport.INSTANCE;
107107
}
108+
109+
@Override
110+
public boolean supportsStandardArrays() {
111+
return true;
112+
}
108113
}

src/main/java/com/mongodb/hibernate/internal/type/ValueConverter.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import java.sql.PreparedStatement;
2424
import java.sql.ResultSet;
2525
import java.sql.SQLFeatureNotSupportedException;
26+
import java.util.ArrayList;
27+
import java.util.Collection;
28+
import java.util.List;
29+
import org.bson.BsonArray;
2630
import org.bson.BsonBinary;
2731
import org.bson.BsonBoolean;
2832
import org.bson.BsonDecimal128;
@@ -34,6 +38,7 @@
3438
import org.bson.BsonValue;
3539
import org.bson.types.Decimal128;
3640
import org.bson.types.ObjectId;
41+
import org.jspecify.annotations.Nullable;
3742

3843
/**
3944
* Provides conversion methods between {@link BsonValue}s, which our {@link PreparedStatement}/{@link ResultSet}
@@ -61,6 +66,10 @@ public static BsonValue toBsonValue(Object value) throws SQLFeatureNotSupportedE
6166
return toBsonValue(v);
6267
} else if (value instanceof ObjectId v) {
6368
return toBsonValue(v);
69+
} else if (value instanceof Object[] v) {
70+
return toBsonValue(v);
71+
} else if (value instanceof Collection<?> v) {
72+
return toBsonValue(v);
6473
} else {
6574
throw new SQLFeatureNotSupportedException(format(
6675
"Value [%s] of type [%s] is not supported",
@@ -100,6 +109,22 @@ public static BsonObjectId toBsonValue(ObjectId value) {
100109
return new BsonObjectId(value);
101110
}
102111

112+
public static BsonArray toBsonValue(Object[] value) throws SQLFeatureNotSupportedException {
113+
var elements = new ArrayList<BsonValue>(value.length);
114+
for (var e : value) {
115+
elements.add(toBsonValue(e));
116+
}
117+
return new BsonArray(elements);
118+
}
119+
120+
public static BsonArray toBsonValue(Collection<?> value) throws SQLFeatureNotSupportedException {
121+
var elements = new ArrayList<BsonValue>(value.size());
122+
for (var e : value) {
123+
elements.add(toBsonValue(e));
124+
}
125+
return new BsonArray(elements);
126+
}
127+
103128
static Object toDomainValue(BsonValue value) throws SQLFeatureNotSupportedException {
104129
assertNotNull(value);
105130
if (value instanceof BsonBoolean v) {
@@ -118,6 +143,8 @@ static Object toDomainValue(BsonValue value) throws SQLFeatureNotSupportedExcept
118143
return toDomainValue(v);
119144
} else if (value instanceof BsonObjectId v) {
120145
return toDomainValue(v);
146+
} else if (value instanceof BsonArray v) {
147+
return toDomainValue(v).domainValue();
121148
} else {
122149
throw new SQLFeatureNotSupportedException(format(
123150
"Value [%s] of type [%s] is not supported",
@@ -188,4 +215,36 @@ public static ObjectId toObjectIdDomainValue(BsonValue value) {
188215
private static ObjectId toDomainValue(BsonObjectId value) {
189216
return value.getValue();
190217
}
218+
219+
public static ArrayWithBaseType toObjectArrayDomainValue(BsonValue value) throws SQLFeatureNotSupportedException {
220+
return toDomainValue(value.asArray());
221+
}
222+
223+
private static ArrayWithBaseType toDomainValue(BsonArray value) throws SQLFeatureNotSupportedException {
224+
var domainValue = new ArrayList<>(value.size());
225+
Class<?> baseDomainType = null;
226+
for (int i = 0; i < value.size(); i++) {
227+
var element = toDomainValue(value.get(i));
228+
domainValue.add(i, element);
229+
// VAKOTODO should we simply decide the base type by looking at the first element?
230+
var elementType = element.getClass();
231+
if (baseDomainType == null) {
232+
baseDomainType = elementType;
233+
} else {
234+
if (baseDomainType.isAssignableFrom(elementType)) {
235+
// do nothing
236+
} else if (elementType.isAssignableFrom(baseDomainType)) {
237+
baseDomainType = elementType;
238+
} else {
239+
throw new SQLFeatureNotSupportedException(format(
240+
"[%s] contains elements of incompatible types: baseDomainType [%s], elementType [%s], element index [%d]",
241+
value, baseDomainType, elementType, i));
242+
}
243+
}
244+
}
245+
return new ArrayWithBaseType(domainValue, baseDomainType); // VAKOTODO do we actually need `ArrayWithBaseType`?
246+
}
247+
248+
@SuppressWarnings("ArrayRecordComponent")
249+
public record ArrayWithBaseType(List<?> domainValue, @Nullable Class<?> baseDomainType) {}
191250
}

0 commit comments

Comments
 (0)