Visitar URL original
feat(firestore): add support for new expressions including nor, coale… · firebase/flutterfire@2612eb2 · GitHub
Skip to content

Commit 2612eb2

Browse files
authored
feat(firestore): add support for new expressions including nor, coalesce, switch_on, timestamp_diff, and timestamp_extract (#18200)
* feat(firestore): add support for new expressions including nor, coalesce, switch_on, timestamp_diff, and timestamp_extract * fix * format * fix formatting * fix formatting * fix formatting * refactor: enhance coalesce and switchOn expressions to accept multiple arguments directly * fix formatting
1 parent e81cb6d commit 2612eb2

10 files changed

Lines changed: 1412 additions & 5 deletions

File tree

‎packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionHelpers.java‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,84 @@ static BooleanExpression parseXorExpression(
9797
return Expression.xor(first, rest);
9898
}
9999

100+
/**
101+
* Parses a "nor" expression from a list of expression maps. Uses Expression.nor() with varargs
102+
* signature.
103+
*/
104+
@SuppressWarnings("unchecked")
105+
static BooleanExpression parseNorExpression(
106+
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
107+
if (exprMaps == null || exprMaps.isEmpty()) {
108+
throw new IllegalArgumentException("'nor' requires at least one expression");
109+
}
110+
111+
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
112+
if (exprMaps.size() == 1) {
113+
return first;
114+
}
115+
116+
BooleanExpression[] rest = new BooleanExpression[exprMaps.size() - 1];
117+
for (int i = 1; i < exprMaps.size(); i++) {
118+
rest[i - 1] = parser.parseBooleanExpression(exprMaps.get(i));
119+
}
120+
return Expression.nor(first, rest);
121+
}
122+
123+
/**
124+
* Parses a "coalesce" expression from a list of expression maps. Uses Expression.coalesce() with
125+
* varargs.
126+
*/
127+
@SuppressWarnings("unchecked")
128+
static Expression parseCoalesceExpression(
129+
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
130+
if (exprMaps == null || exprMaps.size() < 2) {
131+
throw new IllegalArgumentException("'coalesce' requires at least two expressions");
132+
}
133+
134+
Expression first = parser.parseExpression(exprMaps.get(0));
135+
Expression second = parser.parseExpression(exprMaps.get(1));
136+
if (exprMaps.size() == 2) {
137+
return Expression.coalesce(first, second);
138+
}
139+
140+
Object[] rest = new Object[exprMaps.size() - 2];
141+
for (int i = 2; i < exprMaps.size(); i++) {
142+
rest[i - 2] = parser.parseExpression(exprMaps.get(i));
143+
}
144+
return Expression.coalesce(first, second, rest);
145+
}
146+
147+
/**
148+
* Parses a "switch_on" expression: alternating BooleanExpression condition and Expression result,
149+
* with an optional trailing default Expression when the list length is odd.
150+
*/
151+
@SuppressWarnings("unchecked")
152+
static Expression parseSwitchOnExpression(
153+
@NonNull List<Map<String, Object>> exprMaps, @NonNull ExpressionParsers parser) {
154+
int n = exprMaps.size();
155+
if (n < 2) {
156+
throw new IllegalArgumentException("'switch_on' requires at least two expressions");
157+
}
158+
159+
BooleanExpression first = parser.parseBooleanExpression(exprMaps.get(0));
160+
Expression second = parser.parseExpression(exprMaps.get(1));
161+
if (n == 2) {
162+
return Expression.switchOn(first, second);
163+
}
164+
165+
Object[] tail = new Object[n - 2];
166+
for (int i = 2; i < n; i++) {
167+
if (n % 2 == 1 && i == n - 1) {
168+
tail[i - 2] = parser.parseExpression(exprMaps.get(i));
169+
} else if (i % 2 == 0) {
170+
tail[i - 2] = parser.parseBooleanExpression(exprMaps.get(i));
171+
} else {
172+
tail[i - 2] = parser.parseExpression(exprMaps.get(i));
173+
}
174+
}
175+
return Expression.switchOn(first, second, tail);
176+
}
177+
100178
/**
101179
* Parses a constant value based on its type to match Android SDK constant() overloads. Valid
102180
* types: String, Number, Boolean, Date, Timestamp, GeoPoint, byte[], Blob, DocumentReference,

‎packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
185185
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
186186
return ExpressionHelpers.parseXorExpression(exprMaps, this);
187187
}
188+
case "nor":
189+
{
190+
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
191+
return ExpressionHelpers.parseNorExpression(exprMaps, this);
192+
}
188193
case "not":
189194
{
190195
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
@@ -392,6 +397,68 @@ Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
392397
}
393398
return Expression.timestampTruncate(parseExpression(timestampMap), unit);
394399
}
400+
case "timestamp_diff":
401+
{
402+
Map<String, Object> endMap = (Map<String, Object>) args.get("end");
403+
Map<String, Object> startMap = (Map<String, Object>) args.get("start");
404+
Object unitObj = args.get("unit");
405+
Expression endExpr = parseExpression(endMap);
406+
Expression startExpr = parseExpression(startMap);
407+
if (unitObj instanceof String) {
408+
return Expression.timestampDiff(endExpr, startExpr, (String) unitObj);
409+
}
410+
@SuppressWarnings("unchecked")
411+
Map<String, Object> unitMap = (Map<String, Object>) unitObj;
412+
return Expression.timestampDiff(endExpr, startExpr, parseExpression(unitMap));
413+
}
414+
case "timestamp_extract":
415+
{
416+
Map<String, Object> timestampMap = (Map<String, Object>) args.get("timestamp");
417+
Map<String, Object> partMap = (Map<String, Object>) args.get("part");
418+
Expression tsExpr = parseExpression(timestampMap);
419+
Expression partExpr = parseExpression(partMap);
420+
if (!args.containsKey("timezone") || args.get("timezone") == null) {
421+
return Expression.timestampExtract(tsExpr, partExpr);
422+
}
423+
Object tzObj = args.get("timezone");
424+
if (tzObj instanceof String) {
425+
return Expression.timestampExtractWithTimezone(tsExpr, partExpr, (String) tzObj);
426+
}
427+
@SuppressWarnings("unchecked")
428+
Map<String, Object> tzMap = (Map<String, Object>) tzObj;
429+
return Expression.timestampExtractWithTimezone(tsExpr, partExpr, parseExpression(tzMap));
430+
}
431+
case "parent":
432+
{
433+
if (args.containsKey("doc_ref")) {
434+
String path = (String) args.get("doc_ref");
435+
if (path == null) {
436+
throw new IllegalArgumentException("parent requires 'doc_ref' argument");
437+
}
438+
return Expression.parent(firestore.document(path));
439+
}
440+
return Expression.parent(parseChild(args, "expression"));
441+
}
442+
case "if_null":
443+
{
444+
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");
445+
Map<String, Object> replacementMap = (Map<String, Object>) args.get("replacement");
446+
return Expression.ifNull(parseExpression(exprMap), parseExpression(replacementMap));
447+
}
448+
case "coalesce":
449+
{
450+
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
451+
return ExpressionHelpers.parseCoalesceExpression(exprMaps, this);
452+
}
453+
case "switch_on":
454+
{
455+
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
456+
return ExpressionHelpers.parseSwitchOnExpression(exprMaps, this);
457+
}
458+
case "map_keys":
459+
return Expression.mapKeys(parseChild(args, "expression"));
460+
case "map_values":
461+
return Expression.mapValues(parseChild(args, "expression"));
395462
case "array":
396463
{
397464
List<?> elements = (List<?>) args.get("elements");
@@ -561,6 +628,11 @@ BooleanExpression parseBooleanExpression(@NonNull Map<String, Object> expression
561628
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
562629
return ExpressionHelpers.parseXorExpression(exprMaps, this);
563630
}
631+
case "nor":
632+
{
633+
List<Map<String, Object>> exprMaps = (List<Map<String, Object>>) args.get("expressions");
634+
return ExpressionHelpers.parseNorExpression(exprMaps, this);
635+
}
564636
case "not":
565637
{
566638
Map<String, Object> exprMap = (Map<String, Object>) args.get("expression");

‎packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m‎

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,10 @@ - (FIRExprBridge *)parseExpression:(NSDictionary<NSString *, id> *)map error:(NS
257257
}
258258

259259
// -------------------------------------------------------------------------
260-
// N-ary logical (expressions array): and, or, xor
260+
// N-ary logical (expressions array): and, or, xor, nor
261261
// -------------------------------------------------------------------------
262262
if ([name isEqualToString:@"and"] || [name isEqualToString:@"or"] ||
263-
[name isEqualToString:@"xor"]) {
263+
[name isEqualToString:@"xor"] || [name isEqualToString:@"nor"]) {
264264
NSArray *exprMaps = args[@"expressions"];
265265
if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count == 0) {
266266
if (error)
@@ -741,6 +741,173 @@ - (FIRExprBridge *)parseExpression:(NSDictionary<NSString *, id> *)map error:(NS
741741
return FLTNewFunctionExprBridge(@"timestamp_trunc", @[ timestampExpr, unitExpr ]);
742742
}
743743

744+
// -------------------------------------------------------------------------
745+
// map_keys, map_values (unary on map expression)
746+
// -------------------------------------------------------------------------
747+
if ([name isEqualToString:@"map_keys"] || [name isEqualToString:@"map_values"]) {
748+
id exprMap = args[@"expression"];
749+
if (![exprMap isKindOfClass:[NSDictionary class]]) {
750+
if (error) *error = parseError([NSString stringWithFormat:@"%@ requires expression", name]);
751+
return nil;
752+
}
753+
FIRExprBridge *expr = [self parseExpression:(NSDictionary *)exprMap error:error];
754+
if (!expr) return nil;
755+
return FLTNewFunctionExprBridge(name, @[ expr ]);
756+
}
757+
758+
// -------------------------------------------------------------------------
759+
// parent: doc_ref path or expression
760+
// -------------------------------------------------------------------------
761+
if ([name isEqualToString:@"parent"]) {
762+
NSString *docPath = args[@"doc_ref"];
763+
if ([docPath isKindOfClass:[NSString class]] && docPath.length > 0) {
764+
FIRDocumentReference *ref = [self.firestore documentWithPath:docPath];
765+
FIRExprBridge *refExpr = [[FIRConstantBridge alloc] init:ref];
766+
return FLTNewFunctionExprBridge(@"parent", @[ refExpr ]);
767+
}
768+
id exprMap = args[@"expression"];
769+
if (![exprMap isKindOfClass:[NSDictionary class]]) {
770+
if (error) *error = parseError(@"parent requires expression or doc_ref");
771+
return nil;
772+
}
773+
FIRExprBridge *expr = [self parseExpression:(NSDictionary *)exprMap error:error];
774+
if (!expr) return nil;
775+
return FLTNewFunctionExprBridge(@"parent", @[ expr ]);
776+
}
777+
778+
// -------------------------------------------------------------------------
779+
// timestamp_diff: end, start, unit (unit = string or expression map)
780+
// -------------------------------------------------------------------------
781+
if ([name isEqualToString:@"timestamp_diff"]) {
782+
id endMap = args[@"end"];
783+
id startMap = args[@"start"];
784+
id unitObj = args[@"unit"];
785+
if (![endMap isKindOfClass:[NSDictionary class]] ||
786+
![startMap isKindOfClass:[NSDictionary class]] || !unitObj) {
787+
if (error) *error = parseError(@"timestamp_diff requires end, start, and unit");
788+
return nil;
789+
}
790+
FIRExprBridge *endExpr = [self parseExpression:(NSDictionary *)endMap error:error];
791+
FIRExprBridge *startExpr = [self parseExpression:(NSDictionary *)startMap error:error];
792+
if (!endExpr || !startExpr) return nil;
793+
FIRExprBridge *unitExpr = nil;
794+
if ([unitObj isKindOfClass:[NSString class]]) {
795+
unitExpr = [[FIRConstantBridge alloc] init:unitObj];
796+
} else if ([unitObj isKindOfClass:[NSDictionary class]]) {
797+
unitExpr = [self parseExpression:(NSDictionary *)unitObj error:error];
798+
} else {
799+
if (error) *error = parseError(@"timestamp_diff unit must be string or expression");
800+
return nil;
801+
}
802+
if (!unitExpr) return nil;
803+
return FLTNewFunctionExprBridge(@"timestamp_diff", @[ endExpr, startExpr, unitExpr ]);
804+
}
805+
806+
// -------------------------------------------------------------------------
807+
// timestamp_extract: timestamp, part; optional timezone (string or expression)
808+
// -------------------------------------------------------------------------
809+
if ([name isEqualToString:@"timestamp_extract"]) {
810+
id timestampMap = args[@"timestamp"];
811+
id partMap = args[@"part"];
812+
if (![timestampMap isKindOfClass:[NSDictionary class]] ||
813+
![partMap isKindOfClass:[NSDictionary class]]) {
814+
if (error) *error = parseError(@"timestamp_extract requires timestamp and part");
815+
return nil;
816+
}
817+
FIRExprBridge *tsExpr = [self parseExpression:(NSDictionary *)timestampMap error:error];
818+
FIRExprBridge *partExpr = [self parseExpression:(NSDictionary *)partMap error:error];
819+
if (!tsExpr || !partExpr) return nil;
820+
id tzRaw = args[@"timezone"];
821+
if (tzRaw == nil) {
822+
return FLTNewFunctionExprBridge(@"timestamp_extract", @[ tsExpr, partExpr ]);
823+
}
824+
FIRExprBridge *tzExpr = nil;
825+
if ([tzRaw isKindOfClass:[NSString class]]) {
826+
tzExpr = [[FIRConstantBridge alloc] init:tzRaw];
827+
} else if ([tzRaw isKindOfClass:[NSDictionary class]]) {
828+
tzExpr = [self parseExpression:(NSDictionary *)tzRaw error:error];
829+
} else {
830+
if (error) *error = parseError(@"timestamp_extract timezone must be string or expression");
831+
return nil;
832+
}
833+
if (!tzExpr) return nil;
834+
return FLTNewFunctionExprBridge(@"timestamp_extract", @[ tsExpr, partExpr, tzExpr ]);
835+
}
836+
837+
// -------------------------------------------------------------------------
838+
// if_null: expression + replacement
839+
// -------------------------------------------------------------------------
840+
if ([name isEqualToString:@"if_null"]) {
841+
id exprMap = args[@"expression"];
842+
id replMap = args[@"replacement"];
843+
if (![exprMap isKindOfClass:[NSDictionary class]] ||
844+
![replMap isKindOfClass:[NSDictionary class]]) {
845+
if (error) *error = parseError(@"if_null requires expression and replacement");
846+
return nil;
847+
}
848+
FIRExprBridge *ifExpr = [self parseExpression:(NSDictionary *)exprMap error:error];
849+
FIRExprBridge *replExpr = [self parseExpression:(NSDictionary *)replMap error:error];
850+
if (!ifExpr || !replExpr) return nil;
851+
return FLTNewFunctionExprBridge(@"if_null", @[ ifExpr, replExpr ]);
852+
}
853+
854+
// -------------------------------------------------------------------------
855+
// coalesce: expressions[] (>= 2)
856+
// -------------------------------------------------------------------------
857+
if ([name isEqualToString:@"coalesce"]) {
858+
NSArray *exprMaps = args[@"expressions"];
859+
if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count < 2) {
860+
if (error) *error = parseError(@"coalesce requires at least two expressions");
861+
return nil;
862+
}
863+
NSMutableArray<FIRExprBridge *> *exprs = [NSMutableArray array];
864+
for (id em in exprMaps) {
865+
if (![em isKindOfClass:[NSDictionary class]]) continue;
866+
FIRExprBridge *e = [self parseExpression:(NSDictionary *)em error:error];
867+
if (!e) return nil;
868+
[exprs addObject:e];
869+
}
870+
if (exprs.count < 2) {
871+
if (error) *error = parseError(@"coalesce requires at least two expressions");
872+
return nil;
873+
}
874+
return FLTNewFunctionExprBridge(@"coalesce", exprs);
875+
}
876+
877+
// -------------------------------------------------------------------------
878+
// switch_on: alternating condition (bool), result (expr), optional default
879+
// -------------------------------------------------------------------------
880+
if ([name isEqualToString:@"switch_on"]) {
881+
NSArray *exprMaps = args[@"expressions"];
882+
if (![exprMaps isKindOfClass:[NSArray class]] || exprMaps.count < 2) {
883+
if (error) *error = parseError(@"switch_on requires at least two expressions");
884+
return nil;
885+
}
886+
NSUInteger n = exprMaps.count;
887+
FIRExprBridge *first = [self parseBooleanExpression:exprMaps[0] error:error];
888+
FIRExprBridge *second = [self parseExpression:exprMaps[1] error:error];
889+
if (!first || !second) return nil;
890+
if (n == 2) {
891+
return FLTNewFunctionExprBridge(@"switch_on", @[ first, second ]);
892+
}
893+
NSMutableArray<FIRExprBridge *> *rest = [NSMutableArray array];
894+
for (NSUInteger i = 2; i < n; i++) {
895+
FIRExprBridge *e = nil;
896+
if ((n % 2 == 1) && (i == n - 1)) {
897+
e = [self parseExpression:exprMaps[i] error:error];
898+
} else if (i % 2 == 0) {
899+
e = [self parseBooleanExpression:exprMaps[i] error:error];
900+
} else {
901+
e = [self parseExpression:exprMaps[i] error:error];
902+
}
903+
if (!e) return nil;
904+
[rest addObject:e];
905+
}
906+
NSMutableArray<FIRExprBridge *> *all = [NSMutableArray arrayWithObjects:first, second, nil];
907+
[all addObjectsFromArray:rest];
908+
return FLTNewFunctionExprBridge(@"switch_on", all);
909+
}
910+
744911
// -------------------------------------------------------------------------
745912
// PipelineFilter (name "filter"): operator-based (and/or) or field-based
746913
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)