Skip to content

Commit 746c70c

Browse files
committed
Refactor SQL column handling to ensure consistent JSON output
- Updated column name handling in `row_to_json` to use lowercase for ODBC databases. - Added `canonical_col_name` function to standardize column names. - Modified SQL queries in tests to use lowercase column names for consistency. - Enhanced tests for edge cases and variations in column naming.
1 parent 080cd75 commit 746c70c

File tree

3 files changed

+126
-18
lines changed

3 files changed

+126
-18
lines changed

src/webserver/database/sql_to_json.rs

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::webserver::database::blob_to_data_url;
33
use bigdecimal::{BigDecimal, ToPrimitive};
44
use chrono::{DateTime, FixedOffset, NaiveDateTime};
55
use serde_json::{self, Map, Value};
6-
use sqlx::any::{AnyRow, AnyTypeInfo, AnyTypeInfoKind};
6+
use sqlx::any::{AnyColumn, AnyRow, AnyTypeInfo, AnyTypeInfoKind};
77
use sqlx::Decode;
88
use sqlx::{Column, Row, TypeInfo, ValueRef};
99

@@ -13,13 +13,27 @@ pub fn row_to_json(row: &AnyRow) -> Value {
1313
let columns = row.columns();
1414
let mut map = Map::new();
1515
for col in columns {
16-
let key = col.name().to_string();
16+
let key = canonical_col_name(col);
1717
let value: Value = sql_to_json(row, col);
1818
map = add_value_to_map(map, (key, value));
1919
}
2020
Object(map)
2121
}
2222

