~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
911 lines · 24.7 KB Raw History
package repo

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/bluesky-social/indigo/atproto/atcrypto"
	indigorepo "github.com/bluesky-social/indigo/atproto/repo"
	"github.com/bluesky-social/indigo/atproto/repo/mst"
	"github.com/bluesky-social/indigo/atproto/syntax"
	blocks "github.com/ipfs/go-block-format"
	"github.com/ipfs/go-cid"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
	_ "modernc.org/sqlite"
)

const schemaRepos = `CREATE TABLE IF NOT EXISTS atrepo_repos (
	did TEXT PRIMARY KEY,
	root TEXT NOT NULL,
	commit_cid TEXT NOT NULL,
	rev TEXT NOT NULL,
	pubkey TEXT NOT NULL,
	created_at INTEGER NOT NULL
)`

const schemaBlocks = `CREATE TABLE IF NOT EXISTS atrepo_blocks (
	did TEXT NOT NULL,
	cid TEXT NOT NULL,
	data BLOB NOT NULL,
	PRIMARY KEY (did, cid)
) WITHOUT ROWID`

const schemaCommits = `CREATE TABLE IF NOT EXISTS atrepo_commits (
	seq INTEGER PRIMARY KEY AUTOINCREMENT,
	did TEXT NOT NULL,
	cid TEXT NOT NULL,
	rev TEXT NOT NULL,
	data TEXT NOT NULL,
	prev TEXT NOT NULL,
	created_at INTEGER NOT NULL,
	UNIQUE (did, rev),
	UNIQUE (did, cid)
)`

const schemaCommitsIndex = `CREATE INDEX IF NOT EXISTS atrepo_commits_did_seq ON atrepo_commits (did, seq)`

const schemaEvents = `CREATE TABLE IF NOT EXISTS atrepo_events (
	seq INTEGER PRIMARY KEY AUTOINCREMENT,
	did TEXT NOT NULL,
	rev TEXT NOT NULL,
	since_rev TEXT NOT NULL,
	commit_cid TEXT NOT NULL,
	data TEXT NOT NULL,
	prev_data TEXT NOT NULL,
	blocks BLOB NOT NULL,
	ops TEXT NOT NULL,
	blobs TEXT NOT NULL,
	created_at INTEGER NOT NULL,
	UNIQUE (did, rev)
)`

const schemaEventsIndex = `CREATE INDEX IF NOT EXISTS atrepo_events_did_seq ON atrepo_events (did, seq)`

type SQLiteStore struct {
	db      *sql.DB
	writeMu sync.Mutex
	sink    EventSink
	clk     *syntax.TIDClock
}

type repoRow struct {
	root   cid.Cid
	commit cid.Cid
	rev    syntax.TID
	pubkey string
}

func NewSQLiteStore(ctx context.Context, path string) (*SQLiteStore, error) {
	if path == "" {
		return nil, fmt.Errorf("repo: empty sqlite path")
	}
	dsn := ""
	if path == ":memory:" {
		dsn = "file::memory:?cache=shared&_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)"
	} else {
		dir := filepath.Dir(path)
		if dir != "." && dir != "" {
			if err := os.MkdirAll(dir, 0o700); err != nil {
				return nil, fmt.Errorf("repo: creating sqlite directory: %w", err)
			}
		}
		dsn = "file:" + path + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
	}
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("repo: opening sqlite: %w", err)
	}
	if path == ":memory:" {
		db.SetMaxOpenConns(1)
	} else {
		db.SetMaxOpenConns(4)
	}
	db.SetMaxIdleConns(4)
	s := &SQLiteStore{
		db:  db,
		clk: syntax.NewTIDClock(0),
	}
	if err := s.init(ctx); err != nil {
		db.Close()
		return nil, err
	}
	return s, nil
}

func (s *SQLiteStore) init(ctx context.Context) error {
	for _, stmt := range []string{
		schemaRepos,
		schemaBlocks,
		schemaCommits,
		schemaCommitsIndex,
		schemaEvents,
		schemaEventsIndex,
	} {
		if _, err := s.db.ExecContext(ctx, stmt); err != nil {
			return fmt.Errorf("repo: creating schema: %w", err)
		}
	}
	return nil
}

