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.2.0 — The Lock-Table Core

**The first release with real locking.** v0.2.0 builds on the v0.1.0 scaffold and
implements the heart of the lock manager: the shared/exclusive compatibility
matrix and a sharded, non-blocking lock table with acquire, release, bulk
release, and shared-to-exclusive upgrade. The core types are `no_std`; the
table itself is behind the `std` feature. Acquisition is `try`-style — a request
that cannot be granted returns an error rather than blocking. No breaking
changes to the (previously empty) public API; this is purely additive.

## 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.2.0

### `LockMode` and the compatibility matrix

The correctness core of any lock manager is the matrix that decides whether two
transactions can hold the same resource at once. `LockMode` ships the two
fundamental modes, `Shared` and `Exclusive`, and three `const fn` predicates:

```rust
use lock_db::LockMode;

// The only compatible pair is shared/shared.
assert!(LockMode::Shared.compatible_with(LockMode::Shared));
assert!(!LockMode::Shared.compatible_with(LockMode::Exclusive));
assert!(!LockMode::Exclusive.compatible_with(LockMode::Exclusive));

// `covers` drives the idempotent and upgrade paths of acquisition.
assert!(LockMode::Exclusive.covers(LockMode::Shared));
assert!(!LockMode::Shared.covers(LockMode::Exclusive));
```

`compatible_with` is provably symmetric, and the whole matrix lives in one small
function rather than being scattered across the acquire path. The hierarchical
intention modes (IS, IX, SIX) extend this same matrix in a later release.

### `LockManager` — the sharded lock table

`LockManager` is the primary entry point. A single global mutex over the table
would serialise every lock operation in the database, so the table is split into
a power-of-two number of independent shards, each a mutex over its own slice of
the resource space. The shard for a resource is chosen by Fibonacci-hashing its
id, which spreads sequential page and row numbers evenly across shards without
paying for a general-purpose hasher on the hot path. Operations on resources in
different shards never contend.

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

let lm = LockManager::new();             // shard count scaled to the machine
let row = ResourceId::new(1);
let (writer, reader) = (TxnId::new(1), TxnId::new(2));

lm.try_acquire(writer, row, LockMode::Exclusive).unwrap();
assert_eq!(lm.try_acquire(reader, row, LockMode::Shared), Err(LockError::Conflict));
lm.release(writer, row).unwrap();
lm.try_acquire(reader, row, LockMode::Shared).unwrap();
```

`try_acquire` handles three subtle cases beyond a plain grant: re-acquiring a
mode you already cover is a no-op, asking for a weaker mode than you hold is a
no-op, and a sole shared holder can upgrade in place to exclusive. The manager
is `Send + Sync` and every method takes `&self`, so it is shared across threads
behind an `Arc` with no outer lock.

### `release_all` in time proportional to locks held

A transaction layer drops a transaction's entire lock set at commit or abort.
Doing that by scanning the whole table would scale with table size, not with the
transaction. Each shard keeps a reverse index from transaction to the resources
it holds there, so `release_all` visits only the locks the transaction actually
owns:

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

let lm = LockManager::new();
let txn = TxnId::new(1);
for id in 0..5 {
    lm.try_acquire(txn, ResourceId::new(id), LockMode::Exclusive).unwrap();
}
assert_eq!(lm.release_all(txn), 5);
```

### Opaque identifiers and a small error type

`TxnId` and `ResourceId` are `#[repr(transparent)]` newtypes over `u64`, so a
lookup is a hash of one machine word with no allocation. `LockError` is a
two-variant, `#[non_exhaustive]` enum (`Conflict`, `NotHeld`) with `Display` and
a `std::error::Error` impl under `std`. All four core types are `no_std` and
derive `serde` under the `serde` feature.

### Testing: property model, loom, and benchmarks

- **Property tests** run arbitrary acquire/release sequences against both the
  real manager and a hand-written reference model that applies the compatibility
  rules directly, asserting they agree after every step and that no resource ever
  holds two incompatible locks. Run across 1-, 4-, and 16-shard configurations.
- **Loom model checks** explore every thread interleaving of concurrent
  acquire/release on a contended resource and confirm exclusive locks never
  coexist with any other holder.
- **Criterion benchmarks** cover the hot paths: single-lock acquire/release,
  upgrade, `release_all` at several sizes, and multi-threaded contention.

## Breaking changes

**None.** The v0.1.0 public API was empty; everything here is additive.

## 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: 35 unit + 17 doctests.
- `--all-features`: 35 unit + 17 doctests + 2 property tests.
- `--no-default-features`: 12 unit tests (the `no_std` core).
- Loom: 2 model checks.

Indicative single-threaded numbers on the development workstation: an
uncontended acquire+release in the low hundreds of nanoseconds, and `release_all`
scaling linearly at roughly 130 ns per lock held.

## What's next

- **v0.3.0 — hierarchical granularities + intention locks + range locks.**
  Database/table/page/row coupling with IS/IX/SIX modes and key-range locks.
- **v0.4.0 — wait-for graph + deadlock detection + victim selection**, which
  brings blocking acquisition and wait queues.

## Installation

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

# with serde derives on the public types
lock-db = { version = "0.2", 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.1.0...v0.2.0`](https://github.com/jamesgober/lock-db/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/lock-db/blob/main/CHANGELOG.md#020---2026-06-05).