Write the condition the way you would say it. Postgres does the rest.
jev lets you filter, rank and classify rows with plain-language conditions. Every row is judged by
TypeSafe's Jev, a System One model that returns calibrated probabilities
instead of generated text. No index, no embeddings, no vector column.
Website: pgjev.com
CREATE EXTENSION jev CASCADE;
SELECT * FROM people WHERE jev(people, 'the name is European');
SELECT subject, jev_prob(tickets, 'the customer is angry') AS p
FROM tickets ORDER BY p DESC LIMIT 20;
SELECT jev_choice(tickets, 'which team should handle this?',
ARRAY['billing', 'technical', 'security', 'sales']) AS team, count(*)
FROM tickets GROUP BY 1;
SELECT name, jev_score(products, 'how luxurious is this product?',
ARRAY['budget', 'mid-range', 'premium', 'luxury']) AS luxury
FROM products ORDER BY luxury DESC;jev() is an ordinary boolean function, so it composes with everything else in SQL: AND age > 40,
joins, GROUP BY, LIMIT, ORDER BY jev_prob(...).
jev(table, 'condition')receives the row as a composite value. The first call for a table + condition starts a read-ahead that streams the table in physical order (TID range scans;OFFSETpages for views), so memory stays constant whatever the table size.- Rows are packed
jev.batch_size(20) per request into one shared state ({"condition": ..., "rows": [...]}) with one yes/no Noul question per row. Jev evaluates all questions over one state in parallel, which amortises the ~270-token request overhead (about 435 tokens for one row alone vs 175 per row in batches of 20). - Up to 2 ×
jev.concurrencyrequests are in flight over persistent HTTPS connections, and every row is answered as soon as its batch returns, so aLIMITstops the read-ahead after the in-flight window, and rows that cheaper predicates filter out beforejev()runs (WHERE age > 60 AND jev(...)) are skipped rather than judged. - Answers are cached per row content for the session, so re-running, changing the threshold or sorting by
probability is free. Rows from a subquery or CTE (anonymous
recordtype) can't be read ahead and are judged one request at a time; putjev()on base tables or views when you can.
Measured on a 2,000-row table from Europe (~190 ms to the API): first run ≈ 3.5 s in 100 requests, ≈ 296k input
tokens, ≈ $0.012; second run ≈ 50 ms; LIMIT 3 on a new condition ≈ 0.6 s. A new condition in a session that
still holds its pooled connections (idle for less than jev.keepalive) takes ≈ 2.3 s: the first request on each
fresh connection is the slow one. Version 0.1.0 needed 8.5 s (and 338k tokens) for the full query and 8.4 s for
the LIMIT.
Jev has to find rows[i] by position in the array, and that gets unreliable in long arrays. Against ground truth
from structured columns (job title, EU membership, a phrase in a free-text field; 400 rows each), batches of 1–20
rows were 100 % correct, batches of 40 were 92–98 % and batches of 80 were 77–94 %. Wider rows (1,000 characters)
made no difference at 20. Naming rows instead of indexing them did not help. Batches of 20 cost 4 % more tokens than
batches of 40 and are just as fast, because a request's latency barely depends on its size.
Requirements: PostgreSQL 14–17 with plpython3u (package postgresql-plpython3-NN on Debian/Ubuntu,
included in the EDB and Postgres.app builds), a superuser, and a TypeSafe API key from https://console.typesafe.ai.
Managed hosts that withhold superuser or plpython3u (Supabase, Neon, RDS, …) cannot run it; see
Where it runs.
The repo ships an agent skill on skills.sh. Install it into your project and tell Claude Code, Codex, Cursor or any other skill-aware agent to finish the job:
npx skills add realZachi/pg-jevInstall pgjev on this server and set it up.
The agent runs a preflight (PostgreSQL version, plpython3u, superuser), pgxn install jev or make install against the right
pg_config, CREATE EXTENSION jev CASCADE, places the API key and runs a smoke test. Afterwards it also knows how
to write cost-conscious jev() queries ("find the tickets where the customer threatens to cancel") and to explain
what pgjev can do. The docs are readable as Markdown for agents too: append .md to any page under
https://pgjev.com/docs (see For agents).
pip install pgxnclient # once; also available as `pgxn-client` in Debian/Ubuntu and Homebrew
pgxn install jev # downloads the release from pgxn.org and runs `make install` against pg_config on PATH
psql -c "CREATE EXTENSION jev CASCADE"Use pgxn install jev --pg_config=/path/to/pg_config (or sudo pgxn install jev) when the server's pg_config
is not on PATH or the extension directory is not writable.
git clone https://github.com/realZachi/pg-jev.git && cd pg-jev
make install # uses pg_config on PATH; or: make install PG_CONFIG=/path/to/pg_config
psql -c "CREATE EXTENSION jev CASCADE" # superuser required (plpython3u is untrusted); CASCADE creates plpython3udocker build -t pg-jev . # add --build-arg PG_MAJOR=17 for another major
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=pw -e TYPESAFE_API_KEY=your-key pg-jev
psql postgres://postgres:pw@localhost/postgres -c "CREATE EXTENSION jev CASCADE"Either export TYPESAFE_API_KEY in the environment of the PostgreSQL server process, or set it per session
or per role:
SET jev.api_key = 'your-key';
ALTER ROLE analyst SET jev.api_key = 'your-key'; -- persistent, per role| Function | Returns | Purpose |
|---|---|---|
jev(row, condition [, threshold]) |
boolean | WHERE predicate. Threshold: argument → jev.threshold → 0.5 |
jev_prob(row, condition) |
float8 | Probability 0..1 that the row satisfies the condition |
jev_score(row, question, levels text[]) |
float8 | Probability-weighted position on ordered levels (0 .. n-1) |
jev_score_norm(row, question, levels) |
float8 | Same, normalised to 0..1 |
jev_choice(row, question, options text[]) |
text | The most likely option for the row |
jev_confidence(row, question, kind, options) |
float8 | Confidence of a score/choice answer |
jev_eval(row, question, kind, options) |
jsonb | Full raw answer (probabilities, legend, confidence) |
jev_stats() |
jsonb | Requests, tokens, estimated cost, cache hits, in-flight requests and pooled connections for this session |
jev_cache_clear() |
void | Forget cached judgments |
jev_version() |
text | Extension version |
row is the table alias itself (jev(people, ...)) or a subquery alias.
All settings are plain GUCs: SET jev.<name> = ..., ALTER ROLE ... SET, ALTER DATABASE ... SET, or postgresql.conf.
| Setting | Default | Meaning |
|---|---|---|
jev.api_key |
env TYPESAFE_API_KEY |
TypeSafe API key |
jev.model |
jev-latest |
Model name or pinned version such as jev-1.13.0 |
jev.threshold |
0.5 |
Probability at which jev() returns true |
jev.batch_size |
20 |
Rows per API request. Accuracy drops measurably above ~20–25 (see above) |
jev.concurrency |
16 |
Parallel API requests; up to twice that many are queued ahead of the executor |
jev.max_prefetch_rows |
5000 |
How far past a cache miss the read-ahead scans to find the requested row, and how many skipped rows it keeps for later requests (memory bound) |
jev.notices |
on |
Emit a progress NOTICE per finished request and a summary per table with request count, tokens, estimated cost and time |
jev.api_url |
https://api.typesafe.ai/v1/systemone |
Endpoint (proxies, mocks) |
jev.timeout |
30 |
Seconds per API request. Waits are interruptible: statement_timeout and cancel requests apply within 250 ms |
jev.keepalive |
600 |
Seconds a pooled API connection may sit idle before it is reconnected. The first request on a fresh connection costs a TLS handshake plus, measured, up to 1.5 s of server-side setup, so keep connections alive across queries; TCP keepalive probes catch silently dropped ones |
jev.max_rows_per_statement |
0 (off) |
Abort a statement that would send more rows than this to the API. Spend guard for shared deployments |
jev.max_chars_per_statement |
0 (off) |
Same, for characters of row data |
Jev answers the question you wrote, literally. A few things that help (more in the TypeSafe docs):
- State the exact condition:
'the customer threatens to leave, dispute a charge, or take legal action'beats'churn risk'. - Keep arithmetic, dates and exact matches in SQL; let the model judge meaning.
- Look at the distribution with
jev_prob()before picking a threshold. Ambiguous cases really do land near 0.5. - Send only the columns the judgment needs: create a view with the relevant columns (and any pre-filter) and call
jev(view_alias, ...)on the view. Views are read ahead and batched like tables.
- This is a full scan by design: every row the executor asks about goes to the API. Cheaper predicates in the same
WHERErun first and their rejects are skipped; aLIMITstops early;jev.max_rows_per_statementcaps spend. - Row contents are sent to a third-party API. Do not use it on data you may not share.
- The cache lives in the backend session (PL/Python
GD). Connection pools with many sessions each warm their own cache. plpython3uis an untrusted language: only superusers can create the extension, and functions run with the server's OS privileges.
make docker-test # builds test/Dockerfile and runs the regression suite (PG_MAJOR=16 by default)
make docker-test PG_MAJOR=17Locally with a running server and pg_config on PATH:
make install
python3 test/mock_api.py & # deterministic stand-in for the TypeSafe API
make installcheck # pg_regress, tests in test/sql, expected output in test/expectedThe regression tests never call the live API. To try the real thing, SET jev.api_key and run any query.
See CONTRIBUTING.md and docs/PUBLISHING.md for release steps.
PostgreSQL License. Jev and TypeSafe are trademarks of their respective owners; this project is not affiliated with TypeSafe.