diff --git a/crates/dbx-core/src/sql.rs b/crates/dbx-core/src/sql.rs index 6f73e74437..ef261cd96e 100644 --- a/crates/dbx-core/src/sql.rs +++ b/crates/dbx-core/src/sql.rs @@ -2056,7 +2056,7 @@ pub(crate) fn supports_connection_level_database_bootstrap_target( .is_some_and(|profile| is_mysql_compatible_import_profile(&profile) && profile != "manticoresearch") } -fn is_mysql_compatible_import_target(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool { +pub(crate) fn is_mysql_compatible_import_target(db_type: &DatabaseType, driver_profile: Option<&str>) -> bool { matches!( db_type, DatabaseType::Mysql diff --git a/crates/dbx-core/src/sql_file_import.rs b/crates/dbx-core/src/sql_file_import.rs index c532fb9248..98be138045 100644 --- a/crates/dbx-core/src/sql_file_import.rs +++ b/crates/dbx-core/src/sql_file_import.rs @@ -431,7 +431,11 @@ pub async fn execute_sql_file_paths( let mut splitter = StreamingSqlFileSplitter::new(database_type, options); let mut pending_statements = Vec::with_capacity(SQL_FILE_STATEMENT_BATCH_SIZE); - let mut decoder = match SqlFileStreamDecoder::open(file_path).await { + let normalize_mysql_binary_literals = import_target.as_ref().is_some_and(|target| { + crate::sql::is_mysql_compatible_import_target(&target.db_type, target.driver_profile.as_deref()) + }); + let mut decoder = match SqlFileStreamDecoder::open_for_target(file_path, normalize_mysql_binary_literals).await + { Ok(decoder) => decoder, Err(error) => { emit(sql_file_execution_error_progress(&request.execution_id, started_at, &progress, error.clone())); @@ -539,7 +543,9 @@ pub async fn read_sql_file_preview(file_path: &Path, max_chars: usize) -> Result struct SqlFileStreamDecoder { reader: SqlFileByteReader, decoder: encoding_rs::Decoder, + mysql_binary_normalizer: Option, pending_bytes: Vec, + pending_decoded_bytes: Vec, reached_eof: bool, } @@ -600,23 +606,46 @@ fn is_gzip_sql_file_path(path: &Path) -> bool { } impl SqlFileStreamDecoder { + #[cfg(test)] async fn open(file_path: &Path) -> Result { - Self::open_with_detection_limit(file_path, None).await + Self::open_with_options(file_path, None, false).await } async fn open_with_detection_limit(file_path: &Path, detection_limit: Option) -> Result { - let (encoding, bom_len) = detect_sql_file_encoding(file_path, detection_limit).await?; + Self::open_with_options(file_path, detection_limit, false).await + } + + async fn open_for_target(file_path: &Path, normalize_mysql_binary_literals: bool) -> Result { + Self::open_with_options(file_path, None, normalize_mysql_binary_literals).await + } + + async fn open_with_options( + file_path: &Path, + detection_limit: Option, + normalize_mysql_binary_literals: bool, + ) -> Result { + let (encoding, bom_len, detected_mysql_binary_literals) = + detect_sql_file_encoding(file_path, detection_limit).await?; let mut reader = SqlFileByteReader::open(file_path).await?; let mut prefix = [0u8; 3]; let prefix_len = reader.read(&mut prefix).await?; let prefix = &prefix[..prefix_len]; let mut pending_bytes = prefix[bom_len..].to_vec(); pending_bytes.reserve(SQL_FILE_READ_CHUNK_BYTES); - Ok(Self { reader, decoder: encoding.new_decoder_without_bom_handling(), pending_bytes, reached_eof: false }) + Ok(Self { + reader, + decoder: encoding.new_decoder_without_bom_handling(), + mysql_binary_normalizer: (encoding == encoding_rs::UTF_8 + && (normalize_mysql_binary_literals || detected_mysql_binary_literals)) + .then(MysqlDumpBinaryLiteralNormalizer::default), + pending_bytes, + pending_decoded_bytes: Vec::new(), + reached_eof: false, + }) } async fn next_chunk(&mut self) -> Result, String> { - if self.reached_eof && self.pending_bytes.is_empty() { + if self.reached_eof && self.pending_bytes.is_empty() && self.pending_decoded_bytes.is_empty() { return Ok(None); } while !self.reached_eof && self.pending_bytes.len() < SQL_FILE_READ_CHUNK_BYTES { @@ -629,14 +658,22 @@ impl SqlFileStreamDecoder { self.pending_bytes.extend_from_slice(&buffer[..read]); } + let normalized = if let Some(normalizer) = self.mysql_binary_normalizer.as_mut() { + let input = normalizer.normalize(&self.pending_bytes, self.reached_eof)?; + self.pending_bytes.clear(); + input + } else { + std::mem::take(&mut self.pending_bytes) + }; + let mut input = std::mem::take(&mut self.pending_decoded_bytes); + input.extend_from_slice(&normalized); let mut output = String::with_capacity( - self.decoder - .max_utf8_buffer_length_without_replacement(self.pending_bytes.len()) - .unwrap_or(self.pending_bytes.len()), + self.decoder.max_utf8_buffer_length_without_replacement(input.len()).unwrap_or(input.len()), ); - let (result, read) = - self.decoder.decode_to_string_without_replacement(&self.pending_bytes, &mut output, self.reached_eof); - self.pending_bytes.drain(..read); + let (result, read) = self.decoder.decode_to_string_without_replacement(&input, &mut output, self.reached_eof); + if read < input.len() { + self.pending_decoded_bytes.extend_from_slice(&input[read..]); + } match result { encoding_rs::DecoderResult::InputEmpty => Ok((!output.is_empty()).then_some(output)), encoding_rs::DecoderResult::OutputFull => Ok(Some(output)), @@ -645,29 +682,256 @@ impl SqlFileStreamDecoder { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum MysqlDumpBinaryLiteralState { + #[default] + Normal, + SingleQuoted, + DoubleQuoted, + BacktickQuoted, + LineComment, + BlockComment, + BinaryQuoted, +} + +#[derive(Debug, Default)] +struct MysqlDumpBinaryLiteralNormalizer { + state: MysqlDumpBinaryLiteralState, + pending: Vec, + previous_source_byte: Option, + quoted_escape_pending: bool, +} + +impl MysqlDumpBinaryLiteralNormalizer { + fn normalize(&mut self, input: &[u8], eof: bool) -> Result, String> { + self.pending.extend_from_slice(input); + let mut output = Vec::with_capacity(self.pending.len()); + let mut index = 0; + + while index < self.pending.len() { + let byte = self.pending[index]; + match self.state { + MysqlDumpBinaryLiteralState::Normal => { + if byte == b'_' { + if self.pending.len() - index < b"_binary".len() { + if !eof { + break; + } + } else if self.pending[index..index + 7].eq_ignore_ascii_case(b"_binary") + && !is_sql_identifier_byte(self.source_byte_before(index)) + { + let mut quote_index = index + 7; + while quote_index < self.pending.len() && self.pending[quote_index].is_ascii_whitespace() { + quote_index += 1; + } + if quote_index == self.pending.len() && !eof { + break; + } + if self.pending.get(quote_index) == Some(&b'\'') { + output.extend_from_slice(b"X'"); + self.state = MysqlDumpBinaryLiteralState::BinaryQuoted; + self.previous_source_byte = Some(b'\''); + index = quote_index + 1; + continue; + } + } + } + + if byte == b'-' && self.pending.get(index + 1) == Some(&b'-') { + // MySQL requires whitespace or a control character after `--` + // to open a line comment: `5--1` is subtraction, matching the + // splitter's dash_dash_starts_line_comment rule. + let byte_after_dashes = self.pending.get(index + 2).copied(); + if byte_after_dashes.is_none() && !eof { + break; + } + output.extend_from_slice(b"--"); + self.previous_source_byte = Some(b'-'); + index += 2; + if byte_after_dashes.is_none_or(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control()) { + self.state = MysqlDumpBinaryLiteralState::LineComment; + } + } else if byte == b'-' && self.pending.get(index + 1).is_none() && !eof { + break; + } else if byte == b'#' { + output.push(byte); + self.state = MysqlDumpBinaryLiteralState::LineComment; + self.previous_source_byte = Some(byte); + index += 1; + } else if byte == b'/' && self.pending.get(index + 1) == Some(&b'*') { + output.extend_from_slice(b"/*"); + self.state = MysqlDumpBinaryLiteralState::BlockComment; + self.previous_source_byte = Some(b'*'); + index += 2; + } else if byte == b'/' && self.pending.get(index + 1).is_none() && !eof { + break; + } else { + output.push(byte); + self.previous_source_byte = Some(byte); + self.state = match byte { + b'\'' => MysqlDumpBinaryLiteralState::SingleQuoted, + b'"' => MysqlDumpBinaryLiteralState::DoubleQuoted, + b'`' => MysqlDumpBinaryLiteralState::BacktickQuoted, + _ => MysqlDumpBinaryLiteralState::Normal, + }; + index += 1; + } + } + MysqlDumpBinaryLiteralState::SingleQuoted + | MysqlDumpBinaryLiteralState::DoubleQuoted + | MysqlDumpBinaryLiteralState::BacktickQuoted => { + let quote = match self.state { + MysqlDumpBinaryLiteralState::SingleQuoted => b'\'', + MysqlDumpBinaryLiteralState::DoubleQuoted => b'"', + MysqlDumpBinaryLiteralState::BacktickQuoted => b'`', + _ => unreachable!(), + }; + if byte == quote && self.pending.get(index + 1).is_none() && !eof { + break; + } + output.push(byte); + self.previous_source_byte = Some(byte); + index += 1; + if self.quoted_escape_pending { + self.quoted_escape_pending = false; + } else if byte == b'\\' { + if let Some(escaped) = self.pending.get(index) { + output.push(*escaped); + self.previous_source_byte = Some(*escaped); + index += 1; + } else if !eof { + self.quoted_escape_pending = true; + } + } else if byte == quote { + if self.pending.get(index) == Some("e) { + output.push(quote); + self.previous_source_byte = Some(quote); + index += 1; + } else { + self.state = MysqlDumpBinaryLiteralState::Normal; + } + } + } + MysqlDumpBinaryLiteralState::LineComment => { + output.push(byte); + self.previous_source_byte = Some(byte); + index += 1; + if byte == b'\n' { + self.state = MysqlDumpBinaryLiteralState::Normal; + } + } + MysqlDumpBinaryLiteralState::BlockComment => { + if byte == b'*' && self.pending.get(index + 1).is_none() && !eof { + break; + } + output.push(byte); + self.previous_source_byte = Some(byte); + index += 1; + if byte == b'*' && self.pending.get(index) == Some(&b'/') { + output.push(b'/'); + self.previous_source_byte = Some(b'/'); + index += 1; + self.state = MysqlDumpBinaryLiteralState::Normal; + } + } + MysqlDumpBinaryLiteralState::BinaryQuoted => { + if byte == b'\\' { + let Some(escaped) = self.pending.get(index + 1).copied() else { + if !eof { + break; + } + return Err("Unterminated MySQL binary literal".to_string()); + }; + let value = match escaped { + b'0' => 0x00, + b'b' => 0x08, + b'n' => b'\n', + b'r' => b'\r', + b't' => b'\t', + b'Z' => 0x1A, + b'\\' => b'\\', + b'\'' => b'\'', + b'"' => b'"', + other => other, + }; + append_hex(&mut output, value); + self.previous_source_byte = Some(escaped); + index += 2; + } else if byte == b'\'' && self.pending.get(index + 1).is_none() && !eof { + break; + } else if byte == b'\'' { + if self.pending.get(index + 1) == Some(&b'\'') { + append_hex(&mut output, b'\''); + self.previous_source_byte = Some(b'\''); + index += 2; + } else { + output.push(b'\''); + self.previous_source_byte = Some(b'\''); + self.state = MysqlDumpBinaryLiteralState::Normal; + index += 1; + } + } else { + append_hex(&mut output, byte); + self.previous_source_byte = Some(byte); + index += 1; + } + } + } + } + + self.pending.drain(..index); + if eof { + if self.state == MysqlDumpBinaryLiteralState::BinaryQuoted { + return Err("Unterminated MySQL binary literal".to_string()); + } + if !self.pending.is_empty() { + output.extend_from_slice(&self.pending); + self.pending.clear(); + } + } + Ok(output) + } + + fn source_byte_before(&self, index: usize) -> Option { + if index > 0 { + self.pending.get(index - 1).copied() + } else { + self.previous_source_byte + } + } +} + +fn is_sql_identifier_byte(byte: Option) -> bool { + byte.is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')) +} + +fn append_hex(output: &mut Vec, byte: u8) { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0F) as usize]); +} + async fn detect_sql_file_encoding( file_path: &Path, detection_limit: Option, -) -> Result<(&'static encoding_rs::Encoding, usize), String> { +) -> Result<(&'static encoding_rs::Encoding, usize, bool), String> { let mut file = SqlFileByteReader::open(file_path).await?; let mut prefix = [0u8; 3]; let prefix_len = file.read(&mut prefix).await.map_err(|error| error.to_string())?; let prefix = &prefix[..prefix_len]; - if prefix.starts_with(&[0xEF, 0xBB, 0xBF]) { - return Ok((encoding_rs::UTF_8, 3)); - } if prefix.starts_with(&[0xFF, 0xFE]) { - return Ok((encoding_rs::UTF_16LE, 2)); + return Ok((encoding_rs::UTF_16LE, 2, false)); } if prefix.starts_with(&[0xFE, 0xFF]) { - return Ok((encoding_rs::UTF_16BE, 2)); + return Ok((encoding_rs::UTF_16BE, 2, false)); } + let bom_len = usize::from(prefix.starts_with(&[0xEF, 0xBB, 0xBF])) * 3; // SQL dumps often begin with ASCII comments even when the remaining file // is GBK. Validate the entire stream as UTF-8 with bounded buffers before // falling back to the legacy GBK behavior. let mut decoder = encoding_rs::UTF_8.new_decoder_without_bom_handling(); - let mut input = prefix.to_vec(); + let mut input = prefix[bom_len..].to_vec(); let mut inspected_bytes = prefix.len(); let mut reached_eof = false; loop { @@ -690,15 +954,70 @@ async fn detect_sql_file_encoding( let (result, read) = decoder.decode_to_string_without_replacement(&input, &mut output, reached_eof); input.drain(..read); match result { - encoding_rs::DecoderResult::Malformed(_, _) => return Ok((encoding_rs::GBK, 0)), + encoding_rs::DecoderResult::Malformed(_, _) => { + if validate_utf8_with_mysql_binary_literals(file_path, detection_limit).await? { + return Ok((encoding_rs::UTF_8, bom_len, true)); + } + return Ok(if bom_len > 0 { + (encoding_rs::UTF_8, bom_len, false) + } else { + (encoding_rs::GBK, 0, false) + }); + } encoding_rs::DecoderResult::InputEmpty if reached_eof || reached_detection_limit => { - return Ok((encoding_rs::UTF_8, 0)); + return Ok((encoding_rs::UTF_8, bom_len, false)); } encoding_rs::DecoderResult::InputEmpty | encoding_rs::DecoderResult::OutputFull => {} } } } +async fn validate_utf8_with_mysql_binary_literals( + file_path: &Path, + detection_limit: Option, +) -> Result { + let mut file = SqlFileByteReader::open(file_path).await?; + let mut normalizer = MysqlDumpBinaryLiteralNormalizer::default(); + let mut decoder = encoding_rs::UTF_8.new_decoder_without_bom_handling(); + let mut input = Vec::new(); + let mut inspected_bytes = 0; + let mut reached_eof = false; + + loop { + let reached_detection_limit = detection_limit.is_some_and(|limit| inspected_bytes >= limit); + if !reached_eof && !reached_detection_limit { + let remaining = + detection_limit.map(|limit| limit.saturating_sub(inspected_bytes)).unwrap_or(SQL_FILE_READ_CHUNK_BYTES); + let mut buffer = vec![0u8; SQL_FILE_READ_CHUNK_BYTES.min(remaining.max(1))]; + let read = file.read(&mut buffer).await?; + if read == 0 { + reached_eof = true; + } else { + inspected_bytes += read; + input.extend_from_slice(&buffer[..read]); + } + } + + let normalized = match normalizer.normalize(&input, reached_eof) { + Ok(normalized) => normalized, + Err(_) => return Ok(false), + }; + input.clear(); + let mut output = String::with_capacity( + decoder + .max_utf8_buffer_length_without_replacement(normalized.len()) + .unwrap_or(normalized.len().saturating_mul(3)), + ); + let (result, _) = decoder.decode_to_string_without_replacement(&normalized, &mut output, reached_eof); + if !matches!(result, encoding_rs::DecoderResult::InputEmpty) { + return Ok(false); + } + if reached_eof || reached_detection_limit { + return Ok(true); + } + } +} + enum StreamingSqlFileSplitter { Statements(SqlStatementSplitter), SqlServerBatches(SqlServerBatchSplitter), @@ -1794,6 +2113,140 @@ mod tests { assert_eq!(decoded, "SELECT 'compressed';\n"); } + #[tokio::test] + async fn mysql_target_decoder_preserves_utf16le_bom_files() { + let mut bytes = vec![0xFF, 0xFE]; + for unit in "SELECT _binary 'abc', '中文';".encode_utf16() { + bytes.extend_from_slice(&unit.to_le_bytes()); + } + let path = temporary_sql_file(&bytes).await; + let mut decoder = SqlFileStreamDecoder::open_for_target(&path, true).await.unwrap(); + let mut decoded = String::new(); + while let Some(chunk) = decoder.next_chunk().await.unwrap() { + decoded.push_str(&chunk); + } + tokio::fs::remove_file(path).await.unwrap(); + + assert_eq!(decoded, "SELECT _binary 'abc', '中文';"); + } + + #[tokio::test] + async fn mysql_target_decoder_preserves_gbk_files() { + let sql = "INSERT INTO t VALUES ('中文');"; + let (encoded, _, _) = encoding_rs::GBK.encode(sql); + let path = temporary_sql_file(encoded.as_ref()).await; + let mut decoder = SqlFileStreamDecoder::open_for_target(&path, true).await.unwrap(); + let mut decoded = String::new(); + while let Some(chunk) = decoder.next_chunk().await.unwrap() { + decoded.push_str(&chunk); + } + tokio::fs::remove_file(path).await.unwrap(); + + assert_eq!(decoded, sql); + } + + #[test] + fn mysql_binary_literal_normalizer_preserves_bytes_and_mysql_escapes() { + let mut source = b"INSERT INTO t VALUES (_binary '".to_vec(); + source.extend_from_slice(&[0xAC, b'\\', 0xED, b'\\', b'0', b'\\', b'n', b'\\', b'\'', b'\\', b'\\']); + source.extend_from_slice(b"'); -- _binary '\\xFF'\nSELECT 'not _binary x';"); + + let mut normalizer = MysqlDumpBinaryLiteralNormalizer::default(); + let mut normalized = Vec::new(); + for chunk in source.chunks(3) { + normalized.extend(normalizer.normalize(chunk, false).unwrap()); + } + normalized.extend(normalizer.normalize(&[], true).unwrap()); + + assert_eq!( + String::from_utf8(normalized).unwrap(), + "INSERT INTO t VALUES (X'ACED000A275C'); -- _binary '\\xFF'\nSELECT 'not _binary x';" + ); + } + + #[tokio::test] + async fn streaming_decoder_recovers_utf8_mysql_dump_with_raw_binary_bytes() { + let mut bytes = b"-- MySQL dump\nINSERT INTO t VALUES (_binary '".to_vec(); + bytes.extend_from_slice(&[0xAC, b'\\', 0xED, b'\\', b'0', 0x05]); + bytes.extend_from_slice(b"', '\xE4\xB8\xAD\xE6\x96\x87');\n"); + let path = temporary_sql_file(&bytes).await; + let mut decoder = SqlFileStreamDecoder::open(&path).await.unwrap(); + let mut decoded = String::new(); + while let Some(chunk) = decoder.next_chunk().await.unwrap() { + decoded.push_str(&chunk); + } + tokio::fs::remove_file(path).await.unwrap(); + + assert_eq!(decoded, "-- MySQL dump\nINSERT INTO t VALUES (X'ACED0005', '中文');\n"); + } + + #[tokio::test] + async fn streaming_decoder_recovers_bom_utf8_mysql_dump_with_raw_binary_bytes() { + let mut bytes = vec![0xEF, 0xBB, 0xBF]; + bytes.extend_from_slice(b"INSERT INTO t VALUES (_binary '"); + bytes.extend_from_slice(&[0xAC, b'\\', 0xED, b'\\', b'0', 0x05]); + bytes.extend_from_slice(b"');\n"); + let path = temporary_sql_file(&bytes).await; + let mut decoder = SqlFileStreamDecoder::open(&path).await.unwrap(); + let mut decoded = String::new(); + while let Some(chunk) = decoder.next_chunk().await.unwrap() { + decoded.push_str(&chunk); + } + tokio::fs::remove_file(path).await.unwrap(); + + assert_eq!(decoded, "INSERT INTO t VALUES (X'ACED0005');\n"); + } + + #[tokio::test] + async fn streaming_decoder_recovers_gzip_mysql_dump_with_raw_binary_bytes() { + // Encoding detection and binary-literal validation must run on the + // decompressed bytes, not on the raw gzip stream. + let mut bytes = b"-- MySQL dump\nINSERT INTO t VALUES (_binary '".to_vec(); + bytes.extend_from_slice(&[0xAC, b'\\', 0xED, b'\\', b'0', 0x05]); + bytes.extend_from_slice(b"', '\xE4\xB8\xAD\xE6\x96\x87');\n"); + let path = temporary_gzip_sql_file(&bytes).await; + let mut decoder = SqlFileStreamDecoder::open(&path).await.unwrap(); + let mut decoded = String::new(); + while let Some(chunk) = decoder.next_chunk().await.unwrap() { + decoded.push_str(&chunk); + } + tokio::fs::remove_file(path).await.unwrap(); + + assert_eq!(decoded, "-- MySQL dump\nINSERT INTO t VALUES (X'ACED0005', '中文');\n"); + } + + #[test] + fn mysql_binary_literal_normalizer_does_not_rewrite_comments_or_strings() { + let source = b"-- _binary '\xAC'\nSELECT '_binary \'x\'', `col_binary`; /* _binary '\xAC' */"; + let mut normalizer = MysqlDumpBinaryLiteralNormalizer::default(); + let normalized = normalizer.normalize(source, true).unwrap(); + + assert_eq!(normalized, source); + } + + #[test] + fn mysql_binary_literal_normalizer_rejects_unterminated_literal() { + let mut normalizer = MysqlDumpBinaryLiteralNormalizer::default(); + let error = normalizer.normalize(b"SELECT _binary 'abc", true).unwrap_err(); + + assert_eq!(error, "Unterminated MySQL binary literal"); + } + + #[test] + fn mysql_binary_literal_normalizer_treats_dash_dash_digit_as_subtraction() { + // MySQL treats `--` as a comment opener only when followed by whitespace + // or a control character, so `5--1` is subtraction and the `_binary` + // literal later on the same line must still be normalized. + let mut source = b"SELECT 5--1, _binary '".to_vec(); + source.extend_from_slice(&[0xAC, 0x05]); + source.extend_from_slice(b"';\n"); + + let mut normalizer = MysqlDumpBinaryLiteralNormalizer::default(); + let normalized = normalizer.normalize(&source, true).unwrap(); + + assert_eq!(normalized, b"SELECT 5--1, X'AC05';\n".to_vec()); + } + #[test] fn non_sqlserver_sql_file_keeps_statement_splitting_behavior() { assert_eq!( diff --git a/crates/dbx-core/tests/live_mysql57.rs b/crates/dbx-core/tests/live_mysql57.rs index 552a71851c..3f369f7d42 100644 --- a/crates/dbx-core/tests/live_mysql57.rs +++ b/crates/dbx-core/tests/live_mysql57.rs @@ -1217,3 +1217,61 @@ INSERT INTO install_check (id) VALUES (1), (2); assert_eq!(verify.columns, vec!["count"]); assert_eq!(verify.rows, vec![vec![serde_json::json!("2")]]); } + +#[tokio::test] +#[ignore = "requires DBX_LIVE_SQL_FILE_MYSQL_* env vars pointing at a writable MySQL connection without a required default database"] +async fn live_sql_file_import_preserves_raw_mysql_binary_literal_bytes() { + let suffix = uuid::Uuid::new_v4().simple().to_string(); + let config = live_mysql_sql_file_config(&format!("sql-file-binary-{suffix}")); + let (state, db_path) = app_state_with_config(config.clone()).await; + let database_name = std::env::var("DBX_LIVE_SQL_FILE_MYSQL_DATABASE").unwrap_or_else(|_| "dbx_test".to_string()); + let table_name = format!("binary_dump_{suffix}"); + let mut script = format!( + "USE `{database_name}`;\nCREATE TABLE `{table_name}` (id INT PRIMARY KEY, value LONGBLOB);\nINSERT INTO `{table_name}` VALUES (1, _binary '" + ) + .into_bytes(); + script.extend_from_slice(&[0xAC, b'\\', 0xED, b'\\', b'0', 0x05]); + script.extend_from_slice(b"');\n"); + let request = SqlFileRequest { + execution_id: format!("exec-{suffix}"), + connection_id: config.id.clone(), + database: String::new(), + file_path: std::env::temp_dir().join(format!("mysql-binary-dump-{suffix}.sql")).to_string_lossy().into_owned(), + continue_on_error: false, + }; + + tokio::fs::write(&request.file_path, script).await.unwrap(); + let result = execute_sql_file_path( + &state, + &request, + std::path::Path::new(&request.file_path), + CancellationToken::new(), + Instant::now(), + |_| {}, + ) + .await; + let verify = execute_sql_statement( + &state, + &config.id, + &database_name, + &format!("SELECT HEX(value) AS value_hex FROM `{table_name}` WHERE id = 1"), + None, + None, + ) + .await; + let _ = execute_sql_statement( + &state, + &config.id, + &database_name, + &format!("DROP TABLE IF EXISTS `{table_name}`"), + None, + None, + ) + .await; + let _ = std::fs::remove_file(db_path); + let _ = std::fs::remove_file(&request.file_path); + + result.expect("binary SQL file import should succeed"); + let verify = verify.expect("verify imported binary bytes"); + assert_eq!(verify.rows, vec![vec![serde_json::json!("ACED0005")]]); +} diff --git a/docs/data/contributors.json b/docs/data/contributors.json index 0329ca7365..87835bdab6 100644 --- a/docs/data/contributors.json +++ b/docs/data/contributors.json @@ -1,70 +1,70 @@ { "repository": "t8y2/dbx", - "generatedAt": "2026-08-23T18:05:28.346Z", - "stars": 16308, + "generatedAt": "2026-08-30T20:24:13.247Z", + "stars": 17473, "contributors": [ { "login": "t8y2", "avatarUrl": "https://avatars.githubusercontent.com/u/77960507?v=4", "profileUrl": "https://github.com/t8y2", - "commits": 3178, - "mergedPullRequests": 17, + "commits": 3310, + "mergedPullRequests": 23, "firstContributionAt": "2026-04-30T08:14:51Z", - "latestContributionAt": "2026-08-18T04:05:42Z" + "latestContributionAt": "2026-08-30T05:30:01Z" }, { "login": "zipg", "avatarUrl": "https://avatars.githubusercontent.com/u/4047349?v=4", "profileUrl": "https://github.com/zipg", - "commits": 590, - "mergedPullRequests": 590, + "commits": 644, + "mergedPullRequests": 644, "firstContributionAt": "2026-06-22T06:10:59Z", - "latestContributionAt": "2026-08-22T17:39:23Z" + "latestContributionAt": "2026-08-29T19:05:11Z" }, { "login": "eryajf", "avatarUrl": "https://avatars.githubusercontent.com/u/33259379?v=4", "profileUrl": "https://github.com/eryajf", - "commits": 166, - "mergedPullRequests": 165, + "commits": 191, + "mergedPullRequests": 190, "firstContributionAt": "2026-06-08T11:45:45Z", - "latestContributionAt": "2026-08-22T18:53:52Z" - }, - { - "login": "Abeautifulsnow", - "avatarUrl": "https://avatars.githubusercontent.com/u/28704977?v=4", - "profileUrl": "https://github.com/Abeautifulsnow", - "commits": 97, - "mergedPullRequests": 85, - "firstContributionAt": "2026-05-06T03:16:57Z", - "latestContributionAt": "2026-08-21T02:36:18Z" + "latestContributionAt": "2026-08-30T04:26:07Z" }, { "login": "q396921921", "avatarUrl": "https://avatars.githubusercontent.com/u/37297228?v=4", "profileUrl": "https://github.com/q396921921", - "commits": 88, - "mergedPullRequests": 88, + "commits": 104, + "mergedPullRequests": 104, "firstContributionAt": "2026-07-20T13:41:21Z", - "latestContributionAt": "2026-08-23T04:25:45Z" + "latestContributionAt": "2026-08-27T14:25:09Z" + }, + { + "login": "Abeautifulsnow", + "avatarUrl": "https://avatars.githubusercontent.com/u/28704977?v=4", + "profileUrl": "https://github.com/Abeautifulsnow", + "commits": 104, + "mergedPullRequests": 92, + "firstContributionAt": "2026-05-06T03:16:57Z", + "latestContributionAt": "2026-08-30T04:26:55Z" }, { "login": "CN-Scars", "avatarUrl": "https://avatars.githubusercontent.com/u/57054419?v=4", "profileUrl": "https://github.com/CN-Scars", - "commits": 68, - "mergedPullRequests": 68, + "commits": 74, + "mergedPullRequests": 74, "firstContributionAt": "2026-05-30T10:02:05Z", - "latestContributionAt": "2026-08-22T07:41:21Z" + "latestContributionAt": "2026-08-28T08:51:34Z" }, { "login": "onenewcode", "avatarUrl": "https://avatars.githubusercontent.com/u/94808747?v=4", "profileUrl": "https://github.com/onenewcode", - "commits": 62, - "mergedPullRequests": 62, + "commits": 66, + "mergedPullRequests": 66, "firstContributionAt": "2026-06-30T01:53:31Z", - "latestContributionAt": "2026-08-22T04:58:28Z" + "latestContributionAt": "2026-08-27T02:55:20Z" }, { "login": "serenez", @@ -75,14 +75,23 @@ "firstContributionAt": "2026-04-30T04:27:54Z", "latestContributionAt": "2026-07-20T15:31:27Z" }, + { + "login": "0verme", + "avatarUrl": "https://avatars.githubusercontent.com/u/24986278?v=4", + "profileUrl": "https://github.com/0verme", + "commits": 47, + "mergedPullRequests": 47, + "firstContributionAt": "2026-08-15T15:33:44Z", + "latestContributionAt": "2026-08-29T16:47:45Z" + }, { "login": "onceMisery", "avatarUrl": "https://avatars.githubusercontent.com/u/37446017?v=4", "profileUrl": "https://github.com/onceMisery", - "commits": 45, - "mergedPullRequests": 43, + "commits": 46, + "mergedPullRequests": 44, "firstContributionAt": "2026-06-10T14:19:43Z", - "latestContributionAt": "2026-08-23T04:23:46Z" + "latestContributionAt": "2026-08-24T10:35:18Z" }, { "login": "Illuminated2020", @@ -97,10 +106,10 @@ "login": "mapan0424", "avatarUrl": "https://avatars.githubusercontent.com/u/45232878?v=4", "profileUrl": "https://github.com/mapan0424", - "commits": 34, - "mergedPullRequests": 34, + "commits": 36, + "mergedPullRequests": 36, "firstContributionAt": "2026-07-17T05:42:50Z", - "latestContributionAt": "2026-08-20T07:14:20Z" + "latestContributionAt": "2026-08-25T07:15:42Z" }, { "login": "xddcode", @@ -111,15 +120,6 @@ "firstContributionAt": "2026-07-10T06:34:09Z", "latestContributionAt": "2026-08-20T02:23:08Z" }, - { - "login": "0verme", - "avatarUrl": "https://avatars.githubusercontent.com/u/24986278?v=4", - "profileUrl": "https://github.com/0verme", - "commits": 29, - "mergedPullRequests": 29, - "firstContributionAt": "2026-08-15T15:33:44Z", - "latestContributionAt": "2026-08-23T10:08:18Z" - }, { "login": "gggaiitx", "avatarUrl": "https://avatars.githubusercontent.com/u/62124152?v=4", @@ -192,6 +192,15 @@ "firstContributionAt": "2026-06-17T08:11:49Z", "latestContributionAt": "2026-06-26T10:48:46Z" }, + { + "login": "amwps290", + "avatarUrl": "https://avatars.githubusercontent.com/u/18211479?v=4", + "profileUrl": "https://github.com/amwps290", + "commits": 15, + "mergedPullRequests": 15, + "firstContributionAt": "2026-07-21T05:32:24Z", + "latestContributionAt": "2026-08-30T01:59:24Z" + }, { "login": "jischeng", "avatarUrl": "https://avatars.githubusercontent.com/u/49861575?v=4", @@ -220,13 +229,22 @@ "latestContributionAt": "2026-07-21T16:25:09Z" }, { - "login": "amwps290", - "avatarUrl": "https://avatars.githubusercontent.com/u/18211479?v=4", - "profileUrl": "https://github.com/amwps290", + "login": "mr-dadong", + "avatarUrl": "https://avatars.githubusercontent.com/u/102886401?v=4", + "profileUrl": "https://github.com/mr-dadong", + "commits": 14, + "mergedPullRequests": 14, + "firstContributionAt": "2026-08-17T07:04:42Z", + "latestContributionAt": "2026-08-28T03:11:35Z" + }, + { + "login": "DASungta", + "avatarUrl": "https://avatars.githubusercontent.com/u/8896967?v=4", + "profileUrl": "https://github.com/DASungta", "commits": 13, "mergedPullRequests": 13, - "firstContributionAt": "2026-07-21T05:32:24Z", - "latestContributionAt": "2026-08-20T08:04:22Z" + "firstContributionAt": "2026-07-21T04:15:46Z", + "latestContributionAt": "2026-08-24T05:29:24Z" }, { "login": "SpencerZhang", @@ -238,22 +256,13 @@ "latestContributionAt": "2026-07-20T08:54:52Z" }, { - "login": "DASungta", - "avatarUrl": "https://avatars.githubusercontent.com/u/8896967?v=4", - "profileUrl": "https://github.com/DASungta", + "login": "GIWTO", + "avatarUrl": "https://avatars.githubusercontent.com/u/40267153?v=4", + "profileUrl": "https://github.com/GIWTO", "commits": 12, "mergedPullRequests": 12, - "firstContributionAt": "2026-07-21T04:15:46Z", - "latestContributionAt": "2026-08-23T04:24:47Z" - }, - { - "login": "mr-dadong", - "avatarUrl": "https://avatars.githubusercontent.com/u/102886401?v=4", - "profileUrl": "https://github.com/mr-dadong", - "commits": 12, - "mergedPullRequests": 12, - "firstContributionAt": "2026-08-17T07:04:42Z", - "latestContributionAt": "2026-08-20T14:11:49Z" + "firstContributionAt": "2026-07-25T02:38:05Z", + "latestContributionAt": "2026-08-26T13:38:10Z" }, { "login": "dienaso", @@ -318,15 +327,6 @@ "firstContributionAt": "2026-06-16T09:58:42Z", "latestContributionAt": "2026-07-02T06:03:20Z" }, - { - "login": "GIWTO", - "avatarUrl": "https://avatars.githubusercontent.com/u/40267153?v=4", - "profileUrl": "https://github.com/GIWTO", - "commits": 10, - "mergedPullRequests": 10, - "firstContributionAt": "2026-07-25T02:38:05Z", - "latestContributionAt": "2026-08-19T12:23:17Z" - }, { "login": "aiLi0617", "avatarUrl": "https://avatars.githubusercontent.com/u/76681595?v=4", @@ -363,6 +363,24 @@ "firstContributionAt": "2026-07-18T17:32:18Z", "latestContributionAt": "2026-07-29T16:27:55Z" }, + { + "login": "difagume", + "avatarUrl": "https://avatars.githubusercontent.com/u/5271339?v=4", + "profileUrl": "https://github.com/difagume", + "commits": 7, + "mergedPullRequests": 7, + "firstContributionAt": "2026-07-31T04:15:42Z", + "latestContributionAt": "2026-08-29T20:17:33Z" + }, + { + "login": "hanxuanyu", + "avatarUrl": "https://avatars.githubusercontent.com/u/21254318?v=4", + "profileUrl": "https://github.com/hanxuanyu", + "commits": 7, + "mergedPullRequests": 7, + "firstContributionAt": "2026-08-08T05:23:28Z", + "latestContributionAt": "2026-08-27T04:21:29Z" + }, { "login": "Jinmoli", "avatarUrl": "https://avatars.githubusercontent.com/u/49105556?v=4", @@ -399,15 +417,6 @@ "firstContributionAt": "2026-06-11T15:15:39Z", "latestContributionAt": "2026-07-28T19:47:20Z" }, - { - "login": "hanxuanyu", - "avatarUrl": "https://avatars.githubusercontent.com/u/21254318?v=4", - "profileUrl": "https://github.com/hanxuanyu", - "commits": 6, - "mergedPullRequests": 6, - "firstContributionAt": "2026-08-08T05:23:28Z", - "latestContributionAt": "2026-08-18T16:30:40Z" - }, { "login": "jeeinn", "avatarUrl": "https://avatars.githubusercontent.com/u/13930054?v=4", @@ -435,6 +444,24 @@ "firstContributionAt": "2026-08-02T17:09:52Z", "latestContributionAt": "2026-08-08T03:04:17Z" }, + { + "login": "sxhjlzl", + "avatarUrl": "https://avatars.githubusercontent.com/u/12149913?v=4", + "profileUrl": "https://github.com/sxhjlzl", + "commits": 6, + "mergedPullRequests": 6, + "firstContributionAt": "2026-08-19T03:31:47Z", + "latestContributionAt": "2026-08-26T05:52:42Z" + }, + { + "login": "wzlstudy", + "avatarUrl": "https://avatars.githubusercontent.com/u/54768887?v=4", + "profileUrl": "https://github.com/wzlstudy", + "commits": 6, + "mergedPullRequests": 6, + "firstContributionAt": "2026-08-01T01:44:32Z", + "latestContributionAt": "2026-08-29T17:34:00Z" + }, { "login": "ZionLin2016", "avatarUrl": "https://avatars.githubusercontent.com/u/24632648?v=4", @@ -453,15 +480,6 @@ "firstContributionAt": "2026-07-24T07:06:09Z", "latestContributionAt": "2026-08-07T05:43:30Z" }, - { - "login": "difagume", - "avatarUrl": "https://avatars.githubusercontent.com/u/5271339?v=4", - "profileUrl": "https://github.com/difagume", - "commits": 5, - "mergedPullRequests": 5, - "firstContributionAt": "2026-07-31T04:15:42Z", - "latestContributionAt": "2026-08-15T06:41:54Z" - }, { "login": "fraluc06", "avatarUrl": "https://avatars.githubusercontent.com/u/67560467?v=4", @@ -489,6 +507,24 @@ "firstContributionAt": "2026-07-15T03:09:27Z", "latestContributionAt": "2026-08-18T15:17:24Z" }, + { + "login": "Lan-zk", + "avatarUrl": "https://avatars.githubusercontent.com/u/68137514?v=4", + "profileUrl": "https://github.com/Lan-zk", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-08-13T05:20:22Z", + "latestContributionAt": "2026-08-29T19:57:03Z" + }, + { + "login": "lizzzzz777", + "avatarUrl": "https://avatars.githubusercontent.com/u/72645313?v=4", + "profileUrl": "https://github.com/lizzzzz777", + "commits": 5, + "mergedPullRequests": 5, + "firstContributionAt": "2026-08-14T01:26:28Z", + "latestContributionAt": "2026-08-25T06:46:25Z" + }, { "login": "wbean", "avatarUrl": "https://avatars.githubusercontent.com/u/2233455?v=4", @@ -561,15 +597,6 @@ "firstContributionAt": "2026-06-08T17:32:08Z", "latestContributionAt": "2026-08-06T04:13:57Z" }, - { - "login": "Lan-zk", - "avatarUrl": "https://avatars.githubusercontent.com/u/68137514?v=4", - "profileUrl": "https://github.com/Lan-zk", - "commits": 4, - "mergedPullRequests": 4, - "firstContributionAt": "2026-08-13T05:20:22Z", - "latestContributionAt": "2026-08-19T18:26:56Z" - }, { "login": "soddygo", "avatarUrl": "https://avatars.githubusercontent.com/u/13834644?v=4", @@ -588,15 +615,6 @@ "firstContributionAt": "2026-07-22T04:43:40Z", "latestContributionAt": "2026-07-31T05:54:26Z" }, - { - "login": "sxhjlzl", - "avatarUrl": "https://avatars.githubusercontent.com/u/12149913?v=4", - "profileUrl": "https://github.com/sxhjlzl", - "commits": 4, - "mergedPullRequests": 4, - "firstContributionAt": "2026-08-19T03:31:47Z", - "latestContributionAt": "2026-08-22T17:40:51Z" - }, { "login": "wuxiemian", "avatarUrl": "https://avatars.githubusercontent.com/u/29685283?v=4", @@ -607,13 +625,13 @@ "latestContributionAt": "2026-07-12T13:30:37Z" }, { - "login": "wzlstudy", - "avatarUrl": "https://avatars.githubusercontent.com/u/54768887?v=4", - "profileUrl": "https://github.com/wzlstudy", + "login": "xiaou61", + "avatarUrl": "https://avatars.githubusercontent.com/u/113765024?v=4", + "profileUrl": "https://github.com/xiaou61", "commits": 4, "mergedPullRequests": 4, - "firstContributionAt": "2026-08-01T01:44:32Z", - "latestContributionAt": "2026-08-18T07:36:37Z" + "firstContributionAt": "2026-08-14T09:19:51Z", + "latestContributionAt": "2026-08-28T04:36:37Z" }, { "login": "xzxlove", @@ -723,6 +741,15 @@ "firstContributionAt": "2026-06-27T03:48:16Z", "latestContributionAt": "2026-06-28T06:52:25Z" }, + { + "login": "klinge1011", + "avatarUrl": "https://avatars.githubusercontent.com/u/34301334?v=4", + "profileUrl": "https://github.com/klinge1011", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-08-21T12:23:37Z", + "latestContributionAt": "2026-08-25T14:35:40Z" + }, { "login": "lazyanubis", "avatarUrl": "https://avatars.githubusercontent.com/u/46840787?v=4", @@ -733,13 +760,22 @@ "latestContributionAt": "2026-08-07T05:45:00Z" }, { - "login": "lizzzzz777", - "avatarUrl": "https://avatars.githubusercontent.com/u/72645313?v=4", - "profileUrl": "https://github.com/lizzzzz777", + "login": "Mo-Moore", + "avatarUrl": "https://avatars.githubusercontent.com/u/33098900?v=4", + "profileUrl": "https://github.com/Mo-Moore", "commits": 3, "mergedPullRequests": 3, - "firstContributionAt": "2026-08-14T01:26:28Z", - "latestContributionAt": "2026-08-18T02:51:50Z" + "firstContributionAt": "2026-08-27T05:54:45Z", + "latestContributionAt": "2026-08-28T06:27:56Z" + }, + { + "login": "ordinary-s", + "avatarUrl": "https://avatars.githubusercontent.com/u/59008530?v=4", + "profileUrl": "https://github.com/ordinary-s", + "commits": 3, + "mergedPullRequests": 3, + "firstContributionAt": "2026-08-18T11:36:48Z", + "latestContributionAt": "2026-08-26T14:00:04Z" }, { "login": "polemp", @@ -1011,6 +1047,15 @@ "firstContributionAt": "2026-07-13T10:58:52Z", "latestContributionAt": "2026-08-09T15:33:24Z" }, + { + "login": "StaravenX", + "avatarUrl": "https://avatars.githubusercontent.com/u/231610333?v=4", + "profileUrl": "https://github.com/StaravenX", + "commits": 2, + "mergedPullRequests": 2, + "firstContributionAt": "2026-08-26T11:48:56Z", + "latestContributionAt": "2026-08-28T07:29:07Z" + }, { "login": "TangTangH", "avatarUrl": "https://avatars.githubusercontent.com/u/31877158?v=4", @@ -1083,15 +1128,6 @@ "firstContributionAt": "2026-07-03T06:18:00Z", "latestContributionAt": "2026-07-03T11:25:02Z" }, - { - "login": "xiaou61", - "avatarUrl": "https://avatars.githubusercontent.com/u/113765024?v=4", - "profileUrl": "https://github.com/xiaou61", - "commits": 2, - "mergedPullRequests": 2, - "firstContributionAt": "2026-08-14T09:19:51Z", - "latestContributionAt": "2026-08-14T14:46:33Z" - }, { "login": "yangqinlong", "avatarUrl": "https://avatars.githubusercontent.com/u/7039386?v=4", @@ -1191,6 +1227,15 @@ "firstContributionAt": "2026-05-10T12:02:47Z", "latestContributionAt": "2026-05-10T12:53:40Z" }, + { + "login": "cdredfox", + "avatarUrl": "https://avatars.githubusercontent.com/u/315326?v=4", + "profileUrl": "https://github.com/cdredfox", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-27T05:57:53Z", + "latestContributionAt": "2026-08-27T09:16:58Z" + }, { "login": "CGerAJ", "avatarUrl": "https://avatars.githubusercontent.com/u/20548301?v=4", @@ -1209,6 +1254,15 @@ "firstContributionAt": "2026-08-06T04:32:47Z", "latestContributionAt": "2026-08-10T02:54:27Z" }, + { + "login": "Cherrs", + "avatarUrl": "https://avatars.githubusercontent.com/u/6745593?v=4", + "profileUrl": "https://github.com/Cherrs", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-25T09:42:47Z", + "latestContributionAt": "2026-08-26T05:21:51Z" + }, { "login": "cl1107", "avatarUrl": "https://avatars.githubusercontent.com/u/21985353?v=4", @@ -1245,6 +1299,15 @@ "firstContributionAt": "2026-07-02T07:45:55Z", "latestContributionAt": "2026-07-02T08:37:21Z" }, + { + "login": "debugzhao", + "avatarUrl": "https://avatars.githubusercontent.com/u/32617253?v=4", + "profileUrl": "https://github.com/debugzhao", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-28T14:35:35Z", + "latestContributionAt": "2026-08-30T04:25:55Z" + }, { "login": "Dhevenddra", "avatarUrl": "https://avatars.githubusercontent.com/u/121691114?v=4", @@ -1344,6 +1407,15 @@ "firstContributionAt": "2026-07-05T11:00:04Z", "latestContributionAt": "2026-07-05T11:38:51Z" }, + { + "login": "fanchenggang", + "avatarUrl": "https://avatars.githubusercontent.com/u/20358122?v=4", + "profileUrl": "https://github.com/fanchenggang", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-27T07:09:23Z", + "latestContributionAt": "2026-08-27T09:37:55Z" + }, { "login": "fuuulstack", "avatarUrl": "https://avatars.githubusercontent.com/u/38742225?v=4", @@ -1353,6 +1425,15 @@ "firstContributionAt": "2026-07-23T06:23:37Z", "latestContributionAt": "2026-08-03T07:50:44Z" }, + { + "login": "gaoruiqiang2017", + "avatarUrl": "https://avatars.githubusercontent.com/u/26794805?v=4", + "profileUrl": "https://github.com/gaoruiqiang2017", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-25T11:28:46Z", + "latestContributionAt": "2026-08-25T14:15:41Z" + }, { "login": "gdzhu8023", "avatarUrl": "https://avatars.githubusercontent.com/u/21006156?v=4", @@ -1371,6 +1452,15 @@ "firstContributionAt": "2026-06-01T08:54:31Z", "latestContributionAt": "2026-06-01T10:04:58Z" }, + { + "login": "Hanwn", + "avatarUrl": "https://avatars.githubusercontent.com/u/30523763?v=4", + "profileUrl": "https://github.com/Hanwn", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-29T01:18:22Z", + "latestContributionAt": "2026-08-29T17:11:09Z" + }, { "login": "heyrmi", "avatarUrl": "https://avatars.githubusercontent.com/u/42892759?v=4", @@ -1470,15 +1560,6 @@ "firstContributionAt": "2026-07-23T09:40:04Z", "latestContributionAt": "2026-07-28T17:29:20Z" }, - { - "login": "klinge1011", - "avatarUrl": "https://avatars.githubusercontent.com/u/34301334?v=4", - "profileUrl": "https://github.com/klinge1011", - "commits": 1, - "mergedPullRequests": 1, - "firstContributionAt": "2026-08-21T12:23:37Z", - "latestContributionAt": "2026-08-22T07:41:17Z" - }, { "login": "koco-co", "avatarUrl": "https://avatars.githubusercontent.com/u/227195474?v=4", @@ -1506,6 +1587,15 @@ "firstContributionAt": "2026-08-04T15:56:03Z", "latestContributionAt": "2026-08-05T01:49:14Z" }, + { + "login": "LDQONJ", + "avatarUrl": "https://avatars.githubusercontent.com/u/128981031?v=4", + "profileUrl": "https://github.com/LDQONJ", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-26T02:32:37Z", + "latestContributionAt": "2026-08-26T06:43:53Z" + }, { "login": "leic4u", "avatarUrl": "https://avatars.githubusercontent.com/u/32786903?v=4", @@ -1578,6 +1668,15 @@ "firstContributionAt": "2026-07-14T11:25:31Z", "latestContributionAt": "2026-07-28T20:32:27Z" }, + { + "login": "mmorella-dev", + "avatarUrl": "https://avatars.githubusercontent.com/u/4561733?v=4", + "profileUrl": "https://github.com/mmorella-dev", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-28T19:54:04Z", + "latestContributionAt": "2026-08-29T20:36:35Z" + }, { "login": "monellin", "avatarUrl": "https://avatars.githubusercontent.com/u/17845081?v=4", @@ -1596,15 +1695,6 @@ "firstContributionAt": "2026-08-15T16:59:21Z", "latestContributionAt": "2026-08-16T03:21:22Z" }, - { - "login": "octo-patch", - "avatarUrl": "https://avatars.githubusercontent.com/u/266937838?v=4", - "profileUrl": "https://github.com/octo-patch", - "commits": 1, - "mergedPullRequests": 1, - "firstContributionAt": "2026-07-30T10:58:25Z", - "latestContributionAt": "2026-07-30T15:05:13Z" - }, { "login": "OctoBored", "avatarUrl": "https://avatars.githubusercontent.com/u/212877535?v=4", @@ -1659,6 +1749,15 @@ "firstContributionAt": "2026-08-03T06:58:44Z", "latestContributionAt": "2026-08-04T06:23:45Z" }, + { + "login": "s1oopX", + "avatarUrl": "https://avatars.githubusercontent.com/u/185045939?v=4", + "profileUrl": "https://github.com/s1oopX", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-29T11:49:11Z", + "latestContributionAt": "2026-08-29T17:02:01Z" + }, { "login": "shnno13", "avatarUrl": "https://avatars.githubusercontent.com/u/14847218?v=4", @@ -1812,6 +1911,15 @@ "firstContributionAt": "2026-08-17T06:30:17Z", "latestContributionAt": "2026-08-18T04:47:53Z" }, + { + "login": "wzlove", + "avatarUrl": "https://avatars.githubusercontent.com/u/35476126?v=4", + "profileUrl": "https://github.com/wzlove", + "commits": 1, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-25T11:23:05Z", + "latestContributionAt": "2026-08-25T14:34:04Z" + }, { "login": "xcstatus", "avatarUrl": "https://avatars.githubusercontent.com/u/40375067?v=4", @@ -1973,6 +2081,15 @@ "mergedPullRequests": 1, "firstContributionAt": "2026-06-23T05:56:48Z", "latestContributionAt": "2026-06-23T17:44:46Z" + }, + { + "login": "octo-patch", + "avatarUrl": "https://avatars.githubusercontent.com/u/266937838?v=4", + "profileUrl": "https://github.com/octo-patch", + "commits": 0, + "mergedPullRequests": 1, + "firstContributionAt": "2026-07-30T10:58:25Z", + "latestContributionAt": "2026-07-30T15:05:13Z" } ] }