1# postjevsql: a guide in twenty-eight chapters 2 3**Ask your database a question it cannot answer.** "Could this person work 4from home?" is not in any column. No `WHERE` clause can say it, no index can 5find it, and yet it is a perfectly good thing to want to filter on. This 6repository is a PostgreSQL extension that lets you write it anyway: 7 8```sql 9SELECT name, jev_prob(people, 'Could this person work from home?') AS p 10FROM people 11WHERE country = 'PT' AND jev(people, 'Could this person work from home?') 12ORDER BY p DESC, name LIMIT 20; 13``` 14 15The question goes to [Jev](https://docs.typesafe.ai), TypeSafe AI's "System 16One" model. Jev does not write. You cannot ask it to explain anything. You 17hand it a question whose answers are already written down (yes or no, which 18is a *Noul*; one of these labels, a *Choice*; somewhere on this scale, a 19*Score*) and it tells you how likely each one is, with numbers you can trust. 20That shape fits SQL like a glove: a probability is a `float8`, a label is 21`text`, and both can go in a `WHERE` or an `ORDER BY`, or be counted and 22averaged, like any other value. 23 24The extension is written in Rust with [pgrx](https://github.com/pgcentralfoundation/pgrx), 25and most of what is interesting about it is what happens between your `SELECT` 26and Jev: one plan node judges many rows at once over one HTTP/2 connection, 27every answer is kept in a table so it is never paid for twice, and nothing is 28sent at all until the price has been checked against your budget. 29 30> **Try it.** Open [tests/fixtures/ranking.json](tests/fixtures/ranking.json). 31> Those are two real requests this extension sent on 2026-09-26, byte for 32> byte, and what came back. Ana, a backend engineer in Portugal: could she 33> work from home? `0.87`. Rui, a nurse: `0.28`. Each row went alone, as its 34> own request, and the tests replay them from then on without spending a 35> cent (chapter 15 says why). 36 37## How to read this 38 39Every folder of this project's own code is a chapter (the few that are not 40are listed at the end of this page), and each one ends with a link to the 41next. Read them in order and you will know how all of it works; jump 42in anywhere and the chapter tells you what it is and what is beside it. Each 43chapter starts with the idea, in plain words, and ends with the parts a 44maintainer needs (files, invariants, commands). The `CLAUDE.md` beside each 45README is what an AI agent working here is told on top of the chapter; the 46one at the root holds the whole design contract. 47 48| Chapter | Folder | What you learn | 49| --- | --- | --- | 50| 1 | [crates/](crates/) | Four crates, and the wall that keeps `unsafe` in one of them. | 51| 2 | [crates/postjevsql/](crates/postjevsql/) | The extension: every SQL function and every setting. | 52| 3 | [crates/postjevsql/src/](crates/postjevsql/src/) | The judge: how a call becomes a request, a cache row and an answer. | 53| 4 | [crates/postjevsql/src/bin/](crates/postjevsql/src/bin/) | One generated line that cargo-pgrx needs. | 54| 5 | [crates/postjevsql-pg/](crates/postjevsql-pg/) | Where the `unsafe` lives, and why it all has to. | 55| 6 | [crates/postjevsql-pg/src/](crates/postjevsql-pg/src/) | An async runtime built out of Postgres's own wait. | 56| 7 | [crates/postjevsql-pg/src/scan/](crates/postjevsql-pg/src/scan/) | One plan node that judges every call in a query. | 57| 8 | [crates/postjevsql-core/](crates/postjevsql-core/) | The decisions that need no database. | 58| 9 | [crates/postjevsql-core/src/](crates/postjevsql-core/src/) | Layouts, cache keys, rate limits, and choosing among thousands. | 59| 10 | [crates/postjevsql-sidecar/](crates/postjevsql-sidecar/) | Using it with a database that will not let you install anything. | 60| 11 | [crates/postjevsql-sidecar/src/](crates/postjevsql-sidecar/src/) | The sidecar's config, its secrets, and its sync plan. | 61| 12 | [crates/postjevsql-sidecar/cli/](crates/postjevsql-sidecar/cli/) | `converge`, `sync` and `serve`. | 62| 13 | [tests/](tests/) | A real Postgres and a fake Jev for every test. | 63| 14 | [tests/support/](tests/support/) | The harness that starts them. | 64| 15 | [tests/fixtures/](tests/fixtures/) | Live answers, recorded once and replayed forever. | 65| 16 | [nix/](nix/) | The packages, the NixOS module and the container image. | 66| 17 | [tools/](tools/) | The helpers that write files so nobody has to. | 67| 18 | [tools/pgrx-schema/](tools/pgrx-schema/) | The install SQL, read out of the built library. | 68| 19 | [tools/pgrx-schema/src/](tools/pgrx-schema/src/) | Its one file. | 69| 20 | [tools/cargo-gen/](tools/cargo-gen/) | A Cargo workspace written from the buck graph. | 70| 21 | [tools/cargo-gen/src/](tools/cargo-gen/src/) | Its one file. | 71| 22 | [tools/third-party-notices/](tools/third-party-notices/) | Every licence the binaries carry, collected for you. | 72| 23 | [tools/third-party-notices/src/](tools/third-party-notices/src/) | Its one file. | 73| 24 | [tools/record-gates/](tools/record-gates/) | Pricing, then recording, the release gates. | 74| 25 | [tools/record-gates/src/](tools/record-gates/src/) | Its two files. | 75| 26 | [toolchains/](toolchains/) | The compilers buck uses, all from nix. | 76| 27 | [platforms/](platforms/) | One build graph for every Postgres major. | 77| 28 | [third-party/](third-party/) | What came from elsewhere, jevcrates included. | 78 79## If you know a web framework 80 81You do not need to know Postgres's insides or Rust to follow this guide. 82Most of the pieces have a counterpart in a web stack. 83 84| Here | What it is | In web terms | 85| --- | --- | --- | 86| A Postgres extension | A shared library (`postjevsql.so`) the database server loads, and the SQL that declares its functions. | A plugin loaded into the server process, where the server is the database. | 87| pgrx | The Rust framework for writing extensions. It writes the C glue and the install SQL. | The framework, as Nuxt is to a Vue app. | 88| A CustomScan | A node an extension may put into a query's plan. | A middleware inserted into the request pipeline; here the pipeline is a query. | 89| A GUC (the `jev.*` settings) | A setting Postgres knows by name, set in `postgresql.conf` or with `SET` for one session. | Environment variables that one connection can change for itself. | 90| SPI | How an extension runs SQL from inside the server. | A database call from inside a route handler. | 91| `jev_cache` | A table holding every answer, keyed by exactly what was sent. | A response cache that is also an audit log. | 92| buck2 | The build system: one graph of every target, built and tested together. | Bazel, or Turborepo. | 93| Nix | Every tool at an exact version, from `nix develop`. | `package.json` plus a Node version manager. | 94 95## The whole story in one picture 96 97Here is what happens between Enter and the rows coming back. Every arrow is 98a chapter somewhere in this guide. 99 100```mermaid 101sequenceDiagram 102 participant C as Client (psql) 103 participant P as Planner (with postjevsql's hooks) 104 participant S as JevScan (one plan node) 105 participant T as The table's own scan 106 participant K as jev_cache 107 participant J as Jev (api.typesafe.ai) 108 C->>P: SELECT … WHERE jev(t, 'question') 109 P-->>S: wrap the table's scan in a JevScan 110 Note over S: ExecutorStart: price the plan, refuse it over jev.max_cost 111 loop up to jev.concurrency rows at a time 112 S->>T: next row (every other WHERE clause already applied) 113 S->>K: an answer for these exact bytes? 114 alt kept 115 K-->>S: the answer, for nothing 116 else not kept 117 S->>J: one row, one HTTP/2 stream 118 J-->>S: probabilities 119 S->>K: store it, with the receipt 120 end 121 end 122 S-->>C: the rows, in the order the table gave them 123``` 124 125In words: 126 1271. **The planner hooks wrap the scan** (chapter 7). Every call to a `jev*` 128 function over one table is claimed by one plan node, wherever it appears: 129 `WHERE`, the select list, `ORDER BY`, under an aggregate, inside a CTE. 1302. **The price comes first.** Before a single row is read, the plan's row 131 estimates are priced at three billed attempts per request, and a 132 statement over `jev.max_rows` or `jev.max_cost` is refused with nothing 133 spent (chapter 3). 1343. **Rows the SQL filters out are never judged.** The node sits above every 135 ordinary condition, so only rows that pass them reach Jev, and a `LIMIT` 136 stops the judging a window past the last row it returned. 1374. **Each row is judged alone**, in its own request, so no row can sway 138 another's answer (chapter 9 has the measurement), and many go at once, as 139 streams on the backend's one connection (chapter 6). 1405. **Every answer is kept** in `jev_cache`, under the hash of exactly what 141 was sent. Ask again and nothing is sent at all. 142 143> **Aside.** Why one request per row, when a request could carry twenty? It 144> was tried, and measured. With 40 rows in one request, rows in the last 145> slots had their probabilities moved by about 0.42 from what they got 146> alone, just for where they sat. A Postgres extension that answered differently depending on 147> a row's neighbours would be a strange sort of database. So the contract 148> forbids it, and the code cannot build such a request. 149 150## Where it runs 151 152The same extension ships two ways, and the code does not fork. 153 154```mermaid 155flowchart LR 156 subgraph indb["In-database"] 157 A1["your app"] --> D1[("your Postgres + postjevsql")] 158 end 159 subgraph side["Sidecar"] 160 A2["your app"] --> S2[("a small Postgres + postjevsql")] 161 S2 -->|"postgres_fdw: rows that pass the filters"| D2[("your managed database (RDS, Supabase, Neon, …)")] 162 end 163 D1 --> J["Jev"] 164 S2 --> J 165``` 166 167- **In-database**: installed into the Postgres that holds the data. This is 168 the default whenever you control that server. 169- **Sidecar**: a local Postgres runs the extension and reaches the real 170 database through `postgres_fdw`. Nothing is installed on the target, so it 171 works with managed hosts, which do not allow custom compiled extensions. 172 Filters still run on the target, and only the `jev` judgements run 173 locally. Writes through the sidecar are not two-phase committed. 174 Chapter 10 is about this mode. 175 176## The fine print 177 178Everything here is true of the code today, and some of it will bite. 179 180- **`jev_prob` returns two-decimal values, and ties are common.** Add your 181 own tie-break column to `ORDER BY jev_prob(…)`, as the example at the top 182 does with `name`. 183- **Supported PostgreSQL majors: 17 and later** (17 and 18 today). The nix 184 package set is `postgresqlNNPackages.postjevsql` for each. 185- **Not compatible with pg_duckdb** loaded in the same server. pg_duckdb 186 calls Postgres from its own threads, and this extension's planner hooks 187 refuse to run off the backend thread (pgrx #2228). 188- **TypeSafe's terms bind how the results are used.** Cached answers may not 189 be used to train a model that imitates Jev (MCA §2.3(b)), and one API key 190 may not serve third parties through this extension (§2.3(a)). 191- **Row contents are sent to TypeSafe.** Do not point this at data you 192 cannot share with them. TypeSafe states that it does not train on customer 193 requests. Zero data retention is offered to enterprise customers only 194 (docs.typesafe.ai/models, "Data handling"). 195- **A query shape it cannot batch yet is refused, never run row by row**: a 196 call over two relations, or one in `RETURNING`, `ON CONFLICT` or a `MERGE` 197 action. The error comes before anything is sent. 198 199> **Aside: where the ideas came from.** Three earlier projects asked Jev from 200> SQL, and this one takes the strongest part of each. 201> [realZachi/pg-jev](https://github.com/realZachi/pg-jev) (a plpython3u 202> extension) gave the whole-row surface, `jev(alias, …)`; its 20-rows-per-state 203> batching was *not* taken, for the reason in the aside above. 204> [kylemclaren/jevql](https://github.com/kylemclaren/jevql) (a Go query 205> rewriter outside the server) gave canonical row JSON, the column-list form, 206> the price shown before spending, the spend guards, and its list of refused 207> queries, read as a to-do list. 208> [giuliosmall/pg_typesafe](https://github.com/giuliosmall/pg_typesafe) (a C 209> extension) gave cancellable in-backend HTTP, typed composite results, the 210> privilege and secret model, and offline mock testing. On top of those 211> come batching owned by the query plan, a durable cache that doubles as an 212> audit trail, and Choice over more than 255 options. 213 214## Running it yourself 215 216The repository is served by the lmjtfy site, with 217[jevcrates](https://lmjtfy.fun/jevcrates.git) (the shared Jev 218client) as a submodule beside it. No GitHub account is needed: 219 220 git clone --recurse-submodules https://lmjtfy.fun/postjevsql.git 221 cd postjevsql 222 cargo test -p postjevsql-core -p postjevsql-sidecar 223 224Those two crates do no I/O, so their tests need no database, no network and 225no API key, and they finish in about a second. The generated Cargo workspace 226also lets `cargo pgrx install` (cargo-pgrx 0.19.2) build the extension into a 227Postgres you already run. 228 229The whole suite, every crate and every integration test on Postgres 17 and 23018, runs through buck2 in the devshell: 231 232 nix develop -c buck2 test //... 233 234The devshell takes buck2 from the owner's own package set, which is private, 235so that command works on the owner's machines. Chapter 13 explains what it 236runs, and chapter 16 how the packages are built. 237 238To use it, install the package for your major, put the API key in the 239server's environment as `TYPESAFE_API_KEY` (or name a file in 240`jev.api_key_file`), and then: 241 242```sql 243CREATE EXTENSION postjevsql; 244SET jev.model = 'jev-1.13.0'; -- a pinned version; aliases are refused 245GRANT EXECUTE ON FUNCTION jev_prob(anyelement, text) TO app; -- spending is granted, never public 246``` 247 248## Files at the top 249 250| File | What | 251| --- | --- | 252| [CLAUDE.md](CLAUDE.md) | The design contract: every rule, where it came from, and the evidence for it. | 253| [flake.nix](flake.nix) | The devshell (the toolchains, and a server for every supported major), the packages, the sidecar module and its VM checks (chapter 16). | 254| [flake.lock](flake.lock) | The flake's pinned inputs. | 255| [.buckconfig](.buckconfig) | buck2's cells and platforms. The devshell writes `.buckconfig.local` beside it with store paths; that file is not committed. | 256| [BUCK](BUCK) | Exports the generated root files to their drift tests. | 257| [Cargo.toml](Cargo.toml) | The Cargo workspace, generated from the buck targets (chapter 20). Do not edit. | 258| [Cargo.lock](Cargo.lock) | Cargo's lock for that workspace. buck's own versions are locked in `third-party/Cargo.lock`. | 259| [deny.toml](deny.toml) | `cargo deny check licenses`: the allowed licences; AGPL and GPL are denied. | 260| [THIRD-PARTY](THIRD-PARTY), [THIRD-PARTY-sidecar](THIRD-PARTY-sidecar) | The licence notices of every crate linked into the extension and into the sidecar CLI, generated (chapter 22). | 261| [LICENSE-MIT](LICENSE-MIT), [LICENSE-APACHE](LICENSE-APACHE) | The licence. | 262| [.gitmodules](.gitmodules) | jevcrates, by the relative URL `../jevcrates.git`, so a clone from the site finds it beside this one. | 263| [.gitignore](.gitignore) | buck's and Cargo's output, and `.buckconfig.local`. | 264 265Some folders are not chapters, on purpose: 266 267- [build/](build/) holds the shared Starlark: `VERSION`, `PG_MAJORS`, and the 268 `first_party_*` macros that give every Rust target its lint test. It is 269 buck's own plumbing, read in chapters 26 and 27 where it is used, and its 270 README says what each line is for. 271- [none/](none/) is an empty buck cell. The buck prelude refers to Meta's 272 internal cells (`fbcode`, `fbsource`, …), and `.buckconfig` points them all 273 here so those references resolve to nothing. 274- `.claude/` is an agent's memory, `buck-out/`, `target/` and `result` are 275 build output, and the trees under `third-party/` came from elsewhere 276 (chapter 28). 277 278## Licence 279 280Licensed under either of the MIT licence ([LICENSE-MIT](LICENSE-MIT)) or the 281Apache Licence 2.0 ([LICENSE-APACHE](LICENSE-APACHE)), at your option. 282Copyright The postjevsql Authors. 283 284Next: [Chapter 1, crates/](crates/) →