Skip to content

Commit

Permalink
Improved handling of reads after writes in transactions if the get is…
Browse files Browse the repository at this point in the history
… not awaited or returned.
  • Loading branch information
MarkDuckworth committed Oct 23, 2023
1 parent f002ef3 commit e6f5106
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 8 deletions.
10 changes: 6 additions & 4 deletions packages/firestore/src/core/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class Transaction {
* A deferred usage error that occurred previously in this transaction that
* will cause the transaction to fail once it actually commits.
*/
private lastWriteError: FirestoreError | null = null;
private lastTransactionError: FirestoreError | null = null;

/**
* Set of documents that have been written in the transaction.
Expand All @@ -64,10 +64,12 @@ export class Transaction {
this.ensureCommitNotCalled();

if (this.mutations.length > 0) {
this.lastTransactionError = null;
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Firestore transactions require all reads to be executed before all writes.'
);
throw this.lastTransactionError;
}
const docs = await invokeBatchGetDocumentsRpc(this.datastore, keys);
docs.forEach(doc => this.recordVersion(doc));
Expand All @@ -83,7 +85,7 @@ export class Transaction {
try {
this.write(data.toMutation(key, this.preconditionForUpdate(key)));
} catch (e) {
this.lastWriteError = e as FirestoreError | null;
this.lastTransactionError = e as FirestoreError | null;
}
this.writtenDocs.add(key.toString());
}
Expand All @@ -96,8 +98,8 @@ export class Transaction {
async commit(): Promise<void> {
this.ensureCommitNotCalled();

if (this.lastWriteError) {
throw this.lastWriteError;
if (this.lastTransactionError) {
throw this.lastTransactionError;
}
const unwritten = this.readVersions;
// For each mutation, note that the doc was written.
Expand Down
67 changes: 63 additions & 4 deletions packages/firestore/test/integration/api/transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,10 +506,11 @@ apiDescribe('Database transactions', persistence => {
});
});

it('cannot read after writing', () => {
return withTestDb(persistence, db => {
return runTransaction(db, transaction => {
const docRef = doc(collection(db, 'anything'));
it('cannot read after writing and does not commit', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, '00000-anything'));
await setDoc(docRef, { foo: 'baz' });
await runTransaction(db, async transaction => {
transaction.set(docRef, { foo: 'bar' });
return transaction.get(docRef);
})
Expand All @@ -523,6 +524,64 @@ apiDescribe('Database transactions', persistence => {
'Firestore transactions require all reads to be executed'
);
});

const postSnap = await getDoc(docRef);
expect(postSnap.get('foo')).to.equal('baz');
});
});

it('cannot read after writing and does not commit - get is awaited', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, '00000-anything'));
await setDoc(docRef, { foo: 'baz' });
await runTransaction(db, async transaction => {
transaction.set(docRef, { foo: 'bar' });
return transaction.get(docRef);
})
.then(() => {
expect.fail('transaction should fail');
})
.catch((err: FirestoreError) => {
expect(err).to.exist;
expect(err.code).to.equal('invalid-argument');
expect(err.message).to.contain(
'Firestore transactions require all reads to be executed'
);
});

const postSnap = await getDoc(docRef);
expect(postSnap.get('foo')).to.equal('baz');
});
});

it('cannot read after writing and does not commit, even if the user transaction does not bubble up the error', () => {
return withTestDb(persistence, async db => {
const docRef = doc(collection(db, '00000-anything'));
await setDoc(docRef, { foo: 'baz' });
await runTransaction(db, async transaction => {
transaction.set(docRef, { foo: 'bar' });

// Is this valid, a `transaction.get(...)` without an await or return?
// `transaction.get(...)` returns a promise that must
// be awaited to get the result. If not awaited, then, you haven't done
// anything with the result of the get and the caller's code is invalid.
// If awaited, then the transaction will fail and not commit as expected.
// eslint-disable-next-line
transaction.get(docRef);
})
.then(() => {
expect.fail('transaction should fail');
})
.catch((err: FirestoreError) => {
expect(err).to.exist;
expect(err.code).to.equal('invalid-argument');
expect(err.message).to.contain(
'Firestore transactions require all reads to be executed'
);
});

const postSnap = await getDoc(docRef);
expect(postSnap.get('foo')).to.equal('baz');
});
});

Expand Down

0 comments on commit e6f5106

Please sign in to comment.