Procházet zdrojové kódy

Script: Write Field API path manipulation (#89889)

Adds `WriteField` path manipulation APIs:
 * `WriteField` `move(String)`:  Moves this path to the destination path.  Throws an error if destination path exists.
 * `WriteField` `overwrite(String)`: Same as `move(String)` but overwrites the destination path if it exists;
 * `void` `remove()`: Remove the leaf value from this path.

See also: #89738
Refs: #79155
Stuart Tettemer před 3 roky
rodič
revize
a1082a6986

+ 5 - 0
docs/changelog/89889.yaml

@@ -0,0 +1,5 @@
+pr: 89889
+summary: "Script: Write Field API path manipulation"
+area: Infra/Scripting
+type: enhancement
+issues: []

+ 24 - 4
modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/190_script_processor.yml

@@ -396,6 +396,12 @@ teardown:
                   "lang": "painless",
                   "source" : "field('numbers').removeValuesIf(x -> x == null).transform(x -> x instanceof String ? Integer.parseInt(x) : x);"
                 }
+              },
+              {
+                "script" : {
+                  "lang": "painless",
+                  "source" : "field('src.key1').move('dst.key5'); field('src.key2').overwrite('dst.key3'); field('dst.key4').remove()"
+                }
               }
             ]
           }
@@ -427,13 +433,27 @@ teardown:
           append: ["one", "again", "another", "again"]
           duplicate: ["aa", "aa", "bb"]
           numbers: [1, null, "2"]
+          src:
+            key1: value1
+            key2: value2
+          dst:
+            key3: value3
+            key4: value4
 
   - do:
       get:
         index: test
         id: "1"
 
-  - match: { _source.create.depth: "works" }
-  - match: { _source.size: 527 }
-  - match: { _source.duplicate: ["aa", "bb"] }
-  - match: { _source.numbers: [1, 2] }
+  - match:
+      _source:
+        create:
+          depth: "works"
+        append: ["one", "again", "another", "again", "another"]
+        size: 527
+        duplicate: ["aa", "bb"]
+        numbers: [1, 2]
+        src: {}
+        dst:
+          key3: value2
+          key5: value1

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

