postjevsql.git / README.md
README.mdpreviewREADME.mdsource284 lines · 15.6 KB · raw

postjevsql: a guide in twenty-eight chapters

Ask your database a question it cannot answer. "Could this person work from home?" is not in any column. No WHERE clause can say it, no index can find it, and yet it is a perfectly good thing to want to filter on. This repository is a PostgreSQL extension that lets you write it anyway:

SELECT name, jev_prob(people, 'Could this person work from home?') AS p
FROM people
WHERE country = 'PT' AND jev(people, 'Could this person work from home?')
ORDER BY p DESC, name LIMIT 20;

The question goes to Jev, TypeSafe AI's "System One" model. Jev does not write. You cannot ask it to explain anything. You hand it a question whose answers are already written down (yes or no, which is a Noul; one of these labels, a Choice; somewhere on this scale, a Score) and it tells you how likely each one is, with numbers you can trust. That shape fits SQL like a glove: a probability is a float8, a label is text, and both can go in a WHERE or an ORDER BY, or be counted and averaged, like any other value.

The extension is written in Rust with pgrx, and most of what is interesting about it is what happens between your SELECT and Jev: one plan node judges many rows at once over one HTTP/2 connection, every answer is kept in a table so it is never paid for twice, and nothing is sent at all until the price has been checked against your budget.

Try it. Open tests/fixtures/ranking.json. Those are two real requests this extension sent on 2026-09-26, byte for byte, and what came back. Ana, a backend engineer in Portugal: could she work from home? 0.87. Rui, a nurse: 0.28. Each row went alone, as its own request, and the tests replay them from then on without spending a cent (chapter 15 says why).

How to read this

Every folder of this project's own code is a chapter (the few that are not are listed at the end of this page), and each one ends with a link to the next. Read them in order and you will know how all of it works; jump in anywhere and the chapter tells you what it is and what is beside it. Each chapter starts with the idea, in plain words, and ends with the parts a maintainer needs (files, invariants, commands). The CLAUDE.md beside each README is what an AI agent working here is told on top of the chapter; the one at the root holds the whole design contract.

ChapterFolderWhat you learn
1crates/Four crates, and the wall that keeps unsafe in one of them.
2crates/postjevsql/The extension: every SQL function and every setting.
3crates/postjevsql/src/The judge: how a call becomes a request, a cache row and an answer.
4crates/postjevsql/src/bin/One generated line that cargo-pgrx needs.
5crates/postjevsql-pg/Where the unsafe lives, and why it all has to.
6crates/postjevsql-pg/src/An async runtime built out of Postgres's own wait.
7crates/postjevsql-pg/src/scan/One plan node that judges every call in a query.
8crates/postjevsql-core/The decisions that need no database.
9crates/postjevsql-core/src/Layouts, cache keys, rate limits, and choosing among thousands.
10crates/postjevsql-sidecar/Using it with a database that will not let you install anything.
11crates/postjevsql-sidecar/src/The sidecar's config, its secrets, and its sync plan.
12crates/postjevsql-sidecar/cli/converge, sync and serve.
13tests/A real Postgres and a fake Jev for every test.
14tests/support/The harness that starts them.
15tests/fixtures/Live answers, recorded once and replayed forever.
16nix/The packages, the NixOS module and the container image.
17tools/The helpers that write files so nobody has to.
18tools/pgrx-schema/The install SQL, read out of the built library.
19tools/pgrx-schema/src/Its one file.
20tools/cargo-gen/A Cargo workspace written from the buck graph.
21tools/cargo-gen/src/Its one file.
22tools/third-party-notices/Every licence the binaries carry, collected for you.
23tools/third-party-notices/src/Its one file.
24tools/record-gates/Pricing, then recording, the release gates.
25tools/record-gates/src/Its two files.
26toolchains/The compilers buck uses, all from nix.
27platforms/One build graph for every Postgres major.
28third-party/What came from elsewhere, jevcrates included.

If you know a web framework

You do not need to know Postgres's insides or Rust to follow this guide. Most of the pieces have a counterpart in a web stack.

