# Database schema contract

Odla DB supports both strict and schemaless namespaces. The distinction is
per namespace, not per application:

- A namespace present in the latest pushed schema is **strict**.
- A namespace absent from the latest pushed schema is **schemaless**.

This preserves dynamic-data compatibility while making a pushed schema an
enforced runtime contract instead of display-only metadata.

## Strict namespaces

Writes to a strict namespace are checked before commit:

- Scalar fields and links must be declared.
- Values must match the declared type. Numbers must be finite, booleans are not
  coerced from truthy values, and dates must be valid ISO `YYYY-MM-DD` or
  timezone-qualified date-time strings, or finite epoch values.
- `null` is accepted by `json` fields, but is not coerced into a string, zero,
  or `false` for primitive fields. Omit an optional primitive field when it has
  no value.
- `merge` is only valid for `json` fields.
- Every transaction that creates or changes an entity must leave all
  non-optional scalar fields present. A valid entity may be assembled across
  several ordered operations in one atomic transaction.
- Unique fields are checked atomically with the transaction.
- Natural-key lookups are accepted only for declared unique attributes; a
  non-unique or undeclared lookup fails instead of selecting an arbitrary row.

Partial updates to an already-valid entity are supported: a required field
does not need to be repeated when it is already stored.

## Schemaless namespaces

Namespaces omitted from the pushed schema still accept new fields. The first
value written to a field determines its stored type. Later writes to that field
must use the same type; Odla DB no longer silently converts a number to a
string, a string to a number, or a scalar into a link.

Namespace and field identifiers use `^[A-Za-z_$][A-Za-z0-9_$-]*$` at the data
boundary (`$` fields remain server-reserved). Schema-authored identifiers use
the same grammar without `$`. This keeps attribute identities unambiguous.

The first schemaless link fixes its target namespace. When the target id exists
in more than one namespace, a bare id is ambiguous: declare the link in a
schema or use a lookup carrying a namespace.

Legacy schemaless links that predate namespace-aware references must be
declared in a schema before new edges are added. The schema push verifies that
every existing target resolves in the declared namespace, checks cardinality,
and backfills reference namespaces atomically. Missing targets produce
`schema_conflict` rather than a guessed cascade target.

## Schema replacement and compatibility

`POST /app/:id/schema` replaces the app-authored schema. Re-pushing an
identical schema is idempotent. These changes are safe and accepted:

- adding an optional field;
- changing index metadata;
- making a required field optional;
- removing uniqueness;
- adding uniqueness when existing values are already unique; and
- changing an unused field or link.

The server rejects a push with `schema_conflict` before changing metadata when
stored data would be reinterpreted or hidden. Rejected cases include:

- changing the type of a populated field;
- removing a populated field or strict namespace;
- omitting a data-bearing schemaless field when adopting a strict schema;
- changing a populated link's target, forward cardinality, reverse label, or
  reverse cardinality;
- adding uniqueness while duplicate values exist; and
- making a field required while existing entities are missing it.

Backfill or delete incompatible data first, then push the new schema. A future
schema planner can automate expand/backfill/contract migrations; schema pushes
currently provide validation and atomic metadata replacement, not data
transformation.

## Entity identity

Entity ids are namespace-local. `people/shared` and `projects/shared` are
different entities: reads, full-text indexing, links, and deletion are all
scoped by namespace. Deleting one cannot delete the other.

Existing Durable Object stores are upgraded in place. The engine backfills
explicit entity and reference namespaces from attribute metadata. References
created before the upgrade whose target namespace was never declared remain
uncascaded rather than risking deletion of a same-id entity in another
namespace; declare link schemas before relying on inbound-link cleanup.

For rollout safety, an existing app's previously stored schema remains in
compatibility mode until its next successful schema push. That push validates
all current triples and then atomically enables strict enforcement. New apps
become strict on their first schema push. Re-push schemas in development and
inspect any `schema_conflict` before promoting the same schema to production.
`GET /app/:id/schema` exposes `status.mode` (`schemaless`, `compatibility`, or
`strict`) and `status.contractVersion` so automation can block promotion when a
tenant has not activated the validated contract.
