use crate::value::DataValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandType {
Text,
StoredProcedure,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
pub name: String,
pub value: DataValue,
}
impl Parameter {
pub fn new<T: Into<DataValue>>(name: impl Into<String>, value: T) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
pub fn bare_name(&self) -> &str {
self.name.strip_prefix('@').unwrap_or(&self.name)
}
pub fn at_name(&self) -> String {
format!("@{}", self.bare_name())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
text: String,
command_type: CommandType,
parameters: Vec<Parameter>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Prepared {
Text { sql: String, params: Vec<DataValue> },
Proc {
name: String,
params: Vec<(String, DataValue)>,
},
}
impl Command {
pub fn query(text: impl Into<String>) -> Self {
Self {
text: text.into(),
command_type: CommandType::Text,
parameters: Vec::new(),
}
}
pub fn stored_procedure(name: impl Into<String>) -> Self {
Self {
text: name.into(),
command_type: CommandType::StoredProcedure,
parameters: Vec::new(),
}
}
pub fn param<T: Into<DataValue>>(mut self, name: impl Into<String>, value: T) -> Self {
self.parameters.push(Parameter::new(name, value));
self
}
pub fn params<I, N, T>(mut self, params: I) -> Self
where
I: IntoIterator<Item = (N, T)>,
N: Into<String>,
T: Into<DataValue>,
{
self.parameters
.extend(params.into_iter().map(|(n, v)| Parameter::new(n, v)));
self
}
pub fn with_parameter(mut self, param: Parameter) -> Self {
self.parameters.push(param);
self
}
pub fn text(&self) -> &str {
&self.text
}
pub fn command_type(&self) -> CommandType {
self.command_type
}
pub fn parameters(&self) -> &[Parameter] {
&self.parameters
}
pub(crate) fn prepare(&self) -> Prepared {
match self.command_type {
CommandType::StoredProcedure => Prepared::Proc {
name: self.text.clone(),
params: self
.parameters
.iter()
.map(|p| (p.at_name(), p.value.clone()))
.collect(),
},
CommandType::Text => {
let mut sql = self.text.clone();
for (i, p) in self.parameters.iter().enumerate() {
let bare = p.bare_name();
if is_ordinal_placeholder(bare) {
continue;
}
sql = replace_param_token(&sql, &format!("@{bare}"), &format!("@P{}", i + 1));
}
Prepared::Text {
sql,
params: self.parameters.iter().map(|p| p.value.clone()).collect(),
}
}
}
}
}
fn is_ordinal_placeholder(name: &str) -> bool {
match name.strip_prefix('P') {
Some(rest) => !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()),
None => false,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Region {
Sql,
String,
QuotedIdent,
BracketIdent,
LineComment,
BlockComment(u32),
}
fn is_ident_continue(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '@' || c == '$' || c == '#'
}
fn replace_param_token(haystack: &str, needle: &str, replacement: &str) -> String {
let bytes = haystack.as_bytes();
let nlen = needle.len();
let mut out = String::with_capacity(haystack.len());
let mut region = Region::Sql;
let mut prev: Option<char> = None;
let mut i = 0;
macro_rules! emit {
($text:expr) => {{
out.push_str($text);
prev = $text.chars().last();
i += $text.len();
continue;
}};
}
while i < bytes.len() {
match region {
Region::Sql => {
match bytes[i] {
b'\'' => {
region = Region::String;
emit!("'")
}
b'"' => {
region = Region::QuotedIdent;
emit!("\"")
}
b'[' => {
region = Region::BracketIdent;
emit!("[")
}
b'-' if bytes.get(i + 1) == Some(&b'-') => {
region = Region::LineComment;
emit!("--")
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
region = Region::BlockComment(1);
emit!("/*")
}
_ => {}
}
if bytes[i..].starts_with(needle.as_bytes())
&& prev.is_none_or(|c| !is_ident_continue(c))
&& haystack[i + nlen..]
.chars()
.next()
.is_none_or(|c| !is_ident_continue(c))
{
out.push_str(replacement);
prev = replacement.chars().last();
i += nlen;
continue;
}
}
Region::String => {
if bytes[i] == b'\'' {
if bytes.get(i + 1) == Some(&b'\'') {
emit!("''")
}
region = Region::Sql;
}
}
Region::QuotedIdent => {
if bytes[i] == b'"' {
if bytes.get(i + 1) == Some(&b'"') {
emit!("\"\"")
}
region = Region::Sql;
}
}
Region::BracketIdent => {
if bytes[i] == b']' {
if bytes.get(i + 1) == Some(&b']') {
emit!("]]")
}
region = Region::Sql;
}
}
Region::LineComment => {
if bytes[i] == b'\n' {
region = Region::Sql;
}
}
Region::BlockComment(depth) => {
if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
region = Region::BlockComment(depth + 1);
emit!("/*")
}
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
region = if depth == 1 {
Region::Sql
} else {
Region::BlockComment(depth - 1)
};
emit!("*/")
}
}
}
let ch = haystack[i..]
.chars()
.next()
.expect("index is on a char boundary");
out.push(ch);
prev = Some(ch);
i += ch.len_utf8();
}
out
}
#[cfg(test)]
mod tests;