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 bincode of Envelope (raft/net.rs), which is not self-describing. A new field on AppendReply is 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 unknown Op variant fails to apply the entry.
  • A plain Op::Ddl("CREATE SCHEMA x") does not fail on an older node: its parser accepts it, ddl::apply returns None for it, and it is ignored. The next CREATE TABLE x.t then creates a table named t in 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:

  1. A replicated node writes schema records (CREATE/DROP/ALTER SCHEMA, ALTER TABLE ... SET SCHEMA, and any statement that must be recorded with a non-public search path, which is Op::DdlIn) only when ORBIT_SQL_SCHEMAS is on, 1, true or yes. Otherwise the statement fails with 0A000 and a message naming the setting. Nothing is written or applied.
  2. 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.
  3. A node enables it automatically when the data it loads already uses schemas (a non-public schema or a table outside public), since that data could only have been produced by nodes that support it.
  4. A single-node or in-memory engine (Persist::replicated() is false) is always enabled: there is no other node to diverge from.
  5. 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 public with 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.