diff --git a/Cargo.toml b/Cargo.toml index 3755e59e0..fba0a6ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,42 @@ +[workspace] +members = [ + ".", + "sea-query-derive", +] + [package] name = "sea-query" -version = "0.4.0" -authors = ["Billy Chan "] +version = "0.5.0" +authors = [ "Billy Chan " ] edition = "2018" description = "A database agnostic runtime query builder for Rust" license = "MIT OR Apache-2.0" documentation = "https://docs.rs/sea-query" -repository = "https://github.com/SeaQL/Sea-Query" -categories = ["database"] -keywords = ["database", "sql", "mysql", "postgres", "sqlite"] +repository = "https://github.com/SeaQL/sea-query" +categories = [ "database" ] +keywords = [ "database", "sql", "mysql", "postgres", "sqlite" ] [lib] name = "sea_query" +path = "src/lib.rs" [dependencies] serde_json = "1.0" +sea-query-derive = { version = "0.1.0", path = "sea-query-derive", default-features = false, optional = true } [dev-dependencies] async-std = "1.8" sqlx = { version = "0.4.0", default-features = false, features = [ "runtime-async-std-native-tls", "macros", "any", "mysql", "postgres", "sqlite", "tls", "migrate", "decimal" ] } [features] -sqlx-driver = [] +default = [ "derive" ] +sqlx-driver = [ ] +derive = [ "sea-query-derive" ] [[example]] name = "sqlx" -required-features = ["sqlx-driver"] \ No newline at end of file +required-features = [ "sqlx-driver" ] + +[[example]] +name = "derive" +required-features = [ "derive" ] \ No newline at end of file diff --git a/README.md b/README.md index ddfa8c11f..eb9467d89 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,29 @@ impl Iden for Character { } ``` +If you're okay with running another procedural macro, you can activate +the `derive` feature on the crate to save you some boilerplate: + +```rust +use sea_query::Iden; + +// This will implement Iden exactly as shown above +#[derive(Iden)] +pub enum Character { + Table, + Id, + Character, + FontSize, + SizeW, + SizeH, + FontId, +} +``` + +You can also override the generated column names by specifying an `#[iden = ""]` +attribute on the enum or any of its variants; for more information, look at +[the derive example](https://github.com/SeaQL/sea-query/blob/master/examples/derive.rs). + ### Expression Use [`Expr`] to construct select, join, where and having expression in query. diff --git a/examples/derive.rs b/examples/derive.rs new file mode 100644 index 000000000..8ad6d859c --- /dev/null +++ b/examples/derive.rs @@ -0,0 +1,68 @@ +use sea_query::Iden; + +#[derive(Iden)] +enum User { + Table, + Id, + FirstName, + LastName, + Email, +} + +#[derive(Iden)] +// Outer iden attributes overrides what's used for "Table"... +#[iden = "user"] +enum Custom { + Table, + #[iden = "my_id"] + Id, + #[iden = "name"] + FirstName, + #[iden = "surname"] + LastName, + // Custom casing if needed + #[iden = "EMail"] + Email, +} + +#[derive(Iden)] +enum Something { + // ...the Table can also be overwritten like this + #[iden = "something_else"] + Table, + Id, + AssetName, + UserId, +} + +#[derive(Iden)] +pub struct SomeType; +#[derive(Iden)] +#[iden = "another_name"] +pub struct CustomName; + +fn main() { + println!("Default field names"); + assert_eq!(dbg!(Iden::to_string(&User::Table)), "user"); + assert_eq!(dbg!(Iden::to_string(&User::Id)), "id"); + assert_eq!(dbg!(Iden::to_string(&User::FirstName)), "first_name"); + assert_eq!(dbg!(Iden::to_string(&User::LastName)), "last_name"); + assert_eq!(dbg!(Iden::to_string(&User::Email)), "email"); + + println!("Custom field names"); + assert_eq!(dbg!(Iden::to_string(&Custom::Table)), "user"); + assert_eq!(dbg!(Iden::to_string(&Custom::Id)), "my_id"); + assert_eq!(dbg!(Iden::to_string(&Custom::FirstName)), "name"); + assert_eq!(dbg!(Iden::to_string(&Custom::LastName)), "surname"); + assert_eq!(dbg!(Iden::to_string(&Custom::Email)), "EMail"); + + println!("Single custom field name"); + assert_eq!(dbg!(Iden::to_string(&Something::Table)), "something_else"); + assert_eq!(dbg!(Iden::to_string(&Something::Id)), "id"); + assert_eq!(dbg!(Iden::to_string(&Something::AssetName)), "asset_name"); + assert_eq!(dbg!(Iden::to_string(&Something::UserId)), "user_id"); + + println!("Unit structs"); + assert_eq!(dbg!(Iden::to_string(&SomeType)), "some_type"); + assert_eq!(dbg!(Iden::to_string(&CustomName)), "another_name"); +} diff --git a/sea-query-derive/Cargo.toml b/sea-query-derive/Cargo.toml new file mode 100644 index 000000000..2496d89d4 --- /dev/null +++ b/sea-query-derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "sea-query-derive" +version = "0.1.0" +authors = [ "Follpvosten " ] +edition = "2018" +description = "Derive macro for sea-query's Iden trait" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-query" +repository = "https://github.com/SeaQL/sea-query" +categories = [ "database" ] +keywords = [ "database", "sql", "mysql", "postgres", "sqlite" ] + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1", default-features = false, features = [ "derive", "parsing", "proc-macro", "printing" ] } +quote = "1" +sea-query = "0.4" +heck = "0.3" \ No newline at end of file diff --git a/sea-query-derive/src/lib.rs b/sea-query-derive/src/lib.rs new file mode 100644 index 000000000..b2d979976 --- /dev/null +++ b/sea-query-derive/src/lib.rs @@ -0,0 +1,84 @@ +use heck::SnakeCase; +use proc_macro::{self, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::{parse_macro_input, Attribute, DataEnum, DataStruct, DeriveInput, Fields, Meta, Variant}; + +fn get_iden_attr(attrs: &[Attribute]) -> Option { + for attr in attrs { + let name_value = match attr.parse_meta() { + Ok(Meta::NameValue(nv)) => nv, + _ => continue, + }; + if name_value.path.is_ident("iden") { + return Some(name_value.lit); + } + } + None +} + +#[proc_macro_derive(Iden, attributes(iden))] +pub fn derive_iden(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, data, attrs, .. + } = parse_macro_input!(input); + let table_name = match get_iden_attr(&attrs) { + Some(lit) => quote! { #lit }, + None => { + let normalized = ident.to_string().to_snake_case(); + quote! { #normalized } + } + }; + + // Currently we only support enums and unit structs + let variants = + match data { + syn::Data::Enum(DataEnum { variants, .. }) => variants, + syn::Data::Struct(DataStruct { + fields: Fields::Unit, + .. + }) => { + return quote! { + impl sea_query::Iden for #ident { + fn unquoted(&self, s: &mut dyn sea_query::Write) { + write!(s, #table_name).unwrap(); + } + } + } + .into() + } + _ => return quote_spanned! { + ident.span() => compile_error!("you can only derive Iden on enums or unit structs"); + } + .into(), + }; + if variants.is_empty() { + return TokenStream::new(); + } + + let variant = variants + .iter() + .map(|Variant { ident, .. }| quote! { #ident }); + let name = variants.iter().map(|v| { + if let Some(lit) = get_iden_attr(&v.attrs) { + // If the user supplied a name, just adapt it. + return quote! { #lit }; + } + if v.ident == "Table" { + table_name.clone() + } else { + let ident = v.ident.to_string().to_snake_case(); + quote! { #ident } + } + }); + + let output = quote! { + impl sea_query::Iden for #ident { + fn unquoted(&self, s: &mut dyn sea_query::Write) { + write!(s, "{}", match self { + #(Self::#variant => #name),* + }).unwrap(); + } + } + }; + output.into() +} diff --git a/src/lib.rs b/src/lib.rs index 6c3086e9c..7f42cbfef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,33 @@ //! A database agnostic runtime query builder for Rust. //! -//! This library is the foundation of upcoming projects on Document ORM (Sea-ORM) and Database Synchor (Sea-Horse). +//! This library aims to provide an ergonomic API to construct Abstract Syntax Trees for SQL. +//! The AST is generic by design and can be serialized to different SQL variants. +//! We align the behaviour between different engines where appropriate, while offering vendor specific features via extensions. +//! +//! This library is the foundation of upcoming projects: Document ORM (SeaORM) and Database Synchor (SeaHorse). //! //! # Usage //! +//! Table of Content +//! +//! 1. [Iden](#iden) +//! 1. [Expression](#expression) +//! +//! 1. [Query Select](#query-select) +//! 1. [Query Insert](#query-insert) +//! 1. [Query Update](#query-update) +//! 1. [Query Delete](#query-delete) +//! +//! 1. [Table Create](#table-create) +//! 1. [Table Alter](#table-alter) +//! 1. [Table Drop](#table-drop) +//! 1. [Table Rename](#table-rename) +//! 1. [Table Truncate](#table-truncate) +//! 1. [Foreign Key Create](#foreign-key-create) +//! 1. [Foreign Key Drop](#foreign-key-drop) +//! 1. [Index Create](#index-create) +//! 1. [Index Drop](#index-drop) +//! //! Construct a SQL statement with the library then execute the statement with a database connector, //! see SQLx example [here](https://github.com/SeaQL/sea-query/blob/master/examples/sqlx.rs). //! @@ -45,7 +69,32 @@ //! } //! } //! ``` -//! +//! +//! If you're okay with running another procedural macro, you can activate +//! the `derive` feature on the crate to save you some boilerplate: +//! +//! ```rust +//! # #[cfg(feature = "derive")] +//! use sea_query::Iden; +//! +//! // This will implement Iden exactly as shown above +//! # #[cfg(feature = "derive")] +//! #[derive(Iden)] +//! pub enum Character { +//! Table, +//! Id, +//! Character, +//! FontSize, +//! SizeW, +//! SizeH, +//! FontId, +//! } +//! ``` +//! +//! You can also override the generated column names by specifying an `#[iden = ""]` +//! attribute on the enum or any of its variants; for more information, look at +//! [the derive example](https://github.com/SeaQL/sea-query/blob/master/examples/derive.rs). +//! //! ## Expression //! //! Use [`Expr`] to construct select, join, where and having expression in query. @@ -154,7 +203,7 @@ //! use sea_query::{*, tests_cfg::*}; //! //! let query = Query::update() -//! .into_table(Glyph::Table) +//! .table(Glyph::Table) //! .values(vec![ //! (Glyph::Aspect, 1.23.into()), //! (Glyph::Image, "123".into()), @@ -517,4 +566,7 @@ pub use expr::*; pub use prepare::*; pub use types::*; pub use token::*; -pub use value::*; \ No newline at end of file +pub use value::*; + +#[cfg(feature = "derive")] +pub use sea_query_derive::Iden; \ No newline at end of file diff --git a/src/query/update.rs b/src/query/update.rs index 39779c9b6..5daf3e348 100644 --- a/src/query/update.rs +++ b/src/query/update.rs @@ -10,7 +10,7 @@ use crate::{backend::QueryBuilder, types::*, expr::*, value::*, prepare::*}; /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() -/// .into_table(Glyph::Table) +/// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 1.23.into()), /// (Glyph::Image, "123".into()), @@ -64,22 +64,41 @@ impl UpdateStatement { /// /// See [`UpdateStatement::values`] #[allow(clippy::wrong_self_convention)] - pub fn into_table(&mut self, table: T) -> &mut Self + pub fn table(&mut self, table: T) -> &mut Self where T: Iden { - self.into_table_dyn(Rc::new(table)) + self.table_dyn(Rc::new(table)) } - /// Specify which table to update, variation of [`UpdateStatement::into_table`]. + /// Specify which table to update, variation of [`UpdateStatement::table`]. /// /// # Examples /// /// See [`UpdateStatement::values`] #[allow(clippy::wrong_self_convention)] - pub fn into_table_dyn(&mut self, table: Rc) -> &mut Self { + pub fn table_dyn(&mut self, table: Rc) -> &mut Self { self.table = Some(Box::new(TableRef::Table(table))); self } + #[deprecated( + since = "0.5.0", + note = "Please use the UpdateStatement::table function instead" + )] + #[allow(clippy::wrong_self_convention)] + pub fn into_table(&mut self, table: T) -> &mut Self + where T: Iden { + self.table(table) + } + + #[deprecated( + since = "0.5.0", + note = "Please use the UpdateStatement::table_dyn function instead" + )] + #[allow(clippy::wrong_self_convention)] + pub fn into_table_dyn(&mut self, table: Rc) -> &mut Self { + self.table_dyn(table) + } + /// Update column value by [`SimpleExpr`]. /// /// # Examples @@ -88,7 +107,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) /// .values(vec![ /// (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), @@ -128,7 +147,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .json(json!({ /// "aspect": 2.1345, /// "image": "235m", @@ -169,7 +188,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), @@ -203,6 +222,44 @@ impl UpdateStatement { self } + /// Update column values. + /// + /// # Examples + /// + /// ``` + /// use sea_query::{*, tests_cfg::*}; + /// + /// let query = Query::update() + /// .table(Glyph::Table) + /// .value(Glyph::Aspect, 2.1345.into()) + /// .value(Glyph::Image, "235m".into()) + /// .and_where(Expr::col(Glyph::Id).eq(1)) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"UPDATE "glyph" SET "aspect" = 2.1345, "image" = '235m' WHERE "id" = 1"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"UPDATE `glyph` SET `aspect` = 2.1345, `image` = '235m' WHERE `id` = 1"# + /// ); + /// ``` + pub fn value(&mut self, col: T, value: Value) -> &mut Self + where T: Iden { + self.value_dyn(Rc::new(col) as Rc, value) + } + + /// Update column values, variation of [`UpdateStatement::value`]. + pub fn value_dyn(&mut self, col: Rc, value: Value) -> &mut Self { + self.push_boxed_value(col.to_string(), SimpleExpr::Value(value)); + self + } + fn push_boxed_value(&mut self, k: String, v: SimpleExpr) -> &mut Self { self.values.push((k, Box::new(v))); self @@ -216,7 +273,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), @@ -250,7 +307,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), @@ -406,7 +463,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), @@ -456,7 +513,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let (query, params) = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), @@ -500,7 +557,7 @@ impl UpdateStatement { /// use sea_query::{*, tests_cfg::*}; /// /// let query = Query::update() - /// .into_table(Glyph::Table) + /// .table(Glyph::Table) /// .values(vec![ /// (Glyph::Aspect, 2.1345.into()), /// (Glyph::Image, "235m".into()), diff --git a/tests/derive/mod.rs b/tests/derive/mod.rs new file mode 100644 index 000000000..913c4908f --- /dev/null +++ b/tests/derive/mod.rs @@ -0,0 +1,79 @@ +use super::*; + +#[test] +fn derive_1() { + #[derive(Iden)] + enum User { + Table, + Id, + FirstName, + LastName, + Email, + } + + println!("Default field names"); + assert_eq!(Iden::to_string(&User::Table), "user"); + assert_eq!(Iden::to_string(&User::Id), "id"); + assert_eq!(Iden::to_string(&User::FirstName), "first_name"); + assert_eq!(Iden::to_string(&User::LastName), "last_name"); + assert_eq!(Iden::to_string(&User::Email), "email"); +} + +#[test] +fn derive_2() { + #[derive(Iden)] + // Outer iden attributes overrides what's used for "Table"... + #[iden = "user"] + enum Custom { + Table, + #[iden = "my_id"] + Id, + #[iden = "name"] + FirstName, + #[iden = "surname"] + LastName, + // Custom casing if needed + #[iden = "EMail"] + Email, + } + + println!("Custom field names"); + assert_eq!(Iden::to_string(&Custom::Table), "user"); + assert_eq!(Iden::to_string(&Custom::Id), "my_id"); + assert_eq!(Iden::to_string(&Custom::FirstName), "name"); + assert_eq!(Iden::to_string(&Custom::LastName), "surname"); + assert_eq!(Iden::to_string(&Custom::Email), "EMail"); +} + +#[test] +fn derive_3() { + #[derive(Iden)] + enum Something { + // ...the Table can also be overwritten like this + #[iden = "something_else"] + Table, + Id, + AssetName, + UserId, + } + + println!("Single custom field name"); + assert_eq!(Iden::to_string(&Something::Table), "something_else"); + assert_eq!(Iden::to_string(&Something::Id), "id"); + assert_eq!(Iden::to_string(&Something::AssetName), "asset_name"); + assert_eq!(Iden::to_string(&Something::UserId), "user_id"); +} + +#[test] +fn derive_4() { + #[derive(Iden)] + pub struct SomeType; + + #[derive(Iden)] + #[iden = "another_name"] + pub struct CustomName; + + println!("Unit structs"); + assert_eq!(Iden::to_string(&SomeType), "some_type"); + assert_eq!(Iden::to_string(&CustomName), "another_name"); +} \ No newline at end of file diff --git a/tests/mod.rs b/tests/mod.rs index 31a884087..336f98a15 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -2,6 +2,9 @@ mod mysql; mod postgres; mod sqlite; +#[cfg(feature = "derive")] +mod derive; + use sea_query::{*, tests_cfg::*}; use sqlx::{Any, Pool, AnyPool}; diff --git a/tests/mysql/online.rs b/tests/mysql/online.rs index 1f6791e01..d7c193ce6 100644 --- a/tests/mysql/online.rs +++ b/tests/mysql/online.rs @@ -167,7 +167,7 @@ fn online_1() { env.exec(&sql); let sql = Query::update() - .into_table(Char::Table) + .table(Char::Table) .values(vec![ (Char::Character, "S".into()), (Char::SizeW, 1233.into()), diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs index a8abcd106..4555267bd 100644 --- a/tests/mysql/query.rs +++ b/tests/mysql/query.rs @@ -589,7 +589,7 @@ fn insert_3() { fn update_1() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .values(vec![ (Glyph::Aspect, 2.1345.into()), (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), @@ -606,7 +606,7 @@ fn update_1() { fn update_2() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .json(json!({ "aspect": 2.1345, "image": "24B0E11951B03B07F8300FD003983F03F0780060", @@ -623,7 +623,7 @@ fn update_2() { fn update_3() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) .values(vec![ (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), diff --git a/tests/postgres/online.rs b/tests/postgres/online.rs index 0fd5cf696..3b588189d 100644 --- a/tests/postgres/online.rs +++ b/tests/postgres/online.rs @@ -159,7 +159,7 @@ fn online_1() { env.exec(&sql); let sql = Query::update() - .into_table(Char::Table) + .table(Char::Table) .values(vec![ (Char::Character, "S".into()), (Char::SizeW, 1233.into()), diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs index 78ab2d4e0..b361d8b89 100644 --- a/tests/postgres/query.rs +++ b/tests/postgres/query.rs @@ -589,7 +589,7 @@ fn insert_3() { fn update_1() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .values(vec![ (Glyph::Aspect, 2.1345.into()), (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), @@ -604,7 +604,7 @@ fn update_1() { fn update_2() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .json(json!({ "aspect": 2.1345, "image": "24B0E11951B03B07F8300FD003983F03F0780060", @@ -619,7 +619,7 @@ fn update_2() { fn update_3() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) .values(vec![ (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs index a44e4aa8e..8d2cd6798 100644 --- a/tests/sqlite/query.rs +++ b/tests/sqlite/query.rs @@ -589,7 +589,7 @@ fn insert_3() { fn update_1() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .values(vec![ (Glyph::Aspect, 2.1345.into()), (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()), @@ -604,7 +604,7 @@ fn update_1() { fn update_2() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .json(json!({ "aspect": 2.1345, "image": "24B0E11951B03B07F8300FD003983F03F0780060", @@ -619,7 +619,7 @@ fn update_2() { fn update_3() { assert_eq!( Query::update() - .into_table(Glyph::Table) + .table(Glyph::Table) .value_expr(Glyph::Aspect, Expr::cust("60 * 24 * 24")) .values(vec![ (Glyph::Image, "24B0E11951B03B07F8300FD003983F03F0780060".into()),