@@ -46,8 +46,8 @@ class org.elasticsearch.script.IngestScript {
 class org.elasticsearch.script.field.WriteField {
     String getName()
     boolean exists()
-    void move(String)
-    void overwrite(String)
+    WriteField move(String)
+    WriteField overwrite(String)
     void remove()
     WriteField set(def)
     WriteField append(def)

+ 36 - 10
server/src/main/java/org/elasticsearch/script/field/WriteField.java

@@ -58,30 +58,46 @@ public class WriteField implements Field<Object> {
      * Move this path to another path in the map.
      *
      * @throws IllegalArgumentException if the other path has contents
-     * @throws UnsupportedOperationException
      */
-    public void move(String path) {
-        throw new UnsupportedOperationException("unimplemented");
+    public WriteField move(String path) {
+        WriteField dest = new WriteField(path, rootSupplier);
+        if (dest.isEmpty() == false) {
+            throw new IllegalArgumentException("Cannot move to non-empty destination [" + path + "]");
+        }
+        return overwrite(path);
     }
 
     /**
-     * Move this path to another path in the map, overwriting the destination path if it exists
+     * Move this path to another path in the map, overwriting the destination path if it exists.
      *
-     * @throws UnsupportedOperationException
+     * If this Field has no value, the value at {@param path} is removed.
      */
-    public void overwrite(String path) {
-        throw new UnsupportedOperationException("unimplemented");
+    public WriteField overwrite(String path) {
+        Object value = get(MISSING);
+        remove();
+        setPath(path);
+        if (value == MISSING) {
+            // The source has a missing value, remove the value, if it exists, at the destination
+            // to match the missing value at the source.
+            remove();
+        } else {
+            setLeaf();
+            set(value);
+        }
+        return this;
     }
 
     // Path Delete
 
     /**
      * Removes this path from the map.
-     *
-     * @throws UnsupportedOperationException
      */
     public void remove() {
-        throw new UnsupportedOperationException("unimplemented");
+        resolveDepthFlat();
+        if (leaf == null) {
+            return;
+        }
+        container.remove(leaf);
     }
 
     // Value Create
@@ -319,6 +335,16 @@ public class WriteField implements Field<Object> {
         return this;
     }
 
+    /**
+     * 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;
+    }
+
     /**
      * Get the path to a leaf or create it if one does not exist.
      */

+ 101 - 11
server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java

@@ -19,6 +19,7 @@ import java.util.Map;
 import java.util.Objects;
 
 import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.equalTo;
 
 public class WriteFieldTests extends ESTestCase {
 
@@ -85,27 +86,83 @@ public class WriteFieldTests extends ESTestCase {
     }
 
     public void testMove() {
+        String src = "a.b.c";
+        String dst = "d.e.f";
         Map<String, Object> root = new HashMap<>();
-        root.put("a.b.c", "foo");
-        WriteField wf = new WriteField("a.b.c", () -> root);
-        UnsupportedOperationException err = expectThrows(UnsupportedOperationException.class, () -> wf.move("b.c.d"));
-        assertEquals("unimplemented", err.getMessage());
+        MapOfMaps branches = addPath(root, src, "src");
+        branches.putAll(addPath(root, dst, "dst"));
+
+        // All of dst exists, expect failure
+        WriteField wf = new WriteField(src, () -> root);
+        assertEquals("dst", new WriteField(dst, () -> root).get("dne"));
+        IllegalArgumentException err = expectThrows(IllegalArgumentException.class, () -> wf.move(dst));
+        assertEquals("Cannot move to non-empty destination [" + dst + "]", err.getMessage());
+
+        // All of dst other than leaf exists
+        root.clear();
+        branches = addPath(root, src, "src");
+        branches.putAll(addPath(root, dst, "dst"));
+        // dst missing value
+        branches.get("e").remove("f");
+        WriteField wf2 = new WriteField(src, () -> root);
+        wf2.move(dst);
+        assertEquals("src", wf2.get("dne"));
+        assertEquals("src", new WriteField(dst, () -> root).get("dne"));
+        assertFalse(branches.get("b").containsKey("c"));
+
+        // Construct all of dst
+        root.clear();
+        branches = addPath(root, src, "src");
+        WriteField wf3 = new WriteField(src, () -> root);
+        wf3.move(dst);
+        assertEquals("src", wf3.get("dne"));
+        assertEquals("src", new WriteField(dst, () -> root).get("dne"));
+        assertFalse(branches.get("b").containsKey("c"));
     }
 
     public void testOverwrite() {
+        String src = "a.b.c";
+        String dst = "d.e.f";
         Map<String, Object> root = new HashMap<>();
-        root.put("a.b.c", "foo");
-        WriteField wf = new WriteField("a.b.c", () -> root);
-        UnsupportedOperationException err = expectThrows(UnsupportedOperationException.class, () -> wf.overwrite("b.c.d"));
-        assertEquals("unimplemented", err.getMessage());
+        MapOfMaps branches = addPath(root, src, "src");
+        branches.putAll(addPath(root, dst, "dst"));
+
+        WriteField wf = new WriteField(src, () -> root);
+        assertEquals("dst", new WriteField(dst, () -> root).get("dne"));
+        wf.overwrite(dst);
+        assertEquals("src", wf.get("dne"));
+        assertEquals("src", new WriteField(dst, () -> root).get("dne"));
+        assertFalse(branches.get("b").containsKey("c"));
+
+        root.clear();
+        branches = addPath(root, src, "src");
+        branches.putAll(addPath(root, dst, "dst"));
+        // src missing value
+        branches.get("b").remove("c");
+        wf = new WriteField(src, () -> root);
+        wf.overwrite(dst);
+        assertEquals("dne", wf.get("dne"));
+        assertEquals("dne", new WriteField(dst, () -> root).get("dne"));
+        assertFalse(branches.get("e").containsKey("f"));
     }
 
     public void testRemove() {
         Map<String, Object> root = new HashMap<>();
-        root.put("a.b.c", "foo");
+        Map<String, Object> a = new HashMap<>();
+        Map<String, Object> b = new HashMap<>();
+        b.put("c", "foo");
+        a.put("b", b);
+        root.put("a", a);
         WriteField wf = new WriteField("a.b.c", () -> root);
-        UnsupportedOperationException err = expectThrows(UnsupportedOperationException.class, wf::remove);
-        assertEquals("unimplemented", err.getMessage());
+        assertEquals("foo", wf.get("dne"));
+        wf.remove();
+        assertEquals("dne", wf.get("dne"));
+        assertThat(b.containsKey("c"), equalTo(false));
+
+        root.clear();
+        wf = new WriteField("a.b.c", () -> root);
+        wf.remove();
+        assertEquals("dne", wf.get("dne"));
     }
 
     @SuppressWarnings("unchecked")
@@ -329,4 +386,37 @@ public class WriteFieldTests extends ESTestCase {
         assertEquals("bar", new WriteField("a.c", () -> root).get(1, "bar"));
         assertEquals("foo", new WriteField("a.c", () -> root).get(0, "bar"));
     }
+
+    public MapOfMaps addPath(Map<String, Object> root, String path, Object value) {
+        String[] elements = path.split("\\.");
+
+        MapOfMaps containers = new MapOfMaps();
+        Map<String, Object> container = root;
+
+        for (int i = 0; i < elements.length - 1; i++) {
+            Map<String, Object> next = new HashMap<>();
+            assertNull(container.put(elements[i], next));
+            assertNull(containers.put(elements[i], next));
+            container = next;
+        }
+
+        container.put(elements[elements.length - 1], value);
+        return containers;
+    }
+
+    private static class MapOfMaps {
+        Map<String, Map<String, Object>> maps = new HashMap<>();
+
+        public Object put(String key, Map<String, Object> value) {
+            return maps.put(key, value);
+        }
+
+        public Map<String, Object> get(String key) {
+            return maps.get(key);
+        }
+
+        public void putAll(MapOfMaps all) {
+            maps.putAll(all.maps);
+        }
+    }
 }