Skip to content

Commit f2011f1

Browse files
mstrathmanclaude
andcommitted
feat(bindings): node + rust packages, primary names unscoped
Add Node and Rust bindings alongside the Python one, and make the primary package name the generic `sqlite-predict` everywhere -- the discoverable, squattable name worth claiming -- with the everpure-scoped/prefixed mirrors reserved for brand (a thin follow-up, and @Everpure is already scope-protected). - bindings/node: `npm install sqlite-predict`, `require("sqlite-predict").load(db)` for better-sqlite3 and node:sqlite. Per-platform binary packages (`sqlite-predict-<platform>`) as optionalDependencies, the esbuild/sqlite-vec pattern, with a local fallback. Verified loading + forecasting via node:sqlite. - bindings/rust: crate `sqlite-predict`. build.rs compiles the amalgamation via the cc crate (self-contained, no bundled binary); `register()` installs it as a SQLite auto-extension. Verified end-to-end (`cargo test` registers + forecasts through rusqlite). - Python package renamed everpure -> unscoped `sqlite-predict` / `import sqlite_predict`. - CI: a `bindings` job runs the rust cargo test and a node:sqlite load+forecast, so neither binding can rot. wheels.yml gains the trusted-publishing publish job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9d71d2d commit f2011f1

17 files changed

Lines changed: 282 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@ jobs:
4040
- name: Generate + compile amalgamation
4141
run: make amalgamation-check
4242

43+
bindings:
44+
name: bindings (node + rust)
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v4
48+
# rust crate: compiles the amalgamation via build.rs, register + forecast
49+
- name: Rust crate
50+
run: make rust-src && cargo test --manifest-path bindings/rust/Cargo.toml
51+
- uses: actions/setup-node@v4
52+
with:
53+
node-version: "22"
54+
# node binding: load the extension and run a forecast through node:sqlite
55+
- name: Node binding
56+
run: |
57+
make loadable
58+
cp dist/predict0.so bindings/node/predict0.so
59+
node --experimental-sqlite -e '
60+
const sp = require("./bindings/node/index.cjs");
61+
const { DatabaseSync } = require("node:sqlite");
62+
const db = new DatabaseSync(":memory:", { allowExtension: true });
63+
sp.load(db);
64+
db.exec("CREATE TABLE r(ts TEXT, value REAL)");
65+
const ins = db.prepare("INSERT INTO r VALUES (?,?)");
66+
for (let i = 0; i < 96; i++)
67+
ins.run("2024-01-01T" + String(i % 24).padStart(2, "0") + ":00:00", 50 + 10 * Math.sin(i));
68+
const n = db.prepare("SELECT count(*) c FROM forecast(\x27SELECT ts,value FROM r ORDER BY ts\x27, 6)").get().c;
69+
if (n !== 6) { console.error("expected 6, got", n); process.exit(1); }
70+
console.log("node binding OK");
71+
'
72+
4373
sanitizers:
4474
name: ASan + UBSan
4575
runs-on: ubuntu-latest

