drizzle-core 0.1.7

A type-safe SQL query builder for Rust
Documentation
use crate::prelude::*;
use crate::{ColumnRef, SQL, SQLParam, SQLSchema, SQLSchemaType, TableRef, ToSQL};
use core::marker::PhantomData;
use core::ops::Deref;

/// Marker: no fields have been set on this update model.
pub struct Empty;
/// Marker: at least one field has been set on this update model.
pub struct NonEmpty;

/// Type-level name marker used for strongly-typed aliases/CTEs.
pub trait Tag {
    const NAME: &'static str;
}

/// Define a [`Tag`] in one line.
///
/// ```
/// # use drizzle_core::tag;
/// tag!(UsersAlias, "u");
///
/// let u = UsersAlias; // zero-sized, used as a type parameter
/// assert_eq!(<UsersAlias as drizzle_core::Tag>::NAME, "u");
/// ```
///
/// Visibility is supported:
///
/// ```
/// # use drizzle_core::tag;
/// tag!(pub MyTag, "my_tag");
/// ```
#[macro_export]
macro_rules! tag {
    ($vis:vis $name:ident, $sql_name:expr) => {
        $vis struct $name;
        impl $crate::Tag for $name {
            const NAME: &'static str = $sql_name;
        }
    };
}

/// Generic typed wrapper that binds a value to a compile-time [`Tag`].
#[derive(Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Tagged<T, Name: Tag> {
    inner: T,
    _tag: PhantomData<fn() -> Name>,
}

impl<T: Copy, Name: Tag> Copy for Tagged<T, Name> {}

impl<T: Copy, Name: Tag> Clone for Tagged<T, Name> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<T, Name: Tag> Tagged<T, Name> {
    pub const fn new(inner: T) -> Self {
        Self {
            inner,
            _tag: PhantomData,
        }
    }

    pub fn into_inner(self) -> T {
        self.inner
    }
}

impl<T, Name: Tag> Deref for Tagged<T, Name> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a SQL model (Select, Insert, or Update)",
    label = "this type cannot be used as a query model"
)]
pub trait SQLModel<'a, V: SQLParam>: ToSQL<'a, V> {
    /// Columns referenced by this model.
    /// Static models can return a borrowed slice; dynamic models can allocate.
    fn columns(&self) -> Cow<'static, [ColumnRef]>;
    fn values(&self) -> SQL<'a, V>;
}

/// Trait for models that support partial selection of fields
#[diagnostic::on_unimplemented(
    message = "`{Self}` does not support partial field selection",
    label = "this table's Select model does not implement SQLPartial"
)]
pub trait SQLPartial<'a, Value: SQLParam> {
    /// The type representing a partial model where all fields are optional
    /// for selective querying
    type Partial: SQLModel<'a, Value> + Default + 'a;

    #[must_use]
    fn partial() -> Self::Partial {
        Default::default()
    }
}

#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a SQL table for this dialect",
    label = "ensure this type was derived with #[SQLiteTable] or #[PostgresTable]"
)]
pub trait SQLTable<'a, Type: SQLSchemaType, Value: SQLParam + 'a>:
    SQLSchema<'a, Type, Value> + SQLTableInfo + Default + Clone + Copy
{
    type Select: SQLModel<'a, Value> + SQLPartial<'a, Value> + Default + 'a;
    type ForeignKeys;
    type PrimaryKey;
    type Constraints;

    /// The type representing a model for INSERT operations on this table.
    /// Uses `PhantomData` with tuple markers to track which fields are set
    type Insert<T>: SQLModel<'a, Value> + Default;

    /// The type representing a model for UPDATE operations on this table.
    /// This would be generated by the table macro.
    type Update: SQLModel<'a, Value> + 'a;

    /// The aliased version of this table for self-joins and CTEs.
    type Aliased<Name: Tag + 'static>: SQLTable<'a, Type, Value>;

    /// Creates a strongly-typed alias using the compile-time [`Tag`] name.
    fn alias<Name: Tag + 'static>() -> Self::Aliased<Name>;
}

/// Compile-time table metadata.
///
/// Provides table name, schema, columns, keys, and constraints as associated
/// constants. Implementing this trait automatically provides [`SQLTableInfo`]
/// via a blanket implementation.
pub trait DrizzleTable: Send + Sync + 'static {
    /// Unqualified table name.
    const NAME: &'static str;

    /// Fully-qualified table name (e.g. `schema.table`).
    const QUALIFIED_NAME: &'static str;

    /// Schema namespace, if any.
    const SCHEMA: Option<&'static str> = None;

    /// Names of tables this table depends on via foreign keys.
    const DEPENDENCY_NAMES: &'static [&'static str] = &[];

    /// Full table metadata as a const Copy struct.
    const TABLE_REF: TableRef;
}

/// Blanket: any `DrizzleTable` automatically satisfies `SQLTableInfo`.
impl<T: DrizzleTable> SQLTableInfo for T {
    fn name(&self) -> &'static str {
        T::NAME
    }

    fn schema(&self) -> Option<&'static str> {
        T::SCHEMA
    }

    fn qualified_name(&self) -> Cow<'static, str> {
        Cow::Borrowed(T::QUALIFIED_NAME)
    }
}

impl<'a, Type, Value, T> SQLTable<'a, Type, Value> for &T
where
    Type: SQLSchemaType,
    Value: SQLParam + 'a,
    T: SQLTable<'a, Type, Value>,
    for<'r> &'r T: SQLSchema<'a, Type, Value> + SQLTableInfo + Default + Clone,
{
    type Select = T::Select;
    type ForeignKeys = T::ForeignKeys;
    type PrimaryKey = T::PrimaryKey;
    type Constraints = T::Constraints;
    type Insert<I> = T::Insert<I>;
    type Update = T::Update;
    type Aliased<Name: Tag + 'static> = T::Aliased<Name>;

    fn alias<Name: Tag + 'static>() -> Self::Aliased<Name> {
        T::alias::<Name>()
    }
}

#[diagnostic::on_unimplemented(
    message = "`{Self}` does not implement SQLTableInfo",
    label = "ensure this type was derived with #[SQLiteTable] or #[PostgresTable]"
)]
pub trait SQLTableInfo: Send + Sync {
    /// Unqualified table name.
    fn name(&self) -> &'static str;

    /// Schema namespace for this table, if any.
    fn schema(&self) -> Option<&'static str> {
        None
    }

    /// Fully-qualified table name.
    fn qualified_name(&self) -> Cow<'static, str> {
        self.schema().map_or_else(
            || Cow::Borrowed(self.name()),
            |schema| Cow::Owned(format!("{schema}.{}", self.name())),
        )
    }
}

impl core::fmt::Debug for dyn SQLTableInfo {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("SQLTableInfo")
            .field("name", &self.name())
            .field("schema", &self.schema())
            .finish()
    }
}