@@ -75,6 +75,49 @@ public void testIsValidJoinDefinitionOnKey() {
7575 Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
7676 Schema .Field .of ("zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
7777
78+ Schema outputSchema =
79+ Schema .recordOf ("Join" ,
80+ Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
81+ Schema .Field .of ("from_zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
82+
83+ // First join is a right join, second join is a left join
84+ JoinStage shipments = JoinStage .builder ("Shipments" , shipmentSchema ).setRequired (true ).build ();
85+ JoinStage fromAddresses = JoinStage .builder ("FromAddress" , fromAddressSchema ).setRequired (true ).build ();
86+
87+ // null safe
88+ JoinCondition condition = JoinCondition .onKeys ()
89+ .addKey (new JoinKey ("Shipments" , Arrays .asList ("id" )))
90+ .addKey (new JoinKey ("FromAddress" , Arrays .asList ("shipment_id" )))
91+ .setNullSafe (false )
92+ .build ();
93+
94+ JoinDefinition joinDefinition = JoinDefinition .builder ()
95+ .select (new JoinField ("Shipments" , "id" , "shipment_id" ),
96+ new JoinField ("FromAddress" , "zip" , "from_zip" ))
97+ .from (shipments , fromAddresses )
98+ .on (condition )
99+ .setOutputSchemaName ("Join" )
100+ .setOutputSchema (outputSchema )
101+ .build ();
102+
103+ SQLJoinDefinition sqlJoinDefinition = new SQLJoinDefinition ("Join" , joinDefinition );
104+
105+ Assert .assertTrue (BigQuerySQLEngine .isValidJoinDefinition (sqlJoinDefinition ));
106+ verify (logger , times (0 )).warn (anyString (), anyString (), anyString ());
107+ }
108+
109+ @ Test
110+ public void testInnerJoinFor3StagesIsSupported () {
111+ Schema shipmentSchema =
112+ Schema .recordOf ("Shipments" ,
113+ Schema .Field .of ("id" , Schema .of (Schema .Type .INT )));
114+
115+ Schema fromAddressSchema =
116+ Schema .recordOf ("FromAddress" ,
117+ Schema .Field .of ("id" , Schema .of (Schema .Type .INT )),
118+ Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
119+ Schema .Field .of ("zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
120+
78121 Schema toAddressSchema =
79122 Schema .recordOf ("ToAddress" ,
80123 Schema .Field .of ("id" , Schema .of (Schema .Type .INT )),
@@ -116,6 +159,74 @@ public void testIsValidJoinDefinitionOnKey() {
116159 verify (logger , times (0 )).warn (anyString (), anyString (), anyString ());
117160 }
118161
162+ @ Test
163+ public void testOuterJoinFor3StagesIsNotSupported () {
164+ ArgumentCaptor <String > messageTemplateCaptor = ArgumentCaptor .forClass (String .class );
165+ ArgumentCaptor <String > stageNameCaptor = ArgumentCaptor .forClass (String .class );
166+ ArgumentCaptor <String > issuesCaptor = ArgumentCaptor .forClass (String .class );
167+
168+ Schema shipmentSchema =
169+ Schema .recordOf ("Shipments" ,
170+ Schema .Field .of ("id" , Schema .of (Schema .Type .INT )));
171+
172+ Schema fromAddressSchema =
173+ Schema .recordOf ("FromAddress" ,
174+ Schema .Field .of ("id" , Schema .of (Schema .Type .INT )),
175+ Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
176+ Schema .Field .of ("zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
177+
178+ Schema toAddressSchema =
179+ Schema .recordOf ("ToAddress" ,
180+ Schema .Field .of ("id" , Schema .of (Schema .Type .INT )),
181+ Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
182+ Schema .Field .of ("zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
183+
184+ Schema outputSchema =
185+ Schema .recordOf ("Join" ,
186+ Schema .Field .of ("shipment_id" , Schema .of (Schema .Type .INT )),
187+ Schema .Field .of ("from_zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))),
188+ Schema .Field .of ("to_zip" , Schema .nullableOf (Schema .of (Schema .Type .INT ))));
189+
190+ // First join is a right join, second join is a left join
191+ JoinStage shipments = JoinStage .builder ("Shipments" , shipmentSchema ).setRequired (true ).build ();
192+ JoinStage fromAddresses = JoinStage .builder ("FromAddress" , fromAddressSchema ).setRequired (true ).build ();
193+ JoinStage toAddresses = JoinStage .builder ("ToAddress" , toAddressSchema ).setRequired (false ).build ();
194+
195+ // null safe
196+ JoinCondition condition = JoinCondition .onKeys ()
197+ .addKey (new JoinKey ("Shipments" , Arrays .asList ("id" )))
198+ .addKey (new JoinKey ("FromAddress" , Arrays .asList ("shipment_id" )))
199+ .addKey (new JoinKey ("ToAddress" , Arrays .asList ("shipment_id" )))
200+ .setNullSafe (false )
201+ .build ();
202+
203+ JoinDefinition joinDefinition = JoinDefinition .builder ()
204+ .select (new JoinField ("Shipments" , "id" , "shipment_id" ),
205+ new JoinField ("FromAddress" , "zip" , "from_zip" ),
206+ new JoinField ("ToAddress" , "zip" , "to_zip" ))
207+ .from (shipments , fromAddresses , toAddresses )
208+ .on (condition )
209+ .setOutputSchemaName ("Join" )
210+ .setOutputSchema (outputSchema )
211+ .build ();
212+
213+ SQLJoinDefinition sqlJoinDefinition = new SQLJoinDefinition ("Join" , joinDefinition );
214+
215+ Assert .assertFalse (BigQuerySQLEngine .isValidJoinDefinition (sqlJoinDefinition ));
216+ verify (logger ).warn (messageTemplateCaptor .capture (), stageNameCaptor .capture (), issuesCaptor .capture ());
217+
218+ String messageTemplate = messageTemplateCaptor .getValue ();
219+ Assert .assertTrue (messageTemplate .contains (
220+ "Join operation for stage '{}' could not be executed in BigQuery. Issues found:" ));
221+
222+ String stageName = stageNameCaptor .getValue ();
223+ Assert .assertEquals ("Join" , stageName );
224+
225+ String issues = issuesCaptor .getValue ();
226+ Assert .assertTrue (issues .contains (
227+ "Only 2 input stages are supported for outer joins, 3 stages supplied." ));
228+ }
229+
119230 @ Test
120231 public void testIsValidJoinDefinitionOnKeyWithErrors () {
121232 ArgumentCaptor <String > messageTemplateCaptor = ArgumentCaptor .forClass (String .class );
0 commit comments