@@ -823,6 +823,25 @@ const MIGRATIONS: &[Migration] = &[
823823 "ALTER TABLE repos ADD COLUMN IF NOT EXISTS quarantined BOOLEAN NOT NULL DEFAULT FALSE" ,
824824 ] ,
825825 } ,
826+ Migration {
827+ version : 10 ,
828+ name : "ref_cert_unique_per_ref" ,
829+ stmts : & [
830+ // Dedup before the unique index: keep only the most recent row per
831+ // (repo_id, ref_name) so the CREATE UNIQUE INDEX below does not fail
832+ // on existing databases that accumulated duplicates.
833+ r#"DELETE FROM ref_certificates
834+ WHERE id IN (
835+ SELECT id FROM (
836+ SELECT id, ROW_NUMBER() OVER (
837+ PARTITION BY repo_id, ref_name ORDER BY issued_at DESC
838+ ) AS rn
839+ FROM ref_certificates
840+ ) dups WHERE dups.rn > 1
841+ )"# ,
842+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_ref_certs_repo_ref ON ref_certificates(repo_id, ref_name)" ,
843+ ] ,
844+ } ,
826845] ;
827846
828847// ── Repos ─────────────────────────────────────────────────────────────────────
@@ -1909,7 +1928,15 @@ impl Db {
19091928 sqlx:: query (
19101929 "INSERT INTO ref_certificates
19111930 (id, repo_id, ref_name, old_sha, new_sha, pusher_did, node_did, signature, issued_at)
1912- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" ,
1931+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1932+ ON CONFLICT (repo_id, ref_name) DO UPDATE SET
1933+ id = EXCLUDED.id,
1934+ old_sha = EXCLUDED.old_sha,
1935+ new_sha = EXCLUDED.new_sha,
1936+ pusher_did = EXCLUDED.pusher_did,
1937+ node_did = EXCLUDED.node_did,
1938+ signature = EXCLUDED.signature,
1939+ issued_at = EXCLUDED.issued_at" ,
19131940 )
19141941 . bind ( & cert. id )
19151942 . bind ( & cert. repo_id )
@@ -1925,12 +1952,17 @@ impl Db {
19251952 Ok ( ( ) )
19261953 }
19271954
1928- pub async fn list_ref_certificates ( & self , repo_id : & str ) -> Result < Vec < RefCertificate > > {
1955+ pub async fn list_ref_certificates (
1956+ & self ,
1957+ repo_id : & str ,
1958+ limit : i64 ,
1959+ ) -> Result < Vec < RefCertificate > > {
19291960 let rows = sqlx:: query (
19301961 "SELECT id, repo_id, ref_name, old_sha, new_sha, pusher_did, node_did, signature, issued_at
1931- FROM ref_certificates WHERE repo_id = $1 ORDER BY issued_at DESC" ,
1962+ FROM ref_certificates WHERE repo_id = $1 ORDER BY issued_at DESC LIMIT $2 " ,
19321963 )
19331964 . bind ( repo_id)
1965+ . bind ( limit)
19341966 . fetch_all ( & self . pool )
19351967 . await ?;
19361968 Ok ( rows. into_iter ( ) . map ( row_to_cert) . collect ( ) )
@@ -4249,3 +4281,150 @@ mod icaptcha_quarantine_tests {
42494281 assert ! ( with_stars. iter( ) . all( |( r, _) | r. name != "spam" ) ) ;
42504282 }
42514283}
4284+
4285+ #[ cfg( test) ]
4286+ mod ref_certificate_tests {
4287+ use super :: { Db , RefCertificate , RepoRecord } ;
4288+ use chrono:: Utc ;
4289+ use sqlx:: PgPool ;
4290+
4291+ async fn db ( pool : PgPool ) -> Db {
4292+ let db = Db :: for_testing ( pool) ;
4293+ db. run_migrations ( ) . await . unwrap ( ) ;
4294+ db
4295+ }
4296+
4297+ fn make_cert (
4298+ id : & str ,
4299+ repo_id : & str ,
4300+ ref_name : & str ,
4301+ old_sha : & str ,
4302+ new_sha : & str ,
4303+ issued_at : & str ,
4304+ ) -> RefCertificate {
4305+ RefCertificate {
4306+ id : id. to_string ( ) ,
4307+ repo_id : repo_id. to_string ( ) ,
4308+ ref_name : ref_name. to_string ( ) ,
4309+ old_sha : old_sha. to_string ( ) ,
4310+ new_sha : new_sha. to_string ( ) ,
4311+ pusher_did : "did:key:zPUSHER" . to_string ( ) ,
4312+ node_did : "did:key:zNODE" . to_string ( ) ,
4313+ signature : "sig" . to_string ( ) ,
4314+ issued_at : issued_at. to_string ( ) ,
4315+ }
4316+ }
4317+
4318+ #[ sqlx:: test]
4319+ async fn list_ref_certificates_respects_limit ( pool : PgPool ) {
4320+ let db = db ( pool) . await ;
4321+ let repo_id = uuid:: Uuid :: new_v4 ( ) . to_string ( ) ;
4322+
4323+ // Create a repo to satisfy FK
4324+ db. create_repo ( & RepoRecord {
4325+ id : repo_id. clone ( ) ,
4326+ name : "limit-test" . into ( ) ,
4327+ owner_did : "did:key:zOWNER" . into ( ) ,
4328+ description : None ,
4329+ is_public : true ,
4330+ default_branch : "main" . into ( ) ,
4331+ created_at : Utc :: now ( ) ,
4332+ updated_at : Utc :: now ( ) ,
4333+ disk_path : "/tmp/limit-test" . into ( ) ,
4334+ forked_from : None ,
4335+ machine_id : None ,
4336+ } )
4337+ . await
4338+ . unwrap ( ) ;
4339+
4340+ // Insert 5 certs with descending issued_at
4341+ for i in 0 ..5 {
4342+ db. insert_ref_certificate ( & make_cert (
4343+ & format ! ( "cert-{i}" ) ,
4344+ & repo_id,
4345+ & format ! ( "refs/heads/feature-{i}" ) ,
4346+ "0000" ,
4347+ "1111" ,
4348+ & format ! ( "2026-07-03T20:0{i}:00Z" ) ,
4349+ ) )
4350+ . await
4351+ . unwrap ( ) ;
4352+ }
4353+
4354+ // limit=2 returns only 2
4355+ let certs = db. list_ref_certificates ( & repo_id, 2 ) . await . unwrap ( ) ;
4356+ assert_eq ! ( certs. len( ) , 2 , "LIMIT 2 must return exactly 2 certs" ) ;
4357+ assert_eq ! ( certs[ 0 ] . id, "cert-4" , "most recent first" ) ;
4358+ assert_eq ! ( certs[ 1 ] . id, "cert-3" , "second most recent" ) ;
4359+
4360+ // limit=10 returns all 5 (no padding)
4361+ let all = db. list_ref_certificates ( & repo_id, 10 ) . await . unwrap ( ) ;
4362+ assert_eq ! ( all. len( ) , 5 , "LIMIT >= row count returns all rows" ) ;
4363+ }
4364+
4365+ #[ sqlx:: test]
4366+ async fn insert_ref_certificate_upserts_on_repo_ref ( pool : PgPool ) {
4367+ let db = db ( pool) . await ;
4368+ let repo_id = uuid:: Uuid :: new_v4 ( ) . to_string ( ) ;
4369+
4370+ db. create_repo ( & RepoRecord {
4371+ id : repo_id. clone ( ) ,
4372+ name : "upsert-test" . into ( ) ,
4373+ owner_did : "did:key:zOWNER" . into ( ) ,
4374+ description : None ,
4375+ is_public : true ,
4376+ default_branch : "main" . into ( ) ,
4377+ created_at : Utc :: now ( ) ,
4378+ updated_at : Utc :: now ( ) ,
4379+ disk_path : "/tmp/upsert-test" . into ( ) ,
4380+ forked_from : None ,
4381+ machine_id : None ,
4382+ } )
4383+ . await
4384+ . unwrap ( ) ;
4385+
4386+ // First insert
4387+ db. insert_ref_certificate ( & make_cert (
4388+ "cert-original" ,
4389+ & repo_id,
4390+ "refs/heads/main" ,
4391+ "0000" ,
4392+ "1111" ,
4393+ "2026-07-03T20:00:00Z" ,
4394+ ) )
4395+ . await
4396+ . unwrap ( ) ;
4397+
4398+ // Upsert same ref with new values
4399+ db. insert_ref_certificate ( & make_cert (
4400+ "cert-upserted" ,
4401+ & repo_id,
4402+ "refs/heads/main" ,
4403+ "aaaa" ,
4404+ "bbbb" ,
4405+ "2026-07-03T21:00:00Z" ,
4406+ ) )
4407+ . await
4408+ . unwrap ( ) ;
4409+
4410+ // Only one row exists for this ref
4411+ let certs = db. list_ref_certificates ( & repo_id, 10 ) . await . unwrap ( ) ;
4412+ assert_eq ! ( certs. len( ) , 1 , "upsert must not create a duplicate row" ) ;
4413+ assert_eq ! (
4414+ certs[ 0 ] . id, "cert-upserted" ,
4415+ "upsert must replace the original cert"
4416+ ) ;
4417+ assert_eq ! ( certs[ 0 ] . old_sha, "aaaa" , "old_sha updated" ) ;
4418+ assert_eq ! ( certs[ 0 ] . new_sha, "bbbb" , "new_sha updated" ) ;
4419+ }
4420+
4421+ #[ sqlx:: test]
4422+ async fn list_ref_certificates_empty_repo_returns_empty ( pool : PgPool ) {
4423+ let db = db ( pool) . await ;
4424+ let certs = db
4425+ . list_ref_certificates ( "nonexistent-repo-id" , 10 )
4426+ . await
4427+ . unwrap ( ) ;
4428+ assert ! ( certs. is_empty( ) ) ;
4429+ }
4430+ }
0 commit comments