HereWhat it isIn web terms
A Postgres extensionA 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.
pgrxThe Rust framework for writing extensions. It writes the C glue and the install SQL.The framework, as Nuxt is to a Vue app.
A CustomScanA node an extension may put into a query's plan.A middleware inserted into the request pipeline; here the pipeline is a query.
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.
SPIHow an extension runs SQL from inside the server.A database call from inside a route handler.
jev_cacheA table holding every answer, keyed by exactly what was sent.A response cache that is also an audit log.
buck2The build system: one graph of every target, built and tested together.Bazel, or Turborepo.
NixEvery tool at an exact version, from nix develop.package.json plus a Node version manager.

The whole story in one picture

Here is what happens between Enter and the rows coming back. Every arrow is a chapter somewhere in this guide.

sequenceDiagram
  participant C as Client (psql)
  participant P as Planner (with postjevsql's hooks)
  participant S as JevScan (one plan node)
  participant T as The table's own scan
  participant K as jev_cache
  participant J as Jev (api.typesafe.ai)
  C->>P: SELECT … WHERE jev(t, 'question')
  P-->>S: wrap the table's scan in a JevScan
  Note over S: ExecutorStart: price the plan, refuse it over jev.max_cost
  loop up to jev.concurrency rows at a time
    S->>T: next row (every other WHERE clause already applied)
    S->>K: an answer for these exact bytes?
    alt kept
      K-->>S: the answer, for nothing
    else not kept
      S->>J: one row, one HTTP/2 stream
      J-->>S: probabilities
      S->>K: store it, with the receipt
    end
  end
  S-->>C: the rows, in the order the table gave them

In words:

  1. The planner hooks wrap the scan (chapter 7). Every call to a jev* function over one table is claimed by one plan node, wherever it appears: WHERE, the select list, ORDER BY, under an aggregate, inside a CTE.
  2. The price comes first. Before a single row is read, the plan's row estimates are priced at three billed attempts per request, and a statement over jev.max_rows or jev.max_cost is refused with nothing spent (chapter 3).
  3. Rows the SQL filters out are never judged. The node sits above every ordinary condition, so only rows that pass them reach Jev, and a LIMIT stops the judging a window past the last row it returned.
  4. Each row is judged alone, in its own request, so no row can sway another's answer (chapter 9 has the measurement), and many go at once, as streams on the backend's one connection (chapter 6).
  5. Every answer is kept in jev_cache, under the hash of exactly what was sent. Ask again and nothing is sent at all.

Aside. Why one request per row, when a request could carry twenty? It was tried, and measured. With 40 rows in one request, rows in the last slots had their probabilities moved by about 0.42 from what they got alone, just for where they sat. A Postgres extension that answered differently depending on a row's neighbours would be a strange sort of database. So the contract forbids it, and the code cannot build such a request.

Where it runs

The same extension ships two ways, and the code does not fork.

flowchart LR
  subgraph indb["In-database"]
    A1["your app"] --> D1[("your Postgres + postjevsql")]
  end
  subgraph side["Sidecar"]
    A2["your app"] --> S2[("a small Postgres + postjevsql")]
    S2 -->|"postgres_fdw: rows that pass the filters"| D2[("your managed database (RDS, Supabase, Neon, …)")]
  end
  D1 --> J["Jev"]
  S2 --> J
  • In-database: installed into the Postgres that holds the data. This is the default whenever you control that server.
  • Sidecar: a local Postgres runs the extension and reaches the real database through postgres_fdw. Nothing is installed on the target, so it works with managed hosts, which do not allow custom compiled extensions. Filters still run on the target, and only the jev judgements run locally. Writes through the sidecar are not two-phase committed. Chapter 10 is about this mode.

The fine print

Everything here is true of the code today, and some of it will bite.

  • jev_prob returns two-decimal values, and ties are common. Add your own tie-break column to ORDER BY jev_prob(…), as the example at the top does with name.
  • Supported PostgreSQL majors: 17 and later (17 and 18 today). The nix package set is postgresqlNNPackages.postjevsql for each.
  • Not compatible with pg_duckdb loaded in the same server. pg_duckdb calls Postgres from its own threads, and this extension's planner hooks refuse to run off the backend thread (pgrx #2228).
  • TypeSafe's terms bind how the results are used. Cached answers may not be used to train a model that imitates Jev (MCA §2.3(b)), and one API key may not serve third parties through this extension (§2.3(a)).
  • Row contents are sent to TypeSafe. Do not point this at data you cannot share with them. TypeSafe states that it does not train on customer requests. Zero data retention is offered to enterprise customers only (docs.typesafe.ai/models, "Data handling").
  • A query shape it cannot batch yet is refused, never run row by row: a call over two relations, or one in RETURNING, ON CONFLICT or a MERGE action. The error comes before anything is sent.

Aside: where the ideas came from. Three earlier projects asked Jev from SQL, and this one takes the strongest part of each. realZachi/pg-jev (a plpython3u extension) gave the whole-row surface, jev(alias, …); its 20-rows-per-state batching was not taken, for the reason in the aside above. kylemclaren/jevql (a Go query rewriter outside the server) gave canonical row JSON, the column-list form, the price shown before spending, the spend guards, and its list of refused queries, read as a to-do list. giuliosmall/pg_typesafe (a C extension) gave cancellable in-backend HTTP, typed composite results, the privilege and secret model, and offline mock testing. On top of those come batching owned by the query plan, a durable cache that doubles as an audit trail, and Choice over more than 255 options.

Running it yourself

The repository is served by the lmjtfy site, with jevcrates (the shared Jev client) as a submodule beside it. No GitHub account is needed:

git clone --recurse-submodules https://lmjtfy.fun/postjevsql.git
cd postjevsql
cargo test -p postjevsql-core -p postjevsql-sidecar

Those two crates do no I/O, so their tests need no database, no network and no API key, and they finish in about a second. The generated Cargo workspace also lets cargo pgrx install (cargo-pgrx 0.19.2) build the extension into a Postgres you already run.

The whole suite, every crate and every integration test on Postgres 17 and 18, runs through buck2 in the devshell:

nix develop -c buck2 test //...

The devshell takes buck2 from the owner's own package set, which is private, so that command works on the owner's machines. Chapter 13 explains what it runs, and chapter 16 how the packages are built.

To use it, install the package for your major, put the API key in the server's environment as TYPESAFE_API_KEY (or name a file in jev.api_key_file), and then:

CREATE EXTENSION postjevsql;
SET jev.model = 'jev-1.13.0';   -- a pinned version; aliases are refused
GRANT EXECUTE ON FUNCTION jev_prob(anyelement, text) TO app;  -- spending is granted, never public

Files at the top

FileWhat
CLAUDE.mdThe design contract: every rule, where it came from, and the evidence for it.
flake.nixThe devshell (the toolchains, and a server for every supported major), the packages, the sidecar module and its VM checks (chapter 16).
flake.lockThe flake's pinned inputs.
.buckconfigbuck2's cells and platforms. The devshell writes .buckconfig.local beside it with store paths; that file is not committed.
BUCKExports the generated root files to their drift tests.
Cargo.tomlThe Cargo workspace, generated from the buck targets (chapter 20). Do not edit.
Cargo.lockCargo's lock for that workspace. buck's own versions are locked in third-party/Cargo.lock.
deny.tomlcargo deny check licenses: the allowed licences; AGPL and GPL are denied.
THIRD-PARTY, THIRD-PARTY-sidecarThe licence notices of every crate linked into the extension and into the sidecar CLI, generated (chapter 22).
LICENSE-MIT, LICENSE-APACHEThe licence.
.gitmodulesjevcrates, by the relative URL ../jevcrates.git, so a clone from the site finds it beside this one.
.gitignorebuck's and Cargo's output, and .buckconfig.local.

Some folders are not chapters, on purpose:

  • build/ holds the shared Starlark: VERSION, PG_MAJORS, and the first_party_* macros that give every Rust target its lint test. It is buck's own plumbing, read in chapters 26 and 27 where it is used, and its README says what each line is for.
  • none/ is an empty buck cell. The buck prelude refers to Meta's internal cells (fbcode, fbsource, …), and .buckconfig points them all here so those references resolve to nothing.
  • .claude/ is an agent's memory, buck-out/, target/ and result are build output, and the trees under third-party/ came from elsewhere (chapter 28).

Licence

Licensed under either of the MIT licence (LICENSE-MIT) or the Apache Licence 2.0 (LICENSE-APACHE), at your option. Copyright The postjevsql Authors.

Next: Chapter 1, crates/ →