#![allow(clippy::module_name_repetitions)]
use cratefield_core::{DbError, Row};
use sea_query::Value as SeaValue;
use sqlx::postgres::{PgArguments, PgRow};
use sqlx::{Arguments as _, Column as _, Row as _, TypeInfo as _};
pub(crate) fn rebind_placeholders(sql: &str) -> String {
let bytes = sql.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len() + 8);
let mut placeholder = 0usize;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
match b {
b'\'' => {
out.push(b);
i += 1;
while i < bytes.len() {
out.push(bytes[i]);
if bytes[i] == b'\'' {
i += 1;
if i < bytes.len() && bytes[i] == b'\'' {
out.push(bytes[i]);
i += 1;
} else {
break;
}
} else {
i += 1;
}
}
}
b'"' => {
out.push(b);
i += 1;
while i < bytes.len() {
out.push(bytes[i]);
if bytes[i] == b'"' {
i += 1;
if i < bytes.len() && bytes[i] == b'"' {
out.push(bytes[i]);
i += 1;
} else {
break;
}
} else {
i += 1;
}
}
}
b'`' => {
out.push(b);
i += 1;
while i < bytes.len() && bytes[i] != b'`' {
out.push(bytes[i]);
i += 1;
}
if i < bytes.len() {
out.push(bytes[i]);
i += 1;
}
}
b'-' if bytes.get(i + 1) == Some(&b'-') => {
while i < bytes.len() && bytes[i] != b'\n' {
out.push(bytes[i]);
i += 1;
}
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
out.push(b'/');
out.push(b'*');
i += 2;
while i < bytes.len() {
out.push(bytes[i]);
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
out.push(b'/');
i += 2;
break;
}
i += 1;
}
}
b'?' => {
placeholder += 1;
out.extend_from_slice(format!("${placeholder}").as_bytes());
i += 1;
}
other => {
out.push(other);
i += 1;
}
}
}
String::from_utf8(out).expect("rebinding preserves UTF-8")
}
pub(crate) fn bind_values(args: &mut PgArguments, values: &[SeaValue]) -> Result<(), DbError> {
for value in values {
bind_one(args, value)?;
}
Ok(())
}
fn bind_one(args: &mut PgArguments, value: &SeaValue) -> Result<(), DbError> {
let result = match value {
SeaValue::Bool(Some(v)) => args.add(i16::from(*v)),
SeaValue::Bool(None) | SeaValue::SmallInt(None) | SeaValue::TinyUnsigned(None) => {
args.add(Option::<i16>::None)
}
SeaValue::TinyInt(Some(v)) => args.add(*v),
SeaValue::TinyInt(None) => args.add(Option::<i8>::None),
SeaValue::SmallInt(Some(v)) => args.add(*v),
SeaValue::Int(Some(v)) => args.add(*v),
SeaValue::Int(None) | SeaValue::SmallUnsigned(None) => args.add(Option::<i32>::None),
SeaValue::BigInt(Some(v)) => args.add(*v),
SeaValue::TinyUnsigned(Some(v)) => args.add(i16::from(*v)),
SeaValue::SmallUnsigned(Some(v)) => args.add(i32::from(*v)),
SeaValue::Unsigned(Some(v)) => args.add(i64::from(*v)),
SeaValue::BigUnsigned(Some(v)) => {
let widened = i64::try_from(*v).map_err(|_| {
DbError::Execute(format!("u64 value {v} does not fit Postgres BIGINT"))
})?;
args.add(widened)
}
SeaValue::BigInt(None) | SeaValue::Unsigned(None) | SeaValue::BigUnsigned(None) => {
args.add(Option::<i64>::None)
}
SeaValue::Float(Some(v)) => args.add(*v),
SeaValue::Float(None) => args.add(Option::<f32>::None),
SeaValue::Double(Some(v)) => args.add(*v),
SeaValue::Double(None) => args.add(Option::<f64>::None),
SeaValue::String(Some(v)) => args.add(v.as_str()),
SeaValue::String(None) => args.add(Option::<&str>::None),
SeaValue::Char(Some(v)) => args.add(v.to_string()),
SeaValue::Char(None) => args.add(Option::<String>::None),
SeaValue::Bytes(Some(v)) => args.add::<&[u8]>(&**v),
SeaValue::Bytes(None) => args.add(Option::<Vec<u8>>::None),
};
result.map_err(|err| DbError::Execute(err.to_string()))
}
pub(crate) fn pg_row_to_row(row: &PgRow) -> Row {
let mut columns = Vec::with_capacity(row.columns().len());
for (index, column) in row.columns().iter().enumerate() {
let name = column.name().to_string();
let type_name = column.type_info().name().to_ascii_uppercase();
let value = match type_name.as_str() {
"BOOL" => row
.try_get::<Option<bool>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::Bool(None), |v| SeaValue::Bool(Some(v))),
"INT2" | "SMALLINT" => row
.try_get::<Option<i16>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::SmallInt(None), |v| SeaValue::SmallInt(Some(v))),
"INT4" | "INT" | "INTEGER" => row
.try_get::<Option<i32>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::Int(None), |v| SeaValue::Int(Some(v))),
"INT8" | "BIGINT" => row
.try_get::<Option<i64>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::BigInt(None), |v| SeaValue::BigInt(Some(v))),
"FLOAT4" | "REAL" => row
.try_get::<Option<f32>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::Float(None), |v| SeaValue::Float(Some(v))),
"FLOAT8" | "DOUBLE PRECISION" => row
.try_get::<Option<f64>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::Double(None), |v| SeaValue::Double(Some(v))),
"BYTEA" => row
.try_get::<Option<Vec<u8>>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::Bytes(None), |v| {
SeaValue::Bytes(Some(Box::new(v)))
}),
_ => row
.try_get::<Option<String>, usize>(index)
.ok()
.flatten()
.map_or(SeaValue::String(None), |v| {
SeaValue::String(Some(Box::new(v)))
}),
};
columns.push((name, value));
}
Row::new(columns)
}
#[cfg(test)]
mod tests {
use super::rebind_placeholders;
#[test]
fn rewrites_placeholders_positionally() {
assert_eq!(
rebind_placeholders("INSERT INTO t (a, b) VALUES (?, ?)"),
"INSERT INTO t (a, b) VALUES ($1, $2)"
);
}
#[test]
fn leaves_question_marks_in_string_literals_alone() {
assert_eq!(
rebind_placeholders("INSERT INTO t VALUES ('what?', ?, 'a''?b')"),
"INSERT INTO t VALUES ('what?', $1, 'a''?b')"
);
}
#[test]
fn leaves_question_marks_in_identifiers_and_comments_alone() {
assert_eq!(
rebind_placeholders("SELECT \"col?\" FROM t -- real ?\nWHERE id = ? /* ? */"),
"SELECT \"col?\" FROM t -- real ?\nWHERE id = $1 /* ? */"
);
}
#[test]
fn handles_empty_and_placeholder_free_sql() {
assert_eq!(rebind_placeholders("SELECT 1"), "SELECT 1");
assert_eq!(rebind_placeholders(""), "");
}
#[test]
fn handles_backtick_quoted_and_escaped_strings() {
assert_eq!(
rebind_placeholders("UPDATE `t?` SET a = ?"),
"UPDATE `t?` SET a = $1"
);
}
}