♻️ (seaorm): Separa módulo migration de db

- `db::*` queda como API de consultas (connection, fetch_*).
- `migration::*` sube a primer nivel con su propia documentación.
- `DBCONN` y `run_now` se trasladan a la raíz de la extensión.
- Actualiza README.md y docs para reflejar la nueva estructura.
This commit is contained in:
Manuel Cillero 2026-05-15 00:22:55 +02:00
parent 796ae5ce81
commit 026448e511
12 changed files with 279 additions and 217 deletions

View file

@ -69,7 +69,7 @@ async fn main() -> std::io::Result<()> {
**Escribe las migraciones** usando la API de SeaORM: **Escribe las migraciones** usando la API de SeaORM:
```rust,no_run ```rust,no_run
use pagetop_seaorm::db::*; use pagetop_seaorm::migration::*;
pub struct Migration; pub struct Migration;
@ -111,7 +111,7 @@ Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/S
usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas, usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas,
columnas, índices y claves externas). columnas, índices y claves externas).
El módulo de migraciones (`src/db/migration/`) incorpora una adaptación de El módulo de migraciones (`src/migration/`) incorpora una adaptación de
[sea-orm-migration](https://crates.io/crates/sea-orm-migration). El código que se integra procede de [sea-orm-migration](https://crates.io/crates/sea-orm-migration). El código que se integra procede de
la versión [**1.1.20**](https://github.com/SeaQL/sea-orm/tree/1.1.20/sea-orm-migration) en lugar de la versión [**1.1.20**](https://github.com/SeaQL/sea-orm/tree/1.1.20/sea-orm-migration) en lugar de
usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo de vida de las usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo de vida de las
@ -124,7 +124,7 @@ extensión. Los ficheros adaptados del original son:
| `connection.rs` | Integración completa | | `connection.rs` | Integración completa |
| `manager.rs` | Adapta *features* propias | | `manager.rs` | Adapta *features* propias |
| `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | | `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI |
| `prelude.rs` | Excluye exportaciones del CLI | | `prelude.rs` | Absorbido en `migration.rs`, descarta exportaciones del CLI |
| `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) | | `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) |
| `seaql_migrations.rs` | Integración completa | | `seaql_migrations.rs` | Integración completa |

View file

@ -1,30 +1,52 @@
use pagetop::core::TypeInfo; //! API completa de SeaORM para operaciones con la base de datos.
use pagetop::trace; //!
//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…)
//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario
//! para definir entidades y realizar operaciones CRUD:
//!
//! ```rust,ignore
//! use pagetop_seaorm::db::*;
//! ```
//!
//! Para definir el esquema de la base de datos o escribir migraciones usa además
//! [`crate::migration`].
pub(crate) use url::Url as DbUri; pub use sea_orm::prelude::*;
pub use sea_orm::error::{DbErr, RuntimeErr}; use sea_orm::sea_query::{
pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder,
};
use sea_orm::{DatabaseBackend, ExecResult, Statement};
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; /// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades.
///
mod dbconn; /// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades
pub(crate) use dbconn::{run_now, DBCONN}; /// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un
/// valor inicializado una sola vez al arrancar la aplicación.
// Adaptación de `sea-orm-migration` (ver §Créditos en README.md). ///
mod migration; /// ```rust,no_run
pub use migration::prelude::*; /// use pagetop_seaorm::db::*;
pub use migration::schema::*; ///
/// // Consultas tipadas con el sistema de entidades de SeaORM:
/// // let users = User::find().all(connection()).await?;
/// // let user = User::find_by_id(1).one(connection()).await?;
/// // User::insert(model).exec(connection()).await?;
/// let _conn = connection();
/// ```
pub fn connection() -> &'static DatabaseConnection {
&super::DBCONN
}
/// Ejecuta una consulta para devolver todas las filas resultantes. /// Ejecuta una consulta para devolver todas las filas resultantes.
/// ///
/// Acepta cualquier tipo que implemente [`QueryStatementWriter`] (p. ej. [`SelectStatement`]) y /// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y
/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada /// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada
/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con /// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con
/// [`QueryResult::try_get`]. /// [`QueryResult::try_get`].
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop_seaorm::db::*; /// use pagetop_seaorm::db::*;
/// use pagetop_seaorm::migration::*;
/// ///
/// async fn example() -> Result<(), DbErr> { /// async fn example() -> Result<(), DbErr> {
/// let mut stmt = Query::select() /// let mut stmt = Query::select()
@ -40,7 +62,7 @@ pub use migration::schema::*;
/// } /// }
/// ``` /// ```
pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> { pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> {
let dbconn = &*DBCONN; let dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend(); let dbbackend = dbconn.get_database_backend();
dbconn dbconn
.query_all(Statement::from_string( .query_all(Statement::from_string(
@ -61,6 +83,7 @@ pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<Quer
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop_seaorm::db::*; /// use pagetop_seaorm::db::*;
/// use pagetop_seaorm::migration::*;
/// ///
/// async fn example() -> Result<(), DbErr> { /// async fn example() -> Result<(), DbErr> {
/// let mut stmt = Query::select() /// let mut stmt = Query::select()
@ -78,7 +101,7 @@ pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<Quer
pub async fn fetch_one<Q: QueryStatementWriter>( pub async fn fetch_one<Q: QueryStatementWriter>(
stmt: &mut Q, stmt: &mut Q,
) -> Result<Option<QueryResult>, DbErr> { ) -> Result<Option<QueryResult>, DbErr> {
let dbconn = &*DBCONN; let dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend(); let dbbackend = dbconn.get_database_backend();
dbconn dbconn
.query_one(Statement::from_string( .query_one(Statement::from_string(
@ -95,8 +118,8 @@ pub async fn fetch_one<Q: QueryStatementWriter>(
/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de /// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de
/// la operación. /// la operación.
/// ///
/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe como /// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe
/// cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El /// como cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El
/// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado. /// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado.
/// ///
/// ```rust,no_run /// ```rust,no_run
@ -109,76 +132,9 @@ pub async fn fetch_one<Q: QueryStatementWriter>(
/// } /// }
/// ``` /// ```
pub async fn execute(stmt: impl Into<String>) -> Result<ExecResult, DbErr> { pub async fn execute(stmt: impl Into<String>) -> Result<ExecResult, DbErr> {
let dbconn = &*DBCONN; let dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend(); let dbbackend = dbconn.get_database_backend();
dbconn dbconn
.execute(Statement::from_string(dbbackend, stmt.into())) .execute(Statement::from_string(dbbackend, stmt.into()))
.await .await
} }
pub trait MigratorBase {
fn run_up();
fn run_down();
}
#[rustfmt::skip]
impl<M: MigratorTrait> MigratorBase for M {
fn run_up() {
if let Err(e) = run_now(Self::up(SchemaManagerConnection::Connection(&DBCONN), None)) {
trace::error!("Migration upgrade failed ({})", e);
};
}
fn run_down() {
if let Err(e) = run_now(Self::down(SchemaManagerConnection::Connection(&DBCONN), None)) {
trace::error!("Migration downgrade failed ({})", e);
};
}
}
impl<M: MigrationTrait> MigrationName for M {
fn name(&self) -> &str {
TypeInfo::NameTo(-2).of::<M>()
}
}
pub type MigrationItem = Box<dyn MigrationTrait>;
#[macro_export]
macro_rules! install_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::db::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}
}
Migrator::run_up();
}};
}
#[macro_export]
macro_rules! uninstall_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::db::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}
}
Migrator::run_down();
}};
}

View file

@ -1,64 +0,0 @@
use pagetop::trace;
use crate::config;
use crate::db::{DbConn, DbUri};
use std::sync::LazyLock;
use sea_orm::{ConnectOptions, Database};
pub use futures::executor::block_on as run_now;
pub static DBCONN: LazyLock<DbConn> = 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 = match config::SETTINGS.database.db_type.as_str() {
"mysql" | "postgres" => {
let mut tmp_uri = DbUri::parse(
format!(
"{}://{}/{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap();
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.unwrap();
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.unwrap();
if let Some(port) = config::SETTINGS.database.db_port {
tmp_uri.set_port(Some(port)).unwrap();
}
tmp_uri
}
"sqlite" => DbUri::parse(
format!(
"{}://{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap(),
_ => panic!(
"Unrecognized database type \"{}\"",
config::SETTINGS.database.db_type
),
};
run_now(Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri.to_string());
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
}))
.unwrap_or_else(|_| panic!("Failed to connect to database"))
});

View file

@ -1,35 +0,0 @@
//pub mod cli;
pub mod connection;
pub mod manager;
pub mod migrator;
pub mod prelude;
pub mod schema;
pub mod seaql_migrations;
//pub mod util;
pub use connection::*;
pub use manager::*;
//pub use migrator::*;
pub use async_trait;
//pub use sea_orm;
//pub use sea_orm::sea_query;
use sea_orm::DbErr;
pub trait MigrationName {
fn name(&self) -> &str;
}
/// The migration definition
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Err(DbErr::Migration(
"Rollback not implemented for this migration".to_owned(),
))
}
}

View file

@ -1,12 +0,0 @@
//pub use super::cli;
pub use super::connection::IntoSchemaManagerConnection;
pub use super::connection::SchemaManagerConnection;
pub use super::manager::SchemaManager;
pub use super::migrator::MigratorTrait;
pub use super::{MigrationName, MigrationTrait};
pub use async_trait;
pub use sea_orm;
pub use sea_orm::sea_query;
pub use sea_orm::sea_query::*;
pub use sea_orm::DeriveIden;

View file

@ -70,7 +70,7 @@ async fn main() -> std::io::Result<()> {
**Escribe las migraciones** usando la API de SeaORM: **Escribe las migraciones** usando la API de SeaORM:
```rust,no_run ```rust,no_run
use pagetop_seaorm::db::*; use pagetop_seaorm::migration::*;
pub struct Migration; pub struct Migration;
@ -103,12 +103,75 @@ enum Users {
use pagetop::prelude::*; use pagetop::prelude::*;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use url::Url;
use std::sync::LazyLock;
include_locales!(LOCALES_SEAORM); include_locales!(LOCALES_SEAORM);
pub mod config; pub mod config;
pub mod db; pub mod db;
pub mod migration;
pub(crate) use futures::executor::block_on as run_now;
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 = match config::SETTINGS.database.db_type.as_str() {
"mysql" | "postgres" => {
let mut tmp_uri = Url::parse(
format!(
"{}://{}/{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap();
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.unwrap();
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.unwrap();
if let Some(port) = config::SETTINGS.database.db_port {
tmp_uri.set_port(Some(port)).unwrap();
}
tmp_uri
}
"sqlite" => Url::parse(
format!(
"{}://{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap(),
_ => panic!(
"Unrecognized database type \"{}\"",
config::SETTINGS.database.db_type
),
};
run_now(Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri.to_string());
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
}))
.unwrap_or_else(|_| panic!("Failed to connect to database"))
});
/// Implementa la extensión. /// Implementa la extensión.
pub struct SeaORM; pub struct SeaORM;
@ -122,6 +185,6 @@ impl Extension for SeaORM {
} }
fn initialize(&self) { fn initialize(&self) {
std::sync::LazyLock::force(&db::DBCONN); std::sync::LazyLock::force(&DBCONN);
} }
} }

