Skip to content

Commit ae62102

Browse files
committed
feat(firestore): add support for new array expressions
1 parent 739600d commit ae62102

7 files changed

Lines changed: 493 additions & 2 deletions

File tree

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
136136
}
137137
return Expression.field(fieldName);
138138
}
139+
case "variable":
140+
{
141+
String variableName = (String) args.get("name");
142+
if (variableName == null) {
143+
throw new IllegalArgumentException("Variable expression must have a 'name' argument");
144+
}
145+
return Expression.variable(variableName);
146+
}
139147
case "constant":
140148
{
141149
Object value = args.get("value");
@@ -274,8 +282,49 @@ Expression parseExpression(@NonNull Map<String, Object> expressionMap) {
274282
case "array_sum":
275283
return Expression.arraySum(parseChild(args, "expression"));
276284
case "array_slice":
277-
throw new UnsupportedOperationException(
278-
"Expression type 'array_slice' is not supported on Android Firestore pipeline API");
285+
{
286+
Expression array = parseChild(args, "expression");
287+
Expression offset = parseChild(args, "offset");
288+
Map<String, Object> lengthMap = (Map<String, Object>) args.get("length");
289+
if (lengthMap == null) {
290+
return array.arraySliceToEnd(offset);
291+
}
292+
return array.arraySlice(offset, parseExpression(lengthMap));
293+
}
294+
case "array_filter":
295+
{
296+
Expression array = parseChild(args, "expression");
297+
String alias = (String) args.get("alias");
298+
Map<String, Object> filterMap = (Map<String, Object>) args.get("filter");
299+
if (alias == null || filterMap == null) {
300+
throw new IllegalArgumentException("array_filter requires alias and filter");
301+
}
302+
return array.arrayFilter(alias, parseBooleanExpression(filterMap));
303+
}
304+
case "array_transform":
305+
{
306+
Expression array = parseChild(args, "expression");
307+
String elementAlias = (String) args.get("element_alias");
308+
Map<String, Object> transformMap = (Map<String, Object>) args.get("transform");
309+
if (elementAlias == null || transformMap == null) {
310+
throw new IllegalArgumentException(
311+
"array_transform requires element_alias and transform");
312+
}
313+
return array.arrayTransform(elementAlias, parseExpression(transformMap));
314+
}
315+
case "array_transform_with_index":
316+
{
317+
Expression array = parseChild(args, "expression");
318+
String elementAlias = (String) args.get("element_alias");
319+
String indexAlias = (String) args.get("index_alias");
320+
Map<String, Object> transformMap = (Map<String, Object>) args.get("transform");
321+
if (elementAlias == null || indexAlias == null || transformMap == null) {
322+
throw new IllegalArgumentException(
323+
"array_transform_with_index requires element_alias, index_alias, and transform");
324+
}
325+
return array.arrayTransformWithIndex(
326+
elementAlias, indexAlias, parseExpression(transformMap));
327+
}
279328
case "if_absent":
280329
{
281330
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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ - (FIRExprBridge *)parseExpression:(NSDictionary<NSString *, id> *)map error:(NS
116116
return [[FIRFieldBridge alloc] initWithName:field];
117117
}
118118

119+
if ([name isEqualToString:@"variable"]) {
120+
NSString *variableName = args[@"name"];
121+
if (![variableName isKindOfClass:[NSString class]] || variableName.length == 0) {
122+
if (error) *error = parseError(@"Variable expression requires 'name' argument");
123+
return nil;
124+
}
125+
return FLTNewFunctionExprBridge(@"variable",
126+
@[ [[FIRConstantBridge alloc] init:variableName] ]);
127+
}
128+
119129
if ([name isEqualToString:@"constant"]) {
120130
id value = args[@"value"];
121131
if (value == nil) {
@@ -486,6 +496,86 @@ - (FIRExprBridge *)parseExpression:(NSDictionary<NSString *, id> *)map error:(NS
486496
return FLTNewFunctionExprBridge(@"array_concat", all);
487497
}
488498

499+
// -------------------------------------------------------------------------
500+
// expression + offset (+ optional length): array_slice
501+
// -------------------------------------------------------------------------
502+
if ([name isEqualToString:@"array_slice"]) {
503+
id exprMap = args[@"expression"];
504+
id offsetMap = args[@"offset"];
505+
id lengthMap = args[@"length"];
506+
if (![exprMap isKindOfClass:[NSDictionary class]] ||
507+
![offsetMap isKindOfClass:[NSDictionary class]]) {
508+
if (error) *error = parseError(@"array_slice requires expression and offset");
509+
return nil;
510+
}
511+
FIRExprBridge *expr = [self parseExpression:exprMap error:error];
512+
FIRExprBridge *offset = [self parseExpression:offsetMap error:error];
513+
if (!expr || !offset) return nil;
514+
NSMutableArray<FIRExprBridge *> *sliceArgs =
515+
[NSMutableArray arrayWithObjects:expr, offset, nil];
516+
if ([lengthMap isKindOfClass:[NSDictionary class]]) {
517+
FIRExprBridge *length = [self parseExpression:lengthMap error:error];
518+
if (!length) return nil;
519+
[sliceArgs addObject:length];
520+
}
521+
return FLTNewFunctionExprBridge(@"array_slice", sliceArgs);
522+
}
523+
524+
// -------------------------------------------------------------------------
525+
// expression + alias + filter: array_filter
526+
// -------------------------------------------------------------------------
527+
if ([name isEqualToString:@"array_filter"]) {
528+
id exprMap = args[@"expression"];
529+
NSString *alias = args[@"alias"];
530+
id filterMap = args[@"filter"];
531+
if (![exprMap isKindOfClass:[NSDictionary class]] || ![alias isKindOfClass:[NSString class]] ||
532+
![filterMap isKindOfClass:[NSDictionary class]]) {
533+
if (error) *error = parseError(@"array_filter requires expression, alias, and filter");
534+
return nil;
535+
}
536+
FIRExprBridge *expr = [self parseExpression:exprMap error:error];
537+
FIRExprBridge *filter = [self parseBooleanExpression:filterMap error:error];
538+
if (!expr || !filter) return nil;
539+
return FLTNewFunctionExprBridge(@"array_filter",
540+
@[ expr, [[FIRConstantBridge alloc] init:alias], filter ]);
541+
}
542+
543+
// -------------------------------------------------------------------------
544+
// expression + aliases + transform: array_transform / array_transform_with_index
545+
// -------------------------------------------------------------------------
546+
if ([name isEqualToString:@"array_transform"] ||
547+
[name isEqualToString:@"array_transform_with_index"]) {
548+
id exprMap = args[@"expression"];
549+
NSString *elementAlias = args[@"element_alias"];
550+
NSString *indexAlias = args[@"index_alias"];
551+
id transformMap = args[@"transform"];
552+
BOOL withIndex = [name isEqualToString:@"array_transform_with_index"];
553+
if (![exprMap isKindOfClass:[NSDictionary class]] ||
554+
![elementAlias isKindOfClass:[NSString class]] ||
555+
(withIndex && ![indexAlias isKindOfClass:[NSString class]]) ||
556+
![transformMap isKindOfClass:[NSDictionary class]]) {
557+
if (error) {
558+
NSString *message =
559+
withIndex
560+
? @"array_transform_with_index requires expression, element_alias, index_alias, "
561+
@"and transform"
562+
: @"array_transform requires expression, element_alias, and transform";
563+
*error = parseError(message);
564+
}
565+
return nil;
566+
}
567+
FIRExprBridge *expr = [self parseExpression:exprMap error:error];
568+
FIRExprBridge *transform = [self parseExpression:transformMap error:error];
569+
if (!expr || !transform) return nil;
570+
NSMutableArray<FIRExprBridge *> *transformArgs =
571+
[NSMutableArray arrayWithObjects:expr, [[FIRConstantBridge alloc] init:elementAlias], nil];
572+
if (withIndex) {
573+
[transformArgs addObject:[[FIRConstantBridge alloc] init:indexAlias]];
574+
}
575+
[transformArgs addObject:transform];
576+
return FLTNewFunctionExprBridge(name, transformArgs);
577+
}
578+
489579
// -------------------------------------------------------------------------
490580
// elements[]: array (construct) — Expression.array([...]) from Dart
491581
// -------------------------------------------------------------------------

packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,41 @@ abstract class Expression implements PipelineSerializable {
701701
return _ArrayIndexOfAllExpression(this, _toExpression(element));
702702
}
703703

704+
/// Returns a slice of this array starting at [offset].
705+
///
706+
/// When [length] is provided, at most [length] elements are returned.
707+
Expression arraySlice(Object? offset, [Object? length]) {
708+
return _ArraySliceExpression(
709+
this,
710+
_toExpression(offset),
711+
length == null ? null : _toExpression(length),
712+
);
713+
}
714+
715+
/// Filters this array by evaluating [filter] for each element bound to [alias].
716+
Expression arrayFilter(String alias, BooleanExpression filter) {
717+
return _ArrayFilterExpression(this, alias, filter);
718+
}
719+
720+
/// Transforms each element of this array bound to [elementAlias].
721+
Expression arrayTransform(String elementAlias, Expression transform) {
722+
return _ArrayTransformExpression(this, elementAlias, null, transform);
723+
}
724+
725+
/// Transforms each element of this array with both element and index aliases.
726+
Expression arrayTransformWithIndex(
727+
String elementAlias,
728+
String indexAlias,
729+
Expression transform,
730+
) {
731+
return _ArrayTransformExpression(
732+
this,
733+
elementAlias,
734+
indexAlias,
735+
transform,
736+
);
737+
}
738+
704739
// ============================================================================
705740
// AGGREGATE FUNCTIONS
706741
// ============================================================================
@@ -840,6 +875,9 @@ abstract class Expression implements PipelineSerializable {
840875
/// Creates a field reference expression from a field path string
841876
static Field field(String fieldPath) => Field(fieldPath);
842877

878+
/// Creates a variable reference expression from a variable name.
879+
static Variable variable(String name) => Variable(name);
880+
843881
/// Creates a field reference expression from a FieldPath object
844882
static Field fieldPath(FieldPath fieldPath) => Field(fieldPath.toString());
845883

@@ -1596,6 +1634,52 @@ abstract class Expression implements PipelineSerializable {
15961634
);
15971635
}
15981636

1637+
/// Returns a slice of [array] starting at [offset].
1638+
static Expression arraySliceStatic(
1639+
Expression array,
1640+
Object? offset, [
1641+
Object? length,
1642+
]) {
1643+
return _ArraySliceExpression(
1644+
array,
1645+
_toExpression(offset),
1646+
length == null ? null : _toExpression(length),
1647+
);
1648+
}
1649+
1650+
/// Filters [array] by evaluating [filter] for each element bound to [alias].
1651+
static Expression arrayFilterStatic(
1652+
Expression array,
1653+
String alias,
1654+
BooleanExpression filter,
1655+
) {
1656+
return _ArrayFilterExpression(array, alias, filter);
1657+
}
1658+
1659+
/// Transforms each element of [array] bound to [elementAlias].
1660+
static Expression arrayTransformStatic(
1661+
Expression array,
1662+
String elementAlias,
1663+
Expression transform,
1664+
) {
1665+
return _ArrayTransformExpression(array, elementAlias, null, transform);
1666+
}
1667+
1668+
/// Transforms each element of [array] with both element and index aliases.
1669+
static Expression arrayTransformWithIndexStatic(
1670+
Expression array,
1671+
String elementAlias,
1672+
String indexAlias,
1673+
Expression transform,
1674+
) {
1675+
return _ArrayTransformExpression(
1676+
array,
1677+
elementAlias,
1678+
indexAlias,
1679+
transform,
1680+
);
1681+
}
1682+
15991683
/// Creates a raw/custom function expression
16001684
static Expression rawFunction(
16011685
String name,
@@ -1684,6 +1768,26 @@ class Field extends Selectable {
16841768
}
16851769
}
16861770

1771+
/// Represents a variable reference in a pipeline expression.
1772+
class Variable extends Expression {
1773+
final String variableName;
1774+
1775+
Variable(this.variableName);
1776+
1777+
@override
1778+
String get name => 'variable';
1779+
1780+
@override
1781+
Map<String, dynamic> toMap() {
1782+
return {
1783+
'name': name,
1784+
'args': {
1785+
'name': variableName,
1786+
},
1787+
};
1788+
}
1789+
}
1790+
16871791
/// Represents a null value expression
16881792
class _NullExpression extends Expression {
16891793
_NullExpression();
@@ -2423,6 +2527,92 @@ class _ArraySumExpression extends FunctionExpression {
24232527
}
24242528
}
24252529

2530+
/// Represents an array slice expression.
2531+
class _ArraySliceExpression extends FunctionExpression {
2532+
final Expression expression;
2533+
final Expression offset;
2534+
final Expression? length;
2535+
2536+
_ArraySliceExpression(this.expression, this.offset, this.length);
2537+
2538+
@override
2539+
String get name => 'array_slice';
2540+
2541+
@override
2542+
Map<String, dynamic> toMap() {
2543+
final args = <String, dynamic>{
2544+
'expression': expression.toMap(),
2545+
'offset': offset.toMap(),
2546+
};
2547+
if (length != null) {
2548+
args['length'] = length!.toMap();
2549+
}
2550+
return {
2551+
'name': name,
2552+
'args': args,
2553+
};
2554+
}
2555+
}
2556+
2557+
/// Represents an array filter expression.
2558+
class _ArrayFilterExpression extends FunctionExpression {
2559+
final Expression expression;
2560+
final String alias;
2561+
final BooleanExpression filter;
2562+
2563+
_ArrayFilterExpression(this.expression, this.alias, this.filter);
2564+
2565+
@override
2566+
String get name => 'array_filter';
2567+
2568+
@override
2569+
Map<String, dynamic> toMap() {
2570+
return {
2571+
'name': name,
2572+
'args': {
2573+
'expression': expression.toMap(),
2574+
'alias': alias,
2575+
'filter': filter.toMap(),
2576+
},
2577+
};
2578+
}
2579+
}
2580+
2581+
/// Represents an array transform expression.
2582+
class _ArrayTransformExpression extends FunctionExpression {
2583+
final Expression expression;
2584+
final String elementAlias;
2585+
final String? indexAlias;
2586+
final Expression transform;
2587+
2588+
_ArrayTransformExpression(
2589+
this.expression,
2590+
this.elementAlias,
2591+
this.indexAlias,
2592+
this.transform,
2593+
);
2594+
2595+
@override
2596+
String get name =>
2597+
indexAlias == null ? 'array_transform' : 'array_transform_with_index';
2598+
2599+
@override
2600+
Map<String, dynamic> toMap() {
2601+
final args = <String, dynamic>{
2602+
'expression': expression.toMap(),
2603+
'element_alias': elementAlias,
2604+
'transform': transform.toMap(),
2605+
};
2606+
if (indexAlias != null) {
2607+
args['index_alias'] = indexAlias;
2608+
}
2609+
return {
2610+
'name': name,
2611+
'args': args,
2612+
};
2613+
}
2614+
}
2615+
24262616
// ============================================================================
24272617
// CONDITIONAL / LOGIC OPERATION EXPRESSION CLASSES
24282618
// ============================================================================

0 commit comments

Comments
 (0)