♻️ (seaorm): Adapta al arranque async

- Elimina `run_now` y convierte `initialize()` en `async fn`.
- `DBCONN` pasa de `LazyLock` a `OnceLock`; `MigratorBase` adopta
  `#[pagetop::async_trait]` con métodos async nativos.
This commit is contained in:
Manuel Cillero 2026-07-06 19:40:54 +02:00
parent 1139df6210
commit 9b2d430d8b
8 changed files with 94 additions and 100 deletions

View file

@ -20,7 +20,6 @@ postgres = ["sea-orm/sqlx-postgres"]
sqlite = ["sea-orm/sqlx-sqlite"] sqlite = ["sea-orm/sqlx-sqlite"]
[dependencies] [dependencies]
async-trait.workspace = true
pagetop.workspace = true pagetop.workspace = true
sea-orm.workspace = true sea-orm.workspace = true
sea-schema.workspace = true sea-schema.workspace = true

View file

@ -70,6 +70,7 @@ mod migration;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -77,14 +78,14 @@ impl Extension for MyApp {
] ]
} }
fn initialize(&self) { async fn initialize(&self) {
install_migrations!(m20240101_000001_create_users); install_migrations!(m20240101_000001_create_users);
} }
} }
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await Application::prepare(&MyApp).await.run().await
} }
``` ```
@ -96,7 +97,7 @@ use pagetop_seaorm::migration::*;
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[pagetop::async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager

View file

@ -143,10 +143,6 @@ pub use sea_orm::{
ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait, ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait,
}; };
/// Permite implementar *traits* con métodos `async`:
#[doc(inline)]
pub use async_trait;
/// Re-exporta el crate `sea_orm` íntegro como puerta de acceso a su API completa. /// Re-exporta el crate `sea_orm` íntegro como puerta de acceso a su API completa.
/// ///
/// Útil para tipos o utilidades que no están expuestos directamente en [`db::*`](self). La inmensa /// Útil para tipos o utilidades que no están expuestos directamente en [`db::*`](self). La inmensa
@ -176,7 +172,13 @@ pub use sea_orm::sea_query as query;
/// ``` /// ```
#[inline] #[inline]
pub fn dbconn() -> &'static DatabaseConnection { pub fn dbconn() -> &'static DatabaseConnection {
&super::DBCONN super::DBCONN
// La inicialización requiere async (`Database::connect().await`). OnceLock asigna el valor
// con `.set()` en `Extension::initialize()` (contra LazyLock que sólo admite uso síncrono).
.get()
// Este texto no se multiplica al hacer `#[inline]`, es un `&'static str` que el compilador
// almacena una vez. Sólo se duplica una comprobación de nulo y un salto condicional.
.expect("Database not initialized: SeaORM extension must be listed as a dependency")
} }
/// Ejecuta una sentencia SQL en crudo y devuelve su resultado. /// Ejecuta una sentencia SQL en crudo y devuelve su resultado.

View file