View file

@ -0,0 +1,156 @@
//! API para definir y ejecutar migraciones de base de datos.
//!
//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros
//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al
//! arrancar la extensión.
//!
//! ```rust,ignore
//! use pagetop_seaorm::db::*;
//! use pagetop_seaorm::migration::*;
//! ```
// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >*****************************
//pub mod cli;
pub mod connection;
pub mod manager;
pub mod migrator;
//pub mod prelude;
pub mod schema;
pub mod seaql_migrations;
//pub mod util;
pub use connection::*;
pub use manager::*;
//pub use migrator::*;
pub use async_trait;
//pub use sea_orm;
//pub use sea_orm::sea_query;
pub use sea_orm::DbErr;
pub trait MigrationName {
fn name(&self) -> &str;
}
/// The migration definition
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Err(DbErr::Migration(
"Rollback not implemented for this migration".to_owned(),
))
}
}
// *************************************************************************************************
pub use migrator::MigratorTrait;
pub use schema::*;
pub use sea_orm::sea_query::*;
pub use sea_orm::DeriveIden;
use pagetop::core::TypeInfo;
use pagetop::trace;
impl<M: MigrationTrait> MigrationName for M {
fn name(&self) -> &str {
TypeInfo::NameTo(-2).of::<M>()
}
}
pub type MigrationItem = Box<dyn MigrationTrait>;
pub trait MigratorBase {
fn run_up();
fn run_down();
}
#[rustfmt::skip]
impl<M: MigratorTrait> MigratorBase for M {
fn run_up() {
if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
trace::error!("Migration upgrade failed ({})", e);
};
}
fn run_down() {
if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
trace::error!("Migration downgrade failed ({})", e);
};
}
}
/// Aplica las migraciones pendientes al arrancar una extensión.
///
/// Recibe uno o más módulos de migración y ejecuta el método `up` de los que aún no estén
/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde
/// [`Extension::initialize`](pagetop::core::extension::Extension::initialize).
///
/// ```rust,ignore
/// impl Extension for MyExt {
/// fn initialize(&self) {
/// install_migrations!(
/// m20240101_000001_create_users_table,
/// m20240115_000002_add_email_index,
/// );
/// }
/// }
/// ```
#[macro_export]
macro_rules! install_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}
}
Migrator::run_up();
}};
}
/// Revierte las migraciones de una extensión en orden inverso al de su aplicación.
///
/// Ejecuta el método `down` de cada migración indicada. Si alguna no implementa `down`,
/// detiene el proceso con un error. Complementario a [`crate::install_migrations`].
///
/// ```rust,ignore
/// impl Extension for MyExt {
/// fn uninitialize(&self) {
/// uninstall_migrations!(
/// m20240101_000001_create_users_table,
/// m20240115_000002_add_email_index,
/// );
/// }
/// }
/// ```
#[macro_export]
macro_rules! uninstall_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}
}
Migrator::run_down();
}};
}

