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() }