Statements at a glance

Every supported statement, with its transactional behaviour. Keywords are case-insensitive; a single statement per call, with an optional trailing semicolon.

StatementPurposeIn a transaction?
CREATE TABLEDefine a tableAutocommit only — ENGINE_ERR_TXN inside BEGIN
DROP TABLERemove a tableAutocommit only — ENGINE_ERR_TXN inside BEGIN
CREATE INDEX … USING hnswBuild a vector indexAutocommit only — see Vector search
DROP INDEXRemove a vector indexAutocommit only
INSERTAdd rowsFully transactional
UPDATEModify rowsFully transactional
DELETERemove rowsFully transactional
SELECTRead rowsReads a snapshot; never blocks writers
BEGIN / COMMIT / ROLLBACKTransaction control

DDL is autocommit-only by design

Schema changes (CREATE TABLE, DROP TABLE) run only in autocommit. Issuing one inside an explicit transaction returns ENGINE_ERR_TXN. Row DML — INSERT, UPDATE, DELETE — is fully transactional and commits durably.

Data definition (DDL)

CREATE TABLE

Define a table with one or more columns. Each column may carry a type name and the constraints PRIMARY KEY and/or NOT NULL. IF NOT EXISTS makes creation idempotent.

CREATE TABLE notes (
  id    INTEGER PRIMARY KEY,
  title TEXT NOT NULL,
  body  TEXT,
  score REAL,
  data  BLOB
);

CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT);
  • MUST declare at least one column; an empty column list is a parse error.
  • MAY omit a column's type name — an untyped column defaults to TEXT affinity.
  • MAY write a parenthesised size such as VARCHAR(64) or DECIMAL(10,2) — the size is parsed and ignored; only the storage class matters.
  • MAY mark a column UNIQUE or explicitly NULL — these are accepted as constraint markers (uniqueness is enforced for the primary key).

PRIMARY KEY implies NOT NULL

Declaring a column PRIMARY KEY also makes it NOT NULL.

DROP TABLE

Remove a table. IF EXISTS suppresses the error when the table is absent.

DROP TABLE notes;
DROP TABLE IF EXISTS notes;

Data manipulation (DML)

INSERT

Insert one or more rows. The column list is optional; when given, values bind to those columns by position. Multiple parenthesised tuples insert several rows in one statement. OR REPLACE / OR IGNORE are accepted after INSERT and ignored.

INSERT INTO notes (id, title, body) VALUES (1, 'first', 'hello');

INSERT INTO notes (id, title) VALUES (2, 'two'), (3, 'three');

INSERT INTO notes VALUES (4, 'four', NULL, 9.5, NULL);

-- with parameters
INSERT INTO notes (id, title) VALUES (?, ?);

UPDATE

Assign one or more columns, optionally filtered by a WHERE predicate. Without WHERE, every row is updated. The number of rows changed is reported by engine_changes().

UPDATE notes SET title = 'renamed', score = score + 1 WHERE id = 1;
UPDATE notes SET body = NULL;

DELETE

Remove rows matching an optional WHERE predicate. Without WHERE, every row is deleted.

DELETE FROM notes WHERE score IS NULL;
DELETE FROM notes;

Queries (SELECT)

A query reads from a single table (or no table, for bare expressions) under snapshot isolation. The full shape is:

SELECT items
  FROM table
 WHERE predicate
 ORDER BY expr [ASC | DESC], ...
 LIMIT n

Clauses are optional and parsed in this order. SELECT ALL is accepted (it is the default); SELECT DISTINCT is rejected.

Select items

ItemMeaning
*All columns of the table, in declared order
exprAny expression — a column, literal, or computed value
expr AS aliasName the output column (the AS keyword is optional)
COUNT / SUM / MIN / MAX / AVGWhole-table aggregate over * or an expression
SELECT * FROM notes;
SELECT id, title AS heading FROM notes WHERE score >= 5 ORDER BY score DESC LIMIT 10;
SELECT COUNT(*), AVG(score), MAX(score) FROM notes;
SELECT title, score * 2 AS doubled FROM notes ORDER BY id;

Aggregates are whole-table only

COUNT, SUM, MIN, MAX, and AVG compute over the entire (filtered) table. There is no GROUP BY, so you cannot mix a bare aggregate with per-row columns to mean grouped output.

ORDER BY and LIMIT

  • MAY sort by one or more expressions, each independently ASC (default) or DESC.
  • MUST give LIMIT an integer literal — a non-integer (including a parameter) is a parse error.

Expressions

Expressions appear in WHERE, ORDER BY, select items, UPDATE assignments, and INSERT values. The operand forms are:

