fix(export): put each data row on its own line in exported SQL

This commit is contained in:
zipg authored and GitHub committed 2026-09-21 11:35:55 +08:00
1 parent 9422ed279f
commit abefee68f5
3 files changed
+86 -15

No files matched your search

+70 -9
View File
@@ -1253,10 +1253,15 @@ pub fn build_export_insert_statements_excluding(
is_identity_column_extra(options.column_extras.get(*index).and_then(|value| value.as_deref()))
});
let statement_prefix = format!("INSERT INTO {table} ({columns}) VALUES ");
// Multi-row batches are written one tuple per line (issue #9814) so exported
// `.sql` files stay readable in plain text editors. Statements that hold a
// single tuple keep the compact `VALUES (..);` form, which also preserves
// the "one INSERT per row" output of the single-row insert mode.
let statement_head = format!("INSERT INTO {table} ({columns}) VALUES");
let statement_prefix = format!("{statement_head} ");
let statement_overhead_bytes = export_sql_statement_bytes(options.database_type, &statement_prefix) + 1;
let target_statement_bytes = DATABASE_EXPORT_TARGET_STATEMENT_BYTES;
let separator_bytes = export_sql_statement_bytes(options.database_type, ", ");
let separator_bytes = export_sql_statement_bytes(options.database_type, ",\n");
let mut current_values = String::new();
let mut current_values_bytes = 0usize;
let mut current_row_count = 0usize;
@@ -1267,7 +1272,8 @@ pub fn build_export_insert_statements_excluding(
return;
}
let mut insert_sql = String::with_capacity(statement_prefix.len() + values.len() + 1);
insert_sql.push_str(&statement_prefix);
insert_sql.push_str(&statement_head);
insert_sql.push(if *row_count > 1 { '\n' } else { ' ' });
insert_sql.push_str(values);
insert_sql.push(';');
if needs_identity_insert_wrapper {
@@ -1316,7 +1322,9 @@ pub fn build_export_insert_statements_excluding(
flush_values(&mut statements, &mut current_values, &mut current_values_bytes, &mut current_row_count);
}
if current_row_count > 0 {
current_values.push_str(", ");
// A separator is only ever written once a statement holds at least
// two tuples, so it can always use the multi-row layout.
current_values.push_str(",\n");
current_values_bytes += separator_bytes;
}
current_values.push_str(&rendered_row);
@@ -4736,8 +4744,8 @@ mod tests {
.unwrap();
assert_eq!(statements.len(), 2);
assert_eq!(statements[0].matches("), (").count(), 999);
assert_eq!(statements[1].matches("), (").count(), 0);
assert_eq!(statements[0].matches("),\n(").count(), 999);
assert_eq!(statements[1].matches("),\n(").count(), 0);
}
#[test]
@@ -5026,12 +5034,63 @@ mod tests {
assert_eq!(
statements,
vec![
"INSERT INTO `users` (`id`, `name`) VALUES (1, 'Ada'), (2, 'O''Hara');",
"INSERT INTO `users` (`id`, `name`) VALUES\n(1, 'Ada'),\n(2, 'O''Hara');",
"INSERT INTO `users` (`id`, `name`) VALUES (3, 'Linus');",
]
);
}
#[test]
fn batched_insert_statements_write_each_row_on_its_own_line() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("users".to_string()),
qualified_table_name: None,
columns: vec!["id".to_string(), "name".to_string()],
column_types: Vec::new(),
column_extras: Vec::new(),
spatial_columns: Vec::new(),
spatial_values: Vec::new(),
rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("O'Hara")], vec![json!(3), json!("Linus")]],
batch_size: Some(10),
})
.unwrap();
assert_eq!(
statements,
vec!["INSERT INTO `users` (`id`, `name`) VALUES\n(1, 'Ada'),\n(2, 'O''Hara'),\n(3, 'Linus');"]
);
}
#[test]
fn single_row_batches_keep_the_compact_insert_layout() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
schema: None,
table_name: Some("users".to_string()),
qualified_table_name: None,
columns: vec!["id".to_string(), "name".to_string()],
column_types: Vec::new(),
column_extras: Vec::new(),
spatial_columns: Vec::new(),
spatial_values: Vec::new(),
rows: vec![vec![json!(1), json!("Ada")], vec![json!(2), json!("Linus")]],
batch_size: Some(1),
})
.unwrap();
assert_eq!(
statements,
vec![
"INSERT INTO `users` (`id`, `name`) VALUES (1, 'Ada');",
"INSERT INTO `users` (`id`, `name`) VALUES (2, 'Linus');",
]
);
}
#[test]
fn oracle_export_inserts_use_one_statement_per_row() {
let statements = build_export_insert_statements(BuildExportInsertStatementsOptions {
@@ -5274,7 +5333,9 @@ mod tests {
assert_eq!(
statements,
vec!["INSERT INTO `flags` (`enabled`, `mask`, `label`) VALUES (b'1', b'1010', '1010'), (b'0', 3, 'off');"]
vec![
"INSERT INTO `flags` (`enabled`, `mask`, `label`) VALUES\n(b'1', b'1010', '1010'),\n(b'0', 3, 'off');"
]
);
}
@@ -5370,7 +5431,7 @@ mod tests {
assert_eq!(
statements,
vec![
"INSERT INTO `t_test_01` (`id`, `f_blob`, `note`) VALUES (1, 0x68656c6c6f, '0x68656c6c6f'), (2, X'', '1');"
"INSERT INTO `t_test_01` (`id`, `f_blob`, `note`) VALUES\n(1, 0x68656c6c6f, '0x68656c6c6f'),\n(2, X'', '1');"
]
);
}
@@ -2346,7 +2346,7 @@ mod tests {
let output = rendered_sql_insert_output(SqlInsertMode::Batch);
assert_eq!(output.matches("INSERT INTO").count(), 1);
assert!(output.contains("VALUES (1, 'Ada'), (2, 'Lin');"));
assert!(output.contains("VALUES\n(1, 'Ada'),\n(2, 'Lin');"));
}
#[test]
+15 -5
View File
@@ -3538,15 +3538,25 @@ esac"#,
);
for (name, contents) in &sql_parts {
assert!(!contents.is_empty(), "{name} must not be empty");
// Every non-blank line must be a complete statement -- the part
// boundary may only land between the fallback arm's per-batch
// writes, never inside a statement.
// The part boundary may only land between the fallback arm's
// per-batch writes, never inside a statement. A batched INSERT now
// spans several lines (one tuple per line, issue #9814), so walk
// each part statement by statement instead of line by line: every
// statement must start at a line beginning and every part must end
// on a closed statement.
let mut inside_statement = false;
for line in contents.lines().filter(|line| !line.trim().is_empty()) {
let trimmed = line.trim();
if trimmed.starts_with("--") {
continue;
}
assert!(
line.trim_start().starts_with("INSERT INTO") && line.trim_end().ends_with(';'),
"{name} has a malformed line from a mid-statement cut"
inside_statement || trimmed.starts_with("INSERT INTO"),
"{name} has a malformed line from a mid-statement cut: {line}"
);
inside_statement = !trimmed.ends_with(';');
}
assert!(!inside_statement, "{name} ends inside a statement");
}
// All 300 rows must be present across the parts: every row value is
// `(n, 'xxxx…')`, so one `, '` occurrence per row.