Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f1b5b5d
[realppl 3] Arithmetic and comparison expressions
wu-hui Apr 14, 2025
6294868
[realppl 4] Array, debug, field and logical expressions
wu-hui Apr 16, 2025
6c0a698
[realppl 5] map,string,timestamp and mirroring semantics
wu-hui Apr 17, 2025
8efed9e
[realppl 6] offline ppl evaluation and tests
wu-hui Apr 24, 2025
0a8821c
[realppl 7] realppl integration with remote/local and unit tests
wu-hui Apr 30, 2025
f79f655
Remove Fuzzer
wu-hui May 27, 2025
8b8d4a2
[realppl 8] realppl spec tests
wu-hui May 13, 2025
0d65b86
[realppl 9] realppl public api and integration tests
wu-hui Apr 30, 2025
a1ad8f9
[realppl 10] Add server timestamp support
wu-hui Jun 16, 2025
3a10495
[realppl 11] Add query to pipeline support
wu-hui Jul 9, 2025
5a50bd6
Hide realtime pipelines
wu-hui Sep 12, 2025
a54bd45
address API feedbacks
cherylEnkidu Sep 21, 2025
e76d2a2
add documentation
cherylEnkidu Sep 21, 2025
fac2f83
rename Firestore/Swift/Source/ExprImpl.swift
cherylEnkidu Sep 21, 2025
2951a6c
expand expr to expression
cherylEnkidu Sep 25, 2025
afa920e
move files
cherylEnkidu Sep 25, 2025
d77d6d5
move files
cherylEnkidu Sep 25, 2025
18fbbae
move file
cherylEnkidu Sep 25, 2025
7c7eb75
Separate PipelineSource and RealtimePipelineSource
cherylEnkidu Sep 25, 2025
caf8158
add abs()
cherylEnkidu Sep 25, 2025
fb482c7
add arrayReverse
cherylEnkidu Sep 25, 2025
62434c1
add ceil()
cherylEnkidu Sep 25, 2025
d5d5c9d
add collectionId()
cherylEnkidu Sep 26, 2025
11bf771
add countDistinct()
cherylEnkidu Sep 26, 2025
0e39951
[realppl 8] realppl spec tests
wu-hui May 13, 2025
016dac0
[realppl 9] realppl public api and integration tests
wu-hui Apr 30, 2025
00c6256
add exp()
cherylEnkidu Sep 29, 2025
854b6bb
[realppl 10] Add query to pipeline support
wu-hui Jul 9, 2025
814074f
make all bit operations internal
cherylEnkidu Sep 29, 2025
54461f1
add floor()
cherylEnkidu Sep 29, 2025
8e8557f
add length()
cherylEnkidu Sep 29, 2025
ead6684
hide manhattanDistance()
cherylEnkidu Sep 29, 2025
c720527
add ln()
cherylEnkidu Sep 30, 2025
b51b545
Hide realtime pipelines
wu-hui Sep 12, 2025
69ced35
add pow()
cherylEnkidu Sep 30, 2025
3d0a3d3
hide replace expression
cherylEnkidu Sep 30, 2025
617feb4
rename substring
cherylEnkidu Sep 30, 2025
5bc46e3
add more expressions
cherylEnkidu Sep 30, 2025
452875f
make internal impl of Order public
cherylEnkidu Sep 30, 2025
3a258b2
rename timestampSubtract
cherylEnkidu Sep 30, 2025
1c00eea
refactor snapshot
cherylEnkidu Sep 30, 2025
aaa7855
move file
cherylEnkidu Sep 30, 2025
d02c044
remove backpointer to parent
cherylEnkidu Sep 30, 2025
fd8ed65
equivalent()
cherylEnkidu Sep 30, 2025
418e3a6
merge in base branch
cherylEnkidu Sep 30, 2025
e42cce2
merge in base 2
cherylEnkidu Sep 30, 2025
2fb7a14
add concat()
cherylEnkidu Oct 1, 2025
8422b15
add currentTimestamp()
cherylEnkidu Oct 1, 2025
45797e1
hide equivalent
cherylEnkidu Oct 1, 2025
b377edb
add ifAbsent and error
cherylEnkidu Oct 1, 2025
903425b
add join()
cherylEnkidu Oct 1, 2025
50b673e
rename logical
cherylEnkidu Oct 1, 2025
41fcae2
add name tag for FunctionExpression
cherylEnkidu Oct 7, 2025
a4d33e4
add missing tags
cherylEnkidu Oct 9, 2025
91b0dec
add documentations
cherylEnkidu Oct 9, 2025
d8c5c81
add documentations 2
cherylEnkidu Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ profile
DerivedData
*.hmap
*.ipa
# Xcode index build files
.index-build/

