diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ead36e..5122a854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed a bug where [timeline chart tooltips displayed the wrong labels](https://github.com/sqlpage/SQLPage/issues/659). - Fixed an incorrect warning polluting logs when using sqlpage functions with json arguments in sqlite: `WARN sqlpage::webserver::database::execute_queries] The column _sqlpage_f0_a1 is missing from the result set, so it cannot be converted to JSON.`. - Fixed Microsoft SQL Server driver not being able to read VARCHAR columns from databases with non-european collations. +- Added support for `BIT` columns in Microsoft SQL Server. ## 0.30.1 (2024-10-31) - fix a bug where table sorting would break if table search was not also enabled. diff --git a/Cargo.lock b/Cargo.lock index fe3a22f5..bc44050e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -104,7 +104,7 @@ dependencies = [ "parse-size", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -245,7 +245,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -528,7 +528,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -550,7 +550,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -567,7 +567,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -798,9 +798,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.31" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "jobserver", "libc", @@ -858,7 +858,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1088,7 +1088,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1099,7 +1099,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1158,7 +1158,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1202,7 +1202,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1428,7 +1428,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2301,7 +2301,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2332,7 +2332,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2776,7 +2776,7 @@ checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2985,14 +2985,14 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] name = "sqlx-core-oldapi" -version = "0.6.30" +version = "0.6.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d300a966e0d42da5e0efff2cb2cf289b0df9e5d6be9859b716060469da12329" +checksum = "549c5d36f47ea1468260b164561d3824319199e6f9df22be3917e083273540ca" dependencies = [ "ahash", "atoi", @@ -3054,9 +3054,9 @@ dependencies = [ [[package]] name = "sqlx-macros-oldapi" -version = "0.6.30" +version = "0.6.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393d2e9a261be031d7707ac53d128ba2c655097be4a74d087027468014e8512a" +checksum = "5b4e97bfb6525ede22cefeb01166f59b5156fc288c946c3e16dae691c9ae8783" dependencies = [ "dotenvy", "either", @@ -3074,9 +3074,9 @@ dependencies = [ [[package]] name = "sqlx-oldapi" -version = "0.6.30" +version = "0.6.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cde0a390c5079a7d0bca3ea3b900e246c618b176b2606ec586b1cc711312cd5" +checksum = "f6db6ce44bac821c06994efc399f243904390fded53c71cd8cb27017b1221233" dependencies = [ "sqlx-core-oldapi", "sqlx-macros-oldapi", @@ -3084,9 +3084,9 @@ dependencies = [ [[package]] name = "sqlx-rt-oldapi" -version = "0.6.30" +version = "0.6.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870a763531da2e47a4efca6b6483c4ae57aedeafe5c57edef05b6063c0923278" +checksum = "bb59aa67aa1b9b6f79a82f0f234029e47cdb47c3f17f0b57a5c2e1d2c6d8ed77" dependencies = [ "once_cell", "tokio", @@ -3129,9 +3129,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -3180,7 +3180,7 @@ checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3264,7 +3264,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3384,7 +3384,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3555,7 +3555,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -3589,7 +3589,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3851,7 +3851,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 86a21283..9c2fc0e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ panic = "abort" codegen-units = 2 [dependencies] -sqlx = { package = "sqlx-oldapi", version = "0.6.30", features = [ +sqlx = { package = "sqlx-oldapi", version = "0.6.32", features = [ "any", "runtime-actix-rustls", "sqlite", diff --git a/docker-compose.yml b/docker-compose.yml index f6a753f4..8c04db65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,9 @@ # You can easily switch between different databases by changing the value of COMPOSE_PROFILES in the .env file. + +# possible database connection strings: +# DATABASE_URL='postgres://root:Password123!@localhost/sqlpage' +# DATABASE_URL='mssql://root:Password123!@localhost/sqlpage' +# DATABASE_URL='mysql://root:Password123!@localhost/sqlpage' services: web: build: { context: "." } diff --git a/src/webserver/database/sql_to_json.rs b/src/webserver/database/sql_to_json.rs index 3c338beb..6a293c8d 100644 --- a/src/webserver/database/sql_to_json.rs +++ b/src/webserver/database/sql_to_json.rs @@ -1,7 +1,7 @@ use crate::utils::add_value_to_map; use chrono::{DateTime, Utc}; use serde_json::{self, Map, Value}; -use sqlx::any::AnyRow; +use sqlx::any::{AnyRow, AnyTypeInfo, AnyTypeInfoKind}; use sqlx::Decode; use sqlx::{Column, Row, TypeInfo, ValueRef}; @@ -63,6 +63,11 @@ pub fn sql_nonnull_to_json<'r>(mut get_ref: impl FnMut() -> sqlx::any::AnyValueR "BOOL" | "BOOLEAN" => >::decode(raw_value) .unwrap_or_default() .into(), + "BIT" if matches!(*type_info, AnyTypeInfo(AnyTypeInfoKind::Mssql(_))) => { + >::decode(raw_value) + .unwrap_or_default() + .into() + } "DATE" => >::decode(raw_value) .as_ref() .map_or_else(std::string::ToString::to_string, ToString::to_string) @@ -103,30 +108,228 @@ pub fn row_to_string(row: &AnyRow) -> Option { } } -#[actix_web::test] -async fn test_row_to_json() -> anyhow::Result<()> { +#[cfg(test)] +mod tests { + use super::*; use sqlx::Connection; - let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://:memory:".to_string()); - let mut c = sqlx::AnyConnection::connect(&db_url).await?; - let row = sqlx::query( - "SELECT \ - 123.456 as one_value, \ - 1 as two_values, \ - 2 as two_values, \ - 'x' as three_values, \ - 'y' as three_values, \ - 'z' as three_values \ - ", - ) - .fetch_one(&mut c) - .await?; - assert_eq!( - row_to_json(&row), - serde_json::json!({ - "one_value": 123.456, - "two_values": [1,2], - "three_values": ["x","y","z"], - }) - ); - Ok(()) + + fn db_specific_test(db_type: &str) -> Option { + let db_url = + std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://:memory:".to_string()); + if db_url.starts_with(db_type) { + Some(db_url) + } else { + log::warn!("Skipping test because DATABASE_URL is not set to a {db_type} database"); + None + } + } + + #[actix_web::test] + async fn test_row_to_json() -> anyhow::Result<()> { + use sqlx::Connection; + let db_url = + std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://:memory:".to_string()); + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT \ + 123.456 as one_value, \ + 1 as two_values, \ + 2 as two_values, \ + 'x' as three_values, \ + 'y' as three_values, \ + 'z' as three_values \ + ", + ) + .fetch_one(&mut c) + .await?; + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "one_value": 123.456, + "two_values": [1,2], + "three_values": ["x","y","z"], + }) + ); + Ok(()) + } + + #[actix_web::test] + async fn test_mssql_bit_to_json() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("mssql") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query("SELECT CAST(1 AS BIT) as true_bit, CAST(0 AS BIT) as false_bit, CAST(NULL AS BIT) as null_bit") + .fetch_one(&mut c) + .await?; + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "true_bit": true, + "false_bit": false, + "null_bit": null, + }) + ); + Ok(()) + } + + #[actix_web::test] + async fn test_postgres_types() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("postgres") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT + 42::INT2 as small_int, + 42::INT4 as integer, + 42::INT8 as big_int, + 42.42::FLOAT4 as float4, + 42.42::FLOAT8 as float8, + TRUE as boolean, + '2024-03-14'::DATE as date, + '13:14:15'::TIME as time, + '2024-03-14 13:14:15'::TIMESTAMP as timestamp, + '2024-03-14 13:14:15+00'::TIMESTAMPTZ as timestamptz, + '{\"key\": \"value\"}'::JSON as json, + '{\"key\": \"value\"}'::JSONB as jsonb", + ) + .fetch_one(&mut c) + .await?; + + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "small_int": 42, + "integer": 42, + "big_int": 42, + "float4": 42.42, + "float8": 42.42, + "boolean": true, + "date": "2024-03-14", + "time": "13:14:15", + "timestamp": "2024-03-14T13:14:15+00:00", + "timestamptz": "2024-03-14T13:14:15+00:00", + "json": {"key": "value"}, + "jsonb": {"key": "value"}, + }) + ); + Ok(()) + } + + #[actix_web::test] + async fn test_mysql_types() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("mysql") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT + CAST(42 AS SIGNED) as signed_int, + CAST(42 AS UNSIGNED) as unsigned_int, + 42.42 as decimal_number, + TRUE as boolean, + CAST('2024-03-14' AS DATE) as date, + CAST('13:14:15' AS TIME) as time, + CAST('2024-03-14 13:14:15' AS DATETIME) as datetime, + CAST('{\"key\": \"value\"}' AS JSON) as json", + ) + .fetch_one(&mut c) + .await?; + + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "signed_int": 42, + "unsigned_int": 42, + "decimal_number": 42.42, + "boolean": true, + "date": "2024-03-14", + "time": "13:14:15", + "datetime": "2024-03-14T13:14:15+00:00", + "json": {"key": "value"}, + }) + ); + Ok(()) + } + + #[actix_web::test] + async fn test_sqlite_types() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("sqlite") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT + 42 as integer, + 42.42 as real, + 'xxx' as string, + x'68656c6c6f20776f726c64' as blob", + ) + .fetch_one(&mut c) + .await?; + + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "integer": 42, + "real": 42.42, + "string": "xxx", + "blob": "hello world", + }) + ); + Ok(()) + } + + #[actix_web::test] + async fn test_mssql_types() -> anyhow::Result<()> { + let Some(db_url) = db_specific_test("mssql") else { + return Ok(()); + }; + let mut c = sqlx::AnyConnection::connect(&db_url).await?; + let row = sqlx::query( + "SELECT + CAST(1 AS BIT) as true_bit, + CAST(0 AS BIT) as false_bit, + CAST(NULL AS BIT) as null_bit, + CAST(42 AS SMALLINT) as small_int, + CAST(42 AS INT) as integer, + CAST(42 AS BIGINT) as big_int, + CAST(42.42 AS REAL) as real, + CAST(42.42 AS FLOAT) as float, + CAST(42.42 AS DECIMAL(10,2)) as decimal, + CAST('2024-03-14' AS DATE) as date, + CAST('13:14:15' AS TIME) as time, + CAST('2024-03-14 13:14:15' AS DATETIME) as datetime, + CAST('2024-03-14 13:14:15' AS DATETIME2) as datetime2, + CAST('2024-03-14 13:14:15 +00:00' AS DATETIMEOFFSET) as datetimeoffset, + N'Unicode String' as nvarchar, + 'ASCII String' as varchar", + ) + .fetch_one(&mut c) + .await?; + + assert_eq!( + row_to_json(&row), + serde_json::json!({ + "true_bit": true, + "false_bit": false, + "null_bit": null, + "small_int": 42, + "integer": 42, + "big_int": 42, + "real": 42.42, + "float": 42.42, + "decimal": 42.42, + "date": "2024-03-14", + "time": "13:14:15", + "datetime": "2024-03-14T13:14:15+00:00", + "datetime2": "2024-03-14T13:14:15+00:00", + "datetimeoffset": "2024-03-14T13:14:15+00:00", + "nvarchar": "Unicode String", + "varchar": "ASCII String", + }) + ); + Ok(()) + } }