Files
llmgw/internal/store/dialect.go
T
Ilker ManapandClaude Opus 5.5 9b663eb23c Name people and projects in usage reports, and total a usage legend
The cluster page's usage table showed session:usr-... and proj-... ids;
it now shows the person's email, whether it came through a key or the
chat page, and the project's path or "personal". Times read "3 hours
ago" rather than "3h7m25s ago", everywhere.

The usage page's legend gave each line's latest slice, which was 0
beside a table of hundreds; it gives the window's total, and the table's
numbers are right-aligned like everywhere else.

The PostgreSQL placeholder rewriting skips line comments. An apostrophe
in one opened a string that swallowed every placeholder after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:37:21 +02:00

116 lines
3.8 KiB
Go

package store
import (
"context"
"database/sql"
"strconv"
"strings"
)
// Dialect is which SQL database is underneath.
//
// There is one set of queries, not one per database, because the SQL this
// store writes is almost entirely portable — the only construct that genuinely
// differs is the placeholder, and `ON CONFLICT` upserts work in both. Two
// copies of every query would drift, and the drift would be discovered one
// query at a time in production.
type Dialect string
// The supported databases.
const (
SQLiteDialect Dialect = "sqlite"
PostgresDialect Dialect = "postgres"
)
// rebind rewrites `?` placeholders for the dialect in use.
//
// SQLite takes `?`, PostgreSQL takes `$1`, `$2` and so on. Queries are written
// with `?` because that is what the majority of them were written with
// already, and rewriting is cheaper and safer than maintaining two spellings
// of each.
//
// A `?` inside a string literal would be rewritten too, which would be a bug —
// so this skips over quoted sections rather than scanning blindly. No query
// here contains one today; this exists so that adding one is not a silent
// corruption.
func (d Dialect) rebind(query string) string {
if d != PostgresDialect || !strings.Contains(query, "?") {
return query
}
var b strings.Builder
b.Grow(len(query) + 8)
n := 0
inSingle, inDouble := false, false
for i := 0; i < len(query); i++ {
c := query[i]
// A line comment is copied as it is. Read as SQL, the apostrophe in
// "-- the key's owner" opened a string that swallowed every
// placeholder after it, and PostgreSQL refused the query.
if c == '-' && !inSingle && !inDouble && i+1 < len(query) && query[i+1] == '-' {
end := strings.IndexByte(query[i:], '\n')
if end < 0 {
end = len(query) - i
}
b.WriteString(query[i : i+end])
i += end - 1
continue
}
switch {
case c == '\'' && !inDouble:
inSingle = !inSingle
case c == '"' && !inSingle:
inDouble = !inDouble
case c == '?' && !inSingle && !inDouble:
n++
b.WriteByte('$')
b.WriteString(strconv.Itoa(n))
continue
}
b.WriteByte(c)
}
return b.String()
}
// conn is the part of *sql.DB and *sql.Tx this store uses.
//
// Named so the same query helpers work inside and outside a transaction, and
// so every path goes through the rebinding wrapper rather than some of them
// reaching the driver directly with the wrong placeholders.
type conn interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
// PrepareContext is here because a prepared statement carries the query
// with it: rebinding at Exec time would be too late, the placeholders are
// fixed when it is prepared.
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}
// bound is a conn that rewrites placeholders on the way through.
//
// A wrapper rather than a change at each call site: there are sixty of those,
// and sixty chances to forget one. Here the only way to reach the driver is
// through the rewrite.
type bound struct {
c Dialect
// to is the underlying *sql.DB or *sql.Tx.
to conn
}
func (b bound) ExecContext(ctx context.Context, q string, args ...any) (sql.Result, error) {
return b.to.ExecContext(ctx, b.c.rebind(q), args...)
}
func (b bound) QueryContext(ctx context.Context, q string, args ...any) (*sql.Rows, error) {
return b.to.QueryContext(ctx, b.c.rebind(q), args...)
}
func (b bound) QueryRowContext(ctx context.Context, q string, args ...any) *sql.Row {
return b.to.QueryRowContext(ctx, b.c.rebind(q), args...)
}
func (b bound) PrepareContext(ctx context.Context, q string) (*sql.Stmt, error) {
return b.to.PrepareContext(ctx, b.c.rebind(q))
}