Jelajahi Sumber

document SQLite catalog and unique-index behavior

Update the compatibility matrix, statement, function, and protocol references for the SQLite catalog emulation, session state functions, result type OIDs, and unique-index enforcement added for the Gogs backend. Add the git.database.pizza Caddy route.
Danilo Fragoso 1 hari lalu
induk
melakukan
0e493e67e4

+ 7 - 0
deploy/Caddyfile

@@ -63,3 +63,10 @@ docs.database.pizza {
 
 
 import /home/pizzadatabase/testmarket/Caddyfile
 import /home/pizzadatabase/testmarket/Caddyfile
 import /home/pizzadatabase/pizza-admin/Caddyfile
 import /home/pizzadatabase/pizza-admin/Caddyfile
+
+git.database.pizza {
+	tls internal
+	encode zstd gzip
+	header X-Content-Type-Options nosniff
+	reverse_proxy 127.0.0.1:3001
+}

+ 2 - 0
src/content/docs/clients/postgresql.md

@@ -119,6 +119,8 @@ conn, err := pgx.Connect(ctx,
     "postgresql://u:pz_live_REPLACE_ME@db.database.pizza:5432/acme%2Fproduction?sslmode=disable")
     "postgresql://u:pz_live_REPLACE_ME@db.database.pizza:5432/acme%2Fproduction?sslmode=disable")
 ```
 ```
 
 
+pgx decodes the type metadata advertised for single-table direct column selects: `BIGINT` arrives as `int64` and `DATETIME`/`TIMESTAMP` columns arrive as `time.Time` (UTC, via `timestamptz`). Expressions and joins still report `TEXT`. An experimental Gogs fork uses a `pgx`-backed `database/sql` driver against a local PizzaSQL source build only — it is not a deployed configuration (see [Compatibility](/sql-reference/compatibility/)).
+
 ## Placeholders
 ## Placeholders
 
 
 PizzaSQL accepts both `?` (SQLite style) and `$1`, `$2` (PostgreSQL style) placeholders. Prefer the style your driver parameterizes natively — most Postgres drivers use `$1`, `$2`.
 PizzaSQL accepts both `?` (SQLite style) and `$1`, `$2` (PostgreSQL style) placeholders. Prefer the style your driver parameterizes natively — most Postgres drivers use `$1`, `$2`.

+ 1 - 1
src/content/docs/engine/indexes.md

