@@ -163,6 +163,100 @@ public class LookupFromIndexIT extends AbstractEsqlIntegTestCase {
163163 new EsField ("rkey3" , DataType .INTEGER , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
164164 );
165165
166+ // Precreate left-side key attributes (from source index) - up to 4 keys as seen in tests
167+ private static final FieldAttribute KEY0_KEYWORD_ATTR = new FieldAttribute (
168+ Source .EMPTY ,
169+ null ,
170+ null ,
171+ "key0" ,
172+ new EsField ("key0" , DataType .KEYWORD , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
173+ );
174+ private static final FieldAttribute KEY0_LONG_ATTR = new FieldAttribute (
175+ Source .EMPTY ,
176+ null ,
177+ null ,
178+ "key0" ,
179+ new EsField ("key0" , DataType .LONG , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
180+ );
181+ private static final FieldAttribute KEY1_KEYWORD_ATTR = new FieldAttribute (
182+ Source .EMPTY ,
183+ null ,
184+ null ,
185+ "key1" ,
186+ new EsField ("key1" , DataType .KEYWORD , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
187+ );
188+ private static final FieldAttribute KEY1_LONG_ATTR = new FieldAttribute (
189+ Source .EMPTY ,
190+ null ,
191+ null ,
192+ "key1" ,
193+ new EsField ("key1" , DataType .LONG , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
194+ );
195+ private static final FieldAttribute KEY2_KEYWORD_ATTR = new FieldAttribute (
196+ Source .EMPTY ,
197+ null ,
198+ null ,
199+ "key2" ,
200+ new EsField ("key2" , DataType .KEYWORD , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
201+ );
202+ private static final FieldAttribute KEY2_LONG_ATTR = new FieldAttribute (
203+ Source .EMPTY ,
204+ null ,
205+ null ,
206+ "key2" ,
207+ new EsField ("key2" , DataType .LONG , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
208+ );
209+ private static final FieldAttribute KEY3_KEYWORD_ATTR = new FieldAttribute (
210+ Source .EMPTY ,
211+ null ,
212+ null ,
213+ "key3" ,
214+ new EsField ("key3" , DataType .KEYWORD , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
215+ );
216+ private static final FieldAttribute KEY3_INTEGER_ATTR = new FieldAttribute (
217+ Source .EMPTY ,
218+ null ,
219+ null ,
220+ "key3" ,
221+ new EsField ("key3" , DataType .INTEGER , Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
222+ );
223+
224+ // Index all attributes by name and type for easy lookup - built dynamically from attribute names
225+ // Key format: "name:type" (e.g., "key0:KEYWORD", "key0:LONG", "rkey0:KEYWORD", "l:LONG")
226+ private static final Map <String , FieldAttribute > ATTRIBUTES_BY_NAME_AND_TYPE = Map .ofEntries (
227+ // Left-side attributes (from source index)
228+ Map .entry (KEY0_KEYWORD_ATTR .name () + ":" + KEY0_KEYWORD_ATTR .dataType (), KEY0_KEYWORD_ATTR ),
229+ Map .entry (KEY0_LONG_ATTR .name () + ":" + KEY0_LONG_ATTR .dataType (), KEY0_LONG_ATTR ),
230+ Map .entry (KEY1_KEYWORD_ATTR .name () + ":" + KEY1_KEYWORD_ATTR .dataType (), KEY1_KEYWORD_ATTR ),
231+ Map .entry (KEY1_LONG_ATTR .name () + ":" + KEY1_LONG_ATTR .dataType (), KEY1_LONG_ATTR ),
232+ Map .entry (KEY2_KEYWORD_ATTR .name () + ":" + KEY2_KEYWORD_ATTR .dataType (), KEY2_KEYWORD_ATTR ),
233+ Map .entry (KEY2_LONG_ATTR .name () + ":" + KEY2_LONG_ATTR .dataType (), KEY2_LONG_ATTR ),
234+ Map .entry (KEY3_KEYWORD_ATTR .name () + ":" + KEY3_KEYWORD_ATTR .dataType (), KEY3_KEYWORD_ATTR ),
235+ Map .entry (KEY3_INTEGER_ATTR .name () + ":" + KEY3_INTEGER_ATTR .dataType (), KEY3_INTEGER_ATTR ),
236+ // Right-side attributes (from lookup index)
237+ Map .entry (RKEY0_KEYWORD_ATTR .name () + ":" + RKEY0_KEYWORD_ATTR .dataType (), RKEY0_KEYWORD_ATTR ),
238+ Map .entry (RKEY0_LONG_ATTR .name () + ":" + RKEY0_LONG_ATTR .dataType (), RKEY0_LONG_ATTR ),
239+ Map .entry (RKEY1_KEYWORD_ATTR .name () + ":" + RKEY1_KEYWORD_ATTR .dataType (), RKEY1_KEYWORD_ATTR ),
240+ Map .entry (RKEY1_LONG_ATTR .name () + ":" + RKEY1_LONG_ATTR .dataType (), RKEY1_LONG_ATTR ),
241+ Map .entry (RKEY2_KEYWORD_ATTR .name () + ":" + RKEY2_KEYWORD_ATTR .dataType (), RKEY2_KEYWORD_ATTR ),
242+ Map .entry (RKEY2_LONG_ATTR .name () + ":" + RKEY2_LONG_ATTR .dataType (), RKEY2_LONG_ATTR ),
243+ Map .entry (RKEY3_KEYWORD_ATTR .name () + ":" + RKEY3_KEYWORD_ATTR .dataType (), RKEY3_KEYWORD_ATTR ),
244+ Map .entry (RKEY3_INTEGER_ATTR .name () + ":" + RKEY3_INTEGER_ATTR .dataType (), RKEY3_INTEGER_ATTR ),
245+ Map .entry (R_FIELD_ATTR .name () + ":" + R_FIELD_ATTR .dataType (), R_FIELD_ATTR )
246+ );
247+
248+ /**
249+ * Gets a FieldAttribute by name and type. Throws IllegalArgumentException if not found.
250+ */
251+ private static FieldAttribute getAttribute (String name , DataType type ) {
252+ String key = name + ":" + type ;
253+ FieldAttribute attr = ATTRIBUTES_BY_NAME_AND_TYPE .get (key );
254+ if (attr == null ) {
255+ throw new IllegalArgumentException ("Attribute not found: " + key );
256+ }
257+ return attr ;
258+ }
259+
166260 public void testKeywordKey () throws IOException {
167261 runLookup (List .of (DataType .KEYWORD ), new UsingSingleLookupTable (new Object [][] { new String [] { "aa" , "bb" , "cc" , "dd" } }), null );
168262 }
@@ -340,26 +434,40 @@ private PhysicalPlan buildRightPreJoinPlan(List<DataType> keyTypes, Expression f
340434 return new FragmentExec (esRelation );
341435 }
342436
437+ /**
438+ * Gets the left-side attribute for a given index and type.
439+ * This ensures consistent NameId usage across MatchConfig and join conditions.
440+ */
441+ private FieldAttribute getLeftSideAttribute (int index , DataType keyType ) {
442+ String name = "key" + index ;
443+ // Handle special case for key3 which can be INTEGER or KEYWORD
444+ if (index == 3 && keyType == DataType .INTEGER ) {
445+ return getAttribute (name , DataType .INTEGER );
446+ }
447+ // Default to KEYWORD or LONG based on keyType
448+ if (keyType == DataType .KEYWORD ) {
449+ return getAttribute (name , DataType .KEYWORD );
450+ } else {
451+ return getAttribute (name , DataType .LONG );
452+ }
453+ }
454+
343455 /**
344456 * Gets the right-side attribute for a given index and type.
345457 * This ensures consistent NameId usage across join conditions and rightPreJoinPlan.
346458 */
347459 private FieldAttribute getRightSideAttribute (int index , DataType keyType ) {
348- return switch (index ) {
349- case 0 -> keyType == DataType .KEYWORD ? RKEY0_KEYWORD_ATTR : RKEY0_LONG_ATTR ;
350- case 1 -> keyType == DataType .KEYWORD ? RKEY1_KEYWORD_ATTR : RKEY1_LONG_ATTR ;
351- case 2 -> keyType == DataType .KEYWORD ? RKEY2_KEYWORD_ATTR : RKEY2_LONG_ATTR ;
352- case 3 -> {
353- if (keyType == DataType .INTEGER ) {
354- yield RKEY3_INTEGER_ATTR ;
355- } else if (keyType == DataType .KEYWORD ) {
356- yield RKEY3_KEYWORD_ATTR ;
357- } else {
358- throw new IllegalArgumentException ("Unsupported key type for rkey3: " + keyType );
359- }
360- }
361- default -> throw new IllegalArgumentException ("Unsupported number of keys: " + index );
362- };
460+ String name = "rkey" + index ;
461+ // Handle special case for rkey3 which can be INTEGER or KEYWORD
462+ if (index == 3 && keyType == DataType .INTEGER ) {
463+ return getAttribute (name , DataType .INTEGER );
464+ }
465+ // Default to KEYWORD or LONG based on keyType
466+ if (keyType == DataType .KEYWORD ) {
467+ return getAttribute (name , DataType .KEYWORD );
468+ } else {
469+ return getAttribute (name , DataType .LONG );
470+ }
363471 }
364472
365473 private List <Attribute > buildRightSideAttributes (List <DataType > keyTypes ) {
@@ -491,22 +599,19 @@ private void runLookup(List<DataType> keyTypes, PopulateIndices populateIndices,
491599 TEST_REQUEST_TIMEOUT
492600 );
493601 final String finalNodeWithShard = nodeWithShard ;
494- boolean expressionJoin = EsqlCapabilities .Cap .LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION .isEnabled () ? randomBoolean () : false ;
602+ boolean expressionJoin = true ; // EsqlCapabilities.Cap.LOOKUP_JOIN_ON_BOOLEAN_EXPRESSION.isEnabled() ? randomBoolean() : false;
495603 List <MatchConfig > matchFields = new ArrayList <>();
496604 List <Expression > joinOnConditions = new ArrayList <>();
497605 if (expressionJoin ) {
498606 for (int i = 0 ; i < keyTypes .size (); i ++) {
499- FieldAttribute leftAttr = new FieldAttribute (
500- Source .EMPTY ,
501- "key" + i ,
502- new EsField ("key" + i , keyTypes .get (0 ), Collections .emptyMap (), true , EsField .TimeSeriesFieldType .NONE )
503- );
607+ // Use precreated static attributes to ensure NameId consistency
608+ FieldAttribute leftAttr = getLeftSideAttribute (i , keyTypes .get (i ));
504609 FieldAttribute rightAttr = getRightSideAttribute (i , keyTypes .get (i ));
505610 joinOnConditions .add (new Equals (Source .EMPTY , leftAttr , rightAttr ));
506611 // randomly decide to apply the filter as additional join on filter instead of pushed down filter
507- boolean applyAsJoinOnCondition = EsqlCapabilities .Cap .LOOKUP_JOIN_WITH_FULL_TEXT_FUNCTION .isEnabled ()
508- ? randomBoolean ()
509- : false ;
612+ boolean applyAsJoinOnCondition = true ; // EsqlCapabilities.Cap.LOOKUP_JOIN_WITH_FULL_TEXT_FUNCTION.isEnabled()
613+ // ? randomBoolean()
614+ // : false;
510615 if (applyAsJoinOnCondition
511616 && pushedDownFilter instanceof FragmentExec fragmentExec
512617 && fragmentExec .fragment () instanceof Filter filter ) {
@@ -516,8 +621,10 @@ private void runLookup(List<DataType> keyTypes, PopulateIndices populateIndices,
516621 }
517622 }
518623 // the matchFields are shared for both types of join
624+ // Use precreated static attributes to ensure NameId consistency with join conditions
519625 for (int i = 0 ; i < keyTypes .size (); i ++) {
520- matchFields .add (new MatchConfig ("key" + i , i + 1 , keyTypes .get (i )));
626+ FieldAttribute keyAttr = getLeftSideAttribute (i , keyTypes .get (i ));
627+ matchFields .add (new MatchConfig (keyAttr , i + 1 , keyTypes .get (i )));
521628 }
522629 PhysicalPlan rightPreJoinPlan = pushedDownFilter != null ? pushedDownFilter : buildRightPreJoinPlan (keyTypes , null );
523630 LookupFromIndexOperator .Factory lookup = new LookupFromIndexOperator .Factory (
0 commit comments