repos / pico

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

pico / pkg / apps / pgs / db
Eric Bower  ·  2026-03-05

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
 62CREATE TABLE IF NOT EXISTS form_entries (
 63	id INTEGER PRIMARY KEY AUTOINCREMENT,
 64	user_id INTEGER NOT NULL,
 65	name TEXT NOT NULL,
 66	data BLOB NOT NULL,
 67	created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
 68	CONSTRAINT form_entries_user_id_fk
 69		FOREIGN KEY(user_id) REFERENCES app_users(id)
 70		ON DELETE CASCADE
 71		ON UPDATE CASCADE
 72);
 73`
 74
 75var sqliteMigrations = []string{
 76	"", // migration #0 is reserved for schema initialization
 77}
 78
 79func NewSqliteDB(databaseUrl string, logger *slog.Logger) (*PgsPsqlDB, error) {
 80	var err error
 81	d := &PgsPsqlDB{
 82		Logger: logger,
 83	}
 84	d.Logger.Info("connecting to sqlite", "databaseUrl", databaseUrl)
 85
 86	db, err := SqliteOpen(databaseUrl, logger)
 87	if err != nil {
 88		return nil, err
 89	}
 90
 91	d.Db = db
 92	return d, nil
 93}
 94
 95// Open opens a database connection.
 96func SqliteOpen(dsn string, logger *slog.Logger) (*sqlx.DB, error) {
 97	logger.Info("opening db file", "dsn", dsn)
 98	db, err := sqlx.Connect("sqlite", dsn)
 99	if err != nil {
100		return nil, err
101	}
102
103	err = sqliteUpgrade(db)
104	if err != nil {
105		_ = db.Close()
106		return nil, err
107	}
108
109	return db, nil
110}
111
112func sqliteUpgrade(db *sqlx.DB) error {
113	var version int
114	if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
115		return fmt.Errorf("failed to query schema version: %v", err)
116	}
117
118	if version == len(sqliteMigrations) {
119		return nil
120	} else if version > len(sqliteMigrations) {
121		return fmt.Errorf("(version %d) older than schema (version %d)", len(sqliteMigrations), version)
122	}
123
124	tx, err := db.Beginx()
125	if err != nil {
126		return err
127	}
128	defer func() {
129		_ = tx.Rollback()
130	}()
131
132	if version == 0 {
133		if _, err := tx.Exec(sqliteSchema); err != nil {
134			return fmt.Errorf("failed to initialize schema: %v", err)
135		}
136	} else {
137		for i := version; i < len(sqliteMigrations); i++ {
138			if _, err := tx.Exec(sqliteMigrations[i]); err != nil {
139				return fmt.Errorf("failed to execute migration #%v: %v", i, err)
140			}
141		}
142	}
143
144	// For some reason prepared statements don't work here
145	_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", len(sqliteMigrations)))
146	if err != nil {
147		return fmt.Errorf("failed to bump schema version: %v", err)
148	}
149
150	return tx.Commit()
151}