package db
import (
"database/sql"
"fmt"
"regexp"
"strings"
)
// Dialect captures the small set of SQL constructs that differ between MySQL
// and SQLite. The ~95% of the server's queries are plain CRUD with "?"
// placeholders and need no dialect handling; only upserts and row locks do.
type Dialect interface {
// Name reports "mysql" or "sqlite".
Name() string
// LockForUpdate returns the pessimistic row-lock clause to append to a
// SELECT inside a transaction:
// mysql → "FOR UPDATE"
// sqlite → "" (single-writer engine; the BEGIN IMMEDIATE transaction —
// enabled via _txlock=immediate in the DSN — already serializes
// writers, giving equivalent exclusivity).
LockForUpdate() string
// Upsert builds the conflict-resolution tail for an INSERT. conflictCols is
// the unique/primary key that triggers the conflict. setExprs are assignment
// expressions; refer to the would-be-inserted value with the sentinel
// "EXCLUDED.
" (rewritten to VALUES(col) on MySQL, kept as excluded.col
// on SQLite). With no setExprs the conflict is a no-op (insert-or-ignore).
//
// Example (atomic accumulate):
// d.Upsert([]string{"user_id","date"},
// "bytes_up = bytes_up + EXCLUDED.bytes_up")
Upsert(conflictCols []string, setExprs ...string) string
}
// DialectForDB derives the Dialect from the driver backing an open *sql.DB, so
// the dialect always matches the actual connection (no global state, and store
// constructors keep their (db *sql.DB) signatures).
func DialectForDB(database *sql.DB) Dialect {
if database == nil {
return MySQLDialect{}
}
name := strings.ToLower(fmt.Sprintf("%T", database.Driver()))
if strings.Contains(name, "sqlite") {
return SQLiteDialect{}
}
return MySQLDialect{}
}
// DialectFor returns the Dialect for a driver name ("mysql"|"sqlite").
func DialectFor(driver string) Dialect {
if Normalize(driver) == "sqlite" {
return SQLiteDialect{}
}
return MySQLDialect{}
}
var excludedRef = regexp.MustCompile(`EXCLUDED\.(\w+)`)
// MySQLDialect implements Dialect for MySQL.
type MySQLDialect struct{}
func (MySQLDialect) Name() string { return "mysql" }
func (MySQLDialect) LockForUpdate() string { return "FOR UPDATE" }
func (MySQLDialect) Upsert(conflictCols []string, setExprs ...string) string {
if len(setExprs) == 0 {
// No-op upsert (insert-or-ignore): assign a key column to itself.
col := "id"
if len(conflictCols) > 0 {
col = conflictCols[0]
}
return fmt.Sprintf("ON DUPLICATE KEY UPDATE %s = %s", col, col)
}
rewritten := make([]string, len(setExprs))
for i, e := range setExprs {
rewritten[i] = excludedRef.ReplaceAllString(e, "VALUES($1)")
}
return "ON DUPLICATE KEY UPDATE " + strings.Join(rewritten, ", ")
}
// SQLiteDialect implements Dialect for SQLite.
type SQLiteDialect struct{}
func (SQLiteDialect) Name() string { return "sqlite" }
func (SQLiteDialect) LockForUpdate() string { return "" }
func (SQLiteDialect) Upsert(conflictCols []string, setExprs ...string) string {
target := "ON CONFLICT(" + strings.Join(conflictCols, ", ") + ") "
if len(setExprs) == 0 {
return target + "DO NOTHING"
}
// SQLite accepts the EXCLUDED. reference verbatim.
return target + "DO UPDATE SET " + strings.Join(setExprs, ", ")
}