use convert_case::{Case, Casing};
use prax_schema::ast::{Field, FieldType};
use prax_schema::{Model, ScalarType};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote};
use super::LowerCtx;
use crate::macros::dsl::ast::{DslBlock, DslField, DslValue};
#[derive(Debug)]
pub struct NestedRelationOp {
pub op_expr: TokenStream,
}
pub fn lower_create_relation(
relation_field: &Field,
value: &DslValue,
parent_span: Span,
ctx: &LowerCtx<'_>,
) -> syn::Result<Vec<NestedRelationOp>> {
let DslValue::Block(block) = value else {
return Err(syn::Error::new(
parent_span,
format!(
"relation `{}` on `data:` expects a `{{ ... }}` block of nested-write operators (create:, connect:)",
relation_field.name(),
),
));
};
let target_name = match &relation_field.field_type {
FieldType::Model(n) => n.as_str(),
_ => {
return Err(syn::Error::new(
parent_span,
format!("field `{}` is not a relation field", relation_field.name(),),
));
}
};
let target_model = ctx.schema.get_model(target_name).ok_or_else(|| {
syn::Error::new(
parent_span,
format!(
"relation `{}` references unknown target model `{}`",
relation_field.name(),
target_name
),
)
})?;
let foreign_key = resolve_foreign_key(relation_field, ctx.model, target_model)?;
let target_module_ident = format_ident!("{}", target_name.to_case(Case::Snake));
let target_model_ident = format_ident!("{}", target_name);
let target_input_ident = format_ident!("{}CreateInput", target_name);
let target_table = target_model.table_name().to_string();
let target_pk_column = target_model
.id_fields()
.into_iter()
.next()
.map(column_name_of)
.ok_or_else(|| {
syn::Error::new(
parent_span,
format!(
"target model `{target_name}` of relation `{}` has no `@id` column — \
nested connect/create requires a single-column primary key in phase 5b",
relation_field.name(),
),
)
})?;
let relation_name_str = relation_field.name().to_string();
let mut ops: Vec<NestedRelationOp> = Vec::new();
for entry in &block.fields {
let DslField::Pair { key, value, .. } = entry else {
return Err(syn::Error::new(
Span::call_site(),
format!(
"nested relation block on `{}` does not support spread or conditional fields in phase 5b",
relation_field.name(),
),
));
};
let op_key = key.to_string();
match op_key.as_str() {
"create" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
let target_ctx = ctx.for_model(target_model);
let mut child_payloads: Vec<TokenStream> = Vec::with_capacity(children.len());
for child_block in children {
let input_expr =
super::data_input::lower_create_data(child_block, &target_ctx)?;
child_payloads.push(quote! {
<#target_module_ident::#target_input_ident
as ::prax_query::inputs::CreateInput>::into_ir(#input_expr)
});
}
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Create {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
payload: ::std::vec![ #( #child_payloads ),* ],
}
};
ops.push(NestedRelationOp { op_expr });
let _ = &target_model_ident;
}
"connect" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
for child_block in children {
let pk_expr = lower_connect_pk(child_block, target_model, &target_pk_column)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Connect {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
target_pk: #target_pk_column,
pk: ::core::convert::Into::<
::prax_query::filter::FilterValue
>::into(#pk_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
"disconnect" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
for child_block in children {
let pk_expr = lower_connect_pk(child_block, target_model, &target_pk_column)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Disconnect {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
target_pk: #target_pk_column,
pk: ::core::convert::Into::<
::prax_query::filter::FilterValue
>::into(#pk_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
"delete" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
for child_block in children {
let pk_expr = lower_connect_pk(child_block, target_model, &target_pk_column)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Delete {
relation: #relation_name_str,
target_table: #target_table,
target_pk: #target_pk_column,
pk: ::core::convert::Into::<
::prax_query::filter::FilterValue
>::into(#pk_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
"delete_many" => {
let DslValue::Block(filter_block) = value else {
return Err(syn::Error::new(
key.span(),
"`delete_many:` inside a relation expects a filter block `{ ... }`",
));
};
let target_ctx = ctx.for_model(target_model);
let where_expr =
super::where_input::lower_where_input_only(filter_block, &target_ctx)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::DeleteMany {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
filter: <_ as ::prax_query::inputs::WhereInput>::into_ir(#where_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
"update" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
let target_ctx = ctx.for_model(target_model);
for child_block in children {
let (pk_expr, data_expr) = extract_update_entry(
child_block,
target_model,
&target_pk_column,
&target_ctx,
)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Update {
relation: #relation_name_str,
target_table: #target_table,
target_pk: #target_pk_column,
pk: ::core::convert::Into::<
::prax_query::filter::FilterValue
>::into(#pk_expr),
payload: <_ as ::prax_query::inputs::UpdateInput>::into_ir(#data_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
"update_many" => {
let DslValue::Block(entry_block) = value else {
return Err(syn::Error::new(
key.span(),
"`update_many:` inside a relation expects \
`{ where: { ... }, data: { ... } }`",
));
};
let target_ctx = ctx.for_model(target_model);
let (where_expr, data_expr) =
extract_update_many_entry(entry_block, &target_ctx, key.span())?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::UpdateMany {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
filter: <_ as ::prax_query::inputs::WhereInput>::into_ir(#where_expr),
payload: <_ as ::prax_query::inputs::UpdateInput>::into_ir(#data_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
"upsert" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
let target_ctx = ctx.for_model(target_model);
for child_block in children {
let (pk_expr, create_expr, update_expr) = extract_upsert_entry(
child_block,
target_model,
&target_pk_column,
&target_ctx,
)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Upsert {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
target_pk: #target_pk_column,
pk: ::core::convert::Into::<
::prax_query::filter::FilterValue
>::into(#pk_expr),
create_payload: <
#target_module_ident::#target_input_ident
as ::prax_query::inputs::CreateInput
>::into_ir(#create_expr),
update_payload: <_ as ::prax_query::inputs::UpdateInput>::into_ir(#update_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
"set" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
let mut pk_exprs: Vec<TokenStream> = Vec::with_capacity(children.len());
for child_block in children {
let pk_expr = lower_connect_pk(child_block, target_model, &target_pk_column)?;
pk_exprs.push(quote! {
::core::convert::Into::<::prax_query::filter::FilterValue>::into(#pk_expr)
});
}
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::Set {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
target_pk: #target_pk_column,
set_pks: ::std::vec![ #( #pk_exprs ),* ],
}
};
ops.push(NestedRelationOp { op_expr });
}
"connect_or_create" => {
let children = expect_list_of_blocks(value, &op_key, key.span())?;
let target_ctx = ctx.for_model(target_model);
for child_block in children {
let (where_expr, create_expr) =
extract_connect_or_create_entry(child_block, &target_ctx)?;
let op_expr = quote! {
::prax_query::nested::NestedWriteOp::ConnectOrCreate {
relation: #relation_name_str,
target_table: #target_table,
foreign_key: #foreign_key,
where_filter: <_ as ::prax_query::inputs::WhereInput>::into_ir(#where_expr),
create_payload: <
#target_module_ident::#target_input_ident
as ::prax_query::inputs::CreateInput
>::into_ir(#create_expr),
}
};
ops.push(NestedRelationOp { op_expr });
}
}
_ => {
let candidates = vec![
"create".to_string(),
"connect".to_string(),
"connect_or_create".to_string(),
"disconnect".to_string(),
"delete".to_string(),
"delete_many".to_string(),
"set".to_string(),
"update".to_string(),
"update_many".to_string(),
"upsert".to_string(),
];
let suggestion = crate::macros::validate::suggest(&op_key, &candidates);
let msg = match suggestion {
Some(s) => format!(
"unknown nested operator `{op_key}` inside `data:` relation block `{}`. \
Did you mean `{s}`? Valid operators: create, connect, connect_or_create, \
disconnect, delete, delete_many, set, update, update_many, upsert.",
relation_field.name(),
),
None => format!(
"unknown nested operator `{op_key}` inside `data:` relation block `{}`. \
Valid operators: create, connect, connect_or_create, disconnect, delete, \
delete_many, set, update, update_many, upsert.",
relation_field.name(),
),
};
return Err(syn::Error::new(key.span(), msg));
}
}
}
Ok(ops)
}
fn expect_list_of_blocks<'a>(
value: &'a DslValue,
op_name: &str,
span: Span,
) -> syn::Result<Vec<&'a DslBlock>> {
match value {
DslValue::List(items) => {
let mut blocks: Vec<&DslBlock> = Vec::with_capacity(items.len());
for item in items {
let DslValue::Block(b) = item else {
return Err(syn::Error::new(
span,
format!(
"`{op_name}:` expects a list of `{{ ... }}` blocks; \
got a non-block list entry"
),
));
};
blocks.push(b);
}
Ok(blocks)
}
DslValue::Block(b) => Ok(vec![b]),
_ => Err(syn::Error::new(
span,
format!(
"`{op_name}:` expects a list of `{{ ... }}` blocks, e.g. \
`{op_name}: [{{ ... }}, {{ ... }}]`"
),
)),
}
}
fn lower_connect_pk(
block: &DslBlock,
target_model: &Model,
target_pk_col: &str,
) -> syn::Result<TokenStream> {
if block.fields.len() != 1 {
return Err(syn::Error::new(
Span::call_site(),
format!(
"phase 5b `connect:` expects exactly one key (`{target_pk_col}`) on target \
model `{}`. Multi-key connect targets are deferred.",
target_model.name()
),
));
}
let DslField::Pair { key, value, .. } = &block.fields[0] else {
return Err(syn::Error::new(
Span::call_site(),
"`connect:` block does not support spread or conditional fields in phase 5b",
));
};
let key_str = key.to_string();
let field = target_model
.get_field(&key_str)
.or_else(|| {
target_model
.fields
.values()
.find(|f| column_name_of(f) == key_str)
})
.ok_or_else(|| {
syn::Error::new(
key.span(),
format!(
"unknown field `{key_str}` on connect target `{}`",
target_model.name()
),
)
})?;
let column = column_name_of(field);
if column != target_pk_col {
return Err(syn::Error::new(
key.span(),
format!(
"phase 5b `connect:` only accepts the primary key column `{target_pk_col}`. \
Got `{column}`. Other `@unique` keys are deferred."
),
));
}
match value {
DslValue::Lit(lit) => Ok(quote! { (#lit) }),
DslValue::Bool(b) => Ok(quote! { #b }),
DslValue::Expr(e) => Ok(quote! { (#e) }),
DslValue::Path(p) => Ok(quote! { #p }),
DslValue::BareIdent(id) => Err(syn::Error::new(
id.span(),
format!(
"bare identifier `{id}` is not a valid PK value for `connect:`. \
Use a literal, path, or `@(expr)` escape."
),
)),
DslValue::Block(_) | DslValue::List(_) => Err(syn::Error::new(
key.span(),
"`connect:` PK value must be a literal, path, or `@(expr)` escape — \
not a block or list",
)),
}
}
fn take_named_field<'a>(
block: &'a DslBlock,
name: &str,
op: &str,
span: Span,
) -> syn::Result<&'a DslValue> {
for f in &block.fields {
if let DslField::Pair { key, value, .. } = f
&& key == name
{
return Ok(value);
}
}
Err(syn::Error::new(
span,
format!("`{op}:` entry is missing required key `{name}:`"),
))
}
fn reject_extra_keys(block: &DslBlock, expected: &[&str], op: &str, span: Span) -> syn::Result<()> {
for f in &block.fields {
match f {
DslField::Pair { key, .. } => {
let key_str = key.to_string();
if !expected.contains(&key_str.as_str()) {
return Err(syn::Error::new(
key.span(),
format!(
"unexpected key `{key_str}` on `{op}:` entry. Valid keys: {}.",
expected.join(", "),
),
));
}
}
DslField::Spread { .. } | DslField::Conditional { .. } => {
return Err(syn::Error::new(
span,
format!("`{op}:` entry does not support spread or conditional fields",),
));
}
}
}
Ok(())
}
fn extract_update_entry(
block: &DslBlock,
target_model: &Model,
target_pk_col: &str,
target_ctx: &LowerCtx<'_>,
) -> syn::Result<(TokenStream, TokenStream)> {
reject_extra_keys(block, &["where", "data"], "update", Span::call_site())?;
let where_value = take_named_field(block, "where", "update", Span::call_site())?;
let data_value = take_named_field(block, "data", "update", Span::call_site())?;
let DslValue::Block(where_block) = where_value else {
return Err(syn::Error::new(
Span::call_site(),
"`update:` entry expects `where: { <pk>: <value> }`",
));
};
let DslValue::Block(data_block) = data_value else {
return Err(syn::Error::new(
Span::call_site(),
"`update:` entry expects `data: { ... }`",
));
};
let pk_expr = lower_connect_pk(where_block, target_model, target_pk_col)?;
let data_expr = super::data_input::lower_update_data(data_block, target_ctx)?;
Ok((pk_expr, data_expr))
}
fn extract_update_many_entry(
block: &DslBlock,
target_ctx: &LowerCtx<'_>,
span: Span,
) -> syn::Result<(TokenStream, TokenStream)> {
reject_extra_keys(block, &["where", "data"], "update_many", span)?;
let where_value = take_named_field(block, "where", "update_many", span)?;
let data_value = take_named_field(block, "data", "update_many", span)?;
let DslValue::Block(where_block) = where_value else {
return Err(syn::Error::new(
span,
"`update_many:` entry expects `where: { ... }`",
));
};
let DslValue::Block(data_block) = data_value else {
return Err(syn::Error::new(
span,
"`update_many:` entry expects `data: { ... }`",
));
};
let where_expr = super::where_input::lower_where_input_only(where_block, target_ctx)?;
let data_expr = super::data_input::lower_update_data(data_block, target_ctx)?;
Ok((where_expr, data_expr))
}
fn extract_upsert_entry(
block: &DslBlock,
target_model: &Model,
target_pk_col: &str,
target_ctx: &LowerCtx<'_>,
) -> syn::Result<(TokenStream, TokenStream, TokenStream)> {
reject_extra_keys(
block,
&["where", "create", "update"],
"upsert",
Span::call_site(),
)?;
let where_value = take_named_field(block, "where", "upsert", Span::call_site())?;
let create_value = take_named_field(block, "create", "upsert", Span::call_site())?;
let update_value = take_named_field(block, "update", "upsert", Span::call_site())?;
let DslValue::Block(where_block) = where_value else {
return Err(syn::Error::new(
Span::call_site(),
"`upsert:` entry expects `where: { <pk>: <value> }`",
));
};
let DslValue::Block(create_block) = create_value else {
return Err(syn::Error::new(
Span::call_site(),
"`upsert:` entry expects `create: { ... }`",
));
};
let DslValue::Block(update_block) = update_value else {
return Err(syn::Error::new(
Span::call_site(),
"`upsert:` entry expects `update: { ... }`",
));
};
let pk_expr = lower_connect_pk(where_block, target_model, target_pk_col)?;
let create_expr = super::data_input::lower_create_data(create_block, target_ctx)?;
let update_expr = super::data_input::lower_update_data(update_block, target_ctx)?;
Ok((pk_expr, create_expr, update_expr))
}
fn extract_connect_or_create_entry(
block: &DslBlock,
target_ctx: &LowerCtx<'_>,
) -> syn::Result<(TokenStream, TokenStream)> {
reject_extra_keys(
block,
&["where", "create"],
"connect_or_create",
Span::call_site(),
)?;
let where_value = take_named_field(block, "where", "connect_or_create", Span::call_site())?;
let create_value = take_named_field(block, "create", "connect_or_create", Span::call_site())?;
let DslValue::Block(where_block) = where_value else {
return Err(syn::Error::new(
Span::call_site(),
"`connect_or_create:` entry expects `where: { ... }`",
));
};
let DslValue::Block(create_block) = create_value else {
return Err(syn::Error::new(
Span::call_site(),
"`connect_or_create:` entry expects `create: { ... }`",
));
};
let where_expr = super::where_input::lower_where_input_only(where_block, target_ctx)?;
let create_expr = super::data_input::lower_create_data(create_block, target_ctx)?;
Ok((where_expr, create_expr))
}
fn resolve_foreign_key(
relation_field: &Field,
parent_model: &Model,
target_model: &Model,
) -> syn::Result<String> {
let attrs = relation_field.extract_attributes();
if let Some(rel) = &attrs.relation
&& let Some(first) = rel.references.first()
{
return Ok(first.to_string());
}
for f in target_model.fields.values() {
let FieldType::Model(target_pointer) = &f.field_type else {
continue;
};
if target_pointer.as_str() != parent_model.name() {
continue;
}
let target_attrs = f.extract_attributes();
if let Some(rel) = target_attrs.relation
&& let Some(first) = rel.fields.first()
{
return Ok(first.to_string());
}
}
Err(syn::Error::new(
Span::call_site(),
format!(
"cannot resolve FK column on target model `{}` for relation `{}` on `{}`. \
Phase 5b requires either `@relation(references: [<col>])` on the inverse side \
or `@relation(fields: [<col>])` on the back-pointer field of `{}`.",
target_model.name(),
relation_field.name(),
parent_model.name(),
target_model.name(),
),
))
}
fn column_name_of(field: &Field) -> String {
let attrs = field.extract_attributes();
attrs.map.unwrap_or_else(|| field.name().to_string())
}
#[allow(dead_code)]
const _SCALAR_USED: Option<ScalarType> = None;
#[cfg(test)]
mod tests {
use super::*;
use prax_schema::parse_schema;
use quote::quote;
const SCHEMA: &str = include_str!("../../../tests/fixtures/schema.prax");
fn parsed_schema() -> prax_schema::Schema {
let mut v = prax_schema::Validator::new();
v.validate(parse_schema(SCHEMA).unwrap()).unwrap()
}
fn parse_block(tokens: TokenStream) -> DslBlock {
syn::parse2::<DslBlock>(tokens).unwrap()
}
fn pretty(ts: TokenStream) -> String {
ts.to_string()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn lowers_nested_create_to_nested_write_op_create() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
create: [
{ title: "First", published: true },
{ title: "Second", published: false },
]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("NestedWriteOp"), "got: {s}");
assert!(s.contains("Create"), "got: {s}");
assert!(s.contains("posts"), "got: {s}");
}
#[test]
fn lowers_nested_connect_to_nested_write_op_connect() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
connect: [{ id: 42 }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("Connect"), "got: {s}");
assert!(s.contains("target_pk"), "got: {s}");
assert!(s.contains("42"), "got: {s}");
}
#[test]
fn unknown_op_inside_relation_block_suggests_create() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
creat: [{ title: "x" }]
})));
let err = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("unknown nested operator"), "got: {msg}");
assert!(msg.contains("create"), "got: {msg}");
}
#[test]
fn lowers_nested_disconnect_to_nested_write_op_disconnect() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
disconnect: [{ id: 42 }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("Disconnect"), "got: {s}");
assert!(s.contains("foreign_key"), "got: {s}");
assert!(s.contains("42"), "got: {s}");
}
#[test]
fn lowers_nested_delete_to_nested_write_op_delete() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
delete: [{ id: 7 }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("Delete"), "got: {s}");
assert!(s.contains("target_pk"), "got: {s}");
assert!(s.contains("7"), "got: {s}");
}
#[test]
fn lowers_nested_delete_many_to_nested_write_op_delete_many() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
delete_many: { published: false }
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("DeleteMany"), "got: {s}");
assert!(s.contains("filter"), "got: {s}");
}
#[test]
fn lowers_nested_set_to_nested_write_op() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
set: [{ id: 1 }, { id: 2 }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("NestedWriteOp :: Set"), "got: {s}");
assert!(s.contains("set_pks"), "got: {s}");
}
#[test]
fn lowers_nested_set_with_empty_list() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
set: []
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("NestedWriteOp :: Set"), "got: {s}");
}
#[test]
fn lowers_nested_connect_or_create_to_nested_write_op() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
connect_or_create: [{ where: { id: 1 }, create: { title: "x" } }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("ConnectOrCreate"), "got: {s}");
assert!(s.contains("where_filter"), "got: {s}");
assert!(s.contains("create_payload"), "got: {s}");
}
#[test]
fn lowers_nested_update_to_nested_write_op_update() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
update: [{ where: { id: 1 }, data: { title: "renamed" } }]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("NestedWriteOp"), "got: {s}");
assert!(s.contains("Update"), "got: {s}");
assert!(s.contains("target_pk"), "got: {s}");
assert!(s.contains("payload"), "got: {s}");
assert!(s.contains("UpdateInput"), "got: {s}");
}
#[test]
fn lowers_nested_update_many_to_nested_write_op_update_many() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
update_many: { where: { published: false }, data: { title: "x" } }
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("UpdateMany"), "got: {s}");
assert!(s.contains("foreign_key"), "got: {s}");
assert!(s.contains("filter"), "got: {s}");
assert!(s.contains("payload"), "got: {s}");
}
#[test]
fn lowers_nested_upsert_to_nested_write_op_upsert() {
let schema = parsed_schema();
let user = schema.get_model("User").unwrap().clone();
let ctx = LowerCtx::new(&schema, &user);
let field = user.get_field("posts").unwrap();
let value = DslValue::Block(parse_block(quote!({
upsert: [{
where: { id: 99 },
create: { title: "new", published: true },
update: { title: "x" }
}]
})));
let ops = lower_create_relation(field, &value, Span::call_site(), &ctx).unwrap();
assert_eq!(ops.len(), 1);
let s = pretty(ops[0].op_expr.clone());
assert!(s.contains("Upsert"), "got: {s}");
assert!(s.contains("target_pk"), "got: {s}");
assert!(s.contains("create_payload"), "got: {s}");
assert!(s.contains("update_payload"), "got: {s}");
assert!(s.contains("99"), "got: {s}");
}
}