perf: drop intermediate allocs in WebDAV href encoding; fold group-list COUNT into one query

webdav encode_uri_path runs on every PROPFIND href and did
.map(...).collect::<Vec<_>>().join("/"), allocating a String per segment plus
a joined Vec. Write each utf8_percent_encode Display adapter straight into a
single preallocated String. Behavior is identical (split on '/', encode each
segment, join with '/'), including leading/trailing-slash edge cases.

subject_group list / list_with_counts each issued a second SELECT COUNT(*)
round-trip for the total. Fold it into the page query via COUNT(*) OVER() —
the pattern folder_db_repository already uses — halving the round-trips.
total_count is read from the first row and is 0 on an empty page, matching
folder_db_repository's documented convention.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
This commit is contained in:
Claude
2026-06-09 13:29:05 +00:00
parent f82c5ccf47
commit ec8ddebc30
2 changed files with 58 additions and 72 deletions
@@ -126,30 +126,26 @@ impl SubjectGroupRepository for SubjectGroupPgRepository {
offset: u32,
name_query: Option<&str>,
) -> Result<(Vec<SubjectGroup>, u64), SubjectGroupRepositoryError> {
// Two queries: one for the page, one for the total count. The query
// is small and frequent; a window function would add complexity for
// no measurable win.
let (sql_page, sql_count, pattern) = match name_query {
Some(q) => {
let pat = like_escape(q);
(
"SELECT id, name, description, is_virtual, created_at, updated_at
FROM auth.subject_groups
WHERE name ILIKE $1
ORDER BY is_virtual DESC, name
LIMIT $2 OFFSET $3"
.to_string(),
"SELECT COUNT(*) FROM auth.subject_groups WHERE name ILIKE $1".to_string(),
Some(pat),
)
}
// Single query: the page plus `COUNT(*) OVER()` for the total matching
// count, folding what used to be a separate COUNT round-trip into one.
let (sql_page, pattern) = match name_query {
Some(q) => (
"SELECT id, name, description, is_virtual, created_at, updated_at,
COUNT(*) OVER() AS total_count
FROM auth.subject_groups
WHERE name ILIKE $1
ORDER BY is_virtual DESC, name
LIMIT $2 OFFSET $3"
.to_string(),
Some(like_escape(q)),
),
None => (
"SELECT id, name, description, is_virtual, created_at, updated_at
"SELECT id, name, description, is_virtual, created_at, updated_at,
COUNT(*) OVER() AS total_count
FROM auth.subject_groups
ORDER BY is_virtual DESC, name
LIMIT $1 OFFSET $2"
.to_string(),
"SELECT COUNT(*) FROM auth.subject_groups".to_string(),
None,
),
};
@@ -170,19 +166,9 @@ impl SubjectGroupRepository for SubjectGroupPgRepository {
}
.map_err(|e| Self::map_sqlx_err("list page", e))?;
let total: i64 = if let Some(ref p) = pattern {
sqlx::query_scalar(&sql_count)
.bind(p)
.fetch_one(self.pool.as_ref())
.await
} else {
sqlx::query_scalar(&sql_count)
.fetch_one(self.pool.as_ref())
.await
}
.map_err(|e| Self::map_sqlx_err("list count", e))?;
Ok((rows.iter().map(Self::row_to_group).collect(), total as u64))
// total_count is identical in every row; 0 when the page is empty.
let total = rows.first().map_or(0, |r| r.get::<i64, _>("total_count")) as u64;
Ok((rows.iter().map(Self::row_to_group).collect(), total))
}
async fn list_with_counts(
@@ -191,38 +177,35 @@ impl SubjectGroupRepository for SubjectGroupPgRepository {
offset: u32,
name_query: Option<&str>,
) -> Result<(Vec<(SubjectGroup, i64)>, u64), SubjectGroupRepositoryError> {
// Single SQL: groups + COUNT of direct members per group, via LEFT JOIN
// on `auth.subject_group_members`. No N+1; one round-trip for the
// page, a second for the unfiltered total (matches `list`).
let (sql_page, sql_count, pattern) = match name_query {
Some(q) => {
let pat = like_escape(q);
(
"SELECT g.id, g.name, g.description, g.is_virtual,
g.created_at, g.updated_at,
COUNT(m.group_id) AS member_count
FROM auth.subject_groups g
LEFT JOIN auth.subject_group_members m ON m.group_id = g.id
WHERE g.name ILIKE $1
GROUP BY g.id
ORDER BY g.is_virtual DESC, g.name
LIMIT $2 OFFSET $3"
.to_string(),
"SELECT COUNT(*) FROM auth.subject_groups WHERE name ILIKE $1".to_string(),
Some(pat),
)
}
// Single SQL: groups + per-group member COUNT via LEFT JOIN on
// `auth.subject_group_members`, plus `COUNT(*) OVER()` for the total
// group count. No N+1 and no separate COUNT round-trip — one query.
let (sql_page, pattern) = match name_query {
Some(q) => (
"SELECT g.id, g.name, g.description, g.is_virtual,
g.created_at, g.updated_at,
COUNT(m.group_id) AS member_count,
COUNT(*) OVER() AS total_count
FROM auth.subject_groups g
LEFT JOIN auth.subject_group_members m ON m.group_id = g.id
WHERE g.name ILIKE $1
GROUP BY g.id
ORDER BY g.is_virtual DESC, g.name
LIMIT $2 OFFSET $3"
.to_string(),
Some(like_escape(q)),
),
None => (
"SELECT g.id, g.name, g.description, g.is_virtual,
g.created_at, g.updated_at,
COUNT(m.group_id) AS member_count
COUNT(m.group_id) AS member_count,
COUNT(*) OVER() AS total_count
FROM auth.subject_groups g
LEFT JOIN auth.subject_group_members m ON m.group_id = g.id
GROUP BY g.id
ORDER BY g.is_virtual DESC, g.name
LIMIT $1 OFFSET $2"
.to_string(),
"SELECT COUNT(*) FROM auth.subject_groups".to_string(),
None,
),
};
@@ -243,24 +226,14 @@ impl SubjectGroupRepository for SubjectGroupPgRepository {
}
.map_err(|e| Self::map_sqlx_err("list_with_counts page", e))?;
let total: i64 = if let Some(ref p) = pattern {
sqlx::query_scalar(&sql_count)
.bind(p)
.fetch_one(self.pool.as_ref())
.await
} else {
sqlx::query_scalar(&sql_count)
.fetch_one(self.pool.as_ref())
.await
}
.map_err(|e| Self::map_sqlx_err("list_with_counts total", e))?;
// total_count is identical in every row; 0 when the page is empty.
let total = rows.first().map_or(0, |r| r.get::<i64, _>("total_count")) as u64;
let items = rows
.iter()
.map(|r| (Self::row_to_group(r), r.get::<i64, _>("member_count")))
.collect();
Ok((items, total as u64))
Ok((items, total))
}
async fn count_members(&self, id: Uuid) -> Result<i64, SubjectGroupRepositoryError> {
+17 -4
View File
@@ -63,10 +63,23 @@ fn encode_path_segment(segment: &str) -> String {
/// Percent-encode a full slash-separated path, encoding each segment individually.
pub(crate) fn encode_uri_path(path: &str) -> String {
path.split('/')
.map(encode_path_segment)
.collect::<Vec<_>>()
.join("/")
use std::fmt::Write as _;
// `utf8_percent_encode` returns a `Display` adapter, so write each encoded
// segment straight into `out` — avoids a String per segment and the joined
// Vec the previous `.map(...).collect::<Vec<_>>().join("/")` allocated on
// every PROPFIND href.
let mut out = String::with_capacity(path.len() + 8);
for (i, segment) in path.split('/').enumerate() {
if i > 0 {
out.push('/');
}
let _ = write!(
out,
"{}",
utf8_percent_encode(segment, PATH_SEGMENT_ENCODE_SET)
);
}
out
}
/// Build the `<D:href>` value for a non-collection (file) resource.