fix(data-grid): serialize JSON documents in copied SQL assignments

Closes #6871
This commit is contained in:
t8y2 committed 2026-08-21 07:59:26 +00:00
1 parent 0daa23ed8f
commit 49296c8930
1 file changed
+157 -23
+157 -23
View File
@@ -399,7 +399,11 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
format!(
"{} = {}",
data_grid_identifier(options.database_type, column, options.identifier_quote.as_deref()),
format_grid_sql_literal(row.get(*index).unwrap_or(&Value::Null), options.database_type, *info)
format_grid_assignment_sql_literal(
row.get(*index).unwrap_or(&Value::Null),
options.database_type,
*info,
)
)
})
.collect::<Vec<_>>()
@@ -1486,7 +1490,7 @@ fn build_data_grid_rollback_statements(
let values = insert_pairs
.iter()
.map(|(column, value)| {
format_grid_sql_literal(value, options.database_type, column_info_for(column_info, column))
format_grid_assignment_sql_literal(value, options.database_type, column_info_for(column_info, column))
})
.collect::<Vec<_>>()
.join(", ");
@@ -1544,7 +1548,7 @@ fn build_data_grid_rollback_statements(
format!(
"{} = {}",
data_grid_identifier(options.database_type, column, options.identifier_quote.as_deref()),
format_grid_sql_literal(
format_grid_assignment_sql_literal(
row.get(*column_index).unwrap_or(&Value::Null),
options.database_type,
column_info_for(column_info, column)
@@ -1837,7 +1841,7 @@ fn build_hive_values_insert(
if save_literals {
format_grid_save_sql_literal(value, options.database_type, info)
} else {
format_grid_sql_literal(value, options.database_type, info)
format_grid_assignment_sql_literal(value, options.database_type, info)
}
})
.collect::<Vec<_>>()
@@ -1905,23 +1909,27 @@ fn format_grid_copy_insert_sql_literal(
}
}
}
// JSON columns may expose a JSON array/object value (e.g. `[1,2,3]` or `{}`)
// instead of its string form. Keep it as a single JSON literal rather than
// letting format_grid_sql_literal serialize it as a PostgreSQL-style array
// (`{...}`). Serialize the value back to compact JSON text, format that as a
// string literal, then cast it for MySQL so it inserts as JSON.
if column_info.is_some_and(|column| {
let dt = column.data_type.trim();
dt.eq_ignore_ascii_case("json") || dt.eq_ignore_ascii_case("jsonb")
}) && (value.is_array() || value.is_object())
{
let json_text = value.to_string();
let string_literal = format_grid_sql_literal(&Value::String(json_text), database_type, column_info);
return mysql_json_predicate_literal(string_literal, database_type, column_info);
format_grid_assignment_sql_literal(value, database_type, column_info)
}
fn format_grid_assignment_sql_literal(
value: &Value,
database_type: Option<DatabaseType>,
column_info: Option<&DataGridColumnInfo>,
) -> String {
if (value.is_array() || value.is_object()) && is_json_document_column(column_info) {
return format_grid_sql_literal(&Value::String(value.to_string()), database_type, column_info);
}
format_grid_sql_literal(value, database_type, column_info)
}
fn is_json_document_column(column_info: Option<&DataGridColumnInfo>) -> bool {
column_info.is_some_and(|column| {
let data_type = column.data_type.trim();
data_type.eq_ignore_ascii_case("json") || data_type.eq_ignore_ascii_case("jsonb")
})
}
pub fn format_grid_sql_literal(
value: &Value,
database_type: Option<DatabaseType>,
@@ -2142,7 +2150,7 @@ fn format_grid_save_sql_literal(
if empty_string_saves_as_null(value, column_info) {
"NULL".to_string()
} else {
format_grid_sql_literal(value, database_type, column_info)
format_grid_assignment_sql_literal(value, database_type, column_info)
}
}
@@ -2676,9 +2684,9 @@ pub(crate) fn build_column_predicate(
if value.is_null() {
format!("{ident} IS NULL")
} else if use_binary_text_comparison && uses_mysql_binary_text_predicate(database_type, value, column_info) {
format!("BINARY {ident} = {}", format_grid_sql_literal(value, database_type, column_info))
format!("BINARY {ident} = {}", format_grid_assignment_sql_literal(value, database_type, column_info))
} else {
let literal = format_grid_sql_literal(value, database_type, column_info);
let literal = format_grid_assignment_sql_literal(value, database_type, column_info);
if use_binary_text_comparison {
if let Some(predicate) = postgres_keyless_json_predicate(database_type, &ident, &literal, column_info) {
return predicate;
@@ -3600,7 +3608,7 @@ mod tests {
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES (CAST('[1,2,3]' AS JSON));"));
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES ('[1,2,3]');"));
}
#[test]
@@ -3618,7 +3626,7 @@ mod tests {
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES (CAST('[]' AS JSON));"));
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES ('[]');"));
}
#[test]
@@ -3636,7 +3644,95 @@ mod tests {
insert_mode: DataGridCopyInsertMode::Merged,
});
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES (CAST('{\"a\":1}' AS JSON));"));
assert_eq!(statement.as_deref(), Some("INSERT INTO table_name (`data`) VALUES ('{\"a\":1}');"));
}
#[test]
fn copy_update_formats_mysql_json_documents_as_compact_strings() {
let statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: None,
table_name: "documents".to_string(),
primary_keys: vec!["id".to_string()],
columns: Some(vec![column("id", "int", false, None), column("payload", "JSON", true, None)]),
},
columns: vec!["id".to_string(), "payload".to_string()],
source_columns: None,
rows: vec![
vec![json!(1), json!([111, 222, 333])],
vec![json!(2), json!({"nested": [1, 2]})],
vec![json!(3), json!([])],
],
});
assert_eq!(
statements,
vec![
"UPDATE `documents` SET `payload` = '[111,222,333]' WHERE `id` = 1;",
"UPDATE `documents` SET `payload` = '{\"nested\":[1,2]}' WHERE `id` = 2;",
"UPDATE `documents` SET `payload` = '[]' WHERE `id` = 3;",
]
);
}
#[test]
fn copy_update_preserves_json_scalars_and_generic_arrays() {
let json_statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: None,
table_name: "documents".to_string(),
primary_keys: vec!["id".to_string()],
columns: Some(vec![column("id", "int", false, None), column("payload", "json", true, None)]),
},
columns: vec!["id".to_string(), "payload".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!(r#"[1,2]"#)], vec![json!(2), json!(true)], vec![json!(3), Value::Null]],
});
assert_eq!(
json_statements,
vec![
"UPDATE `documents` SET `payload` = '[1,2]' WHERE `id` = 1;",
"UPDATE `documents` SET `payload` = TRUE WHERE `id` = 2;",
"UPDATE `documents` SET `payload` = NULL WHERE `id` = 3;",
]
);
let generic_statements = build_data_grid_copy_update_statements(DataGridCopyUpdateStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: None,
table_name: "arrays".to_string(),
primary_keys: vec!["id".to_string()],
columns: None,
},
columns: vec!["id".to_string(), "payload".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!([1, 2])]],
});
assert_eq!(generic_statements, vec!["UPDATE `arrays` SET `payload` = '{1,2}' WHERE `id` = 1;"]);
assert_eq!(
format_grid_sql_literal(
&json!([1, 2]),
Some(DatabaseType::Postgres),
Some(&column("items", "integer[]", true, None)),
),
"'{1,2}'"
);
for database_type in [DatabaseType::ClickHouse, DatabaseType::Databend] {
assert_eq!(format_grid_sql_literal(&json!([1, 2]), Some(database_type), None), "[1,2]");
}
}
#[test]
@@ -6145,6 +6241,44 @@ mod tests {
);
}
#[test]
fn mysql_json_save_assignments_are_compact_and_predicates_stay_cast() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("app".to_string()),
table_name: "documents".to_string(),
primary_keys: vec!["id".to_string()],
columns: Some(vec![column("id", "int", false, None), column("payload", "JSON", true, None)]),
},
columns: vec!["id".to_string(), "payload".to_string()],
source_columns: None,
rows: vec![vec![json!(1), json!({"before": true})]],
dirty_rows: vec![(0, vec![(1, json!([]))])],
deleted_rows: vec![],
new_rows: vec![vec![json!(2), json!({"nested": [1, 2]})]],
});
assert_eq!(result.validation_error, None);
assert_eq!(
result.statements,
vec![
"UPDATE `app`.`documents` SET `payload` = '[]' WHERE `id` = 1;",
"INSERT INTO `app`.`documents` (`id`, `payload`) VALUES (2, '{\"nested\":[1,2]}');",
]
);
assert_eq!(
result.rollback_statements,
vec![
"DELETE FROM `app`.`documents` WHERE `id` = 2;",
"UPDATE `app`.`documents` SET `payload` = '{\"before\":true}' WHERE `id` = 1 AND `payload` = CAST('[]' AS JSON);",
]
);
}
#[test]
fn formats_mysql_bit_literals_without_string_quotes() {
let bit = column("flag", "bit(1)", true, None);