cido 0.2.0

Core traits and implementations for indexing with cido
Documentation
//! Product types for handling query building from primitive types.
pub(crate) mod order_direction;
pub(crate) mod query_builder;

pub use order_direction::Direction;
pub use query_builder::*;

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct SqlKeywords;

#[allow(non_upper_case_globals)]
impl SqlKeywords {
  pub const Null: &'static str = " IS NULL ";
  #[allow(dead_code)]
  pub const NotNull: &'static str = " IS NOT NULL ";
  pub const Eq: &'static str = " = ";
  pub const Not: &'static str = " != ";
  pub const Gt: &'static str = " > ";
  pub const Gte: &'static str = " >= ";
  pub const Lt: &'static str = " < ";
  pub const Lte: &'static str = " <= ";
  pub const In: &'static str = " IN ( ";
  pub const NotIn: &'static str = " NOT IN ( ";
  pub const Contains: &'static str = " LIKE ";
  pub const ContainsNoCase: &'static str = " ILIKE ";
  pub const NotContains: &'static str = " NOT LIKE ";
  pub const NotContainsNoCase: &'static str = " NOT ILIKE ";
  pub const StartsWith: &'static str = " LIKE ";
  pub const StartsWithNoCase: &'static str = " ILIKE ";
  pub const NotStartsWith: &'static str = " NOT LIKE ";
  pub const NotStartsWithNoCase: &'static str = " NOT ILIKE ";
  pub const EndsWith: &'static str = " LIKE ";
  pub const EndsWithNoCase: &'static str = " ILIKE ";
  pub const NotEndsWith: &'static str = " NOT LIKE ";
  pub const NotEndsWithNoCase: &'static str = " NOT ILIKE ";
}

// This has a pr for merging into sqlx at https://github.com/launchbadge/sqlx/pull/3651
pub struct PgBindIter<I>(pub I);

impl<'q, T, I> From<I> for PgBindIter<I>
where
  T: sqlx::Type<sqlx::Postgres> + sqlx::Encode<'q, sqlx::Postgres> + Send + 'q,
  I: Iterator<Item = T> + Clone + Send,
{
  fn from(inner: I) -> Self {
    Self(inner)
  }
}

// copied from https://github.com/launchbadge/sqlx/blob/main/sqlx-postgres/src/types/array.rs#154
impl<'q, T, I> PgBindIter<I>
where
  I: Iterator<Item = T>,
  T: sqlx::Type<sqlx::Postgres> + sqlx::Encode<'q, sqlx::Postgres> + 'q,
{
  fn encode_owned(
    iter: I,
    buf: &mut sqlx::postgres::PgArgumentBuffer,
  ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
    const I32_SIZE: usize = core::mem::size_of::<i32>();
    let mut iter = iter.peekable();
    let first = iter.peek();
    let type_info = first
      .and_then(sqlx::Encode::produces)
      .unwrap_or_else(T::type_info);
    buf.extend(&1_i32.to_be_bytes()); // number of dimensions
    buf.extend(&0_i32.to_be_bytes()); // flags

    match type_info.oid() {
      Some(oid) => buf.extend(oid.0.to_be_bytes()),
      None => return Err("Unable to implement encode for custom type".into()), // buf.patch_type_by_name(type_info.display_name()),
    }

    let len_start = buf.len();
    buf.extend(0_i32.to_be_bytes()); // len
    buf.extend(1_i32.to_be_bytes()); // lower bound

    let mut encode = |value: T| -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
      // reserve space to write the prefixed length of the value
      let offset = buf.len();
      buf.extend([0; I32_SIZE]);

      // encode the value into our buffer
      let len = if let sqlx::encode::IsNull::No = value.encode(buf)? {
        (buf.len() - offset - I32_SIZE) as i32
      } else {
        // Write a -1 to indicate NULL
        // NOTE: It is illegal for [encode] to write any data
        debug_assert_eq!(buf.len(), offset + I32_SIZE);
        -1_i32
      };

      // write the len to the beginning of the value
      buf[offset..(offset + I32_SIZE)].copy_from_slice(&len.to_be_bytes());
      Ok(())
    };

    let mut count = 0_i32;

    for value in iter {
      encode(value)?;
      match count.checked_add(1) {
        Some(i) => count = i,
        None => {
          return Err(
            format!(
              "Overflowed max array size while encoding {type_info} ({})",
              std::any::type_name::<T>()
            )
            .into(),
          );
        }
      }
    }

    // set the length now that we know what it is.
    buf[len_start..(len_start + I32_SIZE)].copy_from_slice(&count.to_be_bytes());

    Ok(sqlx::encode::IsNull::No)
  }
}

impl<'q, T, I> sqlx::Encode<'q, sqlx::Postgres> for PgBindIter<I>
where
  T: sqlx::Type<sqlx::Postgres> + sqlx::Encode<'q, sqlx::Postgres> + Send + 'q,
  I: Iterator<Item = T> + Clone + Send,
{
  fn encode_by_ref(
    &self,
    buf: &mut sqlx::postgres::PgArgumentBuffer,
  ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
    Self::encode_owned(self.0.clone(), buf)
  }
  fn encode(
    self,
    buf: &mut <sqlx::Postgres as sqlx::database::Database>::ArgumentBuffer<'q>,
  ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>>
  where
    Self: Sized,
  {
    Self::encode_owned(self.0, buf)
  }
}

impl<T, I> sqlx::Type<sqlx::Postgres> for PgBindIter<I>
where
  T: sqlx::Type<sqlx::Postgres> + sqlx::postgres::PgHasArrayType,
  I: Iterator<Item = T>,
{
  fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
    <&[T] as sqlx::Type<sqlx::Postgres>>::type_info()
  }
  fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
    <&[T] as sqlx::Type<sqlx::Postgres>>::compatible(ty)
  }
}