View file

@ -1,17 +1,16 @@
//! Adapted from <https://github.com/loco-rs/loco/blob/master/src/schema.rs> //! Adaptación de <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
//! //!
//! # Database Table Schema Helpers //! # Ayudantes de esquema de base de datos
//! //!
//! This module defines functions and helpers for creating database table //! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`.
//! schemas using the `sea-orm` and `sea-query` libraries.
//! //!
//! # Example //! # Ejemplo
//! //!
//! The following example shows how the user migration file should be and using //! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes
//! the schema helpers to create the Db fields. //! de esquema.
//! //!
//! ```rust //! ```rust
//! use pagetop_seaorm::db::*; //! use pagetop_seaorm::migration::*;
//! //!
//! pub struct Migration; //! pub struct Migration;
//! //!
@ -38,7 +37,7 @@
//! } //! }
//! } //! }
//! //!
//! #[derive(Iden)] //! #[derive(DeriveIden)]
//! pub enum Users { //! pub enum Users {
//! Table, //! Table,
//! Id, //! Id,
@ -51,10 +50,9 @@
//! } //! }
//! ``` //! ```
use crate::db::Iden;
use sea_orm::sea_query::{ use sea_orm::sea_query::{
self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, self, Alias, ColumnDef, ColumnType, Expr, Iden, IntoIden, PgInterval, Table,
TableCreateStatement,
}; };
#[derive(Iden)] #[derive(Iden)]
@ -599,7 +597,7 @@ pub fn array_uniq<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
array(col, elem_type).unique_key().take() array(col, elem_type).unique_key().take()
} }
/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. /// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente.
pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
let mut t = t; let mut t = t;
t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp()))
@ -607,7 +605,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
.take() .take()
} }
/// Create an Alias. /// Crea un alias.
pub fn name<T: Into<String>>(name: T) -> Alias { pub fn name<T: Into<String>>(name: T) -> Alias {
Alias::new(name) Alias::new(name)
} }