diff --git a/Cargo.lock b/Cargo.lock index 98fa71a..75d6233 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,8 +257,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -352,6 +354,40 @@ dependencies = [ "syn", ] +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab8a4ea925ce79678034870834602a2980f4b88c09e97feb266496dbb4493d2" +dependencies = [ + "async-trait", + "deadpool", + "getrandom", + "tokio", + "tokio-postgres", + "tracing", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + [[package]] name = "deranged" version = "0.3.11" @@ -373,27 +409,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "docker_credential" version = "1.3.1" @@ -433,6 +448,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -449,6 +475,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -576,8 +614,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -905,6 +945,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.3", ] [[package]] @@ -1003,6 +1044,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.2" @@ -1024,12 +1075,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "parking_lot" version = "0.12.3" @@ -1094,8 +1139,10 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" name = "pg_filters" version = "0.1.10" dependencies = [ + "chrono", + "deadpool", + "deadpool-postgres", "eyre", - "hex", "testcontainers-modules", "tokio", "tokio-postgres", @@ -1172,13 +1219,17 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9" +checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" dependencies = [ "bytes", + "chrono", "fallible-iterator", "postgres-protocol", + "serde", + "serde_json", + "uuid", ] [[package]] @@ -1256,31 +1307,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", ] [[package]] -name = "redox_users" -version = "0.4.5" +name = "redox_syscall" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "getrandom", - "libredox", - "thiserror", + "bitflags 2.6.0", ] [[package]] @@ -1475,9 +1524,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1555,6 +1604,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1651,17 +1709,17 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.21.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae246d52d140e021ae1c201a9b98dda00cf62f600256c3992bcb7aa8ee9962e" +checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" dependencies = [ "async-trait", "bollard", "bollard-stubs", "bytes", - "dirs", "docker_credential", "either", + "etcetera", "futures", "log", "memchr", @@ -1673,15 +1731,16 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tokio-tar", "tokio-util", "url", ] [[package]] name = "testcontainers-modules" -version = "0.9.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868e8e818fe37b8ed4c21ac72185206b48e8767b5ad3836d7ec0e5c9386e19a2" +checksum = "064a2677e164cad39ef3c1abddb044d5a25c49d27005804563d8c4227aac8bd0" dependencies = [ "testcontainers", ] @@ -1762,7 +1821,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -1781,9 +1842,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3" +checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" dependencies = [ "async-trait", "byteorder", @@ -1827,6 +1888,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -1874,9 +1950,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2129,6 +2217,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2250,6 +2347,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 54dcf2d..9f9604c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,12 @@ path = "src/lib/mod.rs" [dependencies] eyre = "0.6.12" -uuid = { version = "1.11.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } -hex = "0.4.3" [dev-dependencies] -testcontainers-modules = { version = "0.9.0", features = ["postgres", "blocking"] } -tokio = { version = "1", features = ["macros"] } -tokio-postgres = "0.7.11" \ No newline at end of file +testcontainers-modules = { version = "0.11.4", features = ["postgres", "blocking"] } +tokio = { version = "1", features = ["full"] } +tokio-postgres = { version = "0.7.12", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] } +deadpool = { version = "0.12.0", features = ["rt_tokio_1"] } +deadpool-postgres = "0.14.0" +chrono = "0.4.38" +uuid = { version = "1.11.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } diff --git a/src/lib/filtering.rs b/src/lib/filtering.rs index 583cbf5..d414f26 100644 --- a/src/lib/filtering.rs +++ b/src/lib/filtering.rs @@ -43,13 +43,6 @@ impl FilterOperator { match self { FilterOperator::StartsWith => format!("{}%", value), // Append `%` for "starts with" FilterOperator::EndsWith => format!("%{}", value), // Prepend `%` for "ends with" - // FilterOperator::In | FilterOperator::NotIn => { - // // Surround each value with single quotes and join with commas - // let values: Vec = value.split(',') - // .map(|v| format!("'{}'", v.trim())) - // .collect(); - // format!("({})", values.join(", ")) - // } _ => value.to_string(), } } diff --git a/tests/integration/integration_test.rs b/tests/integration/integration_test.rs index 633715d..b08d82f 100644 --- a/tests/integration/integration_test.rs +++ b/tests/integration/integration_test.rs @@ -1,284 +1,548 @@ -use crate::integration::{get_client, get_container, setup_data}; -use eyre::Result; +use chrono::NaiveDateTime; +use uuid::Uuid; +use crate::integration::run_with_container; use pg_filters::{ sorting::{SortOrder, SortedColumn}, - ColumnDef, FilteringOptions, PaginationOptions, - PgFilters, + ColumnDef, FilteringOptions, PaginationOptions, PgFilters, }; #[tokio::test] -async fn test_string_int() -> Result<()> { - let client = get_client().await; - setup_data().await; - - let filters = PgFilters::new( - Some(PaginationOptions { - current_page: 1, - per_page: 10, - per_page_limit: 10, - total_records: 1000, - }), - vec![ - SortedColumn { - column: "age".to_string(), - order: SortOrder::Desc, - }, - SortedColumn { - column: "name".to_string(), +async fn test_logical_filters() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 20, + }), + vec![ + SortedColumn { + column: "age".to_string(), + order: SortOrder::Desc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Asc, + }, + ], + Some(FilteringOptions::new(vec![ + // Less restrictive filters for logical results + (ColumnDef::Text("name"), "LIKE".to_string(), "%name1%".to_string()), + (ColumnDef::Integer("age"), ">".to_string(), "10".to_string()), + (ColumnDef::DoublePrecision("capacity"), "<=".to_string(), "15.0".to_string()), + (ColumnDef::Boolean("active"), "=".to_string(), "true".to_string()), + ])), + ) + .unwrap(); + + // Generate SQL and execute query + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + // Map results + let rows: Vec<(String, i32)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let age: i32 = row.get("age"); + (name, age) + }) + .collect(); + + // Adjust expectations to match the updated data + let expected_rows = vec![ + ("name14".to_string(), 14), + ("name12".to_string(), 12), + ]; + + // Assert results + assert_eq!(rows, expected_rows); + }) + .await; +} + +#[tokio::test] +async fn test_date_and_uuid() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 20, + }), + vec![SortedColumn { + column: "registration".to_string(), order: SortOrder::Asc, - }, - ], - Some(FilteringOptions::new(vec![ - (ColumnDef::Text("name"), "LIKE".to_string(), "%name1%".to_string()), - (ColumnDef::Integer("age"), ">".to_string(), "10".to_string()), - ])), - )?; - - let sql = filters.sql()?; - println!("Generated SQL: {}", sql); - - let query = format!("SELECT * FROM person {}", sql); - let rows = client.query(query.as_str(), &[]).await?; - - let rows: Vec<(String, i32)> = rows - .iter() - .map(|row| { - let name: String = row.get("name"); - let age: i32 = row.get("age"); - (name, age) - }) - .collect(); - - // Only expecting entries where name contains "name1" and age > 10 - let expected_rows = vec![ - ("name19".to_string(), 19), - ("name18".to_string(), 18), - ("name17".to_string(), 17), - ("name16".to_string(), 16), - ("name15".to_string(), 15), - ("name14".to_string(), 14), - ("name13".to_string(), 13), - ("name12".to_string(), 12), - ("name11".to_string(), 11), - ]; - - assert_eq!(rows, expected_rows); - get_container().await.as_ref().unwrap().stop().await?; - Ok(()) + }], + Some(FilteringOptions::new(vec![ + ( + ColumnDef::Timestamp("registration"), + ">=".to_string(), + "2023-10-10 12:00:00".to_string(), + ), + ( + ColumnDef::Uuid("uuid"), + "IN".to_string(), + "550e8400-e29b-41d4-a716-446655440001,550e8400-e29b-41d4-a716-446655440003" + .to_string(), + ), + ])), + ) + .unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, NaiveDateTime, Uuid)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let registration: NaiveDateTime = row.get("registration"); + let uuid: Uuid = row.get("uuid"); + (name, registration, uuid) + }) + .collect(); + + let expected_rows = vec![ + ( + "name11".to_string(), + NaiveDateTime::parse_from_str("2023-10-12 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), + Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap(), + ), + ( + "name13".to_string(), + NaiveDateTime::parse_from_str("2023-10-14 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(), + Uuid::parse_str("550e8400-e29b-41d4-a716-446655440003").unwrap(), + ), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; } #[tokio::test] -async fn test_float_bool() -> Result<()> { - let client = get_client().await; - setup_data().await; - - let filters = PgFilters::new( - Some(PaginationOptions { - current_page: 1, - per_page: 10, - per_page_limit: 10, - total_records: 1000, - }), - vec![ - SortedColumn { +async fn test_boolean_and_capacity() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 20, + }), + vec![SortedColumn { column: "capacity".to_string(), order: SortOrder::Desc, - }, - SortedColumn { - column: "name".to_string(), - order: SortOrder::Asc, - }, - ], - Some(FilteringOptions::new(vec![ - (ColumnDef::Boolean("active"), "=".to_string(), "true".to_string()), - (ColumnDef::DoublePrecision("capacity"), ">".to_string(), "2".to_string()), - (ColumnDef::DoublePrecision("capacity"), "<".to_string(), "6".to_string()), - ])), - )?; - - let sql = filters.sql()?; - println!("Generated SQL: {}", sql); - - let query = format!("SELECT * FROM person {}", sql); - let rows = client.query(query.as_str(), &[]).await?; - - let rows: Vec<(String, f64, bool)> = rows - .iter() - .map(|row| { - let name: String = row.get("name"); - let capacity: f64 = row.get("capacity"); - let active: bool = row.get("active"); - (name, capacity, active) - }) - .collect(); - - // Expecting entries with even index (active=true) and capacity between 2 and 6 - let expected_rows = vec![ - ("name4".to_string(), 4.0, true), - ]; - - assert_eq!(rows, expected_rows); - get_container().await.as_ref().unwrap().stop().await?; - Ok(()) + }], + Some(FilteringOptions::new(vec![ + (ColumnDef::Boolean("active"), "=".to_string(), "true".to_string()), + (ColumnDef::DoublePrecision("capacity"), "<=".to_string(), "10.0".to_string()), + ])), + ) + .unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, f64, bool)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let capacity: f64 = row.get("capacity"); + let active: bool = row.get("active"); + (name, capacity, active) + }) + .collect(); + + let expected_rows = vec![ + ("name10".to_string(), 10.0, true), + ("name8".to_string(), 8.0, true), + ("name6".to_string(), 6.0, true), + ("name4".to_string(), 4.0, true), + ("name2".to_string(), 2.0, true), + ("name0".to_string(), 0.0, true), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; } #[tokio::test] -async fn test_in() -> Result<()> { - let client = get_client().await; - setup_data().await; - - // Use `FilteringOptions` with a comma-separated string for `IN` values - let filters = PgFilters::new( - Some(PaginationOptions { - current_page: 1, - per_page: 10, - per_page_limit: 10, - total_records: 1000, - }), - vec![ - SortedColumn { - column: "age".to_string(), - order: SortOrder::Desc, - }, - SortedColumn { - column: "name".to_string(), - order: SortOrder::Asc, - }, - ], - Some(FilteringOptions::new(vec![ - (ColumnDef::Integer("age"), "IN".to_string(), "11,12,13".to_string()), - ])), - )?; - - let sql = filters.sql()?; - println!("Generated SQL: {}", sql); - - let query = format!("SELECT * FROM person {}", sql); - let rows = client.query(query.as_str(), &[]).await?; - - let rows: Vec<(String, i32)> = rows - .iter() - .map(|row| { - let name: String = row.get("name"); - let age: i32 = row.get("age"); - (name, age) - }) - .collect(); - - let expected_rows = vec![ - ("name13".to_string(), 13), - ("name12".to_string(), 12), - ("name11".to_string(), 11), - ]; - - assert_eq!(rows, expected_rows); - get_container().await.as_ref().unwrap().stop().await?; - Ok(()) +async fn test_name_and_age() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 20, + }), + vec![ + SortedColumn { + column: "age".to_string(), + order: SortOrder::Asc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Desc, + }, + ], + Some(FilteringOptions::new(vec![ + (ColumnDef::Text("name"), "LIKE".to_string(), "%name%".to_string()), + (ColumnDef::Integer("age"), ">".to_string(), "5".to_string()), + ])), + ) + .unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, i32)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let age: i32 = row.get("age"); + (name, age) + }) + .collect(); + + let expected_rows = vec![ + ("name6".to_string(), 6), + ("name7".to_string(), 7), + ("name8".to_string(), 8), + ("name9".to_string(), 9), + ("name10".to_string(), 10), + ("name11".to_string(), 11), + ("name12".to_string(), 12), + ("name13".to_string(), 13), + ("name14".to_string(), 14), + ("name15".to_string(), 15), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; } #[tokio::test] -async fn test_starts_with() -> Result<()> { - let client = get_client().await; - setup_data().await; - - let filters = PgFilters::new( - Some(PaginationOptions { - current_page: 1, - per_page: 10, - per_page_limit: 10, - total_records: 1000, - }), - vec![ - SortedColumn { - column: "age".to_string(), - order: SortOrder::Desc, - }, - SortedColumn { - column: "name".to_string(), - order: SortOrder::Asc, - }, - ], - Some(FilteringOptions::new(vec![ - (ColumnDef::Text("name"), "STARTS WITH".to_string(), "name1".to_string()), - (ColumnDef::Integer("age"), ">=".to_string(), "17".to_string()), - ])), - )?; - - let sql = filters.sql()?; - println!("Generated SQL: {}", sql); - - let query = format!("SELECT * FROM person {}", sql); - let rows = client.query(query.as_str(), &[]).await?; - - let rows: Vec<(String, i32)> = rows - .iter() - .map(|row| { - let name: String = row.get("name"); - let age: i32 = row.get("age"); - (name, age) - }) - .collect(); - - let expected_rows = vec![ - ("name19".to_string(), 19), - ("name18".to_string(), 18), - ("name17".to_string(), 17), - ]; - - assert_eq!(rows, expected_rows); - get_container().await.as_ref().unwrap().stop().await?; - Ok(()) +async fn test_string_int() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 1000, + }), + vec![ + SortedColumn { + column: "age".to_string(), + order: SortOrder::Desc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Asc, + }, + ], + Some(FilteringOptions::new(vec![ + ( + ColumnDef::Text("name"), + "LIKE".to_string(), + "%name1%".to_string(), + ), + (ColumnDef::Integer("age"), ">".to_string(), "10".to_string()), + ])), + ) + .unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, i32)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let age: i32 = row.get("age"); + (name, age) + }) + .collect(); + + // Only expecting entries where name contains "name1" and age > 10 + let expected_rows = vec![ + ("name19".to_string(), 19), + ("name18".to_string(), 18), + ("name17".to_string(), 17), + ("name16".to_string(), 16), + ("name15".to_string(), 15), + ("name14".to_string(), 14), + ("name13".to_string(), 13), + ("name12".to_string(), 12), + ("name11".to_string(), 11), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; +} + +#[tokio::test] +async fn test_float_bool() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 1000, + }), + vec![ + SortedColumn { + column: "capacity".to_string(), + order: SortOrder::Desc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Asc, + }, + ], + Some(FilteringOptions::new(vec![ + ( + ColumnDef::Boolean("active"), + "=".to_string(), + "true".to_string(), + ), + ( + ColumnDef::DoublePrecision("capacity"), + ">".to_string(), + "2".to_string(), + ), + ( + ColumnDef::DoublePrecision("capacity"), + "<".to_string(), + "6".to_string(), + ), + ])), + ).unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, f64, bool)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let capacity: f64 = row.get("capacity"); + let active: bool = row.get("active"); + (name, capacity, active) + }) + .collect(); + + // Expecting entries with even index (active=true) and capacity between 2 and 6 + let expected_rows = vec![("name4".to_string(), 4.0, true)]; + + assert_eq!(rows, expected_rows); + }) + .await; } #[tokio::test] -async fn test_text_search() -> Result<()> { - let client = get_client().await; - setup_data().await; - - let filters = PgFilters::new( - Some(PaginationOptions { - current_page: 1, - per_page: 5, - per_page_limit: 10, - total_records: 1000, - }), - vec![ - SortedColumn { +async fn test_in() { + run_with_container(|pool| async move { + // Use `FilteringOptions` with a comma-separated string for `IN` values + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 1000, + }), + vec![ + SortedColumn { + column: "age".to_string(), + order: SortOrder::Desc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Asc, + }, + ], + Some(FilteringOptions::new(vec![( + ColumnDef::Integer("age"), + "IN".to_string(), + "11,12,13".to_string(), + )])), + ).unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, i32)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let age: i32 = row.get("age"); + (name, age) + }) + .collect(); + + let expected_rows = vec![ + ("name13".to_string(), 13), + ("name12".to_string(), 12), + ("name11".to_string(), 11), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; +} + +#[tokio::test] +async fn test_starts_with() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 10, + per_page_limit: 10, + total_records: 1000, + }), + vec![ + SortedColumn { + column: "age".to_string(), + order: SortOrder::Desc, + }, + SortedColumn { + column: "name".to_string(), + order: SortOrder::Asc, + }, + ], + Some(FilteringOptions::new(vec![ + ( + ColumnDef::Text("name"), + "STARTS WITH".to_string(), + "name1".to_string(), + ), + ( + ColumnDef::Integer("age"), + ">=".to_string(), + "17".to_string(), + ), + ])), + ).unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, i32)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let age: i32 = row.get("age"); + (name, age) + }) + .collect(); + + let expected_rows = vec![ + ("name19".to_string(), 19), + ("name18".to_string(), 18), + ("name17".to_string(), 17), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; +} + +#[tokio::test] +async fn test_text_search() { + run_with_container(|pool| async move { + let filters = PgFilters::new( + Some(PaginationOptions { + current_page: 1, + per_page: 5, + per_page_limit: 10, + total_records: 1000, + }), + vec![SortedColumn { column: "name".to_string(), order: SortOrder::Asc, - }, - ], - Some(FilteringOptions::new(vec![ - (ColumnDef::Text("name"), "LIKE".to_string(), "%name%".to_string()), - (ColumnDef::Text("nickname"), "LIKE".to_string(), "%nickname1%".to_string()), - ])), - )?; - - let sql = filters.sql()?; - println!("Generated SQL: {}", sql); - - let query = format!("SELECT * FROM person {}", sql); - let rows = client.query(query.as_str(), &[]).await?; - - let rows: Vec<(String, String)> = rows - .iter() - .map(|row| { - let name: String = row.get("name"); - let nickname: String = row.get("nickname"); - (name, nickname) - }) - .collect(); - - // Expecting entries where both name contains "name" and nickname contains "nickname1" - let expected_rows = vec![ - ("name1".to_string(), "nickname1".to_string()), - ("name10".to_string(), "nickname10".to_string()), - ("name11".to_string(), "nickname11".to_string()), - ("name12".to_string(), "nickname12".to_string()), - ("name13".to_string(), "nickname13".to_string()), - ]; - - assert_eq!(rows, expected_rows); - get_container().await.as_ref().unwrap().stop().await?; - Ok(()) -} \ No newline at end of file + }], + Some(FilteringOptions::new(vec![ + ( + ColumnDef::Text("name"), + "LIKE".to_string(), + "%name%".to_string(), + ), + ( + ColumnDef::Text("nickname"), + "LIKE".to_string(), + "%nickname1%".to_string(), + ), + ])), + ).unwrap(); + + let sql = filters.sql().unwrap(); + println!("Generated SQL: {}", sql); + + let query = format!("SELECT * FROM person {}", sql); + let client = pool.get().await.unwrap(); + let rows = client.query(query.as_str(), &[]).await.unwrap(); + + let rows: Vec<(String, String)> = rows + .iter() + .map(|row| { + let name: String = row.get("name"); + let nickname: String = row.get("nickname"); + (name, nickname) + }) + .collect(); + + // Expecting entries where both name contains "name" and nickname contains "nickname1" + let expected_rows = vec![ + ("name1".to_string(), "nickname1".to_string()), + ("name10".to_string(), "nickname10".to_string()), + ("name11".to_string(), "nickname11".to_string()), + ("name12".to_string(), "nickname12".to_string()), + ("name13".to_string(), "nickname13".to_string()), + ]; + + assert_eq!(rows, expected_rows); + }) + .await; +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index d86786d..4d88ff7 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -1,84 +1,110 @@ +use chrono::NaiveDateTime; +use deadpool::managed::Timeouts; +use deadpool::Runtime; +use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod}; +use std::str::FromStr; +use std::time::Duration; use testcontainers_modules::postgres::Postgres; -use testcontainers_modules::testcontainers::core::error; use testcontainers_modules::testcontainers::runners::AsyncRunner; -use testcontainers_modules::testcontainers::ContainerAsync; -use tokio::sync::OnceCell; -use tokio_postgres::{Client, NoTls}; +use tokio_postgres::{Config, NoTls}; +use uuid::Uuid; pub mod integration_test; -async fn get_client() -> Client { - let container = get_container().await.as_ref().unwrap(); - let port = container.get_host_port_ipv4(5432).await.unwrap(); - let host = container.get_host().await.unwrap(); - let connection_string = format!( - "host={} user=postgres password=postgres port={}", - host, port - ); +const DB_POOL_MAX_OPEN: u64 = 10; +const DB_POOL_TIMEOUT_SECONDS: u64 = 10; - let (client, connection) = tokio_postgres::connect(connection_string.as_str(), NoTls) - .await - .unwrap(); +fn try_get_pool(db_url: String) -> eyre::Result { + let config = Config::from_str(&db_url).map_err(|e| eyre::eyre!(e))?; - tokio::spawn(async move { - if let Err(e) = connection.await { - eprintln!("connection error: {}", e); - } - }); - client + let manager_config = ManagerConfig { + recycling_method: RecyclingMethod::Fast, + }; + let manager = Manager::from_config(config, NoTls, manager_config); + let pool = Pool::builder(manager) + .max_size(DB_POOL_MAX_OPEN as usize) + .runtime(Runtime::Tokio1) + .timeouts(Timeouts { + wait: Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)), + create: Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)), + recycle: Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)), + }) + .build(); + pool.map_err(|e| eyre::eyre!(e)) } -pub async fn get_container() -> &'static error::Result> { - static ONCE: OnceCell>> = OnceCell::const_new(); - ONCE.get_or_init(|| async { Postgres::default().start().await }) - .await -} +async fn setup_data(pool: &Pool) -> eyre::Result<()> { + let client = pool.get().await?; -pub async fn setup_data() -> &'static bool { - static ONCE: OnceCell = OnceCell::const_new(); - ONCE.get_or_init(|| async { _setup_data().await }).await -} - -async fn _setup_data() -> bool { - println!("Setting up data"); - create_table().await; - create_rows().await; - true -} - -async fn create_table() { - let client = get_client().await; client .execute( "CREATE TABLE person ( - id SERIAL PRIMARY KEY, - name TEXT NOT NULL, - nickname VARCHAR(200) NOT NULL, - age INTEGER NOT NULL, - capacity DOUBLE PRECISION NOT NULL, - active BOOLEAN NOT NULL - )", + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + nickname VARCHAR(200) NOT NULL, + age INTEGER NOT NULL, + capacity DOUBLE PRECISION NOT NULL, + active BOOLEAN NOT NULL, + registration TIMESTAMP NOT NULL, + uuid UUID NOT NULL + )", &[], ) - .await - .unwrap(); -} + .await?; -async fn create_rows() { - let client = get_client().await; for i in 0..20 { + let registration_date = NaiveDateTime::parse_from_str( + &format!("2023-10-{:02} 12:00:00", (i + 1)), + "%Y-%m-%d %H:%M:%S", + )?; + + let uuid = Uuid::parse_str(&format!("550e8400-e29b-41d4-a716-44665544000{}", i % 10))?; + client .execute( - "INSERT INTO person (name, nickname, age, capacity, active) VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO person ( + name, nickname, age, capacity, active, registration, uuid + ) VALUES ($1, $2, $3, $4, $5, $6, $7)", &[ &format!("name{}", i), &format!("nickname{}", i), - &i, + &(i), &(i as f64), &(i % 2 == 0), + ®istration_date, + &uuid, ], ) - .await - .unwrap(); + .await?; + } + Ok(()) +} + +pub async fn run_with_container(test: F) +where + F: FnOnce(Pool) -> Fut, + Fut: std::future::Future, +{ + // Start a new container for each test + let container = Postgres::default() + .start() + .await + .expect("Failed to start container"); + + let port = container.get_host_port_ipv4(5432).await.unwrap(); + let host = container.get_host().await.unwrap(); + let db_url = format!("postgres://postgres:postgres@{}:{}/postgres", host, port); + + let pool = try_get_pool(db_url).expect("Unable to create pool"); + + // Set up schema for this test instance + setup_data(&pool).await.expect("Failed to setup schema"); + + // Run the test + test(pool.clone()).await; + + // Clean up container + if let Err(err) = container.stop().await { + eprintln!("Failed to stop container: {:?}", err); } }