Skip to content

Commit 9f9fb4b

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 <[email protected]>
1 parent 32e7577 commit 9f9fb4b

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
@@ -922,26 +922,36 @@ impl<'a> CrdbScimProviderStore<'a> {
922922
Ok(convert_to_scim_group(new_group, members))
923923
}
924924

925-
async fn list_groups_in_txn(
925+
async fn list_groups_with_members(
926926
&self,
927927
conn: &async_bb8_diesel::Connection<DbConnection>,
928928
err: OptionalError<ProviderStoreError>,
929929
filter: Option<FilterOp>,
930930
) -> Result<Vec<StoredParts<Group>>, diesel::result::Error> {
931-
use nexus_db_schema::schema::silo_group::dsl;
931+
use nexus_db_schema::schema::silo_group::dsl as group_dsl;
932+
use nexus_db_schema::schema::silo_group_membership::dsl as membership_dsl;
933+
use std::collections::HashMap;
932934

933-
let mut query = dsl::silo_group
934-
.filter(dsl::silo_id.eq(self.authz_silo.id()))
935-
.filter(dsl::user_provision_type.eq(model::UserProvisionType::Scim))
936-
.filter(dsl::time_deleted.is_null())
935+
let mut query = group_dsl::silo_group
936+
.left_join(
937+
membership_dsl::silo_group_membership
938+
.on(group_dsl::id.eq(membership_dsl::silo_group_id)),
939+
)
940+
.filter(group_dsl::silo_id.eq(self.authz_silo.id()))
941+
.filter(
942+
group_dsl::user_provision_type
943+
.eq(model::UserProvisionType::Scim),
944+
)
945+
.filter(group_dsl::time_deleted.is_null())
937946
.into_boxed();
938947

939948
match filter {
940949
Some(FilterOp::DisplayNameEq(display_name)) => {
941950
// displayName is defined as `"caseExact" : false` in RFC 7643,
942951
// section 8.7.1
943952
query = query.filter(
944-
lower(dsl::display_name).eq(lower(display_name.clone())),
953+
lower(group_dsl::display_name)
954+
.eq(lower(display_name.clone())),
945955
);
946956
}
947957

@@ -959,20 +969,60 @@ impl<'a> CrdbScimProviderStore<'a> {
959969
}
960970
}
961971

962-
let groups = query
963-
.select(model::SiloGroup::as_returning())
972+
// Select group fields and optional member user_id
973+
type GroupRow = (model::SiloGroup, Option<Uuid>);
974+
type GroupsMap =
975+
HashMap<Uuid, (Option<model::SiloGroup>, Vec<SiloUserUuid>)>;
976+
977+
let rows: Vec<GroupRow> = query
978+
.select((
979+
model::SiloGroup::as_select(),
980+
membership_dsl::silo_user_id.nullable(),
981+
))
964982
.load_async(conn)
965983
.await?;
966984

967-
let mut returned_groups = Vec::with_capacity(groups.len());
985+
// Group the results by group_id
986+
let mut groups_map: GroupsMap = HashMap::new();
968987

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

9771027
let SiloGroup::Scim(group) = group.into() else {
9781028
// With the user provision type filter, this should never be
@@ -1720,14 +1770,7 @@ impl<'a> ProviderStore for CrdbScimProviderStore<'a> {
17201770
let err: OptionalError<ProviderStoreError> = OptionalError::new();
17211771

17221772
let groups = self
1723-
.datastore
1724-
.transaction_retry_wrapper("scim_list_groups")
1725-
.transaction(&conn, |conn| {
1726-
let err = err.clone();
1727-
let filter = filter.clone();
1728-
1729-
async move { self.list_groups_in_txn(&conn, err, filter).await }
1730-
})
1773+
.list_groups_with_members(&conn, err.clone(), filter)
17311774
.await
17321775
.map_err(|e| {
17331776
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
@@ -2187,7 +2187,9 @@ async fn test_scim_list_users_with_groups(cptestctx: &ControlPlaneTestContext) {
21872187
}
21882188

21892189
#[nexus_test]
2190-
async fn test_scim_list_groups_with_members(cptestctx: &ControlPlaneTestContext) {
2190+
async fn test_scim_list_groups_with_members(
2191+
cptestctx: &ControlPlaneTestContext,
2192+
) {
21912193
let client = &cptestctx.external_client;
21922194
let nexus = &cptestctx.server.server_context().nexus;
21932195
let opctx = OpContext::for_tests(

0 commit comments

Comments
 (0)