@@ -1309,7 +1309,7 @@ mod tests {
13091309 use spacetimedb_lib:: error:: ResultTest ;
13101310 use spacetimedb_lib:: st_var:: StVarValue ;
13111311 use spacetimedb_lib:: { resolved_type_via_v9, ScheduleAt , TimeDuration } ;
1312- use spacetimedb_primitives:: { col_list, ArgId , ColId , ScheduleId , ViewId } ;
1312+ use spacetimedb_primitives:: { col_list, ArgId , ColId , ColSet , ScheduleId , ViewId } ;
13131313 use spacetimedb_sats:: algebraic_value:: ser:: value_serialize;
13141314 use spacetimedb_sats:: bsatn:: ToBsatn ;
13151315 use spacetimedb_sats:: layout:: RowTypeLayout ;
@@ -3984,4 +3984,197 @@ mod tests {
39843984 ) ;
39853985 Ok ( ( ) )
39863986 }
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+ }
39874180}
0 commit comments