1use std::collections::BTreeMap;
93use std::sync::atomic::{AtomicU64, Ordering};
94use std::sync::Arc;
95
96use alkgit::StorageError;
97use async_trait::async_trait;
98
99#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct LfsRow {
104 pub repo_id: String,
106 pub oid: String,
109 pub size: u64,
112 pub refcount: u64,
115}
116
117#[async_trait]
126pub trait LfsRows: Send + Sync {
127 async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError>;
132
133 async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError>;
136
137 async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError>;
140
141 async fn rows(&self) -> Result<Vec<LfsRow>, StorageError>;
144
145 async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError>;
147}
148
149fn validated_repo_id(repo_id: &str) -> Result<(), StorageError> {
155 if repo_id.is_empty() {
156 return Err(StorageError::Invalid(
157 "lfs reference assertion: the repo id is empty (the registry id is resolved per \
158 call)"
159 .to_owned(),
160 ));
161 }
162 Ok(())
163}
164
165fn validated_oid(oid: &str) -> Result<(), StorageError> {
166 let hex_ok = oid.len() == 64 && oid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'));
167 if !hex_ok {
168 return Err(StorageError::Invalid(format!(
169 "lfs reference assertion: the oid is not a 64-lowercase-hex sha256 form: {oid:?}"
170 )));
171 }
172 Ok(())
173}
174
175fn validated_size(size: u64) -> Result<(), StorageError> {
176 if size > i64::MAX as u64 {
177 return Err(StorageError::Invalid(format!(
178 "lfs reference assertion: the size exceeds the engine integer range: {size}"
179 )));
180 }
181 Ok(())
182}
183
184pub struct InMemoryLfsRows {
198 state: tokio::sync::RwLock<InMemoryLfsState>,
199}
200
201#[derive(Default)]
202struct InMemoryLfsState {
203 rows: BTreeMap<(String, String), u64>,
204 refcounts: BTreeMap<String, u64>,
205}
206
207impl Default for InMemoryLfsRows {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl InMemoryLfsRows {
214 pub fn new() -> Self {
216 InMemoryLfsRows {
217 state: tokio::sync::RwLock::new(InMemoryLfsState::default()),
218 }
219 }
220
221 fn recount(rows: &BTreeMap<(String, String), u64>, oid: &str) -> u64 {
224 rows.keys().filter(|(_, o)| o == oid).count() as u64
225 }
226}
227
228#[async_trait]
229impl LfsRows for InMemoryLfsRows {
230 async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
231 validated_repo_id(repo_id)?;
232 validated_oid(oid)?;
233 validated_size(size)?;
234 let mut state = self.state.write().await;
235 let newly = state
236 .rows
237 .insert((repo_id.to_owned(), oid.to_owned()), size)
238 .is_none();
239 if newly {
240 let count = Self::recount(&state.rows, oid);
241 state.refcounts.insert(oid.to_owned(), count);
242 }
243 Ok(())
244 }
245
246 async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
247 validated_repo_id(repo_id)?;
248 validated_oid(oid)?;
249 let mut state = self.state.write().await;
250 if state
251 .rows
252 .remove(&(repo_id.to_owned(), oid.to_owned()))
253 .is_some()
254 {
255 let count = Self::recount(&state.rows, oid);
256 if count == 0 {
257 state.refcounts.remove(oid);
258 } else {
259 state.refcounts.insert(oid.to_owned(), count);
260 }
261 }
262 Ok(())
263 }
264
265 async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
266 validated_repo_id(repo_id)?;
267 let mut state = self.state.write().await;
268 let affected: Vec<String> = state
269 .rows
270 .keys()
271 .filter(|(r, _)| r == repo_id)
272 .map(|(_, o)| o.clone())
273 .collect();
274 state.rows.retain(|r, _| r.0 != repo_id);
275 for oid in affected {
276 let count = Self::recount(&state.rows, &oid);
277 if count == 0 {
278 state.refcounts.remove(&oid);
279 } else {
280 state.refcounts.insert(oid, count);
281 }
282 }
283 Ok(())
284 }
285
286 async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
287 Ok(self.read_all().await)
288 }
289
290 async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
291 validated_repo_id(repo_id)?;
292 Ok(self
293 .read_all()
294 .await
295 .into_iter()
296 .filter(|row| row.repo_id == repo_id)
297 .collect())
298 }
299}
300
301impl InMemoryLfsRows {
302 async fn read_all(&self) -> Vec<LfsRow> {
303 let state = self.state.read().await;
304 state
305 .rows
306 .iter()
307 .map(|((repo_id, oid), size)| LfsRow {
308 repo_id: repo_id.clone(),
309 oid: oid.clone(),
310 size: *size,
311 refcount: state.refcounts.get(oid).copied().unwrap_or(0),
312 })
313 .collect()
314 }
315}
316
317#[cfg(feature = "sqlite")]
336pub struct SqliteLfsRows {
337 handle: crate::migrations::CoTenantSqlite,
338}
339
340#[cfg(feature = "sqlite")]
341impl SqliteLfsRows {
342 pub fn new(handle: crate::migrations::CoTenantSqlite) -> Self {
345 SqliteLfsRows { handle }
346 }
347}
348
349#[cfg(feature = "sqlite")]
350mod sqlite_twin {
351 use super::*;
352
353 fn storage_io(context: &str, error: rusqlite::Error) -> StorageError {
354 StorageError::Io(format!("{context}: {error}"))
355 }
356
357 fn recount_sql() -> &'static str {
362 "UPDATE lfs_objects \
363 SET refcount = (SELECT count(*) FROM lfs_objects AS c WHERE c.oid = lfs_objects.oid) \
364 WHERE oid = ?1"
365 }
366
367 fn recount_all_sql() -> &'static str {
373 "UPDATE lfs_objects \
374 SET refcount = (SELECT count(*) FROM lfs_objects AS c WHERE c.oid = lfs_objects.oid)"
375 }
376
377 fn read_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<LfsRow> {
378 Ok(LfsRow {
379 repo_id: row.get(0)?,
380 oid: row.get(1)?,
381 size: row.get::<_, i64>(2)? as u64,
382 refcount: row.get::<_, i64>(3)? as u64,
383 })
384 }
385
386 async fn run_blocking<T: Send + 'static>(
387 handle: crate::migrations::CoTenantSqlite,
388 work: impl FnOnce(&mut rusqlite::Connection) -> Result<T, StorageError> + Send + 'static,
389 ) -> Result<T, StorageError> {
390 tokio::task::spawn_blocking(move || {
391 let mut conn = handle.lock();
392 work(&mut conn)
393 })
394 .await
395 .map_err(|e| StorageError::Io(format!("lfs rows blocking task: {e}")))?
396 }
397
398 async fn run_transaction<T: Send + 'static>(
399 handle: crate::migrations::CoTenantSqlite,
400 work: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<T, StorageError> + Send + 'static,
401 ) -> Result<T, StorageError> {
402 run_blocking(handle, move |conn| {
403 let tx = conn
404 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
405 .map_err(|e| storage_io("lfs rows BEGIN IMMEDIATE", e))?;
406 let value = work(&tx)?;
407 tx.commit().map_err(|e| storage_io("lfs rows commit", e))?;
408 Ok(value)
409 })
410 .await
411 }
412
413 #[async_trait]
414 impl LfsRows for SqliteLfsRows {
415 async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
416 validated_repo_id(repo_id)?;
417 validated_oid(oid)?;
418 validated_size(size)?;
419 let handle = self.handle.clone();
420 let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
421 run_transaction(handle, move |tx| {
422 tx.execute(
423 "INSERT INTO lfs_objects (repo_id, oid, size, refcount) \
424 VALUES (?1, ?2, ?3, 1) \
425 ON CONFLICT(repo_id, oid) DO UPDATE SET size = excluded.size",
426 rusqlite::params![repo_id, oid, size as i64],
427 )
428 .map_err(|e| storage_io("lfs assert upsert", e))?;
429 tx.execute(recount_sql(), rusqlite::params![oid])
430 .map_err(|e| storage_io("lfs assert refcount recount", e))?;
431 Ok(())
432 })
433 .await
434 }
435
436 async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
437 validated_repo_id(repo_id)?;
438 validated_oid(oid)?;
439 let handle = self.handle.clone();
440 let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
441 run_transaction(handle, move |tx| {
442 tx.execute(
443 "DELETE FROM lfs_objects WHERE repo_id = ?1 AND oid = ?2",
444 rusqlite::params![repo_id, oid],
445 )
446 .map_err(|e| storage_io("lfs reference remove", e))?;
447 tx.execute(recount_sql(), rusqlite::params![oid])
448 .map_err(|e| storage_io("lfs remove refcount recount", e))?;
449 Ok(())
450 })
451 .await
452 }
453
454 async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
455 validated_repo_id(repo_id)?;
456 let handle = self.handle.clone();
457 let repo_id = repo_id.to_owned();
458 run_transaction(handle, move |tx| {
459 tx.execute(
460 "DELETE FROM lfs_objects WHERE repo_id = ?1",
461 rusqlite::params![repo_id],
462 )
463 .map_err(|e| storage_io("lfs repo removal", e))?;
464 tx.execute(recount_all_sql(), [])
465 .map_err(|e| storage_io("lfs repo removal recount", e))?;
466 Ok(())
467 })
468 .await
469 }
470
471 async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
472 let handle = self.handle.clone();
473 run_blocking(handle, move |conn| {
474 let mut stmt = conn
475 .prepare(
476 "SELECT repo_id, oid, size, refcount FROM lfs_objects \
477 ORDER BY repo_id, oid",
478 )
479 .map_err(|e| storage_io("lfs rows listing prepare", e))?;
480 let rows = stmt
481 .query_map([], read_row)
482 .map_err(|e| storage_io("lfs rows listing query", e))?;
483 rows.collect::<Result<Vec<_>, _>>()
484 .map_err(|e| storage_io("lfs rows listing row", e))
485 })
486 .await
487 }
488
489 async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
490 validated_repo_id(repo_id)?;
491 let handle = self.handle.clone();
492 let repo_id = repo_id.to_owned();
493 run_blocking(handle, move |conn| {
494 let mut stmt = conn
495 .prepare(
496 "SELECT repo_id, oid, size, refcount FROM lfs_objects \
497 WHERE repo_id = ?1 ORDER BY oid",
498 )
499 .map_err(|e| storage_io("lfs rows listing prepare", e))?;
500 let rows = stmt
501 .query_map(rusqlite::params![repo_id], read_row)
502 .map_err(|e| storage_io("lfs rows listing query", e))?;
503 rows.collect::<Result<Vec<_>, _>>()
504 .map_err(|e| storage_io("lfs rows listing row", e))
505 })
506 .await
507 }
508 }
509}
510
511#[cfg(feature = "postgres")]
527pub struct PgLfsRows {
528 handle: crate::postgres::CoTenantPg,
529}
530
531#[cfg(feature = "postgres")]
532impl PgLfsRows {
533 pub fn new(handle: crate::postgres::CoTenantPg) -> Self {
537 PgLfsRows { handle }
538 }
539}
540
541#[cfg(feature = "postgres")]
542mod pg_twin {
543 use super::*;
544 use crate::migrations::{lfs_objects_lock_key, qualify};
545
546 fn pg_io(context: &str, error: tokio_postgres::Error) -> StorageError {
547 StorageError::Io(format!("{context}: {error}"))
548 }
549
550 fn recount_sql(table: &str) -> String {
556 format!(
557 "UPDATE {table} AS t0 \
558 SET refcount = (SELECT count(*) FROM {table} c WHERE c.oid = t0.oid) \
559 WHERE t0.oid = $1"
560 )
561 }
562
563 fn recount_all_sql(table: &str) -> String {
564 format!(
565 "UPDATE {table} AS t0 \
566 SET refcount = (SELECT count(*) FROM {table} c WHERE c.oid = t0.oid)"
567 )
568 }
569
570 #[async_trait]
571 impl LfsRows for PgLfsRows {
572 async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
573 validated_repo_id(repo_id)?;
574 validated_oid(oid)?;
575 validated_size(size)?;
576 let schema = self.handle.schema().to_owned();
577 let size = size as i64;
578 let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
579 let mut guard = self.handle.lock().await;
580 let tx = guard
581 .transaction()
582 .await
583 .map_err(|e| pg_io("lfs rows BEGIN", e))?;
584 tx.execute(
585 "SELECT pg_advisory_xact_lock($1)",
586 &[&lfs_objects_lock_key(&schema)],
587 )
588 .await
589 .map_err(|e| pg_io("lfs rows advisory lock", e))?;
590 let upsert = format!(
591 "INSERT INTO {} (repo_id, oid, size, refcount) \
592 VALUES ($1, $2, $3, 1) \
593 ON CONFLICT (repo_id, oid) DO UPDATE SET size = excluded.size",
594 qualify(&schema, "lfs_objects")
595 );
596 tx.execute(&upsert, &[&repo_id, &oid, &size])
597 .await
598 .map_err(|e| pg_io("lfs assert upsert", e))?;
599 tx.execute(&recount_sql(&qualify(&schema, "lfs_objects")), &[&oid])
600 .await
601 .map_err(|e| pg_io("lfs assert refcount recount", e))?;
602 tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
603 Ok(())
604 }
605
606 async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
607 validated_repo_id(repo_id)?;
608 validated_oid(oid)?;
609 let schema = self.handle.schema().to_owned();
610 let (repo_id, oid) = (repo_id.to_owned(), oid.to_owned());
611 let mut guard = self.handle.lock().await;
612 let tx = guard
613 .transaction()
614 .await
615 .map_err(|e| pg_io("lfs rows BEGIN", e))?;
616 tx.execute(
617 "SELECT pg_advisory_xact_lock($1)",
618 &[&lfs_objects_lock_key(&schema)],
619 )
620 .await
621 .map_err(|e| pg_io("lfs rows advisory lock", e))?;
622 let delete = format!(
623 "DELETE FROM {} WHERE repo_id = $1 AND oid = $2",
624 qualify(&schema, "lfs_objects")
625 );
626 tx.execute(&delete, &[&repo_id, &oid])
627 .await
628 .map_err(|e| pg_io("lfs reference remove", e))?;
629 tx.execute(&recount_sql(&qualify(&schema, "lfs_objects")), &[&oid])
630 .await
631 .map_err(|e| pg_io("lfs remove refcount recount", e))?;
632 tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
633 Ok(())
634 }
635
636 async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
637 validated_repo_id(repo_id)?;
638 let schema = self.handle.schema().to_owned();
639 let repo_id = repo_id.to_owned();
640 let mut guard = self.handle.lock().await;
641 let tx = guard
642 .transaction()
643 .await
644 .map_err(|e| pg_io("lfs rows BEGIN", e))?;
645 tx.execute(
646 "SELECT pg_advisory_xact_lock($1)",
647 &[&lfs_objects_lock_key(&schema)],
648 )
649 .await
650 .map_err(|e| pg_io("lfs rows advisory lock", e))?;
651 let delete = format!(
652 "DELETE FROM {} WHERE repo_id = $1",
653 qualify(&schema, "lfs_objects")
654 );
655 tx.execute(&delete, &[&repo_id])
656 .await
657 .map_err(|e| pg_io("lfs repo removal", e))?;
658 let recount = recount_all_sql(&qualify(&schema, "lfs_objects"));
659 tx.execute(&recount, &[])
660 .await
661 .map_err(|e| pg_io("lfs repo removal recount", e))?;
662 tx.commit().await.map_err(|e| pg_io("lfs rows commit", e))?;
663 Ok(())
664 }
665
666 async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
667 let select = format!(
668 "SELECT repo_id, oid, size, refcount FROM {} ORDER BY repo_id, oid",
669 qualify(self.handle.schema(), "lfs_objects")
670 );
671 let guard = self.handle.lock().await;
672 let rows = guard
673 .query(&select, &[])
674 .await
675 .map_err(|e| pg_io("lfs rows listing query", e))?;
676 flatten_rows(&rows)
677 }
678
679 async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
680 validated_repo_id(repo_id)?;
681 let select = format!(
682 "SELECT repo_id, oid, size, refcount FROM {} WHERE repo_id = $1 ORDER BY oid",
683 qualify(self.handle.schema(), "lfs_objects")
684 );
685 let guard = self.handle.lock().await;
686 let rows = guard
687 .query(&select, &[&repo_id])
688 .await
689 .map_err(|e| pg_io("lfs rows listing query", e))?;
690 flatten_rows(&rows)
691 }
692 }
693
694 fn flatten_rows(rows: &[tokio_postgres::Row]) -> Result<Vec<LfsRow>, StorageError> {
695 rows.iter()
696 .map(|row| {
697 Ok(LfsRow {
698 repo_id: row
699 .try_get(0)
700 .map_err(|e| pg_io("lfs rows listing row", e))?,
701 oid: row
702 .try_get(1)
703 .map_err(|e| pg_io("lfs rows listing row", e))?,
704 size: row
705 .try_get::<_, i64>(2)
706 .map_err(|e| pg_io("lfs rows listing row", e))?
707 as u64,
708 refcount: row
709 .try_get::<_, i64>(3)
710 .map_err(|e| pg_io("lfs rows listing row", e))?
711 as u64,
712 })
713 })
714 .collect()
715 }
716}
717
718#[cfg(test)]
719pub(crate) mod battery {
720 use super::*;
721
722 pub(crate) fn oid_hex(tag: u8) -> String {
725 let base = format!("{tag:02x}").repeat(32);
726 base
727 }
728
729 pub(crate) async fn assert_and_the_refcount_aware_upsert(
734 table: &dyn LfsRows,
735 ) -> Result<(), StorageError> {
736 table.assert("repo/a", &oid_hex(1), 128).await?;
737 table.assert("repo/a", &oid_hex(1), 128).await?;
738 assert_eq!(
739 table.repo_rows("repo/a").await?,
740 vec![LfsRow {
741 repo_id: "repo/a".to_owned(),
742 oid: oid_hex(1),
743 size: 128,
744 refcount: 1,
745 }],
746 "the same oid re-asserted in one repo is one row"
747 );
748
749 table.assert("repo/a", &oid_hex(1), 256).await?;
750 assert_eq!(
751 table.repo_rows("repo/a").await?[0].size,
752 256,
753 "the re-assert refreshes the row's size"
754 );
755 assert_eq!(table.repo_rows("repo/a").await?[0].refcount, 1);
756
757 table.assert("repo/b", &oid_hex(1), 256).await?;
760 let shared: Vec<LfsRow> = table
761 .rows()
762 .await?
763 .into_iter()
764 .filter(|row| row.oid == oid_hex(1))
765 .collect();
766 assert_eq!(shared.len(), 2);
767 for row in &shared {
768 assert_eq!(
769 row.refcount, 2,
770 "every row of an oid reads the same count: {row:?}"
771 );
772 }
773 Ok(())
774 }
775
776 pub(crate) async fn cross_reference_reprotection(
783 table: &dyn LfsRows,
784 ) -> Result<(), StorageError> {
785 table.assert("repo/one", &oid_hex(2), 64).await?;
786 table.assert("repo/two", &oid_hex(2), 64).await?;
787 assert_eq!(
788 table
789 .rows()
790 .await?
791 .into_iter()
792 .filter(|row| row.oid == oid_hex(2))
793 .count(),
794 2
795 );
796
797 table.remove("repo/one", &oid_hex(2)).await?;
798 let survivor = table.repo_rows("repo/two").await?;
799 assert_eq!(
800 survivor,
801 vec![LfsRow {
802 repo_id: "repo/two".to_owned(),
803 oid: oid_hex(2),
804 size: 64,
805 refcount: 1,
806 }],
807 "one row removed — the survivor carries the decremented count; the key stays referenced"
808 );
809
810 table.remove("repo/two", &oid_hex(2)).await?;
811 assert!(
812 table
813 .rows()
814 .await?
815 .into_iter()
816 .all(|row| row.oid != oid_hex(2)),
817 "all rows removed — nothing references the key (sweep-eligible)"
818 );
819 Ok(())
820 }
821
822 pub(crate) async fn removal_of_an_absent_row_is_a_noop(
825 table: &dyn LfsRows,
826 ) -> Result<(), StorageError> {
827 table.assert("repo/noop", &oid_hex(3), 16).await?;
828 table.remove("repo/absent", &oid_hex(3)).await?;
829 table.remove("repo/noop", &oid_hex(0x3f)).await?;
830 let rows = table.repo_rows("repo/noop").await?;
831 assert_eq!(rows.len(), 1);
832 assert_eq!(
833 rows[0].refcount, 1,
834 "the no-op removals left the count exact"
835 );
836 Ok(())
837 }
838
839 pub(crate) async fn repo_deletion_takes_the_rows_and_recounts(
843 table: &dyn LfsRows,
844 ) -> Result<(), StorageError> {
845 table.assert("victim", &oid_hex(4), 32).await?;
846 table.assert("victim", &oid_hex(5), 48).await?;
847 table.assert("survivor", &oid_hex(4), 32).await?;
848 table.assert("survivor", &oid_hex(9), 72).await?;
849
850 table.remove_repo("victim").await?;
851
852 let touched: Vec<LfsRow> = table
853 .rows()
854 .await?
855 .into_iter()
856 .filter(|row| row.repo_id == "survivor" || row.repo_id == "victim")
857 .collect();
858 assert_eq!(
859 touched,
860 vec![
861 LfsRow {
862 repo_id: "survivor".to_owned(),
863 oid: oid_hex(4),
864 size: 32,
865 refcount: 1,
866 },
867 LfsRow {
868 repo_id: "survivor".to_owned(),
869 oid: oid_hex(9),
870 size: 72,
871 refcount: 1,
872 },
873 ],
874 "the victim's rows went with the repo; the survivor's shared oid recomputed to 1"
875 );
876
877 table.assert("victim", &oid_hex(4), 32).await?;
879 assert_eq!(
880 table.repo_rows("victim").await?[0].refcount,
881 2,
882 "a shared oid re-referenced by the former victim counts both rows again"
883 );
884 Ok(())
885 }
886
887 pub(crate) async fn the_reads_list_canonically(
890 table: &dyn LfsRows,
891 ) -> Result<(), StorageError> {
892 table.assert("repo/z", &oid_hex(6), 8).await?;
893 table.assert("repo/z", &oid_hex(7), 9).await?;
894 table.assert("repo/a", &oid_hex(8), 10).await?;
895
896 let all = table.rows().await?;
897 let keys: Vec<(String, String)> = all
898 .iter()
899 .map(|r| (r.repo_id.clone(), r.oid.clone()))
900 .collect();
901 let mut sorted = keys.clone();
902 sorted.sort();
903 assert_eq!(
904 keys, sorted,
905 "all rows arrive in canonical (repo_id, oid) order"
906 );
907
908 let mine = table.repo_rows("repo/z").await?;
909 assert_eq!(
910 mine.iter().map(|r| r.oid.clone()).collect::<Vec<_>>(),
911 vec![oid_hex(6), oid_hex(7)],
912 "the per-repo view filters and stays oid-ordered"
913 );
914 Ok(())
915 }
916
917 pub(crate) async fn validation_rejects_as_storage_invalid(
921 table: &dyn LfsRows,
922 ) -> Result<(), StorageError> {
923 let before = table.rows().await?.len();
924
925 for oid in [
926 "",
927 "nothex",
928 &"AB".repeat(32),
929 &"1".repeat(63),
930 &"1".repeat(65),
931 ] {
932 assert!(
933 matches!(
934 table.assert("repo/a", oid, 8).await,
935 Err(StorageError::Invalid(_))
936 ),
937 "the oid shape is a table-layer input: {oid:?}"
938 );
939 }
940 assert!(
941 matches!(
942 table.assert("", &oid_hex(1), 8).await,
943 Err(StorageError::Invalid(_))
944 ),
945 "an empty repo id is table-layer invalid"
946 );
947 assert!(
948 matches!(
949 table
950 .assert("repo/a", &oid_hex(1), (i64::MAX as u64) + 1)
951 .await,
952 Err(StorageError::Invalid(_))
953 ),
954 "a size over the engine integer range is table-layer invalid"
955 );
956 assert_eq!(
957 table.rows().await?.len(),
958 before,
959 "the rejected inputs wrote nothing"
960 );
961 Ok(())
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 #[cfg(feature = "sqlite")]
968 use std::sync::Arc;
969
970 use super::battery;
971 use super::*;
972
973 #[cfg(feature = "sqlite")]
974 use crate::testutil::scratch_dir;
975
976 async fn in_memory_reference() -> InMemoryLfsRows {
977 InMemoryLfsRows::new()
978 }
979
980 #[tokio::test]
984 async fn the_in_memory_reference_runs_the_battery() {
985 let table = in_memory_reference().await;
986 battery::assert_and_the_refcount_aware_upsert(&table)
987 .await
988 .unwrap();
989 battery::cross_reference_reprotection(&table).await.unwrap();
990 battery::removal_of_an_absent_row_is_a_noop(&table)
991 .await
992 .unwrap();
993 battery::repo_deletion_takes_the_rows_and_recounts(&table)
994 .await
995 .unwrap();
996 battery::the_reads_list_canonically(&table).await.unwrap();
997 battery::validation_rejects_as_storage_invalid(&table)
998 .await
999 .unwrap();
1000 }
1001
1002 #[tokio::test]
1007 async fn the_in_memory_refcounts_track_the_rows() {
1008 let table = in_memory_reference().await;
1009 let refcount_of = |rows: &Vec<LfsRow>, repo: &str| -> u64 {
1010 rows.iter()
1011 .find(|row| row.repo_id == repo)
1012 .map(|row| row.refcount)
1013 .expect("the row exists")
1014 };
1015 table.assert("r", &battery::oid_hex(1), 8).await.unwrap();
1016 table.assert("r", &battery::oid_hex(1), 8).await.unwrap();
1017 assert_eq!(
1018 refcount_of(&table.rows().await.unwrap(), "r"),
1019 1,
1020 "the re-assert did not bump the count"
1021 );
1022 table.assert("r2", &battery::oid_hex(1), 8).await.unwrap();
1023 assert_eq!(refcount_of(&table.rows().await.unwrap(), "r2"), 2);
1024 table.remove("r", &battery::oid_hex(1)).await.unwrap();
1025 assert_eq!(refcount_of(&table.rows().await.unwrap(), "r2"), 1);
1026 table.remove("r2", &battery::oid_hex(1)).await.unwrap();
1027 assert!(
1028 table.rows().await.unwrap().is_empty(),
1029 "the last removal leaves no row, no refcount entry"
1030 );
1031 }
1032
1033 #[tokio::test]
1043 async fn the_versioned_wrapper_forwards_and_bumps_on_assert() {
1044 let inner = Arc::new(InMemoryLfsRows::new());
1045 let counter = Arc::new(AtomicU64::new(0));
1046 let wrapped = VersionedLfsRows::new(inner.clone(), Arc::clone(&counter));
1047 let count = || counter.load(Ordering::Relaxed);
1048
1049 battery::assert_and_the_refcount_aware_upsert(&wrapped)
1050 .await
1051 .unwrap();
1052 assert_eq!(
1053 count(),
1054 4,
1055 "every assert bumps — same-pair re-asserts included"
1056 );
1057
1058 let before = count();
1059 battery::cross_reference_reprotection(&wrapped)
1060 .await
1061 .unwrap();
1062 assert_eq!(
1063 count() - before,
1064 2,
1065 "the two asserts bumped; the two removals read back without one"
1066 );
1067
1068 let before = count();
1069 battery::removal_of_an_absent_row_is_a_noop(&wrapped)
1070 .await
1071 .unwrap();
1072 assert_eq!(
1073 count() - before,
1074 1,
1075 "the one assert bumped — the no-op removals did not"
1076 );
1077 let before = count();
1078 battery::the_reads_list_canonically(&wrapped).await.unwrap();
1079 assert_eq!(count() - before, 3, "the reads themselves do not bump");
1080
1081 let before = count();
1082 battery::validation_rejects_as_storage_invalid(&wrapped)
1083 .await
1084 .unwrap();
1085 assert_eq!(
1086 count() - before,
1087 0,
1088 "the rejected asserts bump nothing (no rows landed — the bump-after-commit discipline)"
1089 );
1090
1091 let before = count();
1092 battery::removal_of_an_absent_row_is_a_noop(&wrapped)
1093 .await
1094 .unwrap();
1095 assert_eq!(count(), before + 1, "removals never bump");
1096 }
1097
1098 #[cfg(feature = "sqlite")]
1101 #[tokio::test]
1102 async fn the_sqlite_twin_runs_the_battery() {
1103 let dir = scratch_dir("lfs", "sqlite-battery");
1104 let handle = crate::migrations::CoTenantSqlite::open(dir.join("rows.sqlite3")).unwrap();
1105 let table = SqliteLfsRows::new(handle);
1106
1107 battery::assert_and_the_refcount_aware_upsert(&table)
1108 .await
1109 .unwrap();
1110 battery::cross_reference_reprotection(&table).await.unwrap();
1111 battery::removal_of_an_absent_row_is_a_noop(&table)
1112 .await
1113 .unwrap();
1114 battery::repo_deletion_takes_the_rows_and_recounts(&table)
1115 .await
1116 .unwrap();
1117 battery::the_reads_list_canonically(&table).await.unwrap();
1118 battery::validation_rejects_as_storage_invalid(&table)
1119 .await
1120 .unwrap();
1121
1122 std::fs::remove_dir_all(&dir).unwrap_or_default();
1123 }
1124
1125 #[cfg(feature = "sqlite")]
1130 #[tokio::test(flavor = "multi_thread")]
1131 async fn the_sqlite_concurrent_asserts_converge_on_exact_counts() {
1132 let dir = scratch_dir("lfs", "sqlite-race");
1133 let path = dir.join("race.sqlite3");
1134 let first = Arc::new(SqliteLfsRows::new(
1135 crate::migrations::CoTenantSqlite::open(&path).unwrap(),
1136 ));
1137 let second = Arc::new(SqliteLfsRows::new(
1138 crate::migrations::CoTenantSqlite::open(&path).unwrap(),
1139 ));
1140
1141 let oid = battery::oid_hex(2);
1142 let mut joins = Vec::new();
1143 for store in [first.clone(), second.clone()] {
1144 for repo in ["repo/a", "repo/b", "repo/c", "repo/d"] {
1145 joins.push(tokio::spawn({
1146 let store = store.clone();
1147 let oid = oid.clone();
1148 let repo = repo.to_owned();
1149 async move { store.assert(&repo, &oid, 64).await }
1150 }));
1151 }
1152 }
1153 for join in joins {
1154 join.await.unwrap().expect("every assert lands");
1155 }
1156
1157 let rows = first.rows().await.unwrap();
1158 assert_eq!(rows.len(), 4, "four repos — four rows, one key");
1159 for row in &rows {
1160 assert_eq!(
1161 row.refcount, 4,
1162 "every row carries the exact count: {row:?}"
1163 );
1164 }
1165
1166 std::fs::remove_dir_all(&dir).unwrap_or_default();
1167 }
1168}
1169
1170pub struct VersionedLfsRows<T: ?Sized> {
1210 inner: Arc<T>,
1211 version: Arc<AtomicU64>,
1212}
1213
1214impl<T: ?Sized> VersionedLfsRows<T> {
1215 pub fn new(inner: Arc<T>, version: Arc<AtomicU64>) -> Self {
1217 VersionedLfsRows { inner, version }
1218 }
1219
1220 pub fn inner(&self) -> &Arc<T> {
1222 &self.inner
1223 }
1224
1225 pub fn version(&self) -> u64 {
1228 self.version.load(Ordering::Relaxed)
1229 }
1230}
1231
1232#[async_trait]
1233impl<T> LfsRows for VersionedLfsRows<T>
1234where
1235 T: LfsRows + ?Sized,
1236{
1237 async fn assert(&self, repo_id: &str, oid: &str, size: u64) -> Result<(), StorageError> {
1238 self.inner.assert(repo_id, oid, size).await?;
1239 self.version.fetch_add(1, Ordering::Relaxed);
1240 Ok(())
1241 }
1242
1243 async fn remove(&self, repo_id: &str, oid: &str) -> Result<(), StorageError> {
1244 self.inner.remove(repo_id, oid).await
1245 }
1246
1247 async fn remove_repo(&self, repo_id: &str) -> Result<(), StorageError> {
1248 self.inner.remove_repo(repo_id).await
1249 }
1250
1251 async fn rows(&self) -> Result<Vec<LfsRow>, StorageError> {
1252 self.inner.rows().await
1253 }
1254
1255 async fn repo_rows(&self, repo_id: &str) -> Result<Vec<LfsRow>, StorageError> {
1256 self.inner.repo_rows(repo_id).await
1257 }
1258}
1259
1260#[cfg(all(test, feature = "postgres"))]
1267mod pg_twin_tests {
1268 use std::sync::Arc;
1269
1270 use crate::postgres::live_harness::{drop_schema, live_pg, unique_schema};
1271
1272 use super::battery;
1273 use super::*;
1274
1275 #[tokio::test]
1278 async fn pg_rows_twin_carries_the_battery() {
1279 let dsn = live_pg!();
1280 let schema = unique_schema("lfs-battery");
1281 let table = PgLfsRows::new(
1282 crate::postgres::CoTenantPg::open(&dsn, &schema)
1283 .await
1284 .unwrap(),
1285 );
1286
1287 battery::assert_and_the_refcount_aware_upsert(&table)
1288 .await
1289 .unwrap();
1290 battery::cross_reference_reprotection(&table).await.unwrap();
1291 battery::removal_of_an_absent_row_is_a_noop(&table)
1292 .await
1293 .unwrap();
1294 battery::repo_deletion_takes_the_rows_and_recounts(&table)
1295 .await
1296 .unwrap();
1297 battery::the_reads_list_canonically(&table).await.unwrap();
1298 battery::validation_rejects_as_storage_invalid(&table)
1299 .await
1300 .unwrap();
1301
1302 drop(table);
1303 drop_schema(&dsn, &schema).await;
1304 }
1305
1306 #[tokio::test]
1310 async fn pg_concurrent_asserts_converge_on_exact_counts() {
1311 let dsn = live_pg!();
1312 let schema = unique_schema("lfs-race");
1313 let oid = battery::oid_hex(2);
1314 let mut joins = Vec::new();
1315 for index in 0..4 {
1316 let table = Arc::new(PgLfsRows::new(
1317 crate::postgres::CoTenantPg::open(&dsn, &schema)
1318 .await
1319 .unwrap(),
1320 ));
1321 let oid = oid.clone();
1322 joins.push(tokio::spawn(async move {
1323 table
1324 .assert(&format!("repo/{index}"), &oid, 64)
1325 .await
1326 .expect("the assert lands")
1327 }));
1328 }
1329 for join in joins {
1330 join.await.unwrap();
1331 }
1332
1333 let table = PgLfsRows::new(
1334 crate::postgres::CoTenantPg::open(&dsn, &schema)
1335 .await
1336 .unwrap(),
1337 );
1338 for row in table.rows().await.unwrap() {
1339 assert_eq!(
1340 row.refcount, 4,
1341 "every row carries the exact count: {row:?}"
1342 );
1343 }
1344
1345 drop(table);
1346 drop_schema(&dsn, &schema).await;
1347 }
1348}