Просмотр исходного кода

docs: CHECK and FOREIGN KEY are rejected at DDL, not discarded

Update constraints, statements, and compatibility now that the engine returns
0A000 for inline/table-level CHECK and FOREIGN KEY (tasks #61/#62). Note that
GoatCounter's retired schema is intentionally no longer bootstrappable and
that Gogs/Vikunja declare neither clause.
Docs Bot 2 дней назад
Родитель
Сommit
984d038500

+ 4 - 4
src/content/docs/sql-reference/compatibility.md

@@ -50,7 +50,7 @@ The engine repository also runs a pChat-style compatibility suite (`make compat`
 
 Do not rely on the following behavior yet:
 
-- `CHECK` and foreign-key declarations are accepted but not enforced. `UNIQUE` constraints and unique indexes are enforced on the core write paths, but only through validating scans — this is not a durable uniqueness claim and does not imply full SQLite or PostgreSQL concurrency semantics. `UNIQUE` is materialized only when a table is created; tables created before this support are not retroactively enforced after an upgrade.
+- `CHECK` and foreign-key declarations are rejected at DDL with SQLSTATE `0A000`; they are never accepted and discarded. `UNIQUE` constraints and unique indexes are enforced on the core write paths, but only through validating scans — this is not a durable uniqueness claim and does not imply full SQLite or PostgreSQL concurrency semantics. `UNIQUE` is materialized only when a table is created; tables created before this support are not retroactively enforced after an upgrade.
 - Composite primary keys are rejected at DDL with SQLSTATE `0A000`; they are never silently reduced to the first column.
 - Non-constant column defaults (`DEFAULT CURRENT_TIMESTAMP`, function calls) are rejected at DDL with `0A000`; there is no per-INSERT default evaluation.
 - Array parameters and `ANY`/`ALL` predicates are rejected with `0A000`; rewrite them as `IN (...)` with individual parameters.
@@ -109,7 +109,7 @@ Unsupported syntax normally returns an error. The items above are called out sep
 | --- | --- | --- | --- |
 | `CREATE TABLE` and `DROP TABLE` | Supported | Supported | Supported |
 | `CREATE TABLE IF NOT EXISTS` | Supported | Supported | Supported |
-| `ALTER TABLE ADD/DROP/RENAME` | Partial | Partial | Supported |
+| `ALTER TABLE ADD/DROP/RENAME` | Partial; no backfill on ADD, DROP rewrites rows, RENAME leaves index metadata stale | Partial | Supported |
 | Altering column type, default, or nullability | Unsupported | Limited | Supported |
 | `CREATE INDEX` and `DROP INDEX` | Partial; indexes are rebuilt from stored rows | Supported | Supported |
 | `CREATE INDEX IF NOT EXISTS` | Supported in current source; deployed versions may differ | Supported | Supported |
@@ -120,8 +120,8 @@ Unsupported syntax normally returns an error. The items above are called out sep
 | `NOT NULL` | Supported | Supported | Supported |
 | `UNIQUE` constraints | Partial; enforced on core write paths via validating scans | Supported | Supported |
 | Unique indexes | Partial; enforced on core write paths via validating scans | Supported | Supported |
-| `CHECK` constraints | Unsafe; parsed but not enforced | Supported | Supported |
-| Foreign keys | Unsafe; parsed but not enforced | Supported when enabled | Supported |
+| `CHECK` constraints | Rejected with `0A000` | Supported | Supported |
+| Foreign keys | Rejected with `0A000`; cascades are not implemented | Supported when enabled | Supported |
 | Literal defaults | Supported (constant values) | Supported | Supported |
 | Expression defaults | Rejected with `0A000` | Supported | Supported |
 | `DEFAULT CURRENT_TIMESTAMP` | Rejected with `0A000`; no per-row evaluation | Supported | Supported |

+ 9 - 11
src/content/docs/sql-reference/constraints.md

@@ -59,23 +59,21 @@ The wire protocol reports a duplicate as SQLSTATE `23505` with the violated inde
 
 Do not assume an existing table's uniqueness is enforced just because the engine now supports `UNIQUE`.
 
-## The non-enforced constraints
+## The rejected constraints
 
-These are accepted by the parser and then **silently discarded** — the schema does not store them and no code checks them:
+`CHECK` and `FOREIGN KEY` are **rejected at DDL** with SQLSTATE `0A000` instead of being accepted and discarded:
 
-| Constraint | Parsed? | Stored? | Enforced? |
-| --- | --- | --- | --- |
-| `CHECK (expr)` | yes | no | **no** |
-| `FOREIGN KEY ... REFERENCES ...` | yes | no | **no** |
+| Constraint | Status | What to do instead |
+| --- | --- | --- |
+| `CHECK (expr)` | Rejected at `CREATE TABLE` and `ALTER TABLE ADD COLUMN` | Validate in the application, or enforce through a unique/foreign-key-free design (a partial unique index is not available either). |
+| `FOREIGN KEY ... REFERENCES ...` | Rejected, including inline `REFERENCES`, table-level `FOREIGN KEY`, named constraints, and `ON DELETE`/`ON UPDATE` actions | Enforce referential integrity in the application; cascades are not implemented. |
 
-Consequences to internalize:
+This is deliberate: accepting the clause while never evaluating it made applications believe a constraint existed. A migration that carries `CHECK`/`FOREIGN KEY` from PostgreSQL or SQLite now fails loudly at the DDL statement and must be rewritten. Gogs and Vikunja declare neither clause; GoatCounter's retired schema does, so a fresh GoatCounter bootstrap is not supported on this engine build.
 
-- Duplicate values are allowed in any column except the primary key and non-`NULL` unique-indexed columns.
-- Foreign-key relationships are not validated; deleting a parent row does not cascade, restrict, or set null on children.
-- `CHECK` constraints never run. `CREATE TABLE t (age INTEGER CHECK (age > 0))` happily accepts `-5`.
+Constraints that remain accepted and enforced are `PRIMARY KEY` (single column), `NOT NULL`, `UNIQUE`/unique indexes, and constant `DEFAULT`s. Duplicate values are allowed in any other column.
 
 ## Why this matters for migrations
 
-SQLite-flavoured schemas often lean on `UNIQUE` and `FOREIGN KEY` for integrity. `UNIQUE` constraints now import into enforced unique indexes, but `FOREIGN KEY` is still **dropped silently** rather than erroring. Inspect the imported schema afterwards and add application-level checks for anything that must stay referentially consistent.
+SQLite- and PostgreSQL-flavoured schemas often lean on `UNIQUE`, `CHECK`, and `FOREIGN KEY` for integrity. `UNIQUE` imports into enforced unique indexes, but `CHECK`/`FOREIGN KEY` are rejected. Rewrite those DDL statements and add application-level checks for anything that must stay referentially consistent before pointing a migrator at PizzaSQL.
 
 See [Compatibility](/sql-reference/compatibility/) for the wider gap list, and [Indexes](/engine/indexes/) for how `CREATE INDEX`/`UNIQUE INDEX` actually behave at the storage layer.

+ 2 - 1
src/content/docs/sql-reference/statements.md

@@ -89,7 +89,8 @@ CREATE TABLE [IF NOT EXISTS] table (
 ```
 
 - `IF NOT EXISTS` silently succeeds if the table already exists.
-- `PRIMARY KEY`, `NOT NULL`, and `DEFAULT` have an effect; `AUTOINCREMENT` is accepted but adds no behavior. Inline and table-level `UNIQUE` constraints are materialized into unique indexes and enforced on the core write paths — see [Constraints](/sql-reference/constraints/). `CHECK` and `FOREIGN KEY` are still parsed and discarded.
+- `PRIMARY KEY`, `NOT NULL`, and constant `DEFAULT`s have an effect; `AUTOINCREMENT` is accepted but adds no behavior. Inline and table-level `UNIQUE` constraints are materialized into unique indexes and enforced on the core write paths — see [Constraints](/sql-reference/constraints/).
+- `CHECK` and `FOREIGN KEY` are rejected at DDL with SQLSTATE `0A000` (inline and table-level, including named constraints and referential actions). They are not silently discarded; enforce those rules in the application.
 - A table-level `PRIMARY KEY (a, b)` uses the first named column as the primary key; composite keys are not truly supported.
 - `DEFAULT expr` is evaluated when the table is created, so it must be a constant expression and is stored as the evaluated value. Signed numeric literals (`DEFAULT -1`, `DEFAULT +5`, `DEFAULT (-7)`), strings, blobs, `NULL`, and boolean literals are accepted. A non-constant default such as `DEFAULT CURRENT_TIMESTAMP`, `DEFAULT datetime('now')`, or `DEFAULT lower('X')` is rejected at DDL with SQLSTATE `0A000` rather than being frozen at table-creation time. There is no per-row default evaluation; supply the value on `INSERT` instead.
 - A bare `NULL` column specifier is accepted as a no-op (columns are nullable by default); `NOT NULL` still applies whenever it appears.