Architecture#
src/
Ldb.Core contracts, no I/O
Protocol/ envelope records, HMAC signing
Schema/ schema model, validator/normaliser, hasher, differ
Query/ filter syntax tree + parser, request and result records
Config/LdbOptions.cs all settings
IDatabaseProvider.cs what an engine has to implement
Ldb.Providers.Sql shared SQL machinery
SqlDialect engine differences: quoting, types, DDL, upsert
SqlQueryBuilder filter tree -> parameterised WHERE
SqlValues JSON <-> typed driver values
SqlProviderBase schema sync + data operations written once
Ldb.Providers.Postgres dialect + connection + advisory lock
Ldb.Providers.MySql dialect + connection + GET_LOCK
Ldb.Providers.Mongo complete provider (collections, $jsonSchema, lock documents)
Ldb.Api ASP.NET Core minimal API host
RequestPipeline authentication, authorisation, limits, signing
Operations the list of operations and their payload handling
Security/ key registry, secret resolver, nonce / rate / idempotency caches
Buffering/ optional write-behind: queue, journal file, schedule, background flush
tests/Ldb.Tests unit, pipeline and provider-contract tests
Request flow#
Program.cs maps one route per entry of Operations.All. Every route calls RequestPipeline.HandleAsync, which reads the
body (bounded), verifies the envelope, runs the operation against the configured IDatabaseProvider and signs the answer.
Providers receive only validated, typed requests. They never see credentials, and the pipeline never sees SQL.
The provider is chosen once at start-up from Ldb:Database:Provider.
Write-behind#
Operations.Write(...) wraps the four write operations. If the owner enabled write-behind for the namespace and operation,
WriteBuffer.EnqueueAsync validates the request against the stored schema (Ldb.Core.Query.WriteValidator), appends it to the
journal and queues it; otherwise the write goes straight to the provider. WriteBuffer.FlushAsync applies the queue in order
(merging neighbouring inserts), retrying temporary errors and dead-lettering permanent ones. WriteBufferService runs the
schedule (FlushSchedule.Due, pure and unit tested) and the final flush on shutdown. The buffer sits in front of
IDatabaseProvider, so it works unchanged for every engine.
Design decisions#
- Declarative schema + stored previous declaration. Diffing two declarations is deterministic and engine independent. The database is consulted only to repair or skip steps, which avoids reverse-mapping every native type back to a portable one.
- No raw queries, ever. The filter grammar is small on purpose. It is what makes the API safe to expose to semi-trusted callers.
- The signature covers the payload as bytes. No canonical JSON, nothing a proxy can reformat.
- Everything stateful is in memory (nonces, idempotency, rate counters). That keeps the service a single binary with no extra dependency; the cost is documented in SECURITY.md.
- One contract test suite for all engines.
ProviderContractholds the scenarios;PostgresContractTests,MySqlContractTestsandMongoContractTestsonly start a container. A new engine is accepted when it passes the same tests.
Adding a database engine#
- For a SQL engine: derive from
SqlDialect(quoting, types, DDL, upsert, introspection queries) andSqlProviderBase(connection factory, lock). For anything else implementIDatabaseProviderdirectly, likeMongoProvider. - Register it in the
switchinProgram.cs. - Add a
…ContractTestsclass that starts the engine in a container. - Document the type mapping in SCHEMA.md.
Building and testing#
dotnet build # warnings are errors
dotnet test # unit + pipeline tests always; engine tests need a container runtime
LDB_SKIP_DOCKER=1 dotnet test # skip the engine tests explicitly