diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index 23a4ce40..42738cd0 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -45,9 +45,6 @@ opcional; si se omite se usa el puerto predeterminado del motor. ```rust,ignore use pagetop::prelude::*; -use pagetop_seaorm::install_migrations; - -mod migration; struct MyApp; @@ -69,10 +66,9 @@ async fn main() -> std::io::Result<()> { } ``` -**Escribe las migraciones** usando la API de [`migration`]: +**Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -// src/migration/m20240101_000001_create_users.rs use pagetop_seaorm::migration::*; pub struct Migration; @@ -85,7 +81,6 @@ impl MigrationTrait for Migration { table_auto(Users::Table) .col(pk_auto(Users::Id)) .col(string_uniq(Users::Email)) - .col(string(Users::Name)) .to_owned(), ) .await @@ -97,52 +92,6 @@ enum Users { Table, Id, Email, - Name, -} -``` - -**Define las entidades** en un módulo `entity/` usando las macros de derivación de [`db`]: - -```rust,no_run -// src/entity/user.rs -use pagetop_seaorm::db::*; - -#[derive(Clone, Debug, DeriveEntityModel, PartialEq)] -#[sea_orm(table_name = "users")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - pub email: String, - pub name: String, -} - -#[derive(Clone, Copy, Debug, DeriveRelation, EnumIter)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} -``` - -**Opera con la base de datos** pasando la conexión [`db::dbconn()`] a cada consulta: - -```rust,ignore -use pagetop_seaorm::db::*; - -// Asumiendo que existe un módulo `user` con la entidad definida arriba. -async fn example() -> Result<(), DbErr> { - // Listar todos los registros: - let users = user::Entity::find().all(dbconn()).await?; - - // Buscar por clave primaria: - let found = user::Entity::find_by_id(1).one(dbconn()).await?; - - // Insertar un registro: - let new_user = user::ActiveModel { - email: Set("alice@example.com".to_owned()), - name: Set("Alice".to_owned()), - ..Default::default() - }; - user::Entity::insert(new_user).exec(dbconn()).await?; - Ok(()) } ``` @@ -176,7 +125,7 @@ extensión. Los ficheros adaptados del original son: | `manager.rs` | Adapta *features* propias | | `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | | `prelude.rs` | Absorbido en `migration.rs`, descarta exportaciones del CLI | -| `schema.rs` | Integra con ajustes, adaptado 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 | diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index a2b5dd4d..2b47c399 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -1,199 +1,128 @@ -//! Definición de entidades y acceso a la base de datos. +//! API completa de SeaORM para operaciones con la base de datos. //! -//! Agrupa los *traits*, macros y tipos del sistema de entidades de SeaORM, junto con las funciones -//! [`dbconn`], [`execute`], [`fetch_all`] y [`fetch_one`], en una sola importación: -//! -//! ```rust -//! use pagetop_seaorm::db::*; -//! ``` -//! -//! El sistema de entidades (`Entity::find()`, `Entity::insert()`, transacciones) es el camino -//! recomendado para la mayoría de operaciones. Las funciones [`execute`], [`fetch_all`] y -//! [`fetch_one`] ofrecen vías alternativas para cuando ese sistema no es suficiente, como consultas -//! sin entidad concreta, SQL específico para el motor de base de datos utilizado o sentencias -//! puntuales. -//! -//! Estas funciones integran los valores como literales escapados, no como parámetros de base de -//! datos. Para consultas con datos del usuario, el sistema de entidades es más robusto. Si aun así -//! se necesita SQL en crudo con parámetros reales, se puede construir un [`api::Statement`] -//! directamente con [`api::Statement::from_sql_and_values`]. -//! -//! ## Tipos esenciales -//! -//! Destacan los siguientes elementos de uso más frecuente: -//! -//! - **Acceso**: [`DatabaseConnection`], [`dbconn`] (para obtener el pool de conexiones). -//! - **Consultas**: [`EntityTrait`], [`QueryFilter`], [`QueryOrder`], [`QuerySelect`]. -//! - **Transacciones**: [`TransactionTrait`], [`DatabaseTransaction`]. -//! - **Modelos activos**: [`ActiveModelTrait`], [`ActiveValue`] ([`ActiveValue::Set`], -//! [`ActiveValue::Unchanged`], [`ActiveValue::NotSet`]). -//! - **Macros de derivación**: [`DeriveEntityModel`], [`DeriveColumn`], [`DerivePrimaryKey`], -//! [`DeriveRelation`], [`EnumIter`]. -//! - **Errores**: [`DbErr`]. -//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE). -//! -//! ## Definir una entidad -//! -//! ```rust -//! use pagetop_seaorm::db::*; -//! -//! #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -//! #[sea_orm(table_name = "users")] -//! // El struct debe llamarse `Model`: es un requisito de `DeriveEntityModel`. -//! pub struct Model { -//! #[sea_orm(primary_key)] -//! pub id: i32, -//! pub email: String, -//! pub name: String, -//! } -//! -//! #[derive(Clone, Copy, Debug, EnumIter, DeriveRelation)] -//! pub enum Relation {} -//! -//! // `DeriveEntityModel` genera también `ActiveModel`, `Entity`, `Column` y `PrimaryKey`. -//! impl ActiveModelBehavior for ActiveModel {} -//! ``` -//! -//! ## Operaciones CRUD +//! 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::*; -//! -//! // El código asume que existe un módulo `user` con una entidad SeaORM definida. -//! -//! async fn example() -> Result<(), DbErr> { -//! // Buscar todos los registros. -//! let users = user::Entity::find().all(dbconn()).await?; -//! -//! // Buscar con filtro. -//! let alices = user::Entity::find() -//! .filter(user::Column::Name.eq("Alice")) -//! .all(dbconn()) -//! .await?; -//! -//! // Buscar por clave primaria. -//! let found = user::Entity::find_by_id(1).one(dbconn()).await?; -//! -//! // Insertar. -//! let model = user::ActiveModel { -//! name: ActiveValue::Set("Alice".into()), -//! ..Default::default() -//! }; -//! user::Entity::insert(model).exec(dbconn()).await?; -//! -//! // Actualizar: campos con `ActiveValue::Set`, clave primaria con `ActiveValue::Unchanged`. -//! let patch = user::ActiveModel { -//! id: ActiveValue::Unchanged(1), -//! name: ActiveValue::Set("Bob".into()), -//! ..Default::default() -//! }; -//! patch.update(dbconn()).await?; -//! -//! // Eliminar por clave primaria. -//! user::Entity::delete_by_id(1).exec(dbconn()).await?; -//! -//! // Transacción. `Box::pin` es necesario: `TransactionTrait` exige `Pin>`. -//! dbconn().transaction::<_, DbErr, _>(|txn| Box::pin(async move { -//! user::Entity::insert( -//! user::ActiveModel { name: ActiveValue::Set("Carol".into()), ..Default::default() } -//! ).exec(txn).await?; -//! user::Entity::delete_by_id(2).exec(txn).await?; -//! Ok(()) -//! })).await?; -//! Ok(()) -//! } //! ``` //! -//! Para migraciones y definición de esquemas usa [`migration`](crate::migration). -//! -//! ## Acceso completo a SeaORM -//! -//! El módulo [`api`] re-exporta el crate `sea_orm` íntegro bajo ese alias. Úsalo cuando necesites -//! un tipo o función que no esté expuesto directamente en `db::*`: -//! -//! ```rust -//! use pagetop_seaorm::db::api; -//! -//! // Tipos o utilidades no incluidos en db::*: -//! let _: api::DatabaseBackend = api::DatabaseBackend::Sqlite; -//! ``` -//! -//! ## Construcción de consultas en tiempo de ejecución -//! -//! El módulo [`query`] re-exporta `sea_query` para construir las sentencias SQL que se pasan a -//! [`fetch_all`] y [`fetch_one`]. Es el compañero natural de esas funciones dentro del módulo `db`: -//! -//! ```rust -//! use pagetop_seaorm::db::*; -//! use pagetop_seaorm::db::query::*; -//! -//! async fn example() -> Result<(), DbErr> { -//! let stmt = Query::select() -//! .column(Asterisk) -//! .from(Alias::new("users")) -//! .to_owned(); -//! let rows = fetch_all(&stmt).await?; -//! Ok(()) -//! } -//! ``` +//! Para definir el esquema de la base de datos o escribir migraciones usa además +//! [`crate::migration`]. pub use sea_orm::prelude::*; -pub use sea_orm::{ - ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait, +use sea_orm::sea_query::{ + MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder, }; +use sea_orm::{DatabaseBackend, ExecResult, Statement}; -/// 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. +/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades. /// -/// Útil para tipos o utilidades que no están expuestos directamente en [`db::*`](self). La inmensa -/// mayoría de operaciones no necesitan este módulo; `db::*` cubre los casos habituales. -#[doc(inline)] -pub use sea_orm as api; - -/// Re-exporta `sea_query` para construir sentencias SQL en tiempo de ejecución. -/// -/// Proporciona los constructores de consultas (`Query`, `Expr`, `Alias`, ...) que se pasan a -/// [`fetch_all`] y [`fetch_one`]. Aunque [`migration`](crate::migration) expone las mismas -/// herramientas en el contexto de la definición de esquemas, `query` las sitúa donde corresponde -/// cuando se trata del acceso a la base de datos en tiempo de ejecución. -#[doc(inline)] -pub use sea_orm::sea_query as query; - -/// Devuelve una referencia estática al pool de conexiones. -/// -/// El pool se inicializa una sola vez al arrancar la aplicación; las llamadas posteriores devuelven -/// la misma referencia sin coste apreciable. Se puede invocar tantas veces como sea necesario sin -/// penalización. +/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades +/// 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. /// /// ```rust,no_run /// use pagetop_seaorm::db::*; /// -/// let _conn: &DatabaseConnection = dbconn(); +/// // 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(); /// ``` -#[inline] -pub fn dbconn() -> &'static DatabaseConnection { +pub fn connection() -> &'static DatabaseConnection { &super::DBCONN } -/// Ejecuta una sentencia SQL en crudo y devuelve su resultado. +/// Ejecuta una consulta para devolver todas las filas resultantes. /// -/// No construye la sentencia (INSERT, UPDATE, DELETE), sino que la recibe como una cadena ya -/// formada. Útil para SQL que el sistema de entidades no cubre. El [`ExecResult`] devuelto expone -/// [`rows_affected`](ExecResult::rows_affected) y [`last_insert_id`](ExecResult::last_insert_id) -/// (fiable en MySQL y SQLite; en PostgreSQL devuelve `0`, usa `RETURNING` con el sistema de -/// entidades si necesitas el id insertado). +/// 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 +/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con +/// [`QueryResult::try_get`]. /// -/// > **Nota:** no sirve para SELECT porque no devuelve filas. Para leer datos usa [`fetch_all`] o -/// > [`fetch_one`]. +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// use pagetop_seaorm::migration::*; /// -/// > **Advertencia:** nunca interpoles valores externos en la cadena SQL directamente. Para -/// > sentencias con parámetros de usuario usa el sistema de entidades. +/// async fn example() -> Result<(), DbErr> { +/// let mut stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .to_owned(); +/// let rows = fetch_all(&mut stmt).await?; +/// for row in rows { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_all(stmt: &mut Q) -> Result, DbErr> { + let dbconn = &*super::DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_all(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +/// Ejecuta una consulta y devuelve sólo la primera fila, si existe. /// -/// ```rust +/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve +/// `None` si la consulta no produce resultados. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// use pagetop_seaorm::migration::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let mut stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .and_where(Expr::col(Alias::new("id")).eq(1)) +/// .to_owned(); +/// if let Some(row) = fetch_one(&mut stmt).await? { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_one( + stmt: &mut Q, +) -> Result, DbErr> { + let dbconn = &*super::DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_one(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de +/// la operación. +/// +/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe +/// 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. +/// +/// ```rust,no_run /// use pagetop_seaorm::db::*; /// /// async fn example() -> Result<(), DbErr> { @@ -203,106 +132,9 @@ pub fn dbconn() -> &'static DatabaseConnection { /// } /// ``` pub async fn execute(stmt: impl Into) -> Result { - let conn = dbconn(); - let backend = conn.get_database_backend(); - conn.execute(api::Statement::from_string(backend, stmt.into())) + let dbconn = &*super::DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .execute(Statement::from_string(dbbackend, stmt.into())) .await } - -/// Ejecuta una consulta para devolver todas las filas resultantes. -/// -/// Acepta cualquier tipo que implemente [`query::QueryStatementWriter`] (p. ej. -/// [`query::SelectStatement`]) y serializa la sentencia para el motor de base de datos usado antes -/// de ejecutarla. Cada fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con -/// [`QueryResult::try_get`]. -/// -/// Usa esta función cuando la consulta SELECT no mapea una entidad concreta (JOINs, agregaciones, -/// proyecciones parciales) o cuando necesitas control total sobre el SQL generado. Para sentencias -/// que modifican datos (INSERT, UPDATE, DELETE), usa [`execute`]. Para consultas que sí mapean a -/// una entidad, es preferible `Entity::find().all(dbconn())`. -/// -/// Los valores se integran como literales escapados, no como parámetros de base de datos. Para -/// datos procedentes del usuario, el sistema de entidades es más robusto. -/// -/// ```rust -/// use pagetop_seaorm::db::*; -/// use pagetop_seaorm::db::query::*; -/// -/// async fn example() -> Result<(), DbErr> { -/// let stmt = Query::select() -/// .column(Asterisk) -/// .from(Alias::new("users")) -/// .to_owned(); -/// let rows = fetch_all(&stmt).await?; -/// for row in rows { -/// let name: String = row.try_get("", "name")?; -/// println!("{name}"); -/// } -/// Ok(()) -/// } -/// ``` -pub async fn fetch_all( - stmt: &Q, -) -> Result, DbErr> { - let conn = dbconn(); - let backend = conn.get_database_backend(); - conn.query_all(api::Statement::from_string( - backend, - match backend { - api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder), - api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder), - api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder), - }, - )) - .await -} - -/// Ejecuta una consulta y devuelve sólo la primera fila, si existe. -/// -/// Funciona igual que [`fetch_all`] pero devuelve la primera fila si existe, o `None` si la -/// consulta no produce resultados. Está diseñada para sentencias SELECT; para modificar datos sin -/// entidad mapeada, usa [`execute`]. -/// -/// Si la consulta puede devolver varias filas, se recomienda incluir `LIMIT 1` en la sentencia -/// para que el motor detenga la búsqueda en cuanto encuentre la primera fila y no recupere -/// resultados que se descartarán de todas formas. -/// -/// Usa esta función cuando la consulta SELECT no mapea una entidad concreta (JOINs, agregaciones, -/// proyecciones parciales) o cuando necesitas control total sobre el SQL generado. Para consultas -/// que sí mapean a una entidad, es preferible `Entity::find().one(dbconn())`. -/// -/// Los valores se integran como literales escapados, no como parámetros de base de datos. Para -/// datos procedentes del usuario, el sistema de entidades es más robusto. -/// -/// ```rust -/// use pagetop_seaorm::db::*; -/// use pagetop_seaorm::db::query::*; -/// -/// async fn example() -> Result<(), DbErr> { -/// let stmt = Query::select() -/// .column(Asterisk) -/// .from(Alias::new("users")) -/// .and_where(Expr::col(Alias::new("id")).eq(1)) -/// .to_owned(); -/// if let Some(row) = fetch_one(&stmt).await? { -/// let name: String = row.try_get("", "name")?; -/// println!("{name}"); -/// } -/// Ok(()) -/// } -/// ``` -pub async fn fetch_one( - stmt: &Q, -) -> Result, DbErr> { - let conn = dbconn(); - let backend = conn.get_database_backend(); - conn.query_one(api::Statement::from_string( - backend, - match backend { - api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder), - api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder), - api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder), - }, - )) - .await -} diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index ef056f64..8af917ab 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -46,9 +46,6 @@ opcional; si se omite se usa el puerto predeterminado del motor. ```rust,ignore use pagetop::prelude::*; -use pagetop_seaorm::install_migrations; - -mod migration; struct MyApp; @@ -60,7 +57,7 @@ impl Extension for MyApp { } fn initialize(&self) { - install_migrations!(m20240101_000001_create_users); + install_migrations!(m20240101_000001_create_users_table); } } @@ -70,10 +67,9 @@ async fn main() -> std::io::Result<()> { } ``` -**Escribe las migraciones** usando la API de [`migration`]: +**Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -// src/migration/m20240101_000001_create_users.rs use pagetop_seaorm::migration::*; pub struct Migration; @@ -86,7 +82,6 @@ impl MigrationTrait for Migration { table_auto(Users::Table) .col(pk_auto(Users::Id)) .col(string_uniq(Users::Email)) - .col(string(Users::Name)) .to_owned(), ) .await @@ -98,52 +93,6 @@ enum Users { Table, Id, Email, - Name, -} -``` - -**Define las entidades** en un módulo `entity/` usando las macros de derivación de [`db`]: - -```rust,no_run -// src/entity/user.rs -use pagetop_seaorm::db::*; - -#[derive(Clone, Debug, DeriveEntityModel, PartialEq)] -#[sea_orm(table_name = "users")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - pub email: String, - pub name: String, -} - -#[derive(Clone, Copy, Debug, DeriveRelation, EnumIter)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} -``` - -**Opera con la base de datos** pasando la conexión [`db::dbconn()`] a cada consulta: - -```rust,ignore -use pagetop_seaorm::db::*; - -// Asumiendo que existe un módulo `user` con la entidad definida arriba. -async fn example() -> Result<(), DbErr> { - // Listar todos los registros: - let users = user::Entity::find().all(dbconn()).await?; - - // Buscar por clave primaria: - let found = user::Entity::find_by_id(1).one(dbconn()).await?; - - // Insertar un registro: - let new_user = user::ActiveModel { - email: Set("alice@example.com".to_owned()), - name: Set("Alice".to_owned()), - ..Default::default() - }; - user::Entity::insert(new_user).exec(dbconn()).await?; - Ok(()) } ``` */ @@ -167,18 +116,7 @@ pub mod db; pub mod migration; -// Ejecuta un *future* de forma síncrona dentro del runtime de Tokio. -// -// 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(future: F) -> F::Output { - tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(future)) -} +pub(crate) use futures::executor::block_on as run_now; pub(crate) static DBCONN: LazyLock = LazyLock::new(|| { trace::info!( @@ -187,48 +125,51 @@ pub(crate) static DBCONN: LazyLock = LazyLock::new(|| { &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"); + 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()) - .expect("Failed to set db_user in connection URL"); + .unwrap(); // 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"); + .unwrap(); 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.set_port(Some(port)).unwrap(); } - tmp_uri.to_string() - } - config::DbType::Sqlite => { - format!("sqlite://{}", &config::SETTINGS.database.db_name) + 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::({ - let mut db_opt = ConnectOptions::new(db_uri); + let mut db_opt = ConnectOptions::new(db_uri.to_string()); db_opt.max_connections(config::SETTINGS.database.max_pool_size); db_opt })) - .expect("Failed to connect to database") + .unwrap_or_else(|_| panic!("Failed to connect to database")) }); /// Implementa la extensión. diff --git a/extensions/pagetop-seaorm/src/migration.rs b/extensions/pagetop-seaorm/src/migration.rs index f3544f7f..14b8f85b 100644 --- a/extensions/pagetop-seaorm/src/migration.rs +++ b/extensions/pagetop-seaorm/src/migration.rs @@ -1,116 +1,15 @@ //! API para definir y ejecutar migraciones de base de datos. //! -//! Cuando una extensión necesita persistir datos en una base de datos usando `pagetop_seaorm`, -//! define sus migraciones en un submódulo `migration/` y las aplica al arrancar con la macro -//! [`install_migrations!`](crate::install_migrations). +//! 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. //! -//! Con una sola importación tienes todo lo necesario: -//! -//! ```rust +//! ```rust,ignore +//! use pagetop_seaorm::db::*; //! use pagetop_seaorm::migration::*; //! ``` -//! -//! # Convención de nombrado -//! -//! Cada migración es un módulo con el formato `m__`. El -//! prefijo numérico garantiza el orden cronológico de aplicación: -//! -//! ```text -//! src/ -//! └── migration/ -//! ├── m20240101_000001_create_users.rs -//! └── m20240115_000002_add_email_index.rs -//! ``` -//! -//! # Estructura de una migración -//! -//! Cada archivo define un *struct* `Migration` que implementa [`MigrationTrait`]. El método `up` -//! aplica el cambio; `down` lo revierte. Si no se implementa, devuelve un error; es **obligatorio** -//! implementarlo si la extensión usa [`uninstall_migrations!`](crate::uninstall_migrations): -//! -//! ```rust,no_run -//! // src/migration/m20240101_000001_create_users.rs -//! use pagetop_seaorm::migration::*; -//! -//! pub struct Migration; -//! -//! #[async_trait::async_trait] -//! impl MigrationTrait for Migration { -//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { -//! manager -//! .create_table( -//! table_auto(Users::Table) -//! .col(pk_auto(Users::Id)) -//! .col(string_uniq(Users::Email)) -//! .col(string(Users::Name)) -//! .to_owned(), -//! ) -//! .await -//! } -//! } -//! -//! #[derive(DeriveIden)] -//! enum Users { -//! Table, -//! Id, -//! Email, -//! Name, -//! } -//! ``` -//! -//! # Seguimiento automático -//! -//! Las migraciones se mantienen en una tabla `seaql_migrations` de la base de datos. Cada migración -//! aplicada queda registrada con su nombre y su marca de tiempo. Las migraciones ya aplicadas se -//! omiten en ejecuciones posteriores. -//! -//! # Operaciones con `SchemaManager` -//! -//! El parámetro `manager` que recibe cada migración expone los métodos necesarios para -//! modificar el esquema. Estos son los más habituales: -//! -//! | Método | Acción | -//! |-----------------------------------------------|---------------------------------| -//! | [`SchemaManager::create_table`] | Crea una tabla | -//! | [`SchemaManager::drop_table`] | Elimina una tabla | -//! | [`SchemaManager::alter_table`] | Modifica una tabla existente | -//! | [`SchemaManager::rename_table`] | Renombra una tabla | -//! | [`SchemaManager::truncate_table`] | Vacía una tabla | -//! | [`SchemaManager::create_index`] | Crea un índice | -//! | [`SchemaManager::drop_index`] | Elimina un índice | -//! | [`SchemaManager::create_foreign_key`] | Crea una clave foránea | -//! | [`SchemaManager::drop_foreign_key`] | Elimina una clave foránea | -//! | [`SchemaManager::has_table`] | Comprueba si existe una tabla | -//! | [`SchemaManager::has_column`] | Comprueba si existe una columna | -//! | [`SchemaManager::has_index`] | Comprueba si existe un índice | -//! | [`SchemaManager::create_type`] *(PostgreSQL)* | Crea un tipo personalizado | -//! | [`SchemaManager::alter_type`] *(PostgreSQL)* | Modifica un tipo personalizado | -//! | [`SchemaManager::drop_type`] *(PostgreSQL)* | Elimina un tipo personalizado | -//! -//! # Funciones de esquema -//! -//! Las funciones de esquema disponibles simplifican la definición de columnas. Siguen el patrón -//! `(col)` (NOT NULL), `_null(col)` (nulable) y `_uniq(col)` (NOT NULL + UNIQUE). -//! Algunos ejemplos: -//! -//! | Función | SQL equivalente | -//! |---------------------|-----------------------------------------------------| -//! | `table_auto(tabla)` | `CREATE TABLE IF NOT EXISTS` + timestamps | -//! | `pk_auto(col)` | `INTEGER NOT NULL PRIMARY KEY` (con autoincremento) | -//! | `pk_uuid(col)` | `UUID NOT NULL PRIMARY KEY` | -//! | `string(col)` | `VARCHAR NOT NULL` | -//! | `string_null(col)` | `VARCHAR NULL` | -//! | `string_uniq(col)` | `VARCHAR NOT NULL UNIQUE` | -//! | `integer(col)` | `INTEGER NOT NULL` | -//! | `boolean(col)` | `BOOLEAN NOT NULL` | -//! | `timestamp(col)` | `TIMESTAMP NOT NULL` | -//! | `uuid(col)` | `UUID NOT NULL` | -//! -//! Estas son sólo las funciones más habituales. El módulo [`schema`] define la lista completa, con -//! variantes para `decimal`, `date`, `time`, `json`, `blob`, `binary`, `array`, `enumeration`, -//! `char`, `interval` y sus formas `_null` y `_uniq`. -// **< Adaptación de `sea-orm-migration/lib.rs` (ver §Créditos en README.md) >********************** +// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >***************************** //pub mod cli; pub mod connection; @@ -121,14 +20,10 @@ pub mod schema; pub mod seaql_migrations; //pub mod util; -#[doc(inline)] pub use connection::*; -#[doc(inline)] pub use manager::*; //pub use migrator::*; -/// Permite implementar *traits* con métodos `async`: -#[doc(inline)] pub use async_trait; //pub use sea_orm; //pub use sea_orm::sea_query; @@ -154,84 +49,54 @@ pub trait MigrationTrait: MigrationName + Send + Sync { // ************************************************************************************************* -#[doc(inline)] pub use migrator::MigratorTrait; -#[doc(inline)] pub use schema::*; -pub use sea_orm::DeriveIden; pub use sea_orm::sea_query::*; +pub use sea_orm::DeriveIden; use pagetop::core::TypeInfo; +use pagetop::trace; impl MigrationName for M { fn name(&self) -> &str { - // Extrae el módulo contenedor, descartando el segmento final "Migration". TypeInfo::NameTo(-2).of::() } } -/// Elemento de migración listo para incluir en la lista de un [`MigratorTrait`]. pub type MigrationItem = Box; -/// Interfaz síncrona para ejecutar migraciones desde código no asíncrono. -/// -/// Todo tipo que implemente [`MigratorTrait`] obtiene esta interfaz automáticamente, incluidos los -/// tipos generados por los macros [`install_migrations!`](crate::install_migrations) y -/// [`uninstall_migrations!`](crate::uninstall_migrations). pub trait MigratorBase { - /// Ejecuta las migraciones pendientes en orden ascendente. - /// - /// Provoca un `panic!` si alguna migración falla, evitando que la aplicación arranque con un - /// esquema de base de datos inconsistente. fn run_up(); - /// Revierte todas las migraciones en orden descendente. - /// - /// Provoca un `panic!` si alguna reversión falla. fn run_down(); } +#[rustfmt::skip] impl MigratorBase for M { fn run_up() { - let conn = SchemaManagerConnection::Connection(&super::DBCONN); - if let Err(e) = super::run_now(Self::up(conn, None)) { - panic!("Migration upgrade failed: {e}"); - } + if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) { + trace::error!("Migration upgrade failed ({})", e); + }; } fn run_down() { - let conn = SchemaManagerConnection::Connection(&super::DBCONN); - if let Err(e) = super::run_now(Self::down(conn, None)) { - panic!("Migration downgrade failed: {e}"); - } + 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 nombres de módulo de migración y ejecuta el método `up` de los que aún no esté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). /// -/// **Requisito:** cada módulo de migración debe declararse como submódulo público bajo un módulo -/// `migration` accesible desde el punto de llamada, y exportar `pub struct Migration` que -/// implemente [`MigrationTrait`]. El macro genera rutas de la forma -/// `migration::::Migration`. Estructura mínima: -/// -/// En `src/migration.rs`: /// ```rust,ignore -/// pub mod m20240101_000001_create_users; -/// pub mod m20240115_000002_add_email_index; -/// ``` -/// -/// En `src/lib.rs`: -/// ```rust,ignore -/// mod migration; -/// /// impl Extension for MyExt { /// fn initialize(&self) { /// install_migrations!( -/// m20240101_000001_create_users, +/// m20240101_000001_create_users_table, /// m20240115_000002_add_email_index, /// ); /// } @@ -258,21 +123,14 @@ macro_rules! install_migrations { /// 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 el método `down` de alguna migración -/// devuelve un error, provoca un `panic!`. Complementario a -/// [`install_migrations!`](crate::install_migrations). +/// 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`]. /// -/// **Requisito:** los módulos de migración deben declararse como en -/// [`install_migrations!`](crate::install_migrations). Todos los módulos indicados **deben -/// implementar `down`**; la implementación por defecto devuelve error, lo que provoca un pánico en -/// `run_down`. -/// -/// En `src/lib.rs`: /// ```rust,ignore /// impl Extension for MyExt { /// fn uninitialize(&self) { /// uninstall_migrations!( -/// m20240101_000001_create_users, +/// m20240101_000001_create_users_table, /// m20240115_000002_add_email_index, /// ); /// } diff --git a/extensions/pagetop-seaorm/src/migration/connection.rs b/extensions/pagetop-seaorm/src/migration/connection.rs index a34acc47..7c937a37 100644 --- a/extensions/pagetop-seaorm/src/migration/connection.rs +++ b/extensions/pagetop-seaorm/src/migration/connection.rs @@ -1,8 +1,8 @@ +use futures::Future; use sea_orm::{ AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait, }; -use std::future::Future; use std::pin::Pin; pub enum SchemaManagerConnection<'c> { diff --git a/extensions/pagetop-seaorm/src/migration/manager.rs b/extensions/pagetop-seaorm/src/migration/manager.rs index 91d4b100..3f962bdf 100644 --- a/extensions/pagetop-seaorm/src/migration/manager.rs +++ b/extensions/pagetop-seaorm/src/migration/manager.rs @@ -1,9 +1,9 @@ use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; use sea_orm::sea_query::{ + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, TableTruncateStatement, - extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, }; use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; #[allow(unused_imports)] diff --git a/extensions/pagetop-seaorm/src/migration/migrator.rs b/extensions/pagetop-seaorm/src/migration/migrator.rs index d10b4edb..45ecdbac 100644 --- a/extensions/pagetop-seaorm/src/migration/migrator.rs +++ b/extensions/pagetop-seaorm/src/migration/migrator.rs @@ -1,14 +1,14 @@ +use futures::Future; use std::collections::HashSet; use std::fmt::Display; -use std::future::Future; use std::pin::Pin; use std::time::SystemTime; use pagetop::trace::info; use sea_orm::sea_query::{ - self, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, SelectStatement, SimpleExpr, - Table, extension::postgres::Type, + self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, + SelectStatement, SimpleExpr, Table, }; use sea_orm::{ ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden, @@ -18,10 +18,10 @@ use sea_orm::{ #[allow(unused_imports)] use sea_schema::probe::SchemaProbe; -use super::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations}; +use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; -/// Status of migration #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Status of migration pub enum MigrationStatus { /// Not yet applied Pending, diff --git a/extensions/pagetop-seaorm/src/migration/schema.rs b/extensions/pagetop-seaorm/src/migration/schema.rs index 0ad92946..4dea5ee2 100644 --- a/extensions/pagetop-seaorm/src/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/migration/schema.rs @@ -1,14 +1,13 @@ -//! Adapted from +//! Adaptación de //! -//! # Database Table Schema Helpers +//! # Ayudantes de esquema de base de datos //! -//! This module defines functions and helpers for creating database table -//! schemas using the `sea-orm` and `sea-query` libraries. +//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`. //! -//! # Example +//! # Ejemplo //! -//! The following example shows how the user migration file should be and using -//! the schema helpers to create the Db fields. +//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes +//! de esquema. //! //! ```rust //! use pagetop_seaorm::migration::*; @@ -598,7 +597,7 @@ pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { 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 { let mut t = t; t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) @@ -606,7 +605,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { .take() } -/// Create an Alias. +/// Crea un alias. pub fn name>(name: T) -> Alias { Alias::new(name) } diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 63349aa0..a0796a0e 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -461,9 +461,6 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { /// Define funciones de prueba asíncronas para usar con PageTop. /// -/// Usa el *runtime* multi-hilo de **Tokio**, igual que [`#[pagetop::main]`](macro@main), para -/// garantizar compatibilidad con extensiones que ejecutan código asíncrono de forma síncrona. -/// /// # Ejemplo /// /// ```rust,ignore @@ -475,7 +472,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { let mut output: TokenStream = (quote! { - #[::tokio::test(flavor = "multi_thread")] + #[::tokio::test] }) .into(); diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs index 89ff3269..1cd689cb 100644 --- a/tests/component_poweredby.rs +++ b/tests/component_poweredby.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -/// Inicializa PageTop (locale, extensiones...) una sola vez para toda la suite. +/// Inicializa PageTop (locale, extensiones…) una sola vez para toda la suite. /// /// Los tests de este módulo renderizan componentes directamente con `Context::default()`, por lo /// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router.