• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

joaoh82 / rust_sqlite / 25626413047

10 May 2026 10:31AM UTC coverage: 66.053% (+0.2%) from 65.897%
25626413047

push

github

web-flow
feat(engine): Phase 11.1 multi-connection foundation (SQLR-22) (#122)

First slice of Phase 11 (concurrent writes via MVCC + BEGIN CONCURRENT).
Refactors Connection from owning Database by value to holding
Arc<Mutex<Database>>, so multiple Connection handles can address the
same engine state from inside one process.

Connection::connect() mints a sibling handle that shares the backing
Database. Connection is now Send + Sync and can be moved across
threads without an outer Mutex<Connection>. The pager's existing
process-level flock and per-database mutex still serialize commits;
true multi-writer throughput on disjoint rows lands with
BEGIN CONCURRENT in 11.4.

Connection::database() / database_mut() return MutexGuard<'_, Database>;
internal call sites in src/ask, sqlrite-mcp tools, and the
Python/Node/WASM SDK shims bind the guard to a local first.

Six new tests in src/connection.rs:
- connect_shares_underlying_database
- connect_shares_file_backed_database
- handle_count_reflects_live_handles
- threaded_writers_serialize_cleanly (8 threads × 25 INSERTs)
- prep_cache_is_per_handle
- connection_is_send_and_sync (compile-time Send+Sync check)

Docs sweep: roadmap gets a Phase 11 section linking to
concurrent-writes-plan.md (the plan-doc internally numbers
sub-phases as "Phase 10.x" — that's its working title from before
Phase 10 = benchmarks shipped). New design-decisions entry 12a.
embedding.md gains a "Sharing one database across threads" section.

No file-format change. 579/579 workspace tests pass. No new clippy
warnings. fmt clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

91 of 110 new or added lines in 6 files covered. (82.73%)

1 existing line in 1 file now uncovered.

9468 of 14334 relevant lines covered (66.05%)

1.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/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).
45
pub mod schema;
46

47
#[cfg(feature = "ask")]
48
use sqlrite_ask::{ask_with_schema, ask_with_schema_and_provider};
49

50
#[cfg(feature = "ask")]
51
use crate::Connection;
52
#[cfg(feature = "ask")]
53
use 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")]
63
pub 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")]
76
pub 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")]
97
impl 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")]
106
pub fn ask(conn: &Connection, question: &str, config: &AskConfig) -> Result<AskResponse, AskError> {
×
NEW
107
    let db = conn.database();
×
NEW
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")]
116
pub 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")]
129
pub 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")]
142
pub 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
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc