94 lines
2.3 KiB
Go
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()
|
|
}
|