From d5c3f771af2bc464846039ad2edebdaea05bfff0 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 29 Jul 2026 09:39:11 +0000 Subject: [PATCH 1/2] feat(firestore): add string manipulation functions for Android and iOS --- .../firestore/utils/ExpressionParsers.java | 36 +++++++++++ .../cloud_firestore/FLTPipelineParser.m | 64 +++++++++++++++++++ .../pipeline/pipeline_expressions_e2e.dart | 3 +- 3 files changed, 101 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 c58d8a6d833c..e9326e1586f9 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 @@ -244,6 +244,42 @@ Expression parseExpression(@NonNull Map expressionMap) { Map delimiterMap = (Map) args.get("delimiter"); return Expression.join(parseExpression(arrayMap), parseExpression(delimiterMap)); } + case "string_index_of": + return parseChild(args, "expression").stringIndexOf(parseChild(args, "search")); + case "string_repeat": + return parseChild(args, "expression").stringRepeat(parseChild(args, "repetitions")); + case "string_replace_one": + { + Expression expression = parseChild(args, "expression"); + Expression find = parseChild(args, "find"); + Expression replacement = parseChild(args, "replacement"); + return expression.stringReplaceOne(find, replacement); + } + case "string_replace_all": + { + Expression expression = parseChild(args, "expression"); + Expression find = parseChild(args, "find"); + Expression replacement = parseChild(args, "replacement"); + return expression.stringReplaceAll(find, replacement); + } + case "ltrim": + { + Expression expression = parseChild(args, "expression"); + Map valueMap = (Map) args.get("value"); + if (valueMap == null) { + return expression.ltrim(); + } + return expression.ltrimValue(parseExpression(valueMap)); + } + case "rtrim": + { + Expression expression = parseChild(args, "expression"); + Map valueMap = (Map) args.get("value"); + if (valueMap == null) { + return expression.rtrim(); + } + return expression.rtrimValue(parseExpression(valueMap)); + } case "abs": return Expression.abs(parseChild(args, "expression")); case "negate": 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 4cb699fd76db..ed2838214342 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 @@ -437,6 +437,70 @@ - (FIRExprBridge *)parseExpression:(NSDictionary *)map error:(NS return FLTNewFunctionExprBridge(@"string_replace", @[ expr, find, replacement ]); } + // ------------------------------------------------------------------------- + // expression + find + replacement: string_replace_one / string_replace_all + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"string_replace_one"] || + [name isEqualToString:@"string_replace_all"]) { + id exprMap = args[@"expression"]; + id findMap = args[@"find"]; + id replacementMap = args[@"replacement"]; + if (![exprMap isKindOfClass:[NSDictionary class]] || + ![findMap isKindOfClass:[NSDictionary class]] || + ![replacementMap isKindOfClass:[NSDictionary class]]) { + if (error) + *error = parseError( + [NSString stringWithFormat:@"%@ requires expression, find, and replacement", name]); + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + FIRExprBridge *find = [self parseExpression:findMap error:error]; + FIRExprBridge *replacement = [self parseExpression:replacementMap error:error]; + if (!expr || !find || !replacement) return nil; + return FLTNewFunctionExprBridge(name, @[ expr, find, replacement ]); + } + + // ------------------------------------------------------------------------- + // expression + search/repetitions: string_index_of / string_repeat + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"string_index_of"] || [name isEqualToString:@"string_repeat"]) { + id exprMap = args[@"expression"]; + NSString *argumentName = [name isEqualToString:@"string_index_of"] ? @"search" : @"repetitions"; + id argumentMap = args[argumentName]; + if (![exprMap isKindOfClass:[NSDictionary class]] || + ![argumentMap isKindOfClass:[NSDictionary class]]) { + if (error) + *error = parseError( + [NSString stringWithFormat:@"%@ requires expression and %@", name, argumentName]); + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + FIRExprBridge *argument = [self parseExpression:argumentMap error:error]; + if (!expr || !argument) return nil; + return FLTNewFunctionExprBridge(name, @[ expr, argument ]); + } + + // ------------------------------------------------------------------------- + // expression + optional value: ltrim / rtrim + // ------------------------------------------------------------------------- + if ([name isEqualToString:@"ltrim"] || [name isEqualToString:@"rtrim"]) { + id exprMap = args[@"expression"]; + if (![exprMap isKindOfClass:[NSDictionary class]]) { + if (error) *error = parseError([NSString stringWithFormat:@"%@ requires expression", name]); + return nil; + } + FIRExprBridge *expr = [self parseExpression:exprMap error:error]; + if (!expr) return nil; + + id valueMap = args[@"value"]; + if (![valueMap isKindOfClass:[NSDictionary class]]) { + return FLTNewFunctionExprBridge(name, @[ expr ]); + } + FIRExprBridge *value = [self parseExpression:valueMap error:error]; + if (!value) return nil; + return FLTNewFunctionExprBridge(name, @[ expr, value ]); + } + // ------------------------------------------------------------------------- // expression + delimiter: split, join (SDK: split, join) // ------------------------------------------------------------------------- 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 ac854a460ac2..827afed3e150 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 @@ -930,7 +930,6 @@ void runPipelineExpressionsTests() { {'s1': 'Zy', 'iy': 1, 's2': 'xyxy'}, ]); }, - skip: !kIsWeb, ); test('addFields ltrim rtrim on padded s', () async { @@ -949,7 +948,7 @@ void runPipelineExpressionsTests() { expectResultsData(snapshot, [ {'lt': 'AbC ', 'rt': ' AbC'}, ]); - }, skip: !kIsWeb); + }); test('addFields mapSet and mapEntries on m', () async { final snapshot = await firestore From 9e745e2b3ee68b88eacd9882d02b1236911e3aea Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 29 Jul 2026 13:01:14 +0000 Subject: [PATCH 2/2] chore: add e2e test for string_replace_all --- .../pipeline/pipeline_expressions_e2e.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 827afed3e150..c72249ff0460 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 @@ -932,6 +932,23 @@ void runPipelineExpressionsTests() { }, ); + test('addFields stringReplaceAll replaces every match on s', () async { + final snapshot = await firestore + .pipeline() + .collection('pipeline-e2e') + .where(Expression.field('test').equalValue('expressions')) + .where(Expression.field('score').equalValue(50)) + .addFields( + Expression.field('s').stringReplaceAllLiteral('-', '_').as('s_all'), + ) + .limit(1) + .execute(); + expectResultCount(snapshot, 1); + expectResultsData(snapshot, [ + {'s_all': 'a_b_c'}, + ]); + }); + test('addFields ltrim rtrim on padded s', () async { final snapshot = await firestore .pipeline()