Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,15 @@ sqlrite> DELETE FROM users WHERE age < 30;

| Statement | Features |
|---|---|
| `CREATE TABLE` | `PRIMARY KEY`, `UNIQUE`, `NOT NULL`; 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 |
| `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 |
| `CREATE [UNIQUE] INDEX` | Single-column, named indexes; `IF NOT EXISTS`; persists as a dedicated cell-based B-Tree. INTEGER + TEXT columns only |
| `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 |
| `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 |
| `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` |
| `UPDATE` | Multi-column `SET`; `WHERE`; UNIQUE + type enforcement; arithmetic in assignments (`SET age = age + 1`) |
| `DELETE` | `WHERE` predicate or full-table delete |
| `BEGIN` / `COMMIT` / `ROLLBACK` | Real transactions, snapshot-based; WAL-backed commit; single-level (no savepoints); auto-rollback if `COMMIT`'s disk write fails |
| `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 |
| `PRAGMA table_list` | Lists tables (`schema`, `name`, `type`, `ncol`, `wr`, `strict`) plus the `sqlrite_master` catalog — lightweight catalog introspection for SDK / FFI / MCP consumers |

Expressions in `WHERE` and `UPDATE`'s `SET` RHS:

Expand Down
59 changes: 55 additions & 4 deletions docs/supported-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ If you're looking for _how_ to use SQLRite (REPL flow, meta-commands, history, e

