118 lines
3.9 KiB
Go
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()
|
|
}
|