// Package db provides a lightweight SQLite-backed store for data that // doesn't fit the plain-file model used by internal/store — currently // calendar/address-book sharing grants. It's intentionally small: no ORM, // just database/sql with hand-written queries, so it stays easy to // extend when user management and the web UI are added later. package db import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) // DB wraps a SQLite connection and exposes typed helpers for the // application's tables. type DB struct { conn *sql.DB } // Open opens (creating if necessary) the SQLite database at path and runs // schema migrations. func Open(path string) (*DB, error) { conn, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("opening database %q: %w", path, err) } // SQLite only supports one writer at a time; a single connection avoids // "database is locked" errors under concurrent access. conn.SetMaxOpenConns(1) d := &DB{conn: conn} if err := d.migrate(); err != nil { conn.Close() return nil, fmt.Errorf("migrating database: %w", err) } return d, nil } // Close closes the underlying connection. func (d *DB) Close() error { return d.conn.Close() } func (d *DB) migrate() error { const schema = ` CREATE TABLE IF NOT EXISTS calendar_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner TEXT NOT NULL, calendar_name TEXT NOT NULL, shared_with TEXT NOT NULL, permission TEXT NOT NULL CHECK (permission IN ('read', 'write')), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (owner, calendar_name, shared_with) ); CREATE TABLE IF NOT EXISTS addressbook_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner TEXT NOT NULL, addressbook_name TEXT NOT NULL, shared_with TEXT NOT NULL, permission TEXT NOT NULL CHECK (permission IN ('read', 'write')), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (owner, addressbook_name, shared_with) ); ` _, err := d.conn.Exec(schema) return err }