Changeset 0.24.1 (#157)

This commit is contained in:
2026-03-07 17:11:32 +00:00
parent a63728a481
commit b6cc4df6e7
27 changed files with 2094 additions and 453 deletions

View File

@@ -1,6 +1,8 @@
package database
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
@@ -52,6 +54,112 @@ func Q(query string) string {
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,