mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
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:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user