jev

September 18, 2026 · View on GitHub

jev lets you filter, rank and classify rows with plain English. 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.

INSTALL jev FROM community;
LOAD jev;
SET jev_api_key = 'your-key';   -- or export TYPESAFE_API_KEY

SELECT * FROM people p WHERE jev(p, 'the name is European');
SELECT subject, jev_prob(t, 'the customer is angry') AS p FROM tickets t ORDER BY p DESC LIMIT 20;
SELECT jev_choice(t, 'which team should handle this?', ['billing', 'technical', 'security']) AS team, count(*)
FROM tickets t GROUP BY 1;

jev() is an ordinary boolean scalar, so it composes with AND, joins, GROUP BY and ORDER BY jev_prob(...).

Inspired by pg-jev for PostgreSQL — see Credits.

Install

From the community repository (once the extension is listed there — see Status):

INSTALL jev FROM community;
LOAD jev;

Until then, load a build directly. Every push builds binaries for Linux, macOS and Windows in the Actions tab (artifact jev-…-<platform> inside the "Build extension binaries" run), or build it yourself:

git clone --recurse-submodules https://github.com/recodelabs/duckdb-jev.git && cd duckdb-jev
GEN=ninja make release          # needs cmake, ninja and OpenSSL (vcpkg or brew/apt); compiles DuckDB too
# -> build/release/extension/jev/jev.duckdb_extension

A locally built or downloaded extension is unsigned, so start DuckDB with unsigned extensions allowed and LOAD it by path:

duckdb -unsigned
LOAD '/path/to/jev.duckdb_extension';
import duckdb
con = duckdb.connect(config={"allow_unsigned_extensions": "true"})
con.execute("LOAD '/path/to/jev.duckdb_extension'")

The extension is built against DuckDB 1.5.5; use the matching DuckDB version.

Usage

Get an API key from https://console.typesafe.ai and give it to the session:

SET jev_api_key = 'your-key';           -- or: export TYPESAFE_API_KEY=your-key before starting DuckDB

Then ask questions about rows. rec is the table alias, so every column is visible to the model:

CREATE TABLE cities AS SELECT * FROM (VALUES
  ('Nairobi','Kenya'), ('Mombasa','Kenya'), ('Abuja','Nigeria'), ('Lagos','Nigeria'),
  ('Canberra','Australia'), ('Sydney','Australia'), ('Bern','Switzerland'), ('Zurich','Switzerland')
) t(city, country);

-- filter
SELECT city FROM cities c WHERE jev(c, 'the city is the capital of the country');
-- Nairobi
-- Abuja
-- Canberra
-- Bern

-- rank by probability, then pick your own threshold
SELECT city, round(jev_prob(c, 'the city is the capital of the country'), 2) AS p
FROM cities c ORDER BY p DESC;
-- Nairobi  0.98
-- Abuja    0.98
-- Canberra 0.98
-- Bern     0.91
-- Lagos    0.07
-- Zurich   0.06
-- Sydney   0.05
-- Mombasa  0.04

SELECT city FROM cities c WHERE jev(c, 'the city is the capital of the country', 0.95);  -- stricter
-- Nairobi
-- Abuja
-- Canberra

-- classify
SELECT city, jev_choice(c, 'which continent is this city on?', ['africa', 'europe', 'asia', 'oceania'])
FROM cities c;
-- Nairobi  africa
-- Mombasa  africa
-- Abuja    africa
-- Lagos    africa
-- Canberra oceania
-- Sydney   oceania
-- Bern     europe
-- Zurich   europe

-- grade on an ordered scale (0 = first level … n-1 = last level)
SELECT city, jev_score(c, 'how large is the city population?', ['under 500k', '500k-2M', '2M-10M', 'over 10M'])
FROM cities c ORDER BY 2 DESC;
-- Lagos    2.69
-- Sydney   1.96
-- Nairobi  1.79
-- Abuja    1.19
-- Mombasa  1.16
-- Zurich   0.52
-- Canberra 0.11
-- Bern     0.11

-- the raw answer, including per-option probabilities and confidence
SELECT jev_eval(c, 'which continent is this city on?', 'choice', ['africa', 'europe']) FROM cities c LIMIT 1;
-- {"type":"choice","choice":"africa","confidence":1.0,"probabilities":{"africa":1.0,"europe":0.0}}

-- what did that cost?
SELECT jev_stats();
-- {"requests":4,"retries":0,"batches":4,"rows_evaluated":25,"cache_hits":16,
--  "input_tokens":3093,"output_tokens":670,"estimated_cost_usd":0.000129906,
--  "api_ms":2120,"errors":0,"cache_entries":25}

