Skip to content

Commit

Permalink
Script: Write Field API path manipulation (#89889)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
stu-elastic committed Sep 8, 2022
1 parent 691c08a commit a1082a6
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 27 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/89889.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 89889
summary: "Script: Write Field API path manipulation"
area: Infra/Scripting
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -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()"
}
}
]
}
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 36 additions & 10 deletions server/src/main/java/org/elasticsearch/script/field/WriteField.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,46 @@ public boolean exists() {
* 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
Expand Down Expand Up @@ -319,6 +335,16 @@ public WriteField removeValue(int index) {
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.
*/
Expand Down
112 changes: 101 additions & 11 deletions server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Objects;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;

public class WriteFieldTests extends ESTestCase {

Expand Down Expand Up @@ -85,27 +86,83 @@ public void testExists() {
}

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")
Expand Down Expand Up @@ -329,4 +386,37 @@ public void testGetIndex() {
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);
}
}
}

0 comments on commit a1082a6

Please sign in to comment.