use std::sync::Arc;
use bytes::Bytes;
use futures::StreamExt;
use tokio::sync::Mutex;
use crate::error::PgError;
fn escape_copy_value(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 4);
for ch in value.chars() {
match ch {
'\\' => out.push_str(r"\\"),
'\t' => out.push_str(r"\t"),
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
c => out.push(c),
}
}
out
}
fn unescape_copy_value(value: &str) -> String {
let mut out = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('\\') => out.push('\\'),
Some('t') => out.push('\t'),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('N') => {
out.push('\\');
out.push('N');
}
Some(c) => {
out.push('\\');
out.push(c);
}
None => out.push('\\'),
}
} else {
out.push(ch);
}
}
out
}
pub(crate) async fn copy_in_text(
inner: &Arc<Mutex<tokio_postgres::Client>>,
table: &str,
columns: &[&str],
rows: impl Iterator<Item = Vec<String>>,
) -> Result<u64, PgError> {
if columns.is_empty() {
return Err(PgError::Copy(
"column list must not be empty for COPY IN".to_string(),
));
}
let col_list = columns.join(", ");
let query = format!("COPY {table} ({col_list}) FROM STDIN");
let mut buf = Vec::new();
for row in rows {
if row.len() != columns.len() {
return Err(PgError::Copy(format!(
"row has {} fields but {} columns were specified",
row.len(),
columns.len()
)));
}
for (i, cell) in row.iter().enumerate() {
if i > 0 {
buf.push(b'\t');
}
buf.extend_from_slice(escape_copy_value(cell).as_bytes());
}
buf.push(b'\n');
}
let payload = Bytes::from(buf);
let client = inner.lock().await;
let sink = client
.copy_in::<_, Bytes>(&query as &str)
.await
.map_err(|e| PgError::Copy(e.to_string()))?;
tokio::pin!(sink);
use futures::SinkExt;
sink.send(payload)
.await
.map_err(|e| PgError::Copy(e.to_string()))?;
let rows_copied = sink
.finish()
.await
.map_err(|e| PgError::Copy(e.to_string()))?;
Ok(rows_copied)
}
pub(crate) async fn copy_out_text(
inner: &Arc<Mutex<tokio_postgres::Client>>,
table: &str,
columns: &[&str],
) -> Result<Vec<Vec<String>>, PgError> {
if columns.is_empty() {
return Err(PgError::Copy(
"column list must not be empty for COPY OUT".to_string(),
));
}
let col_list = columns.join(", ");
let query = format!("COPY {table} ({col_list}) TO STDOUT");
let client = inner.lock().await;
let stream = client
.copy_out(&query as &str)
.await
.map_err(|e| PgError::Copy(e.to_string()))?;
let chunks: Vec<Bytes> = stream
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.map_err(|e| PgError::Copy(e.to_string()))?;
let total_len: usize = chunks.iter().map(|b| b.len()).sum();
let mut raw = Vec::with_capacity(total_len);
for chunk in &chunks {
raw.extend_from_slice(chunk);
}
let text = String::from_utf8(raw)
.map_err(|e| PgError::Copy(format!("COPY OUT returned non-UTF-8 data: {e}")))?;
let rows = text
.split('\n')
.filter(|line| !line.is_empty())
.map(|line| {
line.split('\t')
.map(unescape_copy_value)
.collect::<Vec<String>>()
})
.collect::<Vec<Vec<String>>>();
Ok(rows)
}