FormExamples
Integer literal42, -7
Real literal3.5, 1e3, 2.0
String literal'hello' (use '' for a literal quote)
NULLNULL
Boolean keywordTRUE1, FALSE0
Column referencetitle, notes.title (table qualifier accepted, only the column part is used)
Parameter? (positional, bound 1-based)
Parenthesised(score + 1) * 2

Operators

Listed from lowest to highest binding precedence.

CategoryOperatorsNotes
Logical ORORThree-valued logic (a NULL operand yields unknown)
Logical ANDAND
Logical NOTNOTPrefix
Comparison=, <>, !=, <, <=, >, >=<> and != are both inequality
Null testIS NULL, IS NOT NULL
PatternLIKE, NOT LIKEPattern match against a string expression
Vector distance<->, <=>, <#>L2 / cosine / inner-product distance — see Vector search
Additive+, -
Multiplicative*, /, %Multiply, divide, modulo
Unary-, +Negation / identity prefix
SELECT * FROM notes
 WHERE (score > 5 OR title LIKE 'a%')
   AND body IS NOT NULL
 ORDER BY score DESC;

NULL comparisons are unknown

Comparing anything to NULL with = / <> yields unknown, not true or false — a row whose predicate is unknown is not returned. Use IS NULL / IS NOT NULL to test for null explicitly.

Types and storage classes

Every value belongs to one of six storage classes. A declared column type maps to a storage class by affinity (the same keyword rules SQLite uses), and values are lightly coerced toward the column's class where lossless.

Storage classHoldsDeclared types that map to it
NULLThe absence of a value
INTEGER64-bit signed integerAny type name containing INT
REAL64-bit floating pointREAL, FLOA…, DOUB…, NUMERIC, DEC…
TEXTUTF-8 stringCHAR…, TEXT, CLOB, STRING; also the default for an unknown or omitted type
BLOBRaw bytesBLOB, BYTEA
VECTORFixed-length f32 arrayVECTOR(N) — dimension N required and validated on insert
  • MAY rely on light affinity: an INTEGER inserted into a REAL column becomes a real; a whole-number REAL in an INTEGER column becomes an integer.
  • MUST read all values back as text across the C ABI — a BLOB renders as standard padded base64, and a SQL NULL comes back as a null pointer.

Parameters

Use a positional ? placeholder anywhere an expression is allowed, then bind by 1-based index. Binding goes through the prepared-statement path of the C ABI.

INSERT INTO notes (id, title) VALUES (?, ?);
SELECT * FROM notes WHERE id = ? OR title LIKE ?;

Each bound value is a NUL-terminated typed literal: a one-character tag followed by the encoded value. This is how the string-only ABI carries a typed argument.

TagTypeExample
iIntegeri42
fFloatf3.5
sTextshello
bBytes (base64)b<base64>
nNULLn
vVectorv[1,2,3] or v1,2,3

The wrapper does the tagging for you

The @twilldb/bun client encodes these typed literals from native JS/TS values, so you bind ordinary numbers, strings, and Uint8Array. The raw tags matter only when calling the C ABI directly. See Connect from Bun.

Transactions

Row DML is fully transactional under snapshot isolation. A single writer serializes through a write lane; readers capture a snapshot LSN and never block.

StatementEffect
BEGINStart an explicit transaction (BEGIN TRANSACTION / START TRANSACTION also accepted)
COMMITPublish the transaction — blocks until the WAL is durable
ROLLBACKDiscard the transaction's pending changes
BEGIN;
INSERT INTO notes (id, title) VALUES (10, 'draft');
UPDATE notes SET score = 1 WHERE id = 10;
COMMIT;   -- returns only once the records are durable on the backend
  • MUST keep schema changes out of an explicit transaction — DDL inside BEGIN returns ENGINE_ERR_TXN.
  • SHOULD retry a transaction that fails with ENGINE_ERR_CONFLICT — a first-committer-wins check rejects conflicting concurrent writers.

Out of scope

These are deliberate boundaries, not bugs. Each parses to a clear ENGINE_ERR_SQL rather than a wrong result.

  • MUST NOT use JOIN — queries read from a single table.
  • MUST NOT use GROUP BY / HAVING — aggregates are whole-table only.
  • MUST NOT use subqueries.
  • MUST NOT use SELECT DISTINCT.
  • MUST NOT use views or ALTER TABLE. The only CREATE INDEX supported is the HNSW vector index (USING hnsw); general secondary/B-tree indexes are not exposed as SQL.

Unsupported syntax fails fast

Anything outside the surface on this page is rejected at parse time with ENGINE_ERR_SQL — the engine never silently mis-parses or partially applies an unsupported statement.

Related

Twill DB documentation · Licensed under AGPL-3.0. · Author