Compare commits
2 commits
dfc1bdbc4c
...
0410b8c060
| Author | SHA1 | Date | |
|---|---|---|---|
| 0410b8c060 | |||
| 830602b24e |
10 changed files with 603 additions and 179 deletions
|
|
@ -45,6 +45,9 @@ opcional; si se omite se usa el puerto predeterminado del motor.
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::install_migrations;
|
||||||
|
|
||||||
|
mod migration;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
|
@ -66,9 +69,10 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Escribe las migraciones** usando la API de SeaORM:
|
**Escribe las migraciones** usando la API de [`migration`]:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
|
// src/migration/m20240101_000001_create_users.rs
|
||||||
use pagetop_seaorm::migration::*;
|
use pagetop_seaorm::migration::*;
|
||||||
|
|
||||||
pub struct Migration;
|
pub struct Migration;
|
||||||
|
|
@ -81,6 +85,7 @@ impl MigrationTrait for Migration {
|
||||||
table_auto(Users::Table)
|
table_auto(Users::Table)
|
||||||
.col(pk_auto(Users::Id))
|
.col(pk_auto(Users::Id))
|
||||||
.col(string_uniq(Users::Email))
|
.col(string_uniq(Users::Email))
|
||||||
|
.col(string(Users::Name))
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -92,6 +97,52 @@ enum Users {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
Email,
|
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(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -125,7 +176,7 @@ extensión. Los ficheros adaptados del original son:
|
||||||
| `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` | Absorbido en `migration.rs`, descarta 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, adaptado de [loco](https://github.com/loco-rs/loco) |
|
||||||
| `seaql_migrations.rs` | Integración completa |
|
| `seaql_migrations.rs` | Integración completa |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,199 @@
|
||||||
//! API completa de SeaORM para operaciones con la base de datos.
|
//! Definición de entidades y acceso a la base de datos.
|
||||||
//!
|
//!
|
||||||
//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…)
|
//! Agrupa los *traits*, macros y tipos del sistema de entidades de SeaORM, junto con las funciones
|
||||||
//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario
|
//! [`dbconn`], [`execute`], [`fetch_all`] y [`fetch_one`], en una sola importación:
|
||||||
//! para definir entidades y realizar operaciones CRUD:
|
|
||||||
//!
|
//!
|
||||||
//! ```rust,ignore
|
//! ```rust
|
||||||
//! use pagetop_seaorm::db::*;
|
//! use pagetop_seaorm::db::*;
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Para definir el esquema de la base de datos o escribir migraciones usa además
|
//! El sistema de entidades (`Entity::find()`, `Entity::insert()`, transacciones) es el camino
|
||||||
//! [`crate::migration`].
|
//! 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
|
||||||
|
//!
|
||||||
|
//! ```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<Box<dyn Future>>`.
|
||||||
|
//! 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(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
pub use sea_orm::prelude::*;
|
pub use sea_orm::prelude::*;
|
||||||
|
|
||||||
use sea_orm::sea_query::{
|
pub use sea_orm::{
|
||||||
MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder,
|
ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait,
|
||||||
};
|
};
|
||||||
use sea_orm::{DatabaseBackend, ExecResult, Statement};
|
|
||||||
|
|
||||||
/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades.
|
/// 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.
|
||||||
///
|
///
|
||||||
/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades
|
/// Útil para tipos o utilidades que no están expuestos directamente en [`db::*`](self). La inmensa
|
||||||
/// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un
|
/// mayoría de operaciones no necesitan este módulo; `db::*` cubre los casos habituales.
|
||||||
/// valor inicializado una sola vez al arrancar la aplicación.
|
#[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.
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use pagetop_seaorm::db::*;
|
/// use pagetop_seaorm::db::*;
|
||||||
///
|
///
|
||||||
/// // Consultas tipadas con el sistema de entidades de SeaORM:
|
/// let _conn: &DatabaseConnection = dbconn();
|
||||||
/// // 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 {
|
#[inline]
|
||||||
|
pub fn dbconn() -> &'static DatabaseConnection {
|
||||||
&super::DBCONN
|
&super::DBCONN
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ejecuta una consulta para devolver todas las filas resultantes.
|
/// Ejecuta una sentencia SQL en crudo y devuelve su resultado.
|
||||||
///
|
///
|
||||||
/// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y
|
/// No construye la sentencia (INSERT, UPDATE, DELETE), sino que la recibe como una cadena ya
|
||||||
/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada
|
/// formada. Útil para SQL que el sistema de entidades no cubre. El [`ExecResult`] devuelto expone
|
||||||
/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con
|
/// [`rows_affected`](ExecResult::rows_affected) y [`last_insert_id`](ExecResult::last_insert_id)
|
||||||
/// [`QueryResult::try_get`].
|
/// (fiable en MySQL y SQLite; en PostgreSQL devuelve `0`, usa `RETURNING` con el sistema de
|
||||||
|
/// entidades si necesitas el id insertado).
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// > **Nota:** no sirve para SELECT porque no devuelve filas. Para leer datos usa [`fetch_all`] o
|
||||||
/// use pagetop_seaorm::db::*;
|
/// > [`fetch_one`].
|
||||||
/// use pagetop_seaorm::migration::*;
|
|
||||||
///
|
///
|
||||||
/// async fn example() -> Result<(), DbErr> {
|
/// > **Advertencia:** nunca interpoles valores externos en la cadena SQL directamente. Para
|
||||||
/// let mut stmt = Query::select()
|
/// > sentencias con parámetros de usuario usa el sistema de entidades.
|
||||||
/// .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<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, 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.
|
|
||||||
///
|
///
|
||||||
/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve
|
/// ```rust
|
||||||
/// `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<Q: QueryStatementWriter>(
|
|
||||||
stmt: &mut Q,
|
|
||||||
) -> Result<Option<QueryResult>, 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::*;
|
/// use pagetop_seaorm::db::*;
|
||||||
///
|
///
|
||||||
/// async fn example() -> Result<(), DbErr> {
|
/// async fn example() -> Result<(), DbErr> {
|
||||||
|
|
@ -132,9 +203,106 @@ 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 = &*super::DBCONN;
|
let conn = dbconn();
|
||||||
let dbbackend = dbconn.get_database_backend();
|
let backend = conn.get_database_backend();
|
||||||
dbconn
|
conn.execute(api::Statement::from_string(backend, stmt.into()))
|
||||||
.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<Q: query::QueryStatementWriter>(
|
||||||
|
stmt: &Q,
|
||||||
|
) -> Result<Vec<QueryResult>, 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<Q: query::QueryStatementWriter>(
|
||||||
|
stmt: &Q,
|
||||||
|
) -> Result<Option<QueryResult>, 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
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ opcional; si se omite se usa el puerto predeterminado del motor.
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::install_migrations;
|
||||||
|
|
||||||
|
mod migration;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
|
@ -57,7 +60,7 @@ impl Extension for MyApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(&self) {
|
fn initialize(&self) {
|
||||||
install_migrations!(m20240101_000001_create_users_table);
|
install_migrations!(m20240101_000001_create_users);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,9 +70,10 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Escribe las migraciones** usando la API de SeaORM:
|
**Escribe las migraciones** usando la API de [`migration`]:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
|
// src/migration/m20240101_000001_create_users.rs
|
||||||
use pagetop_seaorm::migration::*;
|
use pagetop_seaorm::migration::*;
|
||||||
|
|
||||||
pub struct Migration;
|
pub struct Migration;
|
||||||
|
|
@ -82,6 +86,7 @@ impl MigrationTrait for Migration {
|
||||||
table_auto(Users::Table)
|
table_auto(Users::Table)
|
||||||
.col(pk_auto(Users::Id))
|
.col(pk_auto(Users::Id))
|
||||||
.col(string_uniq(Users::Email))
|
.col(string_uniq(Users::Email))
|
||||||
|
.col(string(Users::Name))
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -93,6 +98,52 @@ enum Users {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
Email,
|
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(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
|
|
@ -116,7 +167,18 @@ pub mod db;
|
||||||
|
|
||||||
pub mod migration;
|
pub mod migration;
|
||||||
|
|
||||||
pub(crate) use futures::executor::block_on as run_now;
|
// 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<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(|| {
|
pub(crate) static DBCONN: LazyLock<DatabaseConnection> = LazyLock::new(|| {
|
||||||
trace::info!(
|
trace::info!(
|
||||||
|
|
@ -125,51 +187,48 @@ pub(crate) static DBCONN: LazyLock<DatabaseConnection> = LazyLock::new(|| {
|
||||||
&config::SETTINGS.database.max_pool_size
|
&config::SETTINGS.database.max_pool_size
|
||||||
);
|
);
|
||||||
|
|
||||||
let db_uri = match config::SETTINGS.database.db_type.as_str() {
|
let db_uri: String = match config::SETTINGS.database.db_type {
|
||||||
"mysql" | "postgres" => {
|
config::DbType::Unset => panic!(
|
||||||
let mut tmp_uri = Url::parse(
|
"database.db_type is not configured: set it to \"mysql\", \"postgres\" or \"sqlite\""
|
||||||
format!(
|
),
|
||||||
|
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!(
|
||||||
"{}://{}/{}",
|
"{}://{}/{}",
|
||||||
&config::SETTINGS.database.db_type,
|
scheme,
|
||||||
&config::SETTINGS.database.db_host,
|
&config::SETTINGS.database.db_host,
|
||||||
&config::SETTINGS.database.db_name
|
&config::SETTINGS.database.db_name
|
||||||
)
|
))
|
||||||
.as_str(),
|
.expect("Invalid database URL: check db_host and db_name in config");
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
tmp_uri
|
tmp_uri
|
||||||
.set_username(config::SETTINGS.database.db_user.as_str())
|
.set_username(config::SETTINGS.database.db_user.as_str())
|
||||||
.unwrap();
|
.expect("Failed to set db_user in connection URL");
|
||||||
// https://github.com/launchbadge/sqlx/issues/1624
|
// https://github.com/launchbadge/sqlx/issues/1624
|
||||||
tmp_uri
|
tmp_uri
|
||||||
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
|
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
|
||||||
.unwrap();
|
.expect("Failed to set db_pass in connection URL");
|
||||||
if let Some(port) = config::SETTINGS.database.db_port {
|
if let Some(port) = config::SETTINGS.database.db_port {
|
||||||
tmp_uri.set_port(Some(port)).unwrap();
|
|
||||||
}
|
|
||||||
tmp_uri
|
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)
|
||||||
}
|
}
|
||||||
"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>({
|
run_now(Database::connect::<ConnectOptions>({
|
||||||
let mut db_opt = ConnectOptions::new(db_uri.to_string());
|
let mut db_opt = ConnectOptions::new(db_uri);
|
||||||
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
|
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
|
||||||
db_opt
|
db_opt
|
||||||
}))
|
}))
|
||||||
.unwrap_or_else(|_| panic!("Failed to connect to database"))
|
.expect("Failed to connect to database")
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Implementa la extensión.
|
/// Implementa la extensión.
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,116 @@
|
||||||
//! API para definir y ejecutar migraciones de base de datos.
|
//! API para definir y ejecutar migraciones de base de datos.
|
||||||
//!
|
//!
|
||||||
//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros
|
//! Cuando una extensión necesita persistir datos en una base de datos usando `pagetop_seaorm`,
|
||||||
//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al
|
//! define sus migraciones en un submódulo `migration/` y las aplica al arrancar con la macro
|
||||||
//! arrancar la extensión.
|
//! [`install_migrations!`](crate::install_migrations).
|
||||||
//!
|
//!
|
||||||
//! ```rust,ignore
|
//! Con una sola importación tienes todo lo necesario:
|
||||||
//! use pagetop_seaorm::db::*;
|
//!
|
||||||
|
//! ```rust
|
||||||
//! use pagetop_seaorm::migration::*;
|
//! use pagetop_seaorm::migration::*;
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Convención de nombrado
|
||||||
|
//!
|
||||||
|
//! Cada migración es un módulo con el formato `m<YYYYMMDD>_<NNNNNN>_<nombre_descriptivo>`. 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
|
||||||
|
//! `<tipo>(col)` (NOT NULL), `<tipo>_null(col)` (nulable) y `<tipo>_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` (ver §Créditos en README.md) >*****************************
|
// **< Adaptación de `sea-orm-migration/lib.rs` (ver §Créditos en README.md) >**********************
|
||||||
|
|
||||||
//pub mod cli;
|
//pub mod cli;
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
|
|
@ -20,10 +121,14 @@ pub mod schema;
|
||||||
pub mod seaql_migrations;
|
pub mod seaql_migrations;
|
||||||
//pub mod util;
|
//pub mod util;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
pub use connection::*;
|
pub use connection::*;
|
||||||
|
#[doc(inline)]
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
//pub use migrator::*;
|
//pub use migrator::*;
|
||||||
|
|
||||||
|
/// Permite implementar *traits* con métodos `async`:
|
||||||
|
#[doc(inline)]
|
||||||
pub use async_trait;
|
pub use async_trait;
|
||||||
//pub use sea_orm;
|
//pub use sea_orm;
|
||||||
//pub use sea_orm::sea_query;
|
//pub use sea_orm::sea_query;
|
||||||
|
|
@ -49,54 +154,84 @@ pub trait MigrationTrait: MigrationName + Send + Sync {
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
pub use migrator::MigratorTrait;
|
pub use migrator::MigratorTrait;
|
||||||
|
#[doc(inline)]
|
||||||
pub use schema::*;
|
pub use schema::*;
|
||||||
pub use sea_orm::sea_query::*;
|
|
||||||
pub use sea_orm::DeriveIden;
|
pub use sea_orm::DeriveIden;
|
||||||
|
pub use sea_orm::sea_query::*;
|
||||||
|
|
||||||
use pagetop::core::TypeInfo;
|
use pagetop::core::TypeInfo;
|
||||||
use pagetop::trace;
|
|
||||||
|
|
||||||
impl<M: MigrationTrait> MigrationName for M {
|
impl<M: MigrationTrait> MigrationName for M {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
|
// Extrae el módulo contenedor, descartando el segmento final "Migration".
|
||||||
TypeInfo::NameTo(-2).of::<M>()
|
TypeInfo::NameTo(-2).of::<M>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// 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 {
|
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();
|
fn run_up();
|
||||||
|
|
||||||
|
/// Revierte todas las migraciones en orden descendente.
|
||||||
|
///
|
||||||
|
/// Provoca un `panic!` si alguna reversión falla.
|
||||||
fn run_down();
|
fn run_down();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
impl<M: MigratorTrait> MigratorBase for M {
|
impl<M: MigratorTrait> MigratorBase for M {
|
||||||
fn run_up() {
|
fn run_up() {
|
||||||
if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
|
let conn = SchemaManagerConnection::Connection(&super::DBCONN);
|
||||||
trace::error!("Migration upgrade failed ({})", e);
|
if let Err(e) = super::run_now(Self::up(conn, None)) {
|
||||||
};
|
panic!("Migration upgrade failed: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_down() {
|
fn run_down() {
|
||||||
if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
|
let conn = SchemaManagerConnection::Connection(&super::DBCONN);
|
||||||
trace::error!("Migration downgrade failed ({})", e);
|
if let Err(e) = super::run_now(Self::down(conn, None)) {
|
||||||
};
|
panic!("Migration downgrade failed: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aplica las migraciones pendientes al arrancar una extensión.
|
/// 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
|
/// 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
|
||||||
/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde
|
/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde
|
||||||
/// [`Extension::initialize`](pagetop::core::extension::Extension::initialize).
|
/// [`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::<nombre>::Migration`. Estructura mínima:
|
||||||
|
///
|
||||||
|
/// En `src/migration.rs`:
|
||||||
/// ```rust,ignore
|
/// ```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 {
|
/// impl Extension for MyExt {
|
||||||
/// fn initialize(&self) {
|
/// fn initialize(&self) {
|
||||||
/// install_migrations!(
|
/// install_migrations!(
|
||||||
/// m20240101_000001_create_users_table,
|
/// m20240101_000001_create_users,
|
||||||
/// m20240115_000002_add_email_index,
|
/// m20240115_000002_add_email_index,
|
||||||
/// );
|
/// );
|
||||||
/// }
|
/// }
|
||||||
|
|
@ -123,14 +258,21 @@ macro_rules! install_migrations {
|
||||||
|
|
||||||
/// Revierte las migraciones de una extensión en orden inverso al de su aplicación.
|
/// 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`,
|
/// Ejecuta el método `down` de cada migración indicada. Si el método `down` de alguna migración
|
||||||
/// detiene el proceso con un error. Complementario a [`crate::install_migrations`].
|
/// devuelve un error, provoca un `panic!`. Complementario a
|
||||||
|
/// [`install_migrations!`](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
|
/// ```rust,ignore
|
||||||
/// impl Extension for MyExt {
|
/// impl Extension for MyExt {
|
||||||
/// fn uninitialize(&self) {
|
/// fn uninitialize(&self) {
|
||||||
/// uninstall_migrations!(
|
/// uninstall_migrations!(
|
||||||
/// m20240101_000001_create_users_table,
|
/// m20240101_000001_create_users,
|
||||||
/// m20240115_000002_add_email_index,
|
/// m20240115_000002_add_email_index,
|
||||||
/// );
|
/// );
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use futures::Future;
|
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
|
AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
|
||||||
ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait,
|
ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait,
|
||||||
};
|
};
|
||||||
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
pub enum SchemaManagerConnection<'c> {
|
pub enum SchemaManagerConnection<'c> {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
|
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
|
||||||
use sea_orm::sea_query::{
|
use sea_orm::sea_query::{
|
||||||
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
|
|
||||||
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
|
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
|
||||||
SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement,
|
SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement,
|
||||||
TableRenameStatement, TableTruncateStatement,
|
TableRenameStatement, TableTruncateStatement,
|
||||||
|
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
|
||||||
};
|
};
|
||||||
use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder};
|
use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use futures::Future;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use pagetop::trace::info;
|
use pagetop::trace::info;
|
||||||
|
|
||||||
use sea_orm::sea_query::{
|
use sea_orm::sea_query::{
|
||||||
self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query,
|
self, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, SelectStatement, SimpleExpr,
|
||||||
SelectStatement, SimpleExpr, Table,
|
Table, extension::postgres::Type,
|
||||||
};
|
};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
|
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
|
||||||
|
|
@ -18,10 +18,10 @@ use sea_orm::{
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use sea_schema::probe::SchemaProbe;
|
use sea_schema::probe::SchemaProbe;
|
||||||
|
|
||||||
use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager};
|
use super::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
||||||
/// Status of migration
|
/// Status of migration
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum MigrationStatus {
|
pub enum MigrationStatus {
|
||||||
/// Not yet applied
|
/// Not yet applied
|
||||||
Pending,
|
Pending,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
//! Adaptación de <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
|
//! Adapted from <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
|
||||||
//!
|
//!
|
||||||
//! # Ayudantes de esquema de base de datos
|
//! # Database Table Schema Helpers
|
||||||
//!
|
//!
|
||||||
//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`.
|
//! This module defines functions and helpers for creating database table
|
||||||
|
//! schemas using the `sea-orm` and `sea-query` libraries.
|
||||||
//!
|
//!
|
||||||
//! # Ejemplo
|
//! # Example
|
||||||
//!
|
//!
|
||||||
//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes
|
//! The following example shows how the user migration file should be and using
|
||||||
//! de esquema.
|
//! the schema helpers to create the Db fields.
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use pagetop_seaorm::migration::*;
|
//! use pagetop_seaorm::migration::*;
|
||||||
|
|
@ -597,7 +598,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente.
|
/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table.
|
||||||
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()))
|
||||||
|
|
@ -605,7 +606,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
|
||||||
.take()
|
.take()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un alias.
|
/// Create an 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,9 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
/// Define funciones de prueba asíncronas para usar con PageTop.
|
/// 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
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
|
|
@ -472,7 +475,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let mut output: TokenStream = (quote! {
|
let mut output: TokenStream = (quote! {
|
||||||
#[::tokio::test]
|
#[::tokio::test(flavor = "multi_thread")]
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
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
|
/// 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.
|
/// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue