Files
at-container-registry/pkg/appview/db/schema.go
T

174 lines
4.7 KiB
Go

package db
import (
"database/sql"
"strings"
_ "github.com/mattn/go-sqlite3"
)
const schema = `
CREATE TABLE IF NOT EXISTS users (
did TEXT PRIMARY KEY,
handle TEXT NOT NULL,
pds_endpoint TEXT NOT NULL,
avatar TEXT,
last_seen TIMESTAMP NOT NULL,
UNIQUE(handle)
);
CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle);
CREATE TABLE IF NOT EXISTS manifests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
repository TEXT NOT NULL,
digest TEXT NOT NULL,
hold_endpoint TEXT NOT NULL,
schema_version INTEGER NOT NULL,
media_type TEXT NOT NULL,
config_digest TEXT,
config_size INTEGER,
raw_manifest TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE(did, repository, digest),
FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_manifests_did_repo ON manifests(did, repository);
CREATE INDEX IF NOT EXISTS idx_manifests_created_at ON manifests(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_manifests_digest ON manifests(digest);
CREATE TABLE IF NOT EXISTS layers (
manifest_id INTEGER NOT NULL,
digest TEXT NOT NULL,
size INTEGER NOT NULL,
media_type TEXT NOT NULL,
layer_index INTEGER NOT NULL,
PRIMARY KEY(manifest_id, layer_index),
FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_layers_digest ON layers(digest);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
repository TEXT NOT NULL,
tag TEXT NOT NULL,
digest TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE(did, repository, tag),
FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_tags_did_repo ON tags(did, repository);
CREATE TABLE IF NOT EXISTS firehose_cursor (
id INTEGER PRIMARY KEY CHECK (id = 1),
cursor INTEGER NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS backfill_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
start_cursor INTEGER NOT NULL,
current_cursor INTEGER NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL
);
`
// InitDB initializes the SQLite database with the schema
func InitDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, err
}
// Enable foreign keys
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
return nil, err
}
// Create schema
if _, err := db.Exec(schema); err != nil {
return nil, err
}
// Migration: Add avatar column if it doesn't exist
_, err = db.Exec(`ALTER TABLE users ADD COLUMN avatar TEXT`)
// Ignore error if column already exists
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
// Log but don't fail - column might already exist
}
// Migration: Convert old cdn.bsky.app avatar URLs to imgs.blue
if err := migrateCDNURLs(db); err != nil {
// Log but don't fail - not critical
println("Warning: Failed to migrate CDN URLs:", err.Error())
}
return db, nil
}
// migrateCDNURLs converts old cdn.bsky.app avatar URLs to imgs.blue format
// Old format: https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/bafkreibxuy73...@jpeg
// New format: https://imgs.blue/did:plc:abc123/bafkreibxuy73...
func migrateCDNURLs(db *sql.DB) error {
// Find all users with cdn.bsky.app avatars
rows, err := db.Query(`SELECT did, avatar FROM users WHERE avatar LIKE 'https://cdn.bsky.app/%'`)
if err != nil {
return err
}
defer rows.Close()
updates := []struct {
did string
newURL string
}{}
for rows.Next() {
var did, oldURL string
if err := rows.Scan(&did, &oldURL); err != nil {
continue
}
// Extract CID from old URL
// Format: https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/bafkreibxuy73...@jpeg
parts := strings.Split(oldURL, "/")
if len(parts) < 7 {
continue
}
// Get the last part which contains CID@format
cidPart := parts[len(parts)-1]
// Strip off @jpeg or @png suffix
cid := strings.Split(cidPart, "@")[0]
// Construct new imgs.blue URL
newURL := "https://imgs.blue/" + did + "/" + cid
updates = append(updates, struct {
did string
newURL string
}{did, newURL})
}
// Update all users
stmt, err := db.Prepare(`UPDATE users SET avatar = ? WHERE did = ?`)
if err != nil {
return err
}
defer stmt.Close()
for _, update := range updates {
if _, err := stmt.Exec(update.newURL, update.did); err != nil {
// Log but continue
println("Warning: Failed to update avatar for", update.did, ":", err.Error())
}
}
if len(updates) > 0 {
println("Migrated", len(updates), "avatar URLs from cdn.bsky.app to imgs.blue")
}
return nil
}