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.
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.
Two gaps were measured rather than imagined, and both are already tickets of their own.
number is float64, and an integer above 253 is stored as a
different integer with no refusal: 9007199254740993 comes back as …992.
ISS-35 holds the measurement.string cannot carry arbitrary bytes: 0xFF is silently replaced with U+FFFD on the way
in, and the write answers changed 1. Base64 is therefore mandatory rather than
conventional, which costs a third of the space and makes the bytes uncomparable.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?"
| Candidate | What it replaces today | The argument against |
|---|---|---|
| timestamp | a number of unstated unit, or a string | Two 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. |
| decimal | a number, i.e. a float, i.e. wrong for money | Needs 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. |
| uuid | a 36-character string | It 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. |
| enum | a string the declaration does not constrain | The value list belongs to the declaration, not the type, so this may be a field on a declaration rather than a type at all. |
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.
COMPATIBILITY.md under what a declaration means, since
the list is a frozen surface from the tag onwards.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.