Local database¶
LocalDatabase reconstructs temporal queries from existing local history and
provides an explicit writable mode. New writes prepare their complete state,
append and force an exact journal record, then publish values, historical cutoffs
and request identity together before returning success. The API and local protocol
are experimental, with process crash/retry and temporal conformance
through real writable reopen.
ADR 0013 explains read reconstruction;
ADR 0014 defines creation, writable
startup, acknowledgement and uncertainty. The separate InMemoryEngine retains
its memory-only write contract.
Choose an open mode¶
| Entry point | Behavior |
|---|---|
create(directory, databaseId) |
Initialize a fresh existing directory, select genesis generation one and return a writable database |
open(directory) |
Recover existing history for queries and request lookup; reject transactions |
openWritable(directory) |
Recover existing history, synchronize journal and namespace, then enable transactions |
The directory must be trusted, stable and already durably named by its creator. Creation allows only an existing checkpoint-store lock file; it refuses a journal, manifest, pending file or unrelated content rather than overwriting it. Initial lineage/format validation precedes store mutation. Creation failures retain files for inspection; inspect or reopen valid existing history instead of retrying creation blindly. Incomplete journals require the separately documented explicit repair policy; these APIs never repair automatically.
Create and write¶
try (var database = LocalDatabase.create(directory, new DatabaseId("example"))) {
var request = new Transaction(new RequestId("request-1"), List.of(
new WriteOperation.Put(new EntityId("salary"),
new ValidTimeInterval(10L, null), 100)));
var committed = database.transact(request);
try (var view = database.openView(ReadCutoff.Head.INSTANCE)) {
var query = new TemporalQuery.Get(new EntityId("salary"), 25);
var result = view.query(query);
}
}
Imports are java.util.List, org.meridiandb.engine.LocalDatabase and
org.meridiandb.temporal.*; directory is a java.nio.file.Path.
Use openWritable(directory) for an existing database. A new valid request gets
the next contiguous commit, including a no-op. An identical retry returns its
original position with replayed() == true and performs no new append/force.
Conflicting payloads for the same request ID fail with
EngineException(REQUEST_ID_REUSED) before storage changes. Invalid pure request
or record encoding also leaves the instance usable.
Writers and close serialize. Readers can capture the old committed state while a write appends or waits for force. Once publication completes, a new head view sees the whole transaction. Views opened earlier keep their original answers. There is no dirty read or intermediate per-operation state. No group commit, arrival-order promise or measured throughput claim is provided.
Query recovered history¶
open(directory) exposes the same queries without a write API effect:
try (var database = LocalDatabase.open(directory)) {
var recoveredHead = database.head();
var checkpoint = database.checkpointPosition();
var request = database.lookupRequest(new RequestId("request-1"));
try (var view = database.openView(ReadCutoff.Head.INSTANCE)) {
var query = new TemporalQuery.Get(new EntityId("salary"), 25);
var result = view.query(query);
}
}
checkpointPosition() returns an optional temporal cutoff. It can precede the
head because later journal transactions are replayed too; it does not advance on
each new write. Use new ReadCutoff.At(new CommitPosition(databaseId, sequence))
for an earlier commit in the stored lineage. Foreign or unavailable cutoffs fail
with EngineException(INVALID_CUTOFF). Point, clipped interval scans and exact sums
follow the temporal contract.
Typed selection and filtered scans use the same immutable
queries and resolved-state predicates in memory and after local recovery. Query
results retain no file handle and remain immutable values after database close.
Recovery and writable startup¶
- Acquire the checkpoint store and designated journal owner. Validate the full journal and reject incomplete tails without automatic repair.
- Verify the highest selected checkpoint, complete dependency graph and actual journal-prefix binding. Damaged newest state or missing dependencies fail closed without falling back to an older generation. Journal-only and genesis histories are supported.
- Build private engine state with the stored lineage. Replay checkpoint transactions exactly once and only the journal suffix after its cutoff. Require contiguous complete history and unique stored request identities; these are original commits, not retry calls. Retain every historical cutoff and exact request payload.
- For writable mode, force the complete owner journal and then its directory before enabling acknowledgements. No database or view is exposed before recovery and this required synchronization succeed. Failure discards private state and closes every acquired owner, preserving primary and suppressed cleanup errors.
Complete recovered bytes do not prove earlier force or acknowledgement. Writable startup re-establishes the boundary under the actual provider/OS/device's force behavior, including its treatment of previously dirty data on a reopened file. An identical retry can then acknowledge the recovered position without additional I/O. Read-only open performs no journal/segment data force and makes no such write acknowledgement promise.
Acknowledgement and uncertain outcomes¶
A new write's success follows complete state preparation, encoding preflight, owner-journal append, successful force and one atomic state publication. It relies on the supported local filesystem and device honoring force; it is not a universal power-loss guarantee. Initial journal/genesis publication supplies the namespace boundary. Appending to that established journal requires no per-transaction rename.
After an append attempt, an I/O or runtime failure throws
LocalDatabase.UncertainCommitException. It carries requestId(),
proposedPosition() and its cause. The proposed position is diagnostic; it does
not establish that the request committed. The instance and existing views become
unusable, while storage locks remain held until close. Errors also invalidate the
instance before propagating. Already captured operations may finish under the
usual concurrent-close semantics.
Close the failed database, then reopen and inspect lookupRequest(requestId) or
retry the exact original transaction through a writable instance. A complete
record may be recovered even when its earlier acknowledgement was lost. A partial
record fails strict reopen; inspect the retained evidence and explicit journal
repair procedure. Never infer rollback, continue using the failed instance or
silently truncate its tail. Close itself never forces or revokes a successful
write acknowledgement; retry cleanup if releasing resources fails.
Ownership and checkpoints¶
All modes retain exclusive cooperating store/journal ownership. Logical read-only
access still requires writable journal/lock handles for their locks. Store open
probes directory open and force(true) before creating a lock file; it can perform
metadata I/O. An absent lock file may be created. Unsupported providers report
DirectorySyncUnsupportedException instead of receiving a weaker protocol.
Use trusted stable paths and working local locks. Verification, append and force use the journal owner's channel; a second same-file handle can interfere with platform locks. Other cooperating owners cannot publish checkpoints concurrently. There is no automatic checkpoint advancement or reclamation. To use the separate checkpoint primitives, close the database first, then acquire the store and journal owners. Retain all old files and history.
Read views support concurrent readers. Closing one leaves others usable; database close invalidates all views before releasing storage resources. A query that already captured open state may finish. Returned immutable results and commit positions remain ordinary values without permission to delete storage.
Cost and verification¶
Recovery reconstructs full history into interval snapshots and exact request maps, with multiple sequential validation/replay passes. Every new transaction copies maps while preparing its immutable candidate. Per-object bounds do not bound total heap, retained history or replay work. Checkpoints are not yet fast materialized snapshots or journal-truncation boundaries.
Run verification and the writable example:
./mvnw -B verify
java --class-path target/classes examples/LocalDatabaseExample.java
The example forces two commits, preserves an earlier view, reopens and confirms an identical retry returns commit two without another append. The separate recovery example prepares checkpoint-plus-tail storage and opens read-only queries:
java --class-path target/classes examples/LocalRecoveryExample.java
Examples remove only their successful temporary output. An unsupported initial directory capability is reported explicitly; other failures remain errors and retain diagnostics.
Focused publication checks:
./mvnw -B -Dtest=LocalCommitTest,PreparedCommitTest,LocalInitializationTest test
Recovery checks:
./mvnw -B -Dtest=LocalDatabaseTest,LocalRecoveryFixtureTest test
Tests cover preparation invisibility, exact identities, startup synchronization, append/force errors before or after an effect, uncertainty/poisoning, pinned views, concurrent readers/writers and resource lifetimes. Canonical histories retain their journal-only, genesis and checkpoint-plus-tail reopen coverage. Durability conformance adds real child-JVM exits at creation, append, force, publication, acknowledgement, checkpoint and startup boundaries, 168 writable canonical expectations and 512 seeded durable commits. These checks and injected storage faults do not simulate device power loss.
Recovered databases also retain the checkpoint's shared reader protection until close. Closing invalidates query views, then attempts reader, journal and store cleanup independently, preserving suppressed failures. A failed reader release cannot strand the other owners; retry database close for remaining cleanup. This protection adds no physical deletion or change to historical query/retry semantics.