# ALTER TABLE ADD CONSTRAINT `name` CHECK: `check` is a where filter every row must match,

`POST /datanotes/operations/add_constraint/` (operation `add_constraint`)

ALTER TABLE ADD CONSTRAINT `name` CHECK: `check` is a where filter every row must match,
e.g. `{"value": {"gte": 1, "lte": 5}}`. A condition on a column the row leaves empty passes, like SQL;
inside `or` / `and` / `not` an empty value is tested as it is (`{"exists": true}` there fails).
A key may follow a link column to the row it names, as in queries: `{"parent.kind": "project"}`
(further dots follow further links). An empty link passes at the top level and fails inside `or` / `and` /
`not`; only the first link of a longer path is skipped when empty (`parent.parent.kind` fails for a row
whose parent has no parent). A link to a row that does not exist fails; on a list of links every linked
row must match. `not` turns that into a pass: `{"not": {"parent.kind": "area"}}` passes a link to a
missing row, so add `"parent.kind": {"exists": true}` to require a real one. Links resolve as in the
vault (by path, from the row's note: same-named rows in two folders stay apart); renaming or moving a
linked row keeps them (they follow it).
Refused while existing rows violate it (they are listed); then enforced on every write, by the same rule.
A write that changes a linked row re-checks the rows that link to it and is refused, listing them, when
it breaks them, naming the changed row (in a batch, re-link them first: each step is checked on the vault
the earlier ones left); the inverse links a write adds, undo_migration and restore_table are checked too.
rename_column / rename_value follow into CHECKs; drop_column of a column a CHECK reads is refused.

The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Datanotes-Actor` | header | `string` | no | JSON: on whose behalf the write is made, recorded in the journal (`harness`, `model`, `session`). |

## Request body

`application/json`, required

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `table` | `string` | yes |  |  |
| `name` | `string` | yes |  |  |
| `check` | `object` | yes |  |  |
| `dry_run` | `boolean` | no | `false` |  |

## Responses

| Status | Description | Body |
| --- | --- | --- |
| 200 | Done. | `OpResult` (application/json) |
| 400 | Bad input: the body is not a JSON object, or an argument is unknown, missing or of the wrong type. | `Error` (application/json) |
| 401 | Missing or wrong bearer token. | `Error` (application/json) |
| 403 | A named token beyond its permissions (the engine's administration, tables outside its `write` patterns, schema changes without `ddl`). | `Error` (application/json) |
| 404 | Unknown table, view, migration, note or folder. | `Error` (application/json) |
| 409 | Ambiguous table name: it matches several tables (`paths` or `tables` lists them). | `AmbiguousName` (application/json) |
| 422 | Refused (validation failures, conflicts, an unmet `expect`): nothing written. Same body as 200, with `ok: false` and the reasons. | `OpResult` (application/json) |
| 500 | Unexpected error (`{error}`), or a write that failed partway and could not be rolled back: `rolled_back: false`, `failed` lists the files left (undo_migration on `migration_id` retries the rollback). | `OpResult \| Error` (application/json) |
| 503 | A write that failed partway (a file another program holds) and was rolled back: `rolled_back: true`, nothing written. Send it again later. Same body as 200, with `ok: false` and `failed`. | `OpResult` (application/json) |

The schemas named here are in [openapi.json](https://datanotes.dev/openapi.json) (`components.schemas`).
