Files
lpra/internal/server/dbx.go
T
2026-10-15 22:48:03 +03:00

94 lines
2.3 KiB
Go

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()
}