23+
fn canonical_col_name(col: &AnyColumn) -> String {
24+
// Some databases fold all unquoted identifiers to uppercase but SQLPage uses lowercase property names
25+
if matches!(col.type_info().0, AnyTypeInfoKind::Odbc(_))
26+
&& col
27+
.name()
28+
.chars()
29+
.all(|c| c.is_ascii_uppercase() || c == '_')
30+
{
31+
col.name().to_ascii_lowercase()
32+
} else {
33+
col.name().to_owned()
34+
}
35+
}
36+
2337
pub fn sql_to_json(row: &AnyRow, col: &sqlx::any::AnyColumn) -> Value {
2438
let raw_value_result = row.try_get_raw(col.ordinal());
2539
match raw_value_result {
@@ -160,23 +174,23 @@ mod tests {
160174
let db_url = test_database_url();
161175
let mut c = sqlx::AnyConnection::connect(&db_url).await?;
162176
let row = sqlx::query(
163-
r#"SELECT
164-
123.456 as "ONE_VALUE",
165-
1 as "TWO_VALUES",
166-
2 as "TWO_VALUES",
167-
'x' as "THREE_VALUES",
168-
'y' as "THREE_VALUES",
169-
'z' as "THREE_VALUES"
170-
"#,
177+
"SELECT
178+
123.456 as one_value,
179+
1 as two_values,
180+
2 as two_values,
181+
'x' as three_values,
182+
'y' as three_values,
183+
'z' as three_values
184+
",
171185
)
172186
.fetch_one(&mut c)
173187
.await?;
174188
expect_json_object_equal(
175189
&row_to_json(&row),
176190
&serde_json::json!({
177-
"ONE_VALUE": 123.456,
178-
"TWO_VALUES": [1,2],
179-
"THREE_VALUES": ["x","y","z"],
191+
"one_value": 123.456,
192+
"two_values": [1,2],
193+
"three_values": ["x","y","z"],
180194
}),
181195
);
182196
Ok(())
@@ -506,6 +520,100 @@ mod tests {
506520
);
507521
}
508522

523+
#[actix_web::test]
524+
async fn test_canonical_col_name_variations() -> anyhow::Result<()> {
525+
let db_url = test_database_url();
526+
let mut c = sqlx::AnyConnection::connect(&db_url).await?;
527+
528+
// Test various column name formats to ensure canonical_col_name works correctly
529+
let row = sqlx::query(
530+
r#"SELECT
531+
42 as "UPPERCASE_COL",
532+
42 as "lowercase_col",
533+
42 as "Mixed_Case_Col",
534+
42 as "COL_WITH_123_NUMBERS",
535+
42 as "col-with-dashes",
536+
42 as "col with spaces",
537+
42 as "_UNDERSCORE_PREFIX",
538+
42 as "123_NUMBER_PREFIX"
539+
"#,
540+
)
541+
.fetch_one(&mut c)
542+
.await?;
543+
544+
let json_result = row_to_json(&row);
545+
546+
// For ODBC databases, uppercase columns should be converted to lowercase
547+
// For other databases, names should remain as-is
548+
let expected_json = if c.kind() == sqlx::any::AnyKind::Odbc {
549+
// ODBC database - uppercase should be converted to lowercase
550+
serde_json::json!({
551+
"uppercase_col": 42,
552+
"lowercase_col": 42,
553+
"Mixed_Case_Col": 42,
554+
"COL_WITH_123_NUMBERS": 42,
555+
"col-with-dashes": 42,
556+
"col with spaces": 42,
557+
"_underscore_prefix": 42,
558+
"123_NUMBER_PREFIX": 42
559+
})
560+
} else {
561+
// Non-ODBC database - names remain as-is
562+
serde_json::json!({
563+
"UPPERCASE_COL": 42,
564+
"lowercase_col": 42,
565+
"Mixed_Case_Col": 42,
566+
"COL_WITH_123_NUMBERS": 42,
567+
"col-with-dashes": 42,
568+
"col with spaces": 42,
569+
"_UNDERSCORE_PREFIX": 42,
570+
"123_NUMBER_PREFIX": 42
571+
})
572+
};
573+
574+
expect_json_object_equal(&json_result, &expected_json);
575+
576+
Ok(())
577+
}
578+
579+
#[actix_web::test]
580+
async fn test_row_to_json_edge_cases() -> anyhow::Result<()> {
581+
let db_url = test_database_url();
582+
let mut c = sqlx::AnyConnection::connect(&db_url).await?;
583+
584+
// Test edge cases for row_to_json
585+
let row = sqlx::query(
586+
"SELECT
587+
NULL as null_col,
588+
'' as empty_string,
589+
0 as zero_value,
590+
-42 as negative_int,
591+
1.23456 as my_float,
592+
'special_chars_!@#$%^&*()' as special_chars,
593+
'line1
594+
line2' as multiline_string
595+
",
596+
)
597+
.fetch_one(&mut c)
598+
.await?;
599+
600+
let json_result = row_to_json(&row);
601+
602+
let expected_json = serde_json::json!({
603+
"null_col": null,
604+
"empty_string": "",
605+
"zero_value": 0,
606+
"negative_int": -42,
607+
"my_float": 1.23456,
608+
"special_chars": "special_chars_!@#$%^&*()",
609+
"multiline_string": "line1\nline2"
610+
});
611+
612+
expect_json_object_equal(&json_result, &expected_json);
613+
614+
Ok(())
615+
}
616+
509617
/// Compare JSON values, treating integers and floats that are numerically equal as equal
510618
fn json_values_equal(a: &Value, b: &Value) -> bool {
511619
use Value::*;

tests/data_formats/csv_data.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ select
33
';' as separator;
44

55
select
6-
0 as "ID",
7-
'Hello World !' as "MSG"
6+
0 as id,
7+
'Hello World !' as msg
88
union all
99
select
10-
1 as "ID",
11-
'Tu gères '';'' et ''"'' ?' as "MSG";
10+
1 as id,
11+
'Tu gères '';'' et ''"'' ?' as msg;

tests/data_formats/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async fn test_csv_body() -> actix_web::Result<()> {
4343
let body_str = String::from_utf8(body.to_vec()).unwrap();
4444
assert_eq!(
4545
body_str,
46-
"ID;MSG\n0;Hello World !\n1;\"Tu gères ';' et '\"\"' ?\"\n"
46+
"id;msg\n0;Hello World !\n1;\"Tu gères ';' et '\"\"' ?\"\n"
4747
);
4848
Ok(())
4949
}

0 commit comments

Comments
 (0)