package database import ( "context" "database/sql" "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 } // QArgs adapts a Postgres query AND its args for the current dialect. // // Unlike Q() which only rewrites the query string, QArgs also expands the // args slice to match positional ? placeholders. In Postgres, $1 can appear // multiple times and references the same arg. In SQLite, each ? is // positional — if $1 appears twice, the arg must appear twice. // // Use QArgs for any handler query that references $N more than once // (typically subqueries like "WHERE user_id = $1 OR ... participant_id = $1"). // // On Postgres, returns query and args unchanged. func QArgs(query string, args ...interface{}) (string, []interface{}) { if !IsSQLite() { return query, args } // First, apply the non-placeholder rewrites q := query q = strings.ReplaceAll(q, "::jsonb", "") q = strings.ReplaceAll(q, "::text", "") q = strings.ReplaceAll(q, "NOW()", "datetime('now')") q = strings.ReplaceAll(q, "= true", "= 1") q = strings.ReplaceAll(q, "= false", "= 0") q = strings.ReplaceAll(q, "COALESCE(is_private, false)", "COALESCE(is_private, 0)") q = strings.ReplaceAll(q, "NULLS LAST", "") q = strings.ReplaceAll(q, "ILIKE", "LIKE") // Walk the query left-to-right, find each $N, record which arg index // it maps to, replace with ?, and build a new args list in order. var newArgs []interface{} var result strings.Builder i := 0 for i < len(q) { if q[i] == '$' && i+1 < len(q) && q[i+1] >= '1' && q[i+1] <= '9' { // Parse the number after $ j := i + 1 for j < len(q) && q[j] >= '0' && q[j] <= '9' { j++ } numStr := q[i+1 : j] n := 0 for _, c := range numStr { n = n*10 + int(c-'0') } // $N is 1-based, args is 0-based if n >= 1 && n <= len(args) { newArgs = append(newArgs, args[n-1]) } result.WriteByte('?') i = j } else { result.WriteByte(q[i]) i++ } } return result.String(), newArgs } // ── Safe Query Wrappers ───────────────────── // // These wrappers replace the pattern: // database.DB.QueryRow(database.Q(sql), args...) // with: // database.QueryRow(sql, args...) // // They call QArgs internally, handling $N→? conversion AND arg expansion // for repeated placeholder references. Use these for ALL handler-level // SQL that uses $N placeholders. // QueryRow executes a query with dialect adaptation and arg expansion. func QueryRow(query string, args ...interface{}) *sql.Row { q, expanded := QArgs(query, args...) return DB.QueryRow(q, expanded...) } // QueryRowContext is the context-aware variant of QueryRow. func QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { q, expanded := QArgs(query, args...) return DB.QueryRowContext(ctx, q, expanded...) } // Query executes a query returning rows, with dialect adaptation. func Query(query string, args ...interface{}) (*sql.Rows, error) { q, expanded := QArgs(query, args...) return DB.Query(q, expanded...) } // QueryContext is the context-aware variant of Query. func QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { q, expanded := QArgs(query, args...) return DB.QueryContext(ctx, q, expanded...) } // Exec executes a statement with dialect adaptation. func Exec(query string, args ...interface{}) (sql.Result, error) { q, expanded := QArgs(query, args...) return DB.Exec(q, expanded...) } // ExecContext is the context-aware variant of Exec. func ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { q, expanded := QArgs(query, args...) return DB.ExecContext(ctx, q, expanded...) } // 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 }