func (s *SQLiteStore) Close() error {
	return s.db.Close()
}

func (s *SQLiteStore) SetEventSink(sink EventSink) {
	s.writeMu.Lock()
	s.sink = sink
	s.writeMu.Unlock()
}

func (s *SQLiteStore) nextTID() syntax.TID {
	return s.clk.Next()
}

func (s *SQLiteStore) CreateRepo(ctx context.Context, did syntax.DID, key atcrypto.PrivateKey) (Repo, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	if key == nil {
		return nil, fmt.Errorf("%w: signing key required", ErrInvalidRequest)
	}
	pub, err := key.PublicKey()
	if err != nil {
		return nil, fmt.Errorf("repo: deriving public key: %w", err)
	}
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	exists, err := s.repoExists(ctx, d)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, ErrRepoExists
	}
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("repo: beginning create: %w", err)
	}
	defer tx.Rollback()
	bs := newTxBlockstore(ctx, tx, d.String())
	tree := mst.NewEmptyTree()
	root, err := tree.WriteDiffBlocks(ctx, bs)
	if err != nil {
		return nil, fmt.Errorf("repo: writing empty mst: %w", err)
	}
	rev := s.nextTID()
	commit := &Commit{
		DID:     d.String(),
		Version: indigorepo.ATPROTO_REPO_VERSION,
		Data:    *root,
		Rev:     rev.String(),
	}
	if err := commit.Sign(key); err != nil {
		return nil, fmt.Errorf("repo: signing genesis commit: %w", err)
	}
	cdata, ccid, err := marshalCommit(commit)
	if err != nil {
		return nil, fmt.Errorf("repo: encoding genesis commit: %w", err)
	}
	cblk, err := blocks.NewBlockWithCid(cdata, ccid)
	if err != nil {
		return nil, fmt.Errorf("repo: building genesis commit block: %w", err)
	}
	if err := bs.Put(ctx, cblk); err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	if _, err := tx.ExecContext(ctx,
		"INSERT INTO atrepo_repos (did, root, commit_cid, rev, pubkey, created_at) VALUES (?, ?, ?, ?, ?, ?)",
		d.String(), root.String(), ccid.String(), rev.String(), pub.Multibase(), now.UnixMicro()); err != nil {
		return nil, fmt.Errorf("repo: inserting repo: %w", err)
	}
	cr := &CommitRecord{
		DID:    d,
		CID:    ccid,
		Rev:    rev,
		Data:   *root,
		Signed: commit,
		Time:   now,
	}
	if err := insertCommitRow(ctx, tx, d.String(), cr); err != nil {
		return nil, err
	}
	carBytes, err := WriteCAR(ccid, bs.orderedFrom(ccid))
	if err != nil {
		return nil, err
	}
	ev := &CommitEvent{
		Repo:   d,
		Commit: ccid,
		Rev:    rev,
		Data:   *root,
		Blocks: carBytes,
		Time:   now,
	}
	seq, err := insertEventRow(ctx, tx, ev)
	if err != nil {
		return nil, err
	}
	ev.Seq = seq
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("repo: committing repo creation: %w", err)
	}
	handle := &repoHandle{store: s, did: d, key: key, pub: pub}
	if sink := s.sink; sink != nil {
		if err := sink.OnCommit(ctx, ev); err != nil {
			return handle, err
		}
	}
	return handle, nil
}

func (s *SQLiteStore) OpenRepo(ctx context.Context, did syntax.DID, key atcrypto.PrivateKey) (Repo, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	row, err := s.loadRepoRow(ctx, d)
	if err != nil {
		return nil, err
	}
	pub, err := atcrypto.ParsePublicMultibase(row.pubkey)
	if err != nil {
		return nil, fmt.Errorf("%w: stored public key: %v", ErrCorruptRepo, err)
	}
	cr, err := s.loadCommitByCID(ctx, s.db, d.String(), row.commit)
	if err != nil {
		return nil, err
	}
	if cr.Rev != row.rev || cr.Data != row.root {
		return nil, fmt.Errorf("%w: repo head does not match stored commit", ErrCorruptRepo)
	}
	if key != nil {
		kp, err := key.PublicKey()
		if err != nil {
			return nil, fmt.Errorf("repo: deriving public key: %w", err)
		}
		if !kp.Equal(pub) {
			return nil, fmt.Errorf("%w: signing key does not match repo key", ErrInvalidRequest)
		}
	}
	return &repoHandle{store: s, did: d, key: key, pub: pub}, nil
}

func (s *SQLiteStore) DeleteRepo(ctx context.Context, did syntax.DID) error {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("repo: beginning delete: %w", err)
	}
	defer tx.Rollback()
	for _, table := range []string{"atrepo_events", "atrepo_commits", "atrepo_blocks"} {
		if _, err := tx.ExecContext(ctx, "DELETE FROM "+table+" WHERE did = ?", d.String()); err != nil {
			return fmt.Errorf("repo: deleting from %s: %w", table, err)
		}
	}
	res, err := tx.ExecContext(ctx, "DELETE FROM atrepo_repos WHERE did = ?", d.String())
	if err != nil {
		return fmt.Errorf("repo: deleting repo: %w", err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrRepoNotFound
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("repo: committing delete: %w", err)
	}
	return nil
}

func (s *SQLiteStore) RepoExists(ctx context.Context, did syntax.DID) (bool, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return false, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	return s.repoExists(ctx, d)
}

func (s *SQLiteStore) repoExists(ctx context.Context, did syntax.DID) (bool, error) {
	var one int
	err := s.db.QueryRowContext(ctx, "SELECT 1 FROM atrepo_repos WHERE did = ?", did.String()).Scan(&one)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

func (s *SQLiteStore) ListRepos(ctx context.Context) ([]syntax.DID, error) {
	rows, err := s.db.QueryContext(ctx, "SELECT did FROM atrepo_repos ORDER BY did")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []syntax.DID{}
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			return nil, err
		}
		d, err := syntax.ParseDID(raw)
		if err != nil {
			return nil, fmt.Errorf("%w: stored did: %v", ErrCorruptRepo, err)
		}
		out = append(out, d)
	}
	return out, rows.Err()
}

func (s *SQLiteStore) loadRepoRow(ctx context.Context, did syntax.DID) (*repoRow, error) {
	var rootRaw, commitRaw, revRaw, pubkey string
	err := s.db.QueryRowContext(ctx,
		"SELECT root, commit_cid, rev, pubkey FROM atrepo_repos WHERE did = ?", did.String()).
		Scan(&rootRaw, &commitRaw, &revRaw, &pubkey)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrRepoNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("repo: loading repo: %w", err)
	}
	root, err := parseCID(rootRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: stored root: %v", ErrCorruptRepo, err)
	}
	commit, err := parseCID(commitRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: stored commit: %v", ErrCorruptRepo, err)
	}
	rev, err := syntax.ParseTID(revRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: stored rev: %v", ErrCorruptRepo, err)
	}
	return &repoRow{root: root, commit: commit, rev: rev, pubkey: pubkey}, nil
}

func insertCommitRow(ctx context.Context, tx *sql.Tx, did string, cr *CommitRecord) error {
	_, err := tx.ExecContext(ctx,
		"INSERT INTO atrepo_commits (did, cid, rev, data, prev, created_at) VALUES (?, ?, ?, ?, ?, ?)",
		did, cr.CID.String(), cr.Rev.String(), cr.Data.String(), formatCIDPtr(cr.Prev), cr.Time.UnixMicro())
	if err != nil {
		return fmt.Errorf("repo: inserting commit: %w", err)
	}
	return nil
}

func insertEventRow(ctx context.Context, tx *sql.Tx, ev *CommitEvent) (int64, error) {
	ops, err := encodeOps(ev.Ops)
	if err != nil {
		return 0, err
	}
	blobs, err := encodeCIDs(ev.Blobs)
	if err != nil {
		return 0, err
	}
	since := ""
	if ev.Since != nil {
		since = ev.Since.String()
	}
	res, err := tx.ExecContext(ctx,
		"INSERT INTO atrepo_events (did, rev, since_rev, commit_cid, data, prev_data, blocks, ops, blobs, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
		ev.Repo.String(), ev.Rev.String(), since, ev.Commit.String(), ev.Data.String(), formatCIDPtr(ev.PrevData), ev.Blocks, ops, blobs, ev.Time.UnixMicro())
	if err != nil {
		return 0, fmt.Errorf("repo: inserting event: %w", err)
	}
	return res.LastInsertId()
}

func (s *SQLiteStore) loadCommitByCID(ctx context.Context, q blockQueryer, did string, commitCID cid.Cid) (*CommitRecord, error) {
	var seq, created int64
	var revRaw, dataRaw, prevRaw string
	err := q.QueryRowContext(ctx,
		"SELECT seq, rev, data, prev, created_at FROM atrepo_commits WHERE did = ? AND cid = ?",
		did, commitCID.String()).Scan(&seq, &revRaw, &dataRaw, &prevRaw, &created)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrCommitNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("repo: loading commit: %w", err)
	}
	blk, err := readBlock(ctx, q, did, commitCID)
	if err != nil {
		return nil, err
	}
	commit, err := parseCommitBlock(blk.RawData())
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrCorruptRepo, err)
	}
	if err := commit.VerifyStructure(); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrCorruptRepo, err)
	}
	rev, err := syntax.ParseTID(revRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: commit rev: %v", ErrCorruptRepo, err)
	}
	data, err := parseCID(dataRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: commit data: %v", ErrCorruptRepo, err)
	}
	prev, err := parseCIDPtr(prevRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: commit prev: %v", ErrCorruptRepo, err)
	}
	if commit.DID != did || commit.Rev != rev.String() || commit.Data != data {
		return nil, fmt.Errorf("%w: commit fields do not match stored row", ErrCorruptRepo)
	}
	if formatCIDPtr(prev) != formatCIDPtr(commit.Prev) {
		return nil, fmt.Errorf("%w: commit prev does not match stored row", ErrCorruptRepo)
	}
	d, err := syntax.ParseDID(did)
	if err != nil {
		return nil, fmt.Errorf("%w: stored did: %v", ErrCorruptRepo, err)
	}
	return &CommitRecord{
		DID:    d,
		CID:    commitCID,
		Rev:    rev,
		Data:   data,
		Prev:   prev,
		Signed: commit,
		Seq:    seq,
		Time:   time.UnixMicro(created).UTC(),
	}, nil
}

func (s *SQLiteStore) Head(ctx context.Context, did syntax.DID) (*CommitRecord, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	row, err := s.loadRepoRow(ctx, d)
	if err != nil {
		return nil, err
	}
	cr, err := s.loadCommitByCID(ctx, s.db, d.String(), row.commit)
	if err != nil {
		return nil, err
	}
	if cr.Rev != row.rev || cr.Data != row.root {
		return nil, fmt.Errorf("%w: head does not match repo row", ErrCorruptRepo)
	}
	return cr, nil
}

func (s *SQLiteStore) GetCommit(ctx context.Context, did syntax.DID, rev syntax.TID) (*CommitRecord, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	r, err := syntax.ParseTID(string(rev))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	var cidRaw string
	err = s.db.QueryRowContext(ctx, "SELECT cid FROM atrepo_commits WHERE did = ? AND rev = ?", d.String(), r.String()).Scan(&cidRaw)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrCommitNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("repo: loading commit rev: %w", err)
	}
	c, err := parseCID(cidRaw)
	if err != nil {
		return nil, fmt.Errorf("%w: stored commit cid: %v", ErrCorruptRepo, err)
	}
	return s.loadCommitByCID(ctx, s.db, d.String(), c)
}

