package database import ( "fmt" "strings" "time" "github.com/google/uuid" ) // ── SQL Dialect Adapter ───────────────────── // // Q adapts Postgres-flavoured SQL to the current dialect. // On Postgres it is a no-op. On SQLite it rewrites: // - $1, $2 … $20 → ? // - ::jsonb, ::text → removed // - NOW() → datetime('now') // - true / false → 1 / 0 (bare keywords only, not inside strings) // - NULLS LAST → removed (SQLite default) // - COALESCE(x::text, …) → COALESCE(x, …) // // Call it at every database.DB.Query / Exec / QueryRow site in handlers. func Q(query string) string { if !IsSQLite() { return query } q := query // Replace $N placeholders high-to-low to avoid $1 matching inside $10. for i := 20; i >= 1; i-- { q = strings.ReplaceAll(q, fmt.Sprintf("$%d", i), "?") } // Postgres type casts q = strings.ReplaceAll(q, "::jsonb", "") q = strings.ReplaceAll(q, "::text", "") // Time functions q = strings.ReplaceAll(q, "NOW()", "datetime('now')") // Boolean literals (bare keywords, not inside quotes) q = strings.ReplaceAll(q, "= true", "= 1") q = strings.ReplaceAll(q, "= false", "= 0") q = strings.ReplaceAll(q, "COALESCE(is_private, false)", "COALESCE(is_private, 0)") // NULL sort order (SQLite puts NULLs last by default for DESC) q = strings.ReplaceAll(q, "NULLS LAST", "") // Case-insensitive LIKE (SQLite LIKE is already case-insensitive for ASCII) q = strings.ReplaceAll(q, "ILIKE", "LIKE") return q } // InsertReturningID executes an INSERT … RETURNING id query. // On Postgres it uses RETURNING directly. // On SQLite it strips RETURNING, generates a UUID, prepends it to the args, // adds an "id" column to the INSERT, and executes. func InsertReturningID(query string, args ...interface{}) (string, error) { if !IsSQLite() { var id string err := DB.QueryRow(query, args...).Scan(&id) return id, err } // Generate ID for SQLite id := uuid.New().String() // Adapt SQL: strip RETURNING, convert placeholders, add id column+value q := Q(query) // Remove RETURNING clause if idx := strings.Index(strings.ToUpper(q), "RETURNING"); idx >= 0 { q = strings.TrimSpace(q[:idx]) } // Inject id column and value into INSERT // Pattern: INSERT INTO table (col1, col2, ...) VALUES (?, ?, ...) q = injectIDColumn(q) // Prepend id to args newArgs := make([]interface{}, 0, len(args)+1) newArgs = append(newArgs, id) newArgs = append(newArgs, args...) _, err := DB.Exec(q, newArgs...) return id, err } // InjectIDForTest adds "id" as the first column and "?" as the first value // in an INSERT statement. Exported for use in test helpers. func InjectIDForTest(q string) string { return injectIDColumn(q) } // injectIDColumn adds "id" as the first column and "?" as the first value // in an INSERT statement. func injectIDColumn(q string) string { // Find the opening paren of the column list upper := strings.ToUpper(q) insertIdx := strings.Index(upper, "INSERT INTO") if insertIdx < 0 { return q } // Find first ( after INSERT INTO tablename firstParen := strings.Index(q[insertIdx:], "(") if firstParen < 0 { return q } firstParen += insertIdx // Find VALUES ( valuesIdx := strings.Index(upper, "VALUES") if valuesIdx < 0 { return q } valuesParen := strings.Index(q[valuesIdx:], "(") if valuesParen < 0 { return q } valuesParen += valuesIdx // Insert "id, " after first ( and "?, " after VALUES ( result := q[:firstParen+1] + "id, " + q[firstParen+1:valuesParen+1] + "?, " + q[valuesParen+1:] return result } // ── Time Scan Helpers ─────────────────────── // // SQLite returns timestamps as TEXT strings. These helpers wrap *time.Time // and sql.NullTime destinations so Scan() works on both dialects. // ScanTime wraps a *time.Time for use in handler-level Scan() calls. // On Postgres, time.Time scans natively. On SQLite, parses from string. type ScanTime struct{ T *time.Time } func (s *ScanTime) Scan(src interface{}) error { if src == nil { return nil } switch v := src.(type) { case time.Time: *s.T = v return nil case string: for _, f := range compatTimeFormats { if p, err := time.Parse(f, v); err == nil { *s.T = p return nil } } return fmt.Errorf("ScanTime: cannot parse %q", v) default: return fmt.Errorf("ScanTime: unsupported type %T", src) } } // ST creates a ScanTime wrapper for use in Scan() argument lists. func ST(t *time.Time) *ScanTime { return &ScanTime{T: t} } // ScanNullTime wraps a nullable time destination. type ScanNullTime struct{ T **time.Time } func (s *ScanNullTime) Scan(src interface{}) error { if src == nil { *s.T = nil return nil } var parsed time.Time scanner := ST(&parsed) if err := scanner.Scan(src); err != nil { return err } *s.T = &parsed return nil } // SNT creates a ScanNullTime wrapper for use in Scan() argument lists. func SNT(t **time.Time) *ScanNullTime { return &ScanNullTime{T: t} } // NullTimeValue extracts a time.Time from sql.NullTime or *time.Time, // returning the zero value if nil/invalid. Useful in JSON serialization. func NullTimeValue(t *time.Time) time.Time { if t == nil { return time.Time{} } return *t } var compatTimeFormats = []string{ "2006-01-02 15:04:05", "2006-01-02T15:04:05Z", time.RFC3339, "2006-01-02 15:04:05.000000000+00:00", } // ── Unique Constraint Detection ───────────── // IsUniqueViolation checks if an error is a unique constraint violation, // working on both Postgres and SQLite. func IsUniqueViolation(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "duplicate key") || // Postgres strings.Contains(msg, "UNIQUE constraint failed") // SQLite }