← 1.0.0 scope SAPE-29

Decide the type list, once — because every entry on it is permanent

Status
To do
Component
server
Priority
High — it freezes, and it blocks the two tickets under it
Blocked by
Blocks
SAPE-30, SAPE-31
Waiting on
a product decision — the dossier is below

What exists today, measured

Four declared types, and that is the whole list — internal/store/spec.go:

TypeString = "string"
TypeNumber = "number"
TypeBool   = "bool"
TypeAny    = "any"

They constrain three places: the primary key, an index field, and an operation's arguments. A document body is unconstrained JSON — nested objects, arrays and nulls were stored and read back byte-exact. And null is accepted by every type on purpose, because a field that is explicitly nothing sorts differently from a field that is absent, and refusing it would make the first unindexable.

Why the list has to be decided rather than grown

COMPATIBILITY.md lets a 1.x add a type — that is a loosening. It does not let anything be removed or renamed. So the list only ever grows, and every entry put on it is there for the life of 1.x. A type added because it sounded reasonable cannot be taken back when it turns out nobody used it, or when its name turns out to mean something else.

The cost of an entry is also not small. Counted: nine production files in the server decide on a type name across thirty-nine sites, plus internal/keys, which writes the on-disk sort key — plus five production files in the TypeScript client, whose generator turns a declared type into a TypeScript type. The PHP client models no types at all, so it is zero there, which is its own kind of answer.

There is room on disk. The key encoding is a tag byte followed by the value, and only seven byte values are spent — 0x01, 0x10, 0x20, 0x21, 0x30, 0x40, 0xfe — leaving 249 free, spaced deliberately so a new type can sort between two old ones. Room is not the constraint. The permanence is.

The two that are not a matter of taste

Two gaps were measured rather than imagined, and both are already tickets of their own.

The candidates that are a matter of taste

These are the ones this ticket exists to decide. None of them is measured as broken today; each is a judgement about what an explicit type system owes its reader. For each, the question to answer is not "is it useful" — most are — but "what does a declaration mean without it, and is that meaning wrong or merely unhelpful?"

CandidateWhat it replaces todayThe argument against
timestampa number of unstated unit, or a stringTwo callers can already disagree about seconds versus milliseconds, and a type does not fix that unless it also fixes the unit — which is a second decision hiding inside the first.
decimala number, i.e. a float, i.e. wrong for moneyNeeds a precision and a scale to mean anything, so it is not one type but a family, and the key encoding has to sort it.
uuida 36-character stringIt is a string with a shape. The gain is validation and sixteen bytes instead of thirty-six; the cost is a permanent entry for something a projection could describe.
enuma string the declaration does not constrainThe value list belongs to the declaration, not the type, so this may be a field on a declaration rather than a type at all.

The recommendation, and it is deliberately small

Add exactly the two that are measured — an exact integer and a byte string — and nothing else in 1.0.0. Not because the others are bad, but because the list only grows and the two above are the only ones where a caller is currently told something untrue. Every other candidate can be added in a 1.x without breaking a promise, and will be a better decision when somebody has a use that measures it.

The owner may reasonably overrule this: "explicit" is a property of the whole list, not of any one entry, and a list of six may be a better product than a list of six in two instalments. That is exactly why this is a ticket and not a commit.

Acceptance criteria

  1. A written decision naming every type 1.0.0 will have, and for each of the rejected candidates, one sentence on why not. A ticket closed with a list and no reasons is not closed — the reasons are what stop the same argument being had again in six months.
  2. The decision is cited from COMPATIBILITY.md under what a declaration means, since the list is a frozen surface from the tag onwards.
  3. For each type on the final list: what it accepts, what it refuses, where it sorts relative to the others in the key encoding, and what the TypeScript generator emits for it.
  4. null stays accepted by every type, and a test says so, because that rule is older than this ticket and easy to lose while rewriting the table it lives in.

Out of scope