2323import org .elasticsearch .compute .lucene .DataPartitioning ;
2424import org .elasticsearch .compute .lucene .LuceneSourceOperator ;
2525import org .elasticsearch .compute .lucene .LuceneTopNSourceOperator ;
26+ import org .elasticsearch .compute .lucene .ValuesSourceReaderOperator ;
2627import org .elasticsearch .compute .operator .SourceOperator ;
2728import org .elasticsearch .compute .test .TestBlockFactory ;
2829import org .elasticsearch .core .IOUtils ;
2930import org .elasticsearch .core .Releasable ;
3031import org .elasticsearch .core .Releasables ;
3132import org .elasticsearch .index .IndexMode ;
3233import org .elasticsearch .index .cache .query .TrivialQueryCachingPolicy ;
34+ import org .elasticsearch .index .mapper .BlockLoader ;
35+ import org .elasticsearch .index .mapper .BlockSourceReader ;
36+ import org .elasticsearch .index .mapper .FallbackSyntheticSourceBlockLoader ;
37+ import org .elasticsearch .index .mapper .MappedFieldType ;
3338import org .elasticsearch .index .mapper .MapperServiceTestCase ;
3439import org .elasticsearch .node .Node ;
3540import org .elasticsearch .plugins .ExtensiblePlugin ;
4247import org .elasticsearch .xpack .esql .core .tree .Source ;
4348import org .elasticsearch .xpack .esql .core .type .DataType ;
4449import org .elasticsearch .xpack .esql .core .type .EsField ;
50+ import org .elasticsearch .xpack .esql .core .type .PotentiallyUnmappedKeywordEsField ;
4551import org .elasticsearch .xpack .esql .core .util .StringUtils ;
4652import org .elasticsearch .xpack .esql .expression .Order ;
4753import org .elasticsearch .xpack .esql .index .EsIndex ;
4854import org .elasticsearch .xpack .esql .plan .physical .EsQueryExec ;
55+ import org .elasticsearch .xpack .esql .plan .physical .FieldExtractExec ;
4956import org .elasticsearch .xpack .esql .plan .physical .LimitExec ;
5057import org .elasticsearch .xpack .esql .plan .physical .ParallelExec ;
5158import org .elasticsearch .xpack .esql .plugin .EsqlPlugin ;
6471
6572import static org .hamcrest .Matchers .equalTo ;
6673import static org .hamcrest .Matchers .hasSize ;
74+ import static org .hamcrest .Matchers .instanceOf ;
6775import static org .hamcrest .Matchers .lessThanOrEqualTo ;
6876
6977public class LocalExecutionPlannerTests extends MapperServiceTestCase {
@@ -84,10 +92,17 @@ public static Iterable<Object[]> parameters() throws Exception {
8492
8593 private final ArrayList <Releasable > releasables = new ArrayList <>();
8694
95+ private Settings settings = SETTINGS ;
96+
8797 public LocalExecutionPlannerTests (@ Name ("estimatedRowSizeIsHuge" ) boolean estimatedRowSizeIsHuge ) {
8898 this .estimatedRowSizeIsHuge = estimatedRowSizeIsHuge ;
8999 }
90100
101+ @ Override
102+ protected Settings getIndexSettings () {
103+ return settings ;
104+ }
105+
91106 @ Override
92107 protected Collection <Plugin > getPlugins () {
93108 var plugin = new SpatialPlugin ();
@@ -229,6 +244,47 @@ public void testParallel() throws Exception {
229244 assertThat (plan .driverFactories , hasSize (2 ));
230245 }
231246
247+ public void testPlanUnmappedFieldExtractStoredSource () throws Exception {
248+ var blockLoader = constructBlockLoader ();
249+ // In case of stored source we expect bytes based block source loader (this loads source from _source)
250+ assertThat (blockLoader , instanceOf (BlockSourceReader .BytesRefsBlockLoader .class ));
251+ }
252+
253+ public void testPlanUnmappedFieldExtractSyntheticSource () throws Exception {
254+ // Enables synthetic source, so that fallback synthetic source blocker loader is used:
255+ settings = Settings .builder ().put (settings ).put ("index.mapping.source.mode" , "synthetic" ).build ();
256+
257+ var blockLoader = constructBlockLoader ();
258+ // In case of synthetic source we expect bytes based block source loader (this loads source from _ignored_source)
259+ assertThat (blockLoader , instanceOf (FallbackSyntheticSourceBlockLoader .class ));
260+ }
261+
262+ private BlockLoader constructBlockLoader () throws IOException {
263+ EsQueryExec queryExec = new EsQueryExec (
264+ Source .EMPTY ,
265+ index ().name (),
266+ IndexMode .STANDARD ,
267+ index ().indexNameWithModes (),
268+ List .of (new FieldAttribute (Source .EMPTY , EsQueryExec .DOC_ID_FIELD .getName (), EsQueryExec .DOC_ID_FIELD )),
269+ null ,
270+ null ,
271+ null ,
272+ between (1 , 1000 )
273+ );
274+ FieldExtractExec fieldExtractExec = new FieldExtractExec (
275+ Source .EMPTY ,
276+ queryExec ,
277+ List .of (
278+ new FieldAttribute (Source .EMPTY , "potentially_unmapped" , new PotentiallyUnmappedKeywordEsField ("potentially_unmapped" ))
279+ ),
280+ MappedFieldType .FieldExtractPreference .NONE
281+ );
282+ LocalExecutionPlanner .LocalExecutionPlan plan = planner ().plan ("test" , FoldContext .small (), fieldExtractExec );
283+ var p = plan .driverFactories .get (0 ).driverSupplier ().physicalOperation ();
284+ var fieldInfo = ((ValuesSourceReaderOperator .Factory ) p .intermediateOperatorFactories .get (0 )).fields ().get (0 );
285+ return fieldInfo .blockLoader ().apply (0 );
286+ }
287+
232288 private int randomEstimatedRowSize (boolean huge ) {
233289 int hugeBoundary = SourceOperator .MIN_TARGET_PAGE_SIZE * 10 ;
234290 return huge ? between (hugeBoundary , Integer .MAX_VALUE ) : between (1 , hugeBoundary );
0 commit comments