.github/workflows/wheels.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
# package; cibuildwheel then compiles them per platform.
1212
#
1313
# Publishing uses PyPI Trusted Publishing (OIDC) -- no token in GitHub. ONE-TIME
14-
# setup on PyPI: create the `everpure-sqlite-predict` project's trusted publisher
14+
# setup on PyPI: create the `sqlite-predict` project's trusted publisher
1515
# pointing at this repo, workflow `wheels.yml`, and environment `pypi`
1616
# (https://docs.pypi.org/trusted-publishers/). The `publish` job only runs on a
1717
# v* tag; plain pushes / workflow_dispatch just build wheels.

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,13 @@ bindings/python/src/
3737
bindings/python/build/
3838
bindings/python/dist/
3939
bindings/python/*.egg-info/
40-
bindings/python/everpure/sqlite_predict/predict0.*
40+
bindings/python/sqlite_predict/predict0.*
41+
42+
# node binding (dev binary + deps)
43+
bindings/node/predict0.*
44+
bindings/node/node_modules/
45+
46+
# rust binding (staged C + build)
47+
bindings/rust/csrc/
48+
bindings/rust/target/
49+
bindings/rust/Cargo.lock

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ clean:
200200
format:
201201
clang-format -i sqlite-predict.c predict-*.c
202202

203-
.PHONY: loadable loadable-onnx loadable-onnx-gpu amalgamation amalgamation-check python-src \
203+
.PHONY: loadable loadable-onnx loadable-onnx-gpu amalgamation amalgamation-check python-src rust-src \
204204
debug test test-loadable test-onnx test-asan-onnx clean format
205205

206206
# stage the amalgamation + SQLite ext headers into the python package so it can
@@ -209,3 +209,10 @@ python-src: amalgamation vendor/sqlite3ext.h
209209
mkdir -p bindings/python/src
210210
cp $(AMALGAMATION) bindings/python/src/sqlite-predict.c
211211
cp vendor/sqlite3ext.h vendor/sqlite3.h bindings/python/src/
212+
213+
# stage the amalgamation + SQLite ext header into the rust crate (compiled by
214+
# its build.rs via the cc crate) so `cargo build`/`cargo publish` is self-contained
215+
rust-src: amalgamation vendor/sqlite3ext.h
216+
mkdir -p bindings/rust/csrc
217+
cp $(AMALGAMATION) bindings/rust/csrc/sqlite-predict.c
218+
cp vendor/sqlite3ext.h vendor/sqlite3.h bindings/rust/csrc/

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,11 @@ CPU. The default build never links onnxruntime at all.
205205
**Python** (the fastest way to try it):
206206

207207
```sh
208-
pip install everpure-sqlite-predict
208+
pip install sqlite-predict # also: npm install sqlite-predict, cargo add sqlite-predict
209209
```
210210

211211
```python
212-
import sqlite3
213-
from everpure import sqlite_predict
212+
import sqlite3, sqlite_predict
214213

215214
db = sqlite3.connect("app.db")
216215
sqlite_predict.load(db)

bindings/node/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# sqlite-predict
2+
3+
Prediction as a SQL primitive for SQLite: `forecast()`, `detect_anomalies()`,
4+
`predict()`, with replayable receipts. Bundles the zero-dependency loadable
5+
extension for your platform.
6+
7+
```js
8+
const Database = require("better-sqlite3");
9+
const sqlitePredict = require("sqlite-predict");
10+
11+
const db = new Database(":memory:");
12+
sqlitePredict.load(db);
13+
14+
for (const row of db
15+
.prepare("SELECT * FROM forecast('SELECT ts, value FROM readings', 24)")
16+
.all()) {
17+
console.log(row);
18+
}
19+
```
20+
21+
Also works with Node's built-in `node:sqlite`:
22+
23+
```js
24+
const { DatabaseSync } = require("node:sqlite");
25+
const sqlitePredict = require("sqlite-predict");
26+
const db = new DatabaseSync(":memory:", { allowExtension: true });
27+
db.loadExtension(sqlitePredict.getLoadablePath());
28+
```
29+
30+
See the [project README](https://github.com/PureStorage-OpenConnect/sqlite-predict)
31+
for the full SQL surface, models, and benchmarks.

bindings/node/index.cjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const fs = require("node:fs");
2+
const path = require("node:path");
3+
4+
// per-platform binary packages, installed as optionalDependencies so npm pulls
5+
// only the one matching the host (the esbuild / sqlite-vec pattern)
6+
const PLATFORM_PACKAGES = {
7+
"darwin-arm64": "sqlite-predict-darwin-arm64",
8+
"darwin-x64": "sqlite-predict-darwin-x64",
9+
"linux-x64": "sqlite-predict-linux-x64",
10+
"linux-arm64": "sqlite-predict-linux-arm64",
11+
"win32-x64": "sqlite-predict-windows-x64",
12+
};
13+
14+
/**
15+
* Absolute path to the loadable extension for this platform, without a file
16+
* suffix (the form SQLite's load_extension prefers).
17+
*/
18+
function getLoadablePath() {
19+
// dev / bundled fallback: a binary sitting next to this file
20+
for (const ext of ["dylib", "so", "dll"]) {
21+
const local = path.join(__dirname, `predict0.${ext}`);
22+
if (fs.existsSync(local)) return path.join(__dirname, "predict0");
23+
}
24+
const key = `${process.platform}-${process.arch}`;
25+
const pkg = PLATFORM_PACKAGES[key];
26+
if (!pkg) {
27+
throw new Error(
28+
`sqlite-predict: no prebuilt binary for ${key}; build from source`
29+
);
30+
}
31+
try {
32+
return require(pkg).loadablePath;
33+
} catch (_) {
34+
throw new Error(
35+
`sqlite-predict: the platform package ${pkg} is not installed`
36+
);
37+
}
38+
}
39+
40+
/**
41+
* Load sqlite-predict into a connection with a loadExtension method
42+
* (better-sqlite3, or node:sqlite's DatabaseSync with allowExtension: true).
43+
*/
44+
function load(db) {
45+
db.loadExtension(getLoadablePath());
46+
}
47+
48+
module.exports = { getLoadablePath, load };

