Skip to content

Commit b993aee

Browse files
authored
Firestore: Fix FAILED_PRECONDITION when writing to a deleted document in a transaction (#6550)
1 parent 85b2320 commit b993aee

File tree

4 files changed

+102
-7
lines changed

4 files changed

+102
-7
lines changed

.changeset/smart-plants-sparkle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore': patch
3+
---
4+
5+
Fix FAILED_PRECONDITION when writing to a deleted document in a transaction (#5871)

packages/firestore/src/core/transaction.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class Transaction {
120120
if (doc.isFoundDocument()) {
121121
docVersion = doc.version;
122122
} else if (doc.isNoDocument()) {
123-
// For deleted docs, we must use baseVersion 0 when we overwrite them.
123+
// Represent a deleted doc using SnapshotVersion.min().
124124
docVersion = SnapshotVersion.min();
125125
} else {
126126
throw fail('Document in a transaction was a ' + doc.constructor.name);
@@ -147,7 +147,11 @@ export class Transaction {
147147
private precondition(key: DocumentKey): Precondition {
148148
const version = this.readVersions.get(key.toString());
149149
if (!this.writtenDocs.has(key.toString()) && version) {
150-
return Precondition.updateTime(version);
150+
if (version.isEqual(SnapshotVersion.min())) {
151+
return Precondition.exists(false);
152+
} else {
153+
return Precondition.updateTime(version);
154+
}
151155
} else {
152156
return Precondition.none();
153157
}

packages/firestore/test/integration/api/transactions.test.ts

+73-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
QueryDocumentSnapshot,
2424
Transaction,
2525
collection,
26+
deleteDoc,
2627
doc,
2728
DocumentReference,
2829
DocumentSnapshot,
@@ -87,6 +88,16 @@ apiDescribe('Database transactions', (persistence: boolean) => {
8788
await transaction.get(docRef);
8889
}
8990

91+
enum FromDocumentType {
92+
// The operation will be performed on a document that exists.
93+
EXISTING = 'existing',
94+
// The operation will be performed on a document that has never existed.
95+
NON_EXISTENT = 'non_existent',
96+
// The operation will be performed on a document that existed, but was
97+
// deleted.
98+
DELETED = 'deleted'
99+
}
100+
90101
/**
91102
* Used for testing that all possible combinations of executing transactions
92103
* result in the desired document value or error.
@@ -101,16 +112,21 @@ apiDescribe('Database transactions', (persistence: boolean) => {
101112
constructor(readonly db: Firestore) {}
102113

103114
private docRef!: DocumentReference;
104-
private fromExistingDoc: boolean = false;
115+
private fromDocumentType: FromDocumentType = FromDocumentType.NON_EXISTENT;
105116
private stages: TransactionStage[] = [];
106117

107118
withExistingDoc(): this {
108-
this.fromExistingDoc = true;
119+
this.fromDocumentType = FromDocumentType.EXISTING;
109120
return this;
110121
}
111122

112123
withNonexistentDoc(): this {
113-
this.fromExistingDoc = false;
124+
this.fromDocumentType = FromDocumentType.NON_EXISTENT;
125+
return this;
126+
}
127+
128+
withDeletedDoc(): this {
129+
this.fromDocumentType = FromDocumentType.DELETED;
114130
return this;
115131
}
116132

@@ -176,8 +192,19 @@ apiDescribe('Database transactions', (persistence: boolean) => {
176192

177193
private async prepareDoc(): Promise<void> {
178194
this.docRef = doc(collection(this.db, 'tester-docref'));
179-
if (this.fromExistingDoc) {
180-
await setDoc(this.docRef, { foo: 'bar0' });
195+
switch (this.fromDocumentType) {
196+
case FromDocumentType.EXISTING:
197+
await setDoc(this.docRef, { foo: 'bar0' });
198+
break;
199+
case FromDocumentType.NON_EXISTENT:
200+
// Nothing to do; document does not exist.
201+
break;
202+
case FromDocumentType.DELETED:
203+
await setDoc(this.docRef, { foo: 'bar0' });
204+
await deleteDoc(this.docRef);
205+
break;
206+
default:
207+
throw new Error(`invalid fromDocumentType: ${this.fromDocumentType}`);
181208
}
182209
}
183210

@@ -289,6 +316,47 @@ apiDescribe('Database transactions', (persistence: boolean) => {
289316
});
290317
});
291318

319+
// This test is identical to the test above, except that withNonexistentDoc()
320+
// is replaced by withDeletedDoc(), to guard against regression of
321+
// https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions
322+
// would incorrectly fail with FAILED_PRECONDITION when operations were
323+
// performed on a deleted document (rather than a non-existent document).
324+
it('runs transactions after getting a deleted document', async () => {
325+
return withTestDb(persistence, async db => {
326+
const tt = new TransactionTester(db);
327+
328+
await tt.withDeletedDoc().run(get, delete1, delete1).expectNoDoc();
329+
await tt
330+
.withDeletedDoc()
331+
.run(get, delete1, update2)
332+
.expectError('invalid-argument');
333+
await tt
334+
.withDeletedDoc()
335+
.run(get, delete1, set2)
336+
.expectDoc({ foo: 'bar2' });
337+
338+
await tt
339+
.withDeletedDoc()
340+
.run(get, update1, delete1)
341+
.expectError('invalid-argument');
342+
await tt
343+
.withDeletedDoc()
344+
.run(get, update1, update2)
345+
.expectError('invalid-argument');
346+
await tt
347+
.withDeletedDoc()
348+
.run(get, update1, set1)
349+
.expectError('invalid-argument');
350+
351+
await tt.withDeletedDoc().run(get, set1, delete1).expectNoDoc();
352+
await tt
353+
.withDeletedDoc()
354+
.run(get, set1, update2)
355+
.expectDoc({ foo: 'bar2' });
356+
await tt.withDeletedDoc().run(get, set1, set2).expectDoc({ foo: 'bar2' });
357+
});
358+
});
359+
292360
it('runs transactions on existing document', async () => {
293361
return withTestDb(persistence, async db => {
294362
const tt = new TransactionTester(db);

packages/firestore/test/lite/integration.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,24 @@ describe('Transaction', () => {
581581
});
582582
});
583583

584+
// This test is identical to the test above, except that a non-existent
585+
// document is replaced by a deleted document, to guard against regression of
586+
// https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions
587+
// would incorrectly fail with FAILED_PRECONDITION when operations were
588+
// performed on a deleted document (rather than a non-existent document).
589+
it('can read deleted doc then write', () => {
590+
return withTestDocAndInitialData({ counter: 1 }, async doc => {
591+
await deleteDoc(doc);
592+
await runTransaction(doc.firestore, async transaction => {
593+
const snap = await transaction.get(doc);
594+
expect(snap.exists()).to.be.false;
595+
transaction.set(doc, { counter: 1 });
596+
});
597+
const result = await getDoc(doc);
598+
expect(result.get('counter')).to.equal(1);
599+
});
600+
});
601+
584602
it('retries when document is modified', () => {
585603
return withTestDoc(async doc => {
586604
let retryCounter = 0;

0 commit comments

Comments
 (0)