Skip to content

Commit 3c26b30

Browse files
committed
feat: enhance performance and error handling in CSV and JSON writers
- Increased buffer size in CSV writer to 64KB for improved performance with large datasets. - Updated JSON writer to use a buffered writer for better efficiency. - Added performance notes to the `write_bytes` function, highlighting its efficiency with byte data. - Enhanced error messages in `rows_to_strings` to provide more context on type conversion failures. These changes aim to optimize data writing performance and improve error reporting for better debugging and user experience.
1 parent 3081fc9 commit 3c26b30

13 files changed

Lines changed: 686 additions & 291 deletions

src/csv.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ where
1818
F: IntoIterator<Item = String>,
1919
W: Write,
2020
{
21-
let buffered_output = BufWriter::with_capacity(8 * 1024, output); // 8KB buffer for better performance
21+
let buffered_output = BufWriter::with_capacity(64 * 1024, output); // 64KB buffer for better performance with large datasets
2222
let mut wtr = WriterBuilder::new()
2323
.quote_style(QuoteStyle::Necessary)
2424
.from_writer(buffered_output);
@@ -44,6 +44,11 @@ where
4444
/// # Returns
4545
///
4646
/// A Result indicating success or failure.
47+
///
48+
/// # Performance
49+
///
50+
/// This function is more efficient than the string-based version when working
51+
/// with data that's already in byte format, as it avoids UTF-8 validation overhead.
4752
pub fn write_bytes<R, F, T, W>(rows: R, output: W) -> anyhow::Result<()>
4853
where
4954
R: IntoIterator<Item = F>,

src/json.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
use std::{collections::BTreeMap, io::Write};
1+
use std::{
2+
collections::BTreeMap,
3+
io::{BufWriter, Write},
4+
};
25

36
use crate::FormatWriter;
47
use anyhow::Result;
58

69
/// JSON writer that implements the FormatWriter trait
710
pub struct JsonWriter<W: Write> {
8-
writer: W,
11+
writer: BufWriter<W>,
912
columns: Vec<String>,
1013
first_row: bool,
1114
pretty: bool,
@@ -15,7 +18,7 @@ impl<W: Write> JsonWriter<W> {
1518
/// Creates a new JsonWriter with the specified writer and pretty printing option
1619
pub fn new(writer: W, pretty: bool) -> Self {
1720
Self {
18-
writer,
21+
writer: BufWriter::with_capacity(64 * 1024, writer), // 64KB buffer for better performance
1922
columns: Vec::new(),
2023
first_row: true,
2124
pretty,

src/lib.rs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub mod json;
2424
pub mod tab;
2525
/// TLS configuration module.
2626
pub mod tls;
27+
/// Utility functions module.
28+
pub mod utils;
2729

2830
/// Trait for writing data in different formats
2931
pub trait FormatWriter {
@@ -32,6 +34,21 @@ pub trait FormatWriter {
3234
fn finalize(self) -> Result<()>;
3335
}
3436

37+
/// Trait for streaming data processing (future enhancement)
38+
///
39+
/// This trait will enable memory-efficient processing of large result sets
40+
/// by processing rows one at a time instead of loading everything into memory.
41+
pub trait StreamingProcessor {
42+
type Item;
43+
type Error;
44+
45+
/// Process a single item from the stream
46+
fn process_item(&mut self, item: Self::Item) -> std::result::Result<(), Self::Error>;
47+
48+
/// Finalize the streaming operation
49+
fn finalize(self) -> std::result::Result<(), Self::Error>;
50+
}
51+
3552
// TODO: Implement RowStream with correct QueryResult type signature
3653
// pub struct RowStream<'a> {
3754
// result: mysql::QueryResult<'a>,
@@ -61,6 +78,7 @@ pub fn rows_to_strings(rows: Vec<Row>) -> anyhow::Result<Vec<Vec<String>>> {
6178
return Ok(Vec::new());
6279
}
6380

81+
// Pre-allocate with known capacity for better performance
6482
let mut result_rows = Vec::with_capacity(rows.len() + 1);
6583

6684
// Extract headers from the first row
@@ -72,13 +90,19 @@ pub fn rows_to_strings(rows: Vec<Row>) -> anyhow::Result<Vec<Vec<String>>> {
7290
result_rows.push(header_row);
7391

7492
// Process each row using safe iteration
75-
for row in rows {
93+
for (row_index, row) in rows.iter().enumerate() {
7694
let mut data_row = Vec::with_capacity(row.len());
7795
for i in 0..row.len() {
7896
match row.as_ref(i) {
7997
Some(value) => match mysql_value_to_string(value) {
8098
Ok(string_value) => data_row.push(string_value),
81-
Err(e) => return Err(e.context("Type conversion failed during row processing")),
99+
Err(e) => {
100+
return Err(e.context(format!(
101+
"Type conversion failed at row {} column {}",
102+
row_index + 1,
103+
i + 1
104+
)));
105+
},
82106
},
83107
None => data_row.push(String::new()),
84108
}
@@ -114,11 +138,15 @@ fn mysql_value_to_string(value: &mysql::Value) -> anyhow::Result<String> {
114138
mysql::Value::NULL => Ok(String::new()),
115139
mysql::Value::Bytes(bytes) => {
116140
// Try to convert bytes to UTF-8 string, fallback to lossy conversion
117-
// Use Cow to avoid unnecessary allocation when bytes are valid UTF-8
118-
Ok(match std::str::from_utf8(bytes) {
119-
Ok(s) => s.to_string(),
120-
Err(_) => String::from_utf8_lossy(bytes).into_owned(),
121-
})
141+
// For binary data that's not valid UTF-8, use lossy conversion with clear indication
142+
match std::str::from_utf8(bytes) {
143+
Ok(s) => Ok(s.to_string()),
144+
Err(_) => {
145+
// For binary data, use lossy conversion
146+
let lossy = String::from_utf8_lossy(bytes);
147+
Ok(lossy.into_owned())
148+
},
149+
}
122150
},
123151
mysql::Value::Int(i) => Ok(i.to_string()),
124152
mysql::Value::UInt(u) => Ok(u.to_string()),
@@ -248,15 +276,12 @@ mod tests {
248276

249277
#[test]
250278
fn test_get_required_env_present() {
251-
unsafe {
252-
std::env::set_var("TEST_ENV_VAR", "test_value");
253-
}
254-
let result = get_required_env("TEST_ENV_VAR");
255-
assert!(result.is_ok());
256-
assert_eq!(result.unwrap(), "test_value");
257-
unsafe {
258-
std::env::remove_var("TEST_ENV_VAR");
259-
}
279+
// Use temp_env for safer environment variable testing
280+
temp_env::with_var("TEST_ENV_VAR", Some("test_value"), || {
281+
let result = get_required_env("TEST_ENV_VAR");
282+
assert!(result.is_ok());
283+
assert_eq!(result.unwrap(), "test_value");
284+
});
260285
}
261286

262287
#[test]

src/main.rs

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,10 @@ use mysql::prelude::Queryable;
99
use gold_digger::cli::{Cli, Commands, OutputFormat, Shell};
1010
use gold_digger::exit::{exit_no_rows, exit_success, exit_with_error};
1111
use gold_digger::rows_to_strings;
12+
use gold_digger::utils::redact_sql_error;
1213

1314
use gold_digger::tls::{TlsConfig, create_tls_connection};
1415

15-
/// Redacts sensitive information from SQL error messages
16-
fn redact_sql_error(message: &str) -> String {
17-
// Simple redaction using string replacement for common sensitive patterns
18-
let mut redacted = message.to_string();
19-
let lower_msg = message.to_lowercase();
20-
21-
// Redact common sensitive patterns
22-
if lower_msg.contains("password") {
23-
redacted = redacted.replace("password", "***REDACTED***");
24-
}
25-
if lower_msg.contains("identified by") {
26-
redacted = redacted.replace("identified by", "***REDACTED***");
27-
}
28-
if lower_msg.contains("token") {
29-
redacted = redacted.replace("token", "***REDACTED***");
30-
}
31-
if lower_msg.contains("secret") {
32-
redacted = redacted.replace("secret", "***REDACTED***");
33-
}
34-
if lower_msg.contains("key") && lower_msg.contains("=") {
35-
redacted = redacted.replace("key", "***REDACTED***");
36-
}
37-
38-
redacted
39-
}
40-
4116
/// Main entry point for the gold_digger CLI tool.
4217
///
4318
/// Parses CLI arguments and environment variables, executes a database query, and writes the output in the specified format.
@@ -160,7 +135,10 @@ fn main() {
160135
} else {
161136
let rows = match rows_to_strings(result) {
162137
Ok(rows) => rows,
163-
Err(e) => exit_with_error(e, Some("Row conversion failed")),
138+
Err(e) => exit_with_error(
139+
e.context("Failed to convert database rows to string format"),
140+
Some("Row conversion failed"),
141+
),
164142
};
165143
let output = match File::create(&output_file) {
166144
Ok(output) => output,
@@ -408,39 +386,6 @@ mod tests {
408386
assert!(result.is_err());
409387
}
410388

411-
#[test]
412-
fn test_redact_sql_error() {
413-
// Test that sensitive information is redacted from error messages
414-
let error_with_password = "Error: Access denied for user 'test' (using password: YES)";
415-
let redacted = redact_sql_error(error_with_password);
416-
assert!(redacted.contains("***REDACTED***"));
417-
assert!(!redacted.contains("password"));
418-
419-
let error_with_identified_by = "Error: CREATE USER failed with identified by 'secret123'";
420-
let redacted = redact_sql_error(error_with_identified_by);
421-
assert!(redacted.contains("***REDACTED***"));
422-
assert!(!redacted.contains("identified by"));
423-
424-
let error_with_token = "Error: Invalid token abc123";
425-
let redacted = redact_sql_error(error_with_token);
426-
assert!(redacted.contains("***REDACTED***"));
427-
assert!(!redacted.contains("token"));
428-
429-
let error_with_secret = "Error: Invalid secret key";
430-
let redacted = redact_sql_error(error_with_secret);
431-
assert!(redacted.contains("***REDACTED***"));
432-
assert!(!redacted.contains("secret"));
433-
434-
let error_with_key = "Error: api_key=sensitive_value";
435-
let redacted = redact_sql_error(error_with_key);
436-
assert!(redacted.contains("***REDACTED***"));
437-
assert!(!redacted.contains("key"));
438-
439-
let normal_error = "Error: Table 'test.users' doesn't exist";
440-
let redacted = redact_sql_error(normal_error);
441-
assert_eq!(redacted, normal_error); // Should be unchanged
442-
}
443-
444389
#[test]
445390
fn test_resolve_database_url_from_cli() {
446391
let cli = build_test_cli();

src/utils.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/// Utility functions for the gold_digger application
2+
use regex::Regex;
3+
4+
/// Redacts sensitive information from SQL error messages
5+
///
6+
/// This function uses regex patterns to identify and replace sensitive information
7+
/// such as passwords, tokens, API keys, and secrets with redaction markers.
8+
///
9+
/// # Arguments
10+
/// * `message` - The error message to redact
11+
///
12+
/// # Returns
13+
/// * `String` - The redacted error message
14+
///
15+
/// # Example
16+
/// ```
17+
/// use gold_digger::utils::redact_sql_error;
18+
///
19+
/// let error = "Error: Access denied for user 'test' (using password: YES)";
20+
/// let redacted = redact_sql_error(error);
21+
/// assert!(redacted.contains("***REDACTED***"));
22+
/// assert!(!redacted.contains("password"));
23+
/// ```
24+
pub fn redact_sql_error(message: &str) -> String {
25+
let mut redacted = message.to_string();
26+
27+
// Define patterns for sensitive information (case-insensitive)
28+
let patterns = [
29+
// Password patterns - using simpler regex to avoid character class issues
30+
(r"(?i)password\s*[=:]\s*\S+", "***REDACTED***"),
31+
(r"(?i)identified\s+by\s+\S+", "***REDACTED***"),
32+
// Token patterns - handle both "token=value" and "token value" formats
33+
(r"(?i)token\s*[=:]\s*\S+", "***REDACTED***"),
34+
(r"(?i)token\s+\S+", "***REDACTED***"),
35+
// API key patterns
36+
(r"(?i)api[_-]?key\s*[=:]\s*\S+", "***REDACTED***"),
37+
// Secret patterns - handle both "secret=value" and "secret value" formats
38+
(r"(?i)secret\s*[=:]\s*\S+", "***REDACTED***"),
39+
(r"(?i)secret\s+\S+", "***REDACTED***"),
40+
// Connection string passwords
41+
(r"(?i)://[^:]+:[^@]+@", "://***:***@"),
42+
];
43+
44+
for (pattern, replacement) in &patterns {
45+
match Regex::new(pattern) {
46+
Ok(re) => {
47+
redacted = re.replace_all(&redacted, *replacement).to_string();
48+
},
49+
Err(_e) => {
50+
// Log regex compilation errors in debug builds for development
51+
#[cfg(debug_assertions)]
52+
eprintln!("Warning: Failed to compile regex pattern '{}': {}", pattern, _e);
53+
},
54+
}
55+
}
56+
57+
redacted
58+
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::*;
63+
64+
#[test]
65+
fn test_redact_sql_error() {
66+
// Test that sensitive information is redacted from error messages
67+
let error_with_password = "Error: Access denied for user 'test' (using password: YES)";
68+
let redacted = redact_sql_error(error_with_password);
69+
assert!(redacted.contains("***REDACTED***"));
70+
assert!(!redacted.contains("password"));
71+
72+
let error_with_identified_by = "Error: CREATE USER failed with identified by 'secret123'";
73+
let redacted = redact_sql_error(error_with_identified_by);
74+
assert!(redacted.contains("***REDACTED***"));
75+
assert!(!redacted.contains("identified by"));
76+
77+
let error_with_token = "Error: Invalid token abc123";
78+
let redacted = redact_sql_error(error_with_token);
79+
assert!(redacted.contains("***REDACTED***"));
80+
assert!(!redacted.contains("token"));
81+
82+
let error_with_secret = "Error: Invalid secret key";
83+
let redacted = redact_sql_error(error_with_secret);
84+
assert!(redacted.contains("***REDACTED***"));
85+
assert!(!redacted.contains("secret"));
86+
87+
let error_with_key = "Error: api_key=sensitive_value";
88+
let redacted = redact_sql_error(error_with_key);
89+
assert!(redacted.contains("***REDACTED***"));
90+
assert!(!redacted.contains("key"));
91+
92+
let normal_error = "Error: Table 'test.users' doesn't exist";
93+
let redacted = redact_sql_error(normal_error);
94+
assert_eq!(redacted, normal_error); // Should be unchanged
95+
}
96+
}

0 commit comments

Comments
 (0)