1010import static org .junit .jupiter .api .Assertions .assertTrue ;
1111import static org .junit .jupiter .api .Assertions .fail ;
1212
13+ import com .fasterxml .jackson .databind .ObjectMapper ;
1314import com .fasterxml .jackson .databind .node .TextNode ;
15+ import java .io .IOException ;
1416import java .io .UncheckedIOException ;
1517import java .util .ArrayList ;
1618import java .util .List ;
1719import java .util .Map ;
1820import java .util .UUID ;
21+ import lombok .SneakyThrows ;
1922import lombok .extern .log4j .Log4j2 ;
20- import org .apache .commons .lang3 .tuple .Pair ;
21- import org .apache .commons .lang3 .tuple .Triple ;
2223import org .folio .fqm .exception .InvalidFqlException ;
2324import org .folio .fqm .migration .types .MigratableFqlFieldAndCondition ;
2425import org .folio .fqm .migration .types .SingleFieldMigrationResult ;
26+ import org .folio .fqm .migration .warnings .RemovedFieldWarning ;
2527import org .junit .jupiter .api .Test ;
2628import org .junit .jupiter .params .ParameterizedTest ;
2729import org .junit .jupiter .params .provider .Arguments ;
3133class MigrationUtilsTest {
3234
3335 private static final UUID TEST_UUID = UUID .fromString ("d686ef05-fb3d-5edc-a87f-c3001c579dfb" );
36+ private static final UUID UUID_A = UUID .fromString ("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" );
37+ private static final UUID UUID_B = UUID .fromString ("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" );
38+ private static final UUID UUID_C = UUID .fromString ("cccccccc-cccc-cccc-cccc-cccccccccccc" );
3439 private static final Map <UUID , Map <String , UUID >> EMPTY_SOURCE_MAP = Map .of ();
40+ private static final Map <UUID , Map <String , UUID >> DUMMY_SOURCE_MAP = Map .ofEntries (
41+ Map .entry (TEST_UUID , Map .of ("sourceA" , UUID_A , "sourceB" , UUID_B )),
42+ Map .entry (UUID_B , Map .of ("sourceC" , UUID_C ))
43+ );
3544
36- static List <Arguments > functionCallTestCases () {
45+ static List <Arguments > migrateFqlFunctionCallTestCases () {
3746 return List .of (
38- // query, list of expected field transformation calls from migrateFql,
39- // list of field transformation calls from migrateAndReshapeFql
40- Arguments .of ("{}" , List .of (), List .of ()),
41- Arguments .of ("{\" _version\" :\" 1\" }" , List .of (), List .of ()),
47+ // query, list of expected field transformation calls from migrateFql
48+ Arguments .of ("{}" , List .of ()),
49+ Arguments .of ("{\" _version\" :\" 1\" }" , List .of ()),
4250 // basic single-field query
43- Arguments .of (
44- "{\" field\" :{\" $eq\" :\" foo\" }}" ,
45- List .of (Pair .of ("field" , "{\" $eq\" :\" foo\" }" )),
46- List .of (Triple .of ("field" , "$eq" , "\" foo\" " ))
47- ),
48- Arguments .of (
49- "{\" _version\" :\" 1\" , \" field\" :{\" $eq\" :\" foo\" }}" ,
50- List .of (Pair .of ("field" , "{\" $eq\" :\" foo\" }" )),
51- List .of (Triple .of ("field" , "$eq" , "\" foo\" " ))
52- ),
51+ Arguments .of ("{\" field\" :{\" $eq\" :\" foo\" }}" , List .of (callWith ("field" , "$eq" , "\" foo\" " ))),
52+ Arguments .of ("{\" _version\" :\" 1\" , \" field\" :{\" $eq\" :\" foo\" }}" , List .of (callWith ("field" , "$eq" , "\" foo\" " ))),
5353 // multi-field query, without $and
5454 Arguments .of (
5555 """
@@ -60,14 +60,9 @@ static List<Arguments> functionCallTestCases() {
6060 }
6161 """ ,
6262 List .of (
63- Pair .of ("field1" , "{\" $eq\" :\" foo\" }" ),
64- Pair .of ("field2" , "{\" $le\" :\" bar\" }" ),
65- Pair .of ("field3" , "{\" $ne\" :\" baz\" }" )
66- ),
67- List .of (
68- Triple .of ("field1" , "$eq" , "\" foo\" " ),
69- Triple .of ("field2" , "$le" , "\" bar\" " ),
70- Triple .of ("field3" , "$ne" , "\" baz\" " )
63+ callWith ("field1" , "$eq" , "\" foo\" " ),
64+ callWith ("field2" , "$le" , "\" bar\" " ),
65+ callWith ("field3" , "$ne" , "\" baz\" " )
7166 )
7267 ),
7368 Arguments .of (
@@ -80,14 +75,9 @@ static List<Arguments> functionCallTestCases() {
8075 }
8176 """ ,
8277 List .of (
83- Pair .of ("field1" , "{\" $eq\" :\" foo\" }" ),
84- Pair .of ("field2" , "{\" $le\" :\" bar\" }" ),
85- Pair .of ("field3" , "{\" $ne\" :\" baz\" }" )
86- ),
87- List .of (
88- Triple .of ("field1" , "$eq" , "\" foo\" " ),
89- Triple .of ("field2" , "$le" , "\" bar\" " ),
90- Triple .of ("field3" , "$ne" , "\" baz\" " )
78+ callWith ("field1" , "$eq" , "\" foo\" " ),
79+ callWith ("field2" , "$le" , "\" bar\" " ),
80+ callWith ("field3" , "$ne" , "\" baz\" " )
9181 )
9282 ),
9383 // multi-operator single-field query
@@ -98,8 +88,7 @@ static List<Arguments> functionCallTestCases() {
9888 "$ge": 100
9989 }}
10090 """ ,
101- List .of (Pair .of ("field1" , "{\" $le\" :500,\" $ge\" :100}" )),
102- List .of (Triple .of ("field1" , "$le" , "500" ), Triple .of ("field1" , "$ge" , "100" ))
91+ List .of (callWith ("field1" , "$le" , "500" ), callWith ("field1" , "$ge" , "100" ))
10392 ),
10493 // query with $and
10594 Arguments .of (
@@ -109,8 +98,7 @@ static List<Arguments> functionCallTestCases() {
10998 { "field": {"$ne": "bar"} }
11099 ]}
111100 """ ,
112- List .of (Pair .of ("field" , "{\" $ne\" :\" foo\" }" ), Pair .of ("field" , "{\" $ne\" :\" bar\" }" )),
113- List .of (Triple .of ("field" , "$ne" , "\" foo\" " ), Triple .of ("field" , "$ne" , "\" bar\" " ))
101+ List .of (callWith ("field" , "$ne" , "\" foo\" " ), callWith ("field" , "$ne" , "\" bar\" " ))
114102 ),
115103 // putting everything together
116104 Arguments .of (
@@ -133,56 +121,103 @@ static List<Arguments> functionCallTestCases() {
133121 }
134122 """ ,
135123 List .of (
136- Pair .of ("field1" , "{\" $eq\" :\" foo\" }" ),
137- Pair .of ("field2" , "{\" $le\" :\" bar\" }" ),
138- Pair .of ("field3" , "{\" $ne\" :\" baz\" ,\" $gt\" :100}" ),
139- Pair .of ("field4" , "{\" $ne\" :\" foo\" }" ),
140- Pair .of ("field5" , "{\" $ne\" :\" bar\" ,\" $lt\" :100}" )
141- ),
124+ callWith ("field1" , "$eq" , "\" foo\" " ),
125+ callWith ("field2" , "$le" , "\" bar\" " ),
126+ callWith ("field3" , "$ne" , "\" baz\" " ),
127+ callWith ("field3" , "$gt" , "100" ),
128+ callWith ("field4" , "$ne" , "\" foo\" " ),
129+ callWith ("field5" , "$ne" , "\" bar\" " ),
130+ callWith ("field5" , "$lt" , "100" )
131+ )
132+ ),
133+ // basic nested source
134+ Arguments .of (
135+ "{\" sourceA.field\" :{\" $eq\" :\" foo\" }}" ,
136+ List .of (callWith ("sourceA.field" , "$eq" , "\" foo\" " ), callWith (UUID_A , "sourceA." , "field" , "$eq" , "\" foo\" " ))
137+ ),
138+ // doubly nested source
139+ Arguments .of (
140+ "{\" sourceB.sourceC.field\" :{\" $eq\" :\" foo\" }}" ,
141+ List .of (
142+ callWith (TEST_UUID , "" , "sourceB.sourceC.field" , "$eq" , "\" foo\" " ),
143+ callWith (UUID_B , "sourceB." , "sourceC.field" , "$eq" , "\" foo\" " ),
144+ callWith (UUID_C , "sourceB.sourceC." , "field" , "$eq" , "\" foo\" " )
145+ )
146+ ),
147+ // no nested source match, so we can't traverse inside
148+ Arguments .of (
149+ """
150+ {
151+ "nested.field":{"$eq":"foo"},
152+ "sourceB.garbage.field":{"$eq":"bar"}
153+ }
154+ """ ,
155+ List .of (
156+ callWith (TEST_UUID , "" , "nested.field" , "$eq" , "\" foo\" " ),
157+ callWith (TEST_UUID , "" , "sourceB.garbage.field" , "$eq" , "\" bar\" " ),
158+ callWith (UUID_B , "sourceB." , "garbage.field" , "$eq" , "\" bar\" " )
159+ )
160+ ),
161+ // early breaks. since the outer (composite'd) field was changed, we shouldn't keep traversing
162+ // (e.g. migration applies to composite specifically and/or has composite-specific logic)
163+ // these are configured to perform the $ACTION with exactly one outer source
164+ Arguments .of (
165+ """
166+ {
167+ "sourceB.$REMOVE":{"$eq":"foo"},
168+ "sourceB.$CHANGE":{"$eq":"bar"},
169+ "sourceB.$ADD":{"$eq":"boo"},
170+ "sourceB.$BREAK":{"$eq":"baz"},
171+ "sourceB.sourceC.$WARN":{"$eq":"baz"}
172+ }
173+ """ ,
142174 List .of (
143- Triple .of ("field1" , "$eq" , "\" foo\" " ),
144- Triple .of ("field2" , "$le" , "\" bar\" " ),
145- Triple .of ("field3" , "$ne" , "\" baz\" " ),
146- Triple .of ("field3" , "$gt" , "100" ),
147- Triple .of ("field4" , "$ne" , "\" foo\" " ),
148- Triple .of ("field5" , "$ne" , "\" bar\" " ),
149- Triple .of ("field5" , "$lt" , "100" )
175+ callWith ("sourceB.$REMOVE" , "$eq" , "\" foo\" " ),
176+ callWith ("sourceB.$CHANGE" , "$eq" , "\" bar\" " ),
177+ callWith ("sourceB.$ADD" , "$eq" , "\" boo\" " ),
178+ callWith ("sourceB.$BREAK" , "$eq" , "\" baz\" " ),
179+ callWith ("sourceB.sourceC.$WARN" , "$eq" , "\" baz\" " ),
180+ callWith (UUID_B , "sourceB." , "sourceC.$WARN" , "$eq" , "\" baz\" " )
150181 )
151182 )
152183 );
153184 }
154185
155186 @ ParameterizedTest
156- @ MethodSource ("functionCallTestCases" )
157- void testMigrateAndReshapeFqlFunctionCalls (
158- String query ,
159- List <Pair <String , String >> unused ,
160- List <Triple <String , String , String >> fieldArguments
161- ) {
162- List <Triple <String , String , String >> fieldArgumentsLeftToGet = new ArrayList <>(fieldArguments );
187+ @ MethodSource ("migrateFqlFunctionCallTestCases" )
188+ void testMigrateFqlFunctionCalls (String query , List <MigratableFqlFieldAndCondition > fieldArguments ) {
189+ List <MigratableFqlFieldAndCondition > fieldArgumentsLeftToGet = new ArrayList <>(fieldArguments );
163190
164191 MigrationUtils .migrateFql (
165192 TEST_UUID ,
166193 query ,
167- original -> {
194+ (MigratableFqlFieldAndCondition original ) -> {
195+ assertThat (original .entityTypeId (), is (notNullValue ()));
196+ assertThat (original .fieldPrefix (), is (notNullValue ()));
168197 assertThat (original .field (), is (notNullValue ()));
169198 assertThat (original .operator (), is (notNullValue ()));
170199 assertThat (original .value (), is (notNullValue ()));
171200
172- Triple <String , String , String > actual = Triple .of (
173- original .field (),
174- original .operator (),
175- original .value ().toString ()
176- );
177- if (fieldArgumentsLeftToGet .contains (actual )) {
178- fieldArgumentsLeftToGet .remove (actual );
201+ if (fieldArgumentsLeftToGet .contains (original )) {
202+ fieldArgumentsLeftToGet .remove (original );
179203 } else {
180- fail ("Unexpected field transformation call: " + actual . getLeft () + " -> " + actual . getRight () );
204+ fail ("Unexpected field transformation call: " + original );
181205 }
182206
183- return SingleFieldMigrationResult .removed ();
207+ if (original .field ().matches ("^\\ w+\\ .\\ $REMOVE.*$" )) {
208+ return SingleFieldMigrationResult .removed ();
209+ } else if (original .field ().matches ("^\\ w+\\ .\\ $CHANGE.*$" )) {
210+ return SingleFieldMigrationResult .withField (original .withField ("new_field" ));
211+ } else if (original .field ().matches ("^\\ w+\\ .\\ $ADD.*$" )) {
212+ return new SingleFieldMigrationResult <>(List .of (original , original ), List .of (), false );
213+ } else if (original .field ().matches ("^\\ w+\\ .\\ $BREAK.*$" )) {
214+ return SingleFieldMigrationResult .noop (original ).withHadBreakingChange (true );
215+ } else if (original .field ().matches ("^\\ w+\\ .\\ $WARN.*$" )) {
216+ return SingleFieldMigrationResult .noop (original ).withWarnings (List .of (RemovedFieldWarning .builder ().build ()));
217+ }
218+ return SingleFieldMigrationResult .noop (original );
184219 },
185- EMPTY_SOURCE_MAP
220+ DUMMY_SOURCE_MAP
186221 );
187222
188223 assertThat (fieldArgumentsLeftToGet , is (empty ()));
@@ -255,6 +290,43 @@ void testReshapeWithMultipleFields() {
255290 );
256291 }
257292
293+ @ Test
294+ void testReshapeWithNestedSourceField () {
295+ assertThat (
296+ MigrationUtils
297+ .migrateFql (
298+ TEST_UUID ,
299+ "{\" _version\" :\" old\" ,\" sourceB.sourceC.field\" :{\" $eq\" : 123}}" ,
300+ original -> {
301+ if (original .equals (callWith ("sourceB.sourceC.field" , "$eq" , "123" ))) {
302+ return SingleFieldMigrationResult .noop (original ); // noop
303+ } else if (original .equals (callWith (UUID_B , "sourceB." , "sourceC.field" , "$eq" , "123" ))) {
304+ // change is triggered on sourceB level, replacing sourceC.field with these.
305+ // sourceC will no longer be referenced directly in the resulting query
306+ return new SingleFieldMigrationResult <>(
307+ List .of (
308+ original .withField ("new1" ).withOperator ("$a" ).withValue (new TextNode ("aaa" )),
309+ original .withField ("new2" ).withOperator ("$b" ).withValue (new TextNode ("bbb" ))
310+ ),
311+ List .of (),
312+ false
313+ );
314+ } else {
315+ fail ("Unexpected field transformation call: " + original );
316+ return null ;
317+ }
318+ },
319+ DUMMY_SOURCE_MAP
320+ )
321+ .result (),
322+ is (
323+ equalTo (
324+ "{\" _version\" :\" old\" ,\" $and\" :[{\" sourceB.new1\" :{\" $a\" :\" aaa\" }},{\" sourceB.new2\" :{\" $b\" :\" bbb\" }}]}"
325+ )
326+ )
327+ );
328+ }
329+
258330 @ Test
259331 void testInvalidJson () {
260332 assertThrows (
@@ -328,4 +400,19 @@ void testSingleSegment() {
328400 assertTrue (MigrationUtils .compareVersions ("b" , "a" ) > 0 );
329401 assertEquals (0 , MigrationUtils .compareVersions ("x" , "x" ));
330402 }
403+
404+ private static MigratableFqlFieldAndCondition callWith (String field , String op , String value ) {
405+ return callWith (TEST_UUID , "" , field , op , value );
406+ }
407+
408+ @ SneakyThrows (IOException .class )
409+ private static MigratableFqlFieldAndCondition callWith (
410+ UUID entityId ,
411+ String fieldPrefix ,
412+ String field ,
413+ String op ,
414+ String value
415+ ) {
416+ return new MigratableFqlFieldAndCondition (entityId , fieldPrefix , field , op , new ObjectMapper ().readTree (value ));
417+ }
331418}
0 commit comments