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