repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / apps / pgs / db
Eric Bower  ·  2025-07-04

sqlite.go

  1package pgsdb
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6
  7	"github.com/jmoiron/sqlx"
  8	_ "modernc.org/sqlite"
  9)
 10
 11var sqliteSchema = `
 12CREATE TABLE IF NOT EXISTS app_users (
 13	id INTEGER PRIMARY KEY AUTOINCREMENT,
 14	name TEXT NOT NULL,
 15	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 16	UNIQUE(name)
 17);
 18
 19CREATE TABLE IF NOT EXISTS public_keys (
 20	id INTEGER PRIMARY KEY AUTOINCREMENT,
 21	user_id INTEGER NOT NULL,
 22	public_key TEXT NOT NULL UNIQUE,
 23	name TEXT NOT NULL,
 24	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 25	UNIQUE (user_id, public_key),
 26	CONSTRAINT public_keys_user_id_fk
 27		FOREIGN KEY(user_id) REFERENCES app_users(id)
 28		ON DELETE CASCADE
 29		ON UPDATE CASCADE
 30);
 31
 32CREATE TABLE IF NOT EXISTS projects (
 33	id INTEGER PRIMARY KEY AUTOINCREMENT,
 34	user_id INTEGER NOT NULL,
 35	name TEXT NOT NULL,
 36	project_dir TEXT NOT NULL DEFAULT '',
 37	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 38	updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 39	acl BLOB DEFAULT '{"data": [], "type": "public"}' NOT NULL,
 40	blocked TEXT NOT NULL DEFAULT '',
 41	UNIQUE (user_id, name),
 42	CONSTRAINT projects_user_id_fk
 43		FOREIGN KEY(user_id) REFERENCES app_users(id)
 44		ON DELETE CASCADE
 45		ON UPDATE CASCADE
 46);
 47
 48CREATE TABLE IF NOT EXISTS feature_flags (
 49	id INTEGER PRIMARY KEY AUTOINCREMENT,
 50	user_id INTEGER NOT NULL,
 51	name TEXT NOT NULL,
 52	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 53	expires_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 54	data BLOB DEFAULT '{}' NOT NULL,
 55	payment_history_id INTEGER,
 56	CONSTRAINT feature_flags_user_id_fk
 57		FOREIGN KEY(user_id) REFERENCES app_users(id)
 58		ON DELETE CASCADE
 59		ON UPDATE CASCADE
 60);
 61`
 62
 63var sqliteMigrations = []string{
 64	"", // migration #0 is reserved for schema initialization
 65}
 66
 67func NewSqliteDB(databaseUrl string, logger *slog.Logger) (*PgsPsqlDB, error) {
 68	var err error
 69	d := &PgsPsqlDB{
 70		Logger: logger,
 71	}
 72	d.Logger.Info("connecting to sqlite", "databaseUrl", databaseUrl)
 73
 74	db, err := SqliteOpen(databaseUrl, logger)
 75	if err != nil {
 76		return nil, err
 77	}
 78
 79	d.Db = db
 80	return d, nil
 81}
 82
 83// Open opens a database connection.
 84func SqliteOpen(dsn string, logger *slog.Logger) (*sqlx.DB, error) {
 85	logger.Info("opening db file", "dsn", dsn)
 86	db, err := sqlx.Connect("sqlite", dsn)
 87	if err != nil {
 88		return nil, err
 89	}
 90
 91	err = sqliteUpgrade(db)
 92	if err != nil {
 93		_ = db.Close()
 94		return nil, err
 95	}
 96
 97	return db, nil
 98}
 99
100func sqliteUpgrade(db *sqlx.DB) error {
101	var version int
102	if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
103		return fmt.Errorf("failed to query schema version: %v", err)
104	}
105
106	if version == len(sqliteMigrations) {
107		return nil
108	} else if version > len(sqliteMigrations) {
109		return fmt.Errorf("(version %d) older than schema (version %d)", len(sqliteMigrations), version)
110	}
111
112	tx, err := db.Beginx()
113	if err != nil {
114		return err
115	}
116	defer func() {
117		_ = tx.Rollback()
118	}()
119
120	if version == 0 {
121		if _, err := tx.Exec(sqliteSchema); err != nil {
122			return fmt.Errorf("failed to initialize schema: %v", err)
123		}
124	} else {
125		for i := version; i < len(sqliteMigrations); i++ {
126			if _, err := tx.Exec(sqliteMigrations[i]); err != nil {
127				return fmt.Errorf("failed to execute migration #%v: %v", i, err)
128			}
129		}
130	}
131
132	// For some reason prepared statements don't work here
133	_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", len(sqliteMigrations)))
134	if err != nil {
135		return fmt.Errorf("failed to bump schema version: %v", err)
136	}
137
138	return tx.Commit()
139}