0013. Data-layer contract with declared capabilities
0013. Data-layer contract with declared capabilities
Status
Accepted
Date
2026-10-04
Deciders
operator (Ruling 4); the manifest, closed union and conformance suite are the roadmap author’s design (roadmap M3), which the lead or the operator may overrule
Context
A data layer is the part of a framework that talks to a database. Mesh must work with more than one (SQLite and Postgres in v1). Ash, the Elixir framework Mesh is modelled on, defines the data layer as one behaviour with 46 callbacks, 44 of them optional, plus a can?/2 probe with 47 distinct feature names. When a feature is missing, core reacts inconsistently: “sometimes an exception, sometimes a string, sometimes a silent in-memory fallback” (research synthesis, section 2.2; Ash runtime internals, sections 3.1 to 3.3). The feature type is also not exhaustive: core queries features it never declares (Ash runtime internals, section 3.2).
Decision
Ruling 4, operator, 2026-10-04, rulings of 2026-10-04, table of eight rulings:
Select, insert, update, delete, transactions, filters, sort, pagination. Joins, aggregates, upserts and atomic expressions are declared capabilities; using one an adapter lacks is a build-time error, never a silent in-memory fallback.
The roadmap author’s design on top (roadmap, M3): each adapter publishes a capability manifest, static data and a closed union of names, which the build reads without starting the adapter. A resource that uses a capability the adapter lacks fails the build at the resource-file position. A conformance suite, which every adapter must pass, tests the mandatory set and each declared capability. The contract is Mesh’s own and sits at the level of resources, not of a query library (research synthesis, section 11).
Options considered
Option A: Mandatory set plus static capability manifest (chosen)
| Dimension | Assessment |
|---|---|
| Complexity | Medium: a closed union, a build check, a conformance suite |
| Cost | Moderate in M3; each new capability adds suite cases |
| Failure timing | Build time, with file and line |
| Reversibility | Names can be added to the union; removal breaks adapters |
Pros: missing features surface before deployment; agents and people can read the manifest.
Cons: the manifest can lie; only the suite catches that. A static manifest cannot express capabilities that depend on the server (a Postgres version, SQLite compile options). With one real adapter until M9, the contract is untested against a second dialect (roadmap, M3 risks).
Option B: Ash-style run-time probe with fallbacks
| Dimension | Assessment |
|---|---|
| Complexity | High: every call site decides how to degrade |
| Cost | Low at first, high later |
| Failure timing | Run time, sometimes silent |
| Reversibility | Hard; behaviour depends on fallbacks |
Pros: more programs run on weak adapters.
Cons: the inconsistency the research documents, including silent in-memory evaluation of filters. The ruling forbids it.
Option C: Run-time probe that throws, with no fallback
| Dimension | Assessment |
|---|---|
| Complexity | Medium: adapter answers a probe at start-up or per call |
| Cost | Moderate |
| Failure timing | Start-up or first use, not build |
| Reversibility | Easy to add later beside a manifest |
Pros: honest about version-dependent capabilities; no silent fallback.
Cons: the error appears after deployment, or only on the code path that uses the capability; the build cannot report it.
Option D: Expose the query library directly
| Dimension | Assessment |
|---|---|
| Complexity | Low at first |
| Cost | Leaks into generated code |
| Failure timing | The library’s own |
| Reversibility | Very hard |
Pros: no contract to write.
Cons: filters, joins and aggregates have four different shapes across Drizzle, Kysely, TypeORM and Prisma; relation loading, schema ownership, migrations and pooling cannot be covered (research synthesis, section 11).
Trade-off analysis
The ruling removes B. A beats C for v1 on one point: a missing capability is found at build time, not after deployment. C addresses what A cannot see; it can be added beside A if a version-dependent capability appears.
Consequences
Easier: adding an adapter; reading what it supports. Harder: each capability needs conformance cases before merging. Revisit the closed union when upserts and bulk work arrive after v1 (ADR-0019), and A against C if a capability depends on server version.
Action items
- M3: manifest type, build check, conformance suite.
- M5, M7: add atomic expressions, joins and aggregates in every merged adapter.
- M9: Postgres passes the whole suite.