From 739600dc7ea7fccbf5dd89c84af0104e3a7ec95f Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 13 May 2026 08:41:23 +0000 Subject: [PATCH 1/5] fix(core, iOS): use namespaced iOS Pigeon header import --- .../ios/firebase_core/Sources/firebase_core/messages.g.m | 2 +- .../firebase_core_platform_interface/pigeons/messages.dart | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m b/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m index c0d58e28a6f3..b1a0a1e69e45 100644 --- a/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m +++ b/packages/firebase_core/firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m @@ -4,7 +4,7 @@ // Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon -#import "messages.g.h" +#import "include/firebase_core/messages.g.h" #if TARGET_OS_OSX @import FlutterMacOS; diff --git a/packages/firebase_core/firebase_core_platform_interface/pigeons/messages.dart b/packages/firebase_core/firebase_core_platform_interface/pigeons/messages.dart index 977270c87dc1..a2286f54f1a9 100644 --- a/packages/firebase_core/firebase_core_platform_interface/pigeons/messages.dart +++ b/packages/firebase_core/firebase_core_platform_interface/pigeons/messages.dart @@ -19,6 +19,9 @@ import 'package:pigeon/pigeon.dart'; '../firebase_core/ios/firebase_core/Sources/firebase_core/include/firebase_core/messages.g.h', objcSourceOut: '../firebase_core/ios/firebase_core/Sources/firebase_core/messages.g.m', + objcOptions: ObjcOptions( + headerIncludePath: 'include/firebase_core/messages.g.h', + ), cppHeaderOut: '../firebase_core/windows/messages.g.h', cppSourceOut: '../firebase_core/windows/messages.g.cpp', cppOptions: CppOptions(namespace: 'firebase_core_windows'), From ae621022434ce3001818607065ff4fcda3dd24b3 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 13 May 2026 15:10:29 +0000 Subject: [PATCH 2/5] feat(firestore): add support for new array expressions --- .../firestore/utils/ExpressionParsers.java | 53 ++++- .../cloud_firestore/FLTPipelineParser.m | 90 +++++++++ .../lib/src/pipeline_expression.dart | 190 ++++++++++++++++++ .../pipeline/pipeline_expressions_e2e.dart | 37 ++++ .../test/pipeline_expression_test.dart | 81 ++++++++ .../lib/src/interop/firestore_interop.dart | 7 + .../src/pipeline_expression_parser_web.dart | 37 ++++ 7 files changed, 493 insertions(+), 2 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java index f27d5fd2a42b..c9f836174dd2 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java @@ -136,6 +136,14 @@ Expression parseExpression(@NonNull Map expressionMap) { } return Expression.field(fieldName); } + case "variable": + { + String variableName = (String) args.get("name"); + if (variableName == null) { + throw new IllegalArgumentException("Variable expression must have a 'name' argument"); + } + return Expression.variable(variableName); + } case "constant": { Object value = args.get("value"); @@ -274,8 +282,49 @@ Expression parseExpression(@NonNull Map expressionMap) { case "array_sum": return Expression.arraySum(parseChild(args, "expression")); case "array_slice": - throw new UnsupportedOperationException( - "Expression type 'array_slice' is not supported on Android Firestore pipeline API"); + { + Expression array = parseChild(args, "expression"); + Expression offset = parseChild(args, "offset"); + Map lengthMap = (Map) args.get("length"); + if (lengthMap == null) { + return array.arraySliceToEnd(offset); + } + return array.arraySlice(offset, parseExpression(lengthMap)); + } + case "array_filter": + { + Expression array = parseChild(args, "expression"); + String alias = (String) args.get("alias"); + Map filterMap = (Map) args.get("filter"); + if (alias == null || filterMap == null) { + throw new IllegalArgumentException("array_filter requires alias and filter"); + } + return array.arrayFilter(alias, parseBooleanExpression(filterMap)); + } + case "array_transform": + { + Expression array = parseChild(args, "expression"); + String elementAlias = (String) args.get("element_alias"); + Map transformMap = (Map) args.get("transform"); + if (elementAlias == null || transformMap == null) { + throw new IllegalArgumentException( + "array_transform requires element_alias and transform"); + } + return array.arrayTransform(elementAlias, parseExpression(transformMap)); + } + case "array_transform_with_index": + { + Expression array = parseChild(args, "expression"); + String elementAlias = (String) args.get("element_alias"); + String indexAlias = (String) args.get("index_alias"); + Map transformMap = (Map) args.get("transform"); + if (elementAlias == null || indexAlias == null || transformMap == null) { + throw new IllegalArgumentException( + "array_transform_with_index requires element_alias, index_alias, and transform"); + } + return array.arrayTransformWithIndex( + elementAlias, indexAlias, parseExpression(transformMap)); + } case "if_absent": { Map exprMap = (Map) args.get("expression"); diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m index cc3d36f510dd..b68e1e9036dd 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m @@ -116,6 +116,16 @@ - (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NS return [[FIRFieldBridge alloc] initWithName:field]; } + if ([name isEqualToString:@"variable"]) { + NSString *variableName = args[@"name"]; + if (![variableName isKindOfClass:[NSString class]] || variableName.length == 0) { + if (error) *error = parseError(@"Variable expression requires 'name' argument"); + return nil; + } + return FLTNewFunctionExprBridge(@"variable", + @[ [[FIRConstantBridge alloc] init:variableName] ]); + } + if ([name isEqualToString:@"constant"]) { id value = args[@"value"]; if (value == nil) { @@ -486,6 +496,86 @@ - (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NS return FLTNewFunctionExprBridge(@"array_concat", all); } + // ------------------------------------------------------------------------- + // expression + offset (+ optional length): array_slice + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"array_slice"]) { + id exprMap = args[@"expression"]; + id offsetMap = args[@"offset"]; + id lengthMap = args[@"length"]; + if (![exprMap isKindOfClass:[NSDictionary class]] || + ![offsetMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"array_slice requires expression and offset"); + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + FIRExprBridge *offset = [self parseExpression:offsetMap error:error]; + if (!expr || !offset) return nil; + NSMutableArray *sliceArgs = + [NSMutableArray arrayWithObjects:expr, offset, nil]; + if ([lengthMap isKindOfClass:[NSDictionary class]]) { + FIRExprBridge *length = [self parseExpression:lengthMap error:error]; + if (!length) return nil; + [sliceArgs addObject:length]; + } + return FLTNewFunctionExprBridge(@"array_slice", sliceArgs); + } + + // ------------------------------------------------------------------------- + // expression + alias + filter: array_filter + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"array_filter"]) { + id exprMap = args[@"expression"]; + NSString *alias = args[@"alias"]; + id filterMap = args[@"filter"]; + if (![exprMap isKindOfClass:[NSDictionary class]] || ![alias isKindOfClass:[NSString class]] || + ![filterMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError(@"array_filter requires expression, alias, and filter"); + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + FIRExprBridge *filter = [self parseBooleanExpression:filterMap error:error]; + if (!expr || !filter) return nil; + return FLTNewFunctionExprBridge(@"array_filter", + @[ expr, [[FIRConstantBridge alloc] init:alias], filter ]); + } + + // ------------------------------------------------------------------------- + // expression + aliases + transform: array_transform / array_transform_with_index + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"array_transform"] || + [name isEqualToString:@"array_transform_with_index"]) { + id exprMap = args[@"expression"]; + NSString *elementAlias = args[@"element_alias"]; + NSString *indexAlias = args[@"index_alias"]; + id transformMap = args[@"transform"]; + BOOL withIndex = [name isEqualToString:@"array_transform_with_index"]; + if (![exprMap isKindOfClass:[NSDictionary class]] || + ![elementAlias isKindOfClass:[NSString class]] || + (withIndex && ![indexAlias isKindOfClass:[NSString class]]) || + ![transformMap isKindOfClass:[NSDictionary class]]) { + if (error) { + NSString *message = + withIndex + ? @"array_transform_with_index requires expression, element_alias, index_alias, " + @"and transform" + : @"array_transform requires expression, element_alias, and transform"; + *error = parseError(message); + } + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + FIRExprBridge *transform = [self parseExpression:transformMap error:error]; + if (!expr || !transform) return nil; + NSMutableArray *transformArgs = + [NSMutableArray arrayWithObjects:expr, [[FIRConstantBridge alloc] init:elementAlias], nil]; + if (withIndex) { + [transformArgs addObject:[[FIRConstantBridge alloc] init:indexAlias]]; + } + [transformArgs addObject:transform]; + return FLTNewFunctionExprBridge(name, transformArgs); + } + // ------------------------------------------------------------------------- // elements[]: array (construct) — Expression.array([...]) from Dart // ------------------------------------------------------------------------- diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart index 4f8f85e49c1e..d7a54f883ffd 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart @@ -701,6 +701,41 @@ abstract class Expression implements PipelineSerializable { return _ArrayIndexOfAllExpression(this, _toExpression(element)); } + /// Returns a slice of this array starting at [offset]. + /// + /// When [length] is provided, at most [length] elements are returned. + Expression arraySlice(Object? offset, [Object? length]) { + return _ArraySliceExpression( + this, + _toExpression(offset), + length == null ? null : _toExpression(length), + ); + } + + /// Filters this array by evaluating [filter] for each element bound to [alias]. + Expression arrayFilter(String alias, BooleanExpression filter) { + return _ArrayFilterExpression(this, alias, filter); + } + + /// Transforms each element of this array bound to [elementAlias]. + Expression arrayTransform(String elementAlias, Expression transform) { + return _ArrayTransformExpression(this, elementAlias, null, transform); + } + + /// Transforms each element of this array with both element and index aliases. + Expression arrayTransformWithIndex( + String elementAlias, + String indexAlias, + Expression transform, + ) { + return _ArrayTransformExpression( + this, + elementAlias, + indexAlias, + transform, + ); + } + // ============================================================================ // AGGREGATE FUNCTIONS // ============================================================================ @@ -840,6 +875,9 @@ abstract class Expression implements PipelineSerializable { /// Creates a field reference expression from a field path string static Field field(String fieldPath) => Field(fieldPath); + /// Creates a variable reference expression from a variable name. + static Variable variable(String name) => Variable(name); + /// Creates a field reference expression from a FieldPath object static Field fieldPath(FieldPath fieldPath) => Field(fieldPath.toString()); @@ -1596,6 +1634,52 @@ abstract class Expression implements PipelineSerializable { ); } + /// Returns a slice of [array] starting at [offset]. + static Expression arraySliceStatic( + Expression array, + Object? offset, [ + Object? length, + ]) { + return _ArraySliceExpression( + array, + _toExpression(offset), + length == null ? null : _toExpression(length), + ); + } + + /// Filters [array] by evaluating [filter] for each element bound to [alias]. + static Expression arrayFilterStatic( + Expression array, + String alias, + BooleanExpression filter, + ) { + return _ArrayFilterExpression(array, alias, filter); + } + + /// Transforms each element of [array] bound to [elementAlias]. + static Expression arrayTransformStatic( + Expression array, + String elementAlias, + Expression transform, + ) { + return _ArrayTransformExpression(array, elementAlias, null, transform); + } + + /// Transforms each element of [array] with both element and index aliases. + static Expression arrayTransformWithIndexStatic( + Expression array, + String elementAlias, + String indexAlias, + Expression transform, + ) { + return _ArrayTransformExpression( + array, + elementAlias, + indexAlias, + transform, + ); + } + /// Creates a raw/custom function expression static Expression rawFunction( String name, @@ -1684,6 +1768,26 @@ class Field extends Selectable { } } +/// Represents a variable reference in a pipeline expression. +class Variable extends Expression { + final String variableName; + + Variable(this.variableName); + + @override + String get name => 'variable'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'name': variableName, + }, + }; + } +} + /// Represents a null value expression class _NullExpression extends Expression { _NullExpression(); @@ -2423,6 +2527,92 @@ class _ArraySumExpression extends FunctionExpression { } } +/// Represents an array slice expression. +class _ArraySliceExpression extends FunctionExpression { + final Expression expression; + final Expression offset; + final Expression? length; + + _ArraySliceExpression(this.expression, this.offset, this.length); + + @override + String get name => 'array_slice'; + + @override + Map toMap() { + final args = { + 'expression': expression.toMap(), + 'offset': offset.toMap(), + }; + if (length != null) { + args['length'] = length!.toMap(); + } + return { + 'name': name, + 'args': args, + }; + } +} + +/// Represents an array filter expression. +class _ArrayFilterExpression extends FunctionExpression { + final Expression expression; + final String alias; + final BooleanExpression filter; + + _ArrayFilterExpression(this.expression, this.alias, this.filter); + + @override + String get name => 'array_filter'; + + @override + Map toMap() { + return { + 'name': name, + 'args': { + 'expression': expression.toMap(), + 'alias': alias, + 'filter': filter.toMap(), + }, + }; + } +} + +/// Represents an array transform expression. +class _ArrayTransformExpression extends FunctionExpression { + final Expression expression; + final String elementAlias; + final String? indexAlias; + final Expression transform; + + _ArrayTransformExpression( + this.expression, + this.elementAlias, + this.indexAlias, + this.transform, + ); + + @override + String get name => + indexAlias == null ? 'array_transform' : 'array_transform_with_index'; + + @override + Map toMap() { + final args = { + 'expression': expression.toMap(), + 'element_alias': elementAlias, + 'transform': transform.toMap(), + }; + if (indexAlias != null) { + args['index_alias'] = indexAlias; + } + return { + 'name': name, + 'args': args, + }; + } +} + // ============================================================================ // CONDITIONAL / LOGIC OPERATION EXPRESSION CLASSES // ============================================================================ diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart index e67e2fbdf0f5..448914f41651 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart @@ -820,6 +820,43 @@ void runPipelineExpressionsTests() { expect(snapshot.result[0].data()!['tags_rev'], ['q', 'p']); }); + test( + 'addFields with new array pipeline expressions returns values', + () async { + final value = Expression.variable('value'); + final index = Expression.variable('index'); + final snapshot = await firestore + .pipeline() + .collection('pipeline-e2e') + .where(Expression.field('test').equalValue('expressions')) + .where(Expression.field('score').equalValue(50)) + .addFields( + Expression.field('arr').arraySlice(1, 2).as('arr_slice'), + Expression.field('arr') + .arrayFilter('value', value.greaterThanValue(3)) + .as('arr_filtered'), + Expression.field('arr') + .arrayTransform('value', value.multiplyValue(10)) + .as('arr_transformed'), + Expression.field('arr') + .arrayTransformWithIndex('value', 'index', value.add(index)) + .as('arr_with_index'), + ) + .limit(1) + .execute(); + + expectResultCount(snapshot, 1); + expectResultsData(snapshot, [ + { + 'arr_slice': [4, 6], + 'arr_filtered': [4, 6], + 'arr_transformed': [20, 40, 60], + 'arr_with_index': [2, 5, 8], + }, + ]); + }, + ); + test( 'arraySum addFields succeeds on Android', () async { diff --git a/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart b/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart index dbd4f1729424..314977c95fe2 100644 --- a/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart +++ b/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart @@ -660,6 +660,87 @@ void main() { expect(expr.toMap()['name'], 'array_reverse'); expect(expr.toMap()['args']['expression']['args']['field'], 'order'); }); + + test('arraySlice serializes correctly', () { + final expr = Field('items').arraySlice(1, Field('count')); + expect(expr.toMap(), { + 'name': 'array_slice', + 'args': { + 'expression': Field('items').toMap(), + 'offset': Constant(1).toMap(), + 'length': Field('count').toMap(), + }, + }); + }); + + test('arraySlice without length serializes correctly', () { + final expr = Field('items').arraySlice(1); + expect(expr.toMap(), { + 'name': 'array_slice', + 'args': { + 'expression': Field('items').toMap(), + 'offset': Constant(1).toMap(), + }, + }); + }); + + test('arrayFilter serializes correctly', () { + final item = Expression.variable('item'); + final expr = Field('scores').arrayFilter( + 'item', + item.greaterThanValue(10), + ); + expect(expr.toMap(), { + 'name': 'array_filter', + 'args': { + 'expression': Field('scores').toMap(), + 'alias': 'item', + 'filter': item.greaterThanValue(10).toMap(), + }, + }); + }); + + test('arrayTransform serializes correctly', () { + final score = Expression.variable('score'); + final expr = Field('scores').arrayTransform( + 'score', + score.multiplyValue(10), + ); + expect(expr.toMap(), { + 'name': 'array_transform', + 'args': { + 'expression': Field('scores').toMap(), + 'element_alias': 'score', + 'transform': score.multiplyValue(10).toMap(), + }, + }); + }); + + test('arrayTransformWithIndex serializes correctly', () { + final score = Expression.variable('score'); + final index = Expression.variable('i'); + final expr = Field('scores').arrayTransformWithIndex( + 'score', + 'i', + score.add(index), + ); + expect(expr.toMap(), { + 'name': 'array_transform_with_index', + 'args': { + 'expression': Field('scores').toMap(), + 'element_alias': 'score', + 'index_alias': 'i', + 'transform': score.add(index).toMap(), + }, + }); + }); + + test('variable serializes correctly', () { + expect(Expression.variable('item').toMap(), { + 'name': 'variable', + 'args': {'name': 'item'}, + }); + }); }); group('Numeric expressions', () { diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart index a4069ebab335..ed2d827e0979 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart @@ -353,6 +353,7 @@ extension type PipelinesJsImpl._(JSObject _) implements JSObject { // --- Expression builders --- external ExpressionJsImpl field(JSString path); + external ExpressionJsImpl variable(JSString name); external ExpressionJsImpl constant(JSAny? value); // --- Boolean / comparison --- @@ -483,6 +484,12 @@ extension type ExpressionJsImpl._(JSObject _) implements JSObject { external ExpressionJsImpl arrayIndexOf(JSAny element); external ExpressionJsImpl arrayLastIndexOf(JSAny element); external ExpressionJsImpl arrayIndexOfAll(JSAny element); + external ExpressionJsImpl arraySlice(JSAny offset, [JSAny? length]); + external ExpressionJsImpl arrayFilter(JSString alias, JSAny filter); + external ExpressionJsImpl arrayTransform( + JSString elementAlias, JSAny transform); + external ExpressionJsImpl arrayTransformWithIndex( + JSString elementAlias, JSString indexAlias, JSAny transform); external ExpressionJsImpl mapSet(JSAny key, JSAny value); external ExpressionJsImpl mapEntries(); } diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart index 722c28095d02..744ec45c9f1c 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart @@ -40,6 +40,8 @@ class PipelineExpressionParserWeb { switch (name) { case 'field': return _pipelines.field(((argsMap[_kField] as String?) ?? '').toJS); + case 'variable': + return _pipelines.variable(((argsMap[_kName] as String?) ?? '').toJS); case 'add': return _binaryArithmetic(argsMap, (l, r) => l.add(r)); case 'subtract': @@ -360,6 +362,41 @@ class PipelineExpressionParserWeb { case 'array_index_of_all': return (_expr(argsMap, _kExpression) as interop.ExpressionJsImpl) .arrayIndexOfAll(_expr(argsMap, 'element')); + case 'array_slice': + { + final base = _expr(argsMap, _kExpression) as interop.ExpressionJsImpl; + final length = argsMap['length']; + if (length == null) { + return base.arraySlice(_expr(argsMap, 'offset')); + } + return base.arraySlice( + _expr(argsMap, 'offset'), + toExpression(length as Map), + ); + } + case 'array_filter': + { + final filter = + toBooleanExpression(argsMap['filter'] as Map); + if (filter == null) { + throw UnsupportedError('array_filter requires a boolean filter'); + } + return (_expr(argsMap, _kExpression) as interop.ExpressionJsImpl) + .arrayFilter((argsMap['alias'] as String).toJS, filter); + } + case 'array_transform': + return (_expr(argsMap, _kExpression) as interop.ExpressionJsImpl) + .arrayTransform( + (argsMap['element_alias'] as String).toJS, + _expr(argsMap, 'transform'), + ); + case 'array_transform_with_index': + return (_expr(argsMap, _kExpression) as interop.ExpressionJsImpl) + .arrayTransformWithIndex( + (argsMap['element_alias'] as String).toJS, + (argsMap['index_alias'] as String).toJS, + _expr(argsMap, 'transform'), + ); default: throw FirebaseException( plugin: 'cloud_firestore', From a48e05d3719f7395fc9e9b916095a0abf311e21f Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 13 May 2026 15:52:11 +0000 Subject: [PATCH 3/5] refactor(firestore): remove Variable class and update array expression aliases --- .../lib/src/pipeline_expression.dart | 37 ++++--------------- .../test/pipeline_expression_test.dart | 23 +++--------- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart index d7a54f883ffd..0690924f2011 100644 --- a/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart +++ b/packages/cloud_firestore/cloud_firestore/lib/src/pipeline_expression.dart @@ -875,9 +875,6 @@ abstract class Expression implements PipelineSerializable { /// Creates a field reference expression from a field path string static Field field(String fieldPath) => Field(fieldPath); - /// Creates a variable reference expression from a variable name. - static Variable variable(String name) => Variable(name); - /// Creates a field reference expression from a FieldPath object static Field fieldPath(FieldPath fieldPath) => Field(fieldPath.toString()); @@ -1768,26 +1765,6 @@ class Field extends Selectable { } } -/// Represents a variable reference in a pipeline expression. -class Variable extends Expression { - final String variableName; - - Variable(this.variableName); - - @override - String get name => 'variable'; - - @override - Map toMap() { - return { - 'name': name, - 'args': { - 'name': variableName, - }, - }; - } -} - /// Represents a null value expression class _NullExpression extends Expression { _NullExpression(); @@ -2531,9 +2508,9 @@ class _ArraySumExpression extends FunctionExpression { class _ArraySliceExpression extends FunctionExpression { final Expression expression; final Expression offset; - final Expression? length; + final Expression? sliceLength; - _ArraySliceExpression(this.expression, this.offset, this.length); + _ArraySliceExpression(this.expression, this.offset, this.sliceLength); @override String get name => 'array_slice'; @@ -2544,8 +2521,8 @@ class _ArraySliceExpression extends FunctionExpression { 'expression': expression.toMap(), 'offset': offset.toMap(), }; - if (length != null) { - args['length'] = length!.toMap(); + if (sliceLength != null) { + args['length'] = sliceLength!.toMap(); } return { 'name': name, @@ -2557,10 +2534,10 @@ class _ArraySliceExpression extends FunctionExpression { /// Represents an array filter expression. class _ArrayFilterExpression extends FunctionExpression { final Expression expression; - final String alias; + final String elementAlias; final BooleanExpression filter; - _ArrayFilterExpression(this.expression, this.alias, this.filter); + _ArrayFilterExpression(this.expression, this.elementAlias, this.filter); @override String get name => 'array_filter'; @@ -2571,7 +2548,7 @@ class _ArrayFilterExpression extends FunctionExpression { 'name': name, 'args': { 'expression': expression.toMap(), - 'alias': alias, + 'alias': elementAlias, 'filter': filter.toMap(), }, }; diff --git a/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart b/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart index 314977c95fe2..067efa4d1cb7 100644 --- a/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart +++ b/packages/cloud_firestore/cloud_firestore/test/pipeline_expression_test.dart @@ -685,44 +685,40 @@ void main() { }); test('arrayFilter serializes correctly', () { - final item = Expression.variable('item'); final expr = Field('scores').arrayFilter( 'item', - item.greaterThanValue(10), + Field('item').greaterThanValue(10), ); expect(expr.toMap(), { 'name': 'array_filter', 'args': { 'expression': Field('scores').toMap(), 'alias': 'item', - 'filter': item.greaterThanValue(10).toMap(), + 'filter': Field('item').greaterThanValue(10).toMap(), }, }); }); test('arrayTransform serializes correctly', () { - final score = Expression.variable('score'); final expr = Field('scores').arrayTransform( 'score', - score.multiplyValue(10), + Field('score').multiplyNumber(10), ); expect(expr.toMap(), { 'name': 'array_transform', 'args': { 'expression': Field('scores').toMap(), 'element_alias': 'score', - 'transform': score.multiplyValue(10).toMap(), + 'transform': Field('score').multiplyNumber(10).toMap(), }, }); }); test('arrayTransformWithIndex serializes correctly', () { - final score = Expression.variable('score'); - final index = Expression.variable('i'); final expr = Field('scores').arrayTransformWithIndex( 'score', 'i', - score.add(index), + Field('score').add(Field('i')), ); expect(expr.toMap(), { 'name': 'array_transform_with_index', @@ -730,17 +726,10 @@ void main() { 'expression': Field('scores').toMap(), 'element_alias': 'score', 'index_alias': 'i', - 'transform': score.add(index).toMap(), + 'transform': Field('score').add(Field('i')).toMap(), }, }); }); - - test('variable serializes correctly', () { - expect(Expression.variable('item').toMap(), { - 'name': 'variable', - 'args': {'name': 'item'}, - }); - }); }); group('Numeric expressions', () { From 6c0e63bb7395ff385281d161a12411a2eed05a5d Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 13 May 2026 16:05:41 +0000 Subject: [PATCH 4/5] refactor(firestore): update pipeline expressions test to use arraySlice and remove unused expressions --- .../pipeline/pipeline_expressions_e2e.dart | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart index 448914f41651..e8770bfcc9c1 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart @@ -821,10 +821,8 @@ void runPipelineExpressionsTests() { }); test( - 'addFields with new array pipeline expressions returns values', + 'addFields with arraySlice returns sliced array', () async { - final value = Expression.variable('value'); - final index = Expression.variable('index'); final snapshot = await firestore .pipeline() .collection('pipeline-e2e') @@ -832,15 +830,6 @@ void runPipelineExpressionsTests() { .where(Expression.field('score').equalValue(50)) .addFields( Expression.field('arr').arraySlice(1, 2).as('arr_slice'), - Expression.field('arr') - .arrayFilter('value', value.greaterThanValue(3)) - .as('arr_filtered'), - Expression.field('arr') - .arrayTransform('value', value.multiplyValue(10)) - .as('arr_transformed'), - Expression.field('arr') - .arrayTransformWithIndex('value', 'index', value.add(index)) - .as('arr_with_index'), ) .limit(1) .execute(); @@ -849,9 +838,6 @@ void runPipelineExpressionsTests() { expectResultsData(snapshot, [ { 'arr_slice': [4, 6], - 'arr_filtered': [4, 6], - 'arr_transformed': [20, 40, 60], - 'arr_with_index': [2, 5, 8], }, ]); }, From afab672375df69b9e257ddb8522d72cfaf90c852 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 14 May 2026 07:37:20 +0000 Subject: [PATCH 5/5] refactor(firestore): remove variable expression handling from multiple platforms --- .../firestore/utils/ExpressionParsers.java | 8 ---- .../cloud_firestore/FLTPipelineParser.m | 10 ----- .../pipeline/pipeline_expressions_e2e.dart | 37 ++++++++----------- .../lib/src/interop/firestore_interop.dart | 1 - .../src/pipeline_expression_parser_web.dart | 2 - 5 files changed, 16 insertions(+), 42 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java index c9f836174dd2..9aeb352a9785 100644 --- a/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java +++ b/packages/cloud_firestore/cloud_firestore/android/src/main/java/io/flutter/plugins/firebase/firestore/utils/ExpressionParsers.java @@ -136,14 +136,6 @@ Expression parseExpression(@NonNull Map expressionMap) { } return Expression.field(fieldName); } - case "variable": - { - String variableName = (String) args.get("name"); - if (variableName == null) { - throw new IllegalArgumentException("Variable expression must have a 'name' argument"); - } - return Expression.variable(variableName); - } case "constant": { Object value = args.get("value"); diff --git a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m index b68e1e9036dd..22449aaf96bf 100644 --- a/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m +++ b/packages/cloud_firestore/cloud_firestore/ios/cloud_firestore/Sources/cloud_firestore/FLTPipelineParser.m @@ -116,16 +116,6 @@ - (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NS return [[FIRFieldBridge alloc] initWithName:field]; } - if ([name isEqualToString:@"variable"]) { - NSString *variableName = args[@"name"]; - if (![variableName isKindOfClass:[NSString class]] || variableName.length == 0) { - if (error) *error = parseError(@"Variable expression requires 'name' argument"); - return nil; - } - return FLTNewFunctionExprBridge(@"variable", - @[ [[FIRConstantBridge alloc] init:variableName] ]); - } - if ([name isEqualToString:@"constant"]) { id value = args[@"value"]; if (value == nil) { diff --git a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart index e8770bfcc9c1..4835839efb89 100644 --- a/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/pipeline_example/integration_test/pipeline/pipeline_expressions_e2e.dart @@ -820,28 +820,23 @@ void runPipelineExpressionsTests() { expect(snapshot.result[0].data()!['tags_rev'], ['q', 'p']); }); - test( - 'addFields with arraySlice returns sliced array', - () async { - final snapshot = await firestore - .pipeline() - .collection('pipeline-e2e') - .where(Expression.field('test').equalValue('expressions')) - .where(Expression.field('score').equalValue(50)) - .addFields( - Expression.field('arr').arraySlice(1, 2).as('arr_slice'), - ) - .limit(1) - .execute(); + test('addFields with arraySlice returns sliced array', () async { + final snapshot = await firestore + .pipeline() + .collection('pipeline-e2e') + .where(Expression.field('test').equalValue('expressions')) + .where(Expression.field('score').equalValue(50)) + .addFields(Expression.field('arr').arraySlice(1, 2).as('arr_slice')) + .limit(1) + .execute(); - expectResultCount(snapshot, 1); - expectResultsData(snapshot, [ - { - 'arr_slice': [4, 6], - }, - ]); - }, - ); + expectResultCount(snapshot, 1); + expectResultsData(snapshot, [ + { + 'arr_slice': [4, 6], + }, + ]); + }); test( 'arraySum addFields succeeds on Android', diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart index ed2d827e0979..cd4ee712c568 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/interop/firestore_interop.dart @@ -353,7 +353,6 @@ extension type PipelinesJsImpl._(JSObject _) implements JSObject { // --- Expression builders --- external ExpressionJsImpl field(JSString path); - external ExpressionJsImpl variable(JSString name); external ExpressionJsImpl constant(JSAny? value); // --- Boolean / comparison --- diff --git a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart index 744ec45c9f1c..3460778faba9 100644 --- a/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart +++ b/packages/cloud_firestore/cloud_firestore_web/lib/src/pipeline_expression_parser_web.dart @@ -40,8 +40,6 @@ class PipelineExpressionParserWeb { switch (name) { case 'field': return _pipelines.field(((argsMap[_kField] as String?) ?? '').toJS); - case 'variable': - return _pipelines.variable(((argsMap[_kName] as String?) ?? '').toJS); case 'add': return _binaryArithmetic(argsMap, (l, r) => l.add(r)); case 'subtract':