ADR 0014: Schema records are emitted only when the operator attests every node can apply them
ADR 0014: Schema records are emitted only when the operator attests every node can apply them
Status: Accepted Date: 2026-10-24 Scope: orbit-rs Deciders: Ravindra Boddipalli (accepted 2026-10-24) Amends: ADR 0003 (item 4, the rolling-upgrade rule)
Context
ADR 0003 item 4 says the first non-public schema is refused until every node reports the new version. Building that showed the wire cannot carry a version safely:
- Raft frames are
bincodeofEnvelope(raft/net.rs), which is not self-describing. A new field onAppendReplyis read by an older leader as nothing (trailing bytes are ignored), but a new node reading an older node’s reply expects bytes that are not there, fails to decode it, and drops it. In a mixed cluster that stops replication, which is the situation the guard exists to protect. - Ops are JSON (
serde_json). An older node given an unknownOpvariant fails to apply the entry. - A plain
Op::Ddl("CREATE SCHEMA x")does not fail on an older node: its parser accepts it,ddl::applyreturnsNonefor it, and it is ignored. The nextCREATE TABLE x.tthen creates a table namedtin the single flat namespace, silently, on that node only.
So the dangerous case is silent divergence, and nothing on the wire lets a leader know about it.
Decision
Whether a node may emit schema records is a setting, not a negotiation:
- A replicated node writes schema records (
CREATE/DROP/ALTER SCHEMA,ALTER TABLE ... SET SCHEMA, and any statement that must be recorded with a non-publicsearch path, which isOp::DdlIn) only whenORBIT_SQL_SCHEMASison,1,trueoryes. Otherwise the statement fails with0A000and a message naming the setting. Nothing is written or applied. - An operator sets it only after every node runs a version that can apply schema records. Because every node applies the log regardless of the setting, turning it on needs no restart ordering beyond that.
- A node enables it automatically when the data it loads already uses schemas (a non-
publicschema or a table outsidepublic), since that data could only have been produced by nodes that support it. - A single-node or in-memory engine (
Persist::replicated()is false) is always enabled: there is no other node to diverge from. - Wire formats are not changed. A real version report remains possible later, in a protocol version negotiated at connection time, and would supersede this record.
Consequences
- The guarantee is an operator’s attestation, not a check. A node left on an older version with the setting turned on elsewhere still diverges. Documentation must say so.
- Ordinary statements, including DDL in
publicwith the default path, are unaffected, so a cluster can be upgraded without the setting at any time. - A node added to a cluster that already uses schemas must run the new version; the membership change does not check it.
Verification
schema_upgrade_guard tests in sql/durable/tests.rs: a replicated engine refuses every schema statement and a recorded-path statement until enabled, ordinary DDL still works, and enabling allows them; a single-node engine is enabled. A three-node cluster test that creates a schema before and after enabling is still to do.