@@ -52,7 +52,7 @@ If no index applies, the engine performs a **full table scan** (it reads every r
 
 
 ## UNIQUE indexes
 ## UNIQUE indexes
 
 
-`CREATE UNIQUE INDEX` stores the `unique` flag in the definition (it appears in `pg_indexes` introspection), but **uniqueness is not enforced**. Duplicate values are allowed. See [Constraints](/sql-reference/constraints/).
+`CREATE UNIQUE INDEX` stores the `unique` flag in the definition (it appears in `pg_indexes` introspection) **and enforces it** on the core write paths via validating scans; creating one against a table with existing duplicate non-`NULL` values is rejected. `NULL` values are exempt, matching SQLite. See [Constraints](/sql-reference/constraints/).
 
 
 ## Interaction with the row cache
 ## Interaction with the row cache
 
 

+ 1 - 1
src/content/docs/internals/catalog.md

@@ -41,7 +41,7 @@ This means concurrent DDL and DML from different connections eventually converge
 
 
 The catalog is deliberately minimal:
 The catalog is deliberately minimal:
 
 
-- It does **not** store `UNIQUE`, `CHECK`, or `FOREIGN KEY` constraints — those are discarded at `CREATE TABLE` (see [Constraints](/sql-reference/constraints/)).
+- It does **not** store `CHECK` or `FOREIGN KEY` constraints — those are discarded at `CREATE TABLE`. `UNIQUE` constraints are materialized into unique index definitions (see [Constraints](/sql-reference/constraints/)).
 - It does not store index definitions (those live in the `SchemaManager`/storage, not the analyzer catalog).
 - It does not store index definitions (those live in the `SchemaManager`/storage, not the analyzer catalog).
 - It does not store statistics, histograms, or anything a cost-based planner would use — there is no planner.
 - It does not store statistics, histograms, or anything a cost-based planner would use — there is no planner.
 
 

+ 6 - 2
src/content/docs/internals/postgres-protocol.md

@@ -41,12 +41,16 @@ Many drivers run introspection queries on connect. PizzaSQL intercepts and emula
 - `SHOW server_version` / `server_encoding` / `client_encoding`
 - `SHOW server_version` / `server_encoding` / `client_encoding`
 - `SELECT ... FROM information_schema.tables / columns / table_constraints / key_column_usage`
 - `SELECT ... FROM information_schema.tables / columns / table_constraints / key_column_usage`
 - `SELECT ... FROM pg_tables / pg_indexes`
 - `SELECT ... FROM pg_tables / pg_indexes`
+- `SELECT ... FROM sqlite_master` / `sqlite_schema` — read-only synthesized rows for SQLite migrators
+- `PRAGMA index_list` / `index_info` / `table_xinfo` — answered from the durable schema
 
 
-These are generated from the PizzaSQL schema, not a real PostgreSQL catalog. Filtering (`WHERE table_name = 'x'`, `WHERE schemaname = ...`) is recognized for simple equality patterns; other clauses (joins, subqueries against catalog tables) are not supported. The catalog is a **read-only compatibility shim**, not a queryable schema.
+These are generated from the PizzaSQL schema, not a real PostgreSQL or SQLite catalog. Filtering (`WHERE table_name = 'x'`, `WHERE schemaname = ...`) is recognized for simple equality patterns; other clauses (joins, subqueries against catalog tables) are not supported. The catalog is a **read-only compatibility shim**, not a queryable schema.
 
 
 ### Type mapping on results
 ### Type mapping on results
 
 
-Result columns are advertised with a small OID mapping: `INTEGER`/`INT` → `int4` (23), `TEXT`/`VARCHAR`/`CHAR` → `text` (25), `REAL`/`FLOAT` → `float4` (700), `DOUBLE` → `float8` (701), `BOOLEAN` → `bool` (16), `BLOB` → `bytea` (17), anything else → `text`. Values are sent in text format.
+Result columns are advertised with a small OID mapping: `INTEGER`/`INT` → `int4` (23), `BIGINT` → `int8` (20), `TEXT`/`VARCHAR`/`CHAR`/`UUID` → `text` (25), `REAL`/`FLOAT` → `float4` (700), `DOUBLE` → `float8` (701), `BOOLEAN` → `bool` (16), `BLOB` → `bytea` (17), `DATETIME`/`TIMESTAMP` → `timestamptz` (1184), anything else → `text`. Values are sent in text format; datetime values are exchanged as UTC RFC3339 so `timestamptz` decodes directly.
+
+Type metadata is populated only for **single-table direct column projections** (a column reference, an alias, or `SELECT *`), derived from schema metadata rather than row values so it holds even for empty results. Expressions, joins, multi-table queries, and unknown columns still report `TEXT` (25). A `UUID` column is stored as text and reports `text` (25), not the native PostgreSQL `uuid` OID. The advertised OID reflects the **declared** column type (`INTEGER` stays `int4`, for example) and only guides client decoding — it does not make values strictly typed: storage remains SQLite-style dynamic affinity.
 
 
 ### Protocol hardening
 ### Protocol hardening
 
 

+ 21 - 8
src/content/docs/sql-reference/compatibility.md

@@ -32,12 +32,12 @@ This page describes behavior verified in the current source and tests. The SQLit
 
 
 Do not rely on the following behavior yet:
 Do not rely on the following behavior yet:
 
 
-- `UNIQUE`, `CHECK`, and foreign-key declarations may be accepted but are not enforced.
+- `CHECK` and foreign-key declarations may be accepted but are 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.
 - Composite primary keys retain only their first column.
 - Composite primary keys retain only their first column.
 - `RIGHT JOIN`, `FULL JOIN`, `JOIN ... USING`, and `NATURAL JOIN` may parse but do not execute correctly.
 - `RIGHT JOIN`, `FULL JOIN`, `JOIN ... USING`, and `NATURAL JOIN` may parse but do not execute correctly.
 - DDL changes such as `CREATE TABLE` are not rolled back by `ROLLBACK`.
 - DDL changes such as `CREATE TABLE` are not rolled back by `ROLLBACK`.
 - `GROUP_CONCAT` and `TOTAL` are not safe to execute.
 - `GROUP_CONCAT` and `TOTAL` are not safe to execute.
-- PostgreSQL result columns may be reported as `TEXT` regardless of their SQL type.
+- Result column type metadata is only reported for single-table direct column projections; expressions, joins, and unknown columns are still reported as `TEXT`.
 
 
 Unsupported syntax normally returns an error. The items above are called out separately because accepting syntax without enforcing its semantics can cause incorrect application behavior.
 Unsupported syntax normally returns an error. The items above are called out separately because accepting syntax without enforcing its semantics can cause incorrect application behavior.
 
 
@@ -96,8 +96,8 @@ Unsupported syntax normally returns an error. The items above are called out sep
 | Single-column primary keys | Supported | Supported | Supported |
 | Single-column primary keys | Supported | Supported | Supported |
 | Composite primary keys | Unsafe; only the first column is retained | Supported | Supported |
 | Composite primary keys | Unsafe; only the first column is retained | Supported | Supported |
 | `NOT NULL` | Supported | Supported | Supported |
 | `NOT NULL` | Supported | Supported | Supported |
-| `UNIQUE` constraints | Unsafe; parsed but not enforced | Supported | Supported |
-| Unique indexes | Unsafe; uniqueness is not enforced | 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 |
 | `CHECK` constraints | Unsafe; parsed but not enforced | Supported | Supported |
 | Foreign keys | Unsafe; parsed but not enforced | Supported when enabled | Supported |
 | Foreign keys | Unsafe; parsed but not enforced | Supported when enabled | Supported |
 | Literal defaults | Partial | Supported | Supported |
 | Literal defaults | Partial | Supported | Supported |
@@ -123,7 +123,7 @@ PizzaSQL accepts many familiar type names but does not implement PostgreSQL's st
 | `NUMERIC(p,s)` precision enforcement | Unsupported | Unsupported | Supported |
 | `NUMERIC(p,s)` precision enforcement | Unsupported | Unsupported | Supported |
 | Native date and time types | Unsupported; values remain dynamically typed | Unsupported | Supported |
 | Native date and time types | Unsupported; values remain dynamically typed | Unsupported | Supported |
 | JSON values, functions, and operators | Unsupported; `JSON` names have no JSON semantics | Available with SQLite JSON support | Supported with `JSON` and `JSONB` |
 | JSON values, functions, and operators | Unsupported; `JSON` names have no JSON semantics | Available with SQLite JSON support | Supported with `JSON` and `JSONB` |
-| Arrays, UUIDs, enums, and intervals | Unsupported | Unsupported as native types | Supported |
+| Arrays, UUIDs, enums, and intervals | Partial; `UUID` is accepted as a text alias with no native PostgreSQL UUID semantics | Unsupported as native types | Supported |
 | `CAST` to integer, real, and text | Supported | Supported | Supported |
 | `CAST` to integer, real, and text | Supported | Supported | Supported |
 | Other `CAST` targets | Partial; many targets are no-ops | Affinity based | Strictly typed |
 | Other `CAST` targets | Partial; many targets are no-ops | Affinity based | Strictly typed |
 | PostgreSQL `::` casts | Unsupported | Unsupported | Supported |
 | PostgreSQL `::` casts | Unsupported | Unsupported | Supported |
@@ -144,6 +144,7 @@ PizzaSQL accepts many familiar type names but does not implement PostgreSQL's st
 | Common math functions | Partial | Supported | Supported |
 | Common math functions | Partial | Supported | Supported |
 | `CEIL`, `FLOOR`, and `MOD` | Unsafe; declared but currently return `NULL` | Supported | Supported |
 | `CEIL`, `FLOOR`, and `MOD` | Unsafe; declared but currently return `NULL` | Supported | Supported |
 | SQLite date and time functions | Supported | Supported | Different function set |
 | SQLite date and time functions | Supported | Supported | Different function set |
+| `last_insert_rowid()`, `changes()`, `total_changes()` | Supported; session-local | Supported | Unsupported |
 | PostgreSQL date and time functions | Unsupported | Unsupported | Supported |
 | PostgreSQL date and time functions | Unsupported | Unsupported | Supported |
 | JSON functions | Unsupported | Available with SQLite JSON support | Supported |
 | JSON functions | Unsupported | Available with SQLite JSON support | Supported |
 | Window functions | Unsupported | Supported | Supported |
 | Window functions | Unsupported | Supported | Supported |
@@ -172,7 +173,7 @@ PostgreSQL wire compatibility allows some PostgreSQL clients to connect. It does
 | Text parameters and results | Supported | Not applicable | Supported |
 | Text parameters and results | Supported | Not applicable | Supported |
 | Binary parameters | Partial; limited boolean and integer support | Not applicable | Supported |
 | Binary parameters | Partial; limited boolean and integer support | Not applicable | Supported |
 | Binary results | Unsupported | Not applicable | Supported |
 | Binary results | Unsupported | Not applicable | Supported |
-| Correct result type OIDs | Unsafe; normal query columns may be reported as `TEXT` | Not applicable | Supported |
+| Correct result type OIDs | Partial; single-table direct column projections report typed OIDs, expressions and joins report `TEXT` | Not applicable | Supported |
 | PostgreSQL TLS | Unsupported | Not applicable | Supported |
 | PostgreSQL TLS | Unsupported | Not applicable | Supported |
 | PostgreSQL SCRAM and MD5 authentication | Unsupported | Not applicable | Supported |
 | PostgreSQL SCRAM and MD5 authentication | Unsupported | Not applicable | Supported |
 | `information_schema` | Partial emulation | Unsupported | Supported |
 | `information_schema` | Partial emulation | Unsupported | Supported |
@@ -185,7 +186,7 @@ PostgreSQL wire compatibility allows some PostgreSQL clients to connect. It does
 | ORMs and migration frameworks | Unverified | Varies | Supported |
 | ORMs and migration frameworks | Unverified | Varies | Supported |
 | `psql` schema introspection commands | Partial | Not applicable | Supported |
 | `psql` schema introspection commands | Partial | Not applicable | Supported |
 
 
-Because the wire is PostgreSQL-compatible, drivers may issue introspection queries such as `information_schema`, `pg_catalog`, `SELECT version()`, or `SHOW server_version` on connection. PizzaSQL emulates a small subset of these so basic clients can start. See [PostgreSQL protocol](/internals/postgres-protocol/).
+Because the wire is PostgreSQL-compatible, drivers may issue introspection queries such as `information_schema`, `pg_catalog`, `SELECT version()`, or `SHOW server_version` on connection. PizzaSQL emulates a small subset of these so basic clients can start. SQLite-style migrators are also accommodated with a read-only `sqlite_master`/`sqlite_schema` emulation and the `index_list`, `index_info`, and `table_xinfo` pragmas. See [PostgreSQL protocol](/internals/postgres-protocol/).
 
 
 ## Managed service differences
 ## Managed service differences
 
 
@@ -207,10 +208,22 @@ The PizzaSQL repository includes the SQLite SQLLogicTest corpus and a custom run
 
 
 Compatibility claims should therefore be tied to explicit automated tests, not only to the presence or size of the corpus.
 Compatibility claims should therefore be tied to explicit automated tests, not only to the presence or size of the corpus.
 
 
+## Experimental Gogs backend evidence
+
+An experimental Gogs fork drives PizzaSQL through a custom `pgx`-backed `database/sql` driver that emits SQLite-dialect SQL over the PostgreSQL wire. This is a **local source-build exercise, not a deployed or production configuration**, and transport TLS remains unavailable. Its opt-in smoke test passes end to end against local engine binaries, covering:
+
+- Full fresh-start schema installation and table creation through GORM and XORM, migrations seeding, and XORM `Sync2`.
+- The install flow end to end: `GET /install` returns `200`, `POST /install` redirects with `302`, and the admin user is inserted and reachable through `/user/login`.
+- User, repository, access-token, and permission writes through the public stores, cross-ORM reads, uniqueness rejection, and transaction rollback.
+- Pool close/reopen and full engine-process plus PizzaKV restart against the same `.pkvdb` file, confirming installation and persistence.
+- Core LFS object metadata: multiple objects per repository, the same OID in a different repository kept distinct, duplicate `(repo_id, oid)` rejection, and reads back with a real `time.Time` `created_at`.
+
+This is **not** evidence of complete Gogs compatibility or of the Git LFS transfer path — actual Git LFS upload, SSH, and push have not been tested yet, and the full Gogs test suite has not passed against PizzaSQL.
+
 ## Advice for porting
 ## Advice for porting
 
 
 1. Use explicit `INNER`, `LEFT`, and `CROSS` joins with `ON` conditions.
 1. Use explicit `INNER`, `LEFT`, and `CROSS` joins with `ON` conditions.
-2. Enforce uniqueness, checks, and referential integrity in the application until engine enforcement lands.
+2. Enforce checks and referential integrity in the application. Uniqueness is enforced on the core write paths, but keep application-level checks for anything with stricter concurrency or DDL needs.
 3. Use a single-column primary key.
 3. Use a single-column primary key.
 4. Avoid `AUTOINCREMENT`, expression defaults, and `DEFAULT CURRENT_TIMESTAMP` for semantic behavior.
 4. Avoid `AUTOINCREMENT`, expression defaults, and `DEFAULT CURRENT_TIMESTAMP` for semantic behavior.
 5. Use `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX`; verify other functions against the [function reference](/sql-reference/functions/).
 5. Use `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX`; verify other functions against the [function reference](/sql-reference/functions/).

+ 25 - 7
src/content/docs/sql-reference/constraints.md

@@ -3,7 +3,7 @@ title: Constraints
 description: What PRIMARY KEY, NOT NULL, DEFAULT, UNIQUE, CHECK, and FOREIGN KEY actually do in PizzaSQL — and which ones are not enforced.
 description: What PRIMARY KEY, NOT NULL, DEFAULT, UNIQUE, CHECK, and FOREIGN KEY actually do in PizzaSQL — and which ones are not enforced.
 ---
 ---
 
 
-Constraints in PizzaSQL fall into two camps: the ones that are actually implemented, and the ones that merely parse. This page is deliberately blunt about the difference, because silently assuming a `UNIQUE` or `FOREIGN KEY` constraint is enforced will corrupt data.
+Constraints in PizzaSQL fall into two camps: the ones that are actually implemented, and the ones that merely parse. This page is deliberately blunt about the difference, because silently assuming a `CHECK` or `FOREIGN KEY` constraint is enforced will corrupt data. `UNIQUE` is now enforced on the core write paths, with caveats described below.
 
 
 ## The enforced constraints
 ## The enforced constraints
 
 
@@ -31,27 +31,45 @@ Constraints in PizzaSQL fall into two camps: the ones that are actually implemen
 - `AUTOINCREMENT` **parses but has no behaviour**. It does not prevent rowid reuse, does not maintain a separate sequence, and behaves exactly like a plain `INTEGER PRIMARY KEY`.
 - `AUTOINCREMENT` **parses but has no behaviour**. It does not prevent rowid reuse, does not maintain a separate sequence, and behaves exactly like a plain `INTEGER PRIMARY KEY`.
 - It is retained in the schema JSON for compatibility but is never read.
 - It is retained in the schema JSON for compatibility but is never read.
 
 
+## UNIQUE
+
+`UNIQUE` is enforced, but through validating scans rather than a durable uniqueness claim:
+
+- `CREATE UNIQUE INDEX` and inline/table-level `UNIQUE` constraints both materialize into a unique index definition.
+- The core write paths (`INSERT`, `UPDATE`, `DELETE`, and their `ON CONFLICT`/`OR` variants) validate non-`NULL` values against the table's unique indexes before persisting, rejecting duplicates with `UNIQUE constraint failed`.
+- `CREATE UNIQUE INDEX` on a table that already contains duplicate non-`NULL` values is rejected and the index is not registered.
+- `NULL` values are exempt — multiple `NULL`s are allowed, matching SQLite.
+- Composite unique indexes are enforced, and integral numerics are canonicalized so `1` (integer) and `1.0` (real) collide the way SQLite's affinity does.
+
+The implementation is scan-based: a table with a unique index serializes its core writes on the table gate so the validating scan cannot race another writer, and multi-row DML against such a table runs in an implicit transaction for statement-level atomicity. This is **not** full SQLite or PostgreSQL concurrency semantics, and DDL remains non-transactional. See [Indexes](/engine/indexes/) and [Compatibility](/sql-reference/compatibility/).
+
+### Fresh schemas vs. existing tables
+
+`UNIQUE` constraints are materialized into indexes **only when the table is created**. The effect applies to newly created tables, not retroactively:
+
+- A table created now with `UNIQUE` (inline, table-level, or `CREATE UNIQUE INDEX`) is enforced from the start.
+- A table created by an older engine build, whose `UNIQUE` declarations were silently discarded, is **not** magically enforced after an upgrade — nothing re-parses the old `CREATE TABLE` statement, so the constraint was never retained.
+- After upgrading, re-issue `CREATE UNIQUE INDEX` for any column that must be unique on a pre-existing table; that path validates existing rows and rejects the index if duplicates are already present.
+
+Do not assume an existing table's uniqueness is enforced just because the engine now supports `UNIQUE`.
+
 ## The non-enforced constraints
 ## The non-enforced constraints
 
 
 These are accepted by the parser and then **silently discarded** — the schema does not store them and no code checks them:
 These are accepted by the parser and then **silently discarded** — the schema does not store them and no code checks them:
 
 
 | Constraint | Parsed? | Stored? | Enforced? |
 | Constraint | Parsed? | Stored? | Enforced? |
 | --- | --- | --- | --- |
 | --- | --- | --- | --- |
-| `UNIQUE (col)` | yes | no | **no** |
-| `UNIQUE` column constraint | yes | no | **no** |
 | `CHECK (expr)` | yes | no | **no** |
 | `CHECK (expr)` | yes | no | **no** |
 | `FOREIGN KEY ... REFERENCES ...` | yes | no | **no** |
 | `FOREIGN KEY ... REFERENCES ...` | yes | no | **no** |
-| `CREATE UNIQUE INDEX` | yes | definition stored | **no** |
 
 
 Consequences to internalize:
 Consequences to internalize:
 
 
-- Duplicate values are allowed in any column except the primary key. If you need uniqueness, enforce it in your application.
+- 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.
 - 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`.
 - `CHECK` constraints never run. `CREATE TABLE t (age INTEGER CHECK (age > 0))` happily accepts `-5`.
-- `CREATE UNIQUE INDEX` records an index marked `unique` in the catalog (it shows up in `pg_indexes`), but the uniqueness flag is cosmetic — the index still only accelerates lookups and does not reject duplicates.
 
 
 ## Why this matters for migrations
 ## Why this matters for migrations
 
 
-SQLite-flavoured schemas often lean on `UNIQUE` and `FOREIGN KEY` for integrity. When importing such a schema (SQL dump or SQLite file), those constraints are **dropped silently** rather than erroring. Inspect the imported schema afterwards and add application-level checks for anything that must stay unique or referentially consistent.
+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.
 
 
 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.
 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/data-types.md

@@ -33,7 +33,7 @@ This is substring-based and case-insensitive. `VARCHAR(255)`, `TEXT`, `NCHAR`, a
 | `BLOB` | BLOB | stored as a string in practice |
 | `BLOB` | BLOB | stored as a string in practice |
 | `BOOLEAN`, `BOOL` | BOOLEAN | stored as 1/0 |
 | `BOOLEAN`, `BOOL` | BOOLEAN | stored as 1/0 |
 
 
-There is no `ARRAY`, `JSONB`-specific, `UUID`, `SERIAL`, or `ENUM` type. There are no schemas or user-defined types.
+There is no `ARRAY`, `JSONB`-specific, `SERIAL`, or `ENUM` type. There are no schemas or user-defined types. `UUID` is accepted as a type name but is stored with **TEXT affinity** — it is a plain text column with no native PostgreSQL UUID semantics.
 
 
 ## How values are actually stored
 ## How values are actually stored
 
 
@@ -61,6 +61,7 @@ Values are coerced on use, not on insert. The key rules:
 Every row carries an implicit **`_rowid_`**, even when no `INTEGER PRIMARY KEY` is declared:
 Every row carries an implicit **`_rowid_`**, even when no `INTEGER PRIMARY KEY` is declared:
 
 
 - `rowid`, `oid`, and `_rowid_` all refer to the same value.
 - `rowid`, `oid`, and `_rowid_` all refer to the same value.
+- An **explicit column** named `rowid`, `oid`, or `_rowid_` takes precedence over the hidden rowid alias, matching SQLite — so a table with a real `oid TEXT` column resolves `oid` to that text column, not the integer rowid.
 - `INTEGER PRIMARY KEY` (exactly an integer type, single column) **aliases the rowid** — the primary-key value *is* the rowid, and inserting without a value auto-assigns the next one.
 - `INTEGER PRIMARY KEY` (exactly an integer type, single column) **aliases the rowid** — the primary-key value *is* the rowid, and inserting without a value auto-assigns the next one.
 - Any other primary key (e.g. `TEXT PRIMARY KEY`, or a table-level PK) is a normal column; the rowid is a separate, invisible counter maintained in parallel.
 - Any other primary key (e.g. `TEXT PRIMARY KEY`, or a table-level PK) is a normal column; the rowid is a separate, invisible counter maintained in parallel.
 - `_rowid_` is not included in `SELECT *`; reference it explicitly (`SELECT rowid, * FROM t`).
 - `_rowid_` is not included in `SELECT *`; reference it explicitly (`SELECT rowid, * FROM t`).

+ 15 - 3
src/content/docs/sql-reference/functions.md

@@ -100,6 +100,21 @@ Date/time functions accept SQLite-style time values and modifiers:
 | `pizzasql_version()` | engine build version |
 | `pizzasql_version()` | engine build version |
 | `sqlite_version()` | same value, for SQLite compatibility |
 | `sqlite_version()` | same value, for SQLite compatibility |
 
 
+### Session state
+
+These mirror SQLite's connection-local counters and are tracked per connection:
+
+| Function | Description |
+| --- | --- |
+| `last_insert_rowid()` | rowid of the most recent successful `INSERT` on this connection; `0` before any insert |
+| `changes()` | number of rows changed by the most recent `INSERT`/`UPDATE`/`DELETE` |
+| `total_changes()` | total rows changed since this connection opened; monotonic, never decremented by `ROLLBACK` |
+
+Notes:
+
+- `last_insert_rowid()` reflects the actual generated rowid (not a naive counter or `MAX`), including for multi-row `INSERT` and `INSERT ... SELECT`, and holds across `ROLLBACK`.
+- `total_changes()` is incremented by every completed DML statement even when the change is later undone, matching SQLite.
+
 ## Declared but not implemented
 ## Declared but not implemented
 
 
 These names are recognized by the parser/analyzer but **return `NULL` when called**. Treat them as unsupported:
 These names are recognized by the parser/analyzer but **return `NULL` when called**. Treat them as unsupported:
@@ -110,11 +125,8 @@ These names are recognized by the parser/analyzer but **return `NULL` when calle
 - `iif`
 - `iif`
 - `quote`
 - `quote`
 - `total`, `group_concat`
 - `total`, `group_concat`
-- `last_insert_rowid`, `changes`, `total_changes`
 - `randomblob` is implemented in the executor but not registered with the analyzer, so calls are currently rejected as an unknown function.
 - `randomblob` is implemented in the executor but not registered with the analyzer, so calls are currently rejected as an unknown function.
 
 
-The `last_insert_rowid` gap matters in practice: after an `INSERT` with an auto-generated key, there is no built-in function to retrieve the generated id. If you need it, insert an explicit value instead of relying on auto-generation.
-
 ## Caveats
 ## Caveats
 
 
 - `round` uses `int64(v*mult + 0.5)` and only behaves correctly for non-negative decimal counts; treat it as approximate for edge cases.
 - `round` uses `int64(v*mult + 0.5)` and only behaves correctly for non-negative decimal counts; treat it as approximate for edge cases.

+ 14 - 5
src/content/docs/sql-reference/statements.md

@@ -60,7 +60,7 @@ INSERT INTO table ... ON CONFLICT [(pk)] DO UPDATE SET col = expr, ...;
   - `INSERT OR REPLACE` deletes the conflicting row and inserts the new one.
   - `INSERT OR REPLACE` deletes the conflicting row and inserts the new one.
   - `INSERT OR FAIL`/`OR ABORT` abort on the first duplicate.
   - `INSERT OR FAIL`/`OR ABORT` abort on the first duplicate.
   - `ON CONFLICT (target) DO NOTHING` / `DO UPDATE SET ...` work only when the conflict is on the primary key; the `(target)` list must name the PK column (or be omitted).
   - `ON CONFLICT (target) DO NOTHING` / `DO UPDATE SET ...` work only when the conflict is on the primary key; the `(target)` list must name the PK column (or be omitted).
-- Auto-generated integer primary keys (and the implicit rowid) are assigned when no PK value is supplied. There is **no `RETURNING`** and no way to read back the generated id in the same statement; use `SELECT last_insert_rowid()`-style patterns with caution (see [Functions](/sql-reference/functions/) — the rowid functions are not implemented).
+- Auto-generated integer primary keys (and the implicit rowid) are assigned when no PK value is supplied. There is **no `RETURNING`**, but `SELECT last_insert_rowid()` returns the rowid of the most recent successful `INSERT` on this connection (see [Functions](/sql-reference/functions/)).
 
 
 ## UPDATE and DELETE
 ## UPDATE and DELETE
 
 
@@ -87,9 +87,10 @@ CREATE TABLE [IF NOT EXISTS] table (
 ```
 ```
 
 
 - `IF NOT EXISTS` silently succeeds if the table already exists.
 - `IF NOT EXISTS` silently succeeds if the table already exists.
-- Only `PRIMARY KEY`, `NOT NULL`, and `DEFAULT` have an effect. `AUTOINCREMENT` is accepted but adds no behavior; `UNIQUE`, `CHECK`, and `FOREIGN KEY` are parsed and discarded — see [Constraints](/sql-reference/constraints/).
+- `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.
 - A table-level `PRIMARY KEY (a, b)` uses the first named column as the primary key; composite keys are not truly supported.
 - 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.
+- `DEFAULT expr` is evaluated when the table is created, so it must be a constant expression. Signed numeric literals (`DEFAULT -1`, `DEFAULT +5`, `DEFAULT (-7)`) are accepted.
+- A bare `NULL` column specifier is accepted as a no-op (columns are nullable by default); `NOT NULL` still applies whenever it appears.
 - `AUTOINCREMENT` is accepted but has no extra behaviour over a plain `INTEGER PRIMARY KEY`.
 - `AUTOINCREMENT` is accepted but has no extra behaviour over a plain `INTEGER PRIMARY KEY`.
 
 
 ## ALTER TABLE
 ## ALTER TABLE
@@ -116,7 +117,7 @@ DROP VIEW [IF EXISTS] view;
 ```
 ```
 
 
 - Index definitions persist, but index entries are rebuilt in memory and only used for single-column equality lookups — see [Indexes](/engine/indexes/).
 - Index definitions persist, but index entries are rebuilt in memory and only used for single-column equality lookups — see [Indexes](/engine/indexes/).
-- `UNIQUE` indexes are recorded as unique in the catalog but **uniqueness is not enforced**.
+- `UNIQUE` indexes are recorded as unique in the catalog **and enforced** on the core write paths via validating scans; `CREATE UNIQUE INDEX` rejects a table that already contains duplicate non-`NULL` values.
 - Views are **connection-local and in-memory**: they exist only within the connection that created them and are not persisted or shared. See [Catalog & schema](/internals/catalog/).
 - Views are **connection-local and in-memory**: they exist only within the connection that created them and are not persisted or shared. See [Catalog & schema](/internals/catalog/).
 
 
 ## Transactions
 ## Transactions
@@ -138,6 +139,9 @@ ROLLBACK TO name;
 
 
 ```sql
 ```sql
 PRAGMA table_info(t);      -- column list for table t
 PRAGMA table_info(t);      -- column list for table t
+PRAGMA table_xinfo(t);     -- table_info columns plus a trailing hidden flag
+PRAGMA index_list(t);      -- indexes on table t (seq, name, unique, origin, partial)
+PRAGMA index_info(idx);    -- columns of index idx (seqno, cid, name)
 PRAGMA table_list;         -- all tables
 PRAGMA table_list;         -- all tables
 PRAGMA database_list;      -- attached databases
 PRAGMA database_list;      -- attached databases
 PRAGMA version;            -- engine version
 PRAGMA version;            -- engine version
@@ -146,9 +150,14 @@ EXPLAIN stmt;              -- simplified opcode listing
 EXPLAIN QUERY PLAN stmt;   -- SCAN/FILTER/SORT/LIMIT description
 EXPLAIN QUERY PLAN stmt;   -- SCAN/FILTER/SORT/LIMIT description
 ```
 ```
 
 
-- Only the four `PRAGMA` forms above are implemented; any other pragma name errors.
+- `table_xinfo`, `index_list`, and `index_info` exist to satisfy SQLite migrators (GORM/XORM); they are answered from the durable schema, not a real SQLite file.
+- Only the `PRAGMA` forms above are implemented; any other pragma name errors.
 - `EXPLAIN` output is **illustrative only** — it does not reflect the real execution engine (there is no cost-based planner; see [Query lifecycle](/internals/query-lifecycle/)).
 - `EXPLAIN` output is **illustrative only** — it does not reflect the real execution engine (there is no cost-based planner; see [Query lifecycle](/internals/query-lifecycle/)).
 
 
+## SQLite catalog tables
+
+`sqlite_master` and `sqlite_schema` are emulated as **read-only** virtual tables whose rows are synthesized from the durable schema (`type`, `name`, `tbl_name`, `rootpage`, `sql`). Single-table `SELECT` queries against them work for migrator introspection; they are not real writable SQLite catalog tables and are not persisted. See [PostgreSQL protocol](/internals/postgres-protocol/).
+
 ## ATTACH / DETACH DATABASE
 ## ATTACH / DETACH DATABASE
 
 
 ```sql
 ```sql