README.mdpreviewREADME.mdsource116 lines · 8.3 KB · raw

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.

FunctionReturnsWhat
jev(row, q), jev(row, q, threshold)boolTrue when the yes-probability is at least the threshold: the argument, else jev.threshold, else 0.5.
jev_prob(row, q)float8The probability that the answer to yes/no q is yes (a Noul).
jev_eval(row, q)jev_noul_resultThe same judgment in full: probability, the model that answered, and the request's tokens.
jev_score(row, q, levels)float8The probability-weighted level, 0 to n − 1, on a rubric of 2–10 levels given lowest first.
jev_score_norm(row, q, levels)float8That level over n − 1, so 0 to 1.
jev_score_full(row, q, levels)jev_score_resultScore, confidence, per-level probabilities, legend, model, tokens.
jev_choice(row, q, options)textThe likeliest of 1–255 options, sent in your order. One option is returned without asking.
jev_choice_full(row, q, options)jev_choice_resultChoice, confidence, [label, p] pairs in your order, model, tokens.
jev_confidence(row, q, kind, options)float8The 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])textA 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_resultPath, probability, separation from the runner-up, depth, leaf or not, and fit.
jev_choice_shortlist(row, q, options [, descriptions])textA 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_resultThe final Choice in full, and fit.
jev_stats()one rowThis 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

SettingWho may set itDefaultWhat
jev.modelanyone(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.endpointsuperuserhttps://api.typesafe.ai/v1/systemoneWhere 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_filesuperuserA file holding the API key. Exactly one of this and TYPESAFE_API_KEY in the server's environment; both is an error.
jev.ca_filesuperuserExtra CA certificates to trust, for tests and private endpoints.
jev.dns_serverssuperuserempty: /etc/resolv.confip[:port], … to resolve the endpoint's host with.
jev.keepalive_interval, jev.keepalive_timeoutsuperuser20 s, 10 sHTTP/2 pings while a scan runs, and how long one may go unanswered.
jev.concurrencyanyone100Rows a scan judges at once, each one HTTP/2 stream; clamped to what the server accepts.
jev.price_per_mtoksuperuser0.042Dollars per million input tokens, jev-1.13.0's launch price. Output is not billed.
jev.max_rows, jev.max_costanyone-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_minuteconfig file250,000, 1,200The account's limits, shared by every backend of the cluster; 0 is no limit.
jev.cache_namespaceanyoneemptyPart of every cache key: changing it re-judges everything.
jev.cache_onlyanyoneoffServe answers from the cache only, and fail a miss without sending. Needs no API key.
jev.on_erroranyoneerrorunsure turns a judgment that failed at the remote into NULL, so a jev filter skips that row instead of failing the statement.
jev.thresholdanyone0.5Where 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:

FailureSQLSTATE
a setting, or an invalid request22023
a call left outside the batch scan0A000
no API key, two, or HTTP 401/40328000
DNS, connect, TLS08001
interrupted, timed out08006
429 or 52953000
a miss under jev.cache_only55000
a row a concurrent update changed, in a recheck40001
over jev.max_rows or jev.max_cost, context overflow, a response over 8 MB54000
any other API error, or a wrong answer38000

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 a Custom Scan (JevScan) with Candidate Rows, Estimated Requests, Estimated Input Tokens and Worst-Case Cost: the price, before it is paid. EXPLAIN ANALYZE runs it and adds Cache Hits, Cache Misses, Shared Judgments, Requests, Input Tokens and Cost.

For the people who maintain it

PathWhat
src/The extension's code: settings, SQL, the judge, the failures. Chapter 3.
BUCKThe 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.controlThe control file. Its version is pgrx's @CARGO_VERSION@ placeholder, filled from build/defs.bzl at build.
Cargo.tomlGenerated by tools/cargo-gen (chapter 20), with a pg17 and a pg18 feature.

← Previous: Chapter 1, crates/ · Up: crates · Next: Chapter 3, postjevsql/src/ →