Chapter 2: postjevsql, the functions you call from SQL
This crate is the extension itself: the part Postgres loads when you say
CREATE EXTENSION postjevsql. It declares every SQL function, every jev.*
setting, the jev_cache table and the result types, and it holds the
judge, the code that turns a call into a request, a cache row and an
answer.
Here is the surprise: the SQL functions do nothing. Look at jev_prob
in src/lib.rs and its body is one line, which raises an error. The work is
done by a plan node (chapter 7) that finds every call to these functions in
a query and answers them itself, many rows at once. The function body runs
only if a call escaped that node, and then it refuses rather than quietly
sending one request per row. The functions are, in effect, a vocabulary the
planner recognises.
The functions
Every function takes the row first: a table alias for the whole row
(jev(t, …), which sends every column), or a column list (jev((t.title, t.body), …), which sends only those, costs fewer tokens and is usually what
you want). Every one is STRICT, so a NULL argument gives NULL and sends
nothing.
| Function | Returns | What |
|---|---|---|
jev(row, q), jev(row, q, threshold) | bool | True when the yes-probability is at least the threshold: the argument, else jev.threshold, else 0.5. |
jev_prob(row, q) | float8 | The probability that the answer to yes/no q is yes (a Noul). |
jev_eval(row, q) | jev_noul_result | The same judgment in full: probability, the model that answered, and the request's tokens. |
jev_score(row, q, levels) | float8 | The probability-weighted level, 0 to n − 1, on a rubric of 2–10 levels given lowest first. |
jev_score_norm(row, q, levels) | float8 | That level over n − 1, so 0 to 1. |
jev_score_full(row, q, levels) | jev_score_result | Score, confidence, per-level probabilities, legend, model, tokens. |
jev_choice(row, q, options) | text | The likeliest of 1–255 options, sent in your order. One option is returned without asking. |
jev_choice_full(row, q, options) | jev_choice_result | Choice, confidence, [label, p] pairs in your order, model, tokens. |
jev_confidence(row, q, kind, options) | float8 | The vendor's confidence in a 'choice' or 'score', exactly as returned. A Noul has none, so 'noul' is refused. |
jev_choice_tree(row, q, paths [, tau]) | text | A choice among any number of labels arranged as a tree, each path written A > B > leaf. With tau, the deepest group at least that likely. |
jev_choice_tree_full(row, q, paths [, tau]) | jev_tree_result | Path, probability, separation from the runner-up, depth, leaf or not, and fit. |
jev_choice_shortlist(row, q, options [, descriptions]) | text | A choice among any number of flat labels, in two requests: chunks of up to 254, then a final over the top 3 of each. |
jev_choice_shortlist_full(…) | jev_shortlist_result | The final Choice in full, and fit. |
jev_stats() | one row | This session's requests, retries, redials, connections, cache hits and misses, dedupe_hits, tokens, cost and streams in flight. |
Calls that ask the same question share one judgment: jev and jev_prob
on the same question, jev_score and jev_score_norm, a scalar function and
its _full twin, jev_confidence and the Choice or Score it is about. One
request, one cache row. fit is the probability, asked as a separate yes/no
question in the first request, that any of the labels fits at all, since a
Choice always picks one even when none is right.
Aside. Why must options keep your order? Because order changes the answer. One measurement moved a correct label from 0.34 to 0.83 just by listing it last instead of first. So the extension never shuffles: your order is sent, your order is part of the cache key, and reordering a list is, correctly, a different question.
The settings
| Setting | Who may set it | Default | What |
|---|---|---|---|
jev.model | anyone | (none; required) | The pinned model, such as jev-1.13.0. Aliases like jev-latest are refused when set, and an answer from any other model fails the statement. |
jev.endpoint | superuser | https://api.typesafe.ai/v1/systemone | Where requests go. Must be https, always; the tests' mock serves TLS too, trusted through jev.ca_file. Whoever sets it receives the API key. |
jev.api_key_file | superuser | A file holding the API key. Exactly one of this and TYPESAFE_API_KEY in the server's environment; both is an error. | |
jev.ca_file | superuser | Extra CA certificates to trust, for tests and private endpoints. | |
jev.dns_servers | superuser | empty: /etc/resolv.conf | ip[:port], … to resolve the endpoint's host with. |
jev.keepalive_interval, jev.keepalive_timeout | superuser | 20 s, 10 s | HTTP/2 pings while a scan runs, and how long one may go unanswered. |
jev.concurrency | anyone | 100 | Rows a scan judges at once, each one HTTP/2 stream; clamped to what the server accepts. |
jev.price_per_mtok | superuser | 0.042 | Dollars per million input tokens, jev-1.13.0's launch price. Output is not billed. |
jev.max_rows, jev.max_cost | anyone | -1 (no limit) | Rows and dollars one statement may send or spend, checked before the first request and while it runs. 0 sends nothing. |
jev.max_tokens_per_second, jev.max_requests_per_minute | config file | 250,000, 1,200 | The account's limits, shared by every backend of the cluster; 0 is no limit. |
jev.cache_namespace | anyone | empty | Part of every cache key: changing it re-judges everything. |
jev.cache_only | anyone | off | Serve answers from the cache only, and fail a miss without sending. Needs no API key. |
jev.on_error | anyone | error | unsure turns a judgment that failed at the remote into NULL, so a jev filter skips that row instead of failing the statement. |
jev.threshold | anyone | 0.5 | Where jev() turns true when the call gives no threshold. Yes/no questions only. |
What it keeps, and what it refuses
Every answer is a row in jev_cache, keyed by the hash of exactly what was
sent, and each row is a receipt: the request bytes, the requested and the
answering model, TypeSafe's request id, the tokens billed, the full answer,
the namespace and the time. The table is revoked from PUBLIC; the scan
reaches it as its owner, because a role that could write it could forge
answers.
Every function is revoked from PUBLIC at install, so spending the API key
takes an explicit GRANT EXECUTE. And every failure carries a SQLSTATE you
can branch on, with the request id and attempt count in its DETAIL:
| Failure | SQLSTATE |
|---|---|
| a setting, or an invalid request | 22023 |
| a call left outside the batch scan | 0A000 |
| no API key, two, or HTTP 401/403 | 28000 |
| DNS, connect, TLS | 08001 |
| interrupted, timed out | 08006 |
| 429 or 529 | 53000 |
a miss under jev.cache_only | 55000 |
| a row a concurrent update changed, in a recheck | 40001 |
over jev.max_rows or jev.max_cost, context overflow, a response over 8 MB | 54000 |
| any other API error, or a wrong answer | 38000 |
Try it.
EXPLAIN SELECT * FROM people p WHERE jev(p, 'Could this person work from home?');sends nothing and needs no API key. The plan shows aCustom Scan (JevScan)withCandidate Rows,Estimated Requests,Estimated Input TokensandWorst-Case Cost: the price, before it is paid.EXPLAIN ANALYZEruns it and addsCache Hits,Cache Misses,Shared Judgments,Requests,Input TokensandCost.
For the people who maintain it
| Path | What |
|---|---|
| src/ | The extension's code: settings, SQL, the judge, the failures. Chapter 3. |
| BUCK | The library, and :ext, the installable tree (share/extension/postjevsql.control, the install script generated from the .so by tools/pgrx-schema, and lib/postjevsql.so) that the tests and the nix package install. |
| postjevsql.control | The control file. Its version is pgrx's @CARGO_VERSION@ placeholder, filled from build/defs.bzl at build. |
| Cargo.toml | Generated by tools/cargo-gen (chapter 20), with a pg17 and a pg18 feature. |
← Previous: Chapter 1, crates/ · Up: crates · Next: Chapter 3, postjevsql/src/ →