sqlrite/ask/mod.rs
1//! Engine-side glue for the [`sqlrite-ask`](https://crates.io/crates/sqlrite-ask)
2//! crate — natural-language → SQL.
3//!
4//! Compiled only when the `ask` feature is enabled (default-on for
5//! the CLI binary, off for the WASM SDK and any
6//! `default-features = false` library embedding).
7//!
8//! ## Why this lives in the engine, not in `sqlrite-ask`
9//!
10//! Earlier (v0.1.18) `sqlrite-ask` itself owned the `Connection`
11//! integration — it imported `sqlrite-engine` and exposed
12//! `ConnectionAskExt`. That worked for library callers, but when
13//! the engine's REPL binary tried to depend on `sqlrite-ask` to
14//! wire up the `.ask` meta-command we hit a hard cargo error:
15//!
16//! ```text
17//! cyclic package dependency: package `sqlrite-ask` depends on itself.
18//! Cycle: sqlrite-ask → sqlrite-engine → sqlrite-ask
19//! ```
20//!
21//! Optional / feature-gated deps don't escape this — cargo's static
22//! cycle detection counts every potential edge in the graph. The
23//! structural fix was to flip the dep direction: keep `sqlrite-ask`
24//! pure (operates on `&str` schemas), put the engine integration
25//! here. The dep flow is now one-way: `sqlrite-engine[ask]` →
26//! `sqlrite-ask`. No cycle.
27//!
28//! ## What's here
29//!
30//! - [`schema::dump_schema_for_database`] — walks `Database.tables`
31//! alphabetically, emits `CREATE TABLE … (…);` text the LLM grounds
32//! on. Determinism matters for prompt caching.
33//! - [`ConnectionAskExt`] — extension trait adding `Connection::ask`
34//! that handles schema introspection + `sqlrite_ask::ask_with_schema`
35//! in one call.
36//! - Free functions [`ask`] / [`ask_with_database`] /
37//! [`ask_with_provider`] / [`ask_with_database_and_provider`] —
38//! for callers who don't want to bring the trait into scope, or
39//! who hold a `&Database` directly (the REPL binary does this).
40
41// Schema dump is always available (no sqlrite-ask dep). The
42// `ConnectionAskExt` trait + free helper functions below are gated
43// under the engine's `ask` feature because they pull in `sqlrite-ask`
44// (HTTP transport).
45pub mod schema;
46
47#[cfg(feature = "ask")]
48use sqlrite_ask::{ask_with_schema, ask_with_schema_and_provider};
49
50#[cfg(feature = "ask")]
51use crate::Connection;
52#[cfg(feature = "ask")]
53use crate::sql::db::database::Database;
54
55// Re-export the public surface from sqlrite-ask. Lets callers reach
56// these without listing `sqlrite-ask` as a direct dep — convenient
57// for the Tauri desktop app, the SDK adapters, and any Rust embedder
58// who already pulls the engine in. They can keep saying
59// `use sqlrite::ask::AskConfig` instead of dragging the second crate
60// in just for one type. Gated under `ask` because that's the feature
61// that pulls `sqlrite-ask` into the dep graph in the first place.
62#[cfg(feature = "ask")]
63pub use sqlrite_ask::{
64 AnthropicProvider, AskConfig, AskError, AskResponse, CacheTtl, Provider, ProviderKind, Request,
65 Response, Usage,
66};
67
68/// Extension trait adding `Connection::ask` to
69/// [`crate::Connection`]. Bring it into scope with
70/// `use sqlrite::ConnectionAskExt;` (the engine re-exports it at
71/// the crate root).
72///
73/// Gated under the `ask` feature — pulls in `sqlrite-ask` and its
74/// HTTP transport. WASM and other lean builds skip this entirely.
75#[cfg(feature = "ask")]
76pub trait ConnectionAskExt {
77 /// Generate SQL from a natural-language question.
78 ///
79 /// Internally: dump the schema, build the cache-friendly prompt,
80 /// POST to the configured LLM provider, parse the JSON-shaped
81 /// reply.
82 ///
83 /// ```no_run
84 /// use sqlrite::{Connection, ConnectionAskExt};
85 /// use sqlrite_ask::AskConfig;
86 ///
87 /// let conn = Connection::open("foo.sqlrite")?;
88 /// let cfg = AskConfig::from_env()?; // SQLRITE_LLM_API_KEY etc.
89 /// let resp = conn.ask("how many users are over 30?", &cfg)?;
90 /// println!("{}", resp.sql);
91 /// # Ok::<(), Box<dyn std::error::Error>>(())
92 /// ```
93 fn ask(&self, question: &str, config: &AskConfig) -> Result<AskResponse, AskError>;
94}
95
96#[cfg(feature = "ask")]
97impl ConnectionAskExt for Connection {
98 fn ask(&self, question: &str, config: &AskConfig) -> Result<AskResponse, AskError> {
99 ask(self, question, config)
100 }
101}
102
103/// Free-function form of [`ConnectionAskExt::ask`]. Equivalent —
104/// pick whichever shape reads better at the call site.
105#[cfg(feature = "ask")]
106pub fn ask(conn: &Connection, question: &str, config: &AskConfig) -> Result<AskResponse, AskError> {
107 let db = conn.database();
108 ask_with_database(&db, question, config)
109}
110
111/// Same as [`ask`], but takes the engine's `&Database` directly.
112///
113/// Used by the REPL binary's `.ask` meta-command, which holds a
114/// `&mut Database` rather than a `&Connection`.
115#[cfg(feature = "ask")]
116pub fn ask_with_database(
117 db: &Database,
118 question: &str,
119 config: &AskConfig,
120) -> Result<AskResponse, AskError> {
121 let schema_dump = schema::dump_schema_for_database(db);
122 ask_with_schema(&schema_dump, question, config)
123}
124
125/// Lower-level entry — same flow as [`ask`] but you supply the
126/// provider. For test harnesses + advanced callers driving custom
127/// backends.
128#[cfg(feature = "ask")]
129pub fn ask_with_provider<P: Provider>(
130 conn: &Connection,
131 question: &str,
132 config: &AskConfig,
133 provider: &P,
134) -> Result<AskResponse, AskError> {
135 let db = conn.database();
136 ask_with_database_and_provider(&db, question, config, provider)
137}
138
139/// Lower-level entry taking `&Database` and a provider. Canonical
140/// inner function — the others reduce to this one.
141#[cfg(feature = "ask")]
142pub fn ask_with_database_and_provider<P: Provider>(
143 db: &Database,
144 question: &str,
145 config: &AskConfig,
146 provider: &P,
147) -> Result<AskResponse, AskError> {
148 let schema_dump = schema::dump_schema_for_database(db);
149 ask_with_schema_and_provider(&schema_dump, question, config, provider)
150}