A community based topic aggregation platform built on atproto

Upgrading error handling / gorm column handling / indexes

+167 -81
+5 -5
cmd/server/main.go
··· 46 46 } 47 47 48 48 r := chi.NewRouter() 49 - 49 + 50 50 r.Use(middleware.Logger) 51 51 r.Use(middleware.Recoverer) 52 52 r.Use(middleware.RequestID) ··· 56 56 Conn: db, 57 57 }), &gorm.Config{ 58 58 DisableForeignKeyConstraintWhenMigrating: true, 59 - PrepareStmt: false, 59 + PrepareStmt: true, // Enable prepared statements for better performance 60 60 }) 61 61 if err != nil { 62 62 log.Fatal("Failed to initialize GORM:", err) ··· 65 65 // Initialize repositories 66 66 userRepo := postgresRepo.NewUserRepository(db) 67 67 _ = users.NewUserService(userRepo) // TODO: Use when UserRoutes is fixed 68 - 68 + 69 69 // Initialize carstore for ATProto repository storage 70 70 carDirs := []string{"./data/carstore"} 71 71 repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 72 72 if err != nil { 73 73 log.Fatal("Failed to initialize repo store:", err) 74 74 } 75 - 75 + 76 76 repositoryRepo := postgresRepo.NewRepositoryRepo(db) 77 77 repositoryService := repository.NewService(repositoryRepo, repoStore) 78 78 ··· 93 93 94 94 fmt.Printf("Server starting on port %s\n", port) 95 95 log.Fatal(http.ListenAndServe(":"+port, r)) 96 - } 96 + }
+38 -10
internal/atproto/carstore/carstore.go
··· 21 21 // Initialize Indigo's carstore 22 22 cs, err := carstore.NewCarStore(db, carDirs) 23 23 if err != nil { 24 - return nil, fmt.Errorf("failed to create carstore: %w", err) 24 + return nil, fmt.Errorf("initializing carstore: %w", err) 25 25 } 26 26 27 27 return &CarStore{ ··· 32 32 // ImportSlice imports a CAR file slice for a user 33 33 func (c *CarStore) ImportSlice(ctx context.Context, uid models.Uid, since *string, carData []byte) (cid.Cid, error) { 34 34 rootCid, _, err := c.cs.ImportSlice(ctx, uid, since, carData) 35 - return rootCid, err 35 + if err != nil { 36 + return cid.Undef, fmt.Errorf("importing CAR slice for UID %d: %w", uid, err) 37 + } 38 + return rootCid, nil 36 39 } 37 40 38 41 // ReadUserCar reads a user's repository CAR file 39 42 func (c *CarStore) ReadUserCar(ctx context.Context, uid models.Uid, sinceRev string, incremental bool, w io.Writer) error { 40 - return c.cs.ReadUserCar(ctx, uid, sinceRev, incremental, w) 43 + if err := c.cs.ReadUserCar(ctx, uid, sinceRev, incremental, w); err != nil { 44 + return fmt.Errorf("reading user CAR for UID %d: %w", uid, err) 45 + } 46 + return nil 41 47 } 42 48 43 49 // GetUserRepoHead gets the latest repository head CID for a user 44 50 func (c *CarStore) GetUserRepoHead(ctx context.Context, uid models.Uid) (cid.Cid, error) { 45 - return c.cs.GetUserRepoHead(ctx, uid) 51 + head, err := c.cs.GetUserRepoHead(ctx, uid) 52 + if err != nil { 53 + return cid.Undef, fmt.Errorf("getting repo head for UID %d: %w", uid, err) 54 + } 55 + return head, nil 46 56 } 47 57 48 58 // CompactUserShards performs garbage collection and compaction for a user's data 49 59 func (c *CarStore) CompactUserShards(ctx context.Context, uid models.Uid, aggressive bool) error { 50 60 _, err := c.cs.CompactUserShards(ctx, uid, aggressive) 51 - return err 61 + if err != nil { 62 + return fmt.Errorf("compacting shards for UID %d: %w", uid, err) 63 + } 64 + return nil 52 65 } 53 66 54 67 // WipeUserData removes all data for a user 55 68 func (c *CarStore) WipeUserData(ctx context.Context, uid models.Uid) error { 56 - return c.cs.WipeUserData(ctx, uid) 69 + if err := c.cs.WipeUserData(ctx, uid); err != nil { 70 + return fmt.Errorf("wiping data for UID %d: %w", uid, err) 71 + } 72 + return nil 57 73 } 58 74 59 75 // NewDeltaSession creates a new session for writing deltas 60 76 func (c *CarStore) NewDeltaSession(ctx context.Context, uid models.Uid, since *string) (*carstore.DeltaSession, error) { 61 - return c.cs.NewDeltaSession(ctx, uid, since) 77 + session, err := c.cs.NewDeltaSession(ctx, uid, since) 78 + if err != nil { 79 + return nil, fmt.Errorf("creating delta session for UID %d: %w", uid, err) 80 + } 81 + return session, nil 62 82 } 63 83 64 84 // ReadOnlySession creates a read-only session for reading user data 65 85 func (c *CarStore) ReadOnlySession(uid models.Uid) (*carstore.DeltaSession, error) { 66 - return c.cs.ReadOnlySession(uid) 86 + session, err := c.cs.ReadOnlySession(uid) 87 + if err != nil { 88 + return nil, fmt.Errorf("creating read-only session for UID %d: %w", uid, err) 89 + } 90 + return session, nil 67 91 } 68 92 69 93 // Stat returns statistics about the carstore 70 94 func (c *CarStore) Stat(ctx context.Context, uid models.Uid) ([]carstore.UserStat, error) { 71 - return c.cs.Stat(ctx, uid) 72 - } 95 + stats, err := c.cs.Stat(ctx, uid) 96 + if err != nil { 97 + return nil, fmt.Errorf("getting stats for UID %d: %w", uid, err) 98 + } 99 + return stats, nil 100 + }
+11 -11
internal/atproto/carstore/repo_store.go
··· 22 22 // Create carstore 23 23 cs, err := NewCarStore(db, carDirs) 24 24 if err != nil { 25 - return nil, fmt.Errorf("failed to create carstore: %w", err) 25 + return nil, fmt.Errorf("creating carstore: %w", err) 26 26 } 27 27 28 28 // Create user mapping 29 29 mapping, err := NewUserMapping(db) 30 30 if err != nil { 31 - return nil, fmt.Errorf("failed to create user mapping: %w", err) 31 + return nil, fmt.Errorf("creating user mapping: %w", err) 32 32 } 33 33 34 34 return &RepoStore{ ··· 41 41 func (rs *RepoStore) ImportRepo(ctx context.Context, did string, carData io.Reader) (cid.Cid, error) { 42 42 uid, err := rs.mapping.GetOrCreateUID(ctx, did) 43 43 if err != nil { 44 - return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 44 + return cid.Undef, fmt.Errorf("getting UID for DID %s: %w", did, err) 45 45 } 46 46 47 47 // Read all data from the reader 48 48 data, err := io.ReadAll(carData) 49 49 if err != nil { 50 - return cid.Undef, fmt.Errorf("failed to read CAR data: %w", err) 50 + return cid.Undef, fmt.Errorf("reading CAR data: %w", err) 51 51 } 52 - 52 + 53 53 return rs.cs.ImportSlice(ctx, uid, nil, data) 54 54 } 55 55 ··· 57 57 func (rs *RepoStore) ReadRepo(ctx context.Context, did string, sinceRev string) ([]byte, error) { 58 58 uid, err := rs.mapping.GetUID(did) 59 59 if err != nil { 60 - return nil, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 60 + return nil, fmt.Errorf("getting UID for DID %s: %w", did, err) 61 61 } 62 62 63 63 var buf bytes.Buffer 64 64 err = rs.cs.ReadUserCar(ctx, uid, sinceRev, false, &buf) 65 65 if err != nil { 66 - return nil, fmt.Errorf("failed to read repo for DID %s: %w", did, err) 66 + return nil, fmt.Errorf("reading repo for DID %s: %w", did, err) 67 67 } 68 68 69 69 return buf.Bytes(), nil ··· 73 73 func (rs *RepoStore) GetRepoHead(ctx context.Context, did string) (cid.Cid, error) { 74 74 uid, err := rs.mapping.GetUID(did) 75 75 if err != nil { 76 - return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 76 + return cid.Undef, fmt.Errorf("getting UID for DID %s: %w", did, err) 77 77 } 78 78 79 79 return rs.cs.GetUserRepoHead(ctx, uid) ··· 83 83 func (rs *RepoStore) CompactRepo(ctx context.Context, did string) error { 84 84 uid, err := rs.mapping.GetUID(did) 85 85 if err != nil { 86 - return fmt.Errorf("failed to get UID for DID %s: %w", did, err) 86 + return fmt.Errorf("getting UID for DID %s: %w", did, err) 87 87 } 88 88 89 89 return rs.cs.CompactUserShards(ctx, uid, false) ··· 93 93 func (rs *RepoStore) DeleteRepo(ctx context.Context, did string) error { 94 94 uid, err := rs.mapping.GetUID(did) 95 95 if err != nil { 96 - return fmt.Errorf("failed to get UID for DID %s: %w", did, err) 96 + return fmt.Errorf("getting UID for DID %s: %w", did, err) 97 97 } 98 98 99 99 return rs.cs.WipeUserData(ctx, uid) ··· 119 119 // GetOrCreateUID gets or creates a UID for a DID 120 120 func (rs *RepoStore) GetOrCreateUID(ctx context.Context, did string) (models.Uid, error) { 121 121 return rs.mapping.GetOrCreateUID(ctx, did) 122 - } 122 + }
+11 -11
internal/atproto/carstore/user_mapping.go
··· 11 11 12 12 // UserMapping manages the mapping between DIDs and numeric UIDs required by Indigo's carstore 13 13 type UserMapping struct { 14 - db *gorm.DB 15 - mu sync.RWMutex 16 - didToUID map[string]models.Uid 17 - uidToDID map[models.Uid]string 18 - nextUID models.Uid 14 + db *gorm.DB 15 + mu sync.RWMutex 16 + didToUID map[string]models.Uid 17 + uidToDID map[models.Uid]string 18 + nextUID models.Uid 19 19 } 20 20 21 21 // UserMap represents the database model for DID to UID mapping 22 22 type UserMap struct { 23 23 UID models.Uid `gorm:"primaryKey;autoIncrement"` 24 - DID string `gorm:"uniqueIndex;not null"` 24 + DID string `gorm:"column:did;uniqueIndex;not null"` 25 25 CreatedAt int64 26 26 UpdatedAt int64 27 27 } ··· 30 30 func NewUserMapping(db *gorm.DB) (*UserMapping, error) { 31 31 // Auto-migrate the user mapping table 32 32 if err := db.AutoMigrate(&UserMap{}); err != nil { 33 - return nil, fmt.Errorf("failed to migrate user mapping table: %w", err) 33 + return nil, fmt.Errorf("migrating user mapping table: %w", err) 34 34 } 35 35 36 36 um := &UserMapping{ ··· 42 42 43 43 // Load existing mappings 44 44 if err := um.loadMappings(); err != nil { 45 - return nil, fmt.Errorf("failed to load user mappings: %w", err) 45 + return nil, fmt.Errorf("loading user mappings: %w", err) 46 46 } 47 47 48 48 return um, nil ··· 52 52 func (um *UserMapping) loadMappings() error { 53 53 var mappings []UserMap 54 54 if err := um.db.Find(&mappings).Error; err != nil { 55 - return err 55 + return fmt.Errorf("querying user mappings: %w", err) 56 56 } 57 57 58 58 um.mu.Lock() ··· 93 93 } 94 94 95 95 if err := um.db.Create(userMap).Error; err != nil { 96 - return 0, fmt.Errorf("failed to create user mapping: %w", err) 96 + return 0, fmt.Errorf("creating user mapping for DID %s: %w", did, err) 97 97 } 98 98 99 99 um.didToUID[did] = userMap.UID ··· 124 124 return "", fmt.Errorf("DID not found for UID: %d", uid) 125 125 } 126 126 return did, nil 127 - } 127 + }
+20
internal/core/repository/constants.go
··· 1 + package repository 2 + 3 + import ( 4 + "github.com/ipfs/go-cid" 5 + "github.com/multiformats/go-multihash" 6 + ) 7 + 8 + var ( 9 + // PlaceholderCID is used for empty repositories that have no content yet 10 + // This allows us to maintain consistency in the repository record 11 + // while the actual CAR data is created when records are added 12 + PlaceholderCID cid.Cid 13 + ) 14 + 15 + func init() { 16 + // Initialize the placeholder CID once at startup 17 + emptyData := []byte("empty") 18 + mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1) 19 + PlaceholderCID = cid.NewCidV1(cid.Raw, mh) 20 + }
+28 -35
internal/core/repository/service.go
··· 9 9 10 10 "Coves/internal/atproto/carstore" 11 11 "github.com/ipfs/go-cid" 12 - "github.com/multiformats/go-multihash" 13 12 ) 14 13 15 14 // Service implements the RepositoryService interface using Indigo's carstore 16 15 type Service struct { 17 - repo RepositoryRepository 18 - repoStore *carstore.RepoStore 19 - signingKeys map[string]interface{} // DID -> signing key 16 + repo RepositoryRepository 17 + repoStore *carstore.RepoStore 18 + signingKeys map[string]interface{} // DID -> signing key 20 19 } 21 20 22 21 // NewService creates a new repository service using carstore ··· 38 37 // Check if repository already exists 39 38 existing, err := s.repo.GetByDID(did) 40 39 if err != nil { 41 - return nil, fmt.Errorf("failed to check existing repository: %w", err) 40 + return nil, fmt.Errorf("checking existing repository: %w", err) 42 41 } 43 42 if existing != nil { 44 43 return nil, fmt.Errorf("repository already exists for DID: %s", did) ··· 47 46 // For now, just create the user mapping without importing CAR data 48 47 // The actual repository data will be created when records are added 49 48 ctx := context.Background() 50 - 49 + 51 50 // Ensure user mapping exists 52 51 _, err = s.repoStore.GetOrCreateUID(ctx, did) 53 52 if err != nil { 54 - return nil, fmt.Errorf("failed to create user mapping: %w", err) 53 + return nil, fmt.Errorf("creating user mapping: %w", err) 55 54 } 56 - 57 55 58 - // Create a placeholder CID for the empty repository 59 - emptyData := []byte("empty") 60 - mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1) 61 - placeholderCID := cid.NewCidV1(cid.Raw, mh) 56 + // Use placeholder CID for the empty repository 62 57 63 58 // Create repository record 64 59 repository := &Repository{ 65 60 DID: did, 66 - HeadCID: placeholderCID, 61 + HeadCID: PlaceholderCID, 67 62 Revision: "rev-0", 68 63 RecordCount: 0, 69 64 StorageSize: 0, ··· 73 68 74 69 // Save to database 75 70 if err := s.repo.Create(repository); err != nil { 76 - return nil, fmt.Errorf("failed to save repository: %w", err) 71 + return nil, fmt.Errorf("saving repository: %w", err) 77 72 } 78 73 79 74 return repository, nil ··· 83 78 func (s *Service) GetRepository(did string) (*Repository, error) { 84 79 repo, err := s.repo.GetByDID(did) 85 80 if err != nil { 86 - return nil, fmt.Errorf("failed to get repository: %w", err) 81 + return nil, fmt.Errorf("getting repository: %w", err) 87 82 } 88 83 if repo == nil { 89 84 return nil, fmt.Errorf("repository not found for DID: %s", did) ··· 102 97 func (s *Service) DeleteRepository(did string) error { 103 98 // Delete from carstore 104 99 if err := s.repoStore.DeleteRepo(context.Background(), did); err != nil { 105 - return fmt.Errorf("failed to delete repo from carstore: %w", err) 100 + return fmt.Errorf("deleting repo from carstore: %w", err) 106 101 } 107 102 108 103 // Delete from database 109 104 if err := s.repo.Delete(did); err != nil { 110 - return fmt.Errorf("failed to delete repository: %w", err) 105 + return fmt.Errorf("deleting repository from database: %w", err) 111 106 } 112 107 113 108 return nil ··· 118 113 // First check if repository exists in our database 119 114 repo, err := s.repo.GetByDID(did) 120 115 if err != nil { 121 - return nil, fmt.Errorf("failed to get repository: %w", err) 116 + return nil, fmt.Errorf("getting repository: %w", err) 122 117 } 123 118 if repo == nil { 124 119 return nil, fmt.Errorf("repository not found for DID: %s", did) ··· 132 127 // Check for the specific error pattern from Indigo's carstore 133 128 errMsg := err.Error() 134 129 if strings.Contains(errMsg, "no data found for user") || 135 - strings.Contains(errMsg, "user not found") { 130 + strings.Contains(errMsg, "user not found") { 136 131 return []byte{}, nil 137 132 } 138 - return nil, fmt.Errorf("failed to export repository: %w", err) 133 + return nil, fmt.Errorf("exporting repository: %w", err) 139 134 } 140 135 141 136 return carData, nil ··· 144 139 // ImportRepository imports a repository from a CAR file 145 140 func (s *Service) ImportRepository(did string, carData []byte) error { 146 141 ctx := context.Background() 147 - 142 + 148 143 // If empty CAR data, just create user mapping 149 144 if len(carData) == 0 { 150 145 _, err := s.repoStore.GetOrCreateUID(ctx, did) 151 146 if err != nil { 152 - return fmt.Errorf("failed to create user mapping: %w", err) 147 + return fmt.Errorf("creating user mapping: %w", err) 153 148 } 154 - 155 - // Create placeholder CID 156 - emptyData := []byte("empty") 157 - mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1) 158 - headCID := cid.NewCidV1(cid.Raw, mh) 159 - 149 + 150 + // Use placeholder CID for empty repository 151 + headCID := PlaceholderCID 152 + 160 153 // Create repository record 161 154 repo := &Repository{ 162 155 DID: did, ··· 168 161 UpdatedAt: time.Now(), 169 162 } 170 163 if err := s.repo.Create(repo); err != nil { 171 - return fmt.Errorf("failed to create repository: %w", err) 164 + return fmt.Errorf("creating repository: %w", err) 172 165 } 173 166 return nil 174 167 } 175 - 168 + 176 169 // Import non-empty CAR into carstore 177 170 headCID, err := s.repoStore.ImportRepo(ctx, did, bytes.NewReader(carData)) 178 171 if err != nil { 179 - return fmt.Errorf("failed to import repository: %w", err) 172 + return fmt.Errorf("importing repository: %w", err) 180 173 } 181 174 182 175 // Create or update repository record 183 176 repo, err := s.repo.GetByDID(did) 184 177 if err != nil { 185 - return fmt.Errorf("failed to get repository: %w", err) 178 + return fmt.Errorf("getting repository: %w", err) 186 179 } 187 180 188 181 if repo == nil { ··· 197 190 UpdatedAt: time.Now(), 198 191 } 199 192 if err := s.repo.Create(repo); err != nil { 200 - return fmt.Errorf("failed to create repository: %w", err) 193 + return fmt.Errorf("creating repository: %w", err) 201 194 } 202 195 } else { 203 196 // Update existing repository 204 197 repo.HeadCID = headCID 205 198 repo.UpdatedAt = time.Now() 206 199 if err := s.repo.Update(repo); err != nil { 207 - return fmt.Errorf("failed to update repository: %w", err) 200 + return fmt.Errorf("updating repository: %w", err) 208 201 } 209 202 } 210 203 ··· 247 240 248 241 func (s *Service) ListCommits(did string, limit int, cursor string) ([]*Commit, string, error) { 249 242 return nil, "", fmt.Errorf("commit operations not yet implemented for carstore") 250 - } 243 + }
+17 -9
internal/core/repository/service_test.go
··· 10 10 "Coves/internal/atproto/carstore" 11 11 "Coves/internal/core/repository" 12 12 "Coves/internal/db/postgres" 13 - 13 + 14 14 "github.com/ipfs/go-cid" 15 15 _ "github.com/lib/pq" 16 16 "github.com/pressly/goose/v3" ··· 44 44 // Connect with GORM using a fresh connection 45 45 gormDB, err := gorm.Open(postgresDriver.Open(dbURL), &gorm.Config{ 46 46 DisableForeignKeyConstraintWhenMigrating: true, 47 - PrepareStmt: false, 47 + PrepareStmt: false, 48 48 }) 49 49 if err != nil { 50 50 t.Fatalf("Failed to create GORM connection: %v", err) ··· 58 58 gormDB.Exec("DELETE FROM records") 59 59 gormDB.Exec("DELETE FROM user_maps") 60 60 gormDB.Exec("DELETE FROM car_shards") 61 + gormDB.Exec("DELETE FROM block_refs") 62 + 63 + // Close GORM connection 64 + if sqlGormDB, err := gormDB.DB(); err == nil { 65 + sqlGormDB.Close() 66 + } 67 + 68 + // Close original SQL connection 61 69 sqlDB.Close() 62 70 } 63 71 ··· 88 96 89 97 // Test DID 90 98 testDID := "did:plc:testuser123" 91 - 99 + 92 100 // Set signing key 93 101 service.SetSigningKey(testDID, &mockSigningKey{}) 94 102 ··· 135 143 t.Fatalf("Failed to create temp dir: %v", err) 136 144 } 137 145 defer os.RemoveAll(tempDir) 138 - 146 + 139 147 // Log the temp directory for debugging 140 148 t.Logf("Using carstore directory: %s", tempDir) 141 149 ··· 158 166 t.Fatalf("Failed to create repository 1: %v", err) 159 167 } 160 168 t.Logf("Created repository with HeadCID: %s", repo1.HeadCID) 161 - 169 + 162 170 // Check what's in the database 163 171 var userMapCount int 164 172 gormDB.Raw("SELECT COUNT(*) FROM user_maps").Scan(&userMapCount) 165 173 t.Logf("User maps count: %d", userMapCount) 166 - 174 + 167 175 var carShardCount int 168 176 gormDB.Raw("SELECT COUNT(*) FROM car_shards").Scan(&carShardCount) 169 177 t.Logf("Car shards count: %d", carShardCount) 170 - 178 + 171 179 // Check block_refs too 172 180 var blockRefCount int 173 181 gormDB.Raw("SELECT COUNT(*) FROM block_refs").Scan(&blockRefCount) ··· 333 341 func TestRepositoryService_MockedComponents(t *testing.T) { 334 342 // Use the existing mock repository from the old test file 335 343 _ = NewMockRepositoryRepository() 336 - 344 + 337 345 // For unit testing without real carstore, we would need to mock RepoStore 338 346 // For now, this demonstrates the structure 339 347 t.Skip("Mocked carstore tests would require creating mock RepoStore interface") ··· 502 510 } 503 511 504 512 return records[start:end], nil 505 - } 513 + }
+5
internal/db/migrations/003_update_for_carstore.sql
··· 1 1 -- +goose Up 2 2 -- +goose StatementBegin 3 3 4 + -- WARNING: This migration removes blob storage tables. 5 + -- Ensure all blob data has been migrated to carstore before running this migration. 6 + -- This migration is NOT reversible if blob data exists! 7 + 4 8 -- Remove the value column from records table since blocks are now stored in filesystem 5 9 ALTER TABLE records DROP COLUMN IF EXISTS value; 6 10 7 11 -- Drop blob-related tables since FileCarStore handles block storage 12 + -- WARNING: This will permanently delete all blob data! 8 13 DROP TABLE IF EXISTS blob_refs; 9 14 DROP TABLE IF EXISTS blobs; 10 15
+32
internal/db/migrations/005_add_user_maps_indices.sql
··· 1 + -- +goose Up 2 + -- +goose StatementBegin 3 + 4 + -- Note: The user_maps table is created by GORM's AutoMigrate in the carstore package 5 + -- Only add indices if the table exists 6 + DO $$ 7 + BEGIN 8 + -- Check if user_maps table exists 9 + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_maps') THEN 10 + -- Check if column exists before creating index 11 + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_maps' AND column_name = 'did') THEN 12 + -- Explicit column name specified in GORM tag 13 + CREATE INDEX IF NOT EXISTS idx_user_maps_did ON user_maps(did); 14 + END IF; 15 + 16 + -- Add index on created_at if column exists 17 + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'user_maps' AND column_name = 'created_at') THEN 18 + CREATE INDEX IF NOT EXISTS idx_user_maps_created_at ON user_maps(created_at); 19 + END IF; 20 + END IF; 21 + END $$; 22 + 23 + -- +goose StatementEnd 24 + 25 + -- +goose Down 26 + -- +goose StatementBegin 27 + 28 + -- Remove indices if they exist 29 + DROP INDEX IF EXISTS idx_user_maps_did; 30 + DROP INDEX IF EXISTS idx_user_maps_created_at; 31 + 32 + -- +goose StatementEnd