fix(export): put each data row on its own line in exported SQL
This commit is contained in:
3 files changed
+86
-15
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user