| Statement | Supported today |
|---|---|
| [`CREATE TABLE`](#create-table) | Columns with `PRIMARY KEY` / `UNIQUE` / `NOT NULL` / `DEFAULT <literal>`; typed columns; auto-indexes on constrained columns |
| [`CREATE TABLE`](#create-table) | Columns with `PRIMARY KEY` / `UNIQUE` / `NOT NULL` / `DEFAULT <literal>`; typed columns; auto-indexes on constrained columns; `IF NOT EXISTS` |
| [`CREATE [UNIQUE] INDEX`](#create-index) | Single-column named indexes, `IF NOT EXISTS`, persisted as cell-based B-Trees |
| [`INSERT INTO`](#insert-into) | Auto-ROWID, UNIQUE/PK enforcement, clean type errors, NULL/DEFAULT padding |
| [`SELECT`](#select) | `*` or column list, `WHERE`, single-column `ORDER BY`, `LIMIT`; index probing on `col = literal` |
Expand Down Expand Up @@ -52,9 +52,11 @@ let rows = stmt
## `CREATE TABLE`

```sql
CREATE TABLE <name> (<col> <type> [column_constraint]* [, ...]);
CREATE TABLE [IF NOT EXISTS] <name> (<col> <type> [column_constraint]* [, ...]);
```

`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.

### Column types

| Keyword(s) | Storage class | Notes |
Expand All @@ -80,7 +82,7 @@ CREATE TABLE <name> (<col> <type> [column_constraint]* [, ...]);

### Errors returned

- `Table 'foo' already exists.` — duplicate `CREATE TABLE`.
- `Cannot create, table already exists.` — duplicate `CREATE TABLE` (suppressed to a no-op when the statement uses `IF NOT EXISTS`).
- `'sqlrite_master' is a reserved name used by the internal schema catalog` — you tried to shadow the catalog table.
- `Column 'foo' appears more than once in the table definition` — duplicate column names.
- `PRIMARY KEY column must be INTEGER` — PK on a non-integer column.
Expand Down Expand Up @@ -108,7 +110,7 @@ Every `PRIMARY KEY` and every `UNIQUE` column gets an auto-index at `CREATE TABL
sqlrite_autoindex_<table>_<column>
```

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.
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.

### HNSW indexes (Phase 7d)

Expand Down Expand Up @@ -267,6 +269,34 @@ The executor includes a tiny optimizer: if the `WHERE` is exactly `<indexed_col>

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`).

### Querying the catalog (`sqlrite_master`)

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.

```sql
SELECT name FROM sqlrite_master; -- every table + index
SELECT name FROM sqlrite_master WHERE type = 'table'; -- tables only
SELECT name FROM sqlrite_master WHERE type = 'index'; -- indexes only (incl. auto-indexes)
SELECT * FROM sqlrite_master WHERE name = 'users'; -- full row for one object
```

Columns (same schema the catalog persists with on disk):

| Column | Type | Meaning |
|---|---|---|
| `type` | text | `'table'` or `'index'` |
| `name` | text | object name |
| `sql` | text | the `CREATE TABLE` / `CREATE INDEX` text that recreates the object |
| `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. |
| `last_rowid` | integer | a table's current auto-ROWID high-water mark (`0` for index rows) |

Notes:

- The catalog is synthesized on demand from the live database, so it reflects uncommitted in-memory state, not just what's been saved.
- 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.
- It works in the single-table `SELECT` path (`WHERE`, projections, `ORDER BY`, `LIMIT`). Joining `sqlrite_master` against another table is not supported.
- 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.

---

## `UPDATE`
Expand Down Expand Up @@ -579,6 +609,27 @@ Case-insensitive on both the pragma name and the value. Quoted values (`'mvcc'`)

The setting is **per-database** — every `Connection::connect` sibling sees the same value. Reachable through the public API as `Connection::journal_mode() -> JournalMode`.

### `PRAGMA table_list` (SQLR-10)

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.

```sql
PRAGMA table_list; -- one row per user table, plus sqlrite_master
```

Columns mirror SQLite's `PRAGMA table_list`:

| Column | Meaning |
|---|---|
| `schema` | always `main` (SQLRite has a single schema) |
| `name` | table name |
| `type` | always `table` (SQLRite doesn't expose views) |
| `ncol` | number of declared columns |
| `wr` | always `0` (no WITHOUT ROWID tables) |
| `strict` | always `0` (no STRICT tables) |

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.

---

## `BEGIN CONCURRENT` (Phase 11.4, SQLR-22)
Expand Down
37 changes: 22 additions & 15 deletions examples/go-collector/internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
//
// - No parameter binding in the Go SDK → values are inlined via the
// helpers in sqlquote.go.
// - `CREATE TABLE IF NOT EXISTS` is not honored and `sqlrite_master`
// isn't queryable → migrate() probes for the events table with a
// SELECT and only runs DDL on a fresh database.
// - migrate() probes for the events table with a SELECT and only runs
// DDL on a fresh database. NOTE: as of SQLR-10 the engine now honors
// `CREATE TABLE IF NOT EXISTS` and exposes a queryable `sqlrite_master`
// (and `PRAGMA table_list`), so the table-existence probe is no longer
// strictly required for table creation. We keep the fresh/reopen
// distinction because the `CREATE INDEX` below must NOT be re-issued on
// reopen (it's rejected once `journal_mode = mvcc`); the probe also
// keeps this example working against pre-SQLR-10 engine builds.
// - `CREATE INDEX` is rejected once `journal_mode = mvcc` → all DDL,
// including the optional secondary index, runs at migrate time
// before MVCC is switched on.
Expand Down Expand Up @@ -191,18 +196,18 @@ func (s *Store) Close() error {
}

// migrate creates the schema on a fresh database and is a no-op on
// reopen. Two engine constraints (both verified against the v0 engine)
// shape this:
// reopen. What shapes this:
//
// - `CREATE TABLE IF NOT EXISTS` is NOT honored — a second create of
// an existing table errors "table already exists" — and the
// `sqlrite_master` catalog isn't queryable. So we detect a fresh
// database by probing for the events table with a cheap SELECT and
// only run DDL when it's absent.
// - We detect a fresh database by probing for the events table with a
// cheap SELECT and only run DDL when it's absent. As of SQLR-10 the
// engine honors `CREATE TABLE IF NOT EXISTS` and exposes a queryable
// `sqlrite_master`, so the tables alone wouldn't need the probe — but
// see the next point.
// - `CREATE INDEX` is rejected once `journal_mode = mvcc`. All DDL
// (tables + the optional index) therefore runs on the fresh path,
// in WAL mode, *before* the MVCC switch. On reopen the index already
// exists, so we never re-issue it.
// exists, so we never re-issue it — which is why the fresh/reopen
// probe stays even though IF NOT EXISTS would cover the tables.
func (s *Store) migrate(ctx context.Context) error {
fresh := !s.tableExists(ctx, "events")

Expand Down Expand Up @@ -260,10 +265,12 @@ func (s *Store) migrate(ctx context.Context) error {
return nil
}

// tableExists probes for a table with a zero-row SELECT. The engine has
// no queryable catalog and rejects `CREATE TABLE IF NOT EXISTS`, so this
// probe is how we tell a fresh database from a reopened one. A query
// error (the engine returns "Table '<name>' not found") means absent.
// tableExists probes for a table with a zero-row SELECT. This is how we
// tell a fresh database from a reopened one so the MVCC-incompatible
// `CREATE INDEX` only runs once. A query error (the engine returns
// "Table '<name>' not found") means absent. (As of SQLR-10 the engine
// also exposes `sqlrite_master` and `PRAGMA table_list` for catalog
// introspection — either could back this probe on a current engine.)
func (s *Store) tableExists(ctx context.Context, name string) bool {
rows, err := s.db.QueryContext(ctx, fmt.Sprintf("SELECT id FROM %s LIMIT 1", name))
if err != nil {
Expand Down
20 changes: 17 additions & 3 deletions src/sql/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,23 @@ pub fn execute_select_rows(query: SelectQuery, db: &Database) -> Result<SelectRe
return execute_select_rows_joined(query, db);
}

let table = db
.get_table(query.table_name.clone())
.map_err(|_| SQLRiteError::Internal(format!("Table '{}' not found", query.table_name)))?;
// SQLR-10 — `SELECT … FROM sqlrite_master` introspects the catalog.
// The catalog isn't a live entry in `db.tables` (it's materialized at
// save time), so we synthesize a read-only in-memory snapshot on
// demand and run the normal single-table path against it. WHERE /
// projections / ORDER BY / LIMIT all work unchanged. Writes against
// sqlrite_master remain rejected (it never lands in `db.tables`), and
// joins against it are not supported (the joined path doesn't
// synthesize it).
let master_snapshot;
let table: &Table = if query.table_name == crate::sql::pager::MASTER_TABLE_NAME {
master_snapshot = crate::sql::pager::build_master_table_snapshot(db)?;
&master_snapshot
} else {
db.get_table(query.table_name.clone()).map_err(|_| {
SQLRiteError::Internal(format!("Table '{}' not found", query.table_name))
})?
};

// SQLR-3: Materialize the projection as `Vec<ProjectionItem>` so
// both the simple-row path and the aggregation path can iterate the
Expand Down
Loading
Loading