Files
zhenyi 15b875e18d perf(issues): replace N+1 queries with batch operations
- Add Repo::find_by_ids() batch query using WHERE id = ANY($1)
- Replace 3 sequential validation loops (repos, labels, assignees)
  with batch queries using ANY($1)
- Replace 3 sequential INSERT loops with single INSERT...SELECT
  FROM unnest() statements
- Extract 7 helper methods: validate_issue_repos,
  validate_issue_labels, validate_issue_assignees,
  validate_issue_milestone, insert_issue_repo_relations,
  insert_issue_label_relations, insert_issue_assignees
- Reduce issue_create() from ~243 lines to ~80 lines
2026-06-10 18:49:00 +08:00

126 lines
4.2 KiB
Rust

//! SQL query methods for the `Repo` entity.
use sqlx::PgPool;
use uuid::Uuid;
use super::repo::Repo;
use crate::models::common::{Role, Visibility};
impl Repo {
/// Find a non-deleted repo by primary key.
pub async fn find_by_name(
pool: &PgPool,
workspace_id: Uuid,
name: &str,
) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as::<_, Repo>(
"SELECT id, workspace_id, owner_id, name, description, default_branch, visibility, \
status, is_fork, forked_from_repo_id, storage_node_ids, \
primary_storage_node_id, storage_path, git_service, \
archived_at, created_at, updated_at, deleted_at \
FROM repo WHERE workspace_id = $1 AND name = $2 AND deleted_at IS NULL",
)
.bind(workspace_id)
.bind(name)
.fetch_optional(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as::<_, Repo>(
r#"SELECT id, workspace_id, owner_id, name, description, default_branch, visibility,
status, is_fork, forked_from_repo_id, storage_node_ids,
primary_storage_node_id, storage_path, git_service,
archived_at, created_at, updated_at, deleted_at
FROM repo WHERE id = $1 AND deleted_at IS NULL"#,
)
.bind(id)
.fetch_optional(pool)
.await
}
/// Find multiple non-deleted repos by their IDs in a single query.
pub async fn find_by_ids(
pool: &PgPool,
ids: &[Uuid],
) -> Result<Vec<Self>, sqlx::Error> {
if ids.is_empty() {
return Ok(Vec::new());
}
sqlx::query_as::<_, Repo>(
r#"SELECT id, workspace_id, owner_id, name, description, default_branch, visibility,
status, is_fork, forked_from_repo_id, storage_node_ids,
primary_storage_node_id, storage_path, git_service,
archived_at, created_at, updated_at, deleted_at
FROM repo WHERE id = ANY($1) AND deleted_at IS NULL"#,
)
.bind(ids)
.fetch_all(pool)
.await
}
/// Check if a user is an active member of a repo.
pub async fn is_member(
pool: &PgPool,
repo_id: Uuid,
user_id: Uuid,
) -> Result<bool, sqlx::Error> {
sqlx::query_scalar::<_, bool>(
r#"SELECT EXISTS(
SELECT 1 FROM repo_member
WHERE repo_id = $1 AND user_id = $2 AND status = 'active'
)"#,
)
.bind(repo_id)
.bind(user_id)
.fetch_one(pool)
.await
}
/// Get the role of a user in a repo.
/// Returns `Role::Owner` if the user is the repo owner but has no explicit member row.
pub async fn user_role(
pool: &PgPool,
repo_id: Uuid,
user_id: Uuid,
owner_id: Uuid,
) -> Result<Option<Role>, sqlx::Error> {
if owner_id == user_id {
return Ok(Some(Role::Owner));
}
let role_str: Option<String> = sqlx::query_scalar(
r#"SELECT role FROM repo_member
WHERE repo_id = $1 AND user_id = $2 AND status = 'active'"#,
)
.bind(repo_id)
.bind(user_id)
.fetch_optional(pool)
.await?;
Ok(role_str.and_then(|r| r.parse::<Role>().ok()))
}
/// Check if the user can read the repo.
/// Readable based on visibility + workspace membership + repo membership.
pub async fn is_readable(
pool: &PgPool,
repo: &Repo,
user_id: Uuid,
) -> Result<bool, sqlx::Error> {
use crate::models::workspaces::Workspace;
if repo.owner_id == user_id {
return Ok(true);
}
let is_ws_member = Workspace::is_member(pool, repo.workspace_id, user_id).await?;
let is_repo_member = Self::is_member(pool, repo.id, user_id).await?;
Ok(match repo.visibility {
Visibility::Public => true,
Visibility::Internal => is_ws_member,
Visibility::Private => is_ws_member && is_repo_member,
_ => false,
})
}
}