@@ -758,6 +758,71 @@ fn resolve_composite_type_fields(
758758 } ,
759759 } ;
760760
761+ let relation_path = field. annotations . get ( "relationPath" ) . and_then ( |params| {
762+ match params {
763+ AstAnnotationParams :: Single ( expr, _) => match expr {
764+ AstExpr :: StringLiteral ( path, _) => {
765+ let segments: Vec < String > = path
766+ . split ( '.' )
767+ . map ( |segment| segment. trim ( ) . to_string ( ) )
768+ . filter ( |segment| !segment. is_empty ( ) )
769+ . collect ( ) ;
770+
771+ if segments. is_empty ( ) {
772+ errors. push ( Diagnostic {
773+ level : Level :: Error ,
774+ message : format ! (
775+ "@relationPath for field '{}' must contain at least one segment" ,
776+ field. name
777+ ) ,
778+ code : Some ( "C000" . to_string ( ) ) ,
779+ spans : vec ! [ SpanLabel {
780+ span: field. span,
781+ style: SpanStyle :: Primary ,
782+ label: None ,
783+ } ] ,
784+ } ) ;
785+ None
786+ } else {
787+ Some ( segments)
788+ }
789+ }
790+ _ => {
791+ errors. push ( Diagnostic {
792+ level : Level :: Error ,
793+ message : format ! (
794+ "@relationPath for field '{}' must be a string literal" ,
795+ field. name
796+ ) ,
797+ code : Some ( "C000" . to_string ( ) ) ,
798+ spans : vec ! [ SpanLabel {
799+ span: field. span,
800+ style: SpanStyle :: Primary ,
801+ label: None ,
802+ } ] ,
803+ } ) ;
804+ None
805+ }
806+ } ,
807+ _ => {
808+ errors. push ( Diagnostic {
809+ level : Level :: Error ,
810+ message : format ! (
811+ "@relationPath for field '{}' must use the syntax @relationPath(\" segment1.segment2\" )" ,
812+ field. name
813+ ) ,
814+ code : Some ( "C000" . to_string ( ) ) ,
815+ spans : vec ! [ SpanLabel {
816+ span: field. span,
817+ style: SpanStyle :: Primary ,
818+ label: None ,
819+ } ] ,
820+ } ) ;
821+ None
822+ }
823+ }
824+ } ) ;
825+
761826 let typ = resolve_field_type (
762827 & field. typ . to_typ ( & typechecked_system. types ) ,
763828 & typechecked_system. types ,
@@ -768,6 +833,23 @@ fn resolve_composite_type_fields(
768833 . as_ref ( )
769834 . map ( |v| resolve_field_default_type ( v, & typ, errors) ) ;
770835
836+ if relation_path. is_some ( ) && field. annotations . get ( "computed" ) . is_some ( ) {
837+ errors. push ( Diagnostic {
838+ level : Level :: Error ,
839+ message : format ! (
840+ "Field '{}' cannot use both @computed and @relationPath" ,
841+ field. name
842+ ) ,
843+ code : Some ( "C000" . to_string ( ) ) ,
844+ spans : vec ! [ SpanLabel {
845+ span: field. span,
846+ style: SpanStyle :: Primary ,
847+ label: None ,
848+ } ] ,
849+ } ) ;
850+ continue ;
851+ }
852+
771853 if let Some ( computed_params) = field. annotations . get ( "computed" ) {
772854 let resolved_computed =
773855 match parse_computed_field ( module_base_path, field, computed_params, errors) {
@@ -791,14 +873,29 @@ fn resolve_composite_type_fields(
791873 default_value : None ,
792874 update_sync : false ,
793875 readonly : true ,
876+ relation_path : None ,
794877 doc_comments : field. doc_comments . clone ( ) ,
795878 computed : Some ( resolved_computed) ,
796879 span : field. span ,
797880 } ) ;
798881 continue ;
799882 }
800883
801- let column_info = compute_column_info ( ct, field, & typechecked_system. types , table_managed) ;
884+ if relation_path. is_some ( ) {
885+ readonly = true ;
886+ }
887+
888+ let column_info = if relation_path. is_some ( ) {
889+ Ok ( ColumnInfo {
890+ names : vec ! [ ] ,
891+ self_column : false ,
892+ unique_constraints : vec ! [ ] ,
893+ indices : vec ! [ ] ,
894+ cardinality : None ,
895+ } )
896+ } else {
897+ compute_column_info ( ct, field, & typechecked_system. types , table_managed)
898+ } ;
802899
803900 let ColumnInfo {
804901 names : column_names,
@@ -860,6 +957,7 @@ fn resolve_composite_type_fields(
860957 default_value : field_default,
861958 update_sync,
862959 readonly,
960+ relation_path,
863961 doc_comments : field. doc_comments . clone ( ) ,
864962 computed : None ,
865963 span : field. span ,
0 commit comments