feat(firestore): Add support for 16MB documents (#10078)

* feat(firestore): Add support for 16MB documents

* lint fixes

* use writes in integration tests

* consistent seed name

* add setting but not plugged in

* remove setting, settings fix

* lint

* lint

* fix timeout
This commit is contained in:
Daniel La Rocque
2026-07-22 17:06:01 -04:00
committed by GitHub
parent cfed3b8781
commit 34d8ab381c
4 changed files with 285 additions and 4 deletions
@@ -122,7 +122,9 @@ export class GrpcConnection implements Connection {
: grpc.credentials.createInsecure();
const grpcOptions: Record<string, unknown> = {
'grpc-node.flow_control_window':
this.databaseInfo.grpcFlowControlWindow ?? 256 * 1024
this.databaseInfo.grpcFlowControlWindow ?? 256 * 1024,
'grpc.max_receive_message_length': 17 * 1024 * 1024,
'grpc.max_send_message_length': 17 * 1024 * 1024
};
this.cachedStub = new this.firestore.Firestore(
this.databaseInfo.host,
@@ -0,0 +1,273 @@
/**
* @license
* 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 { expect } from 'chai';
import {
collection,
doc,
disableNetwork,
enableNetwork,
getDocFromCache,
getDocFromServer,
getDocsFromCache,
getDocsFromServer,
onSnapshot,
orderBy,
query,
limit,
where,
documentId,
runTransaction,
serverTimestamp,
setDoc,
updateDoc,
deleteDoc
} from '../util/firebase_export';
import { apiDescribe, withTestDb } from '../util/helpers';
import {
getTargetBackend,
getRunEnterpriseTests,
TargetBackend
} from '../util/settings';
apiDescribe('Large Documents', persistence => {
let seedColName: string;
beforeEach(function () {
this.currentTest?.timeout(120_000); // Tests are very slow because large doc reads have very high latency.
});
before(async function () {
this.timeout(180_000); // Tests are very slow because large doc reads have very high latency.
const runLargeTests = process.env.FIRESTORE_RUN_LARGE_DOC_TESTS;
if (runLargeTests !== 'YES' && runLargeTests !== 'true') {
this.skip();
}
if (
getTargetBackend() !== TargetBackend.NIGHTLY ||
!getRunEnterpriseTests()
) {
this.skip();
}
seedColName = `large_doc_tests_js_${Date.now()}`;
await withTestDb(persistence, async db => {
const docRef = doc(collection(db, seedColName), 'doc_15_9MB_unicode');
const snap = await getDocFromServer(docRef);
if (!snap.exists()) {
const targetBytes = Math.floor(15.9 * 1024 * 1024);
const payload = 'a'.repeat(targetBytes);
await setDoc(docRef, { chunk: payload });
await setDoc(doc(collection(db, seedColName), 'doc_a'), {
chunk: payload
});
await setDoc(doc(collection(db, seedColName), 'doc_b'), {
chunk: payload
});
}
});
});
after(async () => {
if (!seedColName) {
return;
}
await withTestDb(persistence, async db => {
try {
await deleteDoc(doc(collection(db, seedColName), 'doc_15_9MB_unicode'));
await deleteDoc(doc(collection(db, seedColName), 'doc_a'));
await deleteDoc(doc(collection(db, seedColName), 'doc_b'));
} catch (e) {
// Suppress cleanup exceptions
}
});
});
it('can read and cache a 15.9MB Unicode document', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, seedColName), 'doc_15_9MB_unicode');
try {
const serverSnapshot = await getDocFromServer(docRef);
expect(serverSnapshot.exists()).to.be.true;
await disableNetwork(db);
const cacheSnapshot = await getDocFromCache(docRef);
expect(cacheSnapshot.exists()).to.be.true;
expect(serverSnapshot.data()).to.deep.equal(cacheSnapshot.data());
} finally {
await enableNetwork(db);
}
});
});
it('cache integrity with multiple large documents', () => {
return withTestDb(persistence, async db => {
const colRef = collection(db, seedColName);
const docA = doc(colRef, 'doc_a');
const docB = doc(colRef, 'doc_b');
try {
await getDocFromServer(docA);
await getDocFromServer(docB);
await disableNetwork(db);
const cacheSnapshotA = await getDocFromCache(docA);
const cacheSnapshotB = await getDocFromCache(docB);
expect(cacheSnapshotA.exists()).to.be.true;
expect(cacheSnapshotB.exists()).to.be.true;
expect(cacheSnapshotA.data()!.chunk).to.exist;
expect(cacheSnapshotB.data()!.chunk).to.exist;
} finally {
await enableNetwork(db);
}
});
});
it('can run watch snapshot listener on a large document', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, seedColName), 'doc_15_9MB_unicode');
let updateReceived = false;
const deferred = new Promise<void>((resolve, reject) => {
const unsubscribe = onSnapshot(
docRef,
snapshot => {
if (snapshot.exists() && snapshot.data()!['differentialField']) {
updateReceived = true;
unsubscribe();
resolve();
}
},
error => {
unsubscribe();
reject(error);
}
);
});
await updateDoc(docRef, { differentialField: 'updated_value' });
await deferred;
expect(updateReceived).to.be.true;
});
});
it('can run transaction read-modify-write on a large document', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, seedColName), 'doc_15_9MB_unicode');
await runTransaction(db, async transaction => {
const snapshot = await transaction.get(docRef);
expect(snapshot.exists()).to.be.true;
transaction.update(docRef, {
// eslint-disable-next-line camelcase
transaction_timestamp: serverTimestamp()
});
});
});
});
it('can query large documents', () => {
return withTestDb(persistence, async db => {
const colRef = collection(db, seedColName);
const q = query(colRef, where(documentId(), 'in', ['doc_a', 'doc_b']));
try {
const serverSnapshot = await getDocsFromServer(q);
expect(serverSnapshot.size).to.equal(2);
await disableNetwork(db);
const cacheSnapshot = await getDocsFromCache(q);
expect(cacheSnapshot.size).to.equal(2);
expect(serverSnapshot.docs[0].data()).to.deep.equal(
cacheSnapshot.docs[0].data()
);
} finally {
await enableNetwork(db);
}
});
});
it('query large documents forces local scan', () => {
return withTestDb(persistence, async db => {
const colRef = collection(db, seedColName);
const docA = doc(colRef, 'doc_a');
const docB = doc(colRef, 'doc_b');
try {
await getDocFromServer(docA);
await getDocFromServer(docB);
await disableNetwork(db);
const q = query(colRef, orderBy(documentId()), limit(2));
const cacheSnapshot = await getDocsFromCache(q);
expect(cacheSnapshot.size).to.equal(2);
expect(cacheSnapshot.docs[0].data()!.chunk).to.exist;
} finally {
await enableNetwork(db);
}
});
});
it('gracefully rejects oversized payloads', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, seedColName), 'temp_oversized_doc');
const targetBytes = 16 * 1024 * 1024 + 102400;
const largePayload = 'a'.repeat(targetBytes);
try {
await setDoc(docRef, { chunk: largePayload });
throw new Error(
'Setting a document exceeding the 16MB limit should fail.'
);
} catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((error as any).code).to.equal('invalid-argument');
}
});
});
it('can write a 15.9MB document', () => {
return withTestDb(persistence, async db => {
const tempDocId = 'temp_valid_large_doc_' + Date.now();
const docRef = doc(collection(db, seedColName), tempDocId);
const targetBytes = Math.floor(15.9 * 1024 * 1024);
const largePayload = 'a'.repeat(targetBytes);
try {
await setDoc(docRef, { chunk: largePayload });
const snapshot = await getDocFromServer(docRef);
expect(snapshot.exists()).to.be.true;
expect(snapshot.data()!.chunk.length).to.equal(targetBytes);
} finally {
try {
await deleteDoc(docRef);
} catch (e) {
// Suppress cleanup exceptions
}
}
});
});
});
@@ -128,7 +128,9 @@ function getSslEnabled(targetBackend: TargetBackend): boolean {
export const DEFAULT_PROJECT_ID = USE_EMULATOR
? process.env.FIRESTORE_EMULATOR_PROJECT_ID || 'test-emulator'
: PROJECT_CONFIG.projectId;
: process.env.FIRESTORE_PROJECT_ID ||
process.env.GCLOUD_PROJECT ||
PROJECT_CONFIG.projectId;
export const ALT_PROJECT_ID = 'test-db2';
export const COMPOSITE_INDEX_TEST_COLLECTION =
@@ -136,7 +136,9 @@ describe('GrpcConnection', () => {
expect(spyConstructor.calledOnce).to.be.true;
const options = spyConstructor.firstCall.args[2];
expect(options).to.deep.equal({
'grpc-node.flow_control_window': 256 * 1024
'grpc-node.flow_control_window': 256 * 1024,
'grpc.max_receive_message_length': 17 * 1024 * 1024,
'grpc.max_send_message_length': 17 * 1024 * 1024
});
});
@@ -176,7 +178,9 @@ describe('GrpcConnection', () => {
expect(spyConstructor.calledOnce).to.be.true;
const options = spyConstructor.firstCall.args[2];
expect(options).to.deep.equal({
'grpc-node.flow_control_window': 512 * 1024
'grpc-node.flow_control_window': 512 * 1024,
'grpc.max_receive_message_length': 17 * 1024 * 1024,
'grpc.max_send_message_length': 17 * 1024 * 1024
});
});
});