lock-db 1.0.0

Lock manager and deadlock detection for Rust databases - row/range locks, multiple granularities, and wait-for cycle detection.
Documentation
# lock-db v0.3.0 — Multi-Granularity & Range Locking

**Hierarchies and phantoms.** v0.3.0 extends the lock-table core with the two
features a real database lock manager needs beyond plain read/write locks: the
full multi-granularity locking (MGL) mode set, so a transaction can lock a
database/table/page/row hierarchy correctly, and key-range locks, so a
transaction can stop another from inserting into a span it has read. Acquisition
stays `try`-style; wait queues and deadlock detection follow.

## What is lock-db?

The lock manager for a transactional database: the component that lets many
transactions touch shared data at once without corrupting it. It hands out locks
on resources in compatible modes and, in later releases, detects and breaks
deadlocks. It assigns no identities and persists nothing — it is the in-memory
lock table a transaction or storage layer drives.

## What's new in 0.3.0

### The full MGL mode set and the compatibility lattice

v0.2.0 shipped `Shared` and `Exclusive`. v0.3.0 adds the three intention modes
that make hierarchical locking work — `IntentionShared` (IS),
`IntentionExclusive` (IX), and `SharedIntentionExclusive` (SIX) — and the
complete Gray/Reuter compatibility matrix:

|       | IS | IX | S  | SIX | X  |
|-------|----|----|----|-----|----|
| **IS**  ||||||
| **IX**  ||||||
| **S**   ||||||
| **SIX** ||||||
| **X**   ||||||

The five modes form a lattice. Upgrades now resolve to the **join** (least upper
bound) of the held and requested modes, not just shared-to-exclusive:

```rust
use lock_db::LockMode;

// A reader that decides to write part of a subtree becomes SIX.
assert_eq!(LockMode::Shared.join(LockMode::IntentionExclusive), LockMode::SharedIntentionExclusive);
// Holding SIX already covers a plain read.
assert!(LockMode::SharedIntentionExclusive.covers(LockMode::Shared));
```

`compatible_with`, `join`, and `covers` are all `const fn`, and the matrix and
lattice are exhaustively tested for symmetry, commutativity, and associativity.

### Hierarchical locking

The intention modes let a transaction announce "I am working below this node"
on a coarse resource, so a writer wanting the whole table need only check the
table's lock, not every row. The protocol — `IX`/`IS` coarse-to-fine, then the
fine lock — is documented in `docs/API.md` and demonstrated in the new
`hierarchy` example:

```rust
use lock_db::prelude::*;

let lm = LockManager::new();
let (db, table, page, row) = (ResourceId::new(1), ResourceId::new(2), ResourceId::new(3), ResourceId::new(4));
let writer = TxnId::new(1);

for node in [db, table, page] {
    lm.try_acquire(writer, node, LockMode::IntentionExclusive).unwrap();
}
lm.try_acquire(writer, row, LockMode::Exclusive).unwrap();

// A reader can still descend elsewhere, but not into the locked row.
let reader = TxnId::new(2);
lm.try_acquire(reader, table, LockMode::IntentionShared).unwrap();
assert!(lm.try_acquire(reader, row, LockMode::Shared).is_err());
```

lock-db keeps `ResourceId` opaque — the caller maps hierarchy nodes to ids — and
enforces the matrix at every level.

### Range locks for phantom protection

A new `KeyRange` type (an inclusive `[start, end]` interval) and three
`LockManager` methods — `try_acquire_range`, `release_range`, and `range_count`
— lock a contiguous span of keys rather than a single resource. A range request
conflicts only with an **overlapping** range held by **another** transaction in
an **incompatible** mode:

```rust
use lock_db::prelude::*;

let lm = LockManager::new();
let index = ResourceId::new(1);

// "SELECT ... WHERE id BETWEEN 100 AND 200" read-locks the whole span.
lm.try_acquire_range(TxnId::new(1), index, KeyRange::new(100, 200).unwrap(), LockMode::Shared).unwrap();

// Another transaction cannot insert id 150 — no phantom — but a disjoint range is free.
assert!(lm.try_acquire_range(TxnId::new(2), index, KeyRange::point(150), LockMode::Exclusive).is_err());
lm.try_acquire_range(TxnId::new(2), index, KeyRange::new(201, 300).unwrap(), LockMode::Exclusive).unwrap();
```

Range locks are sharded by key space, tracked in each transaction's reverse
index, and dropped by `release_all` alongside point locks. Conflict detection is
a linear overlap scan of the live ranges in a space; an interval tree is a
candidate for a later release if profiling shows range contention dominates.

### Testing and benchmarks

- The property model now generates all five modes and applies the lattice
  upgrade rule, cross-checking the manager after every operation; a new range
  property mirrors the manager's per-space storage exactly and asserts no two
  transactions ever hold overlapping ranges in incompatible modes.
- A third `loom` model check covers concurrent overlapping range acquisition.
- A range benchmark measures acquire/release against spaces holding a growing
  number of live ranges.

## Breaking changes

The `LockMode` enum gained three variants. Code that exhaustively matched on
`LockMode` without a wildcard arm will need to handle the new variants. The
v0.2.0 → v0.3.0 step is a pre-1.0 minor bump; the public API is still being
shaped toward the 1.0 freeze. No method signatures changed.

## Verification

Run on Windows x86_64 (Rust stable 1.95) and Linux via WSL2 Ubuntu (Rust stable
1.95); MSRV verified on Rust 1.85. All commands below pass on both platforms:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --no-default-features --all-targets -- -D warnings
cargo test
cargo test --all-features
cargo build --examples
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check
cargo audit

# concurrency model checks (slow; not part of the default run)
RUSTFLAGS="--cfg loom" cargo test --test loom --release
```

Test counts at this tag:

- Default features: 66 unit + 28 doctests.
- `--all-features`: 66 unit + 28 doctests + 3 property tests.
- `--no-default-features`: 26 unit tests (the `no_std` core).
- Loom: 3 model checks.

Indicative single-threaded numbers on the development workstation: an
uncontended point acquire+release in the low hundreds of nanoseconds, and a
range acquire+release around 100 ns against a lightly populated space (rising
linearly with the number of live ranges in that space).

## What's next

- **v0.4.0 — wait-for graph + deadlock detection + victim selection.** Brings
  blocking acquisition and wait queues, then a feature freeze.
- **v0.5.0 — adversarial contention and deadlock-storm tests, then API freeze.**

## Installation

```toml
[dependencies]
lock-db = "0.3"

# with serde derives on the public types
lock-db = { version = "0.3", features = ["serde"] }
```

MSRV: Rust 1.85 (2024 edition).

## Documentation

- [README]https://github.com/jamesgober/lock-db/blob/main/README.md
- [API Reference]https://github.com/jamesgober/lock-db/blob/main/docs/API.md
- [CHANGELOG]https://github.com/jamesgober/lock-db/blob/main/CHANGELOG.md

---

**Full diff:** [`v0.2.0...v0.3.0`](https://github.com/jamesgober/lock-db/compare/v0.2.0...v0.3.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/lock-db/blob/main/CHANGELOG.md#030---2026-06-05).