@ -71,6 +71,7 @@ mod migration;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -78,14 +79,14 @@ impl Extension for MyApp {
] ]
} }
fn initialize(&self) { async fn initialize(&self) {
install_migrations!(m20240101_000001_create_users); install_migrations!(m20240101_000001_create_users);
} }
} }
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await Application::prepare(&MyApp).await.run().await
} }
``` ```
@ -97,7 +98,7 @@ use pagetop_seaorm::migration::*;
pub struct Migration; pub struct Migration;
#[async_trait::async_trait] #[pagetop::async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
@ -176,7 +177,7 @@ use pagetop::prelude::*;
use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use url::Url; use url::Url;
use std::sync::LazyLock; use std::sync::OnceLock;
include_locales!(LOCALES_SEAORM); include_locales!(LOCALES_SEAORM);
@ -186,73 +187,12 @@ pub mod db;
pub mod migration; pub mod migration;
// Ejecuta un *future* de forma síncrona dentro del runtime de Tokio. static DBCONN: OnceLock<DatabaseConnection> = OnceLock::new();
//
// Usa [`tokio::task::block_in_place`] para ceder el hilo actual al código bloqueante sin detener el
// *pool* de trabajo de Tokio, y a continuación ejecuta el *future* con el *handle* del *runtime*
// activo. Requiere el *runtime* multi-hilo (predeterminado con `#[pagetop::main]`).
//
// En tests, `#[pagetop::test]` aplica `multi_thread` por defecto. Si se utiliza `#[tokio::test]`
// directamente, habría que añadir `(flavor = "multi_thread")` si el test invoca código que llame a
// esta función.
pub(crate) fn run_now<F: std::future::Future>(future: F) -> F::Output {
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(future))
}
pub(crate) static DBCONN: LazyLock<DatabaseConnection> = LazyLock::new(|| {
trace::info!(
"Connecting to database \"{}\" using a pool of {} connections",
&config::SETTINGS.database.db_name,
&config::SETTINGS.database.max_pool_size
);
let db_uri: String = match config::SETTINGS.database.db_type {
config::DbType::Unset => panic!(
"database.db_type is not configured: set it to \"mysql\", \"postgres\" or \"sqlite\""
),
config::DbType::Mysql | config::DbType::Postgres => {
let scheme = if matches!(config::SETTINGS.database.db_type, config::DbType::Mysql) {
"mysql"
} else {
"postgres"
};
let mut tmp_uri = Url::parse(&format!(
"{}://{}/{}",
scheme,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
))
.expect("Invalid database URL: check db_host and db_name in config");
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.expect("Failed to set db_user in connection URL");
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.expect("Failed to set db_pass in connection URL");
if let Some(port) = config::SETTINGS.database.db_port {
tmp_uri
.set_port(Some(port))
.expect("Failed to set db_port in connection URL");
}
tmp_uri.to_string()
}
config::DbType::Sqlite => {
format!("sqlite://{}", &config::SETTINGS.database.db_name)
}
};
run_now(Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri);
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
}))
.expect("Failed to connect to database")
});
/// Implementa la extensión. /// Implementa la extensión.
pub struct SeaORM; pub struct SeaORM;
#[async_trait]
impl Extension for SeaORM { impl Extension for SeaORM {
fn name(&self) -> L10n { fn name(&self) -> L10n {
L10n::t("extension_name", &LOCALES_SEAORM) L10n::t("extension_name", &LOCALES_SEAORM)
@ -262,7 +202,57 @@ impl Extension for SeaORM {
L10n::t("extension_description", &LOCALES_SEAORM) L10n::t("extension_description", &LOCALES_SEAORM)
} }
fn initialize(&self) { async fn initialize(&self) {
std::sync::LazyLock::force(&DBCONN); trace::info!(
"Connecting to database \"{}\" using a pool of {} connections",
&config::SETTINGS.database.db_name,
&config::SETTINGS.database.max_pool_size
);
let db_uri: String = match config::SETTINGS.database.db_type {
config::DbType::Unset => panic!(
"database.db_type is not configured: use \"mysql\", \"postgres\" or \"sqlite\""
),
config::DbType::Mysql | config::DbType::Postgres => {
let scheme = if matches!(config::SETTINGS.database.db_type, config::DbType::Mysql) {
"mysql"
} else {
"postgres"
};
let mut tmp_uri = Url::parse(&format!(
"{}://{}/{}",
scheme,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
))
.expect("Invalid database URL: check db_host and db_name in config");
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.expect("Failed to set db_user in connection URL");
tmp_uri
// https://github.com/launchbadge/sqlx/issues/1624
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.expect("Failed to set db_pass in connection URL");
if let Some(port) = config::SETTINGS.database.db_port {
tmp_uri
.set_port(Some(port))
.expect("Failed to set db_port in connection URL");
}
tmp_uri.to_string()
}
config::DbType::Sqlite => {
format!("sqlite://{}", &config::SETTINGS.database.db_name)
}
};
let conn = Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri);
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
})
.await
.expect("Failed to connect to database");
DBCONN.set(conn).expect("DBCONN already initialized");
} }
} }

View file

