use std::{
any::{Any, TypeId},
marker::PhantomData,
};
use quex::FromRow;
use crate::{
AttachBelongsToMany, AttachInput, AttachMorphToMany, BigInt, DraftField, Draftable, Insert,
Insertable, IntoCtes, ModelKey, MorphName, Qrafting, Query, QueryOf, RelationForeignKeyRef,
RelationMorphType, SyncBelongsToMany, SyncMorphToMany, SyncWithoutDetachingBelongsToMany,
SyncWithoutDetachingMorphToMany, ToggleBelongsToMany, ToggleMorphToMany, VisitParam,
alias::{Alias, IntoColumn},
expression::{
self, Between, BetweenExt, EqExt, Expression, IsNull, IsPredicate, Like, LikeExt, Postfix,
op::{Eq, Neq},
},
lower::LowerCtx,
query::{self, LowerOrderBy, Order},
ty::{Comparable, Likeable, Orderable, Required, TypeMeta},
};
mod builders;
mod collection_impls;
mod fields;
mod morph_to_impls;
mod traits;
pub use builders::*;
pub use fields::*;
pub use traits::*;
#[cfg(test)]
mod tests {
#![allow(dead_code)]
use super::*;
use crate::{DraftField, Nullable, SaveDraft, Sqlite, Text};
#[derive(Debug)]
struct Team {
id: i64,
slug: String,
}
#[derive(Debug)]
struct User {
id: i64,
team_id: i64,
nullable_team_id: Option<i64>,
team_slug: String,
username: String,
created_at: i64,
}
#[derive(Debug)]
struct Post {
id: i64,
title: String,
}
#[derive(Debug)]
struct Video {
id: i64,
title: String,
}
#[derive(Debug)]
struct Comment {
id: i64,
commentable_id: i64,
commentable_type: String,
body: String,
}
#[derive(Debug)]
struct Tag {
id: i64,
name: String,
}
#[derive(Debug)]
struct PostTag {
post_id: i64,
tag_id: i64,
}
#[derive(Debug)]
struct TaggableTag {
taggable_id: i64,
taggable_type: String,
tag_id: i64,
}
macro_rules! impl_from_row_stub {
($($ty:ty),+ $(,)?) => {
$(
impl FromRow for $ty {
fn from_row(_row: &crate::quex::Row) -> crate::quex::Result<Self> {
unreachable!("relation unit tests do not decode rows")
}
}
)+
};
}
impl_from_row_stub!(Team, User, Post, Video, Comment, Tag, PostTag, TaggableTag);
macro_rules! impl_qrafting {
($ty:ty, $table:literal, $field_count:expr) => {
impl Qrafting for $ty {
type Schema = ();
type QueryPolicy = crate::DefaultQueryPolicy<Self>;
const FIELD_COUNT: usize = $field_count;
const TABLE: &'static str = $table;
}
};
}
impl_qrafting!(Team, "teams", 2);
impl_qrafting!(User, "users", 6);
impl_qrafting!(Post, "posts", 2);
impl_qrafting!(Video, "videos", 2);
impl_qrafting!(Comment, "comments", 4);
impl_qrafting!(Tag, "tags", 2);
impl_qrafting!(PostTag, "post_tag", 2);
impl_qrafting!(TaggableTag, "taggable_tag", 3);
impl ModelKey for Team {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl ModelKey for User {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl ModelKey for Post {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl ModelKey for Video {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl ModelKey for Comment {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl ModelKey for Tag {
type Key = i64;
fn __qraft_key(&self) -> &Self::Key {
&self.id
}
fn __qraft_key_column() -> &'static str {
"id"
}
}
impl MorphName for Post {
fn morph_name() -> &'static str {
"post"
}
}
impl MorphName for Video {
fn morph_name() -> &'static str {
"video"
}
}
struct DummyDraft<'a, Model: Qrafting>(&'a mut Model);
impl<Model: Qrafting> SaveDraft for DummyDraft<'_, Model> {
type Model = Model;
fn is_dirty(&self) -> bool {
true
}
fn save_update(&self) -> Option<crate::builder::Update<Self::Model>> {
None
}
}
impl Draftable for User {
type Draft<'a> = DummyDraft<'a, Self>;
fn draft(&mut self) -> Self::Draft<'_> {
DummyDraft(self)
}
fn draft_mark(&mut self, _field: DraftField<Self>) -> Self::Draft<'_> {
DummyDraft(self)
}
fn draft_mark_many(&mut self, _fields: &[DraftField<Self>]) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_with(&mut self, _field: usize) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_for_field(&mut self, _field: &'static str) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_for_fields(&mut self, _fields: &[&'static str]) -> Self::Draft<'_> {
DummyDraft(self)
}
}
impl Draftable for Comment {
type Draft<'a> = DummyDraft<'a, Self>;
fn draft(&mut self) -> Self::Draft<'_> {
DummyDraft(self)
}
fn draft_mark(&mut self, _field: DraftField<Self>) -> Self::Draft<'_> {
DummyDraft(self)
}
fn draft_mark_many(&mut self, _fields: &[DraftField<Self>]) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_with(&mut self, _field: usize) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_for_field(&mut self, _field: &'static str) -> Self::Draft<'_> {
DummyDraft(self)
}
fn __qraft_draft_for_fields(&mut self, _fields: &[&'static str]) -> Self::Draft<'_> {
DummyDraft(self)
}
}
fn team_id() -> ModelField<Team, i64, BigInt> {
ModelField::new(expression::Column::new("id"), |m| &m.id)
}
fn team_slug() -> ModelField<Team, String, Text> {
ModelField::new(expression::Column::new("slug"), |m| &m.slug)
}
fn user_id() -> ModelField<User, i64, BigInt> {
ModelField::new(expression::Column::new("id"), |m| &m.id)
}
fn user_team_id() -> PersistedField<User, i64, BigInt> {
PersistedField::new(
expression::Column::new("team_id"),
|m| &m.team_id,
|m, v| m.team_id = v,
DraftField::new("team_id", 1),
)
}
fn user_nullable_team_id() -> PersistedField<User, Option<i64>, Nullable<BigInt>, NullableField>
{
PersistedField::new(
expression::Column::new("nullable_team_id"),
|m| &m.nullable_team_id,
|m, v| m.nullable_team_id = v,
DraftField::new("nullable_team_id", 2),
)
}
fn user_team_slug() -> PersistedField<User, String, Text> {
PersistedField::new(
expression::Column::new("team_slug"),
|m| &m.team_slug,
|m, v| m.team_slug = v,
DraftField::new("team_slug", 3),
)
}
fn user_username() -> PersistedField<User, String, Text> {
PersistedField::new(
expression::Column::new("username"),
|m| &m.username,
|m, v| m.username = v,
DraftField::new("username", 4),
)
}
fn user_created_at() -> ModelField<User, i64, BigInt> {
ModelField::new(expression::Column::new("created_at"), |m| &m.created_at)
}
fn post_id() -> ModelField<Post, i64, BigInt> {
ModelField::new(expression::Column::new("id"), |m| &m.id)
}
fn post_title() -> PersistedField<Post, String, Text> {
PersistedField::new(
expression::Column::new("title"),
|m| &m.title,
|m, v| m.title = v,
DraftField::new("title", 1),
)
}
fn comment_morph_id() -> PersistedField<Comment, i64, BigInt> {
PersistedField::new(
expression::Column::new("commentable_id"),
|m| &m.commentable_id,
|m, v| m.commentable_id = v,
DraftField::new("commentable_id", 1),
)
}
fn comment_morph_type() -> PersistedField<Comment, String, Text> {
PersistedField::new(
expression::Column::new("commentable_type"),
|m| &m.commentable_type,
|m, v| m.commentable_type = v,
DraftField::new("commentable_type", 2),
)
}
fn comment_body() -> PersistedField<Comment, String, Text> {
PersistedField::new(
expression::Column::new("body"),
|m| &m.body,
|m, v| m.body = v,
DraftField::new("body", 3),
)
}
fn tag_id() -> ModelField<Tag, i64, BigInt> {
ModelField::new(expression::Column::new("id"), |m| &m.id)
}
fn post_tag_post_id() -> expression::Column<PostTag, BigInt> {
expression::Column::new("post_id")
}
fn post_tag_tag_id() -> expression::Column<PostTag, BigInt> {
expression::Column::new("tag_id")
}
fn taggable_tag_parent_id() -> expression::Column<TaggableTag, BigInt> {
expression::Column::new("taggable_id")
}
fn taggable_tag_parent_type() -> expression::Column<TaggableTag, Text> {
expression::Column::new("taggable_type")
}
fn taggable_tag_tag_id() -> expression::Column<TaggableTag, BigInt> {
expression::Column::new("tag_id")
}
#[test]
fn belongs_to_and_nullable_belongs_to_generate_expected_sql() {
let user = User {
id: 10,
team_id: 7,
nullable_team_id: Some(8),
team_slug: "core".into(),
username: "lea".into(),
created_at: 100,
};
let belongs_to_sql = belongs_to(&user)
.foreign_key(user_team_id())
.owner_model_key::<Team>()
.to_debug_sql::<Sqlite>();
assert_eq!(
belongs_to_sql,
r#"select * from "teams" where "teams"."id" = ?; params=[7]"#
);
let nullable_sql = belongs_to(&user)
.foreign_key(user_nullable_team_id())
.owner_model_key::<Team>()
.to_debug_sql::<Sqlite>();
assert_eq!(
nullable_sql,
Some(r#"select * from "teams" where "teams"."id" = ?; params=[8]"#.into())
);
let user = User {
nullable_team_id: None,
..user
};
assert_eq!(
belongs_to(&user)
.foreign_key(user_nullable_team_id())
.owner_model_key::<Team>()
.to_debug_sql::<Sqlite>(),
None
);
}
#[test]
fn has_many_create_injects_foreign_key_and_skips_explicit_field() {
let team = Team {
id: 42,
slug: "infra".into(),
};
let sql = has_many(&team)
.foreign_key(user_team_id())
.create((user_team_id().eq(999_i64), user_username().eq("lea")))
.to_sql::<Sqlite>();
assert_eq!(
sql,
r#"insert into "users" ("team_id", "username") values (?, ?) returning *"#
);
}
#[test]
fn has_many_latest_by_uses_sort_and_primary_key_tiebreaker() {
let team = Team {
id: 42,
slug: "infra".into(),
};
let sql = has_many(&team)
.foreign_key(user_team_id())
.latest_by(user_created_at())
.to_debug_sql::<Sqlite>();
assert_eq!(
sql,
r#"select * from "users" where "users"."team_id" = ? order by "users"."created_at" desc, "users"."id" desc limit ?; params=[42, 1]"#
);
}
#[test]
fn morph_many_create_injects_foreign_key_and_type() {
let post = Post {
id: 5,
title: "hello".into(),
};
let sql = morph_many(&post)
.foreign_key(comment_morph_id())
.morph_type(comment_morph_type())
.morph_name(Post::morph_name())
.create((
comment_morph_id().eq(999_i64),
comment_morph_type().eq("wrong"),
comment_body().eq("first"),
))
.to_sql::<Sqlite>();
assert_eq!(
sql,
r#"insert into "comments" ("commentable_id", "commentable_type", "body") values (?, ?, ?) returning *"#
);
}
#[test]
fn morph_to_as_morph_only_matches_registered_type() {
let comment = Comment {
id: 1,
commentable_id: 5,
commentable_type: "post".into(),
body: "hello".into(),
};
let post_sql = morph_to(&comment)
.morph_id(comment_morph_id())
.morph_type(comment_morph_type())
.morph::<Post>()
.morph::<Video>()
.as_morph::<Post>()
.to_debug_sql::<Sqlite>();
assert_eq!(
post_sql,
Some(r#"select * from "posts" where "posts"."id" = ?; params=[5]"#.into())
);
let video_sql = morph_to(&comment)
.morph_id(comment_morph_id())
.morph_type(comment_morph_type())
.morph::<Post>()
.morph::<Video>()
.as_morph::<Video>()
.to_debug_sql::<Sqlite>();
assert_eq!(video_sql, None);
}
#[test]
fn belongs_to_many_query_and_detach_sql_are_stable() {
let post = Post {
id: 9,
title: "builder".into(),
};
let tag = Tag {
id: 4,
name: "rust".into(),
};
let query_sql = <_ as BelongsToMany<Tag>>::to_debug_sql::<Sqlite>(
belongs_to_many(&post)
.pivot::<PostTag>()
.parent_key(post_tag_post_id())
.related_key(post_tag_tag_id()),
);
assert_eq!(
query_sql,
r#"select * from "tags" inner join "post_tag" as "__qraft_belongs_to_many_pivot" on "tags"."id" = "__qraft_belongs_to_many_pivot"."tag_id" where "__qraft_belongs_to_many_pivot"."post_id" = ?; params=[9]"#
);
let detach_sql = belongs_to_many(&post)
.pivot::<PostTag>()
.parent_key(post_tag_post_id())
.related_key(post_tag_tag_id())
.detach(&tag)
.to_debug_sql::<Sqlite>();
assert_eq!(
detach_sql,
r#"delete from "post_tag" where "post_tag"."post_id" = ? and "post_tag"."tag_id" = ?; params=[9, 4]"#
);
}
#[test]
fn morph_to_many_query_and_detach_sql_are_stable() {
let post = Post {
id: 9,
title: "builder".into(),
};
let tag = Tag {
id: 4,
name: "rust".into(),
};
let query_sql = <_ as MorphToMany<Tag>>::to_debug_sql::<Sqlite>(
morph_to_many(&post)
.pivot::<TaggableTag>()
.parent_key(taggable_tag_parent_id())
.morph_type(taggable_tag_parent_type())
.related_key(taggable_tag_tag_id()),
);
assert_eq!(
query_sql,
r#"select * from "tags" inner join "taggable_tag" as "__qraft_morph_to_many_pivot" on "tags"."id" = "__qraft_morph_to_many_pivot"."tag_id" where "__qraft_morph_to_many_pivot"."taggable_id" = ? and "__qraft_morph_to_many_pivot"."taggable_type" = ?; params=[9, "post"]"#
);
let detach_sql = morph_to_many(&post)
.pivot::<TaggableTag>()
.parent_key(taggable_tag_parent_id())
.morph_type(taggable_tag_parent_type())
.related_key(taggable_tag_tag_id())
.detach(&tag)
.to_debug_sql::<Sqlite>();
assert_eq!(
detach_sql,
r#"delete from "taggable_tag" where "taggable_tag"."taggable_id" = ? and "taggable_tag"."taggable_type" = ? and "taggable_tag"."tag_id" = ?; params=[9, "post", 4]"#
);
}
#[test]
fn has_many_and_belongs_to_many_local_key_overrides_are_applied() {
let team = Team {
id: 1,
slug: "infra".into(),
};
let tag = Tag {
id: 3,
name: "backend".into(),
};
let has_many_sql = has_many(&team)
.foreign_key(user_team_slug())
.local_key(team_slug())
.to_debug_sql::<Sqlite>();
assert_eq!(
has_many_sql,
r#"select * from "users" where "users"."team_slug" = ?; params=["infra"]"#
);
let many_to_many_sql = belongs_to_many(&team)
.pivot::<PostTag>()
.parent_key(post_tag_post_id())
.related_key(post_tag_tag_id())
.local_key(team_id())
.detach(&tag)
.to_debug_sql::<Sqlite>();
assert_eq!(
many_to_many_sql,
r#"delete from "post_tag" where "post_tag"."post_id" = ? and "post_tag"."tag_id" = ?; params=[1, 3]"#
);
}
#[test]
fn morph_to_respects_target_morph_name_overrides() {
let comment = Comment {
id: 1,
commentable_id: 5,
commentable_type: "legacy_post".into(),
body: "hello".into(),
};
let sql = morph_to(&comment)
.morph_id(comment_morph_id())
.morph_type(comment_morph_type())
.morph::<Post>()
.morph_name("legacy_post")
.as_morph::<Post>()
.to_debug_sql::<Sqlite>();
assert_eq!(
sql,
Some(r#"select * from "posts" where "posts"."id" = ?; params=[5]"#.into())
);
}
#[test]
fn model_field_and_tag_helpers_are_well_typed() {
let _ = team_id();
let _ = user_id();
let _ = post_id();
let _ = tag_id();
let _ = post_title();
}
}