@@ -3039,94 +3039,20 @@ mod tests {
3039
3039
pub should_write_data : Option < Arc < AtomicBool > > ,
3040
3040
}
3041
3041
3042
- // This is a not-super-future-maintainer-friendly helper to check that all
3043
- // the subtables related to blueprints have been pruned of a specific
3044
- // blueprint ID. If additional blueprint tables are added in the future,
3045
- // this function will silently ignore them unless they're manually added.
3042
+ // Check that all the subtables related to blueprints have been pruned of a specific
3043
+ // blueprint ID. Uses the shared BlueprintTableCounts struct.
3046
3044
async fn ensure_blueprint_fully_deleted (
3047
3045
datastore : & DataStore ,
3048
3046
blueprint_id : BlueprintUuid ,
3049
3047
) {
3050
- let conn = datastore. pool_connection_for_tests ( ) . await . unwrap ( ) ;
3051
-
3052
- macro_rules! query_count {
3053
- ( $table: ident, $blueprint_id_col: ident) => { {
3054
- use nexus_db_schema:: schema:: $table:: dsl;
3055
- let result = dsl:: $table
3056
- . filter(
3057
- dsl:: $blueprint_id_col
3058
- . eq( to_db_typed_uuid( blueprint_id) ) ,
3059
- )
3060
- . count( )
3061
- . get_result_async( & * conn)
3062
- . await ;
3063
- ( stringify!( $table) , result)
3064
- } } ;
3065
- }
3048
+ let counts = BlueprintTableCounts :: new ( datastore, blueprint_id) . await ;
3066
3049
3067
- // These tables start with `bp_` but do not represent the contents of a
3068
- // specific blueprint. It should be uncommon to add things to this
3069
- // list.
3070
- let tables_ignored: BTreeSet < _ > = [ "bp_target" ] . into_iter ( ) . collect ( ) ;
3071
-
3072
- let mut tables_checked = BTreeSet :: new ( ) ;
3073
- for ( table_name, result) in [
3074
- query_count ! ( blueprint, id) ,
3075
- query_count ! ( bp_sled_metadata, blueprint_id) ,
3076
- query_count ! ( bp_omicron_dataset, blueprint_id) ,
3077
- query_count ! ( bp_omicron_physical_disk, blueprint_id) ,
3078
- query_count ! ( bp_omicron_zone, blueprint_id) ,
3079
- query_count ! ( bp_omicron_zone_nic, blueprint_id) ,
3080
- query_count ! ( bp_clickhouse_cluster_config, blueprint_id) ,
3081
- query_count ! ( bp_clickhouse_keeper_zone_id_to_node_id, blueprint_id) ,
3082
- query_count ! ( bp_clickhouse_server_zone_id_to_node_id, blueprint_id) ,
3083
- query_count ! ( bp_oximeter_read_policy, blueprint_id) ,
3084
- query_count ! ( bp_pending_mgs_update_sp, blueprint_id) ,
3085
- query_count ! ( bp_pending_mgs_update_rot, blueprint_id) ,
3086
- query_count ! ( bp_pending_mgs_update_rot_bootloader, blueprint_id) ,
3087
- query_count ! ( bp_pending_mgs_update_host_phase_1, blueprint_id) ,
3088
- ] {
3089
- let count: i64 = result. unwrap ( ) ;
3090
- assert_eq ! (
3091
- count, 0 ,
3092
- "nonzero row count for blueprint \
3093
- {blueprint_id} in table {table_name}"
3094
- ) ;
3095
- tables_checked. insert ( table_name) ;
3096
- }
3097
-
3098
- // Look for likely blueprint-related tables that we didn't check.
3099
- let mut query = QueryBuilder :: new ( ) ;
3100
- query. sql (
3101
- "SELECT table_name \
3102
- FROM information_schema.tables \
3103
- WHERE table_name LIKE 'bp\\ _%'",
3050
+ // All tables should be empty (no exceptions for deleted blueprints)
3051
+ assert ! (
3052
+ counts. all_empty( ) ,
3053
+ "Blueprint {blueprint_id} not fully deleted. Non-empty tables: {:?}" ,
3054
+ counts. non_empty_tables( )
3104
3055
) ;
3105
- let tables_unchecked: Vec < String > = query
3106
- . query :: < diesel:: sql_types:: Text > ( )
3107
- . load_async ( & * conn)
3108
- . await
3109
- . expect ( "Failed to query information_schema for tables" )
3110
- . into_iter ( )
3111
- . filter ( |f : & String | {
3112
- let t = f. as_str ( ) ;
3113
- !tables_ignored. contains ( t) && !tables_checked. contains ( t)
3114
- } )
3115
- . collect ( ) ;
3116
- if !tables_unchecked. is_empty ( ) {
3117
- // If you see this message, you probably added a blueprint table
3118
- // whose name started with `bp_*`. Add it to the block above so
3119
- // that this function checks whether deleting a blueprint deletes
3120
- // rows from that table. (You may also find you need to update
3121
- // blueprint_delete() to actually delete said rows.)
3122
- panic ! (
3123
- "found table(s) that look related to blueprints, but \
3124
- aren't covered by ensure_blueprint_fully_deleted(). \
3125
- Please add them to that function!\n
3126
- Found: {}" ,
3127
- tables_unchecked. join( ", " )
3128
- ) ;
3129
- }
3130
3056
}
3131
3057
3132
3058
// Create a fake set of `SledDetails`, either with a subnet matching
@@ -3287,6 +3213,9 @@ mod tests {
3287
3213
[ blueprint1. id]
3288
3214
) ;
3289
3215
3216
+ // Ensure every bp_* table received at least one row for this blueprint (issue #8455).
3217
+ ensure_blueprint_fully_populated ( & datastore, blueprint1. id ) . await ;
3218
+
3290
3219
// Check the number of blueprint elements against our collection.
3291
3220
assert_eq ! (
3292
3221
blueprint1. sleds. len( ) ,
@@ -4583,4 +4512,192 @@ mod tests {
4583
4512
found these zones not in service: {not_in_service:?}"
4584
4513
) ;
4585
4514
}
4515
+
4516
+ /// Counts rows in blueprint-related tables for a specific blueprint ID.
4517
+ /// Used by both `ensure_blueprint_fully_populated` and `ensure_blueprint_fully_deleted`.
4518
+ struct BlueprintTableCounts {
4519
+ counts : BTreeMap < String , i64 > ,
4520
+ }
4521
+
4522
+ impl BlueprintTableCounts {
4523
+ /// Create a new BlueprintTableCounts by querying all blueprint tables.
4524
+ async fn new (
4525
+ datastore : & DataStore ,
4526
+ blueprint_id : BlueprintUuid ,
4527
+ ) -> BlueprintTableCounts {
4528
+ let conn = datastore. pool_connection_for_tests ( ) . await . unwrap ( ) ;
4529
+
4530
+ macro_rules! query_count {
4531
+ ( $table: ident, $blueprint_id_col: ident) => { {
4532
+ use nexus_db_schema:: schema:: $table:: dsl;
4533
+ let result = dsl:: $table
4534
+ . filter(
4535
+ dsl:: $blueprint_id_col
4536
+ . eq( to_db_typed_uuid( blueprint_id) ) ,
4537
+ )
4538
+ . count( )
4539
+ . get_result_async( & * conn)
4540
+ . await ;
4541
+ ( stringify!( $table) , result)
4542
+ } } ;
4543
+ }
4544
+
4545
+ let mut counts = BTreeMap :: new ( ) ;
4546
+ for ( table_name, result) in [
4547
+ query_count ! ( blueprint, id) ,
4548
+ query_count ! ( bp_sled_metadata, blueprint_id) ,
4549
+ query_count ! ( bp_omicron_dataset, blueprint_id) ,
4550
+ query_count ! ( bp_omicron_physical_disk, blueprint_id) ,
4551
+ query_count ! ( bp_omicron_zone, blueprint_id) ,
4552
+ query_count ! ( bp_omicron_zone_nic, blueprint_id) ,
4553
+ query_count ! ( bp_clickhouse_cluster_config, blueprint_id) ,
4554
+ query_count ! (
4555
+ bp_clickhouse_keeper_zone_id_to_node_id,
4556
+ blueprint_id
4557
+ ) ,
4558
+ query_count ! (
4559
+ bp_clickhouse_server_zone_id_to_node_id,
4560
+ blueprint_id
4561
+ ) ,
4562
+ query_count ! ( bp_oximeter_read_policy, blueprint_id) ,
4563
+ query_count ! ( bp_pending_mgs_update_sp, blueprint_id) ,
4564
+ query_count ! ( bp_pending_mgs_update_rot, blueprint_id) ,
4565
+ query_count ! (
4566
+ bp_pending_mgs_update_rot_bootloader,
4567
+ blueprint_id
4568
+ ) ,
4569
+ query_count ! ( bp_pending_mgs_update_host_phase_1, blueprint_id) ,
4570
+ ] {
4571
+ let count: i64 = result. unwrap ( ) ;
4572
+ counts. insert ( table_name. to_string ( ) , count) ;
4573
+ }
4574
+
4575
+ let table_counts = BlueprintTableCounts { counts } ;
4576
+
4577
+ // Verify no new blueprint tables were added without updating this function
4578
+ if let Err ( msg) =
4579
+ table_counts. verify_all_tables_covered ( datastore) . await
4580
+ {
4581
+ panic ! ( "{}" , msg) ;
4582
+ }
4583
+
4584
+ table_counts
4585
+ }
4586
+
4587
+ /// Returns true if all tables are empty (0 rows).
4588
+ fn all_empty ( & self ) -> bool {
4589
+ self . counts . values ( ) . all ( |& count| count == 0 )
4590
+ }
4591
+
4592
+ /// Returns a list of table names that are empty.
4593
+ fn empty_tables ( & self ) -> Vec < String > {
4594
+ self . counts
4595
+ . iter ( )
4596
+ . filter_map (
4597
+ |( table, & count) | {
4598
+ if count == 0 { Some ( table. clone ( ) ) } else { None }
4599
+ } ,
4600
+ )
4601
+ . collect ( )
4602
+ }
4603
+
4604
+ /// Returns a list of table names that are non-empty.
4605
+ fn non_empty_tables ( & self ) -> Vec < String > {
4606
+ self . counts
4607
+ . iter ( )
4608
+ . filter_map (
4609
+ |( table, & count) | {
4610
+ if count > 0 { Some ( table. clone ( ) ) } else { None }
4611
+ } ,
4612
+ )
4613
+ . collect ( )
4614
+ }
4615
+
4616
+ /// Get all table names that were checked.
4617
+ fn tables_checked ( & self ) -> BTreeSet < & str > {
4618
+ self . counts . keys ( ) . map ( |s| s. as_str ( ) ) . collect ( )
4619
+ }
4620
+
4621
+ /// Verify no new blueprint tables were added without updating this function.
4622
+ async fn verify_all_tables_covered (
4623
+ & self ,
4624
+ datastore : & DataStore ,
4625
+ ) -> Result < ( ) , String > {
4626
+ let conn = datastore. pool_connection_for_tests ( ) . await . unwrap ( ) ;
4627
+
4628
+ // Tables prefixed with `bp_` that are *not* specific to a single blueprint
4629
+ // and therefore intentionally ignored. There is only one of these right now.
4630
+ let tables_ignored: BTreeSet < _ > =
4631
+ [ "bp_target" ] . into_iter ( ) . collect ( ) ;
4632
+ let tables_checked = self . tables_checked ( ) ;
4633
+
4634
+ let mut query = QueryBuilder :: new ( ) ;
4635
+ query. sql (
4636
+ "SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'bp\\ _%'" ,
4637
+ ) ;
4638
+ let tables_unchecked: Vec < String > = query
4639
+ . query :: < diesel:: sql_types:: Text > ( )
4640
+ . load_async ( & * conn)
4641
+ . await
4642
+ . expect ( "Failed to query information_schema for tables" )
4643
+ . into_iter ( )
4644
+ . filter ( |f : & String | {
4645
+ let t = f. as_str ( ) ;
4646
+ !tables_ignored. contains ( t) && !tables_checked. contains ( t)
4647
+ } )
4648
+ . collect ( ) ;
4649
+
4650
+ if !tables_unchecked. is_empty ( ) {
4651
+ Err ( format ! (
4652
+ "found blueprint-related table(s) not covered by BlueprintTableCounts: {}\n \n \
4653
+ If you see this message, you probably added a blueprint table whose name started with `bp_*`. \
4654
+ Add it to the query list in BlueprintTableCounts::new() so that this function checks the table. \
4655
+ You may also need to update blueprint deletion/insertion code to handle rows in that table.",
4656
+ tables_unchecked. join( ", " )
4657
+ ) )
4658
+ } else {
4659
+ Ok ( ( ) )
4660
+ }
4661
+ }
4662
+ }
4663
+
4664
+ // Verify that every blueprint-related table contains ≥1 row for `blueprint_id`.
4665
+ // Complements `ensure_blueprint_fully_deleted`.
4666
+ async fn ensure_blueprint_fully_populated (
4667
+ datastore : & DataStore ,
4668
+ blueprint_id : BlueprintUuid ,
4669
+ ) {
4670
+ let counts = BlueprintTableCounts :: new ( datastore, blueprint_id) . await ;
4671
+
4672
+ // Exception tables that may be empty in the representative blueprint:
4673
+ // - MGS update tables: only populated when blueprint includes firmware updates
4674
+ // - ClickHouse tables: only populated when blueprint includes ClickHouse configuration
4675
+ let exception_tables = [
4676
+ "bp_pending_mgs_update_sp" ,
4677
+ "bp_pending_mgs_update_rot" ,
4678
+ "bp_pending_mgs_update_rot_bootloader" ,
4679
+ "bp_pending_mgs_update_host_phase_1" ,
4680
+ "bp_clickhouse_cluster_config" ,
4681
+ "bp_clickhouse_keeper_zone_id_to_node_id" ,
4682
+ "bp_clickhouse_server_zone_id_to_node_id" ,
4683
+ ] ;
4684
+
4685
+ // Check that all non-exception tables have at least one row
4686
+ let empty_tables = counts. empty_tables ( ) ;
4687
+ let problematic_tables: Vec < _ > = empty_tables
4688
+ . into_iter ( )
4689
+ . filter ( |table| !exception_tables. contains ( & table. as_str ( ) ) )
4690
+ . collect ( ) ;
4691
+
4692
+ if !problematic_tables. is_empty ( ) {
4693
+ panic ! (
4694
+ "Expected tables to be populated for blueprint {blueprint_id}: {:?}\n \n \
4695
+ If every blueprint should be expected to have a value in this table, then this is a bug. \
4696
+ Otherwise, you may need to add a table to the exception list in `ensure_blueprint_fully_populated()`. \
4697
+ If you do this, please ensure that you add a test to `test_representative_blueprint()` that creates a \
4698
+ blueprint that _does_ populate this table and verifies it.",
4699
+ problematic_tables
4700
+ ) ;
4701
+ }
4702
+ }
4586
4703
}
0 commit comments