Skip to main content

modkit_db/
lib.rs

1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2//! `ModKit` Database abstraction crate.
3//!
4//! This crate provides a unified interface for working with different databases
5//! (`SQLite`, `PostgreSQL`, `MySQL`) through `SQLx`, with optional `SeaORM` integration.
6//! It emphasizes typed connection options over DSN string manipulation and
7//! implements strict security controls (e.g., `SQLite` PRAGMA whitelist).
8//!
9//! # Features
10//! - `pg`, `mysql`, `sqlite`: enable `SQLx` backends
11//! - `sea-orm`: add `SeaORM` integration for type-safe operations
12//! - `preview-outbox`: enable the transactional outbox pipeline (experimental — API may change)
13//!
14//! # New Architecture
15//! The crate now supports:
16//! - Typed `DbConnectOptions` using sqlx `ConnectOptions` (no DSN string building)
17//! - Per-module database factories with configuration merging
18//! - `SQLite` PRAGMA whitelist for security
19//! - Environment variable expansion in passwords and DSNs
20//!
21//! # Example (`DbManager` API)
22//! ```rust,no_run
23//! use modkit_db::{DbManager, GlobalDatabaseConfig, DbConnConfig};
24//! use figment::{Figment, providers::Serialized};
25//! use std::path::PathBuf;
26//! use std::sync::Arc;
27//!
28//! // Create configuration using Figment
29//! let figment = Figment::new()
30//!     .merge(Serialized::defaults(serde_json::json!({
31//!         "db": {
32//!             "servers": {
33//!                 "main": {
34//!                     "host": "localhost",
35//!                     "port": 5432,
36//!                     "user": "app",
37//!                     "password": "${DB_PASSWORD}",
38//!                     "dbname": "app_db"
39//!                 }
40//!             }
41//!         },
42//!         "test_module": {
43//!             "database": {
44//!                 "server": "main",
45//!                 "dbname": "module_db"
46//!             }
47//!         }
48//!     })));
49//!
50//! // Create DbManager
51//! let home_dir = PathBuf::from("/app/data");
52//! let db_manager = Arc::new(DbManager::from_figment(figment, home_dir).unwrap());
53//!
54//! // Use in runtime with DbOptions::Manager(db_manager)
55//! // Modules can then use: ctx.db_required_async().await?
56//! ```
57
58#![cfg_attr(
59    not(any(feature = "pg", feature = "mysql", feature = "sqlite")),
60    allow(
61        unused_imports,
62        unused_variables,
63        dead_code,
64        unreachable_code,
65        unused_lifetimes,
66        clippy::unused_async,
67    )
68)]
69
70// Re-export key types for public API
71pub use advisory_locks::{DbLockGuard, LockConfig};
72
73// Re-export sea_orm_migration for modules that implement DatabaseCapability
74pub use sea_orm_migration;
75
76// Core modules
77pub mod advisory_locks;
78pub mod config;
79pub mod manager;
80pub mod migration_runner;
81pub mod odata;
82pub mod options;
83
84#[cfg(feature = "preview-outbox")]
85pub mod outbox;
86pub mod secure;
87
88mod db_provider;
89
90// Internal modules
91mod pool_opts;
92#[cfg(feature = "sqlite")]
93mod sqlite;
94
95// Re-export important types from new modules
96pub use config::{DbConnConfig, GlobalDatabaseConfig, PoolCfg};
97pub use manager::DbManager;
98pub use options::redact_credentials_in_dsn;
99
100// Re-export secure database types for convenience
101pub use secure::{Db, DbConn, DbTx};
102
103// Re-export service-friendly provider
104pub use db_provider::DBProvider;
105
106/// Connect and return a secure `Db` (no `DbHandle` exposure).
107///
108/// This is the public constructor intended for module code and tests.
109///
110/// # Errors
111///
112/// Returns `DbError` if the connection fails or the DSN/options are invalid.
113pub async fn connect_db(dsn: &str, opts: ConnectOpts) -> Result<Db> {
114    let handle = DbHandle::connect(dsn, opts).await?;
115    Ok(Db::new(handle))
116}
117
118/// Build a secure `Db` from config (no `DbHandle` exposure).
119///
120/// # Errors
121///
122/// Returns `DbError` if configuration is invalid or connection fails.
123pub async fn build_db(cfg: DbConnConfig, global: Option<&GlobalDatabaseConfig>) -> Result<Db> {
124    let handle = options::build_db_handle(cfg, global).await?;
125    Ok(Db::new(handle))
126}
127
128use std::time::Duration;
129
130// Internal imports
131#[cfg(any(feature = "pg", feature = "mysql", feature = "sqlite"))]
132use pool_opts::ApplyPoolOpts;
133#[cfg(feature = "sqlite")]
134use sqlite::{Pragmas, extract_sqlite_pragmas, is_memory_dsn, prepare_sqlite_path};
135
136// Used for parsing SQLite DSN query parameters
137
138#[cfg(feature = "mysql")]
139use sqlx::mysql::MySqlPoolOptions;
140#[cfg(feature = "pg")]
141use sqlx::postgres::PgPoolOptions;
142#[cfg(feature = "sqlite")]
143use sqlx::sqlite::SqlitePoolOptions;
144#[cfg(feature = "sqlite")]
145use std::str::FromStr;
146
147use sea_orm::DatabaseConnection;
148#[cfg(feature = "mysql")]
149use sea_orm::SqlxMySqlConnector;
150#[cfg(feature = "pg")]
151use sea_orm::SqlxPostgresConnector;
152#[cfg(feature = "sqlite")]
153use sea_orm::SqlxSqliteConnector;
154
155use thiserror::Error;
156
157/// Library-local result type.
158pub type Result<T> = std::result::Result<T, DbError>;
159
160/// Typed error for the DB handle and helpers.
161#[derive(Debug, Error)]
162pub enum DbError {
163    #[error("Unknown DSN: {0}")]
164    UnknownDsn(String),
165
166    #[error("Feature not enabled: {0}")]
167    FeatureDisabled(&'static str),
168
169    #[error("Invalid configuration: {0}")]
170    InvalidConfig(String),
171
172    #[error("Configuration conflict: {0}")]
173    ConfigConflict(String),
174
175    #[error("Invalid SQLite PRAGMA parameter '{key}': {message}")]
176    InvalidSqlitePragma { key: String, message: String },
177
178    #[error("Unknown SQLite PRAGMA parameter: {0}")]
179    UnknownSqlitePragma(String),
180
181    #[error("Invalid connection parameter: {0}")]
182    InvalidParameter(String),
183
184    #[error("SQLite pragma error: {0}")]
185    SqlitePragma(String),
186
187    #[error("Environment variable '{name}': {source}")]
188    EnvVar {
189        name: String,
190        source: std::env::VarError,
191    },
192
193    #[error("URL parsing error: {0}")]
194    UrlParse(#[from] url::ParseError),
195
196    #[cfg(any(feature = "pg", feature = "mysql", feature = "sqlite"))]
197    #[error(transparent)]
198    Sqlx(#[from] sqlx::Error),
199
200    #[error(transparent)]
201    Sea(#[from] sea_orm::DbErr),
202
203    #[error(transparent)]
204    Io(#[from] std::io::Error),
205
206    // make advisory_locks errors flow into DbError via `?`
207    #[error(transparent)]
208    Lock(#[from] advisory_locks::DbLockError),
209
210    #[error(transparent)]
211    Other(#[from] anyhow::Error),
212
213    /// Attempted to create a non-transactional connection inside an active transaction.
214    ///
215    /// This error occurs when `Db::conn()` is called from within a transaction closure.
216    /// The transaction guard prevents this to avoid accidental data bypass where writes
217    /// would persist outside the transaction scope.
218    ///
219    /// # Resolution
220    ///
221    /// Use the transaction runner (`tx`) provided to the closure instead of creating
222    /// a new connection:
223    ///
224    /// ```ignore
225    /// // Wrong - fails with ConnRequestedInsideTx
226    /// db.transaction(|_tx| {
227    ///     let conn = some_db.conn()?;  // Error!
228    ///     ...
229    /// });
230    ///
231    /// // Correct - use the transaction runner
232    /// db.transaction(|tx| {
233    ///     Entity::find().secure().scope_with(&scope).one(tx).await?;
234    ///     ...
235    /// });
236    /// ```
237    #[error("Cannot create non-transactional connection inside an active transaction")]
238    ConnRequestedInsideTx,
239}
240
241impl From<modkit_utils::var_expand::ExpandVarsError> for DbError {
242    fn from(err: modkit_utils::var_expand::ExpandVarsError) -> Self {
243        match err {
244            modkit_utils::var_expand::ExpandVarsError::Var { name, source } => {
245                Self::EnvVar { name, source }
246            }
247            modkit_utils::var_expand::ExpandVarsError::Regex(msg) => Self::InvalidParameter(msg),
248        }
249    }
250}
251
252impl From<crate::secure::ScopeError> for DbError {
253    fn from(value: crate::secure::ScopeError) -> Self {
254        // Scope errors are not infra connection errors, but they still originate from the DB
255        // access layer. We keep the wrapper thin and preserve the message for callers.
256        DbError::Other(anyhow::Error::new(value))
257    }
258}
259
260/// Supported engines.
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum DbEngine {
263    Postgres,
264    MySql,
265    Sqlite,
266}
267
268/// Connection options.
269/// Extended to cover common sqlx pool knobs; each driver applies the subset it supports.
270#[derive(Clone, Debug)]
271pub struct ConnectOpts {
272    /// Maximum number of connections in the pool.
273    pub max_conns: Option<u32>,
274    /// Minimum number of connections in the pool.
275    pub min_conns: Option<u32>,
276    /// Timeout to acquire a connection from the pool.
277    pub acquire_timeout: Option<Duration>,
278    /// Idle timeout before a connection is closed.
279    pub idle_timeout: Option<Duration>,
280    /// Maximum lifetime for a connection.
281    pub max_lifetime: Option<Duration>,
282    /// Test connection health before acquire.
283    pub test_before_acquire: bool,
284    /// For `SQLite` file DSNs, create parent directories if missing.
285    pub create_sqlite_dirs: bool,
286}
287impl Default for ConnectOpts {
288    fn default() -> Self {
289        Self {
290            max_conns: Some(10),
291            min_conns: None,
292            acquire_timeout: Some(Duration::from_secs(30)),
293            idle_timeout: None,
294            max_lifetime: None,
295            test_before_acquire: false,
296
297            create_sqlite_dirs: true,
298        }
299    }
300}
301
302/// Main handle.
303#[derive(Debug, Clone)]
304pub(crate) struct DbHandle {
305    engine: DbEngine,
306    dsn: String,
307    sea: DatabaseConnection,
308}
309
310#[cfg(feature = "sqlite")]
311const DEFAULT_SQLITE_BUSY_TIMEOUT: i32 = 5000;
312
313impl DbHandle {
314    /// Detect engine by DSN.
315    ///
316    /// Note: we only check scheme prefixes and don't mutate the tail (credentials etc.).
317    ///
318    /// # Errors
319    /// Returns `DbError::UnknownDsn` if the DSN scheme is not recognized.
320    pub(crate) fn detect(dsn: &str) -> Result<DbEngine> {
321        // Trim only leading spaces/newlines to be forgiving with env files.
322        let s = dsn.trim_start();
323
324        // Explicit, case-sensitive checks for common schemes.
325        // Add more variants as needed (e.g., postgres+unix://).
326        if s.starts_with("postgres://") || s.starts_with("postgresql://") {
327            Ok(DbEngine::Postgres)
328        } else if s.starts_with("mysql://") {
329            Ok(DbEngine::MySql)
330        } else if s.starts_with("sqlite:") || s.starts_with("sqlite://") {
331            Ok(DbEngine::Sqlite)
332        } else {
333            Err(DbError::UnknownDsn(dsn.to_owned()))
334        }
335    }
336
337    /// Connect and build handle.
338    ///
339    /// # Errors
340    /// Returns an error if the connection fails or the DSN is invalid.
341    pub(crate) async fn connect(dsn: &str, opts: ConnectOpts) -> Result<Self> {
342        let engine = Self::detect(dsn)?;
343        match engine {
344            #[cfg(feature = "pg")]
345            DbEngine::Postgres => {
346                let o = PgPoolOptions::new().apply(&opts);
347                let pool = o.connect(dsn).await?;
348                let sea = SqlxPostgresConnector::from_sqlx_postgres_pool(pool);
349                Ok(Self {
350                    engine,
351                    dsn: dsn.to_owned(),
352                    sea,
353                })
354            }
355            #[cfg(not(feature = "pg"))]
356            DbEngine::Postgres => Err(DbError::FeatureDisabled("PostgreSQL feature not enabled")),
357            #[cfg(feature = "mysql")]
358            DbEngine::MySql => {
359                let o = MySqlPoolOptions::new().apply(&opts);
360                let pool = o.connect(dsn).await?;
361                let sea = SqlxMySqlConnector::from_sqlx_mysql_pool(pool);
362                Ok(Self {
363                    engine,
364                    dsn: dsn.to_owned(),
365                    sea,
366                })
367            }
368            #[cfg(not(feature = "mysql"))]
369            DbEngine::MySql => Err(DbError::FeatureDisabled("MySQL feature not enabled")),
370            #[cfg(feature = "sqlite")]
371            DbEngine::Sqlite => {
372                let dsn = prepare_sqlite_path(dsn, opts.create_sqlite_dirs)?;
373
374                // Extract SQLite PRAGMA parameters from DSN
375                let (clean_dsn, pairs) = extract_sqlite_pragmas(&dsn);
376                let pragmas = Pragmas::from_pairs(&pairs);
377
378                // Build pool options with shared trait
379                let o = SqlitePoolOptions::new().apply(&opts);
380
381                // Apply SQLite pragmas using typed `sqlx` connect options (no raw SQL).
382                let is_memory = is_memory_dsn(&clean_dsn);
383                let mut conn_opts = sqlx::sqlite::SqliteConnectOptions::from_str(&clean_dsn)?;
384
385                let journal_mode = if let Some(mode) = &pragmas.journal_mode {
386                    match mode {
387                        sqlite::pragmas::JournalMode::Delete => {
388                            sqlx::sqlite::SqliteJournalMode::Delete
389                        }
390                        sqlite::pragmas::JournalMode::Wal => sqlx::sqlite::SqliteJournalMode::Wal,
391                        sqlite::pragmas::JournalMode::Memory => {
392                            sqlx::sqlite::SqliteJournalMode::Memory
393                        }
394                        sqlite::pragmas::JournalMode::Truncate => {
395                            sqlx::sqlite::SqliteJournalMode::Truncate
396                        }
397                        sqlite::pragmas::JournalMode::Persist => {
398                            sqlx::sqlite::SqliteJournalMode::Persist
399                        }
400                        sqlite::pragmas::JournalMode::Off => sqlx::sqlite::SqliteJournalMode::Off,
401                    }
402                } else if let Some(wal_toggle) = pragmas.wal_toggle {
403                    if wal_toggle {
404                        sqlx::sqlite::SqliteJournalMode::Wal
405                    } else {
406                        sqlx::sqlite::SqliteJournalMode::Delete
407                    }
408                } else if is_memory {
409                    sqlx::sqlite::SqliteJournalMode::Delete
410                } else {
411                    sqlx::sqlite::SqliteJournalMode::Wal
412                };
413                conn_opts = conn_opts.journal_mode(journal_mode);
414
415                let sync_mode = pragmas.synchronous.as_ref().map_or(
416                    sqlx::sqlite::SqliteSynchronous::Normal,
417                    |s| match s {
418                        sqlite::pragmas::SyncMode::Off => sqlx::sqlite::SqliteSynchronous::Off,
419                        sqlite::pragmas::SyncMode::Normal => {
420                            sqlx::sqlite::SqliteSynchronous::Normal
421                        }
422                        sqlite::pragmas::SyncMode::Full => sqlx::sqlite::SqliteSynchronous::Full,
423                        sqlite::pragmas::SyncMode::Extra => sqlx::sqlite::SqliteSynchronous::Extra,
424                    },
425                );
426                conn_opts = conn_opts.synchronous(sync_mode);
427
428                if !is_memory {
429                    let busy_timeout_ms_i64 = pragmas
430                        .busy_timeout_ms
431                        .unwrap_or(DEFAULT_SQLITE_BUSY_TIMEOUT.into())
432                        .max(0);
433                    let busy_timeout_ms = u64::try_from(busy_timeout_ms_i64).unwrap_or(0);
434                    conn_opts =
435                        conn_opts.busy_timeout(std::time::Duration::from_millis(busy_timeout_ms));
436                }
437
438                let pool = o.connect_with(conn_opts).await?;
439                let sea = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
440
441                Ok(Self {
442                    engine,
443                    dsn: clean_dsn,
444                    sea,
445                })
446            }
447            #[cfg(not(feature = "sqlite"))]
448            DbEngine::Sqlite => Err(DbError::FeatureDisabled("SQLite feature not enabled")),
449        }
450    }
451
452    /// Get the backend.
453    #[must_use]
454    pub fn engine(&self) -> DbEngine {
455        self.engine
456    }
457
458    /// Get the DSN used for this connection.
459    #[must_use]
460    pub fn dsn(&self) -> &str {
461        &self.dsn
462    }
463
464    // NOTE: We intentionally do not expose raw `SQLx` pools from `DbHandle`.
465    // Use `SecureConn` for all application-level DB access.
466
467    // --- SeaORM accessor ---
468
469    /// Create a secure database wrapper for module code.
470    ///
471    /// This returns a `Db` which provides controlled access to the database
472    /// via `conn()` and `transaction()` methods.
473    ///
474    /// # Security
475    ///
476    /// **INTERNAL**: Get raw `SeaORM` connection for internal runtime operations.
477    ///
478    /// This is `pub(crate)` and should **only** be used by:
479    /// - The migration runner (for executing module migrations)
480    /// - Internal infrastructure code within `modkit-db`
481    ///
482    #[must_use]
483    pub(crate) fn sea_internal(&self) -> DatabaseConnection {
484        self.sea.clone()
485    }
486
487    /// **INTERNAL**: Get a reference to the raw `SeaORM` connection.
488    ///
489    /// This is `pub(crate)` and should **only** be used by:
490    /// - The `Db` wrapper for creating runners
491    /// - Internal infrastructure code within `modkit-db`
492    ///
493    /// **NEVER expose this to modules.**
494    #[must_use]
495    pub(crate) fn sea_internal_ref(&self) -> &DatabaseConnection {
496        &self.sea
497    }
498
499    // --- Advisory locks ---
500
501    /// Acquire an advisory lock with the given key and module namespace.
502    ///
503    /// # Errors
504    /// Returns an error if the lock cannot be acquired.
505    pub async fn lock(&self, module: &str, key: &str) -> Result<DbLockGuard> {
506        let lock_manager = advisory_locks::LockManager::new(self.dsn.clone());
507        let guard = lock_manager.lock(module, key).await?;
508        Ok(guard)
509    }
510
511    /// Try to acquire an advisory lock with configurable retry/backoff policy.
512    ///
513    /// # Errors
514    /// Returns an error if an unrecoverable lock error occurs.
515    pub async fn try_lock(
516        &self,
517        module: &str,
518        key: &str,
519        config: LockConfig,
520    ) -> Result<Option<DbLockGuard>> {
521        let lock_manager = advisory_locks::LockManager::new(self.dsn.clone());
522        let res = lock_manager.try_lock(module, key, config).await?;
523        Ok(res)
524    }
525
526    // NOTE: We intentionally do not expose raw SQL transactions from `DbHandle`.
527    // Use `SecureConn::transaction` for application-level atomic operations.
528}
529
530// ===================== tests =====================
531
532#[cfg(test)]
533#[cfg_attr(coverage_nightly, coverage(off))]
534mod tests {
535    use super::*;
536    #[cfg(feature = "sqlite")]
537    use tokio::time::Duration;
538
539    #[cfg(feature = "sqlite")]
540    #[tokio::test]
541    async fn test_sqlite_connection() -> Result<()> {
542        let dsn = "sqlite::memory:";
543        let opts = ConnectOpts::default();
544        let db = DbHandle::connect(dsn, opts).await?;
545        assert_eq!(db.engine(), DbEngine::Sqlite);
546        Ok(())
547    }
548
549    #[cfg(feature = "sqlite")]
550    #[tokio::test]
551    async fn test_sqlite_connection_with_pragma_parameters() -> Result<()> {
552        // Test that SQLite connections work with PRAGMA parameters in DSN
553        let dsn = "sqlite::memory:?wal=true&synchronous=NORMAL&busy_timeout=5000&journal_mode=WAL";
554        let opts = ConnectOpts::default();
555        let db = DbHandle::connect(dsn, opts).await?;
556        assert_eq!(db.engine(), DbEngine::Sqlite);
557
558        // Verify that the stored DSN has been cleaned (SQLite parameters removed)
559        // Note: For memory databases, the DSN should still be sqlite::memory: after cleaning
560        assert!(db.dsn == "sqlite::memory:" || db.dsn.starts_with("sqlite::memory:"));
561
562        Ok(())
563    }
564
565    #[tokio::test]
566    async fn test_backend_detection() {
567        assert_eq!(
568            DbHandle::detect("sqlite::memory:").unwrap(),
569            DbEngine::Sqlite
570        );
571        assert_eq!(
572            DbHandle::detect("postgres://localhost/test").unwrap(),
573            DbEngine::Postgres
574        );
575        assert_eq!(
576            DbHandle::detect("mysql://localhost/test").unwrap(),
577            DbEngine::MySql
578        );
579        assert!(DbHandle::detect("unknown://test").is_err());
580    }
581
582    #[cfg(feature = "sqlite")]
583    #[tokio::test]
584    async fn test_advisory_lock_sqlite() -> Result<()> {
585        let dsn = "sqlite:file:memdb1?mode=memory&cache=shared";
586        let db = DbHandle::connect(dsn, ConnectOpts::default()).await?;
587
588        let now = std::time::SystemTime::now()
589            .duration_since(std::time::UNIX_EPOCH)
590            .map_or(0, |d| d.as_nanos());
591        let test_id = format!("test_basic_{now}");
592
593        let guard1 = db.lock("test_module", &format!("{test_id}_key1")).await?;
594        let _guard2 = db.lock("test_module", &format!("{test_id}_key2")).await?;
595        let _guard3 = db
596            .lock("different_module", &format!("{test_id}_key1"))
597            .await?;
598
599        // Deterministic unlock to avoid races with async Drop cleanup
600        guard1.release().await;
601        let _guard4 = db.lock("test_module", &format!("{test_id}_key1")).await?;
602        Ok(())
603    }
604
605    #[cfg(feature = "sqlite")]
606    #[tokio::test]
607    async fn test_advisory_lock_different_keys() -> Result<()> {
608        let dsn = "sqlite:file:memdb_diff_keys?mode=memory&cache=shared";
609        let db = DbHandle::connect(dsn, ConnectOpts::default()).await?;
610
611        let now = std::time::SystemTime::now()
612            .duration_since(std::time::UNIX_EPOCH)
613            .map_or(0, |d| d.as_nanos());
614        let test_id = format!("test_diff_{now}");
615
616        let _guard1 = db.lock("test_module", &format!("{test_id}_key1")).await?;
617        let _guard2 = db.lock("test_module", &format!("{test_id}_key2")).await?;
618        let _guard3 = db.lock("other_module", &format!("{test_id}_key1")).await?;
619        Ok(())
620    }
621
622    #[cfg(feature = "sqlite")]
623    #[tokio::test]
624    async fn test_try_lock_with_config() -> Result<()> {
625        let dsn = "sqlite:file:memdb2?mode=memory&cache=shared";
626        let db = DbHandle::connect(dsn, ConnectOpts::default()).await?;
627
628        let now = std::time::SystemTime::now()
629            .duration_since(std::time::UNIX_EPOCH)
630            .map_or(0, |d| d.as_nanos());
631        let test_id = format!("test_config_{now}");
632
633        let _guard1 = db.lock("test_module", &format!("{test_id}_key")).await?;
634
635        let config = LockConfig {
636            max_wait: Some(Duration::from_millis(200)),
637            initial_backoff: Duration::from_millis(50),
638            max_attempts: Some(3),
639            ..Default::default()
640        };
641
642        let result = db
643            .try_lock("test_module", &format!("{test_id}_different_key"), config)
644            .await?;
645        assert!(
646            result.is_some(),
647            "expected lock acquisition for different key"
648        );
649        Ok(())
650    }
651
652    #[cfg(feature = "sqlite")]
653    #[tokio::test]
654    async fn test_sea_internal_access() -> Result<()> {
655        let dsn = "sqlite::memory:";
656        let db = DbHandle::connect(dsn, ConnectOpts::default()).await?;
657
658        // Internal method for migrations
659        let _raw = db.sea_internal();
660        Ok(())
661    }
662}