feat(group-quota): per-group shared-collection quotas (M3)

Global env-var ceilings enforced in the group write path (mirrors the
per-value caps): PICLOUD_GROUP_KV_MAX_ROWS / _DOCS_MAX_ROWS (per-group row
count, checked only on a NEW key/doc — updates exempt) and
_FILES_MAX_TOTAL_BYTES (per-group total blob bytes). New group_quota env
helpers; count_rows/total_bytes repo methods (trait-default 0 for non-Postgres);
QuotaExceeded error variants on the three group error enums; a with_max_rows
test builder. Pinned by a group_kv_service unit test (3rd key rejected, update
of an existing key at the cap allowed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 21:17:12 +02:00
parent 9a4b83dfcf
commit 2bc3fb677b
12 changed files with 213 additions and 5 deletions

View File

@@ -61,6 +61,13 @@ pub trait GroupKvRepo: Send + Sync {
cursor: Option<&str>,
limit: u32,
) -> Result<KvListPage, GroupKvRepoError>;
/// §11.6 quota: total key count across all of the group's shared-KV
/// collections. Default `Ok(0)` so non-Postgres impls skip the quota check.
async fn count_rows(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
let _ = group_id;
Ok(0)
}
}
pub struct PostgresGroupKvRepo {
@@ -208,6 +215,15 @@ impl GroupKvRepo for PostgresGroupKvRepo {
Ok(KvListPage { keys, next_cursor })
}
async fn count_rows(&self, group_id: GroupId) -> Result<u64, GroupKvRepoError> {
let (n,): (i64,) =
sqlx::query_as("SELECT COUNT(*) FROM group_kv_entries WHERE group_id = $1")
.bind(group_id.into_inner())
.fetch_one(&self.pool)
.await?;
Ok(u64::try_from(n).unwrap_or(0))
}
}
fn encode_cursor(last_key: &str) -> String {