MVP: 按 07 风格重写前端 + Go 后端落地(长文/短文、编辑器、后台管理)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// Package db opens a *sql.DB for either SQLite or PostgreSQL and rewrites
|
||||
// the shared `?` placeholders into PostgreSQL's `$n` form.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type Dialect int
|
||||
|
||||
const (
|
||||
SQLite Dialect = iota
|
||||
Postgres
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
Dialect Dialect
|
||||
}
|
||||
|
||||
func Open(driver, dsn string) (*DB, error) {
|
||||
var d Dialect
|
||||
switch driver {
|
||||
case "sqlite":
|
||||
d = SQLite
|
||||
if err := os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsn = addSQLiteParams(dsn)
|
||||
case "postgres":
|
||||
d = Postgres
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported driver %q", driver)
|
||||
}
|
||||
|
||||
pool, err := sql.Open(driverName(driver), dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d == SQLite {
|
||||
// SQLite is single-writer; keep a small pool to avoid "database is locked".
|
||||
pool.SetMaxOpenConns(1)
|
||||
} else {
|
||||
pool.SetMaxOpenConns(10)
|
||||
}
|
||||
if err := pool.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("connect %s: %w", driver, err)
|
||||
}
|
||||
return &DB{DB: pool, Dialect: d}, nil
|
||||
}
|
||||
|
||||
func driverName(driver string) string {
|
||||
if driver == "postgres" {
|
||||
return "postgres"
|
||||
}
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
func addSQLiteParams(dsn string) string {
|
||||
if strings.HasPrefix(dsn, "file:") || strings.Contains(dsn, "?") {
|
||||
return dsn
|
||||
}
|
||||
return dsn + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
|
||||
}
|
||||
|
||||
// Rebind converts `?` placeholders to `$1..$n` on PostgreSQL.
|
||||
func (d *DB) Rebind(q string) string {
|
||||
if d.Dialect != Postgres {
|
||||
return q
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(q) + 8)
|
||||
n := 0
|
||||
for _, r := range q {
|
||||
if r == '?' {
|
||||
n++
|
||||
b.WriteString("$")
|
||||
b.WriteString(fmt.Sprint(n))
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (d *DB) Q(q string) string { return d.Rebind(q) }
|
||||
|
||||
// AutoInc returns the column definition for an auto-incrementing primary key.
|
||||
func (d *DB) AutoInc() string {
|
||||
if d.Dialect == Postgres {
|
||||
return "BIGSERIAL PRIMARY KEY"
|
||||
}
|
||||
return "INTEGER PRIMARY KEY AUTOINCREMENT"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRebind(t *testing.T) {
|
||||
sqlite := &DB{Dialect: SQLite}
|
||||
pg := &DB{Dialect: Postgres}
|
||||
|
||||
q := "SELECT * FROM posts WHERE kind = ? AND status = ? LIMIT ? OFFSET ?"
|
||||
|
||||
if got := sqlite.Rebind(q); got != q {
|
||||
t.Errorf("sqlite must keep ? placeholders, got %q", got)
|
||||
}
|
||||
|
||||
want := "SELECT * FROM posts WHERE kind = $1 AND status = $2 LIMIT $3 OFFSET $4"
|
||||
if got := pg.Rebind(q); got != want {
|
||||
t.Errorf("postgres rebind:\n got %q\nwant %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoInc(t *testing.T) {
|
||||
if got := (&DB{Dialect: SQLite}).AutoInc(); got != "INTEGER PRIMARY KEY AUTOINCREMENT" {
|
||||
t.Errorf("sqlite autoincrement = %q", got)
|
||||
}
|
||||
if got := (&DB{Dialect: Postgres}).AutoInc(); got != "BIGSERIAL PRIMARY KEY" {
|
||||
t.Errorf("postgres autoincrement = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user