Skip to content

Commit 83c5494

Browse files
mstrathmanclaude
andcommitted
docs: full pre-release audit — every example executed, gaps filled
A fresh-eyes completeness pass over README, quickstart, the site, and the binding READMEs, with every SQL block executed against the built extension across Python, Node (node:sqlite, better-sqlite3, Drizzle), SQLAlchemy, Rust, and the SQLite CLI. Broken examples found by execution and fixed: the README installing snippet still called the deleted forecast TVF; the README distill examples fed a TEXT column as a feature (students are numeric-only, now documented); quickstart printed literal \n in headers. Stale claims corrected: sub-pca is not the anomaly default (theta-classic is), backtest pins theta-classic and accepts only statistical ids or auto, predict_ulid requires an ISO-8601 argument and is deterministic. Gaps filled: accepted timestamp formats and whole-second flooring; predict()'s row_ref convention (first apply column echoed, never a feature; features match train columns by name); distill_forecast's window-mode column layout (context then horizon*nquant, by position); predict_register in the functions reference; the missing quantiles option and training defaults; documented limits (2,097,152 max input rows, context_limit defaults, predict's 100k-row/64-feature caps); and the CLI page now names the GitHub release assets + SHA256SUMS and warns that macOS /usr/bin/sqlite3 cannot .load. The PyPI page body (bindings/python/README.md) loses its stale everpure title and wrong import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent df39601 commit 83c5494

9 files changed

Lines changed: 112 additions & 48 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ that model the same way it owns its tables.
7575
```sql
7676
-- once: distill (your labels, or a teacher's precomputed predictions)
7777
SELECT model_id, holdout_metric FROM distill_predict(
78-
'SELECT tenure, spend, plan, churned FROM customers',
78+
'SELECT tenure, spend, churned FROM customers',
7979
'{"target":"churned","student_id":"churn-v1","student_kind":"gbt"}');
8080

8181
-- forever: serve the student per row, in microseconds, no runtime attached
82-
SELECT * FROM predict(NULL, 'SELECT id, tenure, spend, plan FROM customers',
82+
SELECT * FROM predict(NULL, 'SELECT id, tenure, spend FROM customers',
8383
'{"model":"churn-v1"}');
8484

8585
-- the same move for time series: distill a Chronos-class teacher's forecasts
@@ -208,7 +208,9 @@ interpretable); `'gbt'` is a gradient-boosted forest with second-order
208208
matches or beats tuned XGBoost on most tasks; `'mlp'` is a one-hidden-layer
209209
neural net (classification only) for boundaries an axis-aligned tree ensemble
210210
renders poorly. All three run in the zero-dependency core and are
211-
deterministic.
211+
deterministic. Distillation feature columns must be numeric: encode
212+
categorical text before distilling (the in-context `knn5-incontext` handles
213+
text features itself; the distillers do not).
212214

213215
When the teacher gives calibrated probabilities (as a foundation model does),
214216
distill its whole distribution instead of its hard label with `proba` and
@@ -220,7 +222,7 @@ hard label throws away.
220222
-- distill TabFM's predicted probabilities (columns p_stay, p_churn), while the
221223
-- true `churned` label scores the holdout
222224
SELECT model_id, holdout_metric FROM distill_predict(
223-
'SELECT tenure, spend, plan, p_stay, p_churn, churned FROM scored',
225+
'SELECT tenure, spend, p_stay, p_churn, churned FROM scored',
224226
'{"target":"churned","proba":["p_stay","p_churn"],
225227
"classes":["stay","churn"],"student_id":"churn-soft"}');
226228
```
@@ -270,7 +272,7 @@ import sqlite3, sqlite_predict
270272

271273
db = sqlite3.connect("app.db")
272274
sqlite_predict.load(db)
273-
db.execute("SELECT * FROM forecast('SELECT ts, value FROM readings', 24)")
275+
db.execute("SELECT forecast(ts, value, 24) FROM readings")
274276
```
275277

276278
Or drop the extension into any SQLite client directly. Three ways, in order of

‎bindings/python/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# everpure-sqlite-predict
1+
# sqlite-predict
22

33
Prediction as a SQL primitive for SQLite: `forecast()`, `detect_anomalies()`,
44
`predict()`. The wheel bundles the zero-dependency loadable extension for your
55
platform.
66

77
```python
88
import sqlite3
9-
from everpure import sqlite_predict
9+
import sqlite_predict
1010

1111
db = sqlite3.connect(":memory:")
1212
sqlite_predict.load(db)

‎examples/quickstart.sql‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ SELECT datetime('2024-01-01', '+' || i || ' hours') AS ts,
1313
+ (CASE WHEN i = 300 THEN 60 ELSE 0 END) AS value
1414
FROM t;
1515

16-
.print '\n== forecast the next 24 hours, with prediction intervals =='
16+
.print ''
17+
.print '== forecast the next 24 hours, with prediction intervals =='
1718
-- forecast() is an aggregate: your statement supplies the rows (WHERE,
1819
-- joins, and GROUP BY all compose), and each group returns one JSON
1920
-- document: {"model", "status", "rows"}. Expand it to typed rows with
@@ -26,20 +27,23 @@ SELECT r.step,
2627
FROM forecast_rows((SELECT forecast(ts, value, 24) FROM readings)) AS r
2728
LIMIT 5;
2829

29-
.print '\n== find the anomaly (sub-pca, a SOTA-level subsequence detector) =='
30+
.print ''
31+
.print '== find the anomaly (sub-pca, a SOTA-level subsequence detector) =='
3032
SELECT r.ts,
3133
round(r.value, 1) AS value,
3234
round(r.anomaly_probability, 3) AS score
3335
FROM anomaly_rows((SELECT detect_anomalies(ts, value, '{"model":"sub-pca"}')
3436
FROM readings)) AS r
3537
WHERE r.is_anomaly = 1;
3638

37-
.print '\n== multi-series is just GROUP BY =='
39+
.print ''
40+
.print '== multi-series is just GROUP BY =='
3841
SELECT substr(ts, 12, 2) AS hour_of_day,
3942
json_extract(forecast(ts, value, 4), '$.status') AS status
4043
FROM readings GROUP BY hour_of_day LIMIT 4;
4144

42-
.print '\n== how accurate is it here? backtest on your own data =='
45+
.print ''
46+
.print '== how accurate is it here? backtest on your own data =='
4347
SELECT fold, model, round(mase, 2) AS mase, round(coverage, 2) AS coverage
4448
FROM backtest('SELECT ts, value FROM readings', 24, '{"folds":5}')
4549
LIMIT 5;

‎website/src/content/docs/benchmarks.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ seasonal-naive floor of 1.0.
1616
| Model | MASE | Notes |
1717
| --- | --- | --- |
1818
| seasonal-naive | 1.11 | the floor |
19-
| theta-classic | 1.03 | bundled default, zero dependencies |
19+
| theta-classic | 1.03 | bundled statistical model, zero dependencies |
2020
| distilled Chronos student | 0.89 | native blob, no runtime at serve time |
2121
| Chronos (teacher, ONNX) | ~0.80 | for reference; needs the ONNX build |
2222

‎website/src/content/docs/getting-started/cli.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ description: Load sqlite-predict in the SQLite CLI, or vendor the single C file.
77

88
Download a prebuilt loadable for your platform from the
99
[GitHub releases](https://github.com/PureStorage-OpenConnect/sqlite-predict/releases)
10-
and `.load` it:
10+
(`predict0-<os>-<arch>.{so,dylib,dll}`, checksums in `SHA256SUMS`) and
11+
`.load` it. One macOS note: the system `/usr/bin/sqlite3` is compiled without
12+
extension loading, so use a stock CLI (`brew install sqlite`):
1113

1214
```sh
1315
sqlite3 mydata.db

‎website/src/content/docs/guides/distillation.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@ SELECT row_ref, prediction, confidence FROM predict(NULL,
2727

2828
The `NULL` first argument is the signature of serving a student: `predict`
2929
normally takes a training query for in-context models, but a student carries
30-
its training inside its blob, so passing one is an error.
30+
its training inside its blob, so pass `NULL`; a query in that position is
31+
not read when a student serves the call.
3132

3233
`student_kind` is `tree` (a single CART), `gbt` (a gradient-boosted forest
3334
with second-order leaves, which matches or beats tuned XGBoost on most
3435
tasks), or `mlp` (a one-hidden-layer net for boundaries a tree renders
3536
poorly). With `proba` and `classes` the student learns the teacher's full
3637
probability distribution (soft-label distillation), not just its argmax.
38+
Feature columns must be numeric: encode categorical text before distilling
39+
(the in-context `knn5-incontext` handles text features itself; the distiller
40+
does not).
3741

3842
## Forecasting: distill with `distill_forecast`, serve with `forecast`
3943

@@ -55,9 +59,12 @@ SELECT forecast(ts, value, 12, '{"model":"auto"}') FROM readings;
5559

5660
The student is a DLinear/TiDE-style net (a linear skip plus a small
5761
residual). Without an onnx teacher, `distill_forecast` also trains directly
58-
from windowed rows: each training row is `context` window columns followed by
59-
`horizon` continuation columns (your own teacher's forecasts, computed
60-
anywhere and stored as columns).
62+
from windowed rows: each training row is, by position, `context` window
63+
columns followed by `horizon` continuation columns (your own teacher's
64+
forecasts, computed anywhere and stored as columns), and the column count
65+
must match exactly. Pass `quantiles` to distill a teacher's quantile fan
66+
instead of a point forecast: the continuation part then holds
67+
`horizon * nquant` columns, one block of quantile levels per step.
6168

6269
## Distribute the student
6370

‎website/src/content/docs/reference/functions.md‎

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ is written, so they work on read-only databases and inside views.
1414

1515
One `(ts, value)` observation per input row; rows are sorted by `ts`
1616
internally, so input order never matters. `horizon` is 1 to 1000, and it and
17-
`options` must be constant within a group. Returns one JSON document per
18-
group:
17+
`options` must be constant within a group.
18+
19+
`ts` accepts ISO-8601 UTC text (`YYYY-MM-DD`, optionally followed by
20+
`[T ]HH:MM[:SS][Z]`) or a non-negative integer epoch, read as seconds, or as
21+
milliseconds when the value is at or above 100000000000. Every timestamp is
22+
floored to the whole second before modeling. A statement may feed at most
23+
2,097,152 input rows across its groups; more raises `PREDICT_ERR_RESOURCE`.
24+
25+
Returns one JSON document per group:
1926

2027
```json
2128
{"model": "theta-classic",
@@ -29,7 +36,8 @@ SELECT city, forecast(ts, value, 24) FROM readings GROUP BY city;
2936
```
3037

3138
`model` is the resolved model id (useful with `'{"model":"auto"}'`); `status`
32-
is `ok`, `truncated`, `insufficient_history`, or `non_numeric`. A degraded
39+
is `ok`, `truncated`, `insufficient_history` (fewer than 8 points), or
40+
`non_numeric`. A degraded
3341
series returns a status document with empty `rows`; it does not fail the
3442
statement. An aggregate over zero rows returns `NULL` (like `sum()`).
3543

@@ -38,7 +46,8 @@ error explaining that `forecast` is an aggregate over your rows.
3846

3947
### `detect_anomalies(ts, value [, options])`
4048

41-
Same document pattern; each element of `rows` carries `ts`, `value`,
49+
Same document pattern and the same accepted timestamp forms; each element of
50+
`rows` carries `ts`, `value`,
4251
`forecast`, `lower_bound`, `upper_bound`, `is_anomaly`,
4352
`anomaly_probability`. The interval fields are null during warmup and for
4453
model `sub-pca`.
@@ -74,27 +83,48 @@ of `(time, value)`, optionally with grouping columns. Columns: `series_key`,
7483

7584
Learns from `train_query` (features plus a `target` column) and predicts the
7685
target for every row of `apply_query`. Columns: `row_ref`, `prediction`,
77-
`confidence`, `status`. To serve a distilled student, pass its id as the
86+
`confidence`, `status`.
87+
88+
The first column of `apply_query` is the row reference: it is echoed back as
89+
`row_ref` so you can join predictions to your rows, and it is **never a
90+
feature**. Every column after it is a feature and must match a `train_query`
91+
feature column **by name**; an apply column with no matching train column is
92+
an error. So `predict('SELECT f1, f2, label FROM t', 'SELECT id, f1, f2 FROM
93+
u', '{"target":"label"}')` trains on `f1, f2` and predicts one row per `u`
94+
row, keyed by `id`.
95+
96+
To serve a distilled student, pass its id as the
7897
`model` option and `NULL` as `train_query` (the student already learned):
7998
`predict(NULL, 'SELECT id, f1, f2 FROM t', '{"model":"churn-v1"}')`.
8099

100+
The default in-context model caps training at 100,000 rows and 64 feature
101+
columns; more raises `PREDICT_ERR_CONTEXT_TOO_LARGE` / `PREDICT_ERR_SCHEMA`.
102+
81103
### `distill_predict(train_query [, options])`
82104

83-
Fits a native tabular student and registers it. Columns: `model_id`,
105+
Fits a native tabular student and registers it. Requires the `target` and
106+
`student_id` options; feature columns must be numeric. Columns: `model_id`,
84107
`content_hash`, `train_rows`, `holdout_metric`.
85108

86109
### `distill_forecast(train_query [, options])`
87110

88-
Fits a native forecast student and registers it. Columns: `model_id`,
89-
`content_hash`, `train_rows`, `train_rmse`.
111+
Fits a native forecast student and registers it. Requires the `context`,
112+
`horizon`, and `student_id` options. Two input modes: with a `teacher`
113+
(ONNX build), `train_query` yields `(series_key, value)` rows and the teacher
114+
labels sliding windows; without one, each `train_query` row is a training
115+
window laid out by position as `context` input columns followed by the
116+
`horizon` continuation columns (or `horizon * nquant` quantile columns when
117+
`quantiles` is passed), and the column count must match exactly. Columns:
118+
`model_id`, `content_hash`, `train_rows`, `train_rmse`.
90119

91120
## Scalar utilities
92121

93122
| Function | Returns |
94123
| --- | --- |
95124
| `predict_version()` | the extension version string |
96-
| `predict_ulid([ts])` | a fresh ULID, optionally seeded with an epoch-ms timestamp |
125+
| `predict_ulid(ts)` | the smallest ULID for an ISO-8601 UTC timestamp: deterministic (the time component is set, the entropy bits are zero), useful as a range key |
97126
| `predict_debug()` | build info: profile, compiled features, available runtimes |
127+
| `predict_register(model_id, config)` | registers an external model in `_predict_models` and returns its content hash; `config` is a bare weights path or a JSON object (`runtime`, `kind`, `license`, `weights_uri`, `io_spec`, `target`). Registration is metadata only: executing an `onnx` model still needs the ONNX build |
98128

99129
See [Options](../options/) for the per-function option set and
100130
[Models](../models/) for the `model` values.

‎website/src/content/docs/reference/models.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ bundled model ships in the zero-dependency core, no ONNX runtime required.
1010

1111
| `model` | Kind | Notes |
1212
| --- | --- | --- |
13-
| `theta-classic` | statistical | The Theta method. Default (`default-ts` aliases to it). Strong, cheap trend + seasonality baseline. |
13+
| `auto` | meta | The default when no `model` is named. Rolling-origin backtests each candidate per series and forecasts with the lowest-error one. See [Auto-selection](../../guides/auto-and-conformal/). |
14+
| `theta-classic` | statistical | The Theta method (`default-ts` aliases to it). Strong, cheap trend + seasonality baseline. |
1415
| `stub-seasonal-naive` | statistical | Seasonal-naive with drift. The benchmark floor every forecasting paper reports against. |
1516
| `tsb` | statistical | Teunter-Syntetos-Babai, for intermittent demand (many zeros). Forecast-only. |
16-
| `auto` | meta | Rolling-origin backtests each candidate per series and forecasts with the lowest-error one. See [Auto-selection](../../guides/auto-and-conformal/). |
1717

1818
## Anomaly detection
1919

2020
| `model` | Kind | Notes |
2121
| --- | --- | --- |
22-
| `sub-pca` | statistical | Subsequence PCA (Jacobi eigensolver in C). Default detector; competitive with SOTA on the TSB-AD-U benchmark. |
22+
| `theta-classic` | statistical | The default detector: flags points that fall outside the one-step-ahead forecast interval. |
23+
| `stub-seasonal-naive` | statistical | The same residual detector over the seasonal-naive forecaster. |
24+
| `sub-pca` | statistical | Subsequence PCA (Jacobi eigensolver in C); competitive with SOTA on the TSB-AD-U benchmark. Pick it with `'{"model":"sub-pca"}'`; its rows carry no interval fields. |
2325

2426
## Tabular prediction
2527

‎website/src/content/docs/reference/options.md‎

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ description: Every option each function accepts, passed as a trailing JSON objec
55

66
Options are a single JSON object in the last argument. Unknown keys and
77
wrong-typed values are rejected with `PREDICT_ERR_OPTIONS`, so a typo fails loud
8-
rather than being ignored.
8+
rather than being ignored. `distill_forecast` is the one exception today: it
9+
reads only its known keys and ignores anything else, so check its spelling
10+
yourself.
911

1012
## forecast
1113

@@ -17,7 +19,7 @@ rather than being ignored.
1719
| `folds` | integer | 20 | Rolling origins used by `auto` / conformal. |
1820
| `gap` | integer | 0 | Leakage guard between train and target. |
1921
| `candidates` | string[] | stat models + eligible registered students | Narrows the `auto` pool. |
20-
| `context_limit` | integer | model default | Cap on points fed to the model. |
22+
| `context_limit` | integer | 4096 | Cap on points fed to the model (the most recent are kept; a capped series reports status `truncated`). |
2123

2224
Aggregate rules: the `options` argument and `horizon` must be the same value
2325
on every row of a group, and a `SELECT` string passed as the first argument is
@@ -27,7 +29,9 @@ columns and `GROUP BY` carries the series split.
2729

2830
## detect_anomalies
2931

30-
`model` and `context_limit` as above, plus:
32+
`model` and `context_limit` as above, except that `model` defaults to
33+
`theta-classic` (the residual detector) and `context_limit` defaults to 0:
34+
the detector scores the whole series unless you cap it. Plus:
3135

3236
| Key | Type | Default | Meaning |
3337
| --- | --- | --- | --- |
@@ -37,9 +41,11 @@ The same aggregate rules apply.
3741

3842
## backtest
3943

40-
`confidence_level`, `interval_method`, `folds`, `gap`, `context_limit`, and
41-
`model` as for `forecast`, plus the query-shape keys (backtest takes its data
42-
as a query, so columns must be resolvable):
44+
`confidence_level`, `interval_method`, `folds`, `gap`, and `context_limit` as
45+
for `forecast`. `model` differs: it must be a statistical model id or `auto`,
46+
and it defaults to `theta-classic`, not `auto`; a distilled student competes
47+
inside `auto` but cannot be pinned here. Plus the query-shape keys (backtest
48+
takes its data as a query, so columns must be resolvable):
4349

4450
| Key | Type | Default | Meaning |
4551
| --- | --- | --- | --- |
@@ -51,7 +57,7 @@ as a query, so columns must be resolvable):
5157

5258
| Key | Type | Default | Meaning |
5359
| --- | --- | --- | --- |
54-
| `target` | string | required | Column in `train_query` to learn. |
60+
| `target` | string | required for in-context models | Column in `train_query` to learn. A served student (`train_query` = `NULL`) needs no target. |
5561
| `task` | `classify` \| `regress` | inferred | Prediction task. |
5662
| `model` | string | `knn5-incontext` | Model or distilled student id. |
5763
| `device` | `cpu` \| `gpu` | `cpu` | Inference device (onnx build). |
@@ -60,22 +66,33 @@ as a query, so columns must be resolvable):
6066

6167
## distill_predict
6268

69+
`target` and `student_id` are required. Feature columns must be numeric:
70+
encode categorical text before distilling (the in-context `knn5-incontext`
71+
handles text features itself; the distiller does not).
72+
6373
| Key | Type | Meaning |
6474
| --- | --- | --- |
65-
| `target` | string | Target column. |
66-
| `task` | `classify` \| `regress` | Task. |
67-
| `student_id` | string | Id to register the student under. |
68-
| `student_kind` | `tree` \| `gbt` \| `mlp` | Student architecture. |
69-
| `teacher` | string | Teacher model (onnx build) for soft labels. |
70-
| `proba`, `classes` | | Soft-label distillation of a probability distribution. |
75+
| `target` | string | Target column. Required. |
76+
| `task` | `classify` \| `regress` | Task. Inferred when omitted. |
77+
| `student_id` | string | Id to register the student under. Required. |
78+
| `student_kind` | `tree` \| `gbt` \| `mlp` | Student architecture. Default `tree` (soft-label distillation implies `gbt`). |
79+
| `teacher` | string | A registered model that relabels the training rows first. |
80+
| `proba`, `classes` | string[] | Soft-label distillation: per-class probability columns and their class labels. |
7181

7282
## distill_forecast
7383

84+
`context`, `horizon`, and `student_id` are required. Without a `teacher`,
85+
each `train_query` row is one training window laid out by position:
86+
`context` input columns, then the `horizon` continuation columns (or
87+
`horizon * nquant` quantile columns when `quantiles` is passed); the column
88+
count must match exactly.
89+
7490
| Key | Type | Meaning |
7591
| --- | --- | --- |
76-
| `student_id` | string | Id to register the student under. |
77-
| `teacher` | string | Teacher forecast model (onnx build). |
78-
| `context` | integer | Input window length. |
79-
| `horizon` | integer | Forecast length the student is trained for. |
80-
| `hidden` | integer | Residual-net hidden width. |
81-
| `epochs`, `lr` | | Training epochs and learning rate. |
92+
| `student_id` | string | Id to register the student under. Required. |
93+
| `teacher` | string | Teacher forecast model (onnx build): `train_query` then yields `(series_key, value)` rows and the teacher labels sliding windows. |
94+
| `context` | integer | Input window length. Required; max 4096. |
95+
| `horizon` | integer | Forecast length the student is trained for. Required. |
96+
| `quantiles` | number[] | Quantile levels to distill from a teacher's quantile fan. Default: a point (median-only) student. |
97+
| `hidden` | integer | Residual-net hidden width, 0 to 2048 (0 = pure linear student). Default 256. |
98+
| `epochs`, `lr` | | Training epochs (default 1500) and learning rate (default 0.005). |

0 commit comments

Comments
 (0)