feat: SQLite pager state machine spec (speckl#35)
This commit is contained in:
parent
45d82e2ea0
commit
c12f40ee7d
1 changed files with 276 additions and 0 deletions
276
examples/SQLitePager.speck
Normal file
276
examples/SQLitePager.speck
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
// SQLite Pager State Machine Specification (speckl#35)
|
||||||
|
//
|
||||||
|
// Models the pager storage engine's state machine: page cache, transactions,
|
||||||
|
// journal files, and crash recovery. Covers the core state transitions that
|
||||||
|
// TH3 tests but doesn't formally verify.
|
||||||
|
//
|
||||||
|
// Based on: docs/db-sqlite-reference.md Section 1
|
||||||
|
// Scope: Pager state machine, journal modes, lock escalation, error recovery
|
||||||
|
// Does NOT cover: WAL mode (separate spec), VDBE, B-Tree internal structure
|
||||||
|
|
||||||
|
speck SQLitePager {
|
||||||
|
author: "Engineering (agent-engineering) <engineering@marcus.local>"
|
||||||
|
source: architecture_review ref "SQLite-Pager-Core"
|
||||||
|
review: hybrid
|
||||||
|
derives from docs/db-sqlite-reference.md via "Section 1: Pager State Machine"
|
||||||
|
satisfies speckl#35
|
||||||
|
|
||||||
|
state {
|
||||||
|
// Core pager state: mirrors pager.c PAGER_* enum
|
||||||
|
pagerState: String
|
||||||
|
|
||||||
|
// Page cache counters
|
||||||
|
pageCount: Nat
|
||||||
|
dirtyPageCount: Nat
|
||||||
|
|
||||||
|
// Journal tracking
|
||||||
|
journalMode: String
|
||||||
|
journalExists: Bool
|
||||||
|
syncNeeded: Bool
|
||||||
|
|
||||||
|
// Transaction tracking
|
||||||
|
activeTransaction: Bool
|
||||||
|
transactionType: String
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
errorState: Bool
|
||||||
|
errCode: Nat
|
||||||
|
|
||||||
|
// Lock management
|
||||||
|
currentLock: String
|
||||||
|
readOnly: Bool
|
||||||
|
|
||||||
|
// Dirty page set: pageId → isDirty
|
||||||
|
dirtyPages: Set(Nat)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
pagerState == "Idle"
|
||||||
|
pageCount == 0
|
||||||
|
dirtyPageCount == 0
|
||||||
|
journalMode == "Delete"
|
||||||
|
journalExists == false
|
||||||
|
syncNeeded == false
|
||||||
|
activeTransaction == false
|
||||||
|
transactionType == "None"
|
||||||
|
errorState == false
|
||||||
|
errCode == 0
|
||||||
|
currentLock == "NO_LOCK"
|
||||||
|
readOnly == false
|
||||||
|
dirtyPages == emptySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Safety Invariants ---
|
||||||
|
|
||||||
|
invariant CommitAtomicity {
|
||||||
|
// After commit, all dirty pages are flushed and journal is deleted.
|
||||||
|
// The commit point (journal deletion) atomically transitions the database
|
||||||
|
// from pre-commit to post-commit state.
|
||||||
|
dirtyPageCount >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant RollbackSafety {
|
||||||
|
// After rollback completes, no dirty pages remain.
|
||||||
|
// The database matches the pre-transaction state.
|
||||||
|
dirtyPageCount == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant JournalConsistency {
|
||||||
|
// For DELETE mode, journal exists iff write transaction is in progress.
|
||||||
|
journalExists == true implies activeTransaction == true
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant PageCacheCoherence {
|
||||||
|
// Every dirty page belongs to the current active transaction.
|
||||||
|
activeTransaction == true and dirtyPageCount > 0 implies pagerState != "Idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant LockProgression {
|
||||||
|
// Locks only escalate in the defined order.
|
||||||
|
// Valid lock values: NO_LOCK < SHARED < RESERVED < PENDING < EXCLUSIVE.
|
||||||
|
currentLock != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant ErrorRecovery {
|
||||||
|
// Once in error state, no new transactions can start.
|
||||||
|
errorState == true implies activeTransaction == false
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Actions (8 core operations) ---
|
||||||
|
|
||||||
|
action OpenDatabase(filename: String) {
|
||||||
|
require pagerState == "Idle"
|
||||||
|
require errorState == false
|
||||||
|
|
||||||
|
// Initialize pager, allocate page cache
|
||||||
|
pagerState := "Idle"
|
||||||
|
pageCount := 0
|
||||||
|
dirtyPageCount := 0
|
||||||
|
journalExists := false
|
||||||
|
syncNeeded := false
|
||||||
|
activeTransaction := false
|
||||||
|
transactionType := "None"
|
||||||
|
currentLock := "NO_LOCK"
|
||||||
|
dirtyPages := emptySet
|
||||||
|
|
||||||
|
emit DatabaseOpened { filename: filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
action BeginReadTransaction() {
|
||||||
|
require pagerState == "Idle"
|
||||||
|
require errorState == false
|
||||||
|
require readOnly == false or currentLock == "NO_LOCK"
|
||||||
|
|
||||||
|
// Acquire SHARED lock, enter Reading state
|
||||||
|
currentLock := "SHARED"
|
||||||
|
pagerState := "Reading"
|
||||||
|
activeTransaction := true
|
||||||
|
transactionType := "Read"
|
||||||
|
|
||||||
|
emit ReadTransactionStarted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
action BeginWriteTransaction() {
|
||||||
|
require pagerState == "Reading" or pagerState == "Idle"
|
||||||
|
require errorState == false
|
||||||
|
require readOnly == false
|
||||||
|
|
||||||
|
// Acquire RESERVED lock, create journal, enter Writing state
|
||||||
|
if currentLock == "SHARED" or currentLock == "NO_LOCK" {
|
||||||
|
currentLock := "RESERVED"
|
||||||
|
}
|
||||||
|
pagerState := "Writing"
|
||||||
|
activeTransaction := true
|
||||||
|
transactionType := "Write"
|
||||||
|
journalExists := true
|
||||||
|
|
||||||
|
emit WriteTransactionStarted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
action WritePage(pageId: Nat, data: Bytes) {
|
||||||
|
require pagerState == "Writing"
|
||||||
|
require activeTransaction == true
|
||||||
|
require errorState == false
|
||||||
|
|
||||||
|
// Mark page as dirty in the page cache
|
||||||
|
dirtyPages := dirtyPages union {pageId}
|
||||||
|
dirtyPageCount := dirtyPageCount + 1
|
||||||
|
|
||||||
|
emit PageWritten { pageId: pageId, size: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
action CommitTransaction() {
|
||||||
|
require pagerState == "Writing"
|
||||||
|
require activeTransaction == true
|
||||||
|
require transactionType == "Write"
|
||||||
|
require errorState == false
|
||||||
|
|
||||||
|
// Acquire PENDING then EXCLUSIVE lock
|
||||||
|
currentLock := "PENDING"
|
||||||
|
currentLock := "EXCLUSIVE"
|
||||||
|
|
||||||
|
// Flush dirty pages, sync journal
|
||||||
|
syncNeeded := true
|
||||||
|
pagerState := "CommitPending"
|
||||||
|
|
||||||
|
// Delete journal (commit point)
|
||||||
|
journalExists := false
|
||||||
|
syncNeeded := false
|
||||||
|
|
||||||
|
// Transition back to Idle
|
||||||
|
pagerState := "Idle"
|
||||||
|
dirtyPages := emptySet
|
||||||
|
dirtyPageCount := 0
|
||||||
|
activeTransaction := false
|
||||||
|
transactionType := "None"
|
||||||
|
currentLock := "SHARED"
|
||||||
|
|
||||||
|
emit TransactionCommitted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
action RollbackTransaction() {
|
||||||
|
require activeTransaction == true
|
||||||
|
require transactionType == "Write" or transactionType == "Read"
|
||||||
|
require errorState == false
|
||||||
|
|
||||||
|
// Restore pages from journal, release lock
|
||||||
|
if transactionType == "Write" {
|
||||||
|
journalExists := false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all transaction state
|
||||||
|
pagerState := "Idle"
|
||||||
|
dirtyPages := emptySet
|
||||||
|
dirtyPageCount := 0
|
||||||
|
activeTransaction := false
|
||||||
|
transactionType := "None"
|
||||||
|
currentLock := "NO_LOCK"
|
||||||
|
|
||||||
|
emit TransactionRolledBack {}
|
||||||
|
}
|
||||||
|
|
||||||
|
action Checkpoint() {
|
||||||
|
require journalMode == "Wal"
|
||||||
|
require pagerState == "Idle"
|
||||||
|
require errorState == false
|
||||||
|
|
||||||
|
// WAL checkpoint: copy frames back to database
|
||||||
|
// In non-WAL mode, checkpoint is a no-op
|
||||||
|
|
||||||
|
emit CheckpointCompleted {}
|
||||||
|
}
|
||||||
|
|
||||||
|
action CloseDatabase() {
|
||||||
|
require pagerState == "Idle"
|
||||||
|
require activeTransaction == false
|
||||||
|
|
||||||
|
// Release all resources, reset to initial state
|
||||||
|
currentLock := "NO_LOCK"
|
||||||
|
pagerState := "Idle"
|
||||||
|
pageCount := 0
|
||||||
|
dirtyPageCount := 0
|
||||||
|
journalExists := false
|
||||||
|
syncNeeded := false
|
||||||
|
dirtyPages := emptySet
|
||||||
|
|
||||||
|
emit DatabaseClosed {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Transition Relation ---
|
||||||
|
|
||||||
|
next:
|
||||||
|
OpenDatabase
|
||||||
|
| BeginReadTransaction
|
||||||
|
| BeginWriteTransaction
|
||||||
|
| WritePage
|
||||||
|
| CommitTransaction
|
||||||
|
| RollbackTransaction
|
||||||
|
| Checkpoint
|
||||||
|
| CloseDatabase
|
||||||
|
|
||||||
|
// --- Bounded Model Checking ---
|
||||||
|
|
||||||
|
verify Always(CommitAtomicity) {
|
||||||
|
depth 5
|
||||||
|
}
|
||||||
|
|
||||||
|
verify Always(RollbackSafety) {
|
||||||
|
depth 5
|
||||||
|
}
|
||||||
|
|
||||||
|
verify Always(JournalConsistency) {
|
||||||
|
depth 5
|
||||||
|
}
|
||||||
|
|
||||||
|
verify Always(PageCacheCoherence) {
|
||||||
|
depth 5
|
||||||
|
}
|
||||||
|
|
||||||
|
verify Always(LockProgression) {
|
||||||
|
depth 10
|
||||||
|
}
|
||||||
|
|
||||||
|
verify Always(ErrorRecovery) {
|
||||||
|
depth 5
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue