Skip to main content

sqlmodel/
connection_session.rs

1//! Connection session management for SQLModel Rust.
2//!
3//! This module provides a small "connection + optional console" wrapper plus a builder.
4//!
5//! Important: this is **not** the SQLAlchemy/SQLModel ORM Session (unit of work / identity map).
6//! The ORM-style session lives in `sqlmodel::session` and is re-exported from the
7//! `sqlmodel-session` crate.
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use sqlmodel::prelude::*;
13//!
14//! // Basic connection session without console
15//! let session = ConnectionSession::builder().build_with(connection);
16//!
17//! // Connection session with auto-detected console
18//! #[cfg(feature = "console")]
19//! let session = ConnectionSession::builder()
20//!     .with_auto_console()
21//!     .build_with(connection);
22//! ```
23
24#[cfg(feature = "console")]
25use std::sync::Arc;
26
27#[cfg(feature = "console")]
28use sqlmodel_console::{ConsoleAware, SqlModelConsole};
29
30#[cfg(feature = "console")]
31use crate::global_console::global_console;
32
33use sqlmodel_core::Connection;
34
35/// A database session that combines connection management with optional console output.
36///
37/// This type is a lightweight wrapper around a `Connection` and optional console.
38/// For ORM-style behavior (identity map, unit of work, lazy loading), use
39/// `sqlmodel::Session` (from `sqlmodel-session`).
40#[derive(Debug)]
41pub struct ConnectionSession<C: Connection> {
42    /// The underlying connection
43    connection: C,
44    /// Optional console for rich output
45    #[cfg(feature = "console")]
46    console: Option<Arc<SqlModelConsole>>,
47}
48
49impl<C: Connection> ConnectionSession<C> {
50    /// Create a new session with a connection.
51    pub fn new(connection: C) -> Self {
52        Self {
53            connection,
54            #[cfg(feature = "console")]
55            console: None,
56        }
57    }
58
59    /// Create a session builder.
60    #[must_use]
61    pub fn builder() -> ConnectionSessionBuilder<C> {
62        ConnectionSessionBuilder::new()
63    }
64
65    /// Get a reference to the underlying connection.
66    #[must_use]
67    pub fn connection(&self) -> &C {
68        &self.connection
69    }
70
71    /// Get a mutable reference to the underlying connection.
72    pub fn connection_mut(&mut self) -> &mut C {
73        &mut self.connection
74    }
75
76    /// Consume the session and return the underlying connection.
77    pub fn into_connection(self) -> C {
78        self.connection
79    }
80}
81
82#[cfg(feature = "console")]
83impl<C: Connection> ConsoleAware for ConnectionSession<C> {
84    fn set_console(&mut self, console: Option<Arc<SqlModelConsole>>) {
85        self.console = console;
86    }
87
88    fn console(&self) -> Option<&Arc<SqlModelConsole>> {
89        self.console.as_ref()
90    }
91
92    fn has_console(&self) -> bool {
93        self.console.is_some()
94    }
95}
96
97/// Builder for creating ConnectionSession instances with fluent API.
98///
99/// # Example
100///
101/// ```rust,ignore
102/// let session = ConnectionSession::builder()
103///     .with_auto_console()  // Only available with "console" feature
104///     .build_with(connection);
105/// ```
106#[derive(Debug, Default)]
107pub struct ConnectionSessionBuilder<C: Connection> {
108    #[cfg(feature = "console")]
109    console: Option<Arc<SqlModelConsole>>,
110    #[cfg(not(feature = "console"))]
111    _marker: std::marker::PhantomData<C>,
112    #[cfg(feature = "console")]
113    _marker: std::marker::PhantomData<C>,
114}
115
116impl<C: Connection> ConnectionSessionBuilder<C> {
117    /// Create a new session builder.
118    #[must_use]
119    pub fn new() -> Self {
120        Self {
121            #[cfg(feature = "console")]
122            console: None,
123            _marker: std::marker::PhantomData,
124        }
125    }
126
127    /// Attach a console for rich output.
128    ///
129    /// The console will be used to emit progress information, query results,
130    /// and error messages in a visually rich format.
131    #[cfg(feature = "console")]
132    #[must_use]
133    pub fn with_console(mut self, console: SqlModelConsole) -> Self {
134        self.console = Some(Arc::new(console));
135        self
136    }
137
138    /// Attach a shared console for rich output.
139    ///
140    /// Use this when multiple sessions should share the same console
141    /// (e.g., for coordinated output or shared theme).
142    #[cfg(feature = "console")]
143    #[must_use]
144    pub fn with_shared_console(mut self, console: Arc<SqlModelConsole>) -> Self {
145        self.console = Some(console);
146        self
147    }
148
149    /// Use auto-detected output mode for the console.
150    ///
151    /// This creates a console that automatically detects whether
152    /// the terminal supports rich output or should fall back to plain text.
153    /// Uses `SqlModelConsole::new()` which performs environment detection.
154    #[cfg(feature = "console")]
155    #[must_use]
156    pub fn with_auto_console(mut self) -> Self {
157        self.console = Some(Arc::new(SqlModelConsole::new()));
158        self
159    }
160
161    /// Build the session with the provided connection.
162    ///
163    /// Console selection follows these priorities (highest first):
164    /// 1. Explicit console set via `with_console()` or similar
165    /// 2. Global console (if set via `set_global_console()` or `init_auto_console()`)
166    /// 3. No console (silent operation)
167    #[allow(unused_mut)] // mut only used with console feature
168    pub fn build_with(self, connection: C) -> ConnectionSession<C> {
169        let mut session = ConnectionSession::new(connection);
170
171        #[cfg(feature = "console")]
172        {
173            // Use explicit console if set, otherwise fall back to global console
174            session.console = self.console.or_else(global_console);
175        }
176
177        session
178    }
179}
180
181/// Builder for creating connections with console support.
182///
183/// This trait extends connection factories to support console integration.
184/// Implement this for driver-specific connection builders.
185#[cfg(feature = "console")]
186pub trait ConnectionBuilderExt {
187    /// The connection type produced by this builder.
188    type Connection: Connection + ConsoleAware;
189
190    /// Attach a console for rich output.
191    fn with_console(self, console: SqlModelConsole) -> Self;
192
193    /// Attach a shared console for rich output.
194    fn with_shared_console(self, console: Arc<SqlModelConsole>) -> Self;
195
196    /// Use auto-detected output mode for the console.
197    fn with_auto_console(self) -> Self;
198}
199
200#[cfg(test)]
201#[allow(clippy::manual_async_fn)] // Mock trait impls must match trait signatures
202mod tests {
203    use super::*;
204
205    // Mock connection for testing
206    #[derive(Debug)]
207    struct MockConnection;
208
209    impl sqlmodel_core::Connection for MockConnection {
210        type Tx<'conn>
211            = MockTransaction
212        where
213            Self: 'conn;
214
215        fn query(
216            &self,
217            _cx: &asupersync::Cx,
218            _sql: &str,
219            _params: &[sqlmodel_core::Value],
220        ) -> impl std::future::Future<
221            Output = asupersync::Outcome<Vec<sqlmodel_core::Row>, sqlmodel_core::Error>,
222        > + Send {
223            async { asupersync::Outcome::Ok(vec![]) }
224        }
225
226        fn query_one(
227            &self,
228            _cx: &asupersync::Cx,
229            _sql: &str,
230            _params: &[sqlmodel_core::Value],
231        ) -> impl std::future::Future<
232            Output = asupersync::Outcome<Option<sqlmodel_core::Row>, sqlmodel_core::Error>,
233        > + Send {
234            async { asupersync::Outcome::Ok(None) }
235        }
236
237        fn execute(
238            &self,
239            _cx: &asupersync::Cx,
240            _sql: &str,
241            _params: &[sqlmodel_core::Value],
242        ) -> impl std::future::Future<Output = asupersync::Outcome<u64, sqlmodel_core::Error>> + Send
243        {
244            async { asupersync::Outcome::Ok(0) }
245        }
246
247        fn insert(
248            &self,
249            _cx: &asupersync::Cx,
250            _sql: &str,
251            _params: &[sqlmodel_core::Value],
252        ) -> impl std::future::Future<Output = asupersync::Outcome<i64, sqlmodel_core::Error>> + Send
253        {
254            async { asupersync::Outcome::Ok(0) }
255        }
256
257        fn batch(
258            &self,
259            _cx: &asupersync::Cx,
260            _statements: &[(String, Vec<sqlmodel_core::Value>)],
261        ) -> impl std::future::Future<Output = asupersync::Outcome<Vec<u64>, sqlmodel_core::Error>> + Send
262        {
263            async { asupersync::Outcome::Ok(vec![]) }
264        }
265
266        fn begin(
267            &self,
268            _cx: &asupersync::Cx,
269        ) -> impl std::future::Future<
270            Output = asupersync::Outcome<Self::Tx<'_>, sqlmodel_core::Error>,
271        > + Send {
272            async { asupersync::Outcome::Ok(MockTransaction) }
273        }
274
275        fn begin_with(
276            &self,
277            _cx: &asupersync::Cx,
278            _isolation: sqlmodel_core::connection::IsolationLevel,
279        ) -> impl std::future::Future<
280            Output = asupersync::Outcome<Self::Tx<'_>, sqlmodel_core::Error>,
281        > + Send {
282            async { asupersync::Outcome::Ok(MockTransaction) }
283        }
284
285        fn prepare(
286            &self,
287            _cx: &asupersync::Cx,
288            _sql: &str,
289        ) -> impl std::future::Future<
290            Output = asupersync::Outcome<
291                sqlmodel_core::connection::PreparedStatement,
292                sqlmodel_core::Error,
293            >,
294        > + Send {
295            async {
296                asupersync::Outcome::Ok(sqlmodel_core::connection::PreparedStatement::new(
297                    0,
298                    String::new(),
299                    0,
300                ))
301            }
302        }
303
304        fn query_prepared(
305            &self,
306            _cx: &asupersync::Cx,
307            _stmt: &sqlmodel_core::connection::PreparedStatement,
308            _params: &[sqlmodel_core::Value],
309        ) -> impl std::future::Future<
310            Output = asupersync::Outcome<Vec<sqlmodel_core::Row>, sqlmodel_core::Error>,
311        > + Send {
312            async { asupersync::Outcome::Ok(vec![]) }
313        }
314
315        fn execute_prepared(
316            &self,
317            _cx: &asupersync::Cx,
318            _stmt: &sqlmodel_core::connection::PreparedStatement,
319            _params: &[sqlmodel_core::Value],
320        ) -> impl std::future::Future<Output = asupersync::Outcome<u64, sqlmodel_core::Error>> + Send
321        {
322            async { asupersync::Outcome::Ok(0) }
323        }
324
325        fn ping(
326            &self,
327            _cx: &asupersync::Cx,
328        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
329        {
330            async { asupersync::Outcome::Ok(()) }
331        }
332
333        fn close(
334            self,
335            _cx: &asupersync::Cx,
336        ) -> impl std::future::Future<Output = sqlmodel_core::error::Result<()>> + Send {
337            async { Ok(()) }
338        }
339    }
340
341    struct MockTransaction;
342
343    impl sqlmodel_core::connection::TransactionOps for MockTransaction {
344        fn query(
345            &self,
346            _cx: &asupersync::Cx,
347            _sql: &str,
348            _params: &[sqlmodel_core::Value],
349        ) -> impl std::future::Future<
350            Output = asupersync::Outcome<Vec<sqlmodel_core::Row>, sqlmodel_core::Error>,
351        > + Send {
352            async { asupersync::Outcome::Ok(vec![]) }
353        }
354
355        fn query_one(
356            &self,
357            _cx: &asupersync::Cx,
358            _sql: &str,
359            _params: &[sqlmodel_core::Value],
360        ) -> impl std::future::Future<
361            Output = asupersync::Outcome<Option<sqlmodel_core::Row>, sqlmodel_core::Error>,
362        > + Send {
363            async { asupersync::Outcome::Ok(None) }
364        }
365
366        fn execute(
367            &self,
368            _cx: &asupersync::Cx,
369            _sql: &str,
370            _params: &[sqlmodel_core::Value],
371        ) -> impl std::future::Future<Output = asupersync::Outcome<u64, sqlmodel_core::Error>> + Send
372        {
373            async { asupersync::Outcome::Ok(0) }
374        }
375
376        fn savepoint(
377            &self,
378            _cx: &asupersync::Cx,
379            _name: &str,
380        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
381        {
382            async { asupersync::Outcome::Ok(()) }
383        }
384
385        fn rollback_to(
386            &self,
387            _cx: &asupersync::Cx,
388            _name: &str,
389        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
390        {
391            async { asupersync::Outcome::Ok(()) }
392        }
393
394        fn release(
395            &self,
396            _cx: &asupersync::Cx,
397            _name: &str,
398        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
399        {
400            async { asupersync::Outcome::Ok(()) }
401        }
402
403        fn commit(
404            self,
405            _cx: &asupersync::Cx,
406        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
407        {
408            async { asupersync::Outcome::Ok(()) }
409        }
410
411        fn rollback(
412            self,
413            _cx: &asupersync::Cx,
414        ) -> impl std::future::Future<Output = asupersync::Outcome<(), sqlmodel_core::Error>> + Send
415        {
416            async { asupersync::Outcome::Ok(()) }
417        }
418    }
419
420    #[test]
421    fn test_connection_session_builder_basic() {
422        let conn = MockConnection;
423        let session = ConnectionSession::builder().build_with(conn);
424        assert!(std::ptr::eq(session.connection(), session.connection()));
425    }
426
427    #[test]
428    fn test_connection_session_new() {
429        let conn = MockConnection;
430        let session = ConnectionSession::new(conn);
431        let _ = session.connection();
432    }
433
434    #[test]
435    fn test_connection_session_connection_access() {
436        let conn = MockConnection;
437        let mut session = ConnectionSession::new(conn);
438        let _ = session.connection();
439        let _ = session.connection_mut();
440    }
441
442    #[test]
443    fn test_connection_session_into_connection() {
444        let conn = MockConnection;
445        let session = ConnectionSession::new(conn);
446        let _recovered: MockConnection = session.into_connection();
447    }
448
449    #[cfg(feature = "console")]
450    #[test]
451    fn test_connection_session_builder_with_console() {
452        let console = SqlModelConsole::new();
453        let conn = MockConnection;
454        let session = ConnectionSession::builder()
455            .with_console(console)
456            .build_with(conn);
457        assert!(session.has_console());
458    }
459
460    #[cfg(feature = "console")]
461    #[test]
462    fn test_connection_session_builder_with_shared_console() {
463        let console = Arc::new(SqlModelConsole::new());
464        let conn1 = MockConnection;
465        let conn2 = MockConnection;
466
467        let session1 = ConnectionSession::builder()
468            .with_shared_console(console.clone())
469            .build_with(conn1);
470        let session2 = ConnectionSession::builder()
471            .with_shared_console(console)
472            .build_with(conn2);
473
474        assert!(session1.has_console());
475        assert!(session2.has_console());
476    }
477
478    #[cfg(feature = "console")]
479    #[test]
480    fn test_connection_session_builder_with_auto_console() {
481        let conn = MockConnection;
482        let session = ConnectionSession::builder()
483            .with_auto_console()
484            .build_with(conn);
485        assert!(session.has_console());
486    }
487
488    #[cfg(feature = "console")]
489    #[test]
490    fn test_connection_session_console_aware() {
491        let conn = MockConnection;
492        let mut session = ConnectionSession::new(conn);
493
494        assert!(!session.has_console());
495        assert!(session.console().is_none());
496
497        let console = Arc::new(SqlModelConsole::new());
498        session.set_console(Some(console));
499        assert!(session.has_console());
500        assert!(session.console().is_some());
501
502        session.set_console(None);
503        assert!(!session.has_console());
504    }
505
506    #[test]
507    fn test_builder_chain_fluent_api() {
508        let conn = MockConnection;
509        let builder = ConnectionSession::<MockConnection>::builder();
510        let _session = builder.build_with(conn);
511    }
512}