Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
4 changes: 4 additions & 0 deletions Firestore/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Unreleased

- [feature] Add support for 16MB documents. TODO(dlarocque): add more details

# 12.15.0

- [fix] Remove use of designated initializers for `forceIndex` [#16229].
Expand Down
155 changes: 155 additions & 0 deletions Firestore/Swift/Tests/Integration/LargeDocIntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import FirebaseFirestore
import Foundation
import XCTest

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
final class LargeDocIntegrationTests: FSTIntegrationTestCase {
// Test data prerequisites populated by the nightly seeder script.
let SEED_COLLECTION = "serverSdkTests"
let DOC_15_9MB_UNICODE = "doc_15_9MB_unicode"
let COL_LARGE_DOCS = "col_large_docs"

override func setUp() async throws {
try await super.setUp()

// Skip by default to prevent slowing down CI.
// Add environment variable FIRESTORE_RUN_LARGE_DOC_TESTS = YES.
let runLargeTests = ProcessInfo.processInfo.environment["FIRESTORE_RUN_LARGE_DOC_TESTS"]
if runLargeTests != "YES" && runLargeTests != "true" {
throw XCTSkip("Skipping large doc tests. Set FIRESTORE_RUN_LARGE_DOC_TESTS=YES to run.")
}

// Skip tests if the backend edition is not supported or if not nightly
if FSTIntegrationTestCase.targetBackend() != .nightly || FSTIntegrationTestCase
.backendEdition() == .standard {
throw XCTSkip("Skipping large document tests because backend is not compatible.")
}
}

// MARK: - Helper Methods

private func generateString(sizeInBytes: Int) -> String {
return String(repeating: "a", count: sizeInBytes)
}

override func collectionRef() -> CollectionReference {
return db.collection(SEED_COLLECTION)
}

// MARK: - Test Cases

func testReadAndCacheLargeUnicodeDocument() async throws {
let docRef = collectionRef().document(DOC_15_9MB_UNICODE)
defer { Task { try? await db.enableNetwork() } }

let serverSnapshot = try await docRef.getDocument(source: .server)
XCTAssertTrue(serverSnapshot.exists)

try await db.disableNetwork()

let cacheSnapshot = try await docRef.getDocument(source: .cache)
XCTAssertTrue(cacheSnapshot.exists)

let serverData = serverSnapshot.data() as NSDictionary?
let cacheData = cacheSnapshot.data() as NSDictionary?
XCTAssertEqual(serverData, cacheData)
}

func testQueryLargeDocumentsForcesLocalScan() async throws {
let colRef = db.collection(COL_LARGE_DOCS)
defer { Task { try? await db.enableNetwork() } }

// Populate cache
_ = try await colRef.document("doc_a").getDocument(source: .server)
_ = try await colRef.document("doc_b").getDocument(source: .server)

try await db.disableNetwork()

// Execute offline query which requires full local index scan
let query = colRef.order(by: FieldPath.documentID()).limit(to: 2)
let cacheSnapshot = try await query.getDocuments(source: .cache)

XCTAssertEqual(cacheSnapshot.documents.count, 2)
if let firstDoc = cacheSnapshot.documents.first {
XCTAssertTrue(firstDoc.data().count > 0)
}
}

func testWatchStreamInitializationAndDiff() async throws {
let docRef = collectionRef().document(DOC_15_9MB_UNICODE)
let expectation = XCTestExpectation(description: "Wait for 15.9MB Watch stream payload")

// Attach listener, must not enter a CANCELLED retry loop
let listener = docRef.addSnapshotListener { snapshot, error in
XCTAssertNil(error)
guard let snapshot = snapshot else { return }

if snapshot.exists {
expectation.fulfill()
}
}

listener.remove()

// TODO(dlarocque): Write streams for large docs are currently restricted in client SDKs.
// Once fixed, update this test to trigger a small field mutation and assert the
// listener fires a second time to test differential payload handling.
}

// NOTE: This test will cause stream errors because writes are not working through watch.
func testOversizedPayloadRejection() async throws {
let docRef = collectionRef().document("temp_oversized_doc")

// Generate ~16.1MB payload
let targetBytes = (16 * 1024 * 1024) + 102_400
let largePayload = generateString(sizeInBytes: targetBytes)

do {
try await docRef.setData(["largeField": largePayload])
XCTFail("Setting a document exceeding the 16MB limit should fail.")
} catch {
let nsError = error as NSError
XCTAssertEqual(nsError.domain, FirestoreErrorDomain)
// Must map to standard InvalidArgument/MongoDB compatibility error, not a crash.
XCTAssertEqual(nsError.code, FirestoreErrorCode.invalidArgument.rawValue)
}
}

func testTransactionReadModifyWrite() async throws {
let docRef = collectionRef().document(DOC_15_9MB_UNICODE)

do {
_ = try await db.runTransaction { transaction, errorPointer -> Any? in
do {
_ = try transaction.getDocument(docRef)
// Minor write to test transaction commit payload limits
transaction.updateData(
["transaction_timestamp": FieldValue.serverTimestamp()],
forDocument: docRef
)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
}
return nil
}
} catch {
XCTFail("Transaction failed with error: \(error)")
}
}
}
5 changes: 5 additions & 0 deletions Firestore/core/src/remote/grpc_connection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ std::shared_ptr<grpc::Channel> GrpcConnection::CreateChannel() const {
// This acts as a failsafe.)
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30 * 1000);

// Increase the max receive message size to 17MB, to support 16MB documents
// + overhead.
const int GRPC_MAX_RECEIVE_MESSAGE_SIZE = 17 * 1024 * 1024;
args.SetMaxReceiveMessageSize(GRPC_MAX_RECEIVE_MESSAGE_SIZE);

const HostConfig* host_config = Config().find(host);
if (!host_config) {
std::string root_certificate = LoadGrpcRootCertificate();
Expand Down
Loading