qraft-core 0.1.2

Core type system, query model, decoding, and SQL lowering primitives for qraft.
Documentation
//! Raw SQL fragments and full statements with bound parameters.

use std::{borrow::Cow, marker::PhantomData};

use crate::{
    TypeCast, TypeMeta,
    alias::Aliased,
    lower::LowerCtx,
    param::{Param, ParamValue},
    query::{
        LowerFilter, LowerFromItem, LowerGroupBy, LowerHaving, LowerOrderBy, LowerProject,
        TypedCompiled,
    },
};

/// A raw SQL fragment or whole statement with bound parameters.
#[derive(Debug, Clone)]
pub struct Raw {
    sql: Cow<'static, str>,
    params: Vec<Param>,
    data: Vec<u8>,
}

/// A raw query with an attached row type.
#[derive(Debug, Clone)]
pub struct RawQuery<T> {
    raw: Raw,
    marker: PhantomData<T>,
}

/// A raw scalar query with an attached SQL type.
#[derive(Debug, Clone)]
pub struct RawScalar<T: TypeMeta> {
    raw: Raw,
    marker: PhantomData<T>,
}

/// Starts a raw SQL value using `?` placeholders.
///
/// # Examples
///
/// ```rust
/// use qraft_core::raw;
///
/// let sql = raw("select * from users where id = ?")
///     .bind(42_i64)
///     .to_sql();
///
/// assert_eq!(sql, "select * from users where id = ?");
/// ```
pub fn raw(sql: impl Into<Cow<'static, str>>) -> Raw {
    Raw::new(sql)
}

impl Raw {
    /// Creates a new raw fragment or statement.
    pub fn new(sql: impl Into<Cow<'static, str>>) -> Self {
        Self {
            sql: sql.into(),
            params: Vec::new(),
            data: Vec::new(),
        }
    }

    /// Appends a bound parameter to the raw SQL value.
    pub fn bind<'a>(mut self, value: impl Into<ParamValue<'a>>) -> Self {
        self.push_bind(value);
        self
    }

    /// Appends a parameter in place and returns the same raw value.
    pub fn push_bind<'a>(&mut self, value: impl Into<ParamValue<'a>>) -> &mut Self {
        self.params.push(value.into().into_param(&mut self.data));
        self
    }

    /// Interprets the raw SQL as a row-producing query.
    pub fn typed<T>(self) -> RawQuery<T> {
        RawQuery {
            raw: self,
            marker: PhantomData,
        }
    }

    /// Interprets the raw SQL as a scalar query.
    pub fn scalar<T: TypeMeta>(self) -> RawScalar<T> {
        RawScalar {
            raw: self,
            marker: PhantomData,
        }
    }

    pub fn to_sql(&self) -> String {
        self.sql.to_string()
    }

    /// Compiles the raw SQL into the same representation used by builder queries.
    pub fn into_compiled(self) -> TypedCompiled<()> {
        TypedCompiled {
            sql: self.sql.into_owned(),
            params: self.params,
            data: self.data,
            marker: PhantomData,
        }
    }

    fn lower_fragment(self, ctx: &mut LowerCtx) -> usize {
        ctx.lower_raw(self.sql.as_ref(), self.data, self.params)
    }
}

impl<T> RawQuery<T> {
    pub fn to_sql(&self) -> String {
        self.raw.to_sql()
    }

    /// Compiles the raw query into a typed executable payload.
    pub fn into_compiled(self) -> TypedCompiled<T> {
        TypedCompiled {
            sql: self.raw.sql.into_owned(),
            params: self.raw.params,
            data: self.raw.data,
            marker: PhantomData,
        }
    }
}

impl<T: TypeMeta + TypeCast> RawScalar<T> {
    pub fn to_sql(&self) -> String {
        self.raw.to_sql()
    }

    /// Compiles the raw scalar query into a typed executable payload.
    pub fn into_compiled(self) -> TypedCompiled<<T as TypeCast>::From> {
        TypedCompiled {
            sql: self.raw.sql.into_owned(),
            params: self.raw.params,
            data: self.raw.data,
            marker: PhantomData,
        }
    }
}

impl LowerProject for Raw {
    fn lower_project(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

impl LowerProject for Aliased<Raw> {
    fn lower_project(self, ctx: &mut LowerCtx) {
        let inner = self.inner.lower_fragment(ctx);
        let _ = ctx.lower_alias(self.alias, inner);
    }
}

impl LowerFromItem for Raw {
    fn lower_from_item(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

impl LowerFilter for Raw {
    fn lower_filter(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

impl LowerHaving for Raw {
    fn lower_having(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

impl LowerGroupBy for Raw {
    fn lower_group_by(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

impl LowerOrderBy for Raw {
    fn lower_order_by(self, ctx: &mut LowerCtx) {
        let _ = self.lower_fragment(ctx);
    }
}

#[cfg(test)]
mod tests {
    use super::raw;
    use crate::{Postgres, Sqlite, alias::Alias, query::select};

    #[test]
    fn raw_keeps_question_mark_for_postgres() {
        let sql = raw("select * from users where id = ? and name = ?")
            .bind(1_i64)
            .bind("lea")
            .to_sql();

        assert_eq!(sql, "select * from users where id = ? and name = ?");
    }

    #[test]
    fn raw_keeps_question_mark_for_sqlite() {
        let sql = raw("select * from users where id = ? and name = ?")
            .bind(1_i64)
            .bind("lea")
            .to_sql();

        assert_eq!(sql, "select * from users where id = ? and name = ?");
    }

    #[test]
    fn raw_keeps_sql_text_unchanged() {
        let sql = raw(
            "select '?' as literal, ?? as escaped, body from notes where id = ? -- ?\n/* ? */ and tag = $tag$?$tag$ and slug = ?",
        )
        .bind(1_i64)
        .bind("rust")
        .to_sql();

        assert_eq!(
            sql,
            "select '?' as literal, ?? as escaped, body from notes where id = ? -- ?\n/* ? */ and tag = $tag$?$tag$ and slug = ?"
        );
    }

    #[test]
    fn raw_fragment_works_in_select_from_and_filter() {
        let mut query = select(raw("count(*)").alias("count"))
            .from(raw("users u"))
            .filter(raw("lower(u.name) = ?").bind("lea"));

        let sql = query.to_sql::<Postgres>();
        assert_eq!(
            sql,
            r#"select count(*) as "count" from users u where lower(u.name) = ?"#
        );

        let debug = query.to_debug_sql::<Sqlite>();
        assert_eq!(
            debug,
            r#"select count(*) as "count" from users u where lower(u.name) = ?; params=["lea"]"#
        );
    }
}