bindings/node/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** Absolute path to the bundled loadable extension for this platform. */
2+
export function getLoadablePath(): string;
3+
4+
/**
5+
* Load sqlite-predict into a connection exposing `loadExtension`
6+
* (better-sqlite3, or node:sqlite's `DatabaseSync` with `allowExtension: true`).
7+
*/
8+
export function load(db: { loadExtension(path: string): void }): void;

bindings/node/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "sqlite-predict",
3+
"version": "0.0.1-alpha.1",
4+
"description": "Prediction as a SQL primitive for SQLite: forecast, detect_anomalies, predict, with replayable receipts.",
5+
"main": "index.cjs",
6+
"types": "index.d.ts",
7+
"files": [
8+
"index.cjs",
9+
"index.d.ts",
10+
"README.md"
11+
],
12+
"keywords": [
13+
"sqlite",
14+
"forecasting",
15+
"anomaly-detection",
16+
"time-series",
17+
"machine-learning",
18+
"better-sqlite3"
19+
],
20+
"license": "MIT OR Apache-2.0",
21+
"repository": {
22+
"type": "git",
23+
"url": "https://github.com/PureStorage-OpenConnect/sqlite-predict.git",
24+
"directory": "bindings/node"
25+
},
26+
"optionalDependencies": {
27+
"sqlite-predict-darwin-arm64": "0.0.1-alpha.1",
28+
"sqlite-predict-darwin-x64": "0.0.1-alpha.1",
29+
"sqlite-predict-linux-x64": "0.0.1-alpha.1",
30+
"sqlite-predict-linux-arm64": "0.0.1-alpha.1",
31+
"sqlite-predict-windows-x64": "0.0.1-alpha.1"
32+
}
33+
}

bindings/python/pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["setuptools>=64"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "everpure-sqlite-predict"
6+
name = "sqlite-predict"
77
version = "0.0.1a1"
88
description = "Prediction as a SQL primitive for SQLite: forecast, detect_anomalies, predict, with replayable receipts."
99
readme = "README.md"
@@ -23,9 +23,8 @@ classifiers = [
2323
Homepage = "https://github.com/PureStorage-OpenConnect/sqlite-predict"
2424
Source = "https://github.com/PureStorage-OpenConnect/sqlite-predict"
2525

26-
[tool.setuptools.packages.find]
27-
namespaces = true
28-
include = ["everpure*"]
26+
[tool.setuptools]
27+
packages = ["sqlite_predict"]
2928

3029
[tool.setuptools.package-data]
31-
"everpure.sqlite_predict" = ["predict0.*"]
30+
sqlite_predict = ["predict0.*"]

0 commit comments

Comments
 (0)