feat(db): port git repo reservations to SQLite

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je
2026-08-10 12:11:36 -04:00
committed by Brother Darryl
parent 0cb9897853
commit 6f9241e346
2 changed files with 181 additions and 6 deletions
+30 -4
View File
@@ -4928,7 +4928,12 @@ impl Db {
community: CommunityId,
repo_id: &str,
) -> Result<Option<String>> {
git_repo::repo_name_owner(self.pg_pool()?, community, repo_id).await
match &self.backend {
DbBackend::SQLite(pool) => sqlite::repo_name_owner(pool, community, repo_id).await,
DbBackend::Postgres => {
git_repo::repo_name_owner(self.pg_pool()?, community, repo_id).await
}
}
}
/// Reserve a git repo name for `owner_pubkey` in `community` (NIP-34).
@@ -4941,7 +4946,14 @@ impl Db {
repo_id: &str,
owner_pubkey: &str,
) -> Result<git_repo::ReserveOutcome> {
git_repo::reserve_repo_name(self.pg_pool()?, community, repo_id, owner_pubkey).await
match &self.backend {
DbBackend::SQLite(pool) => {
sqlite::reserve_repo_name(pool, community, repo_id, owner_pubkey).await
}
DbBackend::Postgres => {
git_repo::reserve_repo_name(self.pg_pool()?, community, repo_id, owner_pubkey).await
}
}
}
/// Count git repos reserved by `owner_pubkey` in `community` (quota check).
@@ -4950,7 +4962,14 @@ impl Db {
community: CommunityId,
owner_pubkey: &str,
) -> Result<i64> {
git_repo::count_repos_for_owner(self.pg_pool()?, community, owner_pubkey).await
match &self.backend {
DbBackend::SQLite(pool) => {
sqlite::count_repos_for_owner(pool, community, owner_pubkey).await
}
DbBackend::Postgres => {
git_repo::count_repos_for_owner(self.pg_pool()?, community, owner_pubkey).await
}
}
}
/// Release a git repo name reservation held by `owner_pubkey` (rollback).
@@ -4962,7 +4981,14 @@ impl Db {
repo_id: &str,
owner_pubkey: &str,
) -> Result<u64> {
git_repo::release_repo_name(self.pg_pool()?, community, repo_id, owner_pubkey).await
match &self.backend {
DbBackend::SQLite(pool) => {
sqlite::release_repo_name(pool, community, repo_id, owner_pubkey).await
}
DbBackend::Postgres => {
git_repo::release_repo_name(self.pg_pool()?, community, repo_id, owner_pubkey).await
}
}
}
/// Returns `true` if `pubkey` (64-char hex) is archived in `community_id`.
+151 -2
View File
@@ -230,7 +230,32 @@ pub(crate) async fn migrate(pool: &SqlitePool) -> Result<()> {
tx.commit().await?;
version = 4;
}
if version != 4 {
if version < 5 {
let mut tx = pool.begin().await?;
for statement in [
"DROP TRIGGER IF EXISTS events_fts_insert",
"DROP TRIGGER IF EXISTS events_fts_delete",
"DROP TRIGGER IF EXISTS events_fts_update",
"DROP TABLE IF EXISTS events_fts",
"DROP VIEW IF EXISTS searchable_events",
"CREATE VIEW searchable_events AS SELECT rowid, content FROM events WHERE kind IN (0, 9, 40002, 45001, 45003)",
"CREATE VIRTUAL TABLE events_fts USING fts5(content, content='searchable_events', content_rowid='rowid', tokenize='unicode61')",
"INSERT INTO events_fts(events_fts) VALUES ('rebuild')",
"CREATE TRIGGER events_fts_insert AFTER INSERT ON events WHEN new.kind IN (0, 9, 40002, 45001, 45003) BEGIN INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content); END",
"CREATE TRIGGER events_fts_delete AFTER DELETE ON events WHEN old.kind IN (0, 9, 40002, 45001, 45003) BEGIN INSERT INTO events_fts(events_fts, rowid, content) VALUES ('delete', old.rowid, old.content); END",
"CREATE TRIGGER events_fts_update AFTER UPDATE OF content, kind ON events BEGIN INSERT INTO events_fts(events_fts, rowid, content) SELECT 'delete', old.rowid, old.content WHERE old.kind IN (0, 9, 40002, 45001, 45003); INSERT INTO events_fts(rowid, content) SELECT new.rowid, new.content WHERE new.kind IN (0, 9, 40002, 45001, 45003); END",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_channels_dm_hash ON channels (community_id, participant_hash) WHERE channel_type = 'dm' AND deleted_at IS NULL",
"CREATE TABLE IF NOT EXISTS git_repo_names (community_id TEXT NOT NULL, repo_id TEXT NOT NULL, owner_pubkey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()), PRIMARY KEY (community_id, repo_id), FOREIGN KEY (community_id) REFERENCES communities(id) ON DELETE CASCADE)",
] {
sqlx::query(statement).execute(&mut *tx).await?;
}
sqlx::query("UPDATE schema_version SET version = 5 WHERE singleton = 1")
.execute(&mut *tx)
.await?;
tx.commit().await?;
version = 5;
}
if version != 5 {
return Err(crate::DbError::InvalidData(format!(
"unsupported SQLite schema version {version}"
)));
@@ -1017,6 +1042,68 @@ pub(crate) async fn list_hidden_dms(
.collect()
}
pub(crate) async fn repo_name_owner(
pool: &SqlitePool,
community: CommunityId,
repo_id: &str,
) -> Result<Option<String>> {
Ok(sqlx::query_scalar(
"SELECT owner_pubkey FROM git_repo_names WHERE community_id = ?1 AND repo_id = ?2",
)
.bind(community.as_uuid().to_string())
.bind(repo_id)
.fetch_optional(pool)
.await?)
}
pub(crate) async fn reserve_repo_name(
pool: &SqlitePool,
community: CommunityId,
repo_id: &str,
owner_pubkey: &str,
) -> Result<crate::git_repo::ReserveOutcome> {
let inserted = sqlx::query("INSERT INTO git_repo_names (community_id,repo_id,owner_pubkey) VALUES (?1,?2,?3) ON CONFLICT (community_id,repo_id) DO NOTHING")
.bind(community.as_uuid().to_string()).bind(repo_id).bind(owner_pubkey).execute(pool).await?.rows_affected();
if inserted != 0 {
return Ok(crate::git_repo::ReserveOutcome::Reserved);
}
match repo_name_owner(pool, community, repo_id).await? {
Some(holder) if holder == owner_pubkey => Ok(crate::git_repo::ReserveOutcome::AlreadyOwned),
_ => Ok(crate::git_repo::ReserveOutcome::TakenByOther),
}
}
pub(crate) async fn count_repos_for_owner(
pool: &SqlitePool,
community: CommunityId,
owner_pubkey: &str,
) -> Result<i64> {
Ok(sqlx::query_scalar(
"SELECT COUNT(*) FROM git_repo_names WHERE community_id = ?1 AND owner_pubkey = ?2",
)
.bind(community.as_uuid().to_string())
.bind(owner_pubkey)
.fetch_one(pool)
.await?)
}
pub(crate) async fn release_repo_name(
pool: &SqlitePool,
community: CommunityId,
repo_id: &str,
owner_pubkey: &str,
) -> Result<u64> {
Ok(sqlx::query(
"DELETE FROM git_repo_names WHERE community_id = ?1 AND repo_id = ?2 AND owner_pubkey = ?3",
)
.bind(community.as_uuid().to_string())
.bind(repo_id)
.bind(owner_pubkey)
.execute(pool)
.await?
.rows_affected())
}
pub(crate) async fn is_archived(
pool: &SqlitePool,
community: CommunityId,
@@ -1642,7 +1729,7 @@ mod tests {
.fetch_one(&upgraded)
.await
.unwrap(),
4
5
);
for (table, column) in [
("channels", "participant_hash"),
@@ -1732,6 +1819,68 @@ mod tests {
.is_err());
}
#[tokio::test]
async fn git_repo_names_are_scoped_idempotent_and_owner_released() {
let pool = connect("sqlite::memory:").await.unwrap();
let a = ensure_configured_community(&pool, "git-a.example")
.await
.unwrap()
.id;
let b = ensure_configured_community(&pool, "git-b.example")
.await
.unwrap()
.id;
assert_eq!(
reserve_repo_name(&pool, a, "project", "alice")
.await
.unwrap(),
crate::git_repo::ReserveOutcome::Reserved
);
assert_eq!(
reserve_repo_name(&pool, a, "project", "alice")
.await
.unwrap(),
crate::git_repo::ReserveOutcome::AlreadyOwned
);
assert_eq!(
reserve_repo_name(&pool, a, "project", "bob").await.unwrap(),
crate::git_repo::ReserveOutcome::TakenByOther
);
assert_eq!(
reserve_repo_name(&pool, b, "project", "bob").await.unwrap(),
crate::git_repo::ReserveOutcome::Reserved
);
assert_eq!(
repo_name_owner(&pool, a, "project")
.await
.unwrap()
.as_deref(),
Some("alice")
);
assert_eq!(count_repos_for_owner(&pool, a, "alice").await.unwrap(), 1);
assert_eq!(
release_repo_name(&pool, a, "project", "bob").await.unwrap(),
0
);
assert_eq!(
release_repo_name(&pool, a, "project", "alice")
.await
.unwrap(),
1
);
assert!(repo_name_owner(&pool, a, "project")
.await
.unwrap()
.is_none());
assert_eq!(
repo_name_owner(&pool, b, "project")
.await
.unwrap()
.as_deref(),
Some("bob")
);
}
#[tokio::test]
async fn archived_identities_are_idempotent_and_community_scoped() {
let pool = connect("sqlite::memory:").await.unwrap();