README.mdpreviewREADME.mdsource116 lines · 8.3 KB · raw
1# Chapter 2: postjevsql, the functions you call from SQL
2
3This crate is the extension itself: the part Postgres loads when you say
4`CREATE EXTENSION postjevsql`. It declares every SQL function, every `jev.*`
5setting, the `jev_cache` table and the result types, and it holds the
6*judge*, the code that turns a call into a request, a cache row and an
7answer.
8
9Here is the surprise: **the SQL functions do nothing.** Look at `jev_prob`
10in `src/lib.rs` and its body is one line, which raises an error. The work is
11done by a plan node (chapter 7) that finds every call to these functions in
12a query and answers them itself, many rows at once. The function body runs
13only if a call escaped that node, and then it refuses rather than quietly
14sending one request per row. The functions are, in effect, a vocabulary the
15planner recognises.
16
17## The functions
18
19Every function takes the row first: a table alias for the whole row
20(`jev(t, …)`, which sends every column), or a column list (`jev((t.title,
21t.body), …)`, which sends only those, costs fewer tokens and is usually what
22you want). Every one is `STRICT`, so a NULL argument gives NULL and sends
23nothing.
24
25| Function | Returns | What |
26| --- | --- | --- |
27| `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. |
28| `jev_prob(row, q)` | `float8` | The probability that the answer to yes/no `q` is yes (a Noul). |
29| `jev_eval(row, q)` | `jev_noul_result` | The same judgment in full: probability, the model that answered, and the request's tokens. |
30| `jev_score(row, q, levels)` | `float8` | The probability-weighted level, 0 to n − 1, on a rubric of 2–10 `levels` given lowest first. |
31| `jev_score_norm(row, q, levels)` | `float8` | That level over n − 1, so 0 to 1. |
32| `jev_score_full(row, q, levels)` | `jev_score_result` | Score, confidence, per-level probabilities, legend, model, tokens. |
33| `jev_choice(row, q, options)` | `text` | The likeliest of 1–255 `options`, sent in your order. One option is returned without asking. |
34| `jev_choice_full(row, q, options)` | `jev_choice_result` | Choice, confidence, `[label, p]` pairs in your order, model, tokens. |
35| `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. |
36| `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. |
37| `jev_choice_tree_full(row, q, paths [, tau])` | `jev_tree_result` | Path, probability, separation from the runner-up, depth, leaf or not, and `fit`. |
38| `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. |
39| `jev_choice_shortlist_full(…)` | `jev_shortlist_result` | The final Choice in full, and `fit`. |
40| `jev_stats()` | one row | This session's requests, retries, redials, connections, cache hits and misses, `dedupe_hits`, tokens, cost and streams in flight. |
41
42Calls that ask the same question share one judgment: `jev` and `jev_prob`
43on the same question, `jev_score` and `jev_score_norm`, a scalar function and
44its `_full` twin, `jev_confidence` and the Choice or Score it is about. One
45request, one cache row. `fit` is the probability, asked as a separate yes/no
46question in the first request, that any of the labels fits at all, since a
47Choice always picks one even when none is right.
48
49> **Aside.** Why must options keep your order? Because order changes the
50> answer. One measurement moved a correct label from 0.34 to 0.83 just by
51> listing it last instead of first. So the extension never shuffles: your
52> order is sent, your order is part of the cache key, and reordering a list
53> is, correctly, a different question.
54
55## The settings
56
57| Setting | Who may set it | Default | What |
58| --- | --- | --- | --- |
59| `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. |
60| `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. |
61| `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. |
62| `jev.ca_file` | superuser | | Extra CA certificates to trust, for tests and private endpoints. |
63| `jev.dns_servers` | superuser | empty: `/etc/resolv.conf` | `ip[:port], …` to resolve the endpoint's host with. |
64| `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. |
65| `jev.concurrency` | anyone | 100 | Rows a scan judges at once, each one HTTP/2 stream; clamped to what the server accepts. |
66| `jev.price_per_mtok` | superuser | 0.042 | Dollars per million input tokens, jev-1.13.0's launch price. Output is not billed. |
67| `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. |
68| `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. |
69| `jev.cache_namespace` | anyone | empty | Part of every cache key: changing it re-judges everything. |
70| `jev.cache_only` | anyone | off | Serve answers from the cache only, and fail a miss without sending. Needs no API key. |
71| `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. |
72| `jev.threshold` | anyone | 0.5 | Where `jev()` turns true when the call gives no threshold. Yes/no questions only. |
73
74## What it keeps, and what it refuses
75
76Every answer is a row in `jev_cache`, keyed by the hash of exactly what was
77sent, and each row is a receipt: the request bytes, the requested and the
78answering model, TypeSafe's request id, the tokens billed, the full answer,
79the namespace and the time. The table is revoked from `PUBLIC`; the scan
80reaches it as its owner, because a role that could write it could forge
81answers.
82
83Every function is revoked from `PUBLIC` at install, so spending the API key
84takes an explicit `GRANT EXECUTE`. And every failure carries a SQLSTATE you
85can branch on, with the request id and attempt count in its DETAIL:
86
87| Failure | SQLSTATE |
88| --- | --- |
89| a setting, or an invalid request | 22023 |
90| a call left outside the batch scan | 0A000 |
91| no API key, two, or HTTP 401/403 | 28000 |
92| DNS, connect, TLS | 08001 |
93| interrupted, timed out | 08006 |
94| 429 or 529 | 53000 |
95| a miss under `jev.cache_only` | 55000 |
96| a row a concurrent update changed, in a recheck | 40001 |
97| over `jev.max_rows` or `jev.max_cost`, context overflow, a response over 8 MB | 54000 |
98| any other API error, or a wrong answer | 38000 |
99
100> **Try it.** `EXPLAIN SELECT * FROM people p WHERE jev(p, 'Could this
101> person work from home?');` sends nothing and needs no API key. The plan
102> shows a `Custom Scan (JevScan)` with `Candidate Rows`, `Estimated
103> Requests`, `Estimated Input Tokens` and `Worst-Case Cost`: the price,
104> before it is paid. `EXPLAIN ANALYZE` runs it and adds `Cache Hits`, `Cache
105> Misses`, `Shared Judgments`, `Requests`, `Input Tokens` and `Cost`.
106
107## For the people who maintain it
108
109| Path | What |
110| --- | --- |
111| [src/](src/) | The extension's code: settings, SQL, the judge, the failures. Chapter 3. |
112| [BUCK](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. |
113| [postjevsql.control](postjevsql.control) | The control file. Its version is pgrx's `@CARGO_VERSION@` placeholder, filled from `build/defs.bzl` at build. |
114| [Cargo.toml](Cargo.toml) | Generated by `tools/cargo-gen` (chapter 20), with a `pg17` and a `pg18` feature. |
115
116← Previous: [Chapter 1, crates/](../) · Up: [crates](../) · Next: [Chapter 3, postjevsql/src/](src/) →