Skip to content

Commit b4089d8

Browse files
committed
chore: implement pipeline execution and expression parsing utilities for Android
1 parent d92a2b0 commit b4089d8

File tree

10 files changed

+1511
-147
lines changed

10 files changed

+1511
-147
lines changed

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/FlutterFirebaseFirestorePlugin.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import com.google.firebase.firestore.MemoryCacheSettings;
2929
import com.google.firebase.firestore.PersistentCacheIndexManager;
3030
import com.google.firebase.firestore.PersistentCacheSettings;
31+
import com.google.firebase.firestore.Pipeline;
32+
import com.google.firebase.firestore.PipelineResult;
3133
import com.google.firebase.firestore.Query;
3234
import com.google.firebase.firestore.QuerySnapshot;
3335
import com.google.firebase.firestore.SetOptions;
@@ -52,6 +54,7 @@
5254
import io.flutter.plugins.firebase.firestore.streamhandler.TransactionStreamHandler;
5355
import io.flutter.plugins.firebase.firestore.utils.ExceptionConverter;
5456
import io.flutter.plugins.firebase.firestore.utils.PigeonParser;
57+
import io.flutter.plugins.firebase.firestore.utils.PipelineParser;
5558
import java.util.ArrayList;
5659
import java.util.HashMap;
5760
import java.util.List;
@@ -983,4 +986,66 @@ public void documentReferenceSnapshot(
983986
parameters.getServerTimestampBehavior()),
984987
PigeonParser.parseListenSource(source))));
985988
}
989+
990+
@Override
991+
public void executePipeline(
992+
@NonNull GeneratedAndroidFirebaseFirestore.FirestorePigeonFirebaseApp app,
993+
@NonNull List<Map<String, Object>> stages,
994+
@Nullable Map<String, Object> options,
995+
@NonNull
996+
GeneratedAndroidFirebaseFirestore.Result<
997+
GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot>
998+
result) {
999+
cachedThreadPool.execute(
1000+
() -> {
1001+
try {
1002+
FirebaseFirestore firestore = getFirestoreFromPigeon(app);
1003+
1004+
// Execute pipeline using Android Firestore SDK
1005+
Pipeline.Snapshot snapshot = PipelineParser.executePipeline(firestore, stages, options);
1006+
1007+
// Convert Pipeline.Snapshot to PigeonPipelineSnapshot
1008+
List<GeneratedAndroidFirebaseFirestore.PigeonPipelineResult> pipelineResults =
1009+
new ArrayList<>();
1010+
1011+
// Iterate through snapshot results
1012+
for (PipelineResult pipelineResult : snapshot.getResults()) {
1013+
GeneratedAndroidFirebaseFirestore.PigeonPipelineResult.Builder resultBuilder =
1014+
new GeneratedAndroidFirebaseFirestore.PigeonPipelineResult.Builder();
1015+
resultBuilder.setDocumentPath(pipelineResult.getRef().getPath());
1016+
1017+
// Convert timestamps (assuming they're in milliseconds)
1018+
if (pipelineResult.getCreateTime() != null) {
1019+
resultBuilder.setCreateTime(pipelineResult.getCreateTime().toDate().getTime());
1020+
} else {
1021+
resultBuilder.setCreateTime(0L);
1022+
}
1023+
1024+
if (pipelineResult.getUpdateTime() != null) {
1025+
resultBuilder.setUpdateTime(pipelineResult.getUpdateTime().toDate().getTime());
1026+
} else {
1027+
resultBuilder.setUpdateTime(0L);
1028+
}
1029+
1030+
pipelineResults.add(resultBuilder.build());
1031+
}
1032+
1033+
// Build the snapshot
1034+
GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot.Builder snapshotBuilder =
1035+
new GeneratedAndroidFirebaseFirestore.PigeonPipelineSnapshot.Builder();
1036+
snapshotBuilder.setResults(pipelineResults);
1037+
1038+
// Set execution time (use current time if not available from snapshot)
1039+
if (snapshot.getExecutionTime() != null) {
1040+
snapshotBuilder.setExecutionTime(snapshot.getExecutionTime().toDate().getTime());
1041+
} else {
1042+
snapshotBuilder.setExecutionTime(System.currentTimeMillis());
1043+
}
1044+
1045+
result.success(snapshotBuilder.build());
1046+
} catch (Exception e) {
1047+
ExceptionConverter.sendErrorToFlutter(result, e);
1048+
}
1049+
});
1050+
}
9861051
}

packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/GeneratedAndroidFirebaseFirestore.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ public static final class Builder {
513513
}
514514

