@@ -42,7 +42,7 @@ use spacetimedb_sats::{memory_usage::MemoryUsage, Deserialize};
4242use spacetimedb_schema:: table_name:: TableName ;
4343use spacetimedb_schema:: {
4444 reducer_name:: ReducerName ,
45- schema:: { ColumnSchema , IndexSchema , SequenceSchema , TableSchema } ,
45+ schema:: { ColumnSchema , ConstraintSchema , IndexSchema , SequenceSchema , TableSchema } ,
4646} ;
4747use spacetimedb_snapshot:: { ReconstructedSnapshot , SnapshotRepository } ;
4848use spacetimedb_table:: {
@@ -575,6 +575,14 @@ impl MutTxDatastore for Locking {
575575 tx. sequence_id_from_name ( sequence_name)
576576 }
577577
578+ fn create_constraint_mut_tx (
579+ & self ,
580+ tx : & mut Self :: MutTx ,
581+ constraint : ConstraintSchema ,
582+ ) -> Result < ConstraintId > {
583+ tx. create_constraint ( constraint)
584+ }
585+
578586 fn drop_constraint_mut_tx ( & self , tx : & mut Self :: MutTx , constraint_id : ConstraintId ) -> Result < ( ) > {
579587 tx. drop_constraint ( constraint_id)
580588 }
@@ -1206,13 +1214,14 @@ impl<F: FnMut(u64)> spacetimedb_commitlog::payload::txdata::Visitor for ReplayVi
12061214 // TODO: avoid clone
12071215 Ok ( schema) => schema. table_name . clone ( ) ,
12081216
1209- Err ( _) => match self . dropped_table_names . remove ( & table_id) {
1210- Some ( name) => name,
1211- _ => {
1217+ Err ( _) => {
1218+ if let Some ( name) = self . dropped_table_names . remove ( & table_id) {
1219+ name
1220+ } else {
12121221 return self
12131222 . process_error ( anyhow ! ( "Error looking up name for truncated table {table_id:?}" ) . into ( ) ) ;
12141223 }
1215- } ,
1224+ }
12161225 } ;
12171226
12181227 if let Err ( e) = self . committed_state . replay_truncate ( table_id) . with_context ( || {
@@ -1300,7 +1309,7 @@ mod tests {
13001309 use spacetimedb_lib:: error:: ResultTest ;
13011310 use spacetimedb_lib:: st_var:: StVarValue ;
13021311 use spacetimedb_lib:: { resolved_type_via_v9, ScheduleAt , TimeDuration } ;
1303- use spacetimedb_primitives:: { col_list, ArgId , ColId , ScheduleId , ViewId } ;
1312+ use spacetimedb_primitives:: { col_list, ArgId , ColId , ColSet , ScheduleId , ViewId } ;
13041313 use spacetimedb_sats:: algebraic_value:: ser:: value_serialize;
13051314 use spacetimedb_sats:: bsatn:: ToBsatn ;
13061315 use spacetimedb_sats:: layout:: RowTypeLayout ;
@@ -3975,4 +3984,197 @@ mod tests {
39753984 ) ;
39763985 Ok ( ( ) )
39773986 }
3987+
3988+ /// Helper: create a table with a non-unique btree index on `col_pos` but no constraints.
3989+ fn table_with_non_unique_index ( col_pos : u16 ) -> TableSchema {
3990+ let indices = vec ! [ IndexSchema :: for_test(
3991+ "Foo_idx_btree" ,
3992+ BTreeAlgorithm :: from( col_pos) ,
3993+ ) ] ;
3994+ basic_table_schema_with_indices ( indices, Vec :: < ConstraintSchema > :: new ( ) )
3995+ }
3996+
3997+ /// Helper: create a table with a non-unique btree index on multiple columns but no constraints.
3998+ fn table_with_non_unique_multi_col_index ( cols : impl Into < ColList > ) -> TableSchema {
3999+ let indices = vec ! [ IndexSchema :: for_test(
4000+ "Foo_multi_idx_btree" ,
4001+ BTreeAlgorithm { columns: cols. into( ) } ,
4002+ ) ] ;
4003+ basic_table_schema_with_indices ( indices, Vec :: < ConstraintSchema > :: new ( ) )
4004+ }
4005+
4006+ #[ test]
4007+ fn test_create_constraint_makes_index_unique ( ) -> ResultTest < ( ) > {
4008+ let datastore = get_datastore ( ) ?;
4009+
4010+ // TX1: create table with non-unique index on col 0.
4011+ let mut tx = begin_mut_tx ( & datastore) ;
4012+ let schema = table_with_non_unique_index ( 0 ) ;
4013+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4014+ commit ( & datastore, tx) ?;
4015+
4016+ // TX2: insert unique rows and commit.
4017+ let mut tx = begin_mut_tx ( & datastore) ;
4018+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4019+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 2 , "Bob" , 25 ) ) ?;
4020+ commit ( & datastore, tx) ?;
4021+
4022+ // TX3: add unique constraint — should succeed since data is unique.
4023+ let mut tx = begin_mut_tx ( & datastore) ;
4024+ let mut constraint = ConstraintSchema :: unique_for_test ( "Foo_id_unique" , 0u16 ) ;
4025+ constraint. table_id = table_id;
4026+ datastore. create_constraint_mut_tx ( & mut tx, constraint) ?;
4027+
4028+ // Inserting a duplicate should now fail (index is unique).
4029+ let dup_result = insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Charlie" , 20 ) ) ;
4030+ assert ! ( dup_result. is_err( ) , "duplicate insert should fail after adding unique constraint" ) ;
4031+ commit ( & datastore, tx) ?;
4032+
4033+ Ok ( ( ) )
4034+ }
4035+
4036+ #[ test]
4037+ fn test_create_constraint_rollback_restores_non_unique ( ) -> ResultTest < ( ) > {
4038+ let datastore = get_datastore ( ) ?;
4039+
4040+ // TX1: create table with non-unique index on col 0.
4041+ let mut tx = begin_mut_tx ( & datastore) ;
4042+ let schema = table_with_non_unique_index ( 0 ) ;
4043+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4044+ commit ( & datastore, tx) ?;
4045+
4046+ // TX2: insert unique rows and commit.
4047+ let mut tx = begin_mut_tx ( & datastore) ;
4048+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4049+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 2 , "Bob" , 25 ) ) ?;
4050+ commit ( & datastore, tx) ?;
4051+
4052+ // TX3: add unique constraint, then rollback.
4053+ let mut tx = begin_mut_tx ( & datastore) ;
4054+ let mut constraint = ConstraintSchema :: unique_for_test ( "Foo_id_unique" , 0u16 ) ;
4055+ constraint. table_id = table_id;
4056+ datastore. create_constraint_mut_tx ( & mut tx, constraint) ?;
4057+ let _ = datastore. rollback_mut_tx ( tx) ;
4058+
4059+ // TX4: after rollback, duplicates should be allowed again.
4060+ let mut tx = begin_mut_tx ( & datastore) ;
4061+ let result = insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Charlie" , 20 ) ) ;
4062+ assert ! ( result. is_ok( ) , "duplicate insert should succeed after rollback of unique constraint" ) ;
4063+ Ok ( ( ) )
4064+ }
4065+
4066+ #[ test]
4067+ fn test_create_constraint_fails_with_duplicates ( ) -> ResultTest < ( ) > {
4068+ let datastore = get_datastore ( ) ?;
4069+
4070+ // TX1: create table with non-unique index on col 0.
4071+ let mut tx = begin_mut_tx ( & datastore) ;
4072+ let schema = table_with_non_unique_index ( 0 ) ;
4073+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4074+ commit ( & datastore, tx) ?;
4075+
4076+ // TX2: insert duplicate rows and commit.
4077+ let mut tx = begin_mut_tx ( & datastore) ;
4078+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4079+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Bob" , 25 ) ) ?; // duplicate id=1
4080+ commit ( & datastore, tx) ?;
4081+
4082+ // TX3: try to add unique constraint — should fail.
4083+ let mut tx = begin_mut_tx ( & datastore) ;
4084+ let mut constraint = ConstraintSchema :: unique_for_test ( "Foo_id_unique" , 0u16 ) ;
4085+ constraint. table_id = table_id;
4086+ let result = datastore. create_constraint_mut_tx ( & mut tx, constraint) ;
4087+ assert ! ( result. is_err( ) , "create_constraint should fail when duplicates exist" ) ;
4088+
4089+ Ok ( ( ) )
4090+ }
4091+
4092+ #[ test]
4093+ fn test_create_constraint_multi_col ( ) -> ResultTest < ( ) > {
4094+ let datastore = get_datastore ( ) ?;
4095+
4096+ // TX1: create table with non-unique multi-column index on (col 0, col 2).
4097+ let mut tx = begin_mut_tx ( & datastore) ;
4098+ let schema = table_with_non_unique_multi_col_index ( col_list ! [ 0 , 2 ] ) ;
4099+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4100+ commit ( & datastore, tx) ?;
4101+
4102+ // TX2: insert rows unique on (id, age) and commit.
4103+ let mut tx = begin_mut_tx ( & datastore) ;
4104+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4105+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Bob" , 25 ) ) ?; // same id, different age
4106+ commit ( & datastore, tx) ?;
4107+
4108+ // TX3: add unique constraint on (col 0, col 2) — should succeed.
4109+ let mut tx = begin_mut_tx ( & datastore) ;
4110+ let mut constraint = ConstraintSchema :: unique_for_test (
4111+ "Foo_id_age_unique" ,
4112+ ColSet :: from ( col_list ! [ 0 , 2 ] ) ,
4113+ ) ;
4114+ constraint. table_id = table_id;
4115+ datastore. create_constraint_mut_tx ( & mut tx, constraint) ?;
4116+ commit ( & datastore, tx) ?;
4117+
4118+ Ok ( ( ) )
4119+ }
4120+
4121+ #[ test]
4122+ fn test_drop_constraint_makes_index_non_unique ( ) -> ResultTest < ( ) > {
4123+ let datastore = get_datastore ( ) ?;
4124+
4125+ // TX1: create table with unique constraint.
4126+ let mut tx = begin_mut_tx ( & datastore) ;
4127+ let schema = basic_table_schema_with_indices ( basic_indices ( ) , basic_constraints ( ) ) ;
4128+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4129+ commit ( & datastore, tx) ?;
4130+
4131+ // TX2: insert a row.
4132+ let mut tx = begin_mut_tx ( & datastore) ;
4133+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4134+ commit ( & datastore, tx) ?;
4135+
4136+ // TX3: drop the unique constraint on col 0.
4137+ let mut tx = begin_mut_tx ( & datastore) ;
4138+ let constraint_id = tx
4139+ . constraint_id_from_name ( "Foo_id_key" ) ?
4140+ . expect ( "constraint should exist" ) ;
4141+ datastore. drop_constraint_mut_tx ( & mut tx, constraint_id) ?;
4142+
4143+ // Inserting a duplicate on col 0 should now succeed.
4144+ let result = insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Bob" , 25 ) ) ;
4145+ assert ! ( result. is_ok( ) , "duplicate insert should succeed after dropping unique constraint" ) ;
4146+ commit ( & datastore, tx) ?;
4147+
4148+ Ok ( ( ) )
4149+ }
4150+
4151+ #[ test]
4152+ fn test_drop_constraint_rollback_keeps_unique ( ) -> ResultTest < ( ) > {
4153+ let datastore = get_datastore ( ) ?;
4154+
4155+ // TX1: create table with unique constraint.
4156+ let mut tx = begin_mut_tx ( & datastore) ;
4157+ let schema = basic_table_schema_with_indices ( basic_indices ( ) , basic_constraints ( ) ) ;
4158+ let table_id = datastore. create_table_mut_tx ( & mut tx, schema) ?;
4159+ commit ( & datastore, tx) ?;
4160+
4161+ // TX2: insert a row.
4162+ let mut tx = begin_mut_tx ( & datastore) ;
4163+ insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Alice" , 30 ) ) ?;
4164+ commit ( & datastore, tx) ?;
4165+
4166+ // TX3: drop constraint, then rollback.
4167+ let mut tx = begin_mut_tx ( & datastore) ;
4168+ let constraint_id = tx
4169+ . constraint_id_from_name ( "Foo_id_key" ) ?
4170+ . expect ( "constraint should exist" ) ;
4171+ datastore. drop_constraint_mut_tx ( & mut tx, constraint_id) ?;
4172+ let _ = datastore. rollback_mut_tx ( tx) ;
4173+
4174+ // TX4: after rollback, constraint should be back — duplicates should fail.
4175+ let mut tx = begin_mut_tx ( & datastore) ;
4176+ let dup_result = insert ( & datastore, & mut tx, table_id, & u32_str_u32 ( 1 , "Bob" , 25 ) ) ;
4177+ assert ! ( dup_result. is_err( ) , "duplicate insert should fail after rollback of drop constraint" ) ;
4178+ Ok ( ( ) )
4179+ }
39784180}
0 commit comments