# Swift Package Manager
Package.resolved
Expand Down Expand Up @@ -166,3 +168,4 @@ Firestore/Example/GoogleService-Info.plist

# FirebaseVertexAI test data
vertexai-sdk-test-data

177 changes: 73 additions & 104 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Firestore/Example/Tests/Integration/API/FIRAggregateTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,9 @@ - (void)testCannotPerformMoreThanMaxAggregations {
[self awaitExpectation:expectation];

XCTAssertNotNil(result);
XCTAssertTrue([[result localizedDescription] containsString:@"maximum number of aggregations"]);
if (!FSTIntegrationTestCase.isRunningAgainstEmulator) {
XCTAssertTrue([[result localizedDescription] containsString:@"maximum number of aggregations"]);
}
}

- (void)testThrowsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation {
Expand Down
14 changes: 14 additions & 0 deletions Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,18 @@ - (BOOL)shouldRunWithTags:(NSArray<NSString *> *)tags {

@end

/**
* An implementation of FSTLevelDBSpecTests that runs tests in pipeline mode.
*/
@interface FSTLevelDBPipelineSpecTests : FSTLevelDBSpecTests
@end

@implementation FSTLevelDBPipelineSpecTests

- (BOOL)usePipelineMode {
return YES;
}

@end

NS_ASSUME_NONNULL_END
14 changes: 14 additions & 0 deletions Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,18 @@ - (BOOL)shouldRunWithTags:(NSArray<NSString *> *)tags {

@end

/**
* An implementation of FSTMemorySpecTests that runs tests in pipeline mode.
*/
@interface FSTMemoryPipelineSpecTests : FSTMemorySpecTests
@end

@implementation FSTMemoryPipelineSpecTests

- (BOOL)usePipelineMode {
return YES;
}

@end

NS_ASSUME_NONNULL_END
8 changes: 7 additions & 1 deletion Firestore/Example/Tests/SpecTests/FSTSpecTests.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ extern NSString *const kDurablePersistence;
* + Subclass FSTSpecTests
* + override -persistence to create and return an appropriate Persistence implementation.
*/
@interface FSTSpecTests : XCTestCase
@interface FSTSpecTests : XCTestCase {
@protected
BOOL _convertToPipeline;
}

/** Based on its tags, determine whether the test case should run. */
- (BOOL)shouldRunWithTags:(NSArray<NSString *> *)tags;

/** Do any necessary setup for a single spec test */
- (void)setUpForSpecWithConfig:(NSDictionary *)config;

/** Determines if tests should run in pipeline mode. Subclasses can override. */
- (BOOL)usePipelineMode;

@end

NS_ASSUME_NONNULL_END
181 changes: 138 additions & 43 deletions Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
// if `kRunBenchmarkTests` is set to 'YES'.
static NSString *const kBenchmarkTag = @"benchmark";

// A tag for tests that should skip its pipeline run.
static NSString *const kNoPipelineConversion = @"no-pipeline-conversion";

NSString *const kEagerGC = @"eager-gc";

NSString *const kDurablePersistence = @"durable-persistence";
Expand Down Expand Up @@ -236,11 +239,14 @@ - (BOOL)shouldRunWithTags:(NSArray<NSString *> *)tags {
return NO;
} else if (!kRunBenchmarkTests && [tags containsObject:kBenchmarkTag]) {
return NO;
} else if (self.usePipelineMode && [tags containsObject:kNoPipelineConversion]) {
return NO;
}
return YES;
}

- (void)setUpForSpecWithConfig:(NSDictionary *)config {
_convertToPipeline = [self usePipelineMode]; // Call new method
_reader = FSTTestUserDataReader();
std::unique_ptr<Executor> user_executor = Executor::CreateSerial("user executor");
user_executor_ = absl::ShareUniquePtr(std::move(user_executor));
Expand All @@ -261,6 +267,7 @@ - (void)setUpForSpecWithConfig:(NSDictionary *)config {
self.driver =
[[FSTSyncEngineTestDriver alloc] initWithPersistence:std::move(persistence)
eagerGC:_useEagerGCForMemory
convertToPipeline:_convertToPipeline // Pass the flag
initialUser:User::Unauthenticated()
outstandingWrites:{}
maxConcurrentLimboResolutions:_maxConcurrentLimboResolutions];
Expand All @@ -282,6 +289,11 @@ - (BOOL)isTestBaseClass {
return [self class] == [FSTSpecTests class];
}

// Default implementation for pipeline mode. Subclasses can override.
- (BOOL)usePipelineMode {
return NO;
}

#pragma mark - Methods for constructing objects from specs.

- (Query)parseQuery:(id)querySpec {
Expand Down Expand Up @@ -645,6 +657,7 @@ - (void)doRestart {
self.driver =
[[FSTSyncEngineTestDriver alloc] initWithPersistence:std::move(persistence)
eagerGC:_useEagerGCForMemory
convertToPipeline:_convertToPipeline // Pass the flag
initialUser:currentUser
outstandingWrites:outstandingWrites
maxConcurrentLimboResolutions:_maxConcurrentLimboResolutions];
Expand Down Expand Up @@ -721,8 +734,42 @@ - (void)doStep:(NSDictionary *)step {
}

- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected {
Query expectedQuery = [self parseQuery:expected[@"query"]];
XCTAssertEqual(actual.query, expectedQuery);
// The 'expected' query from JSON is always a standard Query.
Query expectedJSONQuery = [self parseQuery:expected[@"query"]];
core::QueryOrPipeline actualQueryOrPipeline = actual.queryOrPipeline;

if (_convertToPipeline) {
XCTAssertTrue(actualQueryOrPipeline.IsPipeline(),
@"In pipeline mode, actual event query should be a pipeline. Actual: %@",
MakeNSString(actualQueryOrPipeline.ToString()));

// Convert the expected JSON Query to a RealtimePipeline for comparison.
std::vector<std::shared_ptr<api::EvaluableStage>> expectedStages =
core::ToPipelineStages(expectedJSONQuery);
// TODO(specstest): Need access to the database_id for the serializer.
// Assuming self.driver.databaseInfo is accessible and provides it.
// This might require making databaseInfo public or providing a getter in
// FSTSyncEngineTestDriver. For now, proceeding with the assumption it's available.
auto serializer = absl::make_unique<remote::Serializer>(self.driver.databaseInfo.database_id());
api::RealtimePipeline expectedPipeline(std::move(expectedStages), std::move(serializer));
auto expectedQoPForComparison =
core::QueryOrPipeline(expectedPipeline); // Wrap expected pipeline

XCTAssertEqual(actualQueryOrPipeline.CanonicalId(), expectedQoPForComparison.CanonicalId(),
@"Pipeline canonical IDs do not match. Actual: %@, Expected: %@",
MakeNSString(actualQueryOrPipeline.CanonicalId()),
MakeNSString(expectedQoPForComparison.CanonicalId()));

} else {
XCTAssertFalse(actualQueryOrPipeline.IsPipeline(),
@"In non-pipeline mode, actual event query should be a Query. Actual: %@",
MakeNSString(actualQueryOrPipeline.ToString()));
XCTAssertTrue(actualQueryOrPipeline.query() == expectedJSONQuery,
@"Queries do not match. Actual: %@, Expected: %@",
MakeNSString(actualQueryOrPipeline.query().ToString()),
MakeNSString(expectedJSONQuery.ToString()));
}

if ([expected[@"errorCode"] integerValue] != 0) {
XCTAssertNotNil(actual.error);
XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]);
Expand Down Expand Up @@ -787,14 +834,43 @@ - (void)validateExpectedSnapshotEvents:(NSArray *_Nullable)expectedEvents {
XCTAssertEqual(events.count, expectedEvents.count);
events =
[events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) {
return WrapCompare(q1.query.CanonicalId(), q2.query.CanonicalId());
}];
expectedEvents = [expectedEvents
sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *left, NSDictionary *right) {
Query leftQuery = [self parseQuery:left[@"query"]];
Query rightQuery = [self parseQuery:right[@"query"]];
return WrapCompare(leftQuery.CanonicalId(), rightQuery.CanonicalId());
// Use QueryOrPipeline's CanonicalId for sorting
return WrapCompare(q1.queryOrPipeline.CanonicalId(), q2.queryOrPipeline.CanonicalId());
}];
expectedEvents = [expectedEvents sortedArrayUsingComparator:^NSComparisonResult(
NSDictionary *left, NSDictionary *right) {
// Expected query from JSON is always a core::Query.
// For sorting consistency with actual events (which might be pipelines),
// we convert the expected query to QueryOrPipeline then get its CanonicalId.
// If _convertToPipeline is true, this will effectively sort expected items
// by their pipeline canonical ID.
Query leftJSONQuery = [self parseQuery:left[@"query"]];
core::QueryOrPipeline leftQoP;
if (self->_convertToPipeline) {
std::vector<std::shared_ptr<api::EvaluableStage>> stages =
core::ToPipelineStages(leftJSONQuery);
auto serializer =
absl::make_unique<remote::Serializer>(self.driver.databaseInfo.database_id());
leftQoP =
core::QueryOrPipeline(api::RealtimePipeline(std::move(stages), std::move(serializer)));
} else {
leftQoP = core::QueryOrPipeline(leftJSONQuery);
}

Query rightJSONQuery = [self parseQuery:right[@"query"]];
core::QueryOrPipeline rightQoP;
if (self->_convertToPipeline) {
std::vector<std::shared_ptr<api::EvaluableStage>> stages =
core::ToPipelineStages(rightJSONQuery);
auto serializer =
absl::make_unique<remote::Serializer>(self.driver.databaseInfo.database_id());
rightQoP =
core::QueryOrPipeline(api::RealtimePipeline(std::move(stages), std::move(serializer)));
} else {
rightQoP = core::QueryOrPipeline(rightJSONQuery);
}
return WrapCompare(leftQoP.CanonicalId(), rightQoP.CanonicalId());
}];

NSUInteger i = 0;
for (; i < expectedEvents.count && i < events.count; ++i) {
Expand Down Expand Up @@ -842,36 +918,49 @@ - (void)validateExpectedState:(nullable NSDictionary *)expectedState {
}
if (expectedState[@"activeTargets"]) {
__block ActiveTargetMap expectedActiveTargets;
[expectedState[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString,
NSDictionary *queryData,
BOOL *) {
TargetId targetID = [targetIDString intValue];
NSArray *queriesJson = queryData[@"queries"];
std::vector<TargetData> queries;
for (id queryJson in queriesJson) {
Query query = [self parseQuery:queryJson];

QueryPurpose purpose = QueryPurpose::Listen;
if ([queryData objectForKey:@"targetPurpose"] != nil) {
purpose = [self parseQueryPurpose:queryData[@"targetPurpose"]];
}

TargetData target_data(core::TargetOrPipeline(query.ToTarget()), targetID, 0, purpose);
if ([queryData objectForKey:@"resumeToken"] != nil) {
target_data = target_data.WithResumeToken(MakeResumeToken(queryData[@"resumeToken"]),
SnapshotVersion::None());
} else {
target_data = target_data.WithResumeToken(ByteString(),
[self parseVersion:queryData[@"readTime"]]);
}

if ([queryData objectForKey:@"expectedCount"] != nil) {
target_data = target_data.WithExpectedCount([queryData[@"expectedCount"] intValue]);
}
queries.push_back(std::move(target_data));
}
expectedActiveTargets[targetID] = std::move(queries);
}];
[expectedState[@"activeTargets"]
enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, NSDictionary *queryData,
BOOL *) {
TargetId targetID = [targetIDString intValue];
NSArray *queriesJson = queryData[@"queries"];
std::vector<TargetData> queries;
for (id queryJson in queriesJson) {
QueryPurpose purpose = QueryPurpose::Listen;
if ([queryData objectForKey:@"targetPurpose"] != nil) {
purpose = [self parseQueryPurpose:queryData[@"targetPurpose"]];
}

core::TargetOrPipeline top;
Query query = [self parseQuery:queryJson];

if (self->_convertToPipeline &&
purpose != firebase::firestore::local::QueryPurpose::LimboResolution) {
std::vector<std::shared_ptr<api::EvaluableStage>> stages =
core::ToPipelineStages(query);
auto serializer =
absl::make_unique<remote::Serializer>(self.driver.databaseInfo.database_id());
top = core::TargetOrPipeline(
api::RealtimePipeline(std::move(stages), std::move(serializer)));
} else {
top = core::TargetOrPipeline(query.ToTarget());
}

TargetData target_data(top, targetID, 0, purpose);
if ([queryData objectForKey:@"resumeToken"] != nil) {
target_data = target_data.WithResumeToken(
MakeResumeToken(queryData[@"resumeToken"]), SnapshotVersion::None());
} else {
target_data = target_data.WithResumeToken(
ByteString(), [self parseVersion:queryData[@"readTime"]]);
}

if ([queryData objectForKey:@"expectedCount"] != nil) {
target_data = target_data.WithExpectedCount([queryData[@"expectedCount"] intValue]);
}
queries.push_back(std::move(target_data));
}
expectedActiveTargets[targetID] = std::move(queries);
}];
[self.driver setExpectedActiveTargets:std::move(expectedActiveTargets)];
}
}
Expand Down Expand Up @@ -980,9 +1069,13 @@ - (void)validateActiveTargets {
// is ever made to be consistent.
// XCTAssertEqualObjects(actualTargets[targetID], TargetData);
const TargetData &actual = found->second;

auto left = actual.target_or_pipeline();
auto right = targetData.target_or_pipeline();
auto left_p = left.IsPipeline();
auto right_p = right.IsPipeline();
XCTAssertEqual(left_p, right_p);
XCTAssertEqual(left, right);
XCTAssertEqual(actual.purpose(), targetData.purpose());
XCTAssertEqual(actual.target_or_pipeline(), targetData.target_or_pipeline());
XCTAssertEqual(actual.target_id(), targetData.target_id());
XCTAssertEqual(actual.snapshot_version(), targetData.snapshot_version());
XCTAssertEqual(actual.resume_token(), targetData.resume_token());
Expand Down Expand Up @@ -1032,6 +1125,8 @@ - (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config {
- (void)testSpecTests {
if ([self isTestBaseClass]) return;

// LogSetLevel(firebase::firestore::util::kLogLevelDebug);

// Enumerate the .json files containing the spec tests.
NSMutableArray<NSString *> *specFiles = [NSMutableArray array];
NSMutableArray<NSDictionary *> *parsedSpecs = [NSMutableArray array];
Expand Down Expand Up @@ -1121,10 +1216,10 @@ - (void)testSpecTests {
++testPassCount;
} else {
++testSkipCount;
NSLog(@" [SKIPPED] Spec test: %@", name);
// NSLog(@" [SKIPPED] Spec test: %@", name);
NSString *comment = testDescription[@"comment"];
if (comment) {
NSLog(@" %@", comment);
// NSLog(@" %@", comment);
}
}
}];
Expand Down
9 changes: 7 additions & 2 deletions Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "Firestore/core/src/api/load_bundle_task.h"
#include "Firestore/core/src/bundle/bundle_reader.h"
#include "Firestore/core/src/core/database_info.h"
#include "Firestore/core/src/core/pipeline_util.h" // For QueryOrPipeline
#include "Firestore/core/src/core/query.h"
#include "Firestore/core/src/core/view_snapshot.h"
#include "Firestore/core/src/credentials/user.h"
Expand Down Expand Up @@ -66,7 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
* given query.
*/
@interface FSTQueryEvent : NSObject
@property(nonatomic, assign) core::Query query;
@property(nonatomic, assign) core::QueryOrPipeline queryOrPipeline;
@property(nonatomic, strong, nullable) NSError *error;

- (const absl::optional<core::ViewSnapshot> &)viewSnapshot;
Expand Down Expand Up @@ -115,7 +116,10 @@ typedef std::
*
* Each method on the driver injects a different event into the system.
*/
@interface FSTSyncEngineTestDriver : NSObject
@interface FSTSyncEngineTestDriver : NSObject {
@protected
BOOL _convertToPipeline;
}

/**
* Initializes the underlying FSTSyncEngine with the given local persistence implementation and
Expand All @@ -124,6 +128,7 @@ typedef std::
*/
- (instancetype)initWithPersistence:(std::unique_ptr<local::Persistence>)persistence
eagerGC:(BOOL)eagerGC
convertToPipeline:(BOOL)convertToPipeline
initialUser:(const credentials::User &)initialUser
outstandingWrites:(const FSTOutstandingWriteQueues &)outstandingWrites
maxConcurrentLimboResolutions:(size_t)maxConcurrentLimboResolutions NS_DESIGNATED_INITIALIZER;
Expand Down
Loading
Loading