You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SQLR-10 — honor CREATE TABLE IF NOT EXISTS + make sqlrite_master queryable (#151)
Two engine ergonomics gaps that every embedding SDK consumer hits when
writing idempotent "run my schema on startup" migrations:
1. `CREATE TABLE IF NOT EXISTS` was parsed but ignored — a second create
of an existing table errored "table already exists", unlike
`CREATE INDEX IF NOT EXISTS`. Now `CreateQuery` carries the
`if_not_exists` flag and the executor treats a re-create as a no-op
(by name only; no schema diff, matching SQLite).
2. The schema catalog wasn't reachable from SQL. Added:
- `SELECT … FROM sqlrite_master` — synthesizes a read-only in-memory
catalog table on demand (type/name/sql/rootpage/last_rowid), reusing
the same SQL synthesis the persistence path uses. Works through the
normal single-table SELECT path (WHERE/projection/ORDER BY/LIMIT);
reflects live in-memory state. Writes remain rejected; joins against
it are out of scope.
- `PRAGMA table_list` — SQLite-canonical lightweight introspection
(schema/name/type/ncol/wr/strict) listing tables + sqlrite_master.
Covers the REPL and every SDK (all route through execute_select_rows /
process_command). Docs (supported-sql.md, README) updated; the
go-collector example's now-stale workaround comments refreshed.
Tests: parser flag capture; IF NOT EXISTS idempotency + still-errors
without it + fresh-create; sqlrite_master listing/filtering/SELECT */
write-rejection/save-reopen; PRAGMA table_list listing + value-rejection.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|`CREATE TABLE`|`PRIMARY KEY`, `UNIQUE`, `NOT NULL`; `IF NOT EXISTS` (idempotent re-create); duplicate-column detection; types `INTEGER`/`INT`/`BIGINT`/`SMALLINT`, `TEXT`/`VARCHAR`, `REAL`/`FLOAT`/`DOUBLE`/`DECIMAL`, `BOOLEAN`. Auto-creates `sqlrite_autoindex_<table>_<col>` for every PK + UNIQUE column |
176
176
|`CREATE [UNIQUE] INDEX`| Single-column, named indexes; `IF NOT EXISTS`; persists as a dedicated cell-based B-Tree. INTEGER + TEXT columns only |
177
177
|`INSERT INTO`| Explicit column list required; auto-ROWID for `INTEGER PRIMARY KEY`; multi-row `VALUES (…), (…)`; UNIQUE enforcement; clean type errors (no panics); NULL padding for omitted columns |
178
-
|`SELECT`|`*` or column list with optional `AS alias`; `WHERE`; `DISTINCT`; `GROUP BY col[, col …]`; aggregate projections `COUNT(*)` / `COUNT([DISTINCT] col)` / `SUM` / `AVG` / `MIN` / `MAX`; `[INNER\|LEFT OUTER\|RIGHT OUTER\|FULL OUTER] JOIN ... ON ...` with table aliases and qualified `t.col` references; single-column `ORDER BY [ASC\|DESC]` (also resolves alias and aggregate display names); `LIMIT n`. `WHERE col = literal` probes an index when one exists |
178
+
|`SELECT`|`*` or column list with optional `AS alias`; `WHERE`; `DISTINCT`; `GROUP BY col[, col …]`; aggregate projections `COUNT(*)` / `COUNT([DISTINCT] col)` / `SUM` / `AVG` / `MIN` / `MAX`; `[INNER\|LEFT OUTER\|RIGHT OUTER\|FULL OUTER] JOIN ... ON ...` with table aliases and qualified `t.col` references; single-column `ORDER BY [ASC\|DESC]` (also resolves alias and aggregate display names); `LIMIT n`. `WHERE col = literal` probes an index when one exists. Catalog introspection via `SELECT … FROM sqlrite_master`|
179
179
|`UPDATE`| Multi-column `SET`; `WHERE`; UNIQUE + type enforcement; arithmetic in assignments (`SET age = age + 1`) |
180
180
|`DELETE`|`WHERE` predicate or full-table delete |
181
181
|`BEGIN` / `COMMIT` / `ROLLBACK`| Real transactions, snapshot-based; WAL-backed commit; single-level (no savepoints); auto-rollback if `COMMIT`'s disk write fails |
182
182
|`PRAGMA auto_vacuum`| Read (`PRAGMA auto_vacuum;`) returns the trigger threshold as a single-row result set; set (`PRAGMA auto_vacuum = 0.5;` / `= OFF;` / `= NONE;`) tunes or disables auto-VACUUM at the SQL layer for SDK / FFI / MCP consumers |
183
+
|`PRAGMA table_list`| Lists tables (`schema`, `name`, `type`, `ncol`, `wr`, `strict`) plus the `sqlrite_master` catalog — lightweight catalog introspection for SDK / FFI / MCP consumers |
CREATE TABLE [IF NOT EXISTS] <name> (<col><type> [column_constraint]* [, ...]);
56
56
```
57
57
58
+
`IF NOT EXISTS` (SQLR-10) makes a re-create a no-op when a table of that name already exists, instead of erroring — so "run my schema on every startup" migrations work against a populated database. The clause is honoured **by name only**: SQLRite does not diff the existing schema against the new column list (SQLite behaves the same). Without `IF NOT EXISTS`, re-creating an existing table still errors.
-`Cannot create, table already exists.` — duplicate `CREATE TABLE` (suppressed to a no-op when the statement uses `IF NOT EXISTS`).
84
86
-`'sqlrite_master' is a reserved name used by the internal schema catalog` — you tried to shadow the catalog table.
85
87
-`Column 'foo' appears more than once in the table definition` — duplicate column names.
86
88
-`PRIMARY KEY column must be INTEGER` — PK on a non-integer column.
@@ -108,7 +110,7 @@ Every `PRIMARY KEY` and every `UNIQUE` column gets an auto-index at `CREATE TABL
108
110
sqlrite_autoindex_<table>_<column>
109
111
```
110
112
111
-
These are full-citizen indexes — they're visible via `.tables`-adjacent catalog queries (once those land), persist across saves, and accelerate equality probes. You don't need to `CREATE INDEX` them yourself.
113
+
These are full-citizen indexes — they show up in [`sqlrite_master`](#querying-the-catalog-sqlrite_master) catalog queries, persist across saves, and accelerate equality probes. You don't need to `CREATE INDEX` them yourself.
112
114
113
115
### HNSW indexes (Phase 7d)
114
116
@@ -267,6 +269,34 @@ The executor includes a tiny optimizer: if the `WHERE` is exactly `<indexed_col>
267
269
268
270
Any of the above reaches the executor as a parsed AST node that execution doesn't handle, producing either `NotImplemented` or a more specific error (e.g., `joins are not supported`).
269
271
272
+
### Querying the catalog (`sqlrite_master`)
273
+
274
+
SQLRite's schema catalog is exposed to SQL as a read-only table named `sqlrite_master` (SQLR-10), mirroring SQLite's `sqlite_master`. Embedders use it to introspect what's in a database — for example to discover existing tables before running migrations.
275
+
276
+
```sql
277
+
SELECT name FROM sqlrite_master; -- every table + index
278
+
SELECT name FROM sqlrite_master WHERE type ='table'; -- tables only
279
+
SELECT name FROM sqlrite_master WHERE type ='index'; -- indexes only (incl. auto-indexes)
280
+
SELECT*FROM sqlrite_master WHERE name ='users'; -- full row for one object
281
+
```
282
+
283
+
Columns (same schema the catalog persists with on disk):
284
+
285
+
| Column | Type | Meaning |
286
+
|---|---|---|
287
+
|`type`| text |`'table'` or `'index'`|
288
+
|`name`| text | object name |
289
+
|`sql`| text | the `CREATE TABLE` / `CREATE INDEX` text that recreates the object |
290
+
|`rootpage`| integer | always `0` in this live view — page numbers are assigned at save time, not at query time. Kept for schema parity with the persisted catalog. |
291
+
|`last_rowid`| integer | a table's current auto-ROWID high-water mark (`0` for index rows) |
292
+
293
+
Notes:
294
+
295
+
- The catalog is synthesized on demand from the live database, so it reflects uncommitted in-memory state, not just what's been saved.
296
+
- It is **read-only**: `INSERT` / `UPDATE` / `DELETE` against `sqlrite_master` are rejected, as are `CREATE TABLE` / `DROP TABLE` / `ALTER TABLE` that target the reserved name.
297
+
- It works in the single-table `SELECT` path (`WHERE`, projections, `ORDER BY`, `LIMIT`). Joining `sqlrite_master` against another table is not supported.
298
+
- For a lighter-weight "what tables exist" check, [`PRAGMA table_list`](#pragma-table_list-sqlr-10) returns a column count per table without synthesizing SQL.
299
+
270
300
---
271
301
272
302
## `UPDATE`
@@ -579,6 +609,27 @@ Case-insensitive on both the pragma name and the value. Quoted values (`'mvcc'`)
579
609
580
610
The setting is **per-database** — every `Connection::connect` sibling sees the same value. Reachable through the public API as `Connection::journal_mode() -> JournalMode`.
581
611
612
+
### `PRAGMA table_list` (SQLR-10)
613
+
614
+
Lists the tables in the database — the quick "what's in here?" introspection that SDK / FFI / MCP consumers reach for when they can't run a full [`sqlrite_master`](#querying-the-catalog-sqlrite_master) query. Read-only; the write form (`PRAGMA table_list = …`) is rejected.
615
+
616
+
```sql
617
+
PRAGMA table_list; -- one row per user table, plus sqlrite_master
618
+
```
619
+
620
+
Columns mirror SQLite's `PRAGMA table_list`:
621
+
622
+
| Column | Meaning |
623
+
|---|---|
624
+
|`schema`| always `main` (SQLRite has a single schema) |
The synthetic catalog table `sqlrite_master` is listed last (SQLite lists `sqlite_schema` the same way). Indexes are **not** listed here — query [`sqlrite_master`](#querying-the-catalog-sqlrite_master)`WHERE type = 'index'` for those.
0 commit comments