Skip to main content

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
41use sqlrite_ask::{
42    AskConfig, AskError, AskResponse, Provider, ask_with_schema, ask_with_schema_and_provider,
43};
44
45use crate::Connection;
46use crate::sql::db::database::Database;
47
48pub mod schema;
49
50/// Extension trait adding `Connection::ask` to
51/// [`crate::Connection`]. Bring it into scope with
52/// `use sqlrite::ConnectionAskExt;` (the engine re-exports it at
53/// the crate root).
54pub trait ConnectionAskExt {
55    /// Generate SQL from a natural-language question.
56    ///
57    /// Internally: dump the schema, build the cache-friendly prompt,
58    /// POST to the configured LLM provider, parse the JSON-shaped
59    /// reply.
60    ///
61    /// ```no_run
62    /// use sqlrite::{Connection, ConnectionAskExt};
63    /// use sqlrite_ask::AskConfig;
64    ///
65    /// let conn = Connection::open("foo.sqlrite")?;
66    /// let cfg  = AskConfig::from_env()?;          // SQLRITE_LLM_API_KEY etc.
67    /// let resp = conn.ask("how many users are over 30?", &cfg)?;
68    /// println!("{}", resp.sql);
69    /// # Ok::<(), Box<dyn std::error::Error>>(())
70    /// ```
71    fn ask(&self, question: &str, config: &AskConfig) -> Result<AskResponse, AskError>;
72}
73
74impl ConnectionAskExt for Connection {
75    fn ask(&self, question: &str, config: &AskConfig) -> Result<AskResponse, AskError> {
76        ask(self, question, config)
77    }
78}
79
80/// Free-function form of [`ConnectionAskExt::ask`]. Equivalent —
81/// pick whichever shape reads better at the call site.
82pub fn ask(conn: &Connection, question: &str, config: &AskConfig) -> Result<AskResponse, AskError> {
83    ask_with_database(conn.database(), question, config)
84}
85
86/// Same as [`ask`], but takes the engine's `&Database` directly.
87///
88/// Used by the REPL binary's `.ask` meta-command, which holds a
89/// `&mut Database` rather than a `&Connection`.
90pub fn ask_with_database(
91    db: &Database,
92    question: &str,
93    config: &AskConfig,
94) -> Result<AskResponse, AskError> {
95    let schema_dump = schema::dump_schema_for_database(db);
96    ask_with_schema(&schema_dump, question, config)
97}
98
99/// Lower-level entry — same flow as [`ask`] but you supply the
100/// provider. For test harnesses + advanced callers driving custom
101/// backends.
102pub fn ask_with_provider<P: Provider>(
103    conn: &Connection,
104    question: &str,
105    config: &AskConfig,
106    provider: &P,
107) -> Result<AskResponse, AskError> {
108    ask_with_database_and_provider(conn.database(), question, config, provider)
109}
110
111/// Lower-level entry taking `&Database` and a provider. Canonical
112/// inner function — the others reduce to this one.
113pub fn ask_with_database_and_provider<P: Provider>(
114    db: &Database,
115    question: &str,
116    config: &AskConfig,
117    provider: &P,
118) -> Result<AskResponse, AskError> {
119    let schema_dump = schema::dump_schema_for_database(db);
120    ask_with_schema_and_provider(&schema_dump, question, config, provider)
121}