@@ -446,6 +446,13 @@ impl Database {
446446 fn commit_with_source ( & self , tx : TxId , source : CommitSource ) -> Result < ( ) > {
447447 let mut ws = self . tx_mgr . cloned_write_set ( tx) ?;
448448
449+ if !ws. is_empty ( )
450+ && let Err ( err) = self . validate_foreign_keys_in_tx ( tx)
451+ {
452+ let _ = self . rollback ( tx) ;
453+ return Err ( err) ;
454+ }
455+
449456 if !ws. is_empty ( )
450457 && let Err ( err) = self . plugin . pre_commit ( & ws, source)
451458 {
@@ -662,6 +669,12 @@ impl Database {
662669 . default
663670 . as_ref ( )
664671 . map ( crate :: executor:: stored_default_expr) ,
672+ references : col. references . as_ref ( ) . map ( |reference| {
673+ contextdb_core:: ForeignKeyReference {
674+ table : reference. table . clone ( ) ,
675+ column : reference. column . clone ( ) ,
676+ }
677+ } ) ,
665678 expires : col. expires ,
666679 } ) ;
667680 if col. expires {
@@ -751,6 +764,7 @@ impl Database {
751764 table : & str ,
752765 values : HashMap < ColName , Value > ,
753766 ) -> Result < RowId > {
767+ self . validate_row_constraints ( tx, table, & values, None ) ?;
754768 self . relational . insert ( tx, table, values)
755769 }
756770
@@ -761,6 +775,17 @@ impl Database {
761775 conflict_col : & str ,
762776 values : HashMap < ColName , Value > ,
763777 ) -> Result < UpsertResult > {
778+ let snapshot = self . snapshot ( ) ;
779+ let existing_row_id = values
780+ . get ( conflict_col)
781+ . map ( |conflict_value| {
782+ self . point_lookup_in_tx ( tx, table, conflict_col, conflict_value, snapshot)
783+ . map ( |row| row. map ( |row| row. row_id ) )
784+ } )
785+ . transpose ( ) ?
786+ . flatten ( ) ;
787+ self . validate_row_constraints ( tx, table, & values, existing_row_id) ?;
788+
764789 let row_uuid = values. get ( "id" ) . and_then ( Value :: as_uuid) . copied ( ) ;
765790 let meta = self . table_meta ( table) ;
766791 let new_state = meta
@@ -772,7 +797,7 @@ impl Database {
772797
773798 let result = self
774799 . relational
775- . upsert ( tx, table, conflict_col, values, self . snapshot ( ) ) ?;
800+ . upsert ( tx, table, conflict_col, values, snapshot) ?;
776801
777802 if let ( Some ( uuid) , Some ( state) , Some ( _meta) ) =
778803 ( row_uuid, new_state. as_deref ( ) , meta. as_ref ( ) )
@@ -784,6 +809,116 @@ impl Database {
784809 Ok ( result)
785810 }
786811
812+ fn validate_row_constraints (
813+ & self ,
814+ tx : TxId ,
815+ table : & str ,
816+ values : & HashMap < ColName , Value > ,
817+ skip_row_id : Option < RowId > ,
818+ ) -> Result < ( ) > {
819+ let meta = self
820+ . table_meta ( table)
821+ . ok_or_else ( || Error :: TableNotFound ( table. to_string ( ) ) ) ?;
822+ let snapshot = self . snapshot ( ) ;
823+
824+ let visible_rows =
825+ self . relational
826+ . scan_filter_with_tx ( Some ( tx) , table, snapshot, & |row| {
827+ skip_row_id. is_none_or ( |row_id| row. row_id != row_id)
828+ } ) ?;
829+
830+ for column in meta
831+ . columns
832+ . iter ( )
833+ . filter ( |column| column. unique && !column. primary_key )
834+ {
835+ let Some ( value) = values. get ( & column. name ) else {
836+ continue ;
837+ } ;
838+ if * value == Value :: Null {
839+ continue ;
840+ }
841+ if visible_rows
842+ . iter ( )
843+ . any ( |existing| existing. values . get ( & column. name ) == Some ( value) )
844+ {
845+ return Err ( Error :: UniqueViolation {
846+ table : table. to_string ( ) ,
847+ column : column. name . clone ( ) ,
848+ } ) ;
849+ }
850+ }
851+
852+ for unique_constraint in & meta. unique_constraints {
853+ let mut candidate_values = Vec :: with_capacity ( unique_constraint. len ( ) ) ;
854+ let mut has_null = false ;
855+
856+ for column_name in unique_constraint {
857+ match values. get ( column_name) {
858+ Some ( Value :: Null ) | None => {
859+ has_null = true ;
860+ break ;
861+ }
862+ Some ( value) => candidate_values. push ( value) ,
863+ }
864+ }
865+
866+ if has_null {
867+ continue ;
868+ }
869+
870+ if visible_rows. iter ( ) . any ( |existing| {
871+ unique_constraint
872+ . iter ( )
873+ . zip ( candidate_values. iter ( ) )
874+ . all ( |( column_name, value) | existing. values . get ( column_name) == Some ( * value) )
875+ } ) {
876+ return Err ( Error :: UniqueViolation {
877+ table : table. to_string ( ) ,
878+ column : unique_constraint. join ( "," ) ,
879+ } ) ;
880+ }
881+ }
882+
883+ Ok ( ( ) )
884+ }
885+
886+ fn validate_foreign_keys_in_tx ( & self , tx : TxId ) -> Result < ( ) > {
887+ let snapshot = self . snapshot ( ) ;
888+ let relational_inserts = self
889+ . tx_mgr
890+ . with_write_set ( tx, |ws| ws. relational_inserts . clone ( ) ) ?;
891+
892+ for ( table, row) in relational_inserts {
893+ let meta = self
894+ . table_meta ( & table)
895+ . ok_or_else ( || Error :: TableNotFound ( table. clone ( ) ) ) ?;
896+ for column in & meta. columns {
897+ let Some ( reference) = & column. references else {
898+ continue ;
899+ } ;
900+ let Some ( value) = row. values . get ( & column. name ) else {
901+ continue ;
902+ } ;
903+ if * value == Value :: Null {
904+ continue ;
905+ }
906+ if self
907+ . point_lookup_in_tx ( tx, & reference. table , & reference. column , value, snapshot) ?
908+ . is_none ( )
909+ {
910+ return Err ( Error :: ForeignKeyViolation {
911+ table : table. clone ( ) ,
912+ column : column. name . clone ( ) ,
913+ ref_table : reference. table . clone ( ) ,
914+ } ) ;
915+ }
916+ }
917+ }
918+
919+ Ok ( ( ) )
920+ }
921+
787922 pub ( crate ) fn propagate_state_change_if_needed (
788923 & self ,
789924 tx : TxId ,
@@ -3561,11 +3696,19 @@ fn sql_type_for_meta_column(col: &contextdb_core::ColumnDef, rules: &[Propagatio
35613696 } )
35623697 . collect :: < Vec < _ > > ( ) ;
35633698
3564- if let Some ( ( referenced_table, referenced_column, ..) ) = fk_rules. first ( ) {
3699+ if let Some ( reference) = & col. references {
3700+ ty. push_str ( & format ! (
3701+ " REFERENCES {}({})" ,
3702+ reference. table, reference. column
3703+ ) ) ;
3704+ } else if let Some ( ( referenced_table, referenced_column, ..) ) = fk_rules. first ( ) {
35653705 ty. push_str ( & format ! (
35663706 " REFERENCES {}({})" ,
35673707 referenced_table, referenced_column
35683708 ) ) ;
3709+ }
3710+
3711+ if col. references . is_some ( ) || !fk_rules. is_empty ( ) {
35693712 for ( _, _, trigger_state, target_state, max_depth, abort_on_failure) in fk_rules {
35703713 ty. push_str ( & format ! (
35713714 " ON STATE {} PROPAGATE SET {}" ,
@@ -3630,6 +3773,10 @@ fn create_table_constraints_from_ast(ct: &CreateTable) -> Vec<String> {
36303773 constraints. push ( clause) ;
36313774 }
36323775
3776+ for unique_constraint in & ct. unique_constraints {
3777+ constraints. push ( format ! ( "UNIQUE ({})" , unique_constraint. join( ", " ) ) ) ;
3778+ }
3779+
36333780 for rule in & ct. propagation_rules {
36343781 match rule {
36353782 contextdb_parser:: ast:: AstPropagationRule :: EdgeState {
@@ -3700,6 +3847,10 @@ fn create_table_constraints_from_meta(meta: &TableMeta) -> Vec<String> {
37003847 constraints. push ( clause) ;
37013848 }
37023849
3850+ for unique_constraint in & meta. unique_constraints {
3851+ constraints. push ( format ! ( "UNIQUE ({})" , unique_constraint. join( ", " ) ) ) ;
3852+ }
3853+
37033854 for rule in & meta. propagation_rules {
37043855 match rule {
37053856 PropagationRule :: Edge {
0 commit comments