diff --git a/migra/Cargo.toml b/migra/Cargo.toml index 2346e80..74dc7d2 100644 --- a/migra/Cargo.toml +++ b/migra/Cargo.toml @@ -1,8 +1,15 @@ [package] name = "migra" -version = "0.1.0" +version = "1.0.0" authors = ["Dmitriy Pleshevskiy "] edition = "2018" +description = "Migra is a simple library for managing SQL in your application" +homepage = "https://github.com/pleshevskiy/migra" +repository = "https://github.com/pleshevskiy/migra" +license = "MIT OR Apache-2.0" +keywords = ["migration", "sql", "manager"] +categories = ["accessibility", "database"] +readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/migra/README.md b/migra/README.md new file mode 100644 index 0000000..0284a2e --- /dev/null +++ b/migra/README.md @@ -0,0 +1,55 @@ +# Migra + +[![CI](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/pleshevskiy/migra/actions/workflows/rust.yml) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) +[![Crates.io](https://img.shields.io/crates/v/migra)](https://crates.io/crates/migra) +![Crates.io](https://img.shields.io/crates/l/migra) + +Migra is a simple library for managing SQL in your application. + +For example, if you have a task list application, you can update the local user database from version to version. + +This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web +servers in any program language without being bound to SQL frameworks. + + +### Installation + +Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`. + +This crate has not required predefined database clients in features with similar name. +If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`). + +`Cargo.toml` example: + +```toml +[package] +name = "my-crate" +version = "0.1.0" +authors = ["Me "] + +[dependencies] +migra = { version = "1.0", features = ["postgres"] } +``` + +### Usage + +For more information about the crate, please read doc. + +### Supported databases + +| Database | Feature | +|----------|--------------| +| Postgres | postgres | +| MySQL | mysql | +| Sqlite | sqlite | + + +## License + +Licensed under either of these: + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE_APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE_MIT) or + https://opensource.org/licenses/MIT) diff --git a/migra/src/clients/mod.rs b/migra/src/clients/mod.rs index 9291b69..15c75d4 100644 --- a/migra/src/clients/mod.rs +++ b/migra/src/clients/mod.rs @@ -1,37 +1,39 @@ -// #![deny(missing_debug_implementations)] -// #![deny(clippy::all, clippy::pedantic)] -// #![allow(clippy::module_name_repetitions)] -// #![allow(clippy::missing_errors_doc)] - use crate::errors::MigraResult; use crate::managers::{ManageMigrations, ManageTransaction}; +/// A trait that helps to open a connection to a specific database client. pub trait OpenDatabaseConnection where Self: Sized, { + /// Open database connection with predefined migrations table name. fn new(connection_string: &str) -> MigraResult { Self::manual(connection_string, "migrations") } + /// Open database connection manually with additional migration table name parameter. fn manual(connection_string: &str, migrations_table_name: &str) -> MigraResult; } +/// All client implementations that have migration and transaction manager implementations +/// are considered clients. pub trait Client: ManageMigrations + ManageTransaction {} +/// If you have complex application mechanics that allow users to choose which +/// database they can use, then you will most likely need this helper for that. pub type AnyClient = Box<(dyn Client + 'static)>; #[cfg(feature = "postgres")] -pub mod postgres; +mod postgres; #[cfg(feature = "postgres")] pub use self::postgres::Client as PostgresClient; #[cfg(feature = "mysql")] -pub mod mysql; +mod mysql; #[cfg(feature = "mysql")] pub use self::mysql::Client as MysqlClient; #[cfg(feature = "sqlite")] -pub mod sqlite; +mod sqlite; #[cfg(feature = "sqlite")] pub use self::sqlite::Client as SqliteClient; diff --git a/migra/src/clients/mysql.rs b/migra/src/clients/mysql.rs index f3c9105..f26f295 100644 --- a/migra/src/clients/mysql.rs +++ b/migra/src/clients/mysql.rs @@ -5,6 +5,9 @@ use crate::migration; use mysql::prelude::*; use mysql::{Pool, PooledConn}; +/// Predefined `MySQL` client. +/// +/// **Note:** Requires enabling `mysql` feature. #[derive(Debug)] pub struct Client { conn: PooledConn, @@ -12,6 +15,7 @@ pub struct Client { } impl Client { + /// Provide access to the original database connection. #[must_use] pub fn conn(&self) -> &PooledConn { &self.conn diff --git a/migra/src/clients/postgres.rs b/migra/src/clients/postgres.rs index ae9f49b..c60ddf6 100644 --- a/migra/src/clients/postgres.rs +++ b/migra/src/clients/postgres.rs @@ -5,12 +5,16 @@ use crate::migration; use postgres::{Client as PostgresClient, NoTls}; use std::fmt; +/// Predefined `Postgres` client. +/// +/// **Note:** Requires enabling `postgres` feature. pub struct Client { conn: PostgresClient, migrations_table_name: String, } impl Client { + /// Provide access to the original database connection. #[must_use] pub fn conn(&self) -> &PostgresClient { &self.conn diff --git a/migra/src/clients/sqlite.rs b/migra/src/clients/sqlite.rs index d4a1d5b..738846e 100644 --- a/migra/src/clients/sqlite.rs +++ b/migra/src/clients/sqlite.rs @@ -4,6 +4,9 @@ use crate::managers::{BatchExecute, ManageMigrations, ManageTransaction}; use crate::migration; use rusqlite::Connection; +/// Predefined `Sqlite` client. +/// +/// **Note:** Requires enabling `sqlite` feature. #[derive(Debug)] pub struct Client { conn: Connection, @@ -11,6 +14,7 @@ pub struct Client { } impl Client { + /// Provide access to the original database connection. #[must_use] pub fn conn(&self) -> &Connection { &self.conn diff --git a/migra/src/errors.rs b/migra/src/errors.rs index 182ce67..1a346a2 100644 --- a/migra/src/errors.rs +++ b/migra/src/errors.rs @@ -1,13 +1,22 @@ use std::fmt; use std::io; +/// A helper type for any standard error. pub type StdError = Box; + +/// A helper type for any result with standard error. pub type StdResult = Result; + +/// A helper type for any result with migra error. pub type MigraResult = Result; +/// Migra error #[derive(Debug)] pub enum Error { + /// Represents database errors. Db(DbError), + + /// Represents standard input output errors. Io(io::Error), } @@ -36,24 +45,41 @@ impl From for Error { } impl Error { + /// Creates a database error. #[must_use] pub fn db(origin: StdError, kind: DbKind) -> Self { Error::Db(DbError { kind, origin }) } } +/// All kinds of errors with witch this crate works. #[derive(Debug)] pub enum DbKind { + /// Failed to database connection. DatabaseConnection, + /// Failed to open transaction. OpenTransaction, + + /// Failed to commit transaction. CommitTransaction, + + /// Failed to rollback transaction. RollbackTransaction, + /// Failed to create a migrations table. CreateMigrationsTable, + + /// Failed to apply SQL. ApplySql, + + /// Failed to insert a migration. InsertMigration, + + /// Failed to delete a migration. DeleteMigration, + + /// Failed to get applied migrations. GetAppliedMigrations, } @@ -73,6 +99,7 @@ impl fmt::Display for DbKind { } } +/// Represents database error. #[derive(Debug)] pub struct DbError { kind: DbKind, @@ -84,3 +111,17 @@ impl fmt::Display for DbError { write!(fmt, "{} - {}", &self.kind, &self.origin) } } + +impl DbError { + /// Returns database error kind. + #[must_use] + pub fn kind(&self) -> &DbKind { + &self.kind + } + + /// Returns origin database error. + #[must_use] + pub fn origin(&self) -> &StdError { + &self.origin + } +} diff --git a/migra/src/fs.rs b/migra/src/fs.rs index 5bfb9e1..157f8fb 100644 --- a/migra/src/fs.rs +++ b/migra/src/fs.rs @@ -3,11 +3,19 @@ use crate::migration; use std::io; use std::path::Path; +/// Checks if the directory is a migration according to the principles of the crate. #[must_use] pub fn is_migration_dir(path: &Path) -> bool { path.join("up.sql").exists() && path.join("down.sql").exists() } +/// Get all migration directories from path and returns as [List]. +/// +/// This utility checks if the directory is a migration. See [`is_migration_dir`] for +/// more information. +/// +/// [List]: migration::List +/// [is_migration_dir]: fs::is_migration_dir pub fn get_all_migrations(dir_path: &Path) -> MigraResult { let mut entries = match dir_path.read_dir() { Err(e) if e.kind() == io::ErrorKind::NotFound => vec![], @@ -24,11 +32,3 @@ pub fn get_all_migrations(dir_path: &Path) -> MigraResult { entries.sort(); Ok(migration::List::from(entries)) } - -#[must_use] -pub fn filter_pending_migrations( - all_migrations: &migration::List, - applied_migrations: &migration::List, -) -> migration::List { - all_migrations.exclude(applied_migrations) -} diff --git a/migra/src/lib.rs b/migra/src/lib.rs index b3e4c82..fce3e33 100644 --- a/migra/src/lib.rs +++ b/migra/src/lib.rs @@ -1,13 +1,61 @@ +//! # Migra +//! +//! Migra is a simple library for managing SQL in your application. +//! +//! For example, if you have a task list application, you can update the local user database from version to version. +//! +//! This is main crate for [migra-cli](https://crates.io/crates/migra-cli), which allows you to manege SQL for web +//! servers in any program language without being bound to SQL frameworks. +//! +//! ## Installation +//! +//! Add `migra = { version = "1.0" }` as a dependency in `Cargo.toml`. +//! +//! This crate has not required predefined database clients in features with similar name. +//! If you want to add them, just install crate with additional features (`postgres`, `mysql`, `sqlite`). +//! +//! `Cargo.toml` example: +//! +//! ```toml +//! [package] +//! name = "my-crate" +//! version = "0.1.0" +//! authors = ["Me "] +//! +//! [dependencies] +//! migra = { version = "1.0", features = ["postgres"] } +//! ``` +//! +//! ### Supported databases +//! +//! | Database Client | Feature | +//! |-----------------|--------------| +//! | `Postgres` | postgres | +//! | `MySQL` | mysql | +//! | `Sqlite` | sqlite | +//! #![deny(missing_debug_implementations)] +#![warn(missing_docs)] #![deny(clippy::all, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] +/// Includes additional client tools and contains predefined +/// database clients that have been enabled in the features. pub mod clients; -mod errors; +/// Includes all types of errors that uses in the crate. +pub mod errors; + +/// Includes utilities that use the file system to work. pub mod fs; + +/// Includes all the basic traits that will allow you +/// to create your own client. pub mod managers; + +/// Includes basic structures of migration and migration +/// lists, that are used in managers and fs utils. pub mod migration; pub use errors::{Error, MigraResult as Result, StdResult}; -pub use migration::Migration; +pub use migration::{List as MigrationList, Migration}; diff --git a/migra/src/managers.rs b/migra/src/managers.rs index e05a764..87d80a2 100644 --- a/migra/src/managers.rs +++ b/migra/src/managers.rs @@ -1,47 +1,71 @@ use crate::errors::{DbKind, Error, MigraResult, StdResult}; use crate::migration; +/// Used to execute SQL. +/// +/// Is a super trait for managers. pub trait BatchExecute { + /// Executes sql via original database client fn batch_execute(&mut self, sql: &str) -> StdResult<()>; } +/// Used to manage transaction in the database connection. pub trait ManageTransaction: BatchExecute { + /// Opens transaction in database connection. fn begin_transaction(&mut self) -> MigraResult<()> { self.batch_execute("BEGIN") .map_err(|err| Error::db(err, DbKind::OpenTransaction)) } + /// Cancels (Rollbacks) transaction in database connection. fn rollback_transaction(&mut self) -> MigraResult<()> { self.batch_execute("ROLLBACK") .map_err(|err| Error::db(err, DbKind::RollbackTransaction)) } + /// Apply (Commit) transaction in database connection. fn commit_transaction(&mut self) -> MigraResult<()> { self.batch_execute("COMMIT") .map_err(|err| Error::db(err, DbKind::CommitTransaction)) } } +/// Used to manage migrations in the database connection. pub trait ManageMigrations: BatchExecute { + /// Applies SQL. Similar to [`BatchExecute`], but returns migra [Error]. + /// + /// [BatchExecute]: managers::BatchExecute fn apply_sql(&mut self, sql: &str) -> MigraResult<()> { self.batch_execute(sql) .map_err(|err| Error::db(err, DbKind::ApplySql)) } + /// Creates migration table. fn create_migrations_table(&mut self) -> MigraResult<()>; + /// Inserts new migration to table. fn insert_migration(&mut self, name: &str) -> MigraResult; + /// Deletes migration from table. fn delete_migration(&mut self, name: &str) -> MigraResult; + /// Get applied migrations from table. fn get_applied_migrations(&mut self) -> MigraResult; + /// Applies SQL to upgrade database schema and inserts new migration to table. + /// + /// **Note:** Must be run in a transaction otherwise if the migration causes any + /// error the data in the database may be inconsistent. fn run_upgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> { self.apply_sql(content)?; self.insert_migration(name)?; Ok(()) } + /// Applies SQL to downgrade database schema and deletes migration from table. + /// + /// **Note:** Must be run in a transaction otherwise if the migration causes any + /// error the data in the database may be inconsistent. fn run_downgrade_migration(&mut self, name: &str, content: &str) -> MigraResult<()> { self.apply_sql(content)?; self.delete_migration(name)?; diff --git a/migra/src/migration.rs b/migra/src/migration.rs index 7234477..c0c712b 100644 --- a/migra/src/migration.rs +++ b/migra/src/migration.rs @@ -2,12 +2,14 @@ use crate::errors::MigraResult; use crate::managers::ManageMigrations; use std::iter::FromIterator; +/// A simple wrap over string. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Migration { name: String, } impl Migration { + /// Creates new migration by name. #[must_use] pub fn new(name: &str) -> Self { Migration { @@ -15,12 +17,21 @@ impl Migration { } } + /// Returns name of migration. #[must_use] pub fn name(&self) -> &String { &self.name } } +/// Wrap over migration vector. Can be implicitly converted to a vector and has +/// a few of additional utilities for handling migrations. +/// +/// Can be presented as a list of all migrations, a list of pending migrations +/// or a list of applied migrations, depending on the implementation. +/// +/// +/// #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct List { inner: Vec, @@ -82,19 +93,38 @@ impl std::ops::Deref for List { } impl List { + /// Creates empty migration list. #[must_use] pub fn new() -> Self { List { inner: Vec::new() } } + /// Push migration to list. pub fn push(&mut self, migration: Migration) { self.inner.push(migration) } + /// Push migration name to list. + /// + /// # Example + /// + /// ```rust + /// # let mut list = List::new(); + /// list.push_name("name"); + /// # assert_eq!(list, List::from(vec!["name"])); + /// ``` + /// + /// Is identical to the following + /// ```rust + /// # let mut list = List::new(); + /// list.push(Migration::new("name")); + /// # assert_eq!(list, List::from(vec!["name"])); + /// ``` pub fn push_name(&mut self, name: &str) { self.inner.push(Migration::new(name)) } + /// Check if list contains specific migration. #[must_use] pub fn contains(&self, other_migration: &Migration) -> bool { self.inner @@ -102,11 +132,13 @@ impl List { .any(|migration| migration == other_migration) } + /// Check if list contains migration with specific name. #[must_use] pub fn contains_name(&self, name: &str) -> bool { self.inner.iter().any(|migration| migration.name() == name) } + /// Exclude specific list from current list. #[must_use] pub fn exclude(&self, list: &List) -> List { self.inner @@ -115,6 +147,8 @@ impl List { .collect() } + /// Runs a upgrade migration with SQL content and adds a new migration to the current list + /// If there is no migration migration with specific name in the list. pub fn should_run_upgrade_migration( &mut self, client: &mut dyn ManageMigrations, @@ -131,6 +165,8 @@ impl List { Ok(is_missed) } + /// Runs a downgrade migration with SQL content and removes the last migration from the + /// current list if the last item in the list has the specified name. pub fn should_run_downgrade_migration( &mut self, client: &mut dyn ManageMigrations,