sqlrite 0.1.21__tar.gz → 0.1.23__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {sqlrite-0.1.21 → sqlrite-0.1.23}/Cargo.lock +8 -6
  2. {sqlrite-0.1.21 → sqlrite-0.1.23}/Cargo.toml +1 -1
  3. {sqlrite-0.1.21 → sqlrite-0.1.23}/PKG-INFO +1 -1
  4. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/package.json +1 -1
  5. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/phase-7-plan.md +2 -2
  6. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/roadmap.md +1 -1
  7. {sqlrite-0.1.21 → sqlrite-0.1.23}/pyproject.toml +1 -1
  8. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/README.md +77 -10
  9. sqlrite-0.1.23/sdk/go/ask.go +293 -0
  10. sqlrite-0.1.23/sdk/go/ask_test.go +396 -0
  11. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/python/Cargo.toml +1 -1
  12. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/Cargo.toml +1 -1
  13. {sqlrite-0.1.21 → sqlrite-0.1.23}/.github/workflows/ci.yml +0 -0
  14. {sqlrite-0.1.21 → sqlrite-0.1.23}/.github/workflows/release-pr.yml +0 -0
  15. {sqlrite-0.1.21 → sqlrite-0.1.23}/.github/workflows/release.yml +0 -0
  16. {sqlrite-0.1.21 → sqlrite-0.1.23}/.github/workflows/rust.yml +0 -0
  17. {sqlrite-0.1.21 → sqlrite-0.1.23}/.gitignore +0 -0
  18. {sqlrite-0.1.21 → sqlrite-0.1.23}/CODE_OF_CONDUCT.md +0 -0
  19. {sqlrite-0.1.21 → sqlrite-0.1.23}/LICENSE +0 -0
  20. {sqlrite-0.1.21 → sqlrite-0.1.23}/MAINTAINERS +0 -0
  21. {sqlrite-0.1.21 → sqlrite-0.1.23}/Makefile +0 -0
  22. {sqlrite-0.1.21 → sqlrite-0.1.23}/README.md +0 -0
  23. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/index.html +0 -0
  24. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/package-lock.json +0 -0
  25. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/src/App.svelte +0 -0
  26. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/src/app.css +0 -0
  27. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/src/main.ts +0 -0
  28. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/src/vite-env.d.ts +0 -0
  29. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/svelte.config.js +0 -0
  30. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/tsconfig.json +0 -0
  31. {sqlrite-0.1.21 → sqlrite-0.1.23}/desktop/vite.config.ts +0 -0
  32. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/_index.md +0 -0
  33. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/architecture.md +0 -0
  34. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/design-decisions.md +0 -0
  35. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/desktop.md +0 -0
  36. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/embedding.md +0 -0
  37. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/file-format.md +0 -0
  38. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/getting-started.md +0 -0
  39. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/pager.md +0 -0
  40. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/release-plan.md +0 -0
  41. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/release-secrets.md +0 -0
  42. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/smoke-test.md +0 -0
  43. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/sql-engine.md +0 -0
  44. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/storage-model.md +0 -0
  45. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/supported-sql.md +0 -0
  46. {sqlrite-0.1.21 → sqlrite-0.1.23}/docs/usage.md +0 -0
  47. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/README.md +0 -0
  48. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/c/Makefile +0 -0
  49. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/c/hello.c +0 -0
  50. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/go/go.mod +0 -0
  51. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/go/hello.go +0 -0
  52. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/nodejs/hello.mjs +0 -0
  53. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/python/hello.py +0 -0
  54. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/rust/quickstart.rs +0 -0
  55. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/wasm/Makefile +0 -0
  56. {sqlrite-0.1.21 → sqlrite-0.1.23}/examples/wasm/index.html +0 -0
  57. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite - Desktop.png +0 -0
  58. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite Data Structures.png +0 -0
  59. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite Simple SQL Execution High Level Diagram.png +0 -0
  60. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite Simple SQL INSERT Execution High Level Diagram (Insert Row).png +0 -0
  61. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite Simple SQL INSERT Execution High Level Diagram.png +0 -0
  62. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/SQLRite_logo.png +0 -0
  63. {sqlrite-0.1.21 → sqlrite-0.1.23}/images/architecture.png +0 -0
  64. {sqlrite-0.1.21 → sqlrite-0.1.23}/rust-toolchain.toml +0 -0
  65. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/AST.delete.example +0 -0
  66. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/AST.insert.exemple +0 -0
  67. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/AST.select.example +0 -0
  68. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/AST.update.example +0 -0
  69. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/CREATE TABLE sqlrite_schema.sql +0 -0
  70. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/CREATE_TABLE with duplicate.sql +0 -0
  71. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/CREATE_TABLE.sql +0 -0
  72. {sqlrite-0.1.21 → sqlrite-0.1.23}/samples/INSERT.sql +0 -0
  73. {sqlrite-0.1.21 → sqlrite-0.1.23}/scripts/bump-version.sh +0 -0
  74. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/conn.go +0 -0
  75. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/go.mod +0 -0
  76. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/rows.go +0 -0
  77. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/sqlrite.go +0 -0
  78. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/sqlrite_test.go +0 -0
  79. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/go/stmt.go +0 -0
  80. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/python/README.md +0 -0
  81. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/python/src/lib.rs +0 -0
  82. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/python/tests/test_ask.py +0 -0
  83. {sqlrite-0.1.21 → sqlrite-0.1.23}/sdk/python/tests/test_sqlrite.py +0 -0
  84. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/src/lib.rs +0 -0
  85. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/src/prompt.rs +0 -0
  86. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/src/provider/anthropic.rs +0 -0
  87. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/src/provider/mock.rs +0 -0
  88. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/src/provider/mod.rs +0 -0
  89. {sqlrite-0.1.21 → sqlrite-0.1.23}/sqlrite-ask/tests/anthropic_http.rs +0 -0
  90. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/ask/mod.rs +0 -0
  91. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/ask/schema.rs +0 -0
  92. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/connection.rs +0 -0
  93. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/error.rs +0 -0
  94. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/lib.rs +0 -0
  95. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/main.rs +0 -0
  96. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/meta_command/mod.rs +0 -0
  97. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/repl/mod.rs +0 -0
  98. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/db/database.rs +0 -0
  99. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/db/mod.rs +0 -0
  100. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/db/secondary_index.rs +0 -0
  101. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/db/table.rs +0 -0
  102. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/executor.rs +0 -0
  103. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/hnsw.rs +0 -0
  104. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/mod.rs +0 -0
  105. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/cell.rs +0 -0
  106. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/file.rs +0 -0
  107. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/header.rs +0 -0
  108. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/hnsw_cell.rs +0 -0
  109. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/index_cell.rs +0 -0
  110. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/interior_page.rs +0 -0
  111. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/mod.rs +0 -0
  112. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/overflow.rs +0 -0
  113. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/page.rs +0 -0
  114. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/pager.rs +0 -0
  115. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/table_page.rs +0 -0
  116. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/varint.rs +0 -0
  117. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/pager/wal.rs +0 -0
  118. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/parser/create.rs +0 -0
  119. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/parser/insert.rs +0 -0
  120. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/parser/mod.rs +0 -0
  121. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/parser/select.rs +0 -0
  122. {sqlrite-0.1.21 → sqlrite-0.1.23}/src/sql/tokenizer.rs +0 -0
