diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
index 363046c39..8dfbc1423 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java
@@ -1955,6 +1955,172 @@ public static Expression mapRemove(String mapField, String key) {
return mapRemove(field(mapField), key);
}
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ *
builder = ImmutableList.builder();
+ builder.add(mapExpr);
+ builder.add(key);
+ builder.add(value);
+ builder.add(moreKeyValues);
+ return new FunctionExpression("map_set", builder.build());
+ }
+
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ *
+ * - Only performs shallow updates to the map.
+ *
- Setting a value to {@code null} will retain the key with a {@code null} value. To remove
+ * a key entirely, use {@code mapRemove}.
+ *
+ *
+ * @param mapField The map field to set entries in.
+ * @param key The key to set.
+ * @param value The value to set.
+ * @param moreKeyValues Additional key-value pairs to set.
+ * @return A new {@link Expression} representing the map with the entries set.
+ */
+ @BetaApi
+ public static Expression mapSet(
+ Expression mapExpr, String key, Object value, Object... moreKeyValues) {
+ return mapSet(
+ mapExpr,
+ constant(key),
+ toExprOrConstant(value),
+ toArrayOfExprOrConstant(moreKeyValues).toArray(new Expression[0]));
+ }
+
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ *
+ * - Only performs shallow updates to the map.
+ *
- Setting a value to {@code null} will retain the key with a {@code null} value. To remove
+ * a key entirely, use {@code mapRemove}.
+ *
+ *
+ * @param mapField The map field to set entries in.
+ * @param key The key to set. Must be an expression representing a string.
+ * @param value The value to set.
+ * @param moreKeyValues Additional key-value pairs to set.
+ * @return A new {@link Expression} representing the map with the entries set.
+ */
+ @BetaApi
+ public static Expression mapSet(
+ String mapField, Expression key, Expression value, Expression... moreKeyValues) {
+ return mapSet(field(mapField), key, value, moreKeyValues);
+ }
+
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ *
+ * - Only performs shallow updates to the map.
+ *
- Setting a value to {@code null} will retain the key with a {@code null} value. To remove
+ * a key entirely, use {@code mapRemove}.
+ *
+ *
+ * @param mapField The map field to set entries in.
+ * @param key The key to set. Must be an expression representing a string.
+ * @param value The value to set.
+ * @param moreKeyValues Additional key-value pairs to set.
+ * @return A new {@link Expression} representing the map with the entries set.
+ */
+ @BetaApi
+ public static Expression mapSet(
+ String mapField, String key, Object value, Object... moreKeyValues) {
+ return mapSet(field(mapField), key, value, moreKeyValues);
+ }
+
+ /**
+ * Creates an expression that returns the keys of a map.
+ *
+ * @param mapExpr The map expression to get the keys of.
+ * @return A new {@link Expression} representing the keys of the map.
+ */
+ @BetaApi
+ public static Expression mapKeys(Expression mapExpr) {
+ return new FunctionExpression("map_keys", ImmutableList.of(mapExpr));
+ }
+
+ /**
+ * Creates an expression that returns the keys of a map.
+ *
+ * @param mapField The map field to get the keys of.
+ * @return A new {@link Expression} representing the keys of the map.
+ */
+ @BetaApi
+ public static Expression mapKeys(String mapField) {
+ return mapKeys(field(mapField));
+ }
+
+ /**
+ * Creates an expression that returns the values of a map.
+ *
+ * While the backend generally preserves insertion order, relying on the order of the output
+ * array is not guaranteed and should be avoided.
+ *
+ * @param mapExpr The expression representing the map to get the values of.
+ * @return A new {@link Expression} representing the values of the map.
+ */
+ @BetaApi
+ public static Expression mapValues(Expression mapExpr) {
+ return new FunctionExpression("map_values", ImmutableList.of(mapExpr));
+ }
+
+ /**
+ * Creates an expression that returns the values of a map.
+ *
+ * @param mapField The map field to get the values of.
+ * @return A new {@link Expression} representing the values of the map.
+ */
+ @BetaApi
+ public static Expression mapValues(String mapField) {
+ return mapValues(field(mapField));
+ }
+
+ /**
+ * Creates an expression that returns the entries of a map as an array of maps, where each map
+ * contains a "k" property for the key and a "v" property for the value.
+ *
+ *
While the backend generally preserves insertion order, relying on the order of the output
+ * array is not guaranteed and should be avoided.
+ *
+ * @param mapExpr The expression representing the map to get the entries of.
+ * @return A new {@link Expression} representing the entries of the map.
+ */
+ @BetaApi
+ public static Expression mapEntries(Expression mapExpr) {
+ return new FunctionExpression("map_entries", ImmutableList.of(mapExpr));
+ }
+
+ /**
+ * Creates an expression that returns the entries of a map as an array of maps.
+ *
+ * @param mapField The map field to get the entries of.
+ * @return A new {@link Expression} representing the entries of the map.
+ */
+ @BetaApi
+ public static Expression mapEntries(String mapField) {
+ return mapEntries(field(mapField));
+ }
+
/**
* Creates an expression that reverses a string, blob, or array.
*
@@ -4376,6 +4542,80 @@ public final Expression mapRemove(String key) {
return mapRemove(this, key);
}
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ *
Note that {@code mapSet} only performs shallow updates to the map. Setting a value to {@code
+ * null} will retain the key with a {@code null} value. To remove a key entirely, use {@code
+ * mapRemove}.
+ *
+ * @param key The key to set.
+ * @param value The value to set.
+ * @param moreKeyValues Additional key-value pairs to set.
+ * @return A new {@link Expression} representing the map with the entries set.
+ */
+ @BetaApi
+ public final Expression mapSet(Expression key, Expression value, Expression... moreKeyValues) {
+ return mapSet(this, key, value, moreKeyValues);
+ }
+
+ /**
+ * Creates an expression that returns a new map with the specified entries added or updated.
+ *
+ * @param key The key to set.
+ * @param value The value to set.
+ * @param moreKeyValues Additional key-value pairs to set.
+ * @return A new {@link Expression} representing the map with the entries set.
+ */
+ @BetaApi
+ public final Expression mapSet(String key, Object value, Object... moreKeyValues) {
+ return mapSet(
+ this,
+ constant(key),
+ toExprOrConstant(value),
+ toArrayOfExprOrConstant(moreKeyValues).toArray(new Expression[0]));
+ }
+
+ /**
+ * Creates an expression that returns the keys of this map expression.
+ *
+ *
While the backend generally preserves insertion order, relying on the order of the output
+ * array is not guaranteed and should be avoided.
+ *
+ * @return A new {@link Expression} representing the keys of the map.
+ */
+ @BetaApi
+ public final Expression mapKeys() {
+ return mapKeys(this);
+ }
+
+ /**
+ * Creates an expression that returns the values of this map expression.
+ *
+ *
While the backend generally preserves insertion order, relying on the order of the output
+ * array is not guaranteed and should be avoided.
+ *
+ * @return A new {@link Expression} representing the values of the map.
+ */
+ @BetaApi
+ public final Expression mapValues() {
+ return mapValues(this);
+ }
+
+ /**
+ * Creates an expression that returns the entries of this map expression as an array of maps,
+ * where each map contains a "k" property for the key and a "v" property for the value.
+ *
+ *
While the backend generally preserves insertion order, relying on the order of the output
+ * array is not guaranteed and should be avoided.
+ *
+ * @return A new {@link Expression} representing the entries of the map.
+ */
+ @BetaApi
+ public final Expression mapEntries() {
+ return mapEntries(this);
+ }
+
/**
* Creates an expression that reverses this expression, which must be a string, blob, or array.
*
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java
index 5f810332f..9c445a720 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java
@@ -107,6 +107,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@@ -1377,6 +1378,205 @@ public void testMapGet() throws Exception {
map("hugoAward", true, "title", "Dune")));
}
+ @Test
+ public void testMapSet() throws Exception {
+ Map docData = new HashMap<>();
+ docData.put("existingField", ImmutableMap.of("foo", 1L));
+
+ Pipeline.Snapshot results =
+ firestore
+ .pipeline()
+ .collection(collection.getPath())
+ .replaceWith(Expression.map(docData))
+ .limit(1)
+ .select(
+ Expression.mapSet("existingField", "bar", 2).as("modifiedField"),
+ Expression.mapSet(Expression.map(ImmutableMap.of()), "a", 1).as("simple"),
+ Expression.mapSet(Expression.map(ImmutableMap.of("a", 1)), "b", 2).as("add"),
+ Expression.mapSet(Expression.map(ImmutableMap.of("a", 1)), "a", 2).as("overwrite"),
+ Expression.mapSet(Expression.map(ImmutableMap.of("a", 1, "b", 2)), "a", 3, "c", 4)
+ .as("multi"),
+ Expression.mapSet(
+ Expression.map(ImmutableMap.of("a", 1)), "a", field("non_existent"))
+ .as("remove"),
+ Expression.mapSet(Expression.map(ImmutableMap.of("a", 1)), "b", null).as("setNull"),
+ Expression.mapSet(
+ Expression.map(ImmutableMap.of("a", ImmutableMap.of("b", 1))), "a.b", 2)
+ .as("setDotted"),
+ Expression.mapSet(Expression.map(ImmutableMap.of()), "", "empty").as("setEmptyKey"),
+ Expression.mapSet(
+ Expression.map(ImmutableMap.of("a", 1)),
+ "b",
+ Expression.add(constant(1), constant(2)))
+ .as("setExprVal"),
+ Expression.mapSet(
+ Expression.map(ImmutableMap.of()), "obj", ImmutableMap.of("hidden", true))
+ .as("setNestedMap"),
+ Expression.mapSet(Expression.map(ImmutableMap.of()), "~!@#$%^&*()_+", "special")
+ .as("setSpecialChars"),
+ field("existingField").mapSet("instanceKey", 100).as("instanceSetField"),
+ Expression.map(ImmutableMap.of("x", 1))
+ .mapSet(constant("y"), constant(2))
+ .as("instanceSetConstant"))
+ .execute()
+ .get();
+
+ List resultList = results.getResults();
+ assertThat(resultList).isNotEmpty();
+ Map data = resultList.get(0).getData();
+
+ assertThat((Map, ?>) data.get("modifiedField")).containsExactly("foo", 1L, "bar", 2L);
+ assertThat((Map, ?>) data.get("simple")).containsExactly("a", 1L);
+ assertThat((Map, ?>) data.get("add")).containsExactly("a", 1L, "b", 2L);
+ assertThat((Map, ?>) data.get("overwrite")).containsExactly("a", 2L);
+ assertThat((Map, ?>) data.get("multi")).containsExactly("a", 3L, "b", 2L, "c", 4L);
+ assertThat((Map, ?>) data.get("remove")).isEmpty();
+ assertThat((Map, ?>) data.get("setNull")).containsExactly("a", 1L, "b", null);
+
+ Map, ?> setDotted = (Map, ?>) data.get("setDotted");
+ assertThat(setDotted).containsEntry("a.b", 2L);
+ assertThat((Map, ?>) setDotted.get("a")).containsExactly("b", 1L);
+
+ assertThat((Map, ?>) data.get("setEmptyKey")).containsExactly("", "empty");
+ assertThat((Map, ?>) data.get("setExprVal")).containsExactly("a", 1L, "b", 3L);
+ assertThat((Map, ?>) data.get("setNestedMap"))
+ .isEqualTo(ImmutableMap.of("obj", ImmutableMap.of("hidden", true)));
+ assertThat((Map, ?>) data.get("setSpecialChars")).containsExactly("~!@#$%^&*()_+", "special");
+
+ assertThat((Map, ?>) data.get("instanceSetField"))
+ .containsExactly("foo", 1L, "instanceKey", 100L);
+ assertThat((Map, ?>) data.get("instanceSetConstant")).containsExactly("x", 1L, "y", 2L);
+ }
+
+ @Test
+ public void testMapKeys() throws Exception {
+ Map docData = new HashMap<>();
+ docData.put("existingField", ImmutableMap.of("foo", 1L));
+
+ Pipeline.Snapshot results =
+ firestore
+ .pipeline()
+ .collection(collection.getPath())
+ .replaceWith(Expression.map(docData))
+ .limit(1)
+ .select(
+ Expression.mapKeys("existingField").as("existingKeys"),
+ Expression.mapKeys(Expression.map(ImmutableMap.of("a", 1, "b", 2))).as("keys"),
+ Expression.mapKeys(Expression.map(ImmutableMap.of())).as("empty_keys"),
+ Expression.mapKeys(
+ Expression.map(ImmutableMap.of("a", ImmutableMap.of("nested", true))))
+ .as("nested_keys"),
+ field("existingField").mapKeys().as("instanceExistingKeys"),
+ Expression.map(ImmutableMap.of("x", 10, "y", 20)).mapKeys().as("instanceKeys"))
+ .execute()
+ .get();
+
+ List resultList = results.getResults();
+ assertThat(resultList).isNotEmpty();
+ Map data = resultList.get(0).getData();
+
+ assertThat((List>) data.get("existingKeys")).containsExactly("foo");
+ assertThat((List>) data.get("keys")).containsExactly("a", "b");
+ assertThat((List>) data.get("empty_keys")).isEmpty();
+ assertThat((List>) data.get("nested_keys")).containsExactly("a");
+
+ assertThat((List>) data.get("instanceExistingKeys")).containsExactly("foo");
+ assertThat((List>) data.get("instanceKeys")).containsExactly("x", "y");
+ }
+
+ @Test
+ public void testMapValues() throws Exception {
+ Map docData = new HashMap<>();
+ docData.put("existingField", ImmutableMap.of("foo", 1L));
+
+ Pipeline.Snapshot results =
+ firestore
+ .pipeline()
+ .collection(collection.getPath())
+ .replaceWith(Expression.map(docData))
+ .limit(1)
+ .select(
+ Expression.mapValues("existingField").as("existingValues"),
+ Expression.mapValues(Expression.map(ImmutableMap.of("a", 1, "b", 2))).as("values"),
+ Expression.mapValues(Expression.map(ImmutableMap.of())).as("empty_values"),
+ Expression.mapValues(
+ Expression.map(ImmutableMap.of("a", ImmutableMap.of("nested", true))))
+ .as("nested_values"),
+ field("existingField").mapValues().as("instanceExistingValues"),
+ Expression.map(ImmutableMap.of("x", 10, "y", 20)).mapValues().as("instanceValues"))
+ .execute()
+ .get();
+
+ List resultList = results.getResults();
+ assertThat(resultList).isNotEmpty();
+ Map data = resultList.get(0).getData();
+
+ assertThat((List>) data.get("existingValues")).containsExactly(1L);
+ assertThat((List>) data.get("values")).containsExactly(1L, 2L);
+ assertThat((List>) data.get("empty_values")).isEmpty();
+ assertThat((List>) data.get("nested_values"))
+ .containsExactly(ImmutableMap.of("nested", true));
+
+ assertThat((List>) data.get("instanceExistingValues")).containsExactly(1L);
+ assertThat((List>) data.get("instanceValues")).containsExactly(10L, 20L);
+ }
+
+ @Test
+ public void testMapEntries() throws Exception {
+ Map docData = new HashMap<>();
+ docData.put("existingField", ImmutableMap.of("foo", 1L));
+
+ Pipeline.Snapshot results =
+ firestore
+ .pipeline()
+ .collection(collection.getPath())
+ .replaceWith(Expression.map(docData))
+ .limit(1)
+ .select(
+ Expression.mapEntries("existingField").as("existingEntries"),
+ Expression.mapEntries(Expression.map(ImmutableMap.of("a", 1, "b", 2)))
+ .as("entries"),
+ Expression.mapEntries(Expression.map(ImmutableMap.of())).as("empty_entries"),
+ Expression.mapEntries(
+ Expression.map(ImmutableMap.of("a", ImmutableMap.of("nested", true))))
+ .as("nested_entries"),
+ field("existingField").mapEntries().as("instanceExistingEntries"),
+ Expression.map(ImmutableMap.of("x", 10, "y", 20))
+ .mapEntries()
+ .as("instanceEntries"))
+ .execute()
+ .get();
+
+ List resultList = results.getResults();
+ assertThat(resultList).isNotEmpty();
+ Map data = resultList.get(0).getData();
+
+ assertThat((List>) data.get("existingEntries"))
+ .containsExactly(ImmutableMap.of("k", "foo", "v", 1L));
+
+ @SuppressWarnings("unchecked")
+ List