munin/internal/store/dialekt.go

118 lines
4 KiB
Go

package store
import (
"context"
"strconv"
"strings"
)
// dialekt 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 dialekt interface {
// rebind wandelt die Platzhalter einer Abfrage in die Schreibweise der
// Datenbank um. Geschrieben werden alle Abfragen mit "?" (SQLite);
// PostgreSQL erwartet "$1, $2, …".
rebind(abfrage string) string
// migrationsUnterordner nennt den Ordner innerhalb von migrations/ bzw.
// migrations_control/, aus dem die Migrationen dieser Datenbank kommen.
migrationsUnterordner() string
// belegung misst, wie viel Platz ein Workspace belegt: bei SQLite die
// Datei samt WAL, bei PostgreSQL die Tabellen des Schemas. Die
// Umsetzungen stehen in systemstand.go.
belegung(ctx context.Context, p pool) (Belegung, error)
}
// ── SQLite ───────────────────────────────────────────────────────────────────
type sqliteDialekt struct{}
// rebind ist bei SQLite nichts zu tun: Die Abfragen sind bereits in der
// SQLite-Schreibweise geschrieben.
func (sqliteDialekt) rebind(abfrage string) string { return abfrage }
func (sqliteDialekt) migrationsUnterordner() string { return "sqlite" }
// ── PostgreSQL ───────────────────────────────────────────────────────────────
type postgresDialekt struct{}
func (postgresDialekt) rebind(abfrage string) string { return platzhalterNummerieren(abfrage) }
func (postgresDialekt) migrationsUnterordner() string { return "postgres" }
// platzhalterNummerieren 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 platzhalterNummerieren(abfrage string) string {
// Schneller Weg: Die meisten Abfragen sind kurz, aber ohne "?" gibt es
// gar nichts zu tun.
if !strings.Contains(abfrage, "?") {
return abfrage
}
var b strings.Builder
b.Grow(len(abfrage) + 16)
nummer := 0
for i := 0; i < len(abfrage); i++ {
c := abfrage[i]
switch {
case c == '\'' || c == '"':
// Text bzw. Bezeichner: bis zum passenden Gegenstück unverändert
// übernehmen.
ende := strings.IndexByte(abfrage[i+1:], c)
if ende < 0 {
b.WriteString(abfrage[i:])
return b.String()
}
b.WriteString(abfrage[i : i+1+ende+1])
i += ende + 1
case c == '-' && i+1 < len(abfrage) && abfrage[i+1] == '-':
// Kommentar bis zum Zeilenende.
ende := strings.IndexByte(abfrage[i:], '\n')
if ende < 0 {
b.WriteString(abfrage[i:])
return b.String()
}
b.WriteString(abfrage[i : i+ende])
i += ende - 1
case c == '/' && i+1 < len(abfrage) && abfrage[i+1] == '*':
// Blockkommentar bis "*/".
ende := strings.Index(abfrage[i+2:], "*/")
if ende < 0 {
b.WriteString(abfrage[i:])
return b.String()
}
b.WriteString(abfrage[i : i+2+ende+2])
i += 2 + ende + 1
case c == '?':
nummer++
b.WriteByte('$')
b.WriteString(strconv.Itoa(nummer))
default:
b.WriteByte(c)
}
}
return b.String()
}