package server import ( "database/sql" "strconv" "strings" ) // dbx is the database the server talks to: SQLite, or PostgreSQL. The // queries are written once, for SQLite; on PostgreSQL each is rewritten // (pgSQL) on its way. type dbx struct { *sql.DB pg bool } func (d *dbx) q(query string) string { if d.pg { return pgSQL(query) } return query } func (d *dbx) Exec(query string, args ...any) (sql.Result, error) { return d.DB.Exec(d.q(query), args...) } func (d *dbx) Query(query string, args ...any) (*sql.Rows, error) { return d.DB.Query(d.q(query), args...) } func (d *dbx) QueryRow(query string, args ...any) *sql.Row { return d.DB.QueryRow(d.q(query), args...) } func (d *dbx) Begin() (*txx, error) { tx, err := d.DB.Begin() return &txx{Tx: tx, pg: d.pg}, err } // insert runs an INSERT and gives the new row's id (LastInsertId on SQLite, // RETURNING id on PostgreSQL, which has no LastInsertId). func (d *dbx) insert(query string, args ...any) (int64, error) { if d.pg { var id int64 err := d.DB.QueryRow(pgSQL(query)+" RETURNING id", args...).Scan(&id) return id, err } res, err := d.DB.Exec(query, args...) if err != nil { return 0, err } return res.LastInsertId() } type txx struct { *sql.Tx pg bool } func (t *txx) Exec(query string, args ...any) (sql.Result, error) { if t.pg { query = pgSQL(query) } return t.Tx.Exec(query, args...) } // pgSQL rewrites a query for SQLite into PostgreSQL's: $n placeholders, // INSERT OR IGNORE as ON CONFLICT DO NOTHING, and in table definitions // BIGSERIAL keys, BIGINT numbers (times are Unix seconds), BYTEA blobs and // ADD COLUMN IF NOT EXISTS. func pgSQL(q string) string { if strings.HasPrefix(q, "INSERT OR IGNORE ") { q = "INSERT " + strings.TrimPrefix(q, "INSERT OR IGNORE ") + " ON CONFLICT DO NOTHING" } if strings.Contains(q, "CREATE TABLE") || strings.Contains(q, "ALTER TABLE") { q = strings.ReplaceAll(q, "INTEGER PRIMARY KEY AUTOINCREMENT", "BIGSERIAL PRIMARY KEY") q = strings.ReplaceAll(q, "INTEGER", "BIGINT") q = strings.ReplaceAll(q, "BLOB", "BYTEA") q = strings.ReplaceAll(q, "ADD COLUMN ", "ADD COLUMN IF NOT EXISTS ") } var b strings.Builder n, quoted := 0, false for _, r := range q { switch { case r == '\'': quoted = !quoted case r == '?' && !quoted: n++ b.WriteString("$" + strconv.Itoa(n)) continue } b.WriteRune(r) } return b.String() }