Skip to content

Commit

Permalink
repeatable read isolation level
Browse files Browse the repository at this point in the history
the same as read committed but with the following anomaly not allowed:
P2 (“Non-repeatable read”): SQL-transaction T1 reads a row. SQL-transaction T2 then modifies or deletes that row and performs a COMMIT.
If T1 then attempts to reread the row, it may receive the modified value or discover that the row has been deleted. Thus breaking the consistency guarantee from ACID properties.
  • Loading branch information
mukeshjc committed Jan 18, 2025
1 parent 62a17b9 commit 63f94f7
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 4 deletions.
71 changes: 71 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,74 @@ func TestReadCommitted(t *testing.T) {
utils.AssertEq(res, "", "c4 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c4 get x")
}

func TestRepeatableRead(t *testing.T) {
database := mvcc.NewDatabase(mvcc.RepeatableReadIsolation)

c1 := database.NewConnection()
c1.MustExecCommand("begin", nil)

c2 := database.NewConnection()
c2.MustExecCommand("begin", nil)

// local change is visible locally
c1.MustExecCommand("set", []string{"x", "hey"})
res := c1.MustExecCommand("get", []string{"x"})
utils.AssertEq(res, "hey", "c1 get x")

// update not available to this transaction since it is not committed
res, err := c2.ExecCommand("get", []string{"x"})
utils.AssertEq(res, "", "c2 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")

c1.MustExecCommand("commit", nil)

// even after committing the update isn't visible because c1 was in-progress when c2 began
res, err = c2.ExecCommand("get", []string{"x"})
utils.AssertEq(res, "", "c2 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")

// but is available in a new transaction
c3 := database.NewConnection()
c3.MustExecCommand("begin", nil)

res = c3.MustExecCommand("get", []string{"x"})
utils.AssertEq(res, "hey", "c3 get x")

// local change is visible locally
c3.MustExecCommand("set", []string{"x", "yall"})
res = c3.MustExecCommand("get", []string{"x"})
utils.AssertEq(res, "yall", "c3 get x")

// But not on the other connection, again.
res, err = c2.ExecCommand("get", []string{"x"})
utils.AssertEq(res, "", "c2 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")

c3.MustExecCommand("rollback", nil)

// And still not, regardless of rollback, because it's an older
// transaction.
res, err = c2.ExecCommand("get", []string{"x"})
utils.AssertEq(res, "", "c2 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")

// And again the rollbacked set is still not on a new transaction.
c4 := database.NewConnection()
res = c4.MustExecCommand("begin", nil)

res = c4.MustExecCommand("get", []string{"x"})
utils.AssertEq(res, "hey", "c4 get x")

c4.MustExecCommand("delete", []string{"x"})
c4.MustExecCommand("commit", nil)

// But the delete is visible to new transactions now that this
// has been committed.
c5 := database.NewConnection()
res = c5.MustExecCommand("begin", nil)

res, err = c5.ExecCommand("get", []string{"x"})
utils.AssertEq(res, "", "c5 get x")
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c5 get x")
}
49 changes: 45 additions & 4 deletions mvcc/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
return false
}

// ... by other transaction and it is committed, then it's no good.
if value.txEndId > 0 && d.transactionState(value.txEndId).state == CommittedTransaction {
// ... by other transaction that is committed, then it's no good.
if d.transactionState(value.txEndId).state == CommittedTransaction {
return false
}
}
Expand All @@ -131,6 +131,47 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
// Another transaction B may have committed changes between two statements in this transaction A.
}

utils.Assert(false, "unsupported isolation level")
return false
// Repeatable Read, Snapshot Isolation and Serializable further restricts Read Committed so only versions from transactions that completed before this one started are visible.
// we will add additional checks for the Read Committed logic that make sure the value was not created and not deleted within a transaction that started before this transaction started.
// As it happens, this is the same logic that will be necessary for Snapshot Isolation and Serializable Isolation.
// The additional logic (that makes Snapshot Isolation and Serializable Isolation different) happens at commit time.

utils.Assert(t.isolation == RepeatableReadIsolation || t.isolation == SnapshotIsolation || t.isolation == SerializableIsolation, "unsupported isolation level")

////// now the specifics for a RepeatableReadIsolation level and above, rest of the checks for stricter isolation levels happens at Commit Time.

// ignore values from transactions started after the current one
if value.txStartId > t.id {
return false
}

// ignore values created from transactions in-progress i.e. ongoing when this transaction began but may have committed when this transaction was in progress.
// if we didn't check for this, then our current transaction may have performed some reads at the beginning, then an in-progress transaction committed and if we made
// another read, we might see the values because now that would be a committed transaction as per ReadCommittedIsolation level. Thus it would be a dirty read and violate
// RepeatableReadIsolation guarantee.
if t.inprogress.Contains(value.txStartId) {
return false
}

////// a copy of all checks we did for ReadUncommittedIsolation is below with slight **MODIFICATION** to the second statement in the bigger IF block

// If the value wasn't created by current transaction and the other transaction that created it isn't committed yet, then it's no good.
if value.txStartId != t.id && d.transactionState(value.txStartId).state != CommittedTransaction {
return false
}

// If the value was deleted ...
if value.txEndId > 0 {
// ... in the current transaction, then it's no good
if value.txEndId == t.id {
return false
}

// ... by other transaction **that began before the current one** and it is committed, then it's no good.
if value.txEndId < t.id && d.transactionState(value.txEndId).state == CommittedTransaction {
return false
}
}

return true
}

0 comments on commit 63f94f7

Please sign in to comment.