515515
@NonNull
516-
ArrayList<Object> toList() {
516+
public ArrayList<Object> toList() {
517517
ArrayList<Object> toListResult = new ArrayList<Object>(2);
518518
toListResult.add(hasPendingWrites);
519519
toListResult.add(isFromCache);
@@ -604,7 +604,7 @@ public static final class Builder {
604604
}
605605

606606
@NonNull
607-
ArrayList<Object> toList() {
607+
public ArrayList<Object> toList() {
608608
ArrayList<Object> toListResult = new ArrayList<Object>(3);
609609
toListResult.add(path);
610610
toListResult.add(data);
@@ -725,7 +725,7 @@ public static final class Builder {
725725
}
726726

727727
@NonNull
728-
ArrayList<Object> toList() {
728+
public ArrayList<Object> toList() {
729729
ArrayList<Object> toListResult = new ArrayList<Object>(4);
730730
toListResult.add(type == null ? null : type.index);
731731
toListResult.add((document == null) ? null : document.toList());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2026, the Chromium project authors. Please see the AUTHORS file
3+
* for details. All rights reserved. Use of this source code is governed by a
4+
* BSD-style license that can be found in the LICENSE file.
5+
*/
6+
7+
package io.flutter.plugins.firebase.firestore.utils;
8+
9+
import androidx.annotation.NonNull;
10+
import com.google.firebase.Timestamp;
11+
import com.google.firebase.firestore.Blob;
12+
import com.google.firebase.firestore.DocumentReference;
13+
import com.google.firebase.firestore.GeoPoint;
14+
import com.google.firebase.firestore.VectorValue;
15+
import com.google.firebase.firestore.pipeline.BooleanExpression;
16+
import com.google.firebase.firestore.pipeline.Expression;
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
/** Helper utilities for parsing expressions and handling common patterns. */
21+
class ExpressionHelpers {
22+
23+
/**
24+
* Parses an "and" expression from a list of expression maps. Uses Expression.and() with varargs
25+
* signature.
26+
*
27+
* @param exprMaps List of expression maps to combine with AND
28+
* @param parser Reference to ExpressionParsers for recursive parsing
29+
*/
30+
@SuppressWarnings("unchecked")
31+
static BooleanExpression parseAndExpression(
32+
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
33+
if (exprMaps == null || exprMaps.isEmpty()) {
34+
throw new IllegalArgumentException("'and' requires at least one expression");
35+
}
36+
37+
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
38+
if (exprMaps.size() == 1) {
39+
return first;
40+
}
41+
42+
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
43+
for (int i = 1; i < exprMaps.size(); i++) {
44+
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
45+
}
46+
return Expression.and(first, rest);
47+
}
48+
49+
/**
50+
* Parses an "or" expression from a list of expression maps. Uses Expression.or() with varargs
51+
* signature.
52+
*
53+
* @param exprMaps List of expression maps to combine with OR
54+
* @param parser Reference to ExpressionParsers for recursive parsing
55+
*/
56+
@SuppressWarnings("unchecked")
57+
static BooleanExpression parseOrExpression(
58+
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
59+
if (exprMaps == null || exprMaps.isEmpty()) {
60+
throw new IllegalArgumentException("'or' requires at least one expression");
61+
}
62+
63+
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
64+
if (exprMaps.size() == 1) {
65+
return first;
66+
}
67+
68+
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
69+
for (int i = 1; i < exprMaps.size(); i++) {
70+
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
71+
}
72+
return Expression.or(first, rest);
73+
}
74+
75+
/**
76+
* Parses a constant value based on its type to match Android SDK constant() overloads. Valid
77+
* types: String, Number, Boolean, Date, Timestamp, GeoPoint, byte[], Blob, DocumentReference,
78+
* VectorValue
79+
*/
80+
static Expression parseConstantValue(@NonNull Object value) {
81+
82+
if (value instanceof String) {
83+
return Expression.constant((String) value);
84+
} else if (value instanceof Number) {
85+
return Expression.constant((Number) value);
86+
} else if (value instanceof Boolean) {
87+
return Expression.constant((Boolean) value);
88+
} else if (value instanceof java.util.Date) {
89+
return Expression.constant((java.util.Date) value);
90+
} else if (value instanceof Timestamp) {
91+
return Expression.constant((Timestamp) value);
92+
} else if (value instanceof GeoPoint) {
93+
return Expression.constant((GeoPoint) value);
94+
} else if (value instanceof byte[]) {
95+
return Expression.constant((byte[]) value);
96+
} else if (value instanceof List) {
97+
// Handle List<int> from Dart which comes as List<Integer> or List<Number>
98+
// This represents byte[] (byte array) for constant expressions
99+
@SuppressWarnings("unchecked")
100+
List<?> list = (List<?>) value;
101+
// Check if all elements are numbers (for byte array)
102+
boolean isByteArray = true;
103+
for (Object item : list) {
104+
if (!(item instanceof Number)) {
105+
isByteArray = false;
106+
break;
107+
}
108+
}
109+
if (isByteArray && !list.isEmpty()) {
110+
byte[] byteArray = new byte[list.size()];
111+
for (int i = 0; i < list.size(); i++) {
112+
byteArray[i] = ((Number) list.get(i)).byteValue();
113+
}
114+
return Expression.constant(byteArray);
115+
}
116+
// If not a byte array, fall through to error
117+
} else if (value instanceof Blob) {
118+
return Expression.constant((Blob) value);
119+
} else if (value instanceof DocumentReference) {
120+
return Expression.constant((DocumentReference) value);
121+
} else if (value instanceof VectorValue) {
122+
return Expression.constant((VectorValue) value);
123+
}
124+
125+
throw new IllegalArgumentException(
126+
"Constant value must be one of: String, Number, Boolean, Date, Timestamp, "
127+
+ "GeoPoint, byte[], Blob, DocumentReference, or VectorValue. Got: "
128+
+ value.getClass().getName());
129+
}
130+
131+
/** Safely extracts a single expression from args map. */
132+
@SuppressWarnings("unchecked")
133+
static Map<String, Object> extractExpression(
134+
@NonNull Map<String, Object> args, @NonNull String key) {
135+
Map<String, Object> exprMap = (Map<String, Object>) args.get(key);
136+
if (exprMap == null) {
137+
throw new IllegalArgumentException("Missing required expression argument: " + key);
138+
}
139+
return exprMap;
140+
}
141+
142+
/** Safely extracts a list of expressions from args map. */
143+
@SuppressWarnings("unchecked")
144+
static List<Map<String, Object>> extractExpressionList(
145+
@NonNull Map<String, Object> args, @NonNull String key) {
146+
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get(key);
147+
if (exprMaps == null || exprMaps.isEmpty()) {
148+
throw new IllegalArgumentException("Missing or empty expression list: " + key);
149+
}
150+
return exprMaps;
151+
}
152+
}

0 commit comments

Comments
 (0)