Skip to content

Phase 7g.6: Go SDK sqlrite.Ask() / AskRun() / AskConfig#68

Merged
joaoh82 merged 1 commit into
mainfrom
feat/go-sdk-ask
May 1, 2026
Merged

Phase 7g.6: Go SDK sqlrite.Ask() / AskRun() / AskConfig#68
joaoh82 merged 1 commit into
mainfrom
feat/go-sdk-ask

Conversation

@joaoh82
Copy link
Copy Markdown
Owner

@joaoh82 joaoh82 commented May 1, 2026

Summary

cgo wrapper for natural-language → SQL via the engine's sqlrite::ask module. Same three-layer config precedence as Python (7g.4) and Node (7g.5), idiomatic Go shape: free functions on *sql.DB plus context-aware variants.

import (
    "database/sql"
    sqlrite "github.com/joaoh82/rust_sqlite/sdk/go"
)

db, _ := sql.Open("sqlrite", "foo.sqlrite")

// Path 1: nil cfg → reads SQLRITE_LLM_API_KEY etc. from env
resp, err := sqlrite.Ask(db, "How many users are over 30?", nil)
fmt.Println(resp.SQL)            // "SELECT COUNT(*) FROM users WHERE age > 30"
fmt.Println(resp.Explanation)    // "Counts users older than thirty."

// Path 2: explicit per-call config
cfg := &sqlrite.AskConfig{
    APIKey:    "sk-ant-...",
    Model:     "claude-haiku-4-5",
    MaxTokens: 512,
    CacheTTL:  "1h",          // "5m" (default) | "1h" | "off"
}
resp, _ := sqlrite.Ask(db, "list users", cfg)

// Convenience: generate + execute
rows, _ := sqlrite.AskRun(db, "list users", nil)
defer rows.Close()

What's new

Surface Description
AskConfig struct Provider / APIKey / Model / MaxTokens / CacheTTL / BaseURL. JSON tags match the FFI's snake_case ABI.
AskConfigFromEnv() Reads SQLRITE_LLM_*. Errors on invalid MAX_TOKENS.
(*AskConfig).String() Debug repr that omits the API key value (shows <set> or <unset>).
Ask(db, q, *AskConfig) Generates SQL. Does NOT execute. Returns *AskResponse.
AskContext(ctx, db, q, *AskConfig) Context-aware variant — ctx flows to db.Conn(ctx).
AskRun(db, q, *AskConfig) Generates + executes; returns *sql.Rows. Errors on empty SQL response (model declined) with the model's explanation.
AskRunContext(ctx, db, q, *AskConfig) Context-aware variant — ctx flows through both Ask and Query.
AskResponse { SQL, Explanation, Usage } What Ask returns.
AskUsage { InputTokens, OutputTokens, CacheCreationInputTokens, CacheReadInputTokens } Token usage breakdown. Inspect CacheReadInputTokens to verify caching is working.

C ABI: one new FFI function

