···4646 }
47474848 r := chi.NewRouter()
4949-4949+5050 r.Use(middleware.Logger)
5151 r.Use(middleware.Recoverer)
5252 r.Use(middleware.RequestID)
···5656 Conn: db,
5757 }), &gorm.Config{
5858 DisableForeignKeyConstraintWhenMigrating: true,
5959- PrepareStmt: false,
5959+ PrepareStmt: true, // Enable prepared statements for better performance
6060 })
6161 if err != nil {
6262 log.Fatal("Failed to initialize GORM:", err)
···6565 // Initialize repositories
6666 userRepo := postgresRepo.NewUserRepository(db)
6767 _ = users.NewUserService(userRepo) // TODO: Use when UserRoutes is fixed
6868-6868+6969 // Initialize carstore for ATProto repository storage
7070 carDirs := []string{"./data/carstore"}
7171 repoStore, err := carstore.NewRepoStore(gormDB, carDirs)
7272 if err != nil {
7373 log.Fatal("Failed to initialize repo store:", err)
7474 }
7575-7575+7676 repositoryRepo := postgresRepo.NewRepositoryRepo(db)
7777 repositoryService := repository.NewService(repositoryRepo, repoStore)
7878···93939494 fmt.Printf("Server starting on port %s\n", port)
9595 log.Fatal(http.ListenAndServe(":"+port, r))
9696-}9696+}
+38-10
internal/atproto/carstore/carstore.go
···2121 // Initialize Indigo's carstore
2222 cs, err := carstore.NewCarStore(db, carDirs)
2323 if err != nil {
2424- return nil, fmt.Errorf("failed to create carstore: %w", err)
2424+ return nil, fmt.Errorf("initializing carstore: %w", err)
2525 }
26262727 return &CarStore{
···3232// ImportSlice imports a CAR file slice for a user
3333func (c *CarStore) ImportSlice(ctx context.Context, uid models.Uid, since *string, carData []byte) (cid.Cid, error) {
3434 rootCid, _, err := c.cs.ImportSlice(ctx, uid, since, carData)
3535- return rootCid, err
3535+ if err != nil {
3636+ return cid.Undef, fmt.Errorf("importing CAR slice for UID %d: %w", uid, err)
3737+ }
3838+ return rootCid, nil
3639}
37403841// ReadUserCar reads a user's repository CAR file
3942func (c *CarStore) ReadUserCar(ctx context.Context, uid models.Uid, sinceRev string, incremental bool, w io.Writer) error {
4040- return c.cs.ReadUserCar(ctx, uid, sinceRev, incremental, w)
4343+ if err := c.cs.ReadUserCar(ctx, uid, sinceRev, incremental, w); err != nil {
4444+ return fmt.Errorf("reading user CAR for UID %d: %w", uid, err)
4545+ }
4646+ return nil
4147}
42484349// GetUserRepoHead gets the latest repository head CID for a user
4450func (c *CarStore) GetUserRepoHead(ctx context.Context, uid models.Uid) (cid.Cid, error) {
4545- return c.cs.GetUserRepoHead(ctx, uid)
5151+ head, err := c.cs.GetUserRepoHead(ctx, uid)
5252+ if err != nil {
5353+ return cid.Undef, fmt.Errorf("getting repo head for UID %d: %w", uid, err)
5454+ }
5555+ return head, nil
4656}
47574858// CompactUserShards performs garbage collection and compaction for a user's data
4959func (c *CarStore) CompactUserShards(ctx context.Context, uid models.Uid, aggressive bool) error {
5060 _, err := c.cs.CompactUserShards(ctx, uid, aggressive)
5151- return err
6161+ if err != nil {
6262+ return fmt.Errorf("compacting shards for UID %d: %w", uid, err)
6363+ }
6464+ return nil
5265}
53665467// WipeUserData removes all data for a user
5568func (c *CarStore) WipeUserData(ctx context.Context, uid models.Uid) error {
5656- return c.cs.WipeUserData(ctx, uid)
6969+ if err := c.cs.WipeUserData(ctx, uid); err != nil {
7070+ return fmt.Errorf("wiping data for UID %d: %w", uid, err)
7171+ }
7272+ return nil
5773}
58745975// NewDeltaSession creates a new session for writing deltas
6076func (c *CarStore) NewDeltaSession(ctx context.Context, uid models.Uid, since *string) (*carstore.DeltaSession, error) {
6161- return c.cs.NewDeltaSession(ctx, uid, since)
7777+ session, err := c.cs.NewDeltaSession(ctx, uid, since)
7878+ if err != nil {
7979+ return nil, fmt.Errorf("creating delta session for UID %d: %w", uid, err)
8080+ }
8181+ return session, nil
6282}
63836484// ReadOnlySession creates a read-only session for reading user data
6585func (c *CarStore) ReadOnlySession(uid models.Uid) (*carstore.DeltaSession, error) {
6666- return c.cs.ReadOnlySession(uid)
8686+ session, err := c.cs.ReadOnlySession(uid)
8787+ if err != nil {
8888+ return nil, fmt.Errorf("creating read-only session for UID %d: %w", uid, err)
8989+ }
9090+ return session, nil
6791}
68926993// Stat returns statistics about the carstore
7094func (c *CarStore) Stat(ctx context.Context, uid models.Uid) ([]carstore.UserStat, error) {
7171- return c.cs.Stat(ctx, uid)
7272-}9595+ stats, err := c.cs.Stat(ctx, uid)
9696+ if err != nil {
9797+ return nil, fmt.Errorf("getting stats for UID %d: %w", uid, err)
9898+ }
9999+ return stats, nil
100100+}
+11-11
internal/atproto/carstore/repo_store.go
···2222 // Create carstore
2323 cs, err := NewCarStore(db, carDirs)
2424 if err != nil {
2525- return nil, fmt.Errorf("failed to create carstore: %w", err)
2525+ return nil, fmt.Errorf("creating carstore: %w", err)
2626 }
27272828 // Create user mapping
2929 mapping, err := NewUserMapping(db)
3030 if err != nil {
3131- return nil, fmt.Errorf("failed to create user mapping: %w", err)
3131+ return nil, fmt.Errorf("creating user mapping: %w", err)
3232 }
33333434 return &RepoStore{
···4141func (rs *RepoStore) ImportRepo(ctx context.Context, did string, carData io.Reader) (cid.Cid, error) {
4242 uid, err := rs.mapping.GetOrCreateUID(ctx, did)
4343 if err != nil {
4444- return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err)
4444+ return cid.Undef, fmt.Errorf("getting UID for DID %s: %w", did, err)
4545 }
46464747 // Read all data from the reader
4848 data, err := io.ReadAll(carData)
4949 if err != nil {
5050- return cid.Undef, fmt.Errorf("failed to read CAR data: %w", err)
5050+ return cid.Undef, fmt.Errorf("reading CAR data: %w", err)
5151 }
5252-5252+5353 return rs.cs.ImportSlice(ctx, uid, nil, data)
5454}
5555···5757func (rs *RepoStore) ReadRepo(ctx context.Context, did string, sinceRev string) ([]byte, error) {
5858 uid, err := rs.mapping.GetUID(did)
5959 if err != nil {
6060- return nil, fmt.Errorf("failed to get UID for DID %s: %w", did, err)
6060+ return nil, fmt.Errorf("getting UID for DID %s: %w", did, err)
6161 }
62626363 var buf bytes.Buffer
6464 err = rs.cs.ReadUserCar(ctx, uid, sinceRev, false, &buf)
6565 if err != nil {
6666- return nil, fmt.Errorf("failed to read repo for DID %s: %w", did, err)
6666+ return nil, fmt.Errorf("reading repo for DID %s: %w", did, err)
6767 }
68686969 return buf.Bytes(), nil
···7373func (rs *RepoStore) GetRepoHead(ctx context.Context, did string) (cid.Cid, error) {
7474 uid, err := rs.mapping.GetUID(did)
7575 if err != nil {
7676- return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err)
7676+ return cid.Undef, fmt.Errorf("getting UID for DID %s: %w", did, err)
7777 }
78787979 return rs.cs.GetUserRepoHead(ctx, uid)
···8383func (rs *RepoStore) CompactRepo(ctx context.Context, did string) error {
8484 uid, err := rs.mapping.GetUID(did)
8585 if err != nil {
8686- return fmt.Errorf("failed to get UID for DID %s: %w", did, err)
8686+ return fmt.Errorf("getting UID for DID %s: %w", did, err)
8787 }
88888989 return rs.cs.CompactUserShards(ctx, uid, false)
···9393func (rs *RepoStore) DeleteRepo(ctx context.Context, did string) error {
9494 uid, err := rs.mapping.GetUID(did)
9595 if err != nil {
9696- return fmt.Errorf("failed to get UID for DID %s: %w", did, err)
9696+ return fmt.Errorf("getting UID for DID %s: %w", did, err)
9797 }
98989999 return rs.cs.WipeUserData(ctx, uid)
···119119// GetOrCreateUID gets or creates a UID for a DID
120120func (rs *RepoStore) GetOrCreateUID(ctx context.Context, did string) (models.Uid, error) {
121121 return rs.mapping.GetOrCreateUID(ctx, did)
122122-}122122+}
+11-11
internal/atproto/carstore/user_mapping.go
···11111212// UserMapping manages the mapping between DIDs and numeric UIDs required by Indigo's carstore
1313type UserMapping struct {
1414- db *gorm.DB
1515- mu sync.RWMutex
1616- didToUID map[string]models.Uid
1717- uidToDID map[models.Uid]string
1818- nextUID models.Uid
1414+ db *gorm.DB
1515+ mu sync.RWMutex
1616+ didToUID map[string]models.Uid
1717+ uidToDID map[models.Uid]string
1818+ nextUID models.Uid
1919}
20202121// UserMap represents the database model for DID to UID mapping
2222type UserMap struct {
2323 UID models.Uid `gorm:"primaryKey;autoIncrement"`
2424- DID string `gorm:"uniqueIndex;not null"`
2424+ DID string `gorm:"column:did;uniqueIndex;not null"`
2525 CreatedAt int64
2626 UpdatedAt int64
2727}
···3030func NewUserMapping(db *gorm.DB) (*UserMapping, error) {
3131 // Auto-migrate the user mapping table
3232 if err := db.AutoMigrate(&UserMap{}); err != nil {
3333- return nil, fmt.Errorf("failed to migrate user mapping table: %w", err)
3333+ return nil, fmt.Errorf("migrating user mapping table: %w", err)
3434 }
35353636 um := &UserMapping{
···42424343 // Load existing mappings
4444 if err := um.loadMappings(); err != nil {
4545- return nil, fmt.Errorf("failed to load user mappings: %w", err)
4545+ return nil, fmt.Errorf("loading user mappings: %w", err)
4646 }
47474848 return um, nil
···5252func (um *UserMapping) loadMappings() error {
5353 var mappings []UserMap
5454 if err := um.db.Find(&mappings).Error; err != nil {
5555- return err
5555+ return fmt.Errorf("querying user mappings: %w", err)
5656 }
57575858 um.mu.Lock()
···9393 }
94949595 if err := um.db.Create(userMap).Error; err != nil {
9696- return 0, fmt.Errorf("failed to create user mapping: %w", err)
9696+ return 0, fmt.Errorf("creating user mapping for DID %s: %w", did, err)
9797 }
98989999 um.didToUID[did] = userMap.UID
···124124 return "", fmt.Errorf("DID not found for UID: %d", uid)
125125 }
126126 return did, nil
127127-}127127+}
+20
internal/core/repository/constants.go
···11+package repository
22+33+import (
44+ "github.com/ipfs/go-cid"
55+ "github.com/multiformats/go-multihash"
66+)
77+88+var (
99+ // PlaceholderCID is used for empty repositories that have no content yet
1010+ // This allows us to maintain consistency in the repository record
1111+ // while the actual CAR data is created when records are added
1212+ PlaceholderCID cid.Cid
1313+)
1414+1515+func init() {
1616+ // Initialize the placeholder CID once at startup
1717+ emptyData := []byte("empty")
1818+ mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1)
1919+ PlaceholderCID = cid.NewCidV1(cid.Raw, mh)
2020+}
+28-35
internal/core/repository/service.go
···991010 "Coves/internal/atproto/carstore"
1111 "github.com/ipfs/go-cid"
1212- "github.com/multiformats/go-multihash"
1312)
14131514// Service implements the RepositoryService interface using Indigo's carstore
1615type Service struct {
1717- repo RepositoryRepository
1818- repoStore *carstore.RepoStore
1919- signingKeys map[string]interface{} // DID -> signing key
1616+ repo RepositoryRepository
1717+ repoStore *carstore.RepoStore
1818+ signingKeys map[string]interface{} // DID -> signing key
2019}
21202221// NewService creates a new repository service using carstore
···3837 // Check if repository already exists
3938 existing, err := s.repo.GetByDID(did)
4039 if err != nil {
4141- return nil, fmt.Errorf("failed to check existing repository: %w", err)
4040+ return nil, fmt.Errorf("checking existing repository: %w", err)
4241 }
4342 if existing != nil {
4443 return nil, fmt.Errorf("repository already exists for DID: %s", did)
···4746 // For now, just create the user mapping without importing CAR data
4847 // The actual repository data will be created when records are added
4948 ctx := context.Background()
5050-4949+5150 // Ensure user mapping exists
5251 _, err = s.repoStore.GetOrCreateUID(ctx, did)
5352 if err != nil {
5454- return nil, fmt.Errorf("failed to create user mapping: %w", err)
5353+ return nil, fmt.Errorf("creating user mapping: %w", err)
5554 }
5656-57555858- // Create a placeholder CID for the empty repository
5959- emptyData := []byte("empty")
6060- mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1)
6161- placeholderCID := cid.NewCidV1(cid.Raw, mh)
5656+ // Use placeholder CID for the empty repository
62576358 // Create repository record
6459 repository := &Repository{
6560 DID: did,
6666- HeadCID: placeholderCID,
6161+ HeadCID: PlaceholderCID,
6762 Revision: "rev-0",
6863 RecordCount: 0,
6964 StorageSize: 0,
···73687469 // Save to database
7570 if err := s.repo.Create(repository); err != nil {
7676- return nil, fmt.Errorf("failed to save repository: %w", err)
7171+ return nil, fmt.Errorf("saving repository: %w", err)
7772 }
78737974 return repository, nil
···8378func (s *Service) GetRepository(did string) (*Repository, error) {
8479 repo, err := s.repo.GetByDID(did)
8580 if err != nil {
8686- return nil, fmt.Errorf("failed to get repository: %w", err)
8181+ return nil, fmt.Errorf("getting repository: %w", err)
8782 }
8883 if repo == nil {
8984 return nil, fmt.Errorf("repository not found for DID: %s", did)
···10297func (s *Service) DeleteRepository(did string) error {
10398 // Delete from carstore
10499 if err := s.repoStore.DeleteRepo(context.Background(), did); err != nil {
105105- return fmt.Errorf("failed to delete repo from carstore: %w", err)
100100+ return fmt.Errorf("deleting repo from carstore: %w", err)
106101 }
107102108103 // Delete from database
109104 if err := s.repo.Delete(did); err != nil {
110110- return fmt.Errorf("failed to delete repository: %w", err)
105105+ return fmt.Errorf("deleting repository from database: %w", err)
111106 }
112107113108 return nil
···118113 // First check if repository exists in our database
119114 repo, err := s.repo.GetByDID(did)
120115 if err != nil {
121121- return nil, fmt.Errorf("failed to get repository: %w", err)
116116+ return nil, fmt.Errorf("getting repository: %w", err)
122117 }
123118 if repo == nil {
124119 return nil, fmt.Errorf("repository not found for DID: %s", did)
···132127 // Check for the specific error pattern from Indigo's carstore
133128 errMsg := err.Error()
134129 if strings.Contains(errMsg, "no data found for user") ||
135135- strings.Contains(errMsg, "user not found") {
130130+ strings.Contains(errMsg, "user not found") {
136131 return []byte{}, nil
137132 }
138138- return nil, fmt.Errorf("failed to export repository: %w", err)
133133+ return nil, fmt.Errorf("exporting repository: %w", err)
139134 }
140135141136 return carData, nil
···144139// ImportRepository imports a repository from a CAR file
145140func (s *Service) ImportRepository(did string, carData []byte) error {
146141 ctx := context.Background()
147147-142142+148143 // If empty CAR data, just create user mapping
149144 if len(carData) == 0 {
150145 _, err := s.repoStore.GetOrCreateUID(ctx, did)
151146 if err != nil {
152152- return fmt.Errorf("failed to create user mapping: %w", err)
147147+ return fmt.Errorf("creating user mapping: %w", err)
153148 }
154154-155155- // Create placeholder CID
156156- emptyData := []byte("empty")
157157- mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1)
158158- headCID := cid.NewCidV1(cid.Raw, mh)
159159-149149+150150+ // Use placeholder CID for empty repository
151151+ headCID := PlaceholderCID
152152+160153 // Create repository record
161154 repo := &Repository{
162155 DID: did,
···168161 UpdatedAt: time.Now(),
169162 }
170163 if err := s.repo.Create(repo); err != nil {
171171- return fmt.Errorf("failed to create repository: %w", err)
164164+ return fmt.Errorf("creating repository: %w", err)
172165 }
173166 return nil
174167 }
175175-168168+176169 // Import non-empty CAR into carstore
177170 headCID, err := s.repoStore.ImportRepo(ctx, did, bytes.NewReader(carData))
178171 if err != nil {
179179- return fmt.Errorf("failed to import repository: %w", err)
172172+ return fmt.Errorf("importing repository: %w", err)
180173 }
181174182175 // Create or update repository record
183176 repo, err := s.repo.GetByDID(did)
184177 if err != nil {
185185- return fmt.Errorf("failed to get repository: %w", err)
178178+ return fmt.Errorf("getting repository: %w", err)
186179 }
187180188181 if repo == nil {
···197190 UpdatedAt: time.Now(),
198191 }
199192 if err := s.repo.Create(repo); err != nil {
200200- return fmt.Errorf("failed to create repository: %w", err)
193193+ return fmt.Errorf("creating repository: %w", err)
201194 }
202195 } else {
203196 // Update existing repository
204197 repo.HeadCID = headCID
205198 repo.UpdatedAt = time.Now()
206199 if err := s.repo.Update(repo); err != nil {
207207- return fmt.Errorf("failed to update repository: %w", err)
200200+ return fmt.Errorf("updating repository: %w", err)
208201 }
209202 }
210203···247240248241func (s *Service) ListCommits(did string, limit int, cursor string) ([]*Commit, string, error) {
249242 return nil, "", fmt.Errorf("commit operations not yet implemented for carstore")
250250-}243243+}
+17-9
internal/core/repository/service_test.go
···1010 "Coves/internal/atproto/carstore"
1111 "Coves/internal/core/repository"
1212 "Coves/internal/db/postgres"
1313-1313+1414 "github.com/ipfs/go-cid"
1515 _ "github.com/lib/pq"
1616 "github.com/pressly/goose/v3"
···4444 // Connect with GORM using a fresh connection
4545 gormDB, err := gorm.Open(postgresDriver.Open(dbURL), &gorm.Config{
4646 DisableForeignKeyConstraintWhenMigrating: true,
4747- PrepareStmt: false,
4747+ PrepareStmt: false,
4848 })
4949 if err != nil {
5050 t.Fatalf("Failed to create GORM connection: %v", err)
···5858 gormDB.Exec("DELETE FROM records")
5959 gormDB.Exec("DELETE FROM user_maps")
6060 gormDB.Exec("DELETE FROM car_shards")
6161+ gormDB.Exec("DELETE FROM block_refs")
6262+6363+ // Close GORM connection
6464+ if sqlGormDB, err := gormDB.DB(); err == nil {
6565+ sqlGormDB.Close()
6666+ }
6767+6868+ // Close original SQL connection
6169 sqlDB.Close()
6270 }
6371···88968997 // Test DID
9098 testDID := "did:plc:testuser123"
9191-9999+92100 // Set signing key
93101 service.SetSigningKey(testDID, &mockSigningKey{})
94102···135143 t.Fatalf("Failed to create temp dir: %v", err)
136144 }
137145 defer os.RemoveAll(tempDir)
138138-146146+139147 // Log the temp directory for debugging
140148 t.Logf("Using carstore directory: %s", tempDir)
141149···158166 t.Fatalf("Failed to create repository 1: %v", err)
159167 }
160168 t.Logf("Created repository with HeadCID: %s", repo1.HeadCID)
161161-169169+162170 // Check what's in the database
163171 var userMapCount int
164172 gormDB.Raw("SELECT COUNT(*) FROM user_maps").Scan(&userMapCount)
165173 t.Logf("User maps count: %d", userMapCount)
166166-174174+167175 var carShardCount int
168176 gormDB.Raw("SELECT COUNT(*) FROM car_shards").Scan(&carShardCount)
169177 t.Logf("Car shards count: %d", carShardCount)
170170-178178+171179 // Check block_refs too
172180 var blockRefCount int
173181 gormDB.Raw("SELECT COUNT(*) FROM block_refs").Scan(&blockRefCount)
···333341func TestRepositoryService_MockedComponents(t *testing.T) {
334342 // Use the existing mock repository from the old test file
335343 _ = NewMockRepositoryRepository()
336336-344344+337345 // For unit testing without real carstore, we would need to mock RepoStore
338346 // For now, this demonstrates the structure
339347 t.Skip("Mocked carstore tests would require creating mock RepoStore interface")
···502510 }
503511504512 return records[start:end], nil
505505-}513513+}
···11-- +goose Up
22-- +goose StatementBegin
3344+-- WARNING: This migration removes blob storage tables.
55+-- Ensure all blob data has been migrated to carstore before running this migration.
66+-- This migration is NOT reversible if blob data exists!
77+48-- Remove the value column from records table since blocks are now stored in filesystem
59ALTER TABLE records DROP COLUMN IF EXISTS value;
610711-- Drop blob-related tables since FileCarStore handles block storage
1212+-- WARNING: This will permanently delete all blob data!
813DROP TABLE IF EXISTS blob_refs;
914DROP TABLE IF EXISTS blobs;
1015
···11+-- +goose Up
22+-- +goose StatementBegin
33+44+-- Note: The user_maps table is created by GORM's AutoMigrate in the carstore package
55+-- Only add indices if the table exists
66+DO $$
77+BEGIN
88+ -- Check if user_maps table exists
99+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_maps') THEN
1010+ -- Check if column exists before creating index
1111+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_maps' AND column_name = 'did') THEN
1212+ -- Explicit column name specified in GORM tag
1313+ CREATE INDEX IF NOT EXISTS idx_user_maps_did ON user_maps(did);
1414+ END IF;
1515+1616+ -- Add index on created_at if column exists
1717+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_maps' AND column_name = 'created_at') THEN
1818+ CREATE INDEX IF NOT EXISTS idx_user_maps_created_at ON user_maps(created_at);
1919+ END IF;
2020+ END IF;
2121+END $$;
2222+2323+-- +goose StatementEnd
2424+2525+-- +goose Down
2626+-- +goose StatementBegin
2727+2828+-- Remove indices if they exist
2929+DROP INDEX IF EXISTS idx_user_maps_did;
3030+DROP INDEX IF EXISTS idx_user_maps_created_at;
3131+3232+-- +goose StatementEnd