From 92b0220f51b868e206dee20560c2a277d62302d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Fugulin?= Date: Fri, 8 Sep 2023 16:41:33 -0400 Subject: [PATCH] Diesel integration (#658) * feat: Prepare new crate for diesel * feat: add sqlite support for diesel integration * fix: diesel sqlite change some type mapping * fix: More sqlite fixes * feat: add mysql support for diesel integration * fix: Multiple small fixes to diesel integration * feat: add postgres support for diesel integration * chore: add diesel mysql example * chore: add diesel postgres example * fix: remove defaults for diesel integration * chore: add diesel sqlite example * fix: fix diesel postgres example missing features * chore: use official diesel commit * Add diesel to build * Update Diesel version * Fix workflow expression * Fix problem with bigdecimal build * Remove useless install step * Simplify update logic --- .github/workflows/rust.yml | 48 ++- examples/diesel_mysql/Cargo.toml | 26 ++ examples/diesel_mysql/Readme.md | 35 ++ examples/diesel_mysql/src/main.rs | 287 +++++++++++++++++ examples/diesel_postgres/Cargo.toml | 35 ++ examples/diesel_postgres/Readme.md | 51 +++ examples/diesel_postgres/src/main.rs | 388 +++++++++++++++++++++++ examples/diesel_sqlite/Cargo.toml | 26 ++ examples/diesel_sqlite/Readme.md | 27 ++ examples/diesel_sqlite/src/main.rs | 302 ++++++++++++++++++ sea-query-diesel/Cargo.toml | 66 ++++ sea-query-diesel/src/backend/mod.rs | 61 ++++ sea-query-diesel/src/backend/mysql.rs | 79 +++++ sea-query-diesel/src/backend/postgres.rs | 211 ++++++++++++ sea-query-diesel/src/backend/sqlite.rs | 104 ++++++ sea-query-diesel/src/lib.rs | 43 +++ sea-query-diesel/src/query.rs | 49 +++ sea-query-diesel/src/value.rs | 36 +++ 18 files changed, 1862 insertions(+), 12 deletions(-) create mode 100644 examples/diesel_mysql/Cargo.toml create mode 100644 examples/diesel_mysql/Readme.md create mode 100644 examples/diesel_mysql/src/main.rs create mode 100644 examples/diesel_postgres/Cargo.toml create mode 100644 examples/diesel_postgres/Readme.md create mode 100644 examples/diesel_postgres/src/main.rs create mode 100644 examples/diesel_sqlite/Cargo.toml create mode 100644 examples/diesel_sqlite/Readme.md create mode 100644 examples/diesel_sqlite/src/main.rs create mode 100644 sea-query-diesel/Cargo.toml create mode 100644 sea-query-diesel/src/backend/mod.rs create mode 100644 sea-query-diesel/src/backend/mysql.rs create mode 100644 sea-query-diesel/src/backend/postgres.rs create mode 100644 sea-query-diesel/src/backend/sqlite.rs create mode 100644 sea-query-diesel/src/lib.rs create mode 100644 sea-query-diesel/src/query.rs create mode 100644 sea-query-diesel/src/value.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f24ad33db..8b618973d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,8 +3,8 @@ name: tests on: pull_request: paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' + - "**.md" + - ".github/ISSUE_TEMPLATE/**" - .gitignore push: branches: @@ -12,8 +12,8 @@ on: - 0.*.x - pr/**/ci paths-ignore: - - '**.md' - - '.github/ISSUE_TEMPLATE/**' + - "**.md" + - ".github/ISSUE_TEMPLATE/**" - .gitignore concurrency: @@ -135,6 +135,26 @@ jobs: - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=with-mac_address - run: cargo build --manifest-path sea-query-postgres/Cargo.toml --workspace --features=postgres-array + diesel-build: + name: Build `sea-query-diesel` + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - run: cargo update --manifest-path sea-query-diesel/Cargo.toml --workspace -p bigdecimal:0.4.1 --precise 0.3.1 + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-chrono,with-json,with-rust_decimal,with-bigdecimal,with-uuid,with-time,with-ipnetwork,with-mac_address,postgres-array + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-chrono + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-json + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-rust_decimal + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres --features=with-rust_decimal-postgres + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features mysql --features=with-rust_decimal-mysql + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-bigdecimal + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-uuid + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-time + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-ipnetwork + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=with-mac_address + - run: cargo build --manifest-path sea-query-diesel/Cargo.toml --workspace --features postgres,sqlite,mysql --features=postgres-array + test: name: Unit Test runs-on: ubuntu-latest @@ -157,10 +177,11 @@ jobs: sqlite: name: SQLite runs-on: ubuntu-latest - needs: ["test", "derive-test", "rusqlite-build", "binder-build"] + needs: + ["test", "derive-test", "rusqlite-build", "binder-build", "diesel-build"] strategy: matrix: - example: [rusqlite, sqlx_sqlite] + example: [rusqlite, sqlx_sqlite, diesel_sqlite] steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable @@ -170,11 +191,11 @@ jobs: mysql: name: MySQL runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build"] + needs: ["test", "derive-test", "binder-build", "diesel-build"] strategy: matrix: version: [8.0, 5.7] - example: [sqlx_mysql] + example: [sqlx_mysql, diesel_mysql] services: mysql: image: mysql:${{ matrix.version }} @@ -200,11 +221,11 @@ jobs: mariadb: name: MariaDB runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build"] + needs: ["test", "derive-test", "binder-build", "diesel-build"] strategy: matrix: version: [10.6] - example: [sqlx_mysql] + example: [sqlx_mysql, diesel_mysql] services: mariadb: image: mariadb:${{ matrix.version }} @@ -230,11 +251,12 @@ jobs: postgres: name: PostgreSQL runs-on: ubuntu-latest - needs: ["test", "derive-test", "binder-build", "postgres-build"] + needs: + ["test", "derive-test", "binder-build", "postgres-build", "diesel-build"] strategy: matrix: version: [14.4, 13.7, 12.11] - example: [postgres, sqlx_postgres] + example: [postgres, sqlx_postgres, diesel_postgres] services: postgres: image: postgres:${{ matrix.version }} @@ -253,6 +275,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + - if: ${{ matrix.example == 'diesel_postgres' }} + run: cargo update --manifest-path examples/${{ matrix.example }}/Cargo.toml -p bigdecimal:0.4.1 --precise 0.3.1 - run: cargo build --manifest-path examples/${{ matrix.example }}/Cargo.toml - run: cargo run --manifest-path examples/${{ matrix.example }}/Cargo.toml diff --git a/examples/diesel_mysql/Cargo.toml b/examples/diesel_mysql/Cargo.toml new file mode 100644 index 000000000..d7c67ca0d --- /dev/null +++ b/examples/diesel_mysql/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-query-diesel-mysql-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4", default-features = false, features = ["clock"] } +time = { version = "0.3", features = ["parsing", "macros"] } +serde_json = { version = "1" } +uuid = { version = "1", features = ["serde", "v4"] } +diesel = { version = "2.1.1", features = ["mysql"] } +sea-query = { path = "../.." } +sea-query-diesel = { path = "../../sea-query-diesel", features = [ + "mysql", + "with-chrono", + "with-json", + "with-uuid", + "with-time", +] } + +# NOTE: if you are copying this example into your own project, use the following line instead: +# sea-query = { version = "0"} +# sea-query-diesel = { version = "0", features = [...] } diff --git a/examples/diesel_mysql/Readme.md b/examples/diesel_mysql/Readme.md new file mode 100644 index 000000000..ec1cf18ad --- /dev/null +++ b/examples/diesel_mysql/Readme.md @@ -0,0 +1,35 @@ +# SeaQuery Diesel MySQL example + +Running: + +```sh +cargo run +``` + +Example output: + +``` +Create table character: Ok(()) + +Insert into character Ok(4) + +Select one from character: +CharacterStructChrono { id: 4, uuid: UUID(3a23c42d-8cd9-4a0f-a8c3-0ced15d42228), name: "A", font_size: 12, meta: Object {"notes": String("some notes here")}, created: Some(2020-01-01T02:02:02) } + +Select one from character: +CharacterStructTime { id: 4, uuid: UUID(3a23c42d-8cd9-4a0f-a8c3-0ced15d42228), name: "A", font_size: 12, meta: Object {"notes": String("some notes here")}, created: Some(2020-01-01 2:02:02.0) } + + +Update character: Ok(1) + +Select one from character: +CharacterStructChrono { id: 4, uuid: UUID(3a23c42d-8cd9-4a0f-a8c3-0ced15d42228), name: "A", font_size: 24, meta: Object {"notes": String("some notes here")}, created: Some(2020-01-01T02:02:02) } + +Select one from character: +CharacterStructTime { id: 4, uuid: UUID(3a23c42d-8cd9-4a0f-a8c3-0ced15d42228), name: "A", font_size: 24, meta: Object {"notes": String("some notes here")}, created: Some(2020-01-01 2:02:02.0) } + + +Count character: Ok(CountField { count: 4 }) + +Delete character: Ok(1) +``` diff --git a/examples/diesel_mysql/src/main.rs b/examples/diesel_mysql/src/main.rs new file mode 100644 index 000000000..272d4fc3c --- /dev/null +++ b/examples/diesel_mysql/src/main.rs @@ -0,0 +1,287 @@ +use chrono::{NaiveDate, NaiveDateTime}; +use diesel::{ + backend::Backend, + connection::SimpleConnection, + deserialize::{self, FromSql}, + sql_types::{BigInt, Blob}, + Connection, MysqlConnection, QueryableByName, RunQueryDsl, +}; +use sea_query::{Alias, ColumnDef, Expr, Func, Iden, MysqlQueryBuilder, Order, Query, Table}; +use sea_query_diesel::DieselBinder; +use serde_json::{json, Value as Json}; +use time::{ + macros::{date, time}, + PrimitiveDateTime, +}; +use uuid::Uuid; + +fn main() { + let conn = &mut MysqlConnection::establish("mysql://sea:sea@127.0.0.1/query").unwrap(); + + // Schema + + let sql = [ + Table::drop() + .table(Character::Table) + .if_exists() + .build(MysqlQueryBuilder), + Table::create() + .table(Character::Table) + .if_not_exists() + .col( + ColumnDef::new(Character::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Character::Uuid).uuid().not_null()) + .col(ColumnDef::new(Character::FontSize).integer().not_null()) + .col(ColumnDef::new(Character::Name).string().not_null()) + .col(ColumnDef::new(Character::Meta).json().not_null()) + .col(ColumnDef::new(Character::Created).date_time()) + .build(MysqlQueryBuilder), + ] + .join("; "); + + let result = conn.batch_execute(&sql); + println!("Create table character: {result:?}"); + println!(); + + // Create + + let query = Query::insert() + .into_table(Character::Table) + .columns([ + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + None::.into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Some( + NaiveDate::from_ymd_opt(2020, 1, 1) + .unwrap() + .and_hms_opt(2, 2, 2) + .unwrap(), + ) + .into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + None::.into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Some(date!(2020 - 1 - 1).with_time(time!(2:2:2))).into(), + ]) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Insert into character {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Update + + let query = Query::update() + .table(Character::Table) + .values([(Character::FontSize, 24.into())]) + .and_where(Expr::col(Character::Id).eq(4)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Update character: {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Count + + let query = Query::select() + .from(Character::Table) + .expr_as(Func::count(Expr::col(Character::Id)), Alias::new("count")) + .to_owned(); + + print!("Count character: "); + let count = query.build_diesel().unwrap().get_result::(conn); + println!("{count:?}"); + println!(); + + // Delete + + let query = Query::delete() + .from_table(Character::Table) + .and_where(Expr::col(Character::Id).eq(1)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Delete character: {result:?}"); +} + +#[derive(Iden)] +enum Character { + Table, + Id, + Uuid, + Name, + FontSize, + Meta, + Created, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructChrono { + id: i32, + uuid: UUID, + name: String, + font_size: i32, + meta: Json, + created: Option, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructTime { + id: i32, + uuid: UUID, + name: String, + font_size: i32, + meta: Json, + created: Option, +} + +#[derive(QueryableByName, Debug)] +#[allow(dead_code)] +struct CountField { + #[diesel(sql_type = BigInt)] + count: i64, +} + +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +struct UUID(uuid::Uuid); + +impl FromSql for UUID +where + DB: Backend, + *const [u8]: deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let raw = <*const [u8] as FromSql>::from_sql(bytes)?; + let slice = unsafe { &*raw }; // We know that the pointer impl will never return null + let value = uuid::Uuid::from_slice(slice)?; + Ok(Self(value)) + } +} + +diesel::table! { + character (id) { + id -> Integer, + uuid -> Blob, + name -> Text, + font_size -> Integer, + meta -> Json, + created -> Nullable, + } +} diff --git a/examples/diesel_postgres/Cargo.toml b/examples/diesel_postgres/Cargo.toml new file mode 100644 index 000000000..580378ba9 --- /dev/null +++ b/examples/diesel_postgres/Cargo.toml @@ -0,0 +1,35 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-query-diesel-postgres-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4", default-features = false, features = ["clock"] } +time = { version = "0.3", features = ["parsing", "macros"] } +serde_json = { version = "1" } +uuid = { version = "1", features = ["serde", "v4"] } +ipnetwork = { version = "0.20" } +mac_address = { version = "1.1" } +rust_decimal = { version = "1" } +bigdecimal = { version = "0.3" } +diesel = { version = "2.1.1", features = ["postgres"] } +sea-query = { path = "../.." } +sea-query-diesel = { path = "../../sea-query-diesel", features = [ + "postgres", + "with-chrono", + "with-json", + "with-uuid", + "with-time", + "with-rust_decimal-postgres", + "with-bigdecimal", + "with-ipnetwork", + "with-mac_address", + "postgres-array", +] } + +# NOTE: if you are copying this example into your own project, use the following line instead: +# sea-query = { version = "0"} +# sea-query-diesel = { version = "0", features = [...] } diff --git a/examples/diesel_postgres/Readme.md b/examples/diesel_postgres/Readme.md new file mode 100644 index 000000000..937085f35 --- /dev/null +++ b/examples/diesel_postgres/Readme.md @@ -0,0 +1,51 @@ +# SeaQuery Diesel Postgres example + +> WARN: If you enable `with-bigdecimal`, you HAVE to update the version used by default by `diesel` +> otherwise it will fail to build. Use `cargo update -p bigdecimal:0.4.1 --precise 0.3.1`. + +Running: + +```sh +cargo run +``` + +Example output: + +``` +Create table character: Ok(()) + +Insert into character Ok(2) + +Select one from character: +CharacterStructChrono { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "A", font_size: 12, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20T00:00:00, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + +Select one from character: +CharacterStructTime { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "A", font_size: 12, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20 0:00:00.0, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + + +Update character: Ok(1) + +Select one from character: +CharacterStructChrono { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "A", font_size: 24, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20T00:00:00, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + +Select one from character: +CharacterStructTime { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "A", font_size: 24, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20 0:00:00.0, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + + +Count character: Ok(CountField { count: 2 }) + +Insert into character (with upsert): Ok(2) + +Select all characters: +CharacterStructChrono { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "C", font_size: 24, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20T00:00:00, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + +CharacterStructChrono { id: 1, uuid: c9384d8e-bbf4-401e-a395-17cee77022fb, name: "B", font_size: 16, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20T00:00:00, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [true, false] } + +Select all characters: +CharacterStructTime { id: 2, uuid: 6c95bcb8-3411-484c-9b33-f70c97c2a13a, name: "C", font_size: 24, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20 0:00:00.0, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [false, false] } + +CharacterStructTime { id: 1, uuid: c9384d8e-bbf4-401e-a395-17cee77022fb, name: "B", font_size: 16, meta: Object {"notes": String("some notes here")}, decimal: 3.141, big_decimal: BigDecimal("3141.000"), created: 2020-08-20 0:00:00.0, inet: V4(Ipv4Network { addr: 127.0.0.1, prefix: 8 }), mac_address: MacAddress(MacAddress { bytes: [96, 3, 8, 143, 139, 58] }), array_bool: [true, false] } + + +Delete character: Ok(1) +``` diff --git a/examples/diesel_postgres/src/main.rs b/examples/diesel_postgres/src/main.rs new file mode 100644 index 000000000..b55d7d80f --- /dev/null +++ b/examples/diesel_postgres/src/main.rs @@ -0,0 +1,388 @@ +use bigdecimal::{BigDecimal, FromPrimitive}; +use chrono::{NaiveDate, NaiveDateTime}; +use diesel::backend::Backend; +use diesel::connection::SimpleConnection; +use diesel::deserialize::{self, FromSql}; +use diesel::pg::sql_types::MacAddr; +use diesel::sql_types::BigInt; +use diesel::{Connection, PgConnection, QueryableByName, RunQueryDsl}; +use ipnetwork::IpNetwork; +use mac_address::get_mac_address; +use rust_decimal::Decimal; +use sea_query::{ + ColumnDef, ColumnType, Expr, Func, Iden, OnConflict, Order, PostgresQueryBuilder, Query, Table, +}; +use sea_query_diesel::DieselBinder; +use serde_json::{json, Value as Json}; +use std::net::{IpAddr, Ipv4Addr}; +use time::{ + macros::{date, time}, + PrimitiveDateTime, +}; +use uuid::Uuid; + +fn main() { + let conn = &mut PgConnection::establish("postgres://sea:sea@127.0.0.1/query").unwrap(); + + // Schema + + let sql = [ + Table::drop() + .table(Character::Table) + .if_exists() + .build(PostgresQueryBuilder), + Table::create() + .table(Character::Table) + .if_not_exists() + .col( + ColumnDef::new(Character::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Character::Uuid).uuid()) + .col(ColumnDef::new(Character::FontSize).integer()) + .col(ColumnDef::new(Character::Name).string()) + .col(ColumnDef::new(Character::Meta).json()) + .col(ColumnDef::new(Character::Decimal).decimal()) + .col(ColumnDef::new(Character::BigDecimal).decimal()) + .col(ColumnDef::new(Character::Created).date_time()) + .col(ColumnDef::new(Character::Inet).inet()) + .col(ColumnDef::new(Character::MacAddress).mac_address()) + .col(ColumnDef::new(Character::ArrayBool).array(ColumnType::Boolean)) + .build(PostgresQueryBuilder), + ] + .join("; "); + + let result = conn.batch_execute(&sql); + println!("Create table character: {result:?}"); + println!(); + + // Create + + let query = Query::insert() + .into_table(Character::Table) + .columns([ + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Decimal, + Character::BigDecimal, + Character::Created, + Character::Inet, + Character::MacAddress, + Character::ArrayBool, + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Decimal::from_i128_with_scale(3141i128, 3).into(), + BigDecimal::from_i128(3141i128) + .unwrap() + .with_scale(3) + .into(), + NaiveDate::from_ymd_opt(2020, 8, 20) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .into(), + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8) + .unwrap() + .into(), + get_mac_address().unwrap().unwrap().into(), + vec![true, false].into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Decimal::from_i128_with_scale(3141i128, 3).into(), + BigDecimal::from_i128(3141i128) + .unwrap() + .with_scale(3) + .into(), + date!(2020 - 8 - 20).with_time(time!(0:0:0)).into(), + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8) + .unwrap() + .into(), + get_mac_address().unwrap().unwrap().into(), + vec![false, false].into(), + ]) + .returning_col(Character::Id) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Insert into character {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::Name, + Character::FontSize, + Character::Meta, + Character::Decimal, + Character::BigDecimal, + Character::Created, + Character::Inet, + Character::MacAddress, + Character::ArrayBool, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Update + + let query = Query::update() + .table(Character::Table) + .values([(Character::FontSize, 24.into())]) + .and_where(Expr::col(Character::Id).eq(2)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Update character: {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::Name, + Character::FontSize, + Character::Meta, + Character::Decimal, + Character::BigDecimal, + Character::Created, + Character::Inet, + Character::MacAddress, + Character::ArrayBool, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Count + + let query = Query::select() + .from(Character::Table) + .expr(Func::count(Expr::col(Character::Id))) + .to_owned(); + + print!("Count character: "); + let count = query.build_diesel().unwrap().get_result::(conn); + println!("{count:?}"); + println!(); + + // Upsert + + let query = Query::insert() + .into_table(Character::Table) + .columns([Character::Id, Character::FontSize, Character::Name]) + .values_panic([1.into(), 16.into(), "B".into()]) + .values_panic([2.into(), 24.into(), "C".into()]) + .on_conflict( + OnConflict::column(Character::Id) + .update_columns([Character::FontSize, Character::Name]) + .to_owned(), + ) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Insert into character (with upsert): {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::Name, + Character::FontSize, + Character::Meta, + Character::Decimal, + Character::BigDecimal, + Character::Created, + Character::Inet, + Character::MacAddress, + Character::ArrayBool, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .to_owned(); + + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select all characters:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select all characters:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Delete + + let query = Query::delete() + .from_table(Character::Table) + .and_where(Expr::col(Character::Id).eq(1)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Delete character: {result:?}"); +} + +#[derive(Iden)] +enum Character { + Table, + Id, + Uuid, + Name, + FontSize, + Meta, + Decimal, + BigDecimal, + Created, + Inet, + MacAddress, + ArrayBool, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructChrono { + id: i32, + uuid: Uuid, + name: String, + font_size: i32, + meta: Json, + decimal: Decimal, + big_decimal: BigDecimal, + created: NaiveDateTime, + inet: IpNetwork, + mac_address: MacAddress, + array_bool: Vec, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructTime { + id: i32, + uuid: Uuid, + name: String, + font_size: i32, + meta: Json, + decimal: Decimal, + big_decimal: BigDecimal, + created: PrimitiveDateTime, + inet: IpNetwork, + mac_address: MacAddress, + array_bool: Vec, +} + +#[derive(QueryableByName, Debug)] +#[allow(dead_code)] +struct CountField { + #[diesel(sql_type = BigInt)] + count: i64, +} + +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +struct MacAddress(mac_address::MacAddress); + +impl FromSql for MacAddress +where + DB: Backend, + [u8; 6]: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let slice = <[u8; 6] as FromSql>::from_sql(bytes)?; + let value = mac_address::MacAddress::new(slice); + Ok(Self(value)) + } +} + +diesel::table! { + character (id) { + id -> Integer, + uuid -> Uuid, + name -> Text, + font_size -> Integer, + meta -> Json, + decimal -> Numeric, + big_decimal -> Numeric, + created -> Timestamp, + inet -> Inet, + mac_address -> MacAddr, + array_bool -> Array, + } +} diff --git a/examples/diesel_sqlite/Cargo.toml b/examples/diesel_sqlite/Cargo.toml new file mode 100644 index 000000000..d0f3e7ac8 --- /dev/null +++ b/examples/diesel_sqlite/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-query-diesel-sqlite-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = { version = "0.4", default-features = false, features = ["clock"] } +time = { version = "0.3", features = ["parsing", "macros"] } +serde_json = { version = "1" } +uuid = { version = "1", features = ["serde", "v4"] } +diesel = { version = "2.1.1", features = ["sqlite"] } +sea-query = { path = "../.." } +sea-query-diesel = { path = "../../sea-query-diesel", features = [ + "sqlite", + "with-chrono", + "with-json", + "with-uuid", + "with-time", +] } + +# NOTE: if you are copying this example into your own project, use the following line instead: +# sea-query = { version = "0"} +# sea-query-diesel = { version = "0", features = [...] } diff --git a/examples/diesel_sqlite/Readme.md b/examples/diesel_sqlite/Readme.md new file mode 100644 index 000000000..7b8287424 --- /dev/null +++ b/examples/diesel_sqlite/Readme.md @@ -0,0 +1,27 @@ +# SeaQuery Diesel Sqlite example + +Running: + +```sh +cargo run +``` + +Example output: + +``` +Create table character: Ok() + +Insert into character: Ok(1) + +Select one from character: +CharacterStruct { id: 1, character: "A", font_size: 12 } + +Update character: Ok(1) + +Select one from character: +CharacterStruct { id: 1, character: "A", font_size: 24 } + +Count character: 1 + +Delete character: Ok(1) +``` diff --git a/examples/diesel_sqlite/src/main.rs b/examples/diesel_sqlite/src/main.rs new file mode 100644 index 000000000..6c6ee1970 --- /dev/null +++ b/examples/diesel_sqlite/src/main.rs @@ -0,0 +1,302 @@ +use chrono::{NaiveDate, NaiveDateTime}; +use diesel::backend::Backend; +use diesel::connection::SimpleConnection; +use diesel::deserialize::{self, FromSql}; +use diesel::sql_types::BigInt; +use diesel::sql_types::{Blob, Text}; +use diesel::{Connection, QueryableByName, RunQueryDsl, SqliteConnection}; +use sea_query::{Alias, ColumnDef, Expr, Func, Iden, Order, Query, SqliteQueryBuilder, Table}; +use sea_query_diesel::DieselBinder; +use serde_json::json; +use time::macros::{date, time}; +use time::PrimitiveDateTime; +use uuid::Uuid; + +// NOTE: Until https://github.com/diesel-rs/diesel/issues/3693 is fixed, we can't mix and match +// values from time and chrono, just be mindful of that! +fn main() { + let conn = &mut SqliteConnection::establish(":memory:").unwrap(); + + // Schema + + let sql = [ + Table::drop() + .table(Character::Table) + .if_exists() + .build(SqliteQueryBuilder), + Table::create() + .table(Character::Table) + .if_not_exists() + .col( + ColumnDef::new(Character::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Character::Uuid).uuid().not_null()) + .col(ColumnDef::new(Character::FontSize).integer().not_null()) + .col(ColumnDef::new(Character::Name).string().not_null()) + .col(ColumnDef::new(Character::Meta).json().not_null()) + .col(ColumnDef::new(Character::Created).date_time()) + .build(SqliteQueryBuilder), + ] + .join("; "); + + let result = conn.batch_execute(&sql); + println!("Create table character: {result:?}"); + println!(); + + // Create + + let query = Query::insert() + .into_table(Character::Table) + .columns([ + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + None::.into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Some( + NaiveDate::from_ymd_opt(2020, 1, 1) + .unwrap() + .and_hms_opt(2, 2, 2) + .unwrap(), + ) + .into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + None::.into(), + ]) + .values_panic([ + Uuid::new_v4().into(), + 12.into(), + "A".into(), + json!({ + "notes": "some notes here", + }) + .into(), + Some(date!(2020 - 1 - 1).with_time(time!(2:2:2))).into(), + ]) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Insert into character {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + // let rows = query + // .build_diesel() + // .unwrap() + // .get_results::(conn) + // .unwrap(); + // println!("Select one from character:"); + // for row in rows.iter() { + // println!("{row:?}\n"); + // } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Update + + let query = Query::update() + .table(Character::Table) + .values([(Character::FontSize, 24.into())]) + .and_where(Expr::col(Character::Id).eq(4)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Update character: {result:?}\n"); + + // Read + + let query = Query::select() + .columns([ + Character::Id, + Character::Uuid, + Character::FontSize, + Character::Name, + Character::Meta, + Character::Created, + ]) + .from(Character::Table) + .order_by(Character::Id, Order::Desc) + .limit(1) + .to_owned(); + + // let rows = query + // .build_diesel() + // .unwrap() + // .get_results::(conn) + // .unwrap(); + // println!("Select one from character:"); + // for row in rows.iter() { + // println!("{row:?}\n"); + // } + let rows = query + .build_diesel() + .unwrap() + .get_results::(conn) + .unwrap(); + println!("Select one from character:"); + for row in rows.iter() { + println!("{row:?}\n"); + } + println!(); + + // Count + + let query = Query::select() + .from(Character::Table) + .expr_as(Func::count(Expr::col(Character::Id)), Alias::new("count")) + .to_owned(); + + print!("Count character: "); + let count = query.build_diesel().unwrap().get_result::(conn); + println!("{count:?}"); + println!(); + + // Delete + + let query = Query::delete() + .from_table(Character::Table) + .and_where(Expr::col(Character::Id).eq(4)) + .to_owned(); + + let result = query.build_diesel().unwrap().execute(conn); + println!("Delete character: {result:?}"); +} + +#[derive(Iden)] +enum Character { + Table, + Id, + Uuid, + Name, + FontSize, + Meta, + Created, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructChrono { + id: i32, + uuid: UUID, + name: String, + font_size: i32, + meta: Json, + created: Option, +} + +#[derive(QueryableByName, Debug)] +#[diesel(table_name = character)] +#[allow(dead_code)] +struct CharacterStructTime { + id: i32, + uuid: UUID, + name: String, + font_size: i32, + meta: Json, + created: Option, +} + +#[derive(QueryableByName, Debug)] +#[allow(dead_code)] +struct CountField { + #[diesel(sql_type = BigInt)] + count: i64, +} + +#[derive(Debug)] +struct Json(serde_json::Value); + +impl FromSql for Json +where + DB: Backend, + *const str: deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let raw = <*const str as FromSql>::from_sql(bytes)?; + let string = unsafe { &*raw }; // We know that the pointer impl will never return null + let value = serde_json::from_str(string)?; + Ok(Self(value)) + } +} + +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +struct UUID(uuid::Uuid); + +impl FromSql for UUID +where + DB: Backend, + *const [u8]: deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let raw = <*const [u8] as FromSql>::from_sql(bytes)?; + let slice = unsafe { &*raw }; // We know that the pointer impl will never return null + let value = uuid::Uuid::from_slice(slice)?; + Ok(Self(value)) + } +} + +diesel::table! { + character (id) { + id -> Integer, + uuid -> Blob, + name -> Text, + font_size -> Integer, + meta -> Text, + created -> Nullable, + } +} diff --git a/sea-query-diesel/Cargo.toml b/sea-query-diesel/Cargo.toml new file mode 100644 index 000000000..4f0c5d85b --- /dev/null +++ b/sea-query-diesel/Cargo.toml @@ -0,0 +1,66 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-query-diesel" +version = "0.1.0" +authors = ["Emile Fugulin "] +edition = "2021" +description = "Binder traits for connecting sea-query with diesel" +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/sea-query" +repository = "https://github.com/SeaQL/sea-query" +categories = ["database"] +keywords = ["database", "sql", "diesel"] +rust-version = "1.60" + +[lib] + +[dependencies] +sea-query = { version = "0.31", path = "..", default-features = false } +diesel = { version = "2.1.1", features = [ + "i-implement-a-third-party-backend-and-opt-into-breaking-changes", +] } +bigdecimal = { version = "0.3", default-features = false, optional = true } +rust_decimal = { version = "1", default-features = false, optional = true } +chrono = { version = "0.4", default-features = false, optional = true } +time = { version = "0.3", default-features = false, optional = true } +uuid = { version = "1", default-features = false, optional = true } +serde_json = { version = "1", default-features = false, optional = true } +ipnetwork = { version = "0.20", default-features = false, optional = true } +mac_address = { version = "1.1", default-features = false, optional = true } + +[features] +default = [] +extras = [ + "with-chrono", + "with-json", + "with-rust_decimal", + "with-bigdecimal", + "with-uuid", + "with-time", + "with-ipnetwork", + "with-mac_address", + "postgres-array", +] +postgres = ["diesel/postgres", "sea-query/backend-postgres"] +mysql = ["diesel/mysql", "sea-query/backend-mysql"] +sqlite = ["diesel/sqlite", "sea-query/backend-sqlite"] +with-chrono = ["diesel/chrono", "sea-query/with-chrono", "chrono"] +with-json = ["diesel/serde_json", "sea-query/with-json", "serde_json"] +with-rust_decimal = ["sea-query/with-rust_decimal", "rust_decimal"] +with-rust_decimal-mysql = ["with-rust_decimal", "rust_decimal/db-diesel2-mysql"] +with-rust_decimal-postgres = [ + "with-rust_decimal", + "rust_decimal/db-diesel2-postgres", +] +with-bigdecimal = ["diesel/numeric", "sea-query/with-bigdecimal", "bigdecimal"] +with-uuid = ["diesel/uuid", "sea-query/with-uuid", "uuid"] +with-time = ["diesel/time", "sea-query/with-time", "time"] +with-ipnetwork = [ + "diesel/network-address", + "sea-query/with-ipnetwork", + "ipnetwork", +] +with-mac_address = ["sea-query/with-mac_address", "mac_address"] +postgres-array = ["sea-query/postgres-array"] diff --git a/sea-query-diesel/src/backend/mod.rs b/sea-query-diesel/src/backend/mod.rs new file mode 100644 index 000000000..04e33c677 --- /dev/null +++ b/sea-query-diesel/src/backend/mod.rs @@ -0,0 +1,61 @@ +use diesel::backend::Backend; +use diesel::query_builder::QueryFragment; +use diesel::result::QueryResult; +use sea_query::{QueryBuilder, Value}; + +#[cfg(feature = "mysql")] +mod mysql; + +#[cfg(feature = "postgres")] +mod postgres; + +#[cfg(feature = "sqlite")] +mod sqlite; + +pub trait ExtractBuilder { + type Builder: QueryBuilder; + + fn builder() -> Self::Builder; +} + +pub trait TransformValue: Backend { + fn transform_value(value: Value) -> QueryResult + Send>>; +} + +#[allow(unused_macros, unused_imports)] +mod macros { + macro_rules! err { + ($msg: expr) => { + ::diesel::result::Error::SerializationError($msg.into()) + }; + } + + macro_rules! bail { + ($msg: tt) => { + return Err($crate::backend::macros::err!($msg)) + }; + } + + macro_rules! build { + ($type: ty, $value: expr) => { + $crate::value::SeaValue::<::diesel::sql_types::Nullable<$type>, _>::build($value) + }; + } + + macro_rules! refine { + ($target: ty, $source: expr, $value: expr) => { + > as ::sea_query::ValueType>::try_from(::sea_query::Value::Array( + $source, $value, + )) + .map_err(|_| { + err!(::std::concat!( + "This Value::Array should consist of ", + ::std::stringify!($target), + " values" + )) + })? + }; + } + + pub(crate) use {bail, build, err, refine}; +} diff --git a/sea-query-diesel/src/backend/mysql.rs b/sea-query-diesel/src/backend/mysql.rs new file mode 100644 index 000000000..ee8ccd0bd --- /dev/null +++ b/sea-query-diesel/src/backend/mysql.rs @@ -0,0 +1,79 @@ +use diesel::mysql::sql_types::*; +use diesel::mysql::Mysql; +use diesel::query_builder::QueryFragment; +use diesel::result::QueryResult; +use diesel::sql_types::*; +use sea_query::{MysqlQueryBuilder, Value}; + +#[allow(unused_imports)] +use super::macros::{bail, build}; +use super::{ExtractBuilder, TransformValue}; + +impl ExtractBuilder for Mysql { + type Builder = MysqlQueryBuilder; + + fn builder() -> Self::Builder { + MysqlQueryBuilder + } +} + +impl TransformValue for Mysql { + fn transform_value(value: Value) -> QueryResult + Send>> { + let transformed = match value { + Value::Bool(v) => build!(Bool, v), + Value::TinyInt(v) => build!(TinyInt, v), + Value::SmallInt(v) => build!(SmallInt, v), + Value::Int(v) => build!(Integer, v), + Value::BigInt(v) => build!(BigInt, v), + Value::TinyUnsigned(v) => build!(Unsigned, v), + Value::SmallUnsigned(v) => build!(Unsigned, v), + Value::Unsigned(v) => build!(Unsigned, v), + Value::BigUnsigned(v) => build!(Unsigned, v), + Value::Float(v) => build!(Float, v), + Value::Double(v) => build!(Double, v), + Value::String(v) => build!(Text, v.map(|v| *v)), + Value::Char(v) => build!(Text, v.map(|v| v.to_string())), + Value::Bytes(v) => build!(Blob, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDate(v) => build!(Date, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoTime(v) => build!(Time, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTime(v) => build!(Timestamp, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeUtc(v) => build!(Timestamp, v.map(|v| v.naive_utc())), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeLocal(v) => build!(Timestamp, v.map(|v| v.naive_utc())), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeWithTimeZone(v) => build!(Timestamp, v.map(|v| v.naive_utc())), + #[cfg(feature = "with-time")] + Value::TimeDate(v) => build!(Date, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeTime(v) => build!(Time, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeDateTime(v) => build!(Timestamp, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeDateTimeWithTimeZone(v) => build!(Timestamp, v.map(|v| *v)), + #[cfg(feature = "with-uuid")] + Value::Uuid(v) => build!(Blob, v.map(|v| v.as_bytes().to_vec())), + #[cfg(feature = "with-rust_decimal-mysql")] + Value::Decimal(v) => build!(Numeric, v.map(|v| *v)), + #[cfg(all( + feature = "with-rust_decimal", + not(feature = "with-rust_decimal-mysql") + ))] + Value::Decimal(_) => bail!("Enable feature with-rust_decimal-mysql"), + #[cfg(feature = "with-bigdecimal")] + Value::BigDecimal(v) => build!(Numeric, v.map(|v| *v)), + #[cfg(feature = "with-json")] + Value::Json(v) => build!(Json, v.map(|v| *v)), + #[cfg(feature = "with-ipnetwork")] + Value::IpNetwork(_) => bail!("Mysql doesn't support IpNetwork arguments"), + #[cfg(feature = "with-mac_address")] + Value::MacAddress(_) => bail!("Mysql doesn't support MacAddress arguments"), + #[cfg(feature = "postgres-array")] + Value::Array(_, _) => bail!("Mysql doesn't support array arguments"), + }; + Ok(transformed) + } +} diff --git a/sea-query-diesel/src/backend/postgres.rs b/sea-query-diesel/src/backend/postgres.rs new file mode 100644 index 000000000..ebf634a9d --- /dev/null +++ b/sea-query-diesel/src/backend/postgres.rs @@ -0,0 +1,211 @@ +use diesel::pg::sql_types::*; +use diesel::pg::Pg; +use diesel::query_builder::QueryFragment; +use diesel::result::QueryResult; +use diesel::sql_types::*; +#[allow(unused_imports)] +use sea_query::{ArrayType, PostgresQueryBuilder, Value}; + +#[allow(unused_imports)] +use super::macros::{bail, build, err, refine}; +use super::{ExtractBuilder, TransformValue}; + +impl ExtractBuilder for Pg { + type Builder = PostgresQueryBuilder; + + fn builder() -> Self::Builder { + PostgresQueryBuilder + } +} + +impl TransformValue for Pg { + fn transform_value(value: Value) -> QueryResult + Send>> { + let transformed = match value { + Value::Bool(v) => build!(Bool, v), + Value::TinyInt(v) => build!(SmallInt, v.map(i16::from)), + Value::SmallInt(v) => build!(SmallInt, v), + Value::Int(v) => build!(Integer, v), + Value::BigInt(v) => build!(BigInt, v), + Value::TinyUnsigned(v) => build!(SmallInt, v.map(i16::from)), + Value::SmallUnsigned(v) => build!(Integer, v.map(i32::from)), + Value::Unsigned(v) => build!(BigInt, v.map(i64::from)), + // There is no i128 support, so hope the unsigned can be converted + Value::BigUnsigned(v) => { + let v = v + .map(|v| { + i64::try_from(v) + .map_err(|_| err!("BigUnsigned cannot be represented as i64")) + }) + .transpose()?; + build!(BigInt, v) + } + Value::Float(v) => build!(Float, v), + Value::Double(v) => build!(Double, v), + Value::String(v) => build!(Text, v.map(|v| *v)), + Value::Char(v) => build!(Text, v.map(|v| v.to_string())), + Value::Bytes(v) => build!(Blob, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDate(v) => build!(Date, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoTime(v) => build!(Time, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTime(v) => build!(Timestamp, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeUtc(v) => build!(Timestamptz, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeLocal(v) => build!(Timestamptz, v.map(|v| *v)), + #[cfg(feature = "with-chrono")] + Value::ChronoDateTimeWithTimeZone(v) => build!(Timestamptz, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeDate(v) => build!(Date, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeTime(v) => build!(Time, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeDateTime(v) => build!(Timestamp, v.map(|v| *v)), + #[cfg(feature = "with-time")] + Value::TimeDateTimeWithTimeZone(v) => build!(Timestamptz, v.map(|v| *v)), + #[cfg(feature = "with-uuid")] + Value::Uuid(v) => build!(Uuid, v.map(|v| *v)), + #[cfg(feature = "with-rust_decimal-postgres")] + Value::Decimal(v) => build!(Numeric, v.map(|v| *v)), + #[cfg(all( + feature = "with-rust_decimal", + not(feature = "with-rust_decimal-postgres") + ))] + Value::Decimal(_) => bail!("Enable feature with-rust_decimal-postgres"), + #[cfg(feature = "with-bigdecimal")] + Value::BigDecimal(v) => build!(Numeric, v.map(|v| *v)), + #[cfg(feature = "with-json")] + Value::Json(v) => build!(Json, v.map(|v| *v)), + #[cfg(feature = "with-ipnetwork")] + Value::IpNetwork(v) => build!(Inet, v.map(|v| *v)), + #[cfg(feature = "with-mac_address")] + Value::MacAddress(v) => build!(MacAddr, v.map(|v| v.bytes())), + #[cfg(feature = "postgres-array")] + Value::Array(ty, v) => match ty { + ArrayType::Bool => build!(Array, refine!(bool, ty, v)), + ArrayType::TinyInt => { + build!( + Array, + refine!(i8, ty, v) + .map(|v| v.into_iter().map(i16::from).collect::>()) + ) + } + ArrayType::SmallInt => build!(Array, refine!(i16, ty, v)), + ArrayType::Int => build!(Array, refine!(i32, ty, v)), + ArrayType::BigInt => build!(Array, refine!(i64, ty, v)), + ArrayType::TinyUnsigned => { + build!( + Array, + refine!(u8, ty, v) + .map(|v| v.into_iter().map(i16::from).collect::>()) + ) + } + ArrayType::SmallUnsigned => { + build!( + Array, + refine!(u16, ty, v) + .map(|v| v.into_iter().map(i32::from).collect::>()) + ) + } + ArrayType::Unsigned => { + build!( + Array, + refine!(u32, ty, v) + .map(|v| v.into_iter().map(i64::from).collect::>()) + ) + } + ArrayType::BigUnsigned => { + build!( + Array, + refine!(u64, ty, v) + .map(|v| v + .into_iter() + .map(|v| i64::try_from(v) + .map_err(|_| err!("BigUnsigned cannot be represented as i64"))) + .collect::, _>>()) + .transpose()? + ) + } + ArrayType::Float => build!(Array, refine!(f32, ty, v)), + ArrayType::Double => build!(Array, refine!(f64, ty, v)), + ArrayType::String => build!(Array, refine!(String, ty, v)), + ArrayType::Char => { + build!( + Array, + refine!(char, ty, v) + .map(|v| v.into_iter().map(|v| v.to_string()).collect::>()) + ) + } + ArrayType::Bytes => build!(Array, refine!(Vec, ty, v)), + #[cfg(feature = "with-chrono")] + ArrayType::ChronoDate => build!(Array, refine!(chrono::NaiveDate, ty, v)), + #[cfg(feature = "with-chrono")] + ArrayType::ChronoTime => build!(Array