WIP: Añade nueva extensión para dar soporte a bases de datos #12

Draft
manuelcillero wants to merge 12 commits from pagetop-seaorm-support into main
12 changed files with 279 additions and 217 deletions
Showing only changes of commit 026448e511 - Show all commits

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,16 @@
//! Adapted from <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
//! Adaptación de <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
//!
//! # Database Table Schema Helpers
//! # Ayudantes de esquema de base de datos
//!
//! This module defines functions and helpers for creating database table
//! 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::db::*;
//! use pagetop_seaorm::migration::*;
//!
//! pub struct Migration;
//!
@ -38,7 +37,7 @@
//! }
//! }
//!
//! #[derive(Iden)]
//! #[derive(DeriveIden)]
//! pub enum Users {
//! Table,
//! Id,
@ -51,10 +50,9 @@
//! }
//! ```
use crate::db::Iden;
use sea_orm::sea_query::{
self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement,
self, Alias, ColumnDef, ColumnType, Expr, Iden, IntoIden, PgInterval, Table,
TableCreateStatement,
};
#[derive(Iden)]
@ -599,7 +597,7 @@ pub fn array_uniq<T: IntoIden>(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()))
@ -607,7 +605,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
.take()
}
/// Create an Alias.
/// Crea un alias.
pub fn name<T: Into<String>>(name: T) -> Alias {
Alias::new(name)
}