Browse Source

Support Fields API in conditional ingest processors (#131581)

eyalkoren 2 months ago
parent
commit
558cc7af60
16 changed files with 639 additions and 364 deletions
  1. 5 0
      docs/changelog/131581.yaml
  2. 147 0
      modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml
  3. 0 3
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt
  4. 22 0
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt
  5. 0 3
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt
  6. 0 3
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt
  7. 0 3
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt
  8. 11 10
      server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java
  9. 24 8
      server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java
  10. 35 0
      server/src/main/java/org/elasticsearch/script/SourceMapFieldScript.java
  11. 214 0
      server/src/main/java/org/elasticsearch/script/field/SourceMapField.java
  12. 5 178
      server/src/main/java/org/elasticsearch/script/field/WriteField.java
  13. 4 4
      server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java
  14. 169 0
      server/src/test/java/org/elasticsearch/script/field/SourceMapFieldTests.java
  15. 0 149
      server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java
  16. 3 3
      test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java

+ 5 - 0
docs/changelog/131581.yaml

@@ -0,0 +1,5 @@
+pr: 131581
+summary: Support Fields API in conditional ingest processors
+area: Infra/Core
+type: enhancement
+issues: []

+ 147 - 0
modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml

@@ -74,3 +74,150 @@ teardown:
   - match: { _source.bytes_source_field: "1kb" }
   - match: { _source.conditional_field: "bar" }
   - is_false: _source.bytes_target_field
+
+---
+"Test conditional processor with fields API":
+  - do:
+      ingest.put_pipeline:
+        id: "my_pipeline"
+        body:
+          description: "_description"
+          processors:
+            - set:
+                if: "field('get.field').get('') == 'one'"
+                field: "one"
+                value: 1
+            - set:
+                if: "field('get.field').get('') == 'two'"
+                field: "missing"
+                value: "missing"
+            - set:
+                if: " /* avoid yaml stash */ $('get.field', 'one') == 'one'"
+                field: "dollar"
+                value: true
+            - set:
+                if: "field('missing.field').get('fallback') == 'fallback'"
+                field: "fallback"
+                value: "fallback"
+            - set:
+                if: "field('nested.array.get.with.index.field').get(1, null) == 'two'"
+                field: "two"
+                value: 2
+            - set:
+                if: "field('getName.field').getName() == 'getName.field'"
+                field: "three"
+                value: 3
+            - set:
+                if: "field('existing.field').exists()"
+                field: "four"
+                value: 4
+            - set:
+                if: "!field('empty.field').isEmpty()"
+                field: "missing"
+                value: "missing"
+            - set:
+                if: "field('size.field').size() == 2"
+                field: "five"
+                value: 5
+            - set:
+                if: >
+                  def iterator = field('iterator.field').iterator();
+                  def sum = 0;
+                  while (iterator.hasNext()) {
+                    sum += iterator.next();
+                  }
+                  return sum == 6;
+                field: "six"
+                value: 6
+            - set:
+                if: "field('hasValue.field').hasValue(v -> v == 'two')"
+                field: "seven"
+                value: 7
+  - match: { acknowledged: true }
+
+  - do:
+      index:
+        index: test
+        id: "1"
+        pipeline: "my_pipeline"
+        body:
+          get.field: "one"
+          nested:
+            array:
+              get.with.index.field: ["one", "two", "three"]
+          getName.field: "my_name"
+          existing.field: "indeed"
+          empty.field: []
+          size.field: ["one", "two"]
+          iterator.field: [1, 2, 3]
+          hasValue.field: ["one", "two", "three"]
+
+  - do:
+      get:
+        index: test
+        id: "1"
+  - match: { _source.get\.field: "one" }
+  - match: { _source.one: 1 }
+  - is_false: _source.missing
+  - is_true: _source.dollar
+  - match: { _source.fallback: "fallback" }
+  - match: { _source.nested.array.get\.with\.index\.field: ["one", "two", "three"] }
+  - match: { _source.two: 2 }
+  - match: { _source.three: 3 }
+  - match: { _source.four: 4 }
+  - match: { _source.five: 5 }
+  - match: { _source.six: 6 }
+  - match: { _source.seven: 7 }
+
+---
+"Test fields iterator is unmodifiable":
+  - do:
+      ingest.put_pipeline:
+        id: "my_pipeline"
+        body:
+          description: "_description"
+          processors:
+            - set:
+                if: >
+                  def iterator = field('iterator.field').iterator();
+                  def sum = 0;
+                    while (iterator.hasNext()) {
+                      sum += iterator.next();
+                      iterator.remove();
+                    }
+                  return sum == 6;
+                field: "sum"
+                value: 6
+  - match: { acknowledged: true }
+
+  - do:
+      index:
+        index: test
+        id: "1"
+        pipeline: "my_pipeline"
+        body:
+          test.field: [1, 2, 3]
+  - match: { error: null }
+
+  - do:
+      index:
+        index: test
+        id: "2"
+        pipeline: "my_pipeline"
+        body:
+          iterator.field: [1, 2, 3]
+      catch: bad_request
+  - length: { error.root_cause: 1 }
+
+  - do:
+      get:
+        index: test
+        id: "1"
+  - match: { _source.test\.field: [1, 2, 3] }
+  - is_false: _source.sum
+
+  - do:
+      get:
+        index: test
+        id: "2"
+      catch: missing

+ 0 - 3
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt

@@ -46,15 +46,12 @@ class org.elasticsearch.script.IngestScript {
 }
 
 class org.elasticsearch.script.field.WriteField {
-    String getName()
     boolean exists()
     WriteField move(def)
     WriteField overwrite(def)
     void remove()
     WriteField set(def)
     WriteField append(def)
-    boolean isEmpty()
-    int size()
     Iterator iterator()
     def get(def)
     def get(int, def)

+ 22 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt

@@ -0,0 +1,22 @@
+#
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ # or more contributor license agreements. Licensed under the "Elastic License
+ # 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ # Public License v 1"; you may not use this file except in compliance with, at
+ # your election, the "Elastic License 2.0", the "GNU Affero General Public
+ # License v3.0 only", or the "Server Side Public License, v 1".
+#
+
+# This file contains a whitelist for conditional ingest scripts
+
+class org.elasticsearch.script.IngestConditionalScript {
+    SourceMapField field(String)
+}
+
+class org.elasticsearch.script.field.SourceMapField {
+    boolean exists()
+    Iterator iterator()
+    def get(def)
+    def get(int, def)
+    boolean hasValue(Predicate)
+}

+ 0 - 3
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt

@@ -42,15 +42,12 @@ class org.elasticsearch.script.ReindexScript {
 }
 
 class org.elasticsearch.script.field.WriteField {
-    String getName()
     boolean exists()
     WriteField move(def)
     WriteField overwrite(def)
     void remove()
     WriteField set(def)
     WriteField append(def)
-    boolean isEmpty()
-    int size()
     Iterator iterator()
     def get(def)
     def get(int, def)

+ 0 - 3
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt

@@ -37,15 +37,12 @@ class org.elasticsearch.script.UpdateScript {
 }
 
 class org.elasticsearch.script.field.WriteField {
-    String getName()
     boolean exists()
     WriteField move(def)
     WriteField overwrite(def)
     void remove()
     WriteField set(def)
     WriteField append(def)
-    boolean isEmpty()
-    int size()
     Iterator iterator()
     def get(def)
     def get(int, def)

+ 0 - 3
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt

@@ -36,15 +36,12 @@ class org.elasticsearch.script.UpdateByQueryScript {
 }
 
 class org.elasticsearch.script.field.WriteField {
-    String getName()
     boolean exists()
     WriteField move(def)
     WriteField overwrite(def)
     void remove()
     WriteField set(def)
     WriteField append(def)
-    boolean isEmpty()
-    int size()
     Iterator iterator()
     def get(def)
     def get(int, def)

+ 11 - 10
server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java

@@ -56,7 +56,7 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP
     private final Processor processor;
     private final IngestMetric metric;
     private final LongSupplier relativeTimeProvider;
-    private final IngestConditionalScript precompiledConditionScript;
+    private final IngestConditionalScript.Factory precompiledConditionalScriptFactory;
 
     ConditionalProcessor(String tag, String description, Script script, ScriptService scriptService, Processor processor) {
         this(tag, description, script, scriptService, processor, System::nanoTime);
@@ -78,12 +78,11 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP
         this.relativeTimeProvider = relativeTimeProvider;
 
         try {
-            final IngestConditionalScript.Factory factory = scriptService.compile(script, IngestConditionalScript.CONTEXT);
             if (ScriptType.INLINE.equals(script.getType())) {
-                precompiledConditionScript = factory.newInstance(script.getParams());
+                precompiledConditionalScriptFactory = scriptService.compile(script, IngestConditionalScript.CONTEXT);
             } else {
                 // stored script, so will have to compile at runtime
-                precompiledConditionScript = null;
+                precompiledConditionalScriptFactory = null;
             }
         } catch (ScriptException e) {
             throw newConfigurationException(TYPE, tag, null, e);
@@ -141,12 +140,14 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP
     }
 
     boolean evaluate(IngestDocument ingestDocument) {
-        IngestConditionalScript script = precompiledConditionScript;
-        if (script == null) {
-            IngestConditionalScript.Factory factory = scriptService.compile(condition, IngestConditionalScript.CONTEXT);
-            script = factory.newInstance(condition.getParams());
-        }
-        return script.execute(new UnmodifiableIngestData(new DynamicMap(ingestDocument.getSourceAndMetadata(), FUNCTIONS)));
+        IngestConditionalScript.Factory factory = precompiledConditionalScriptFactory;
+        if (factory == null) {
+            factory = scriptService.compile(condition, IngestConditionalScript.CONTEXT);
+        }
+        return factory.newInstance(
+            condition.getParams(),
+            new UnmodifiableIngestData(new DynamicMap(ingestDocument.getSourceAndMetadata(), FUNCTIONS))
+        ).execute();
     }
 
     public Processor getInnerProcessor() {

+ 24 - 8
server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java

@@ -16,11 +16,13 @@ import java.util.Map;
 /**
  * A script used by {@link org.elasticsearch.ingest.ConditionalProcessor}.
  */
-public abstract class IngestConditionalScript {
+public abstract class IngestConditionalScript extends SourceMapFieldScript {
 
-    public static final String[] PARAMETERS = { "ctx" };
+    public static final String[] PARAMETERS = {};
 
-    /** The context used to compile {@link IngestConditionalScript} factories. */
+    /**
+     * The context used to compile {@link IngestConditionalScript} factories.
+     * */
     public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>(
         "processor_conditional",
         Factory.class,
@@ -30,21 +32,35 @@ public abstract class IngestConditionalScript {
         true
     );
 
-    /** The generic runtime parameters for the script. */
+    /**
+     * The generic runtime parameters for the script.
+     */
     private final Map<String, Object> params;
 
-    public IngestConditionalScript(Map<String, Object> params) {
+    public IngestConditionalScript(Map<String, Object> params, Map<String, Object> ctxMap) {
+        super(ctxMap);
         this.params = params;
     }
 
-    /** Return the parameters for this script. */
+    /**
+     * Provides backwards compatibility access to ctx
+     * @return the context map containing the source data
+     */
+    public Map<String, Object> getCtx() {
+        return ctxMap;
+    }
+
+    /**
+     * Return the parameters for this script.
+     * @return a map of parameters
+     */
     public Map<String, Object> getParams() {
         return params;
     }
 
-    public abstract boolean execute(Map<String, Object> ctx);
+    public abstract boolean execute();
 
     public interface Factory {
-        IngestConditionalScript newInstance(Map<String, Object> params);
+        IngestConditionalScript newInstance(Map<String, Object> params, Map<String, Object> ctxMap);
     }
 }

+ 35 - 0
server/src/main/java/org/elasticsearch/script/SourceMapFieldScript.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.script.field.SourceMapField;
+
+import java.util.Map;
+
+/**
+ * Abstract base class for that exposes the non-mutable field APIs to scripts.
+ */
+public abstract class SourceMapFieldScript {
+    protected final Map<String, Object> ctxMap;
+
+    public SourceMapFieldScript(Map<String, Object> ctxMap) {
+        this.ctxMap = ctxMap;
+    }
+
+    /**
+     * Expose the {@link SourceMapField field} API
+     *
+     * @param path the path to the field in the source map
+     * @return a new {@link SourceMapField} instance for the specified path
+     */
+    public SourceMapField field(String path) {
+        return new SourceMapField(path, () -> ctxMap);
+    }
+}

+ 214 - 0
server/src/main/java/org/elasticsearch/script/field/SourceMapField.java

@@ -0,0 +1,214 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.script.field;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * Provides an immutable view of a field in a source map.
+ */
+public class SourceMapField implements Field<Object> {
+    protected String path;
+    protected Supplier<Map<String, Object>> rootSupplier;
+
+    protected Map<String, Object> container;
+    protected String leaf;
+
+    protected static final Object MISSING = new Object();
+
+    public SourceMapField(String path, Supplier<Map<String, Object>> rootSupplier) {
+        this.path = path;
+        this.rootSupplier = rootSupplier;
+        resolveDepthFlat();
+    }
+
+    // Path Read
+
+    /**
+     * Get the path represented by this Field
+     */
+    public String getName() {
+        return path;
+    }
+
+    /**
+     * Does the path exist?
+     */
+    public boolean exists() {
+        return leaf != null && container.containsKey(leaf);
+    }
+
+    /**
+     * Is this path associated with any values?
+     */
+    @Override
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    /**
+     * How many elements are at the leaf of this path?
+     */
+    @Override
+    public int size() {
+        if (leaf == null) {
+            return 0;
+        }
+
+        Object value = container.getOrDefault(leaf, MISSING);
+        if (value == MISSING) {
+            return 0;
+        }
+
+        if (value instanceof List<?> list) {
+            return list.size();
+        }
+        return 1;
+    }
+
+    /**
+     * Iterate through all elements of this path with an iterator that cannot mutate the underlying map.
+     */
+    @Override
+    public Iterator<Object> iterator() {
+        if (leaf == null) {
+            return Collections.emptyIterator();
+        }
+
+        Object value = container.getOrDefault(leaf, MISSING);
+        if (value == MISSING) {
+            return Collections.emptyIterator();
+        }
+
+        if (value instanceof List<?> list) {
+            return getListIterator(list);
+        }
+        return Collections.singleton(value).iterator();
+    }
+
+    /**
+     * Get an iterator for the given list that cannot mutate the underlying list. Subclasses can override this method to allow for
+     * mutating iterators.
+     * @param list the list to get an iterator for
+     * @return an iterator that cannot mutate the underlying list
+     */
+    @SuppressWarnings("unchecked")
+    protected Iterator<Object> getListIterator(List<?> list) {
+        return (Iterator<Object>) Collections.unmodifiableList(list).iterator();
+    }
+
+    /**
+     * Get the value at this path, if there is no value then get the provided {@param defaultValue}
+     */
+    public Object get(Object defaultValue) {
+        if (leaf == null) {
+            return defaultValue;
+        }
+
+        return container.getOrDefault(leaf, defaultValue);
+    }
+
+    /**
+     * Get the value at the given index at this path or {@param defaultValue} if there is no such value.
+     */
+    public Object get(int index, Object defaultValue) {
+        if (leaf == null) {
+            return defaultValue;
+        }
+
+        Object value = container.getOrDefault(leaf, MISSING);
+        if (value instanceof List<?> list) {
+            if (index < list.size()) {
+                return list.get(index);
+            }
+        } else if (value != MISSING && index == 0) {
+            return value;
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Is there any value matching {@param predicate} at this path?
+     */
+    public boolean hasValue(Predicate<Object> predicate) {
+        if (leaf == null) {
+            return false;
+        }
+
+        Object value = container.getOrDefault(leaf, MISSING);
+        if (value == MISSING) {
+            return false;
+        }
+
+        if (value instanceof List<?> list) {
+            return list.stream().anyMatch(predicate);
+        }
+
+        return predicate.test(value);
+    }
+
+    /**
+     * Change the path and clear the existing resolution by setting {@link #leaf} and {@link #container} to null.
+     * Caller needs to re-resolve after this call.
+     */
+    protected void setPath(String path) {
+        this.path = path;
+        this.leaf = null;
+        this.container = null;
+    }
+
+    /**
+     * Resolve {@link #path} from the root.
+     *
+     * Tries to resolve the path one segment at a time, if the segment is not mapped to a Java Map, then
+     * treats that segment and the rest as the leaf if it resolves.
+     *
+     * a.b.c could be resolved as
+     *  I)   ['a']['b']['c'] if 'a' is a Map at the root and 'b' is a Map in 'a', 'c' need not exist in 'b'.
+     *  II)  ['a']['b.c'] if 'a' is a Map at the root and 'b' does not exist in 'a's Map but 'b.c' does.
+     *  III) ['a.b.c'] if 'a' doesn't exist at the root but 'a.b.c' does.
+     *
+     * {@link #container} and {@link #leaf} and non-null if resolved.
+     */
+    @SuppressWarnings("unchecked")
+    protected final void resolveDepthFlat() {
+        container = rootSupplier.get();
+
+        int index = path.indexOf('.');
+        int lastIndex = 0;
+        String segment;
+
+        while (index != -1) {
+            segment = path.substring(lastIndex, index);
+            Object value = container.get(segment);
+            if (value instanceof Map<?, ?> map) {
+                container = (Map<String, Object>) map;
+                lastIndex = index + 1;
+                index = path.indexOf('.', lastIndex);
+            } else {
+                // Check rest of segments as a single key
+                String rest = path.substring(lastIndex);
+                if (container.containsKey(rest)) {
+                    leaf = rest;
+                } else {
+                    leaf = null;
+                }
+                return;
+            }
+        }
+        leaf = path.substring(lastIndex);
+    }
+}

+ 5 - 178
server/src/main/java/org/elasticsearch/script/field/WriteField.java

@@ -24,35 +24,10 @@ import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
-public final class WriteField implements Field<Object> {
-    private String path;
-    private Supplier<Map<String, Object>> rootSupplier;
-
-    private Map<String, Object> container;
-    private String leaf;
-
-    private static final Object MISSING = new Object();
+public final class WriteField extends SourceMapField {
 
     public WriteField(String path, Supplier<Map<String, Object>> rootSupplier) {
-        this.path = path;
-        this.rootSupplier = rootSupplier;
-        resolveDepthFlat();
-    }
-
-    // Path Read
-
-    /**
-     * Get the path represented by this Field
-     */
-    public String getName() {
-        return path;
-    }
-
-    /**
-     * Does the path exist?
-     */
-    public boolean exists() {
-        return leaf != null && container.containsKey(leaf);
+        super(path, rootSupplier);
     }
 
     // Path Update
@@ -225,106 +200,10 @@ public final class WriteField implements Field<Object> {
         return this;
     }
 
-    // Value Read
-
-    /**
-     * Is this path associated with any values?
-     */
-    @Override
-    public boolean isEmpty() {
-        return size() == 0;
-    }
-
-    /**
-     * How many elements are at the leaf of this path?
-     */
-    @Override
-    public int size() {
-        if (leaf == null) {
-            return 0;
-        }
-
-        Object value = container.getOrDefault(leaf, MISSING);
-        if (value == MISSING) {
-            return 0;
-        }
-
-        if (value instanceof List<?> list) {
-            return list.size();
-        }
-        return 1;
-    }
-
-    /**
-     * Iterate through all elements of this path
-     */
-    @Override
     @SuppressWarnings("unchecked")
-    public Iterator<Object> iterator() {
-        if (leaf == null) {
-            return Collections.emptyIterator();
-        }
-
-        Object value = container.getOrDefault(leaf, MISSING);
-        if (value == MISSING) {
-            return Collections.emptyIterator();
-        }
-
-        if (value instanceof List<?> list) {
-            return (Iterator<Object>) list.iterator();
-        }
-        return Collections.singleton(value).iterator();
-    }
-
-    /**
-     * Get the value at this path, if there is no value then get the provided {@param defaultValue}
-     */
-    public Object get(Object defaultValue) {
-        if (leaf == null) {
-            return defaultValue;
-        }
-
-        return container.getOrDefault(leaf, defaultValue);
-    }
-
-    /**
-     * Get the value at the given index at this path or {@param defaultValue} if there is no such value.
-     */
-    public Object get(int index, Object defaultValue) {
-        if (leaf == null) {
-            return defaultValue;
-        }
-
-        Object value = container.getOrDefault(leaf, MISSING);
-        if (value instanceof List<?> list) {
-            if (index < list.size()) {
-                return list.get(index);
-            }
-        } else if (value != MISSING && index == 0) {
-            return value;
-        }
-
-        return defaultValue;
-    }
-
-    /**
-     * Is there any value matching {@param predicate} at this path?
-     */
-    public boolean hasValue(Predicate<Object> predicate) {
-        if (leaf == null) {
-            return false;
-        }
-
-        Object value = container.getOrDefault(leaf, MISSING);
-        if (value == MISSING) {
-            return false;
-        }
-
-        if (value instanceof List<?> list) {
-            return list.stream().anyMatch(predicate);
-        }
-
-        return predicate.test(value);
+    @Override
+    protected Iterator<Object> getListIterator(List<?> list) {
+        return (Iterator<Object>) list.iterator();
     }
 
     // Value Update
@@ -600,16 +479,6 @@ public final class WriteField implements Field<Object> {
         throw new IllegalStateException("Unexpected value [" + value + "] of type [" + typeName(value) + "] at [" + path + "] for docs()");
     }
 
-    /**
-     * Change the path and clear the existing resolution by setting {@link #leaf} and {@link #container} to null.
-     * Caller needs to re-resolve after this call.
-     */
-    private void setPath(String path) {
-        this.path = path;
-        this.leaf = null;
-        this.container = null;
-    }
-
     /**
      * Get the path to a leaf or create it if one does not exist.
      */
@@ -622,48 +491,6 @@ public final class WriteField implements Field<Object> {
         }
     }
 
-    /**
-     * Resolve {@link #path} from the root.
-     *
-     * Tries to resolve the path one segment at a time, if the segment is not mapped to a Java Map, then
-     * treats that segment and the rest as the leaf if it resolves.
-     *
-     * a.b.c could be resolved as
-     *  I)   ['a']['b']['c'] if 'a' is a Map at the root and 'b' is a Map in 'a', 'c' need not exist in 'b'.
-     *  II)  ['a']['b.c'] if 'a' is a Map at the root and 'b' does not exist in 'a's Map but 'b.c' does.
-     *  III) ['a.b.c'] if 'a' doesn't exist at the root but 'a.b.c' does.
-     *
-     * {@link #container} and {@link #leaf} and non-null if resolved.
-     */
-    @SuppressWarnings("unchecked")
-    private void resolveDepthFlat() {
-        container = rootSupplier.get();
-
-        int index = path.indexOf('.');
-        int lastIndex = 0;
-        String segment;
-
-        while (index != -1) {
-            segment = path.substring(lastIndex, index);
-            Object value = container.get(segment);
-            if (value instanceof Map<?, ?> map) {
-                container = (Map<String, Object>) map;
-                lastIndex = index + 1;
-                index = path.indexOf('.', lastIndex);
-            } else {
-                // Check rest of segments as a single key
-                String rest = path.substring(lastIndex);
-                if (container.containsKey(rest)) {
-                    leaf = rest;
-                } else {
-                    leaf = null;
-                }
-                return;
-            }
-        }
-        leaf = path.substring(lastIndex);
-    }
-
     /**
      * Create a new Map for each segment in path, if that segment is unmapped or mapped to null.
      *

+ 4 - 4
server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java

@@ -206,9 +206,9 @@ public class ConditionalProcessorTests extends ESTestCase {
             if (fail.get()) {
                 throw new ScriptException("bad script", new ParseException("error", 0), List.of(), "", "lang", null);
             } else {
-                return params -> new IngestConditionalScript(params) {
+                return (params, ctxMap) -> new IngestConditionalScript(params, ctxMap) {
                     @Override
-                    public boolean execute(Map<String, Object> ctx) {
+                    public boolean execute() {
                         return false;
                     }
                 };
@@ -226,9 +226,9 @@ public class ConditionalProcessorTests extends ESTestCase {
     public void testRuntimeError() {
         ScriptService scriptService = MockScriptService.singleContext(
             IngestConditionalScript.CONTEXT,
-            code -> params -> new IngestConditionalScript(params) {
+            code -> (params, ctxMapWrapper) -> new IngestConditionalScript(params, ctxMapWrapper) {
                 @Override
-                public boolean execute(Map<String, Object> ctx) {
+                public boolean execute() {
                     throw new IllegalArgumentException("runtime problem");
                 }
             },

+ 169 - 0
server/src/test/java/org/elasticsearch/script/field/SourceMapFieldTests.java

@@ -0,0 +1,169 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.script.field;
+
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class SourceMapFieldTests extends ESTestCase {
+
+    public void testResolveDepthFlat() {
+        Map<String, Object> map = new HashMap<>();
+        map.put("abc.d.ef", "flat");
+
+        Map<String, Object> abc = new HashMap<>();
+        map.put("abc", abc);
+        abc.put("d.ef", "mixed");
+
+        Map<String, Object> d = new HashMap<>();
+        abc.put("d", d);
+        d.put("ef", "nested");
+
+        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { "ef": "nested" } } }
+        SourceMapField field = new SourceMapField("abc.d.ef", () -> map);
+        assertTrue(field.exists());
+
+        assertEquals("nested", field.get("missing"));
+        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { } } }
+        d.remove("ef");
+        assertEquals("missing", field.get("missing"));
+        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed" }
+        // TODO(stu): this should be inaccessible
+        abc.remove("d");
+        assertEquals("missing", field.get("missing"));
+
+        // resolution at construction time
+        field = new SourceMapField("abc.d.ef", () -> map);
+        assertEquals("mixed", field.get("missing"));
+        abc.remove("d.ef");
+        assertEquals("missing", field.get("missing"));
+
+        field = new SourceMapField("abc.d.ef", () -> map);
+        // abc is still there
+        assertEquals("missing", field.get("missing"));
+        map.remove("abc");
+        assertEquals("missing", field.get("missing"));
+
+        field = new SourceMapField("abc.d.ef", () -> map);
+        assertEquals("flat", field.get("missing"));
+    }
+
+    public void testExists() {
+        Map<String, Object> a = new HashMap<>();
+        a.put("b.c", null);
+        assertTrue(new SourceMapField("a.b.c", () -> Map.of("a", a)).exists());
+
+        a.clear();
+        Map<String, Object> level1 = new HashMap<>();
+        level1.put("null", null);
+        a.put("level1", level1);
+        a.put("null", null);
+        // SourceMapField.leaf is null
+        assertFalse(new SourceMapField("missing.leaf", () -> a).exists());
+
+        // SourceMapField.leaf non-null but missing
+        assertFalse(new SourceMapField("missing", () -> a).exists());
+
+        // Check mappings with null values exist
+        assertTrue(new SourceMapField("level1.null", () -> a).exists());
+        assertTrue(new SourceMapField("null", () -> a).exists());
+    }
+
+    public void testSizeIsEmpty() {
+        Map<String, Object> root = new HashMap<>();
+        SourceMapField field = new SourceMapField("a.b.c", () -> root);
+        assertTrue(field.isEmpty());
+        assertEquals(0, field.size());
+
+        root.put("a.b.c", List.of(1, 2));
+        field = new SourceMapField("a.b.c", () -> root);
+        assertFalse(field.isEmpty());
+        assertEquals(2, field.size());
+
+        Map<String, Object> d = new HashMap<>();
+        root.put("d", d);
+        field = new SourceMapField("d.e", () -> root);
+        assertTrue(field.isEmpty());
+        assertEquals(0, field.size());
+        d.put("e", "foo");
+        assertFalse(field.isEmpty());
+        assertEquals(1, field.size());
+    }
+
+    public void testIterator() {
+        Map<String, Object> root = new HashMap<>();
+        Map<String, Object> a = new HashMap<>();
+        Map<String, Object> b = new HashMap<>();
+        a.put("b", b);
+        root.put("a", a);
+
+        SourceMapField field = new SourceMapField("a.b.c", () -> root);
+        assertFalse(field.iterator().hasNext());
+
+        b.put("c", "value");
+        Iterator<Object> it = field.iterator();
+        assertTrue(it.hasNext());
+        assertEquals("value", it.next());
+        assertFalse(it.hasNext());
+
+        b.put("c", List.of(1, 2, 3));
+        it = field.iterator();
+        assertTrue(it.hasNext());
+        assertEquals(1, it.next());
+        assertTrue(it.hasNext());
+        assertEquals(2, it.next());
+        assertTrue(it.hasNext());
+        assertEquals(3, it.next());
+        assertFalse(it.hasNext());
+
+        assertFalse(new SourceMapField("dne.dne", () -> root).iterator().hasNext());
+    }
+
+    @SuppressWarnings("unchecked")
+    public void testHasValue() {
+        Map<String, Object> root = new HashMap<>();
+        Map<String, Object> a = new HashMap<>();
+        Map<String, Object> b = new HashMap<>();
+        a.put("b", b);
+        root.put("a", a);
+        b.put("c", new ArrayList<>(List.of(10, 11, 12)));
+        SourceMapField field = new SourceMapField("a.b.c", () -> root);
+        assertFalse(field.hasValue(v -> (Integer) v < 10));
+        assertTrue(field.hasValue(v -> (Integer) v <= 10));
+
+        root.clear();
+        a.clear();
+        a.put("null", null);
+        a.put("b", List.of(1, 2, 3, 4));
+        root.put("a", a);
+        field = new SourceMapField("a.b", () -> root);
+        assertTrue(field.hasValue(x -> (Integer) x % 2 == 0));
+        assertFalse(field.hasValue(x -> (Integer) x > 4));
+        assertFalse(new SourceMapField("d.e", () -> root).hasValue(Objects::isNull));
+        assertTrue(new SourceMapField("a.null", () -> root).hasValue(Objects::isNull));
+        assertFalse(new SourceMapField("a.null2", () -> root).hasValue(Objects::isNull));
+    }
+
+    public void testGetIndex() {
+        Map<String, Object> root = new HashMap<>();
+        root.put("a", Map.of("b", List.of(1, 2, 3, 5), "c", "foo"));
+        SourceMapField field = new SourceMapField("a.b", () -> root);
+        assertEquals(5, field.get(3, 100));
+        assertEquals(100, new SourceMapField("c.d", () -> root).get(3, 100));
+        assertEquals("bar", new SourceMapField("a.c", () -> root).get(1, "bar"));
+        assertEquals("foo", new SourceMapField("a.c", () -> root).get(0, "bar"));
+    }
+}

+ 0 - 149
server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java

@@ -19,7 +19,6 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
-import java.util.Objects;
 
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.equalTo;
@@ -29,68 +28,6 @@ import static org.hamcrest.Matchers.is;
 
 public class WriteFieldTests extends ESTestCase {
 
-    public void testResolveDepthFlat() {
-        Map<String, Object> map = new HashMap<>();
-        map.put("abc.d.ef", "flat");
-
-        Map<String, Object> abc = new HashMap<>();
-        map.put("abc", abc);
-        abc.put("d.ef", "mixed");
-
-        Map<String, Object> d = new HashMap<>();
-        abc.put("d", d);
-        d.put("ef", "nested");
-
-        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { "ef": "nested" } } }
-        WriteField wf = new WriteField("abc.d.ef", () -> map);
-        assertTrue(wf.exists());
-
-        assertEquals("nested", wf.get("missing"));
-        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { } } }
-        d.remove("ef");
-        assertEquals("missing", wf.get("missing"));
-        // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed" }
-        // TODO(stu): this should be inaccessible
-        abc.remove("d");
-        assertEquals("missing", wf.get("missing"));
-
-        // resolution at construction time
-        wf = new WriteField("abc.d.ef", () -> map);
-        assertEquals("mixed", wf.get("missing"));
-        abc.remove("d.ef");
-        assertEquals("missing", wf.get("missing"));
-
-        wf = new WriteField("abc.d.ef", () -> map);
-        // abc is still there
-        assertEquals("missing", wf.get("missing"));
-        map.remove("abc");
-        assertEquals("missing", wf.get("missing"));
-
-        wf = new WriteField("abc.d.ef", () -> map);
-        assertEquals("flat", wf.get("missing"));
-    }
-
-    public void testExists() {
-        Map<String, Object> a = new HashMap<>();
-        a.put("b.c", null);
-        assertTrue(new WriteField("a.b.c", () -> Map.of("a", a)).exists());
-
-        a.clear();
-        Map<String, Object> level1 = new HashMap<>();
-        level1.put("null", null);
-        a.put("level1", level1);
-        a.put("null", null);
-        // WriteField.leaf is null
-        assertFalse(new WriteField("missing.leaf", () -> a).exists());
-
-        // WriteField.leaf non-null but missing
-        assertFalse(new WriteField("missing", () -> a).exists());
-
-        // Check mappings with null values exist
-        assertTrue(new WriteField("level1.null", () -> a).exists());
-        assertTrue(new WriteField("null", () -> a).exists());
-    }
-
     public void testMoveString() {
         String src = "a.b.c";
         String dst = "d.e.f";
@@ -397,56 +334,6 @@ public class WriteFieldTests extends ESTestCase {
         assertEquals(new ArrayList<>(List.of("bar")), b.get("c"));
     }
 
-    public void testSizeIsEmpty() {
-        Map<String, Object> root = new HashMap<>();
-        WriteField wf = new WriteField("a.b.c", () -> root);
-        assertTrue(wf.isEmpty());
-        assertEquals(0, wf.size());
-
-        root.put("a.b.c", List.of(1, 2));
-        wf = new WriteField("a.b.c", () -> root);
-        assertFalse(wf.isEmpty());
-        assertEquals(2, wf.size());
-
-        Map<String, Object> d = new HashMap<>();
-        root.put("d", d);
-        wf = new WriteField("d.e", () -> root);
-        assertTrue(wf.isEmpty());
-        assertEquals(0, wf.size());
-        d.put("e", "foo");
-        assertFalse(wf.isEmpty());
-        assertEquals(1, wf.size());
-    }
-
-    public void testIterator() {
-        Map<String, Object> root = new HashMap<>();
-        Map<String, Object> a = new HashMap<>();
-        Map<String, Object> b = new HashMap<>();
-        a.put("b", b);
-        root.put("a", a);
-
-        WriteField wf = new WriteField("a.b.c", () -> root);
-        assertFalse(wf.iterator().hasNext());
-
-        b.put("c", "value");
-        Iterator<Object> it = wf.iterator();
-        assertTrue(it.hasNext());
-        assertEquals("value", it.next());
-        assertFalse(it.hasNext());
-
-        b.put("c", List.of(1, 2, 3));
-        it = wf.iterator();
-        assertTrue(it.hasNext());
-        assertEquals(1, it.next());
-        assertTrue(it.hasNext());
-        assertEquals(2, it.next());
-        assertTrue(it.hasNext());
-        assertEquals(3, it.next());
-        assertFalse(it.hasNext());
-
-        assertFalse(new WriteField("dne.dne", () -> root).iterator().hasNext());
-    }
-
     @SuppressWarnings("unchecked")
     public void testDeduplicate() {
         Map<String, Object> root = new HashMap<>();
@@ -529,42 +416,6 @@ public class WriteFieldTests extends ESTestCase {
         assertNull(wf.get(null));
     }
 
-    public void testHasValue() {
-        Map<String, Object> root = new HashMap<>();
-        Map<String, Object> a = new HashMap<>();
-        Map<String, Object> b = new HashMap<>();
-        a.put("b", b);
-        root.put("a", a);
-        b.put("c", new ArrayList<>(List.of(10, 11, 12)));
-        WriteField wf = new WriteField("a.b.c", () -> root);
-        assertFalse(wf.hasValue(v -> (Integer) v < 10));
-        assertTrue(wf.hasValue(v -> (Integer) v <= 10));
-        wf.append(9);
-        assertTrue(wf.hasValue(v -> (Integer) v < 10));
-
-        root.clear();
-        a.clear();
-        a.put("null", null);
-        a.put("b", List.of(1, 2, 3, 4));
-        root.put("a", a);
-        wf = new WriteField("a.b", () -> root);
-        assertTrue(wf.hasValue(x -> (Integer) x % 2 == 0));
-        assertFalse(wf.hasValue(x -> (Integer) x > 4));
-        assertFalse(new WriteField("d.e", () -> root).hasValue(Objects::isNull));
-        assertTrue(new WriteField("a.null", () -> root).hasValue(Objects::isNull));
-        assertFalse(new WriteField("a.null2", () -> root).hasValue(Objects::isNull));
-    }
-
-    public void testGetIndex() {
-        Map<String, Object> root = new HashMap<>();
-        root.put("a", Map.of("b", List.of(1, 2, 3, 5), "c", "foo"));
-        WriteField wf = new WriteField("a.b", () -> root);
-        assertEquals(5, wf.get(3, 100));
-        assertEquals(100, new WriteField("c.d", () -> root).get(3, 100));
-        assertEquals("bar", new WriteField("a.c", () -> root).get(1, "bar"));
-        assertEquals("foo", new WriteField("a.c", () -> root).get(0, "bar"));
-    }
-
     @SuppressWarnings("unchecked")
     public void testDoc() {
         Map<String, Object> root = new HashMap<>();

+ 3 - 3
test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java

@@ -169,10 +169,10 @@ public class MockScriptEngine implements ScriptEngine {
         } else if (context.instanceClazz.equals(AggregationScript.class)) {
             return context.factoryClazz.cast(new MockAggregationScript(script));
         } else if (context.instanceClazz.equals(IngestConditionalScript.class)) {
-            IngestConditionalScript.Factory factory = parameters -> new IngestConditionalScript(parameters) {
+            IngestConditionalScript.Factory factory = (parameters, ctxMap) -> new IngestConditionalScript(parameters, ctxMap) {
                 @Override
-                public boolean execute(Map<String, Object> ctx) {
-                    return (boolean) script.apply(ctx);
+                public boolean execute() {
+                    return (boolean) script.apply(ctxMap);
                 }
             };
             return context.factoryClazz.cast(factory);