sqlrite_ask(conn, question, config_json, *out) accepts the AskConfig as a JSON string rather than 6+ separate FFI parameters. Smaller surface, more extensible (adding fields later doesn't break the C ABI for existing bindings).

The response is also JSON: {"sql":..., "explanation":..., "usage":{...}}. Caller frees with sqlrite_free_string.

build_ask_config(json) on the Rust side starts from AskConfig::from_env() and applies overrides — so a Go caller passing {"model": "claude-haiku-4-5"} still picks up SQLRITE_LLM_API_KEY from the environment without having to re-read it Go-side.

Plumbing through database/sql

The existing pattern is sql.Open("sqlrite", path) → *sql.DB. The underlying *conn (which holds the opaque *C.SqlriteConnection) is reachable via db.Conn(ctx).Raw(...). That's how Ask gets at the FFI handle — abstracted in ask.go's internal helper. Callers just see the natural Go API.

Tests — 11/11 new pass

  • AskConfig + AskConfigFromEnv (4): defaults, env overrides, invalid MAX_TOKENS, String() doesn't leak the API key.
  • Error paths (3): nil db, missing API key, closed db.
  • Happy path (3): Ask round-trip (SQL + explanation + usage + request body shape + auth headers verified), AskRun executes the generated SQL and returns rows, AskRun on empty SQL response surfaces the model's explanation.
  • API error surfacing (1): 400 with structured Anthropic error → Go error carrying status + inner type+message.

httptest.Server runs on a Go runtime goroutine, so unlike Python (GIL) and Node (event-loop), there's no deadlock concern with synchronous cgo calls. Cleanest test setup of the three SDKs.

Plus all 9 pre-existing Go SDK tests still pass.

Test plan

  • cargo fmt --all -- --check — clean
  • cargo build -p sqlrite-ffi --release — clean (regenerates sqlrite.h with new function)
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs — 301/301 pass (engine + sqlrite-ask unchanged; FFI extended)
  • cd sdk/go && go vet ./... — clean
  • cd sdk/go && go test — 11 new + 9 existing pass
  • CI: rust matrix, python-sdk, nodejs-sdk, go-sdk (linux/macos), wasm-build, desktop-build all green
  • Manual smoke: export SQLRITE_LLM_API_KEY=...cd sdk/go && go run -tags ./... against a small test program

Docs

  • sdk/go/README.md — "coming soon" preview replaced with full reference.
  • docs/phase-7-plan.md — 7g.6 marked ✅ with the JSON-config-over-cgo design note.
  • docs/roadmap.md — 7g bullet updated.

Next up after merge

  1. Merge → dispatch release-pr.yml at v0.1.23sdk/go/v0.1.23 tag goes live; go get github.com/joaoh82/rust_sqlite/sdk/go@v0.1.23 carries the new Ask/AskRun surface.
  2. 7g.7 — WASM SDK with JS-callback shape per Q9 (browser does prompt construction in-page; HTTP call goes through a JS function the caller supplies, so the API key stays in their backend). The doc requirement from Q9: a complete worked example in sdk/wasm/README.md showing browser → backend proxy → LLM provider → response back to WASM.
  3. 7g.8 — MCP ask tool (after 7h lays the MCP server framework).

🤖 Generated with Claude Code

cgo wrapper for natural-language → SQL via the engine's
`sqlrite::ask` module. Same three-layer config precedence as
Python (7g.4) and Node (7g.5), with idiomatic Go shape: free
functions on `*sql.DB` plus context-aware variants.

## Public surface

```go
import (
    "database/sql"
    sqlrite "github.com/joaoh82/rust_sqlite/sdk/go"
)

db, _ := sql.Open("sqlrite", "foo.sqlrite")

// Path 1: nil cfg → reads SQLRITE_LLM_API_KEY etc. from env
resp, err := sqlrite.Ask(db, "How many users are over 30?", nil)

// Path 2: explicit per-call config
cfg := &sqlrite.AskConfig{
    APIKey:    "sk-ant-...",
    Model:     "claude-haiku-4-5",
    MaxTokens: 512,
    CacheTTL:  "1h",          // "5m" (default) | "1h" | "off"
}
resp, _ := sqlrite.Ask(db, "list users", cfg)

// Convenience: generate + execute
rows, _ := sqlrite.AskRun(db, "list users", nil)
defer rows.Close()
for rows.Next() {
    var id int64; var name string
    rows.Scan(&id, &name)
}

// Context-aware variants for connection-pool acquisition control
resp, err := sqlrite.AskContext(ctx, db, "...", cfg)
rows, err := sqlrite.AskRunContext(ctx, db, "...", cfg)
```

## What's new

  * `sqlrite.AskConfig` struct — Provider / APIKey / Model /
    MaxTokens / CacheTTL / BaseURL. JSON tags match the FFI's
    snake_case ABI for direct serialization.
  * `sqlrite.AskConfigFromEnv()` — reads SQLRITE_LLM_*. Builds a
    config callers can inspect / log / mutate before passing to
    Ask. Returns error on invalid SQLRITE_LLM_MAX_TOKENS.
  * `(*AskConfig).String()` — debug-friendly repr that **omits the
    API key value** (shows `<set>` or `<unset>`). Lets callers
    `fmt.Println(cfg)` in logs without leaking the secret.
  * `sqlrite.Ask(db, question, *AskConfig) (*AskResponse, error)`
    — generates SQL, does NOT execute. Plus `AskContext(ctx, ...)`.
  * `sqlrite.AskRun(db, question, *AskConfig) (*sql.Rows, error)`
    — generates AND executes; returns rows for iteration. Plus
    `AskRunContext(ctx, ...)`. Throws on empty SQL response (model
    declined) with the model's explanation in the error.
  * `AskResponse { SQL, Explanation, Usage }` + `AskUsage { Input,
    Output, CacheCreation, CacheRead Tokens }`. The API key is
    never carried through here.

## C ABI: one new FFI function

`sqlrite_ask(conn, question, config_json, *out)` accepts the
AskConfig as a **JSON string** rather than 6+ separate FFI
parameters. Smaller surface, more extensible (adding fields later
doesn't break the C ABI for existing bindings) — matches how the
Python and Node SDKs ended up structuring config too.

The response is also JSON: `{"sql":..., "explanation":...,
"usage":{...}}`. Caller frees with `sqlrite_free_string`.

`build_ask_config(json)` on the Rust side starts from
`AskConfig::from_env()` and applies overrides — so a Go caller
passing `{"model": "claude-haiku-4-5"}` still picks up
`SQLRITE_LLM_API_KEY` from the environment without having to
re-read it Go-side.

## Plumbing through `database/sql`

The Go SDK's existing pattern is `sql.Open("sqlrite", path) →
*sql.DB`. The underlying `*conn` (which holds the opaque
`*C.SqlriteConnection`) is reachable via `db.Conn(ctx).Raw(...)`.
That's how `Ask` gets at the FFI handle — abstracted in `ask.go`'s
internal helper. Callers just see the natural Go API.

## Tests — 11 new tests pass

11 new tests in `sdk/go/ask_test.go`:

  * **AskConfig + AskConfigFromEnv (4)**: defaults, env overrides,
    invalid MAX_TOKENS, String() doesn't leak the API key.
  * **Error paths (3)**: nil db, missing API key, closed db.
  * **Happy path (3)**: Ask round-trip, AskRun executes the
    generated SQL and returns rows, AskRun on empty SQL response
    surfaces the model's explanation as an error.
  * **API error surfacing (1)**: 400 with structured Anthropic
    error → Go error carrying status + inner type+message.

`httptest.Server` runs on a Go runtime goroutine, so unlike the
Python (GIL) and Node (event-loop) SDKs there's no deadlock
concern with synchronous cgo calls. Test setup is the cleanest
of the three SDKs.

Plus all 9 pre-existing Go SDK tests still pass.

## Docs

  * `sdk/go/README.md` — "coming soon" preview replaced with full
    reference: 3-layer precedence, context-aware variants, errors,
    AskResponse type signatures, no-key-in-String guarantee.
  * `docs/phase-7-plan.md` — 7g.6 marked ✅ with the JSON-config-
    over-cgo design note and the test-setup-is-cleanest insight.
  * `docs/roadmap.md` — 7g bullet updated.

cargo fmt clean. cargo workspace tests 301/301 still green (FFI
gained 200 LOC; engine + sqlrite-ask unchanged). go vet clean.
go test passes (11 new + 9 existing). Generated `sqlrite.h`
header now declares `sqlrite_ask` for downstream C / FFI consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit aedf11e into main May 1, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant