mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(sql-file): import mysql dumps with binary literals
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<MysqlDumpBinaryLiteralNormalizer>,
|
||||
pending_bytes: Vec<u8>,
|
||||
pending_decoded_bytes: Vec<u8>,
|
||||
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, String> {
|
||||
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<usize>) -> Result<Self, String> {
|
||||
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, String> {
|
||||
Self::open_with_options(file_path, None, normalize_mysql_binary_literals).await
|
||||
}
|
||||
|
||||
async fn open_with_options(
|
||||
file_path: &Path,
|
||||
detection_limit: Option<usize>,
|
||||
normalize_mysql_binary_literals: bool,
|
||||
) -> Result<Self, String> {
|
||||
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<Option<String>, 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<u8>,
|
||||
previous_source_byte: Option<u8>,
|
||||
quoted_escape_pending: bool,
|
||||
}
|
||||
|
||||
impl MysqlDumpBinaryLiteralNormalizer {
|
||||
fn normalize(&mut self, input: &[u8], eof: bool) -> Result<Vec<u8>, 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<u8> {
|
||||
if index > 0 {
|
||||
self.pending.get(index - 1).copied()
|
||||
} else {
|
||||
self.previous_source_byte
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_sql_identifier_byte(byte: Option<u8>) -> bool {
|
||||
byte.is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$'))
|
||||
}
|
||||
|
||||
fn append_hex(output: &mut Vec<u8>, 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<usize>,
|
||||
) -> 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<usize>,
|
||||
) -> Result<bool, String> {
|
||||
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!(
|
||||
|
||||
@@ -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")]]);
|
||||
}
|
||||
|
||||
+262
-145
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user