@ -34,7 +34,7 @@
//! //!
//! pub struct Migration; //! pub struct Migration;
//! //!
//! #[async_trait::async_trait] //! #[pagetop::async_trait]
//! impl MigrationTrait for Migration { //! impl MigrationTrait for Migration {
//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { //! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
//! manager //! manager
@ -127,9 +127,7 @@ pub use connection::*;
pub use manager::*; pub use manager::*;
//pub use migrator::*; //pub use migrator::*;
/// Permite implementar *traits* con métodos `async`: //pub use async_trait;
#[doc(inline)]
pub use async_trait;
//pub use sea_orm; //pub use sea_orm;
//pub use sea_orm::sea_query; //pub use sea_orm::sea_query;
pub use sea_orm::DbErr; pub use sea_orm::DbErr;
@ -139,7 +137,7 @@ pub trait MigrationName {
} }
/// The migration definition /// The migration definition
#[async_trait::async_trait] #[pagetop::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync { pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration /// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
@ -173,35 +171,37 @@ impl<M: MigrationTrait> MigrationName for M {
/// Elemento de migración listo para incluir en la lista de un [`MigratorTrait`]. /// Elemento de migración listo para incluir en la lista de un [`MigratorTrait`].
pub type MigrationItem = Box<dyn MigrationTrait>; pub type MigrationItem = Box<dyn MigrationTrait>;
/// Interfaz síncrona para ejecutar migraciones desde código no asíncrono. /// Interfaz asíncrona para ejecutar migraciones.
/// ///
/// Todo tipo que implemente [`MigratorTrait`] obtiene esta interfaz automáticamente, incluidos los /// Todo tipo que implemente [`MigratorTrait`] obtiene esta interfaz automáticamente, incluidos los
/// tipos generados por los macros [`install_migrations!`](crate::install_migrations) y /// tipos generados por los macros [`install_migrations!`](crate::install_migrations) y
/// [`uninstall_migrations!`](crate::uninstall_migrations). /// [`uninstall_migrations!`](crate::uninstall_migrations).
#[pagetop::async_trait]
pub trait MigratorBase { pub trait MigratorBase {
/// Ejecuta las migraciones pendientes en orden ascendente. /// Ejecuta las migraciones pendientes en orden ascendente.
/// ///
/// Provoca un `panic!` si alguna migración falla, evitando que la aplicación arranque con un /// Provoca un `panic!` si alguna migración falla, evitando que la aplicación arranque con un
/// esquema de base de datos inconsistente. /// esquema de base de datos inconsistente.
fn run_up(); async fn run_up();
/// Revierte todas las migraciones en orden descendente. /// Revierte todas las migraciones en orden descendente.
/// ///
/// Provoca un `panic!` si alguna reversión falla. /// Provoca un `panic!` si alguna reversión falla.
fn run_down(); async fn run_down();
} }
#[pagetop::async_trait]
impl<M: MigratorTrait> MigratorBase for M { impl<M: MigratorTrait> MigratorBase for M {
fn run_up() { async fn run_up() {
let conn = SchemaManagerConnection::Connection(&super::DBCONN); let conn = SchemaManagerConnection::Connection(super::db::dbconn());
if let Err(e) = super::run_now(Self::up(conn, None)) { if let Err(e) = Self::up(conn, None).await {
panic!("Migration upgrade failed: {e}"); panic!("Migration upgrade failed: {e}");
} }
} }
fn run_down() { async fn run_down() {
let conn = SchemaManagerConnection::Connection(&super::DBCONN); let conn = SchemaManagerConnection::Connection(super::db::dbconn());
if let Err(e) = super::run_now(Self::down(conn, None)) { if let Err(e) = Self::down(conn, None).await {
panic!("Migration downgrade failed: {e}"); panic!("Migration downgrade failed: {e}");
} }
} }
@ -228,8 +228,9 @@ impl<M: MigratorTrait> MigratorBase for M {
/// ```rust,ignore /// ```rust,ignore
/// mod migration; /// mod migration;
/// ///
/// #[async_trait]
/// impl Extension for MyExt { /// impl Extension for MyExt {
/// fn initialize(&self) { /// async fn initialize(&self) {
/// install_migrations!( /// install_migrations!(
/// m20240101_000001_create_users, /// m20240101_000001_create_users,
/// m20240115_000002_add_email_index, /// m20240115_000002_add_email_index,
@ -252,7 +253,7 @@ macro_rules! install_migrations {
m m
} }
} }
Migrator::run_up(); Migrator::run_up().await;
}}; }};
} }
@ -269,8 +270,9 @@ macro_rules! install_migrations {
/// ///
/// En `src/lib.rs`: /// En `src/lib.rs`:
/// ```rust,ignore /// ```rust,ignore
/// #[async_trait]
/// impl Extension for MyExt { /// impl Extension for MyExt {
/// fn uninitialize(&self) { /// async fn uninitialize(&self) {
/// uninstall_migrations!( /// uninstall_migrations!(
/// m20240101_000001_create_users, /// m20240101_000001_create_users,
/// m20240115_000002_add_email_index, /// m20240115_000002_add_email_index,
@ -293,6 +295,6 @@ macro_rules! uninstall_migrations {
m m
} }
} }
Migrator::run_down(); Migrator::run_down().await;
}}; }};
} }

View file

@ -10,7 +10,7 @@ pub enum SchemaManagerConnection<'c> {
Transaction(&'c DatabaseTransaction), Transaction(&'c DatabaseTransaction),
} }
#[async_trait::async_trait] #[pagetop::async_trait]
impl ConnectionTrait for SchemaManagerConnection<'_> { impl ConnectionTrait for SchemaManagerConnection<'_> {
fn get_database_backend(&self) -> DbBackend { fn get_database_backend(&self) -> DbBackend {
match self { match self {
@ -55,7 +55,7 @@ impl ConnectionTrait for SchemaManagerConnection<'_> {
} }
} }
#[async_trait::async_trait] #[pagetop::async_trait]
impl TransactionTrait for SchemaManagerConnection<'_> { impl TransactionTrait for SchemaManagerConnection<'_> {
async fn begin(&self) -> Result<DatabaseTransaction, DbErr> { async fn begin(&self) -> Result<DatabaseTransaction, DbErr> {
match self { match self {

View file

@ -57,7 +57,7 @@ impl Migration {
} }
/// Performing migrations on a database /// Performing migrations on a database
#[async_trait::async_trait] #[pagetop::async_trait]
pub trait MigratorTrait: Send { pub trait MigratorTrait: Send {
/// Vector of migrations in time sequence /// Vector of migrations in time sequence
fn migrations() -> Vec<Box<dyn MigrationTrait>>; fn migrations() -> Vec<Box<dyn MigrationTrait>>;

View file

@ -15,7 +15,7 @@
//! //!
//! pub struct Migration; //! pub struct Migration;
//! //!
//! #[async_trait::async_trait] //! #[pagetop::async_trait]
//! impl MigrationTrait for Migration { //! impl MigrationTrait for Migration {
//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { //! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
//! let table = table_auto(Users::Table) //! let table = table_auto(Users::Table)