munin/internal/store/dialect.go
2026-10-19 10:13:56 +02:00

118 lines
3.9 KiB
Go

package store
import (
"context"
"strconv"
"strings"
)
// dialect beschreibt, worin sich SQLite und PostgreSQL unterscheiden.
//
// # Warum so klein?
//
// Alle Abfragen im Store sind für beide Datenbanken dieselben (siehe
// AUSBAUSTUFEN.md 4.10, „ein Store mit Dialekt, keine zwei Stores“). Hier steht
// nur, was sich wirklich unterscheidet. Jede Methode, die hier dazukommt, muss
// für beide Datenbanken umgesetzt werden — fehlt eine, baut das Programm nicht.
// Der Compiler passt also auf, dass keine Seite vergessen wird.
type dialect interface {
// rebind wandelt die Platzhalter einer Abfrage in die Schreibweise der
// Datenbank um. Geschrieben werden alle Abfragen mit "?" (SQLite);
// PostgreSQL erwartet "$1, $2, …".
rebind(query string) string
// migrationsSubdir nennt den Ordner innerhalb von migrations/ bzw.
// migrations_control/, aus dem die Migrationen dieser Datenbank kommen.
migrationsSubdir() string
// diskUsage misst, wie viel Platz ein Workspace belegt: bei SQLite die
// Datei samt WAL, bei PostgreSQL die Tabellen des Schemas. Die
// Umsetzungen stehen in sysinfo.go.
diskUsage(ctx context.Context, p pool) (DiskUsage, error)
}
// ── SQLite ───────────────────────────────────────────────────────────────────
type sqliteDialect struct{}
// rebind ist bei SQLite nichts zu tun: Die Abfragen sind bereits in der
// SQLite-Schreibweise geschrieben.
func (sqliteDialect) rebind(query string) string { return query }
func (sqliteDialect) migrationsSubdir() string { return "sqlite" }
// ── PostgreSQL ───────────────────────────────────────────────────────────────
type postgresDialect struct{}
func (postgresDialect) rebind(query string) string { return numberPlaceholders(query) }
func (postgresDialect) migrationsSubdir() string { return "postgres" }
// numberPlaceholders ersetzt jedes "?" durch "$1", "$2", … in der
// Reihenfolge des Auftretens.
//
// Ein "?" innerhalb eines Textes in Hochkommas ('Wie geht das?'), eines
// Bezeichners in Anführungszeichen oder eines Kommentars ist kein Platzhalter
// und bleibt stehen. Deshalb geht die Funktion Zeichen für Zeichen durch und
// merkt sich, wo sie gerade steht, statt blind zu ersetzen.
//
// Ein verdoppeltes Hochkomma (so schreibt SQL ein Hochkomma innerhalb eines
// Textes) braucht keine Sonderbehandlung: Das erste beendet den Text, das
// zweite beginnt sofort einen neuen — das Ergebnis ist dasselbe.
func numberPlaceholders(query string) string {
// Schneller Weg: Die meisten Abfragen sind kurz, aber ohne "?" gibt es
// gar nichts zu tun.
if !strings.Contains(query, "?") {
return query
}
var b strings.Builder
b.Grow(len(query) + 16)
number := 0
for i := 0; i < len(query); i++ {
c := query[i]
switch {
case c == '\'' || c == '"':
// Text bzw. Bezeichner: bis zum passenden Gegenstück unverändert
// übernehmen.
end := strings.IndexByte(query[i+1:], c)
if end < 0 {
b.WriteString(query[i:])
return b.String()
}
b.WriteString(query[i : i+1+end+1])
i += end + 1
case c == '-' && i+1 < len(query) && query[i+1] == '-':
// Kommentar bis zum Zeilenende.
end := strings.IndexByte(query[i:], '\n')
if end < 0 {
b.WriteString(query[i:])
return b.String()
}
b.WriteString(query[i : i+end])
i += end - 1
case c == '/' && i+1 < len(query) && query[i+1] == '*':
// Blockkommentar bis "*/".
end := strings.Index(query[i+2:], "*/")
if end < 0 {
b.WriteString(query[i:])
return b.String()
}
b.WriteString(query[i : i+2+end+2])
i += 2 + end + 1
case c == '?':
number++
b.WriteByte('$')
b.WriteString(strconv.Itoa(number))
default:
b.WriteByte(c)
}
}
return b.String()
}