Probabilities depend on the whole table sent in each request (the rows are the model's context), so the same city can score differently in a different table.

Only the columns you select are sent, so a subquery or view narrows what the model sees:

SELECT s.city FROM (SELECT city, country FROM cities) s WHERE jev(s, 'the city is coastal');

Answers are cached per row content for the life of the database instance, so re-running a query, changing the threshold or sorting by jev_prob() costs nothing extra. The cache has no size limit and no expiry — a long-lived session judging millions of distinct rows will keep them all until jev_cache_clear(). Each API request prints one line to stderr (rows, tokens, estimated cost, time); those lines come from several DuckDB threads, so they can arrive interleaved. Turn them off with SET jev_notices = false.

Tips for conditions: state the exact condition ('the customer threatens to leave or dispute a charge' beats 'churn risk'), keep arithmetic and exact matches in SQL, and look at the jev_prob() distribution before choosing a threshold — ambiguous rows really do land near 0.5.

Functions

FunctionReturnsNotes
jev(rec, condition)BOOLEANjev_prob(...) >= jev_threshold
jev(rec, condition, threshold)BOOLEANjev_prob(...) >= coalesce(threshold, jev_threshold)
jev_prob(rec, condition)DOUBLEP(row satisfies condition), 0..1
jev_score(rec, question, levels)DOUBLEprobability-weighted level index, 0..n-1
jev_score_norm(rec, question, levels)DOUBLEthe same, rescaled to 0..1
jev_choice(rec, question, options)VARCHARmost likely option
jev_confidence(rec, question, kind, options)DOUBLEconfidence of a score or choice answer
jev_eval(rec, question, kind, options)JSONthe raw answer object
jev_stats()JSONrequests, retries, tokens, estimated cost, cache hits (per database instance)
jev_cache_clear()BOOLEANforget cached judgments
jev_version()VARCHARextension version
jev_set_api_key(v), jev_set_api_url(v), jev_set_model(v)VARCHARfor clients that cannot issue SET

rec is any struct: a table alias (jev(p, ...)), a subquery alias, or struct_pack(...). kind is 'noul', 'score' or 'choice'; options is the list of levels or choices and may be NULL for 'noul'.

jev builds on DuckDB's json extension (autoloaded) to turn a row into the JSON document the API sees.

Settings

SettingDefaultMeaning
jev_api_keyenv TYPESAFE_API_KEYTypeSafe API key
jev_api_urlhttps://api.typesafe.ai/v1/systemoneendpoint (proxies, mocks)
jev_modeljev-latestmodel name
jev_threshold0.5probability at which jev() is true
jev_batch_size40rows per API request (must be >= 1)
jev_concurrency6parallel requests per vector, capped at 64
jev_timeout90seconds per request
jev_noticestrueprint one line per API request to stderr

How it works

DuckDB hands the extension up to 2048 rows at a time. Rows that share a question, a kind and a set of options are grouped, identical rows are judged once, and whatever is not already cached is split into jev_batch_size batches that run on up to jev_concurrency threads. Answers and usage counters live in a database-scoped cache that every connection of that database shares; jev_cache_clear() empties it.

jev_concurrency is per DuckDB thread, not per query: DuckDB may evaluate the function on several threads at once, so the requests in flight can reach DuckDB threads x jev_concurrency (capped at 64 per thread). The default batch of 40 rows carries 40 questions in one request, which the live API answers in well under a second.

Requests retry 429, 529, 5xx and transport errors with exponential backoff (0.5 s doubling to 8 s, six attempts); a Retry-After header on a 429 or 503 wins over that schedule, up to 30 s. Any other status raises jev: TypeSafe API error <code> <body>.

See docs/DESIGN.md for the full design.

Building and testing

GEN=ninja make release      # first build compiles DuckDB itself
./test/run.sh               # starts test/mock_api.py, runs make test, stops it again

The SQL tests never touch the real API: test/mock_api.py is a deterministic stand-in on 127.0.0.1:8765. They only run when JEV_MOCK_RUNNING=1 is exported (test/run.sh does it), so a bare make test on a machine without the mock skips them instead of failing.

Status

Version 0.1.0. Builds and tests are green on Linux, macOS and Windows; submission to the DuckDB community extension repository is pending, so for now install from a build (see Install).

Credits

duckdb-jev was inspired by pg-jev, Zachi's natural-language WHERE clauses for PostgreSQL, which showed how well a calibrated yes/no model fits inside a SQL predicate. This is an independent implementation of that idea for DuckDB; the two share no code. Row judgments come from TypeSafe's Jev.