@@ -140,6 +140,22 @@ pub trait RestrictedPathsManifestIdStore: Send + Sync {
140140 // TODO(T239041722): add limit
141141 ) -> Result < Vec < RestrictedPathManifestIdEntry > > ;
142142
143+ /// Get all entries in this repo matching a manifest id, regardless of
144+ /// manifest type. Returns the manifest type and path of each matching entry.
145+ async fn get_all_paths_by_manifest_id (
146+ & self ,
147+ ctx : & CoreContext ,
148+ manifest_id : & ManifestId ,
149+ ) -> Result < Vec < ( ManifestType , NonRootMPath ) > > ;
150+
151+ /// Delete all entries in this repo matching a manifest id, regardless of
152+ /// manifest type. Returns the number of rows deleted.
153+ async fn delete_by_manifest_id (
154+ & self ,
155+ ctx : & CoreContext ,
156+ manifest_id : & ManifestId ,
157+ ) -> Result < u64 > ;
158+
143159 fn repo_id ( & self ) -> RepositoryId ;
144160}
145161
@@ -186,6 +202,18 @@ mononoke_queries! {
186202 "
187203 }
188204
205+ read SelectByManifestId (
206+ repo_id: RepositoryId ,
207+ manifest_id: ManifestId ,
208+ ) -> ( ManifestType , NonRootMPath ) {
209+ "SELECT manifest_type, path FROM restricted_paths_manifest_ids WHERE repo_id = {repo_id} AND manifest_id = {manifest_id}"
210+ }
211+
212+ write DeleteByManifestId ( repo_id: RepositoryId , manifest_id: ManifestId ) {
213+ none,
214+ "DELETE FROM restricted_paths_manifest_ids WHERE repo_id = {repo_id} AND manifest_id = {manifest_id}"
215+ }
216+
189217}
190218
191219pub struct SqlRestrictedPathsManifestIdStore {
@@ -278,6 +306,38 @@ impl RestrictedPathsManifestIdStore for SqlRestrictedPathsManifestIdStore {
278306 . collect :: < Result < _ > > ( )
279307 }
280308
309+ async fn get_all_paths_by_manifest_id (
310+ & self ,
311+ ctx : & CoreContext ,
312+ manifest_id : & ManifestId ,
313+ ) -> Result < Vec < ( ManifestType , NonRootMPath ) > > {
314+ let rows = SelectByManifestId :: query (
315+ & self . connections . read_connection ,
316+ ctx. sql_query_telemetry ( ) ,
317+ & self . repo_id ,
318+ manifest_id,
319+ )
320+ . await ?;
321+
322+ Ok ( rows)
323+ }
324+
325+ async fn delete_by_manifest_id (
326+ & self ,
327+ ctx : & CoreContext ,
328+ manifest_id : & ManifestId ,
329+ ) -> Result < u64 > {
330+ let result = DeleteByManifestId :: query (
331+ & self . connections . write_connection ,
332+ ctx. sql_query_telemetry ( ) ,
333+ & self . repo_id ,
334+ manifest_id,
335+ )
336+ . await ?;
337+
338+ Ok ( result. affected_rows ( ) )
339+ }
340+
281341 fn repo_id ( & self ) -> RepositoryId {
282342 self . repo_id
283343 }
@@ -467,3 +527,119 @@ fn fmt_path_bytes(path: &PathBytes, f: &mut fmt::Formatter) -> fmt::Result {
467527fn fmt_path_hash_bytes ( path_hash : & PathHashBytes , f : & mut fmt:: Formatter ) -> fmt:: Result {
468528 write ! ( f, "\" {}\" " , hex:: encode( & path_hash. 0 ) )
469529}
530+
531+ #[ cfg( test) ]
532+ mod tests {
533+ use fbinit:: FacebookInit ;
534+ use mononoke_macros:: mononoke;
535+
536+ use super :: * ;
537+
538+ fn manifest_id_from ( bytes : & [ u8 ] ) -> ManifestId {
539+ ManifestId :: new ( SmallVec :: from_slice ( bytes) )
540+ }
541+
542+ fn entry (
543+ manifest_type : ManifestType ,
544+ manifest_id : & ManifestId ,
545+ path : & str ,
546+ ) -> RestrictedPathManifestIdEntry {
547+ RestrictedPathManifestIdEntry :: new (
548+ manifest_type,
549+ manifest_id. clone ( ) ,
550+ RepoPath :: dir ( NonRootMPath :: new ( path) . expect ( "valid path" ) )
551+ . expect ( "valid directory repo path" ) ,
552+ )
553+ . expect ( "valid entry" )
554+ }
555+
556+ #[ mononoke:: fbinit_test]
557+ async fn test_get_and_delete_by_manifest_id_per_repo ( fb : FacebookInit ) -> Result < ( ) > {
558+ let ctx = CoreContext :: test_mock ( fb) ;
559+
560+ // Build the first store via the builder so it owns an in-memory sqlite
561+ // DB, then build a second store for a different repo that shares the
562+ // SAME connections so both repos hit the same underlying DB. This lets
563+ // us assert that the per-repo operations only touch their own repo.
564+ let builder = SqlRestrictedPathsManifestIdStoreBuilder :: with_sqlite_in_memory ( ) ?;
565+ let connections = builder. connections . clone ( ) ;
566+ let store_repo1 = builder. with_repo_id ( RepositoryId :: new ( 1 ) ) ;
567+ let store_repo2 = SqlRestrictedPathsManifestIdStore :: new ( RepositoryId :: new ( 2 ) , connections) ;
568+
569+ let shared_id = manifest_id_from ( & [ 1u8 ; 32 ] ) ;
570+ let control_id = manifest_id_from ( & [ 2u8 ; 32 ] ) ;
571+
572+ // The same manifest id is stored in both repos under two manifest types.
573+ store_repo1
574+ . add_entry ( & ctx, entry ( ManifestType :: Hg , & shared_id, "repo1/hg" ) )
575+ . await ?;
576+ store_repo1
577+ . add_entry (
578+ & ctx,
579+ entry ( ManifestType :: HgAugmented , & shared_id, "repo1/hg_aug" ) ,
580+ )
581+ . await ?;
582+ store_repo2
583+ . add_entry ( & ctx, entry ( ManifestType :: Hg , & shared_id, "repo2/hg" ) )
584+ . await ?;
585+ store_repo2
586+ . add_entry (
587+ & ctx,
588+ entry ( ManifestType :: HgAugmented , & shared_id, "repo2/hg_aug" ) ,
589+ )
590+ . await ?;
591+
592+ // A control entry with a different manifest id must survive deletion.
593+ store_repo1
594+ . add_entry ( & ctx, entry ( ManifestType :: Hg , & control_id, "repo1/control" ) )
595+ . await ?;
596+
597+ // (a) get_all_paths_by_manifest_id is scoped to the store's repo: repo1
598+ // sees only its own two entries, not repo2's.
599+ let found = store_repo1
600+ . get_all_paths_by_manifest_id ( & ctx, & shared_id)
601+ . await ?;
602+ assert_eq ! (
603+ found. len( ) ,
604+ 2 ,
605+ "repo1 should only see its own two entries for the shared manifest id"
606+ ) ;
607+
608+ // (b) delete_by_manifest_id removes only repo1's two entries.
609+ let deleted = store_repo1. delete_by_manifest_id ( & ctx, & shared_id) . await ?;
610+ assert_eq ! ( deleted, 2 , "delete should remove only repo1's two entries" ) ;
611+ assert ! (
612+ store_repo1
613+ . get_all_paths_by_manifest_id( & ctx, & shared_id)
614+ . await ?
615+ . is_empty( ) ,
616+ "no repo1 shared-id entries should remain after deletion"
617+ ) ;
618+
619+ // (c) repo2's entries for the same manifest id are untouched.
620+ assert_eq ! (
621+ store_repo2
622+ . get_all_paths_by_manifest_id( & ctx, & shared_id)
623+ . await ?
624+ . len( ) ,
625+ 2 ,
626+ "repo2's entries must not be affected by deleting from repo1"
627+ ) ;
628+
629+ // (d) a second delete is a no-op.
630+ let deleted_again = store_repo1. delete_by_manifest_id ( & ctx, & shared_id) . await ?;
631+ assert_eq ! ( deleted_again, 0 , "second delete should affect no rows" ) ;
632+
633+ // (e) the control entry with a different id is untouched.
634+ assert_eq ! (
635+ store_repo1
636+ . get_all_paths_by_manifest_id( & ctx, & control_id)
637+ . await ?
638+ . len( ) ,
639+ 1 ,
640+ "the control entry with a different manifest id should remain"
641+ ) ;
642+
643+ Ok ( ( ) )
644+ }
645+ }
0 commit comments