Skip to content

Commit 2506dcf

Browse files
david-crespoclaude
andcommitted
rewrite list groups to use a single query
Rewrites list_groups_in_txn as list_groups_with_members to use a single query that fetches both groups and their members in one database roundtrip. This uses a LEFT JOIN to the silo_group_membership table and aggregates the results in application code, similar to the list_users_with_groups implementation. The transaction wrapper is removed since it's no longer needed - the function now performs a single read-only query. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 452064c commit 2506dcf

File tree

2 files changed

+71
-26
lines changed

2 files changed

+71
-26
lines changed

nexus/db-queries/src/db/datastore/scim_provider_store.rs

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -917,26 +917,36 @@ impl<'a> CrdbScimProviderStore<'a> {
917917
Ok(convert_to_scim_group(new_group, members))
918918
}
919919

920-
async fn list_groups_in_txn(
920+
async fn list_groups_with_members(
921921
&self,
922922
conn: &async_bb8_diesel::Connection<DbConnection>,
923923
err: OptionalError<ProviderStoreError>,
924924
filter: Option<FilterOp>,
925925
) -> Result<Vec<StoredParts<Group>>, diesel::result::Error> {
926-
use nexus_db_schema::schema::silo_group::dsl;
926+
use nexus_db_schema::schema::silo_group::dsl as group_dsl;
927+
use nexus_db_schema::schema::silo_group_membership::dsl as membership_dsl;
928+
use std::collections::HashMap;
927929

928-
let mut query = dsl::silo_group
929-
.filter(dsl::silo_id.eq(self.authz_silo.id()))
930-
.filter(dsl::user_provision_type.eq(model::UserProvisionType::Scim))
931-
.filter(dsl::time_deleted.is_null())
930+
let mut query = group_dsl::silo_group
931+
.left_join(
932+
membership_dsl::silo_group_membership
933+
.on(group_dsl::id.eq(membership_dsl::silo_group_id)),
934+
)
935+
.filter(group_dsl::silo_id.eq(self.authz_silo.id()))
936+
.filter(
937+
group_dsl::user_provision_type
938+
.eq(model::UserProvisionType::Scim),
939+
)
940+
.filter(group_dsl::time_deleted.is_null())
932941
.into_boxed();
933942

934943
match filter {
935944
Some(FilterOp::DisplayNameEq(display_name)) => {
936945
// displayName is defined as `"caseExact" : false` in RFC 7643,
937946
// section 8.7.1
938947
query = query.filter(
939-
lower(dsl::display_name).eq(lower(display_name.clone())),
948+
lower(group_dsl::display_name)
949+
.eq(lower(display_name.clone())),
940950
);
941951
}
942952

@@ -954,20 +964,60 @@ impl<'a> CrdbScimProviderStore<'a> {
954964
}
955965
}
956966

957-
let groups = query
958-
.select(model::SiloGroup::as_returning())
967+
// Select group fields and optional member user_id
968+
type GroupRow = (model::SiloGroup, Option<Uuid>);
969+
type GroupsMap =
970+
HashMap<Uuid, (Option<model::SiloGroup>, Vec<SiloUserUuid>)>;
971+
972+
let rows: Vec<GroupRow> = query
973+
.select((
974+
model::SiloGroup::as_select(),
975+
membership_dsl::silo_user_id.nullable(),
976+
))
959977
.load_async(conn)
960978
.await?;
961979

962-
let mut returned_groups = Vec::with_capacity(groups.len());
980+
// Group the results by group_id
981+
let mut groups_map: GroupsMap = HashMap::new();
963982

964-
for group in groups {
965-
let members = self
966-
.get_group_members_for_group_in_txn(
967-
conn,
968-
group.identity.id.into(),
969-
)
970-
.await?;
983+
for (group, maybe_user_id) in rows {
984+
let group_id = group.identity.id.into_untyped_uuid();
985+
986+
let entry = groups_map
987+
.entry(group_id)
988+
.or_insert_with(|| (None, Vec::new()));
989+
990+
// Store the group on first occurrence
991+
if entry.0.is_none() {
992+
entry.0 = Some(group);
993+
}
994+
995+
// If this row has a member, add it to the group's members
996+
if let Some(user_id) = maybe_user_id {
997+
entry.1.push(SiloUserUuid::from_untyped_uuid(user_id));
998+
}
999+
}
1000+
1001+
// Convert to the expected return type
1002+
let mut returned_groups = Vec::with_capacity(groups_map.len());
1003+
1004+
for (_group_id, (maybe_group, members)) in groups_map {
1005+
let group = maybe_group.expect("group should always be present");
1006+
1007+
let members = if members.is_empty() {
1008+
None
1009+
} else {
1010+
let mut id_ord_map = IdOrdMap::with_capacity(members.len());
1011+
for user_id in members {
1012+
id_ord_map
1013+
.insert_unique(GroupMember {
1014+
resource_type: Some(ResourceType::User.to_string()),
1015+
value: Some(user_id.to_string()),
1016+
})
1017+
.expect("user_id should be unique");
1018+
}
1019+
Some(id_ord_map)
1020+
};
9711021

9721022
let SiloGroup::Scim(group) = group.into() else {
9731023
// With the user provision type filter, this should never be
@@ -1723,14 +1773,7 @@ impl<'a> ProviderStore for CrdbScimProviderStore<'a> {
17231773
let err: OptionalError<ProviderStoreError> = OptionalError::new();
17241774

17251775
let groups = self
1726-
.datastore
1727-
.transaction_retry_wrapper("scim_list_groups")
1728-
.transaction(&conn, |conn| {
1729-
let err = err.clone();
1730-
let filter = filter.clone();
1731-
1732-
async move { self.list_groups_in_txn(&conn, err, filter).await }
1733-
})
1776+
.list_groups_with_members(&conn, err.clone(), filter)
17341777
.await
17351778
.map_err(|e| {
17361779
if let Some(e) = err.take() {

nexus/tests/integration_tests/scim.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2184,7 +2184,9 @@ async fn test_scim_list_users_with_groups(cptestctx: &ControlPlaneTestContext) {
21842184
}
21852185

21862186
#[nexus_test]
2187-
async fn test_scim_list_groups_with_members(cptestctx: &ControlPlaneTestContext) {
2187+
async fn test_scim_list_groups_with_members(
2188+
cptestctx: &ControlPlaneTestContext,
2189+
) {
21882190
let client = &cptestctx.external_client;
21892191
let nexus = &cptestctx.server.server_context().nexus;
21902192
let opctx = OpContext::for_tests(

0 commit comments

Comments
 (0)