@@ -3804,7 +3804,7 @@ dependencies = [
3804
3804
 
3805
3805
  [[package]]
3806
3806
  name = "sqlrite-ask"
3807
- version = "0.1.21"
3807
+ version = "0.1.23"
3808
3808
  dependencies = [
3809
3809
  "serde",
3810
3810
  "serde_json",
@@ -3815,7 +3815,7 @@ dependencies = [
3815
3815
 
3816
3816
  [[package]]
3817
3817
  name = "sqlrite-desktop"
3818
- version = "0.1.21"
3818
+ version = "0.1.23"
3819
3819
  dependencies = [
3820
3820
  "serde",
3821
3821
  "serde_json",
@@ -3827,7 +3827,7 @@ dependencies = [
3827
3827
 
3828
3828
  [[package]]
3829
3829
  name = "sqlrite-engine"
3830
- version = "0.1.21"
3830
+ version = "0.1.23"
3831
3831
  dependencies = [
3832
3832
  "clap",
3833
3833
  "env_logger",
@@ -3844,15 +3844,17 @@ dependencies = [
3844
3844
 
3845
3845
  [[package]]
3846
3846
  name = "sqlrite-ffi"
3847
- version = "0.1.21"
3847
+ version = "0.1.23"
3848
3848
  dependencies = [
3849
3849
  "cbindgen",
3850
+ "serde",
3851
+ "serde_json",
3850
3852
  "sqlrite-engine",
3851
3853
  ]
3852
3854
 
3853
3855
  [[package]]
3854
3856
  name = "sqlrite-nodejs"
3855
- version = "0.1.21"
3857
+ version = "0.1.23"
3856
3858
  dependencies = [
3857
3859
  "napi",
3858
3860
  "napi-build",
@@ -3862,7 +3864,7 @@ dependencies = [
3862
3864
 
3863
3865
  [[package]]
3864
3866
  name = "sqlrite-python"
3865
- version = "0.1.21"
3867
+ version = "0.1.23"
3866
3868
  dependencies = [
3867
3869
  "pyo3",
3868
3870
  "sqlrite-engine",
@@ -27,7 +27,7 @@ resolver = "3"
27
27
  # `package =` key so the import name stays `sqlrite` internally:
28
28
  # sqlrite = { package = "sqlrite-engine", path = "…" }
29
29
  name = "sqlrite-engine"
30
- version = "0.1.21"
30
+ version = "0.1.23"
31
31
  authors = ["Joao Henrique Machado Silva <joaoh82@gmail.com>"]
32
32
  edition = "2024"
33
33
  rust-version = "1.85"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlrite
3
- Version: 0.1.21
3
+ Version: 0.1.23
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sqlrite-desktop-frontend",
3
3
  "private": true,
4
- "version": "0.1.21",
4
+ "version": "0.1.23",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -253,8 +253,8 @@ let rows = conn.execute(&resp.sql)?;
253
253
  - **✅ 7g.2 — REPL `.ask` + structural refactor.** New `MetaCommand::Ask(String)` variant + `handle_ask` that calls `sqlrite::ask::ask_with_database`, prints the generated SQL + rationale, prompts `Run? [Y/n] ` via rustyline (Ctrl-C / EOF map to skip — paranoid default for LLM-generated SQL), and pipes through `process_command` if confirmed. The thin REPL part itself is ~120 LOC. **Required a bigger structural fix that 7g.1 didn't anticipate:** wiring the binary to call into `sqlrite-ask` created a cargo cycle (`sqlrite-engine[bin] → sqlrite-ask → sqlrite-engine[lib]`) that even `optional = true` doesn't break — cargo's static cycle detection includes all potential edges. Resolution: flipped the dep direction. `sqlrite-ask` is now pure (no engine dep, canonical API takes a `&str` schema dump + `&str` question). Schema introspection + the `Connection`/`Database` integration (`ConnectionAskExt`, `ask`, `ask_with_database`, `ask_with_provider`, `ask_with_database_and_provider`) moved into `sqlrite-engine` itself under a new `ask` feature (default-on for the CLI binary, off for `--no-default-features` / WASM). Public API for end users is unchanged in spirit: `use sqlrite::{Connection, ConnectionAskExt}` instead of `use sqlrite_ask::ConnectionAskExt`. Net effect — `sqlrite-ask` shrunk + got easier to test in isolation; the engine carries the integration weight, scoped behind a feature flag so SDK / WASM / Tauri builds opt in. 30+ tests pass on both sides (sqlrite-ask: 20 unit + 4 integration; engine ask module: 6 schema + 3 .ask parser).
254
254
  - **✅ 7g.3 — Desktop UI "Ask…" button.** New "Ask…" button in the editor toolbar plus a slide-in composer panel above the editor surface (question textarea + "Generate SQL" button + explanation slot). Submitting calls a new `ask_sql` Tauri command that reads `AskConfig::from_env()` server-side, locks the engine's `Database`, calls `sqlrite::ask::ask_with_database` (so the schema dump + LLM HTTP call stay in the Rust backend — the API key never crosses into the webview). Generated SQL drops into the editor textarea for the user to review + Run themselves; the rationale shows in the panel; an empty SQL response (model declined) surfaces the model's explanation in the same slot. Cmd/Ctrl+Enter in the question textarea submits; Esc closes. ~110 LOC of Svelte + ~30 LOC of Rust + ~85 LOC of CSS. As a side benefit, ergonomic re-exports added on `sqlrite::ask::*` (`AskConfig`, `AskResponse`, `AskError`, `Provider`, etc.) so library callers don't have to add `sqlrite-ask` as a direct dep alongside the engine.
255
255
  - **✅ 7g.4 — Python SDK `conn.ask` / `ask_run` / `AskConfig`.** PyO3 wrappers over `sqlrite::ask::*`. New `Connection.ask(question, config=None)` returns an `AskResponse` (`.sql` / `.explanation` / `.usage` with cache-hit fields). `Connection.ask_run(question, config=None)` generates SQL then immediately executes via cursor — convenience for one-shot scripts/notebooks. `Connection.set_ask_config(cfg)` stashes a per-connection config. `AskConfig(api_key=..., model=..., max_tokens=..., cache_ttl=..., base_url=...)` constructor + `AskConfig.from_env()` static method. Three precedence layers: per-call > per-connection > env > defaults. `AskConfig.__repr__` and `AskResponse.__repr__` deliberately omit the API key — printing config in logs won't leak it. Empty SQL response (model declined) on `ask_run()` raises `SQLRiteError` with the model's explanation rather than executing the empty string. ~370 LOC of Rust binding + 415 LOC of pytest tests across 20 cases (config construction, env-var parsing, error paths, full happy-path through a localhost HTTP mock built on Python's stdlib `http.server`). README rewritten — three precedence layers explained, defaults enumerated, errors documented.
256
- - **7g.5 — Node.js SDK (~80 LOC).** Same shape via napi-rs. `db.ask(question)` / `db.askRun(question)`.
257
- - **7g.6 — Go SDK (~80 LOC).** cgo wrapper. `sqlrite.Ask(db, question)` returns `(AskResponse, error)`.
256
+ - **✅ 7g.5 — Node.js SDK `db.ask` / `askRun` / `AskConfig`.** napi-rs wrappers over `sqlrite::ask::*`. `db.ask(question, config?)` returns `AskResponse { sql, explanation, usage }`. `db.askRun(question, config?)` generates SQL then immediately executes — returns rows directly (`Array<Object>`); throws on empty-SQL response (model declined) with the model's explanation, rather than executing the empty string. `db.setAskConfig(config)` stashes per-connection config (pass `null` to clear). `new AskConfig({apiKey, model, maxTokens, cacheTtl, baseUrl})` constructor + `AskConfig.fromEnv()` static. Three precedence layers: per-call > per-connection > env > defaults — same shape as the Python SDK, with idiomatic JS option-object instead of kwargs (camelCase: `maxTokens`/`cacheTtl`/`baseUrl`). `AskConfig.toString()` deliberately omits the API key value (shows `<set>` or `null`). Auto-generated TypeScript types in `index.d.ts` — `AskConfigOptions`, `AskResponse`, `AskUsage` interfaces. ~370 LOC of Rust binding + 380 LOC of node:test cases (30 total — 19 new for ask plus the original 11). Mock HTTP server runs in a `worker_thread` so napi-rs's blocking sync POST on the main thread doesn't deadlock the response — same insight as Python's GIL deadlock, different mitigation. README rewritten — three-layer precedence, defaults table, error behavior, what AskResponse carries, the no-key-in-toString guarantee, full TypeScript shapes.
257
+ - **✅ 7g.6 — Go SDK `sqlrite.Ask` / `AskRun` / `AskConfig`.** cgo wrapper. New FFI function `sqlrite_ask(conn, question, config_json, *out)` accepts the AskConfig as a JSON string (smaller, more extensible C ABI than 6+ separate parameters) and returns `{sql, explanation, usage}` as JSON. Go side: `Ask(db *sql.DB, question, *AskConfig) (*AskResponse, error)` plus `AskContext(ctx, ...)` for context-aware connection-pool acquisition, `AskRun(db, question, *AskConfig) (*sql.Rows, error)` (and `AskRunContext`) that generates and executes in one call. `AskConfigFromEnv()` reads `SQLRITE_LLM_*`. `AskConfig.String()` deliberately omits the API key value (shows `<set>` or `<unset>`). Plumbs through `db.Conn(ctx).Raw()` to reach the underlying `*conn`'s opaque `*C.SqlriteConnection` handle. ~310 LOC of Go + ~200 LOC of Rust FFI extension + 380 LOC of Go tests (11 cases — config defaults / from-env / overrides / invalid-max-tokens, error paths for nil-db / missing-key / closed-db, full happy-path through `httptest.Server`, AskRun execution, empty-SQL declined-model, 4xx error surfacing). `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 — Go test setup is the cleanest of the three. README rewritten — three-layer precedence, context-aware variants documented, errors enumerated, AskResponse type signatures shown.
258
258
  - **7g.7 — WASM SDK (~150 LOC, see Q9).** Either skipped, or implemented with a JS-side fetch hook (the WASM binary calls back into JS to make the HTTP request, since `reqwest`'s wasm32 story is messy and CORS/keys are a separate problem).
259
259
  - **7g.8 — MCP server `ask` tool (~50 LOC).** Wires the existing tool framework from 7h to a single new tool that calls into `sqlrite-ask`.
260
260
 
@@ -476,7 +476,7 @@ Approved sub-phases (Q1–Q10 resolved):
476
476
  - **✅ 7d — HNSW ANN index** — three PRs: 7d.1 (algorithm w/ recall@10 ≥ 0.95), 7d.2 (SQL integration + query optimizer), 7d.3 (persistence + DELETE/UPDATE rebuild). `CREATE INDEX … USING hnsw (col)`; fixed defaults `M=16, ef_construction=200, ef_search=50` (Q2). New `KIND_HNSW` cell tag.
477
477
  - **✅ 7e — JSON column type + path queries** — `JSON` data type stored as canonical text (validated via `serde_json::from_str` at INSERT/UPDATE time; SQLite-JSON1-style — Q3 scope correction since bincode was removed in Phase 3c). Functions: `json_extract` / `json_type` / `json_array_length` / `json_object_keys`. Path subset supports `$`, `.key`, `[N]`, chained. `json_object_keys` returns a JSON-array text rather than a table-valued result (no set-returning functions in the executor yet).
478
478
  - **7f — ~~Full-text search with BM25~~** — **deferred to Phase 8** (Q1).
479
- - **7g — `ask()` API across the product surface** — natural-language → SQL via Anthropic API (Q4), Anthropic-first then OpenAI + Ollama follow-ups. Foundational **✅ 7g.1** introduces a new `sqlrite-ask` crate (Q10 — separate crate, not a feature flag) — `ask_with_schema()` over `&str` inputs (Phase 7g.2 made it pure — see retrospective below), sync `ureq` POST to `/v1/messages`, schema-aware prompt with prompt-caching on the schema dump (Sonnet 4.6 default; configurable). **✅ 7g.2** wires the REPL's `.ask` meta-command (`MetaCommand::Ask(String)` + confirm-and-run UX) and adds the `sqlrite::ask` module on the engine side (gated under a new `ask` feature) carrying `ConnectionAskExt` + the schema introspection helper. **✅ 7g.3** adds the desktop "Ask…" composer (slide-in panel above the editor; Tauri command runs the LLM call in the Rust backend so the API key stays out of the webview). **✅ 7g.4** ships the Python SDK surface — `conn.ask(question, config=None)` returns an `AskResponse(.sql, .explanation, .usage)`; `conn.ask_run()` adds the one-shot generate-and-execute convenience; `AskConfig` carries the three-layer precedence (per-call > per-connection > env > defaults). Per-product adapters in 7g.5-7g.8 cover Node.js, Go, WASM (JS-callback shape per Q9), and the MCP `ask` tool — and fold in the SDK README catch-up for VECTOR / JSON / HNSW capabilities along the way.
479
+ - **7g — `ask()` API across the product surface** — natural-language → SQL via Anthropic API (Q4), Anthropic-first then OpenAI + Ollama follow-ups. Foundational **✅ 7g.1** introduces a new `sqlrite-ask` crate (Q10 — separate crate, not a feature flag) — `ask_with_schema()` over `&str` inputs (Phase 7g.2 made it pure — see retrospective below), sync `ureq` POST to `/v1/messages`, schema-aware prompt with prompt-caching on the schema dump (Sonnet 4.6 default; configurable). **✅ 7g.2** wires the REPL's `.ask` meta-command (`MetaCommand::Ask(String)` + confirm-and-run UX) and adds the `sqlrite::ask` module on the engine side (gated under a new `ask` feature) carrying `ConnectionAskExt` + the schema introspection helper. **✅ 7g.3** adds the desktop "Ask…" composer (slide-in panel above the editor; Tauri command runs the LLM call in the Rust backend so the API key stays out of the webview). **✅ 7g.4** ships the Python SDK surface — `conn.ask(question, config=None)` returns an `AskResponse(.sql, .explanation, .usage)`; `conn.ask_run()` adds the one-shot generate-and-execute convenience; `AskConfig` carries the three-layer precedence (per-call > per-connection > env > defaults). **✅ 7g.5** ships the Node.js SDK surface — `db.ask(question, config?)`, `db.askRun(question, config?)`, `db.setAskConfig(cfg)`, `new AskConfig({apiKey, model, maxTokens, cacheTtl, baseUrl})` + `AskConfig.fromEnv()`. Same three-layer precedence; idiomatic JS camelCase option-object. **✅ 7g.6** ships the Go SDK surface via cgo — `sqlrite.Ask(db, q, *AskConfig)` / `AskRun(...)` plus `AskContext`/`AskRunContext` for context-aware variants. The FFI grew one new C function (`sqlrite_ask`) that takes the config as a JSON string and returns the response as JSON — smaller, more extensible ABI than plumbing 6+ struct fields across cgo. Per-product adapters in 7g.7-7g.8 cover WASM (JS-callback shape per Q9) and the MCP `ask` tool — and fold in the SDK README catch-up for VECTOR / JSON / HNSW capabilities along the way.
480
480
  - **7h — MCP server adapter** — new `sqlrite-mcp` binary, hand-rolled JSON-RPC + tool framework (Q5).
481
481
 
482
482
  Total scope budget: ~3-4 kLOC of new Rust across the wave. Each sub-phase ships as its own PR + release wave through the Phase 6 pipeline. The Phase 7 wave will likely close out **v0.2.0** (first minor bump after the 0.1.x Phase 6 cycle). Two new product lines added to lockstep versioning: `sqlrite-ask` and `sqlrite-mcp`.
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "sqlrite"
7
- version = "0.1.21"
7
+ version = "0.1.23"
8
8
  description = "Python bindings for SQLRite — a small, embeddable SQLite clone written in Rust."
9
9
  authors = [{ name = "Joao Henrique Machado Silva", email = "joaoh82@gmail.com" }]
10
10
  license = { text = "MIT" }
@@ -123,23 +123,90 @@ fmt.Println(name) // alice
123
123
 
124
124
  > `json_object_keys` returns a JSON-array text rather than a table-valued result (set-returning functions aren't supported yet).
125
125
 
126
- ### Natural-language → SQL (Phase 7g — *coming soon*)
126
+ ### Natural-language → SQL (Phase 7g.6)
127
127
 
128
- The Phase 7g.2-7g.8 wave adds `sqlrite.Ask(db, ...)` / `sqlrite.AskRun(db, ...)` that wrap the new [`sqlrite-ask`](https://crates.io/crates/sqlrite-ask) Rust crate through cgo. Today it's available only from Rust; the Go wrapper lands in 7g.6 and will look like:
128
+ `sqlrite.Ask(db, question, *AskConfig)` generates SQL via the configured LLM provider (Anthropic by default). `sqlrite.AskRun(db, question, *AskConfig)` is the convenience that calls `Ask` then immediately executes returns `*sql.Rows` ready for iteration.
129
129
 
130
130
  ```go
131
- // 7g.6 preview — not yet released
132
- import "github.com/joaoh82/rust_sqlite/sdk/go"
131
+ import (
132
+ "database/sql"
133
+ sqlrite "github.com/joaoh82/rust_sqlite/sdk/go"
134
+ )
133
135
 
134
- cfg := sqlrite.AskConfigFromEnv() // SQLRITE_LLM_API_KEY etc.
135
- resp, err := sqlrite.Ask(db, "How many users are over 30?", cfg)
136
- fmt.Println(resp.SQL) // "SELECT COUNT(*) FROM users WHERE age > 30"
137
- fmt.Println(resp.Explanation) // "Counts users over the age threshold."
136
+ db, _ := sql.Open("sqlrite", "foo.sqlrite")
137
+ db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)`)
138
+ db.Exec(`INSERT INTO users (name, age) VALUES ('alice', 30)`)
139
+
140
+ // Path 1: nil cfg → reads SQLRITE_LLM_API_KEY etc. from env.
141
+ resp, err := sqlrite.Ask(db, "How many users are over 30?", nil)
142
+ fmt.Println(resp.SQL) // "SELECT COUNT(*) FROM users WHERE age > 30"
143
+ fmt.Println(resp.Explanation) // "Counts users older than thirty."
144
+
145
+ // Path 2: explicit per-call config.
146
+ cfg := &sqlrite.AskConfig{
147
+ APIKey: "sk-ant-...",
148
+ Model: "claude-haiku-4-5",
149
+ MaxTokens: 512,
150
+ CacheTTL: "1h", // "5m" (default) | "1h" | "off"
151
+ }
152
+ resp, _ := sqlrite.Ask(db, "list users over 30", cfg)
153
+
154
+ // Caller decides whether to run the SQL — Ask does NOT auto-execute.
155
+ rows, _ := db.Query(resp.SQL)
156
+ defer rows.Close()
138
157
 
139
- // Convenience:
140
- rows, _ := sqlrite.AskRun(db, "How many users are over 30?", cfg)
158
+ // Or one-shot:
159
+ rows, _ := sqlrite.AskRun(db, "list users", nil)
141
160
  ```
142
161
 
162
+ #### Configuration
163
+
164
+ Three precedence layers — explicit-wins:
165
+
166
+ 1. **Per-call config**: `sqlrite.Ask(db, "...", &sqlrite.AskConfig{APIKey: "..."})`
167
+ 2. **Environment vars**: `SQLRITE_LLM_PROVIDER` / `_API_KEY` / `_MODEL` / `_MAX_TOKENS` / `_CACHE_TTL` — picked up automatically when cfg is nil
168
+ 3. **Built-in defaults**: `anthropic` / `claude-sonnet-4-6` / `1024` / `5m`
169
+
170
+ The config flows across cgo as a JSON string (`"api_key"`, `"model"`, etc. — snake_case, matching the FFI ABI). Adding fields later is non-breaking; older bindings ignore unknown JSON keys.
171
+
172
+ #### Context-aware variants
173
+
174
+ ```go
175
+ // Pass a context for connection-pool acquisition. The HTTP call to
176
+ // the LLM is currently uncancellable (the FFI doesn't expose a
177
+ // cancel hook yet); the ctx flows to db.Conn(ctx) and to the
178
+ // db.QueryContext call inside AskRunContext.
179
+ resp, err := sqlrite.AskContext(ctx, db, "list users", cfg)
180
+ rows, err := sqlrite.AskRunContext(ctx, db, "list users", cfg)
181
+ ```
182
+
183
+ #### Defaults
184
+
185
+ `Provider="anthropic"`, `Model="claude-sonnet-4-6"`, `MaxTokens=1024`, `CacheTTL="5m"`. The schema dump goes inside an Anthropic prompt-cache breakpoint — repeat asks against the same DB hit the cache (verify via `resp.Usage.CacheReadInputTokens`).
186
+
187
+ #### Errors
188
+
189
+ Missing API key → `error` with message `"sqlrite: ask: missing API key (set SQLRITE_LLM_API_KEY ...)"`. API errors (4xx/5xx) include the status code + Anthropic's structured error type+message. `AskRun()` on an empty SQL response (model declined) returns an error rather than executing the empty string.
190
+
191
+ #### What `AskResponse` carries
192
+
193
+ ```go
194
+ type AskResponse struct {
195
+ SQL string
196
+ Explanation string
197
+ Usage AskUsage
198
+ }
199
+
200
+ type AskUsage struct {
201
+ InputTokens uint64
202
+ OutputTokens uint64
203
+ CacheCreationInputTokens uint64
204
+ CacheReadInputTokens uint64
205
+ }
206
+ ```
207
+
208
+ `AskConfig.String()` deliberately omits the API key value — `fmt.Println(cfg)` shows `apiKey=<set>` or `apiKey=<unset>` without leaking the secret. There's no separate `cfg.HasAPIKey()` method because Go's zero-value semantics make `cfg.APIKey != ""` the idiomatic check.
209
+
143
210
  ## API surface
144
211
 
145
212
  | Symbol | Purpose |
@@ -0,0 +1,293 @@
1
+ // Phase 7g.6 — natural-language → SQL surface for the Go SDK.
2
+ //
3
+ // Wraps the C FFI `sqlrite_ask` function with idiomatic Go types:
4
+ //
5
+ // import (
6
+ // "database/sql"
7
+ // sqlrite "github.com/joaoh82/rust_sqlite/sdk/go"
8
+ // )
9
+ //
10
+ // db, _ := sql.Open("sqlrite", "foo.sqlrite")
11
+ // resp, err := sqlrite.Ask(db, "How many users are over 30?", nil)
12
+ // fmt.Println(resp.SQL) // "SELECT COUNT(*) FROM users WHERE age > 30"
13
+ // fmt.Println(resp.Explanation) // one-sentence rationale
14
+ //
15
+ // Three precedence layers for the API key (matching the Python and
16
+ // Node SDKs):
17
+ //
18
+ // 1. Per-call AskConfig (highest — pass via the `cfg` arg)
19
+ // 2. AskConfigFromEnv() (zero-config fallback)
20
+ // 3. Built-in defaults (anthropic / claude-sonnet-4-6 / 1024 / 5m)
21
+ //
22
+ // The cgo bridge passes the config to C as a JSON string and receives
23
+ // the response as a JSON string, parsed on the Go side. That keeps
24
+ // the C ABI tiny (one function) and lets us add fields later without
25
+ // breaking the bindings.
26
+
27
+ package sqlrite
28
+
29
+ /*
30
+ #include <stdlib.h>
31
+ #include "sqlrite.h"
32
+ */
33
+ import "C"
34
+
35
+ import (
36
+ "context"
37
+ "database/sql"
38
+ "database/sql/driver"
39
+ "encoding/json"
40
+ "errors"
41
+ "fmt"
42
+ "os"
43
+ "strconv"
44
+ "strings"
45
+ "unsafe"
46
+ )
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // AskConfig
50
+
51
+ // AskConfig configures one or more `Ask` / `AskRun` calls. All fields
52
+ // are optional; missing fields fall back to env vars and then to
53
+ // built-in defaults.
54
+ //
55
+ // Field tags map to the JSON keys the C FFI expects, so encoding the
56
+ // struct with `encoding/json` and handing it across the cgo boundary
57
+ // is a one-liner.
58
+ type AskConfig struct {
59
+ Provider string `json:"provider,omitempty"`
60
+ APIKey string `json:"api_key,omitempty"`
61
+ Model string `json:"model,omitempty"`
62
+ MaxTokens uint32 `json:"max_tokens,omitempty"`
63
+ // CacheTTL accepts "5m" (default), "1h", or "off". Aliases like
64
+ // "5min" / "1hr" / "none" / "disabled" are also recognized — the
65
+ // C side does the canonicalization.
66
+ CacheTTL string `json:"cache_ttl,omitempty"`
67
+ // BaseURL overrides the API endpoint. Production callers leave
68
+ // empty; tests point this at a localhost mock.
69
+ BaseURL string `json:"base_url,omitempty"`
70
+ }
71
+
72
+ // AskConfigFromEnv reads SQLRITE_LLM_* env vars and builds an
73
+ // AskConfig. Mirrors the Rust `AskConfig::from_env()` shape so the
74
+ // Go side has the same zero-config experience as every other SDK.
75
+ //
76
+ // Recognized vars:
77
+ // - SQLRITE_LLM_PROVIDER (default: anthropic)
78
+ // - SQLRITE_LLM_API_KEY
79
+ // - SQLRITE_LLM_MODEL (default: claude-sonnet-4-6)
80
+ // - SQLRITE_LLM_MAX_TOKENS (default: 1024)
81
+ // - SQLRITE_LLM_CACHE_TTL (default: 5m)
82
+ //
83
+ // A missing API key is NOT an error here — the call to Ask() raises
84
+ // the friendlier "missing API key" message later.
85
+ //
86
+ // (Note: the C side ALSO reads env vars during ask(), so passing a
87
+ // zero-value `&AskConfig{}` to Ask() works too. AskConfigFromEnv is
88
+ // useful when callers want to inspect / log / mutate the resolved
89
+ // config before sending it.)
90
+ func AskConfigFromEnv() (*AskConfig, error) {
91
+ cfg := &AskConfig{
92
+ Provider: envOrDefault("SQLRITE_LLM_PROVIDER", "anthropic"),
93
+ APIKey: os.Getenv("SQLRITE_LLM_API_KEY"),
94
+ Model: envOrDefault("SQLRITE_LLM_MODEL", "claude-sonnet-4-6"),
95
+ CacheTTL: envOrDefault("SQLRITE_LLM_CACHE_TTL", "5m"),
96
+ }
97
+ if v := os.Getenv("SQLRITE_LLM_MAX_TOKENS"); v != "" {
98
+ n, err := strconv.ParseUint(v, 10, 32)
99
+ if err != nil {
100
+ return nil, fmt.Errorf("sqlrite: SQLRITE_LLM_MAX_TOKENS not a u32: %s", v)
101
+ }
102
+ cfg.MaxTokens = uint32(n)
103
+ } else {
104
+ cfg.MaxTokens = 1024
105
+ }
106
+ return cfg, nil
107
+ }
108
+
109
+ // String returns a human-readable representation that **deliberately
110
+ // omits the API key value**. Lets callers `fmt.Println(cfg)` in
111
+ // debug output without leaking the secret. Shows `apiKey=<set>` or
112
+ // `apiKey=<unset>` so callers can tell whether a key is configured.
113
+ func (c *AskConfig) String() string {
114
+ keyStatus := "<unset>"
115
+ if c.APIKey != "" {
116
+ keyStatus = "<set>"
117
+ }
118
+ return fmt.Sprintf(
119
+ "AskConfig(provider=%q, model=%q, maxTokens=%d, cacheTtl=%q, apiKey=%s)",
120
+ c.Provider, c.Model, c.MaxTokens, c.CacheTTL, keyStatus,
121
+ )
122
+ }
123
+
124
+ func envOrDefault(key, dflt string) string {
125
+ if v := os.Getenv(key); v != "" {
126
+ return v
127
+ }
128
+ return dflt
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // AskResponse
133
+
134
+ // AskResponse is what `Ask` returns. Carries the generated SQL, the
135
+ // model's one-sentence rationale, and token usage.
136
+ //
137
+ // **The API key is not in here** — by design.
138
+ type AskResponse struct {
139
+ SQL string `json:"sql"`
140
+ Explanation string `json:"explanation"`
141
+ Usage AskUsage `json:"usage"`
142
+ }
143
+
144
+ // AskUsage is the token-usage breakdown from an `Ask` call. Inspect
145
+ // `CacheReadInputTokens` to verify prompt-caching is actually
146
+ // working — if it stays zero across repeated calls with the same
147
+ // schema, something in the prefix is invalidating the cache.
148
+ type AskUsage struct {
149
+ InputTokens uint64 `json:"input_tokens"`
150
+ OutputTokens uint64 `json:"output_tokens"`
151
+ CacheCreationInputTokens uint64 `json:"cache_creation_input_tokens"`
152
+ CacheReadInputTokens uint64 `json:"cache_read_input_tokens"`
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Public API: Ask + AskContext + AskRun + AskRunContext
157
+
158
+ // Ask generates SQL from a natural-language question via the
159
+ // configured LLM provider. Returns an `AskResponse` with the
160
+ // generated SQL, rationale, and token usage. Does **not** execute
161
+ // the SQL — call `db.Query(resp.SQL)` (or use `AskRun` for one-shot).
162
+ //
163
+ // `cfg` may be nil to use env vars + defaults. Pass an explicit
164
+ // `*AskConfig` to override per-call.
165
+ //
166
+ // Equivalent to `AskContext(context.Background(), db, question, cfg)`.
167
+ func Ask(db *sql.DB, question string, cfg *AskConfig) (*AskResponse, error) {
168
+ return AskContext(context.Background(), db, question, cfg)
169
+ }
170
+
171
+ // AskContext is the context-aware form of `Ask`. The context is used
172
+ // for `db.Conn(ctx)` (acquiring a connection from the pool); the
173
+ // LLM HTTP call inside the C library is currently uncancellable —
174
+ // it'll run to completion / error regardless of the context. A
175
+ // future SDK rev can plumb cancellation through `sqlrite_ask` once
176
+ // the FFI grows a cancel hook.
177
+ func AskContext(ctx context.Context, db *sql.DB, question string, cfg *AskConfig) (*AskResponse, error) {
178
+ if db == nil {
179
+ return nil, errors.New("sqlrite: Ask: db is nil")
180
+ }
181
+ dbConn, err := db.Conn(ctx)
182
+ if err != nil {
183
+ return nil, fmt.Errorf("sqlrite: Ask: %w", err)
184
+ }
185
+ defer dbConn.Close()
186
+
187
+ var resp *AskResponse
188
+ err = dbConn.Raw(func(driverConn any) error {
189
+ c, ok := driverConn.(*conn)
190
+ if !ok {
191
+ return fmt.Errorf("sqlrite: Ask: driver connection is %T, not *sqlrite.conn — Ask requires a sqlrite-backed *sql.DB", driverConn)
192
+ }
193
+ r, err := c.ask(question, cfg)
194
+ if err != nil {
195
+ return err
196
+ }
197
+ resp = r
198
+ return nil
199
+ })
200
+ return resp, err
201
+ }
202
+
203
+ // AskRun generates SQL **and executes it as a query**. Returns
204
+ // `*sql.Rows` ready for iteration (matching `db.Query`).
205
+ //
206
+ // `cfg` may be nil to use env vars + defaults.
207
+ //
208
+ // **Returns an error on empty SQL response** (model declined to
209
+ // generate SQL for this schema) rather than executing the empty
210
+ // string — the model's explanation is in the error message.
211
+ //
212
+ // Convenience for one-shot scripts. For interactive use, prefer
213
+ // `Ask` + manual review (the model can be wrong; auto-execute hides
214
+ // that).
215
+ //
216
+ // Equivalent to `AskRunContext(context.Background(), db, question, cfg)`.
217
+ func AskRun(db *sql.DB, question string, cfg *AskConfig) (*sql.Rows, error) {
218
+ return AskRunContext(context.Background(), db, question, cfg)
219
+ }
220
+
221
+ // AskRunContext is the context-aware form of `AskRun`. The context
222
+ // flows through to both the `Ask` call and the subsequent `Query`.
223
+ func AskRunContext(ctx context.Context, db *sql.DB, question string, cfg *AskConfig) (*sql.Rows, error) {
224
+ resp, err := AskContext(ctx, db, question, cfg)
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ trimmed := strings.TrimSpace(resp.SQL)
229
+ if trimmed == "" {
230
+ expl := resp.Explanation
231
+ if expl == "" {
232
+ expl = "(no explanation)"
233
+ }
234
+ return nil, fmt.Errorf("sqlrite: AskRun: model declined to generate SQL: %s", expl)
235
+ }
236
+ return db.QueryContext(ctx, trimmed)
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // Internal: ask on the conn
241
+
242
+ // ask runs the C FFI `sqlrite_ask` call. Holds the conn mutex for
243
+ // the duration of the call (which includes the synchronous HTTP
244
+ // round-trip to the LLM, ~hundreds of ms typical, capped at ~90s by
245
+ // ureq). Other goroutines using the same connection wait — same
246
+ // lock discipline as `exec` / `query`.
247
+ func (c *conn) ask(question string, cfg *AskConfig) (*AskResponse, error) {
248
+ c.mu.Lock()
249
+ defer c.mu.Unlock()
250
+ if c.closed {
251
+ return nil, driver.ErrBadConn
252
+ }
253
+
254
+ // Marshal the config (or empty string if cfg is nil).
255
+ var configJSON string
256
+ if cfg != nil {
257
+ raw, err := json.Marshal(cfg)
258
+ if err != nil {
259
+ return nil, fmt.Errorf("sqlrite: ask: marshal config: %w", err)
260
+ }
261
+ configJSON = string(raw)
262
+ }
263
+
264
+ cQuestion := cString(question)
265
+ defer freeCString(cQuestion)
266
+ var cConfig *C.char
267
+ if configJSON != "" {
268
+ cConfig = cString(configJSON)
269
+ defer freeCString(cConfig)
270
+ }
271
+
272
+ var out *C.char
273
+ status := Status(C.sqlrite_ask(c.handle, cQuestion, cConfig, &out))
274
+ if err := wrapErr(status, "ask"); err != nil {
275
+ return nil, err
276
+ }
277
+ if out == nil {
278
+ return nil, errors.New("sqlrite: ask: FFI returned status=ok but null response")
279
+ }
280
+ defer C.sqlrite_free_string(out)
281
+
282
+ jsonStr := C.GoString(out)
283
+ var resp AskResponse
284
+ if err := json.Unmarshal([]byte(jsonStr), &resp); err != nil {
285
+ return nil, fmt.Errorf("sqlrite: ask: parse response JSON: %w (raw=%q)", err, jsonStr)
286
+ }
287
+ return &resp, nil
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Compile-time check that `unsafe` is imported (cgo emits unused-import
292
+ // warnings otherwise).
293
+ var _ = unsafe.Sizeof(C.int(0))