···2233import (
44 "database/sql"
55+ "fmt"
66+ "strings"
57 "time"
68)
79···9193 if lastPushStr != "" {
9294 // Try multiple timestamp formats
9395 formats := []string{
9494- time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00
9595- "2006-01-02 15:04:05.999999999-07:00", // SQLite with microseconds and timezone
9696- "2006-01-02 15:04:05.999999999", // SQLite with microseconds
9797- time.RFC3339, // 2006-01-02T15:04:05Z07:00
9898- "2006-01-02 15:04:05", // SQLite default
9696+ time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00
9797+ "2006-01-02 15:04:05.999999999-07:00", // SQLite with microseconds and timezone
9898+ "2006-01-02 15:04:05.999999999", // SQLite with microseconds
9999+ time.RFC3339, // 2006-01-02T15:04:05Z07:00
100100+ "2006-01-02 15:04:05", // SQLite default
99101 }
100102101103 for _, format := range formats {
···162164 return repos, nil
163165}
164166167167+// GetUserByDID retrieves a user by DID
168168+func GetUserByDID(db *sql.DB, did string) (*User, error) {
169169+ var user User
170170+ err := db.QueryRow(`
171171+ SELECT did, handle, pds_endpoint, last_seen
172172+ FROM users
173173+ WHERE did = ?
174174+ `, did).Scan(&user.DID, &user.Handle, &user.PDSEndpoint, &user.LastSeen)
175175+176176+ if err == sql.ErrNoRows {
177177+ return nil, nil
178178+ }
179179+ if err != nil {
180180+ return nil, err
181181+ }
182182+183183+ return &user, nil
184184+}
185185+165186// UpsertUser inserts or updates a user record
166187func UpsertUser(db *sql.DB, user *User) error {
167188 _, err := db.Exec(`
···175196 return err
176197}
177198199199+// GetManifestDigestsForDID returns all manifest digests for a DID
200200+func GetManifestDigestsForDID(db *sql.DB, did string) ([]string, error) {
201201+ rows, err := db.Query(`
202202+ SELECT digest FROM manifests WHERE did = ?
203203+ `, did)
204204+ if err != nil {
205205+ return nil, err
206206+ }
207207+ defer rows.Close()
208208+209209+ var digests []string
210210+ for rows.Next() {
211211+ var digest string
212212+ if err := rows.Scan(&digest); err != nil {
213213+ return nil, err
214214+ }
215215+ digests = append(digests, digest)
216216+ }
217217+218218+ return digests, rows.Err()
219219+}
220220+221221+// DeleteManifestsNotInList deletes all manifests for a DID that are not in the provided list
222222+func DeleteManifestsNotInList(db *sql.DB, did string, keepDigests []string) error {
223223+ if len(keepDigests) == 0 {
224224+ // No manifests to keep - delete all for this DID
225225+ _, err := db.Exec(`DELETE FROM manifests WHERE did = ?`, did)
226226+ return err
227227+ }
228228+229229+ // Build placeholders for IN clause
230230+ placeholders := make([]string, len(keepDigests))
231231+ args := []interface{}{did}
232232+ for i, digest := range keepDigests {
233233+ placeholders[i] = "?"
234234+ args = append(args, digest)
235235+ }
236236+237237+ query := fmt.Sprintf(`
238238+ DELETE FROM manifests
239239+ WHERE did = ? AND digest NOT IN (%s)
240240+ `, strings.Join(placeholders, ","))
241241+242242+ _, err := db.Exec(query, args...)
243243+ return err
244244+}
245245+246246+// GetTagsForDID returns all (repository, tag) pairs for a DID
247247+func GetTagsForDID(db *sql.DB, did string) ([]struct{ Repository, Tag string }, error) {
248248+ rows, err := db.Query(`
249249+ SELECT repository, tag FROM tags WHERE did = ?
250250+ `, did)
251251+ if err != nil {
252252+ return nil, err
253253+ }
254254+ defer rows.Close()
255255+256256+ var tags []struct{ Repository, Tag string }
257257+ for rows.Next() {
258258+ var t struct{ Repository, Tag string }
259259+ if err := rows.Scan(&t.Repository, &t.Tag); err != nil {
260260+ return nil, err
261261+ }
262262+ tags = append(tags, t)
263263+ }
264264+265265+ return tags, rows.Err()
266266+}
267267+268268+// DeleteTagsNotInList deletes all tags for a DID that are not in the provided list
269269+func DeleteTagsNotInList(db *sql.DB, did string, keepTags []struct{ Repository, Tag string }) error {
270270+ if len(keepTags) == 0 {
271271+ // No tags to keep - delete all for this DID
272272+ _, err := db.Exec(`DELETE FROM tags WHERE did = ?`, did)
273273+ return err
274274+ }
275275+276276+ // For tags, we need to check (repository, tag) pairs
277277+ // Build a DELETE query that excludes the pairs we want to keep
278278+ tx, err := db.Begin()
279279+ if err != nil {
280280+ return err
281281+ }
282282+ defer tx.Rollback()
283283+284284+ // First, get all current tags
285285+ rows, err := tx.Query(`SELECT id, repository, tag FROM tags WHERE did = ?`, did)
286286+ if err != nil {
287287+ return err
288288+ }
289289+290290+ var toDelete []int64
291291+ for rows.Next() {
292292+ var id int64
293293+ var repo, tag string
294294+ if err := rows.Scan(&id, &repo, &tag); err != nil {
295295+ rows.Close()
296296+ return err
297297+ }
298298+299299+ // Check if this tag should be kept
300300+ found := false
301301+ for _, keep := range keepTags {
302302+ if keep.Repository == repo && keep.Tag == tag {
303303+ found = true
304304+ break
305305+ }
306306+ }
307307+308308+ if !found {
309309+ toDelete = append(toDelete, id)
310310+ }
311311+ }
312312+ rows.Close()
313313+314314+ // Delete tags not in keep list
315315+ for _, id := range toDelete {
316316+ if _, err := tx.Exec(`DELETE FROM tags WHERE id = ?`, id); err != nil {
317317+ return err
318318+ }
319319+ }
320320+321321+ return tx.Commit()
322322+}
323323+178324// InsertManifest inserts a new manifest record
179325func InsertManifest(db *sql.DB, manifest *Manifest) (int64, error) {
180326 result, err := db.Exec(`
···311457312458 return count > 0, nil
313459}
460460+461461+// BackfillState represents the backfill progress
462462+type BackfillState struct {
463463+ StartCursor int64
464464+ CurrentCursor int64
465465+ Completed bool
466466+ UpdatedAt time.Time
467467+}
468468+469469+// GetBackfillState retrieves the backfill state
470470+func GetBackfillState(db *sql.DB) (*BackfillState, error) {
471471+ var state BackfillState
472472+ var updatedAtStr string
473473+474474+ err := db.QueryRow(`
475475+ SELECT start_cursor, current_cursor, completed, updated_at
476476+ FROM backfill_state
477477+ WHERE id = 1
478478+ `).Scan(&state.StartCursor, &state.CurrentCursor, &state.Completed, &updatedAtStr)
479479+480480+ if err == sql.ErrNoRows {
481481+ return nil, nil // No backfill state exists
482482+ }
483483+ if err != nil {
484484+ return nil, err
485485+ }
486486+487487+ // Parse timestamp
488488+ if updatedAtStr != "" {
489489+ formats := []string{
490490+ time.RFC3339Nano,
491491+ "2006-01-02 15:04:05.999999999-07:00",
492492+ "2006-01-02 15:04:05.999999999",
493493+ time.RFC3339,
494494+ "2006-01-02 15:04:05",
495495+ }
496496+ for _, format := range formats {
497497+ if t, err := time.Parse(format, updatedAtStr); err == nil {
498498+ state.UpdatedAt = t
499499+ break
500500+ }
501501+ }
502502+ }
503503+504504+ return &state, nil
505505+}
506506+507507+// UpsertBackfillState updates or creates backfill state
508508+func UpsertBackfillState(db *sql.DB, state *BackfillState) error {
509509+ _, err := db.Exec(`
510510+ INSERT INTO backfill_state (id, start_cursor, current_cursor, completed, updated_at)
511511+ VALUES (1, ?, ?, ?, datetime('now'))
512512+ ON CONFLICT(id) DO UPDATE SET
513513+ start_cursor = excluded.start_cursor,
514514+ current_cursor = excluded.current_cursor,
515515+ completed = excluded.completed,
516516+ updated_at = excluded.updated_at
517517+ `, state.StartCursor, state.CurrentCursor, state.Completed)
518518+ return err
519519+}
520520+521521+// UpdateBackfillCursor updates just the current cursor position
522522+func UpdateBackfillCursor(db *sql.DB, cursor int64) error {
523523+ _, err := db.Exec(`
524524+ UPDATE backfill_state
525525+ SET current_cursor = ?, updated_at = datetime('now')
526526+ WHERE id = 1
527527+ `, cursor)
528528+ return err
529529+}
530530+531531+// MarkBackfillCompleted marks the backfill as completed
532532+func MarkBackfillCompleted(db *sql.DB) error {
533533+ _, err := db.Exec(`
534534+ UPDATE backfill_state
535535+ SET completed = 1, updated_at = datetime('now')
536536+ WHERE id = 1
537537+ `)
538538+ return err
539539+}
+8
pkg/appview/db/schema.go
···6363 cursor INTEGER NOT NULL,
6464 updated_at TIMESTAMP NOT NULL
6565);
6666+6767+CREATE TABLE IF NOT EXISTS backfill_state (
6868+ id INTEGER PRIMARY KEY CHECK (id = 1),
6969+ start_cursor INTEGER NOT NULL,
7070+ current_cursor INTEGER NOT NULL,
7171+ completed BOOLEAN NOT NULL DEFAULT 0,
7272+ updated_at TIMESTAMP NOT NULL
7373+);
6674`
67756876// InitDB initializes the SQLite database with the schema