fix(mysql): handle export terminators and escaped quotes
This commit is contained in:
parent
8a546be58d
commit
a12f8d09b7
|
|
@ -783,9 +783,8 @@ pub fn build_database_sql_export(options: BuildDatabaseSqlExportOptions) -> Resu
|
|||
|
||||
for table in options.tables {
|
||||
if let Some(ddl) = table.ddl.as_ref().map(|ddl| ddl.trim()).filter(|ddl| !ddl.is_empty()) {
|
||||
let ddl = normalize_export_table_ddl(ddl, table.database_type);
|
||||
lines.push(format!("-- Structure for {}", table.display_name));
|
||||
lines.push(format!("{};", ddl.trim_end_matches(';')));
|
||||
lines.push(format_export_table_ddl(ddl, table.database_type));
|
||||
lines.push(String::new());
|
||||
}
|
||||
|
||||
|
|
@ -844,6 +843,12 @@ fn normalize_export_table_ddl(ddl: &str, database_type: Option<DatabaseType>) ->
|
|||
LEGACY_MYSQL_ROW_FORMAT_RE.replace_all(ddl, "ROW_FORMAT=DYNAMIC").into_owned()
|
||||
}
|
||||
|
||||
fn format_export_table_ddl(ddl: &str, database_type: Option<DatabaseType>) -> String {
|
||||
let ddl = normalize_export_table_ddl(ddl, database_type);
|
||||
let ddl = ddl.trim().trim_end_matches(';').trim_end();
|
||||
format!("{ddl};")
|
||||
}
|
||||
|
||||
fn postgres_sequence_qualified_name(schema: &str, sequence_name: &str) -> String {
|
||||
let db_type = DatabaseType::Postgres;
|
||||
if schema.trim().is_empty() {
|
||||
|
|
@ -1534,8 +1539,8 @@ pub async fn export_database_sql_core(
|
|||
};
|
||||
match ddl_result {
|
||||
Ok(ddl) => {
|
||||
let ddl = normalize_export_table_ddl(&ddl, Some(db_type));
|
||||
writeln!(file, "{};\n", ddl).map_err(|e| format!("Failed to write file: {e}"))?;
|
||||
let ddl = format_export_table_ddl(&ddl, Some(db_type));
|
||||
writeln!(file, "{ddl}\n").map_err(|e| format!("Failed to write file: {e}"))?;
|
||||
}
|
||||
Err(e) => {
|
||||
record_export_error(
|
||||
|
|
@ -1952,7 +1957,7 @@ mod tests {
|
|||
use super::concurrent_metadata_prefetch_allowed;
|
||||
use super::{
|
||||
build_database_export_object_source_sql, build_database_sql_export, build_export_insert_statements,
|
||||
drop_table_if_exists_sql, filter_export_table_infos, format_export_sql_literal,
|
||||
drop_table_if_exists_sql, filter_export_table_infos, format_export_sql_literal, format_export_table_ddl,
|
||||
generate_postgres_extension_ddl, generate_postgres_sequence_create_ddl, generate_postgres_sequence_owner_ddl,
|
||||
generate_postgres_sequence_setval_sql, is_postgres_extension_member_routine, normalize_export_table_ddl,
|
||||
record_export_error, BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions, ExportedTableSql,
|
||||
|
|
@ -2629,6 +2634,17 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_ddl_export_has_one_statement_terminator() {
|
||||
let ddl = "CREATE TABLE `users` (`id` int);;\n";
|
||||
|
||||
assert_eq!(format_export_table_ddl(ddl, Some(DatabaseType::Mysql)), "CREATE TABLE `users` (`id` int);");
|
||||
assert_eq!(
|
||||
format_export_table_ddl("CREATE TABLE users (id int)", Some(DatabaseType::Postgres)),
|
||||
"CREATE TABLE users (id int);"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_legacy_mysql_row_format_for_export_compatibility() {
|
||||
let ddl = "CREATE TABLE `wide_table` (\n `payload` varchar(4096) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT";
|
||||
|
|
|
|||
|
|
@ -382,11 +382,11 @@ impl SqlStatementSplitter {
|
|||
}
|
||||
|
||||
match ch {
|
||||
'\'' if !self.in_double_quote && !self.in_backtick && self.previous != Some('\\') => {
|
||||
'\'' if !self.in_double_quote && !self.in_backtick && !has_odd_trailing_backslashes(&self.buffer) => {
|
||||
self.in_single_quote = !self.in_single_quote;
|
||||
self.buffer.push(ch);
|
||||
}
|
||||
'"' if !self.in_single_quote && !self.in_backtick && self.previous != Some('\\') => {
|
||||
'"' if !self.in_single_quote && !self.in_backtick && !has_odd_trailing_backslashes(&self.buffer) => {
|
||||
self.in_double_quote = !self.in_double_quote;
|
||||
self.buffer.push(ch);
|
||||
}
|
||||
|
|
@ -522,6 +522,10 @@ impl SqlStatementSplitter {
|
|||
}
|
||||
}
|
||||
|
||||
fn has_odd_trailing_backslashes(sql: &str) -> bool {
|
||||
sql.as_bytes().iter().rev().take_while(|byte| **byte == b'\\').count() % 2 == 1
|
||||
}
|
||||
|
||||
pub fn split_sql_statements(sql: &str) -> Vec<String> {
|
||||
split_sql_statements_with_options(sql, SqlParsingOptions::default())
|
||||
}
|
||||
|
|
@ -2457,6 +2461,39 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closes_mysql_string_after_even_trailing_backslashes() {
|
||||
let sql = r#"CREATE TABLE paths (value varchar(100) COMMENT 'Windows path\\'); DROP TABLE paths;"#;
|
||||
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::Mysql),
|
||||
vec![r#"CREATE TABLE paths (value varchar(100) COMMENT 'Windows path\\')"#, "DROP TABLE paths"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_mysql_string_open_after_odd_trailing_backslashes() {
|
||||
let sql = r#"INSERT INTO notes VALUES ('it\'s; still one value'); SELECT 1;"#;
|
||||
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::Mysql),
|
||||
vec![r#"INSERT INTO notes VALUES ('it\'s; still one value')"#, "SELECT 1"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closes_mysql_string_after_even_trailing_backslashes_across_chunks() {
|
||||
let mut splitter =
|
||||
SqlStatementSplitter::with_options(SqlParsingOptions::for_database_type(DatabaseType::Mysql));
|
||||
|
||||
assert!(splitter.push_chunk("CREATE TABLE paths (value varchar(100) COMMENT 'Windows path\\").is_empty());
|
||||
assert_eq!(
|
||||
splitter.push_chunk("\\'); DROP TABLE paths;"),
|
||||
vec![r#"CREATE TABLE paths (value varchar(100) COMMENT 'Windows path\\')"#, "DROP TABLE paths"]
|
||||
);
|
||||
assert!(splitter.finish().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emits_trailing_statement_without_semicolon() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue