package database import ( "context" "database/sql" "fmt" "sync" _ "modernc.org/sqlite" ) type DB struct { conn *sql.DB writeLock sync.Mutex } func New(ctx context.Context, path string) (*DB, error) { conn, err := sql.Open("sqlite", path+"?_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=5000") if err != nil { return nil, fmt.Errorf("opening database: %w", err) } if err := conn.PingContext(ctx); err != nil { if closeErr := conn.Close(); closeErr != nil { Fatal("failed to close database connection: %v", closeErr) } return nil, fmt.Errorf("pinging database: %w", err) } db := &DB{conn: conn} if err := db.createSchema(ctx); err != nil { if closeErr := conn.Close(); closeErr != nil { Fatal("failed to close database connection: %v", closeErr) } return nil, fmt.Errorf("creating schema: %w", err) } return db, nil } func (db *DB) Close() error { if err := db.conn.Close(); err != nil { Fatal("failed to close database: %v", err) } return nil } func (db *DB) Conn() *sql.DB { return db.conn } func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { return db.conn.BeginTx(ctx, opts) } // LockForWrite acquires the write lock func (db *DB) LockForWrite() { db.writeLock.Lock() } // UnlockWrite releases the write lock func (db *DB) UnlockWrite() { db.writeLock.Unlock() } // ExecWithLock executes a write query with the write lock held func (db *DB) ExecWithLock(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { db.writeLock.Lock() defer db.writeLock.Unlock() return db.conn.ExecContext(ctx, query, args...) } // QueryRowWithLock executes a write query that returns a row with the write lock held func (db *DB) QueryRowWithLock(ctx context.Context, query string, args ...interface{}) *sql.Row { db.writeLock.Lock() defer db.writeLock.Unlock() return db.conn.QueryRowContext(ctx, query, args...) } func (db *DB) createSchema(ctx context.Context) error { schema := ` CREATE TABLE IF NOT EXISTS files ( path TEXT PRIMARY KEY, mtime INTEGER NOT NULL, ctime INTEGER NOT NULL, size INTEGER NOT NULL, mode INTEGER NOT NULL, uid INTEGER NOT NULL, gid INTEGER NOT NULL, link_target TEXT ); CREATE TABLE IF NOT EXISTS file_chunks ( path TEXT NOT NULL, idx INTEGER NOT NULL, chunk_hash TEXT NOT NULL, PRIMARY KEY (path, idx) ); CREATE TABLE IF NOT EXISTS chunks ( chunk_hash TEXT PRIMARY KEY, sha256 TEXT NOT NULL, size INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS blobs ( blob_hash TEXT PRIMARY KEY, created_ts INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS blob_chunks ( blob_hash TEXT NOT NULL, chunk_hash TEXT NOT NULL, offset INTEGER NOT NULL, length INTEGER NOT NULL, PRIMARY KEY (blob_hash, chunk_hash) ); CREATE TABLE IF NOT EXISTS chunk_files ( chunk_hash TEXT NOT NULL, file_path TEXT NOT NULL, file_offset INTEGER NOT NULL, length INTEGER NOT NULL, PRIMARY KEY (chunk_hash, file_path) ); CREATE TABLE IF NOT EXISTS snapshots ( id TEXT PRIMARY KEY, hostname TEXT NOT NULL, vaultik_version TEXT NOT NULL, created_ts INTEGER NOT NULL, file_count INTEGER NOT NULL, chunk_count INTEGER NOT NULL, blob_count INTEGER NOT NULL, total_size INTEGER NOT NULL, blob_size INTEGER NOT NULL, compression_ratio REAL NOT NULL ); ` _, err := db.conn.ExecContext(ctx, schema) return err } // NewTestDB creates an in-memory SQLite database for testing func NewTestDB() (*DB, error) { return New(context.Background(), ":memory:") }