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, ", ") }