func (s *SQLiteStore) ListCommits(ctx context.Context, did syntax.DID, since *syntax.TID, limit int) ([]CommitRecord, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	limit = min(opt.Positive(limit, DefaultCommits), MaxCommits)
	sinceRaw := ""
	if since != nil {
		r, err := syntax.ParseTID(string(*since))
		if err != nil {
			return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
		}
		sinceRaw = r.String()
	}
	rows, err := s.db.QueryContext(ctx,
		"SELECT cid FROM atrepo_commits WHERE did = ? AND rev > ? ORDER BY seq ASC LIMIT ?",
		d.String(), sinceRaw, limit)
	if err != nil {
		return nil, err
	}
	var cids []cid.Cid
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			rows.Close()
			return nil, err
		}
		c, err := parseCID(raw)
		if err != nil {
			rows.Close()
			return nil, fmt.Errorf("%w: stored commit cid: %v", ErrCorruptRepo, err)
		}
		cids = append(cids, c)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, err
	}
	rows.Close()
	out := make([]CommitRecord, 0, len(cids))
	for _, c := range cids {
		cr, err := s.loadCommitByCID(ctx, s.db, d.String(), c)
		if err != nil {
			return nil, err
		}
		out = append(out, *cr)
	}
	return out, nil
}

func (s *SQLiteStore) ListEvents(ctx context.Context, since int64, limit int) ([]*CommitEvent, error) {
	limit = min(opt.Positive(limit, DefaultCommits), MaxCommits)
	rows, err := s.db.QueryContext(ctx,
		"SELECT seq, did, rev, since_rev, commit_cid, data, prev_data, blocks, ops, blobs, created_at FROM atrepo_events WHERE seq > ? ORDER BY seq ASC LIMIT ?",
		since, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []*CommitEvent{}
	for rows.Next() {
		var seq, created int64
		var didRaw, revRaw, sinceRaw, commitRaw, dataRaw, prevRaw string
		var carBytes, opsBytes, blobBytes []byte
		if err := rows.Scan(&seq, &didRaw, &revRaw, &sinceRaw, &commitRaw, &dataRaw, &prevRaw, &carBytes, &opsBytes, &blobBytes, &created); err != nil {
			return nil, err
		}
		d, err := syntax.ParseDID(didRaw)
		if err != nil {
			return nil, fmt.Errorf("%w: event did: %v", ErrCorruptRepo, err)
		}
		rev, err := syntax.ParseTID(revRaw)
		if err != nil {
			return nil, fmt.Errorf("%w: event rev: %v", ErrCorruptRepo, err)
		}
		var sincePtr *syntax.TID
		if sinceRaw != "" {
			s, err := syntax.ParseTID(sinceRaw)
			if err != nil {
				return nil, fmt.Errorf("%w: event since: %v", ErrCorruptRepo, err)
			}
			sincePtr = &s
		}
		commit, err := parseCID(commitRaw)
		if err != nil {
			return nil, fmt.Errorf("%w: event commit: %v", ErrCorruptRepo, err)
		}
		data, err := parseCID(dataRaw)
		if err != nil {
			return nil, fmt.Errorf("%w: event data: %v", ErrCorruptRepo, err)
		}
		prevData, err := parseCIDPtr(prevRaw)
		if err != nil {
			return nil, fmt.Errorf("%w: event prev data: %v", ErrCorruptRepo, err)
		}
		ops, err := decodeOps(opsBytes)
		if err != nil {
			return nil, fmt.Errorf("%w: event ops: %v", ErrCorruptRepo, err)
		}
		blobs, err := decodeCIDs(blobBytes)
		if err != nil {
			return nil, fmt.Errorf("%w: event blobs: %v", ErrCorruptRepo, err)
		}
		out = append(out, &CommitEvent{
			Repo:     d,
			Seq:      seq,
			Commit:   commit,
			Rev:      rev,
			Since:    sincePtr,
			Data:     data,
			PrevData: prevData,
			Blocks:   carBytes,
			Ops:      ops,
			Blobs:    blobs,
			Time:     time.UnixMicro(created).UTC(),
		})
	}
	return out, rows.Err()
}

func (s *SQLiteStore) headRoot(ctx context.Context, did syntax.DID) (cid.Cid, *CommitRecord, error) {
	head, err := s.Head(ctx, did)
	if err != nil {
		return cid.Undef, nil, err
	}
	return head.Data, head, nil
}

func (s *SQLiteStore) GetRecord(ctx context.Context, did syntax.DID, collection syntax.NSID, rkey syntax.RecordKey) (*Record, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	nsid, err := syntax.ParseNSID(string(collection))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	rk, err := syntax.ParseRecordKey(string(rkey))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	root, _, err := s.headRoot(ctx, d)
	if err != nil {
		return nil, err
	}
	br := &sqlBlockReader{db: s.db, did: d.String()}
	tree, err := loadFullTree(ctx, br, root)
	if err != nil {
		return nil, err
	}
	atr := &indigorepo.Repo{
		DID:         d,
		Clock:       syntax.NewTIDClock(0),
		RecordStore: br,
		MST:         *tree,
	}
	data, rc, err := atr.GetRecordBytes(ctx, nsid, rk)
	if errors.Is(err, indigorepo.ErrNotFound) {
		return nil, ErrRecordNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("repo: reading record: %w", err)
	}
	value, err := decodeRecord(data)
	if err != nil {
		return nil, fmt.Errorf("%w: record data: %v", ErrCorruptRepo, err)
	}
	return &Record{
		Collection: nsid,
		Rkey:       rk,
		CID:        *rc,
		Bytes:      data,
		Value:      value,
	}, nil
}

func (s *SQLiteStore) ListRecords(ctx context.Context, did syntax.DID, collection syntax.NSID, cursor syntax.RecordKey, limit int) ([]Record, *syntax.RecordKey, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	nsid, err := syntax.ParseNSID(string(collection))
	if err != nil {
		return nil, nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	limit = min(opt.Positive(limit, DefaultList), MaxList)
	cursorRaw := ""
	if cursor != "" {
		rk, err := syntax.ParseRecordKey(string(cursor))
		if err != nil {
			return nil, nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
		}
		cursorRaw = rk.String()
	}
	root, _, err := s.headRoot(ctx, d)
	if err != nil {
		return nil, nil, err
	}
	br := &sqlBlockReader{db: s.db, did: d.String()}
	tree, err := loadFullTree(ctx, br, root)
	if err != nil {
		return nil, nil, err
	}
	prefix := nsid.String() + "/"
	out := make([]Record, 0, limit)
	var last syntax.RecordKey
	more := false
	stop := errors.New("stop")
	err = tree.Walk(func(key []byte, val cid.Cid) error {
		path := string(key)
		if !strings.HasPrefix(path, prefix) {
			return nil
		}
		rkeyRaw := strings.TrimPrefix(path, prefix)
		rk, err := syntax.ParseRecordKey(rkeyRaw)
		if err != nil {
			return fmt.Errorf("%w: mst key: %v", ErrCorruptRepo, err)
		}
		if cursorRaw != "" && rk.String() <= cursorRaw {
			return nil
		}
		if len(out) == limit {
			more = true
			return stop
		}
		blk, err := br.Get(ctx, val)
		if err != nil {
			return err
		}
		value, err := decodeRecord(blk.RawData())
		if err != nil {
			return fmt.Errorf("%w: record data: %v", ErrCorruptRepo, err)
		}
		out = append(out, Record{
			Collection: nsid,
			Rkey:       rk,
			CID:        val,
			Bytes:      blk.RawData(),
			Value:      value,
		})
		last = rk
		return nil
	})
	if err != nil && !errors.Is(err, stop) {
		return nil, nil, err
	}
	var next *syntax.RecordKey
	if more {
		next = &last
	}
	return out, next, nil
}

func (s *SQLiteStore) CAR(ctx context.Context, did syntax.DID, since *syntax.TID) ([]byte, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	head, err := s.Head(ctx, d)
	if err != nil {
		return nil, err
	}
	if since == nil || *since == "" {
		return s.fullCAR(ctx, d, head)
	}
	sinceRev, err := syntax.ParseTID(string(*since))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	if _, err := s.GetCommit(ctx, d, sinceRev); errors.Is(err, ErrCommitNotFound) {
		return s.fullCAR(ctx, d, head)
	} else if err != nil {
		return nil, err
	}
	if sinceRev == head.Rev {
		blk, err := readBlock(ctx, s.db, d.String(), head.CID)
		if err != nil {
			return nil, err
		}
		return WriteCAR(head.CID, []blocks.Block{blk})
	}
	rows, err := s.db.QueryContext(ctx,
		"SELECT blocks FROM atrepo_events WHERE did = ? AND rev > ? ORDER BY seq ASC",
		d.String(), sinceRev.String())
	if err != nil {
		return nil, err
	}
	seen := make(map[cid.Cid]bool)
	var ordered []blocks.Block
	headBlk, err := readBlock(ctx, s.db, d.String(), head.CID)
	if err != nil {
		rows.Close()
		return nil, err
	}
	seen[head.CID] = true
	ordered = append(ordered, headBlk)
	for rows.Next() {
		var carBytes []byte
		if err := rows.Scan(&carBytes); err != nil {
			rows.Close()
			return nil, err
		}
		_, blks, err := ReadCARBytes(ctx, carBytes)
		if err != nil {
			rows.Close()
			return nil, fmt.Errorf("%w: event car: %v", ErrCorruptRepo, err)
		}
		for _, blk := range blks {
			if seen[blk.Cid()] {
				continue
			}
			seen[blk.Cid()] = true
			ordered = append(ordered, blk)
		}
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, err
	}
	rows.Close()
	return WriteCAR(head.CID, ordered)
}

func (s *SQLiteStore) fullCAR(ctx context.Context, did syntax.DID, head *CommitRecord) ([]byte, error) {
	rows, err := s.db.QueryContext(ctx, "SELECT cid, data FROM atrepo_blocks WHERE did = ? ORDER BY cid", did.String())
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	seen := make(map[cid.Cid]bool)
	var ordered []blocks.Block
	var headBlk blocks.Block
	for rows.Next() {
		var raw string
		var data []byte
		if err := rows.Scan(&raw, &data); err != nil {
			return nil, err
		}
		c, err := parseCID(raw)
		if err != nil {
			return nil, fmt.Errorf("%w: stored block cid: %v", ErrCorruptRepo, err)
		}
		if err := verifyBlock(c, data); err != nil {
			return nil, err
		}
		blk, err := blocks.NewBlockWithCid(data, c)
		if err != nil {
			return nil, err
		}
		if c == head.CID {
			headBlk = blk
			seen[c] = true
			continue
		}
		if seen[c] {
			continue
		}
		seen[c] = true
		ordered = append(ordered, blk)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if headBlk == nil {
		return nil, fmt.Errorf("%w: head commit block missing", ErrCorruptRepo)
	}
	ordered = append([]blocks.Block{headBlk}, ordered...)
	return WriteCAR(head.CID, ordered)
}

func (s *SQLiteStore) CommitCAR(ctx context.Context, did syntax.DID, commit cid.Cid) ([]byte, error) {
	d, err := syntax.ParseDID(string(did))
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
	}
	if !commit.Defined() {
		return nil, fmt.Errorf("%w: commit cid required", ErrInvalidRequest)
	}
	var carBytes []byte
	err = s.db.QueryRowContext(ctx,
		"SELECT blocks FROM atrepo_events WHERE did = ? AND commit_cid = ? ORDER BY seq DESC LIMIT 1",
		d.String(), commit.String()).Scan(&carBytes)
	if err == nil {
		return carBytes, nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return nil, err
	}
	blk, err := readBlock(ctx, s.db, d.String(), commit)
	if err != nil {
		return nil, err
	}
	return WriteCAR(commit, []blocks.Block{blk})
}

var _ RepoStore = (*SQLiteStore)(nil)