From d76b75656f0a8c4bc62b9a9032f308712acff24a Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Tue, 21 Jul 2026 15:50:48 -0700 Subject: [PATCH 1/5] refactor @sunshowers' limit-reached query into a reusable thing --- .../src/db/check_if_limit_reached.rs | 153 ++++++++++++++++++ .../db-queries/src/db/datastore/deployment.rs | 65 ++------ nexus/db-queries/src/db/mod.rs | 2 + .../app/background/tasks/blueprint_planner.rs | 6 +- 4 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 nexus/db-queries/src/db/check_if_limit_reached.rs diff --git a/nexus/db-queries/src/db/check_if_limit_reached.rs b/nexus/db-queries/src/db/check_if_limit_reached.rs new file mode 100644 index 00000000000..67ef5831131 --- /dev/null +++ b/nexus/db-queries/src/db/check_if_limit_reached.rs @@ -0,0 +1,153 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! A query for checking if the number of records in a table exceeds +//! some limit. + +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::QueryResult; +use diesel::Table; +use diesel::pg::Pg; +use diesel::query_builder::AstPass; +use diesel::query_builder::Query; +use diesel::query_builder::QueryFragment; +use diesel::query_builder::QueryId; +use diesel::query_source::QuerySource; +use diesel::result::Error as DieselError; +use diesel::sql_types; +use nexus_db_lookup::DbConnection; +use omicron_common::api::external::Error; + +/// A query for (semi-)efficiently checking if the number of rows in a [`Table`] +/// exceeds a provided limit. +pub struct LimitQuery { + limit: i64, + from: ::FromClause, +} + +#[derive(Copy, Clone, Debug)] +pub enum IsLimitReached { + Yes, + No { count: u64 }, +} + +impl LimitQuery +where + T: Table, + ::FromClause: QueryFragment, +{ + pub fn new(table: T, limit: u64) -> Result { + let limit = i64::try_from(limit).map_err(|e| { + Error::invalid_value( + "limit", + format!("limit cannot be converted to i64: {e}"), + ) + })?; + Ok(Self { limit, from: table.from_clause() }) + } + + /// Check if the number of rows in the table exceeds the limit. + /// + /// # Usage Notes + /// + /// This will perform a "full-table scan", except for the fact that...it + /// doesn't actually scan the entire table, due to the use of a `LIMIT` + /// clause. However, CRDB is not smart enough to realize this, and will fail + /// to execute this query unless it is performed in a transaction that runs + /// the `ALLOW_FULL_TABLE_SCAN_SQL`. So, you have to do that for this to + /// work. + pub async fn check_if_limit_reached_async( + self, + conn: &async_bb8_diesel::Connection, + ) -> Result, DieselError> + where + Self: Send + 'static, + { + let limit = u64::try_from(self.limit).expect( + "this i64 was initially converted from a u64, so it should \ + not be negative...", + ); + + // self.first_async fails with `the trait bound + // `TypedSqlQuery: diesel::Table` is not satisfied`. + // So we use load_async, knowing that only one row will be + // returned. + self.load_async::(conn).await.map(|results| { + // There must be exactly one row in the returned result. + let count = *results.get(0).ok_or_else(|| { + Error::internal_error( + "check_if_limit_reached query returned no values", + ) + })?; + let count = + u64::try_from(count).map_err(|_| Error::InternalError { + internal_message: format!( + "error converting record count {count} to u64 (how is \ + it negative?)" + ), + })?; + + // Note count >= limit (and not count > limit): for a limit of 5000 we + // want to fail if it's reached 5000. + if count >= limit { + Ok(IsLimitReached::Yes) + } else { + Ok(IsLimitReached::No { count }) + } + }) + } +} + +impl QueryFragment for LimitQuery +where + T: Table, + ::FromClause: QueryFragment, +{ + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.push_sql( + "SELECT COUNT(*) FROM (\ + SELECT 1 FROM ", + ); + self.from.walk_ast(out.reborrow())?; + out.push_sql(" LIMIT "); + out.push_bind_param::(&self.limit)?; + out.push_sql(")"); + + Ok(()) + } +} + +impl Query for LimitQuery +where + T: Table, + ::FromClause: QueryFragment, +{ + type SqlType = sql_types::BigInt; +} + +impl QueryId for LimitQuery +where + T: Table, + ::FromClause: QueryFragment, +{ + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; +} + +impl diesel::RunQueryDsl for LimitQuery +where + T: Table, + ::FromClause: QueryFragment, +{ +} + +impl Clone for LimitQuery +where + T: Table, + ::FromClause: Clone, +{ + fn clone(&self) -> Self { + Self { from: self.from.clone(), limit: self.limit } + } +} diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 9068cdad70f..8bdb6e30814 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -620,28 +620,26 @@ impl DataStore { &self, opctx: &OpContext, limit: u64, - ) -> Result { + ) -> Result { + use nexus_db_schema::schema::blueprint::dsl; + // The "full" table scan below is treated as a complex operation. (This // should only be called from the blueprint planner background task, // for which complex operations are allowed.) opctx.check_complex_operations_allowed()?; opctx.authorize(authz::Action::Read, &authz::BLUEPRINT_CONFIG).await?; + let limit_query = crate::db::check_if_limit_reached::LimitQuery::new( + dsl::blueprint, + limit, + )?; let conn = self.pool_connection_authorized(opctx).await?; - let limit_i64 = i64::try_from(limit).map_err(|e| { - Error::invalid_value( - "limit", - format!("limit cannot be converted to i64: {e}"), - ) - })?; - let err = OptionalError::new(); - let count = self - .transaction_retry_wrapper("blueprint_count") + self.transaction_retry_wrapper("blueprint_count") .transaction(&conn, |conn| { let err = err.clone(); - + let limit_query = limit_query.clone(); async move { // We need this to call the COUNT(*) query below. But note // that this isn't really a "full" table scan; the number of @@ -652,56 +650,19 @@ impl DataStore { // Rather than doing a full table scan, we use a LIMIT // clause to limit the number of rows returned. - let mut count_star_sql = QueryBuilder::new(); - count_star_sql - .sql( - "SELECT COUNT(*) FROM \ - (SELECT 1 FROM omicron.public.blueprint \ - LIMIT $1)", - ) - .bind::(limit_i64); - - let query = - count_star_sql.query::(); - - // query.first_async fails with `the trait bound - // `TypedSqlQuery: diesel::Table` is not satisfied`. - // So we use load_async, knowing that only one row will be - // returned. - let value = query - .load_async::(&conn) + let result = limit_query + .check_if_limit_reached_async(&conn) .await .map_err(|e| err.bail(TransactionError::Database(e)))?; - Ok(value) + Ok(result) } }) .await .map_err(|e| match err.take() { Some(err) => err.into_public_ignore_retries(), None => public_error_from_diesel(e, ErrorHandler::Server), - })?; - - // There must be exactly one row in the returned result. - let count = *count.get(0).ok_or_else(|| { - Error::internal_error("error getting blueprint count from database") - })?; - - let count = u64::try_from(count).map_err(|_| { - Error::internal_error(&format!( - "error converting blueprint count {} into \ - u64 (how is it negative?)", - count - )) - })?; - - // Note count >= limit (and not count > limit): for a limit of 5000 we - // want to fail if it's reached 5000. - if count >= limit { - Ok(BlueprintLimitReachedOutput::Yes) - } else { - Ok(BlueprintLimitReachedOutput::No { count }) - } + })? } /// Read a complete blueprint from the database diff --git a/nexus/db-queries/src/db/mod.rs b/nexus/db-queries/src/db/mod.rs index 9c0b6723b75..185ffdacdc4 100644 --- a/nexus/db-queries/src/db/mod.rs +++ b/nexus/db-queries/src/db/mod.rs @@ -24,6 +24,7 @@ mod pool; mod pool_connection; // This is marked public because the error types are used elsewhere, e.g., in // sagas. +pub(crate) mod check_if_limit_reached; pub mod queries; mod raw_query_builder; mod sec_store; @@ -44,6 +45,7 @@ pub use nexus_db_fixed_data as fixed_data; pub use nexus_db_model as model; use nexus_db_model::saga_types; +pub use check_if_limit_reached::IsLimitReached; pub use config::Config; pub use datastore::DataStore; pub use on_conflict_ext::IncompleteOnConflictExt; diff --git a/nexus/src/app/background/tasks/blueprint_planner.rs b/nexus/src/app/background/tasks/blueprint_planner.rs index 4c0eead2323..69b023d10b5 100644 --- a/nexus/src/app/background/tasks/blueprint_planner.rs +++ b/nexus/src/app/background/tasks/blueprint_planner.rs @@ -11,8 +11,8 @@ use chrono::Utc; use futures::future::BoxFuture; use nexus_auth::authz; use nexus_db_queries::context::OpContext; +use nexus_db_queries::db; use nexus_db_queries::db::DataStore; -use nexus_db_queries::db::datastore::BlueprintLimitReachedOutput; use nexus_reconfigurator_planning::planner::Planner; use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_reconfigurator_preparation::PlanningInputFromDb; @@ -353,7 +353,7 @@ impl BlueprintPlanner { .check_blueprint_limit_reached(opctx, self.blueprint_limit) .await { - Ok(BlueprintLimitReachedOutput::Yes) => { + Ok(db::IsLimitReached::Yes) => { error!( &opctx.log, "blueprint count at or over limit, not running \ @@ -365,7 +365,7 @@ impl BlueprintPlanner { report: report.clone(), }); } - Ok(BlueprintLimitReachedOutput::No { count }) => count, + Ok(db::IsLimitReached::No { count }) => count, Err(error) => { error!( &opctx.log, From 69c0521c50537cbf7a412f944760312ba1aec6de Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 22 Jul 2026 11:03:16 -0700 Subject: [PATCH 2/5] various suggestions from @sunshowers --- .../src/db/check_if_limit_reached.rs | 25 +++++++++---------- .../db-queries/src/db/datastore/deployment.rs | 6 ----- nexus/db-queries/src/db/mod.rs | 2 +- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/nexus/db-queries/src/db/check_if_limit_reached.rs b/nexus/db-queries/src/db/check_if_limit_reached.rs index 67ef5831131..d2a7143427c 100644 --- a/nexus/db-queries/src/db/check_if_limit_reached.rs +++ b/nexus/db-queries/src/db/check_if_limit_reached.rs @@ -47,7 +47,8 @@ where Ok(Self { limit, from: table.from_clause() }) } - /// Check if the number of rows in the table exceeds the limit. + /// Check if the number of rows in the table has reached or exceeded the + /// limit. /// /// # Usage Notes /// @@ -64,11 +65,9 @@ where where Self: Send + 'static, { - let limit = u64::try_from(self.limit).expect( - "this i64 was initially converted from a u64, so it should \ - not be negative...", - ); - + // Copy this out of `self` first`kq, because `load_async` takes the + // query by value. + let limit = self.limit; // self.first_async fails with `the trait bound // `TypedSqlQuery: diesel::Table` is not satisfied`. // So we use load_async, knowing that only one row will be @@ -80,19 +79,19 @@ where "check_if_limit_reached query returned no values", ) })?; - let count = - u64::try_from(count).map_err(|_| Error::InternalError { - internal_message: format!( - "error converting record count {count} to u64 (how is \ - it negative?)" - ), - })?; // Note count >= limit (and not count > limit): for a limit of 5000 we // want to fail if it's reached 5000. if count >= limit { Ok(IsLimitReached::Yes) } else { + let count = + u64::try_from(count).map_err(|_| Error::InternalError { + internal_message: format!( + "error converting record count {count} to u64 (how \ + is it negative?)" + ), + })?; Ok(IsLimitReached::No { count }) } }) diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 8bdb6e30814..9792646a8e3 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -2861,12 +2861,6 @@ async fn insert_pending_mgs_update( Ok(()) } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BlueprintLimitReachedOutput { - No { count: u64 }, - Yes, -} - // Helper to process BpPendingMgsUpdateComponent rows fn process_update_row( row: T, diff --git a/nexus/db-queries/src/db/mod.rs b/nexus/db-queries/src/db/mod.rs index 185ffdacdc4..256255d463a 100644 --- a/nexus/db-queries/src/db/mod.rs +++ b/nexus/db-queries/src/db/mod.rs @@ -4,6 +4,7 @@ //! Facilities for working with the Omicron database +pub(crate) mod check_if_limit_reached; // This is not intended to be public, but this is necessary to use it from // doctests pub mod collection_attach; @@ -24,7 +25,6 @@ mod pool; mod pool_connection; // This is marked public because the error types are used elsewhere, e.g., in // sagas. -pub(crate) mod check_if_limit_reached; pub mod queries; mod raw_query_builder; mod sec_store; From 08c15663585fbfd53a1624dffcb6e9f072ffaab2 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 22 Jul 2026 11:09:01 -0700 Subject: [PATCH 3/5] @sunshowers's error suggestions --- .../src/db/check_if_limit_reached.rs | 48 ++++++++++--------- .../db-queries/src/db/datastore/deployment.rs | 8 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/nexus/db-queries/src/db/check_if_limit_reached.rs b/nexus/db-queries/src/db/check_if_limit_reached.rs index d2a7143427c..3c692581940 100644 --- a/nexus/db-queries/src/db/check_if_limit_reached.rs +++ b/nexus/db-queries/src/db/check_if_limit_reached.rs @@ -14,8 +14,8 @@ use diesel::query_builder::Query; use diesel::query_builder::QueryFragment; use diesel::query_builder::QueryId; use diesel::query_source::QuerySource; -use diesel::result::Error as DieselError; use diesel::sql_types; +use nexus_db_errors::TransactionError; use nexus_db_lookup::DbConnection; use omicron_common::api::external::Error; @@ -61,7 +61,7 @@ where pub async fn check_if_limit_reached_async( self, conn: &async_bb8_diesel::Connection, - ) -> Result, DieselError> + ) -> Result> where Self: Send + 'static, { @@ -72,29 +72,31 @@ where // `TypedSqlQuery: diesel::Table` is not satisfied`. // So we use load_async, knowing that only one row will be // returned. - self.load_async::(conn).await.map(|results| { - // There must be exactly one row in the returned result. - let count = *results.get(0).ok_or_else(|| { - Error::internal_error( - "check_if_limit_reached query returned no values", - ) - })?; + let results = self.load_async::(conn).await?; + + // There must be exactly one row in the returned result. + let count = *results.get(0).ok_or_else(|| { + TransactionError::CustomError(Error::internal_error( + "check_if_limit_reached query returned no values", + )) + })?; - // Note count >= limit (and not count > limit): for a limit of 5000 we - // want to fail if it's reached 5000. - if count >= limit { - Ok(IsLimitReached::Yes) - } else { - let count = - u64::try_from(count).map_err(|_| Error::InternalError { - internal_message: format!( - "error converting record count {count} to u64 (how \ + // Note count >= limit (and not count > limit): for a limit of 5000 we + // want to fail if it's reached 5000. + if count >= limit { + Ok(IsLimitReached::Yes) + } else { + let count = u64::try_from(count).map_err(|_| { + let error = Error::InternalError { + internal_message: format!( + "error converting record count {count} to u64 (how \ is it negative?)" - ), - })?; - Ok(IsLimitReached::No { count }) - } - }) + ), + }; + TransactionError::CustomError(error) + })?; + Ok(IsLimitReached::No { count }) + } } } diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 9792646a8e3..e7c3cdc8ab2 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -644,16 +644,14 @@ impl DataStore { // We need this to call the COUNT(*) query below. But note // that this isn't really a "full" table scan; the number of // rows scanned is limited by the LIMIT clause. - conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL) - .await - .map_err(|e| err.bail(TransactionError::Database(e)))?; + conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL).await?; // Rather than doing a full table scan, we use a LIMIT // clause to limit the number of rows returned. let result = limit_query .check_if_limit_reached_async(&conn) .await - .map_err(|e| err.bail(TransactionError::Database(e)))?; + .map_err(|e| e.into_diesel(&err))?; Ok(result) } @@ -662,7 +660,7 @@ impl DataStore { .map_err(|e| match err.take() { Some(err) => err.into_public_ignore_retries(), None => public_error_from_diesel(e, ErrorHandler::Server), - })? + }) } /// Read a complete blueprint from the database From 6ddb5c437762288dcec21b9ab3996bb9afa65ce2 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 22 Jul 2026 11:16:37 -0700 Subject: [PATCH 4/5] oops i forgot to delete this --- nexus/db-queries/src/db/datastore/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index f2cf1cf4e3f..72ff32be966 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -160,7 +160,6 @@ pub use alert::AlertFilters; pub use alert::FmRendezvousAlertCreateError; pub use db_metadata::DatastoreSetupAction; pub use db_metadata::ValidatedDatastoreSetupAction; -pub use deployment::BlueprintLimitReachedOutput; pub use deployment::ExternalServiceNetworkingConfig; pub use disk::CrucibleDisk; pub use disk::Disk; From 24719b0281eca143b5b490efa618c11e7fdb11c8 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 22 Jul 2026 12:26:59 -0700 Subject: [PATCH 5/5] throw together a test --- .../src/db/check_if_limit_reached.rs | 183 +++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/nexus/db-queries/src/db/check_if_limit_reached.rs b/nexus/db-queries/src/db/check_if_limit_reached.rs index 3c692581940..354a4d66dfc 100644 --- a/nexus/db-queries/src/db/check_if_limit_reached.rs +++ b/nexus/db-queries/src/db/check_if_limit_reached.rs @@ -26,7 +26,7 @@ pub struct LimitQuery { from: ::FromClause, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IsLimitReached { Yes, No { count: u64 }, @@ -152,3 +152,184 @@ where Self { from: self.from.clone(), limit: self.limit } } } + +#[cfg(test)] +mod tests { + use super::IsLimitReached; + use super::LimitQuery; + use crate::db::DataStore; + use crate::db::pub_test_utils::TestDatabase; + use crate::db::queries::ALLOW_FULL_TABLE_SCAN_SQL; + use async_bb8_diesel::AsyncRunQueryDsl; + use async_bb8_diesel::AsyncSimpleConnection; + use diesel::prelude::*; + use omicron_test_utils::dev; + use uuid::Uuid; + + use std::sync::Arc; + + table! { + test_limit_items (id) { + id -> Uuid, + } + } + + #[derive(Clone, Insertable)] + #[diesel(table_name = test_limit_items)] + struct Item { + id: Uuid, + } + + async fn create_table(datastore: &DataStore) { + let conn = datastore.pool_connection_for_tests().await.unwrap(); + (*conn) + .batch_execute_async( + "CREATE TABLE test_limit_items (id UUID PRIMARY KEY);", + ) + .await + .unwrap(); + } + + struct Test { + ids: Vec, + datastore: Arc, + } + + impl Test { + /// Insert `n` new rows, recording their ids in `ids` so they can later be + /// deleted by primary key (which avoids a full table scan). + async fn insert_items(&mut self, n: usize) { + let len = self.ids.len(); + let conn = + self.datastore.pool_connection_for_tests().await.unwrap(); + let new: Vec = + (0..n).map(|_| Item { id: Uuid::new_v4() }).collect(); + let inserted = diesel::insert_into(test_limit_items::table) + .values(new.clone()) + .returning(test_limit_items::id) + .get_results_async::(&*conn) + .await + .unwrap(); + eprintln!("[{len}].insert({n}) -> {inserted:?})"); + self.ids.extend(inserted); + assert_eq!(self.ids.len(), len + n); + } + + /// Delete `n` rows by primary key. + async fn delete_items(&mut self, n: usize) { + let conn = + self.datastore.pool_connection_for_tests().await.unwrap(); + let len = self.ids.len(); + let to_delete = self.ids.split_off(len - n); + let deleted = diesel::delete( + test_limit_items::table + .filter(test_limit_items::id.eq_any(to_delete.clone())), + ) + .execute_async(&*conn) + .await + .unwrap(); + assert_eq!(deleted, n, "expected to delete exactly {n} rows"); + eprintln!("[{len}].delete({n}) -> {to_delete:?})"); + } + + /// Run the limit query against `test_limit_items` in a transaction, the + /// same way it would be used with a real table. + async fn check_limit(&mut self, limit: u64) -> IsLimitReached { + let len = self.ids.len(); + let conn = + self.datastore.pool_connection_for_tests().await.unwrap(); + let result = self + .datastore + .transaction_non_retry_wrapper("test_check_if_limit_reached") + .transaction(&conn, move |conn| async move { + // The query performs a (bounded) full table scan, which CRDB + // refuses to run unless it's been explicitly allowed for the + // transaction. + conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL).await?; + let query = LimitQuery::new(test_limit_items::table, limit) + .expect("limit should convert to i64"); + query.check_if_limit_reached_async(&conn).await + }) + .await + .expect("check_if_limit_reached query should succeed"); + eprintln!("[{len}].check_if_limit_reached({limit}) -> {result:?}"); + result + } + } + + #[track_caller] + fn assert_reached(result: IsLimitReached) { + assert_eq!( + result, + IsLimitReached::Yes, + "expected limit to be reached, but query returned {result:?}" + ); + } + + #[track_caller] + fn assert_not_reached(result: IsLimitReached, expected_count: u64) { + assert_eq!( + result, + IsLimitReached::No { count: expected_count }, + "expected limit not to be reached (count {expected_count}), \ + but query returned {result:?}" + ); + } + + // XXX(eliza): note that this is more or less a property test, and could + // probably be rewritten as one pretty easily. + #[tokio::test] + async fn test_check_if_limit_reached() { + let logctx = dev::test_setup_log("test_check_if_limit_reached"); + let db = TestDatabase::new_with_raw_datastore(&logctx.log).await; + let datastore = db.datastore(); + + create_table(&datastore).await; + + let mut test = Test { datastore: datastore.clone(), ids: Vec::new() }; + + // Empty table. Note that a table is always "at" a limit of zero, even + // when empty. + assert_reached(test.check_limit(0).await); + assert_not_reached(test.check_limit(1).await, 0); + assert_not_reached(test.check_limit(5).await, 0); + + // 3 rows. + test.insert_items(3).await; + assert_not_reached(test.check_limit(5).await, 3); + assert_not_reached(test.check_limit(4).await, 3); + assert_reached(test.check_limit(3).await); // Exactly the limit + assert_reached(test.check_limit(2).await); + assert_reached(test.check_limit(1).await); + + // 5 rows + test.insert_items(2).await; + assert_eq!(test.ids.len(), 5); + assert_reached(test.check_limit(5).await); + assert_not_reached(test.check_limit(6).await, 5); + assert_not_reached(test.check_limit(100).await, 5); + + // 15 rows + test.insert_items(10).await; + assert_eq!(test.ids.len(), 15); + assert_reached(test.check_limit(5).await); + assert_reached(test.check_limit(15).await); + assert_not_reached(test.check_limit(16).await, 15); + + // 1 row (delete some records) + test.delete_items(14).await; + assert_eq!(test.ids.len(), 1); + assert_not_reached(test.check_limit(5).await, 1); + assert_reached(test.check_limit(1).await); + assert_not_reached(test.check_limit(2).await, 1); + + // Empty the table again. + test.delete_items(1).await; + assert!(test.ids.is_empty()); + assert_not_reached(test.check_limit(1).await, 0); + assert_not_reached(test.check_limit(5).await, 0); + + db.terminate().await; + logctx.cleanup_successful(); + } +}