4
4
*/
5
5
package org .hibernate .search .integrationtest .backend .tck .search .projection ;
6
6
7
+ import static org .assertj .core .api .Assertions .assertThat ;
8
+ import static org .assertj .core .api .Assertions .assertThatThrownBy ;
9
+
7
10
import java .math .BigDecimal ;
8
11
import java .util .ArrayList ;
9
12
import java .util .Comparator ;
10
13
import java .util .List ;
11
14
15
+ import org .hibernate .search .engine .backend .document .DocumentElement ;
16
+ import org .hibernate .search .engine .backend .document .model .dsl .IndexSchemaElement ;
17
+ import org .hibernate .search .engine .backend .document .model .dsl .IndexSchemaObjectField ;
12
18
import org .hibernate .search .engine .backend .types .ObjectStructure ;
19
+ import org .hibernate .search .engine .backend .types .Projectable ;
13
20
import org .hibernate .search .engine .backend .types .dsl .SearchableProjectableIndexFieldTypeOptionsStep ;
14
21
import org .hibernate .search .engine .search .projection .ProjectionCollector ;
15
22
import org .hibernate .search .engine .search .projection .dsl .ProjectionFinalStep ;
18
25
import org .hibernate .search .integrationtest .backend .tck .testsupport .types .StandardFieldTypeDescriptor ;
19
26
import org .hibernate .search .integrationtest .backend .tck .testsupport .util .TckConfiguration ;
20
27
import org .hibernate .search .integrationtest .backend .tck .testsupport .util .extension .SearchSetupHelper ;
28
+ import org .hibernate .search .util .common .SearchException ;
21
29
import org .hibernate .search .util .impl .integrationtest .mapper .stub .BulkIndexer ;
22
30
import org .hibernate .search .util .impl .integrationtest .mapper .stub .SimpleMappedIndex ;
23
31
24
32
import org .junit .jupiter .api .BeforeAll ;
25
33
import org .junit .jupiter .api .Nested ;
34
+ import org .junit .jupiter .api .Test ;
26
35
import org .junit .jupiter .api .extension .RegisterExtension ;
27
36
import org .junit .jupiter .params .provider .Arguments ;
28
37
@@ -53,7 +62,9 @@ static void setup() {
53
62
InObjectProjectionConfigured .missingLevel2SingleValuedFieldIndex ,
54
63
InvalidFieldConfigured .index ,
55
64
ProjectableConfigured .projectableDefaultIndex , ProjectableConfigured .projectableYesIndex ,
56
- ProjectableConfigured .projectableNoIndex )
65
+ ProjectableConfigured .projectableNoIndex ,
66
+ CardinalityCheckConfigured .index ,
67
+ CardinalityCheckConfigured .containing )
57
68
.setup ();
58
69
59
70
BulkIndexer compositeForEachMainIndexer = InObjectProjectionConfigured .mainIndex .bulkIndexer ();
@@ -74,7 +85,12 @@ static void setup() {
74
85
75
86
compositeForEachMainIndexer .join ( compositeForEachMissingLevel1Indexer ,
76
87
compositeForEachMissingLevel1SingleValuedFieldIndexer , compositeForEachMissingLevel2Indexer ,
77
- compositeForEachMissingLevel2SingleValuedFieldIndexer );
88
+ compositeForEachMissingLevel2SingleValuedFieldIndexer ,
89
+ CardinalityCheckConfigured .containing .binding ()
90
+ .contribute ( CardinalityCheckConfigured .containing .bulkIndexer () ),
91
+ CardinalityCheckConfigured .index .binding ()
92
+ .contribute ( CardinalityCheckConfigured .index .bulkIndexer () )
93
+ );
78
94
}
79
95
80
96
@ Nested
@@ -211,4 +227,208 @@ protected String projectionTrait() {
211
227
return "projection:field" ;
212
228
}
213
229
}
230
+
231
+ @ Nested
232
+ class CardinalityCheckIT extends CardinalityCheckConfigured {
233
+ // JDK 11 does not allow static fields in non-static inner class and JUnit does not allow running @Nested tests in static inner classes...
234
+ }
235
+
236
+ abstract static class CardinalityCheckConfigured {
237
+ private static final SimpleMappedIndex <IndexContaining > containing =
238
+ SimpleMappedIndex .of ( IndexContaining ::new ).name ( "cardinality-containing" );
239
+ private static final SimpleMappedIndex <Index > index = SimpleMappedIndex .of ( Index ::new ).name ( "cardinality-index" );
240
+
241
+ /**
242
+ * In this case we build a projection with object/fields projections recreating the entity tree structure.
243
+ * Hence, it should be close to what a projection constructor is doing.
244
+ */
245
+ @ Test
246
+ void testAsIfItWasProjectionConstructors () {
247
+ List <List <?>> hits = index .createScope ().query ()
248
+ .select ( f -> f .composite (
249
+ f .id (),
250
+ f .field ( "string" ),
251
+ f .object ( "contained" ).from (
252
+ f .field ( "contained.id" ),
253
+ f .field ( "contained.containedString" ),
254
+ f .object ( "contained.deeperContained" ).from (
255
+ f .field ( "contained.deeperContained.id" ),
256
+ f .field ( "contained.deeperContained.deeperContainedString" )
257
+ ).asList ()
258
+ ).asList ().list ()
259
+ ) )
260
+ .where ( f -> f .matchAll () )
261
+ .fetchAllHits ();
262
+
263
+ assertThat ( hits ).hasSize ( 1 )
264
+ .contains (
265
+ List .of (
266
+ "1" ,
267
+ "string" ,
268
+ List .of (
269
+ List .of ( "10" , "containedString1" ,
270
+ List .of ( "100" , "deeperContainedString1" ) ),
271
+ List .of ( "20" , "containedString2" ,
272
+ List .of ( "200" , "deeperContainedString2" ) )
273
+ )
274
+ )
275
+ );
276
+ }
277
+
278
+ /**
279
+ * We want to make sure that cardinality is correctly checked when we request a projection for a single field with a "long" path
280
+ * containing both single and multi fields in it.
281
+ * <p>
282
+ * Since there is a multi-`contained` the resulting caridnality of the projected field is expected to be also multi
283
+ */
284
+ @ Test
285
+ void testFieldProjectionLongPath_correctCardinality () {
286
+ List <List <Object >> hits = index .createScope ().query ()
287
+ .select ( f -> f .field ( "contained.deeperContained.id" ).list () )
288
+ .where ( f -> f .matchAll () )
289
+ .fetchAllHits ();
290
+
291
+ assertThat ( hits ).hasSize ( 1 )
292
+ .contains ( List .of ( "100" , "200" ) );
293
+ }
294
+
295
+ @ Test
296
+ void testFieldProjectionLongPath_incorrectCardinality () {
297
+ assertThatThrownBy ( () -> index .createScope ().query ()
298
+ .select ( f -> f .field ( "contained.deeperContained.id" ) )
299
+ .where ( f -> f .matchAll () )
300
+ .fetchAllHits () )
301
+ .isInstanceOf ( SearchException .class )
302
+ .hasMessageContaining ( "Invalid cardinality for projection on field 'contained.deeperContained.id'" );
303
+ }
304
+
305
+ @ Test
306
+ void testFieldProjectionLongPath_correctCardinality_multiAtDifferentLevelInPath () {
307
+ assertThat ( containing .createScope ().query ()
308
+ .select ( f -> f .field ( "single.contained.deeperContained.id" ).list () )
309
+ .where ( f -> f .matchAll () )
310
+ .fetchAllHits ()
311
+ ).hasSize ( 1 )
312
+ .contains ( List .of ( "100" , "200" ) );
313
+ }
314
+
315
+ @ Test
316
+ void testFieldProjectionLongPath_correctCardinality_multiAtDifferentLevelInPath_multipleMultis () {
317
+ assertThat ( containing .createScope ().query ()
318
+ .select ( f -> f .field ( "list.contained.deeperContained.id" ).list () )
319
+ .where ( f -> f .matchAll () )
320
+ .fetchAllHits () ).hasSize ( 1 )
321
+ .contains ( List .of ( "100" , "200" ) );
322
+ }
323
+
324
+ /**
325
+ * Here we take all multi fields as object projections with expected cardinalities and then
326
+ * add the "deepest" field as a simple single-valued field:
327
+ */
328
+ @ Test
329
+ void testFieldProjectionLongPath_correctCardinality_multiFieldsAsObjects () {
330
+ assertThat ( containing .createScope ().query ()
331
+ .select ( f -> f .object ( "list" )
332
+ .from ( f .object ( "list.contained" )
333
+ .from ( f .field ( "list.contained.deeperContained.id" ) ).asList ().list () )
334
+ .asList ().list () )
335
+ .where ( f -> f .matchAll () )
336
+ .fetchAllHits () ).hasSize ( 1 )
337
+ .contains (
338
+ List .of ( List .of ( List .of ( List .of ( "100" ), List .of ( "200" ) ) ) ) );
339
+ }
340
+
341
+ @ Test
342
+ void testFieldProjectionLongPath_incorrectCardinality_multiAtDifferentLevelInPath_multipleMultis () {
343
+ assertThatThrownBy ( () -> containing .createScope ().query ()
344
+ .select ( f -> f .field ( "list.contained.deeperContained.id" ) )
345
+ .where ( f -> f .matchAll () )
346
+ .fetchAllHits () )
347
+ .isInstanceOf ( SearchException .class )
348
+ .hasMessageContaining (
349
+ "Invalid cardinality for projection on field 'list.contained.deeperContained.id'" );
350
+ }
351
+
352
+ @ Test
353
+ void testFieldProjectionLongPath_incorrectCardinality_multiAtDifferentLevelInPath () {
354
+ assertThatThrownBy ( () -> containing .createScope ().query ()
355
+ .select ( f -> f .field ( "single.contained.deeperContained.id" ) )
356
+ .where ( f -> f .matchAll () )
357
+ .fetchAllHits () )
358
+ .isInstanceOf ( SearchException .class )
359
+ .hasMessageContaining (
360
+ "Invalid cardinality for projection on field 'single.contained.deeperContained.id'" );
361
+ }
362
+
363
+ public static final class IndexContaining extends IndexBase {
364
+ public IndexContaining (IndexSchemaElement root ) {
365
+ IndexSchemaObjectField single = root .objectField ( "single" , ObjectStructure .NESTED );
366
+ addIndexFields ( single );
367
+ single .toReference ();
368
+
369
+ IndexSchemaObjectField list = root .objectField ( "list" , ObjectStructure .NESTED ).multiValued ();
370
+ addIndexFields ( list );
371
+ list .toReference ();
372
+ }
373
+
374
+ BulkIndexer contribute (BulkIndexer indexer ) {
375
+ indexer .add ( "1" , d -> {
376
+ contribute ( d .addObject ( "single" ) );
377
+ contribute ( d .addObject ( "list" ) );
378
+ } );
379
+ return indexer ;
380
+ }
381
+ }
382
+
383
+ public static class Index extends IndexBase {
384
+
385
+ public Index (IndexSchemaElement root ) {
386
+ addIndexFields ( root );
387
+ }
388
+
389
+ BulkIndexer contribute (BulkIndexer indexer ) {
390
+ indexer .add ( "1" , this ::contribute );
391
+ return indexer ;
392
+ }
393
+ }
394
+
395
+ public abstract static class IndexBase {
396
+ protected void addIndexFields (IndexSchemaElement el ) {
397
+ el .field ( "id" , f -> f .asString ().projectable ( Projectable .YES ) ).toReference ();
398
+ el .field ( "string" , f -> f .asString ().projectable ( Projectable .YES ) ).toReference ();
399
+
400
+ IndexSchemaObjectField contained = el .objectField ( "contained" , ObjectStructure .NESTED ).multiValued ();
401
+ contained .field ( "id" , f -> f .asString ().projectable ( Projectable .YES ) ).toReference ();
402
+ contained .field ( "containedString" , f -> f .asString ().projectable ( Projectable .YES ) ).toReference ();
403
+
404
+ IndexSchemaObjectField deeperContained = contained .objectField ( "deeperContained" , ObjectStructure .NESTED );
405
+ deeperContained .field ( "id" , f -> f .asString ().projectable ( Projectable .YES ) ).toReference ();
406
+ deeperContained .field ( "deeperContainedString" , f -> f .asString ().projectable ( Projectable .YES ) )
407
+ .toReference ();
408
+
409
+ deeperContained .toReference ();
410
+ contained .toReference ();
411
+ }
412
+
413
+ protected void contribute (DocumentElement element ) {
414
+ element .addValue ( "id" , "1" );
415
+ element .addValue ( "string" , "string" );
416
+
417
+ DocumentElement contained = element .addObject ( "contained" );
418
+ contained .addValue ( "id" , "10" );
419
+ contained .addValue ( "containedString" , "containedString1" );
420
+ DocumentElement deeperContained = contained .addObject ( "deeperContained" );
421
+ deeperContained .addValue ( "id" , "100" );
422
+ deeperContained .addValue ( "deeperContainedString" , "deeperContainedString1" );
423
+
424
+ contained = element .addObject ( "contained" );
425
+ contained .addValue ( "id" , "20" );
426
+ contained .addValue ( "containedString" , "containedString2" );
427
+ deeperContained = contained .addObject ( "deeperContained" );
428
+ deeperContained .addValue ( "id" , "200" );
429
+ deeperContained .addValue ( "deeperContainedString" , "deeperContainedString2" );
430
+
431
+ }
432
+ }
433
+ }
214
434
}
0 commit comments