Postcrust
Postcrust is Cruft's embedded PostgreSQL engine: it answers SQL queries in-process, with no server, socket, or network round-trip. It reproduces PostgreSQL 17 semantics, its type system, a deep SQL surface, PL/pgSQL, MVCC transactions, and an optional file-backed database, reached today through the ORM's openPostgres.
Postcrust, Cruft's PostgreSQL engine (the postcrust crate), is the second dialect on the SQL stack. The important thing about it is structural: it is the server side, compiled into the runtime. A driver like pg, postgres, or tokio-postgres is a wire protocol client to an external server, doing authentication, TLS, connection pooling, and message framing, and it cannot answer a query on its own. postcrust answers the query in process. There is no socket, no server process, no libpq, and no round-trip: a query is a Rust function call, and the rows come back from an in-process store. It shares the sql-core relational executor with the CQuilite; Postgres is the dialect and type projection on top.
Alpha.postcrust(Cruft 0.0.10) is an embedded, in-process PostgreSQL engine standing in fortokio-postgres/pgplus a running server. It reproduces PostgreSQL semantics rather than the network protocol, and it is not production-ready. Do not rely on it in production or point it at adversarial SQL as your only defense.
It reproduces Postgres semantics
Because postcrust reimplements the semantics rather than delegating to a real server, it pins the Postgres it imitates: it targets PostgreSQL 17 and follows that version's behavior. Every dialect decision, type coercion, and error message is checked against Postgres 17. What it does not have is the network side of Postgres, so everything protocol-level (the frontend/backend messages, authentication, TLS, connection pooling) simply does not exist, because there is no connection.
The type system
Postgres is defined by its type system, and postcrust implements it as an OID-keyed catalog with strict coercion, the opposite of SQLite's affinity. A value carries its Postgres type, and casting formats under the source type and re-parses under the target, so 'abc'::int and '34.5'::int are rejected with Postgres's own invalid input syntax error rather than silently coerced.
The implemented types cover most of what applications use, one Rust module per type:
numeric is genuinely arbitrary-precision and exact, with its own big-decimal arithmetic and Postgres's scale rules, so 555.50 keeps its scale rather than collapsing to a float. Type modifiers are enforced: numeric(p,s) rounds and range-checks, varchar(n) length-checks, and the modifier travels with the column through the catalog and onto disk.
SQL surface
The dialect is deep:
- Queries. Full
SELECT, withDISTINCTandDISTINCT ON,ROLLUP/CUBE/GROUPING SETS, window functions (with partitions, frames, and namedWINDOWclauses),WITHandWITH RECURSIVE(semi-naive fixed-point, with an iteration cap), the set operations, the join kinds (INNER,LEFT,RIGHT,FULL,CROSS) withON/USING/NATURAL,LATERAL, correlated subqueries, set-returning functions likegenerate_series, and row locking (FOR UPDATE/NO KEY UPDATE/SHARE/KEY SHAREwithSKIP LOCKEDand wait modes). - Modification.
INSERT/UPDATE/DELETEwithRETURNING,ON CONFLICT DO NOTHING/DO UPDATEwithEXCLUDEDand arbiter inference,MERGE, andTRUNCATE. - Definition.
CREATE TABLEwith the full constraint set (NOT NULL,UNIQUE,PRIMARY KEY,CHECK,DEFAULT,FOREIGN KEYwith referential actions,IDENTITY,SERIAL, andGENERATED ... STORED) and partitioning;CREATE INDEX(unique, enforced);CREATE VIEWand materialized views;CREATE TYPE(composite and enum),CREATE DOMAIN,CREATE SEQUENCE;CREATE FUNCTION/PROCEDURE(SQL and PL/pgSQL);CREATE TRIGGER,OPERATOR,AGGREGATE,CAST;COMMENT ON; andALTER TABLE/SEQUENCE/TYPEacross the common alterations.
Functions, operators, and PL/pgSQL
The function catalog is large, roughly two hundred and fifty scalar and set-returning functions plus about fifty aggregates, spanning string, math, the full trigonometric and hyperbolic set, JSON and JSONB (with a SQL/JSONPath engine), a regular-expression engine, full-text search (to_tsvector, to_tsquery, the @@ match operator, ranking, and ts_headline), arrays, network types, the to_char/to_date/to_timestamp template engine, base encoding and hashing, fuzzy-match, and range and multirange constructors. The aggregate set includes the statistical family (variance, stddev, corr, the covar_* and regr_* regressions, percentile_cont/disc, mode) alongside the ordinary ones, with DISTINCT, ORDER BY inside the call, and WITHIN GROUP ordered-set aggregates. All of this is Cruft's own Rust, including the big-decimal arithmetic, the regex engine, and the JSONPath engine.
postcrust also runs a real PL/pgSQL interpreter: typed local variables with block scope, IF/ELSIF, CASE, the loop forms, EXIT/CONTINUE, RETURN, RETURN NEXT/RETURN QUERY for set-returning functions, dynamic EXECUTE, RAISE, ASSERT, embedded DML, SELECT ... INTO, and EXCEPTION handlers with subtransaction rollback and SQLSTATE/SQLERRM binding. Functions are callable from triggers and from within queries, and SQL-language functions inline into the calling plan. Call depth and loop iterations are bounded.
Transactions and MVCC
Transactions are genuine multi-version concurrency control. Each row carries xmin/xmax visibility information alongside a stable logical row id that survives an UPDATE (a new version is appended under the same id), ROLLBACK rewinds the transaction's changes, and the isolation levels behave as Postgres defines them: READ COMMITTED takes a fresh per-statement snapshot, REPEATABLE READ takes a transaction-start snapshot. SERIALIZABLE is accepted and adds a write-write conflict check that raises a serialization failure (40001), but it does not yet implement full predicate-locking SSI (see Limitations). SAVEPOINT, ROLLBACK TO, and RELEASE are supported, and sequences are deliberately non-transactional, so nextval survives a rollback as in Postgres.
Durability: a file-backed database
postcrust can persist. openPostgres(path) opens a durable, file-backed database; openPostgres(), openPostgres(""), and openPostgres(":memory:") stay in memory and die with the process, as before. When a path is given and no file exists yet, the handle binds to that path and the file is created on the first successful write; an existing file is loaded on open.
The format is a Cruft-owned logical catalog snapshot rather than PostgreSQL's physical data files. It begins with an eight-byte magic (CRUFTPG\0), a format version, and a feature bitmask, followed by the admitted state: base tables with their column types and modifiers, every row with its MVCC tuple header (xmin/xmax) and logical row id, and the catalog objects, each section gated by its feature bit: NOT NULL / UNIQUE / PRIMARY KEY / CHECK constraints, defaults, foreign keys, identity and generated columns, domains, explicit indexes, sequences, plain and materialized views, user-defined enum and composite types, comments, SQL and PL/pgSQL function bodies (with their operator, aggregate, and cast reference namespaces), row-level triggers, partition routing metadata, and ANALYZE statistics. On reopen, admitted features restore with identical query and typed-boundary behavior.
Two properties make it safe to depend on:
- Writes are atomic and durable at the commit boundary. A successful autocommit statement is flushed before the call returns; statements inside an explicit transaction flush once, at
COMMIT. A rolled-back or failed statement never touches the file, because no flush happens while a transaction is open. Each flush serializes the catalog to a temporary file,fsyncs it, atomically renames it over the target, andfsyncs the directory, so a crash mid-write leaves either the old file or the new one, never a torn one. Stale temporaries from a crashed writer are swept on the next open. - It fails closed. A wrong magic, an unknown version, an unknown feature bit, a truncated payload, or internally inconsistent metadata (a dangling function reference, a bad OID) is rejected on load rather than half-read. A file-backed database also takes a single-writer lock (an OS advisory lock plus an in-process guard), so a second
openPostgreson the same path reports that the database is already open for writing instead of racing it.
Prepared statements and cursors are explicitly session state: they work while a handle is open but are not serialized, so they are absent after a reopen. The open transaction, its savepoints, and the isolation level are likewise not persisted.
Catalog and introspection
The engine synthesizes the Postgres catalog views at query time from its live catalog: pg_class, pg_type, pg_collation, pg_description, pg_tables, pg_indexes, pg_matviews, and pg_stats under pg_catalog, and the tables, columns, sequences, table_constraints, key_column_usage, referential_constraints, and check_constraints views under information_schema, plus pg_typeof and the pg_get_constraintdef/pg_get_expr description functions. ANALYZE produces real column statistics (null fraction, distinct estimate, most-common values, an equi-depth histogram, correlation), which are exact here because the tables are small and in memory.
This introspection is what lets the ORM read a live schema and derive CruftScript row types from it, rather than from a separate schema file.
Reaching it from JavaScript
Today postcrust is reached through the ORM's openPostgres(), which returns a handle with exec and query plus a fluent query builder (from/insertInto/update/deleteFrom, where, select, orderBy, the join methods, with for eager relations, onConflict*, returning, and the aggregates). Pass a path to openPostgres(path) for a durable database; pass nothing for an in-memory one. The ORM builder's derived output type is exactly the type its row-soundness boundary checks against.
The ORM's soundness boundary is about result-row soundness: a row crossing from the database into a typed CruftScript compartment must match its declared type, and a drift (a NULL in a non-null column, or an enum widened by ALTER TYPE ADD VALUE) is reported rather than passed through. Injection resistance is a separate mechanism, resting on value and identifier separation, $n parameters, escaped typed-literal rendering, and catalog-validated identifiers.
A canonical cruft:postgres module and a Bun.sql idiom are planned but not yet installed; the JavaScript surface is the ORM today.
Safety
The engine contains no unsafe at all, no FFI and no raw pointers, and no third-party crates beyond its sibling sql-core and crizzle-core. The parser caps query and expression nesting depth, recursive CTEs and PL/pgSQL loops carry iteration caps, and call and inline depth are bounded. Because the engine executes SQL text rather than a bind protocol, prepared statements and the ORM re-inline values through the escaped typed-literal path, and identifiers are validated against the catalog and a safe-identifier rule. The one place FFI enters is the host-side single-writer file lock, a guarded flock call outside the engine crate.
Limitations
- Logical snapshots, not a WAL. Durability is a whole-catalog atomic-rename snapshot per commit, not an incremental write-ahead log. It is well suited to small logical catalogs; it is not designed for large-heap incremental durability, and a product-scale performance and memory characterization is future work.
- No PostgreSQL physical compatibility. The on-disk format is Cruft's own. It is not a PostgreSQL data directory, cannot read or write PostgreSQL heap/index files, and is not a
pg_ctl/server replacement. - Single-process, single-writer. The MVCC is genuine but single-process; a file-backed database allows one writer at a time. Dead tuples accumulate (there is no
VACUUM), and there is no server-grade concurrency. - No wire protocol, no server. No client, socket, authentication, TLS, or pooling. If the workload needs to talk to an existing PostgreSQL server over the network,
postcrustis the wrong tool and a real driver is the answer. SERIALIZABLEis not full SSI. It adds a write-write conflict check on top ofREPEATABLE READsnapshots but does not track predicate read/write sets, so it does not yet detect every read-write antidependency cycle a true serializable isolation would.- No planner cost model. Access-path and join-method selection is rule-based; statistics exist but do not drive plan choice.
- Type and SQL gaps. Some Postgres types are not implemented;
GROUPING SETSnesting and a few call positions are outstanding; prepared-statement type inference for omitted parameter types, PL/pgSQL cursors, andGET DIAGNOSTICSare not yet implemented. - No replication or extensions.