Files
llmgw/internal/store/open.go
T
Ilker ManapandClaude Opus 5 ebce41e22d Stop calling the PostgreSQL store SQLite, and stop guessing where it is
Two things that were only ever confusing, and one of them stopped a command
working on the cluster.

The store type was named SQLite and kept the name when PostgreSQL was added, so
OpenPostgres returned a *SQLite and all seventy-odd methods on the production
store were spelled after the database it was not running on. Nothing broke;
people did. Earlier today I added three methods to *SQLite for a table that on
this cluster only ever lives in PostgreSQL, and the name did not once look
wrong. It is a field, which is all it ever was.

`llmgw users` and `llmgw keys` defaulted --store to a SQLite path. On a
deployment whose store is PostgreSQL every one of them failed with "unable to
open database file" while the right answer sat in auth.store in the config file
beside them; the conclusion an operator draws is that the tool is broken, and
they are not far off. They now ask the server's own configuration before
falling back, and the error names which of the three sources chose the address
— the failure was never the path, it was which one picked it.

The single-machine default still applies last, so a deployment with no config
file keeps working with no flags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 12:20:40 +02:00

62 lines
2.0 KiB
Go

package store
import (
"context"
"fmt"
"os"
"strings"
)
// Open picks a backend from what the address looks like.
//
// A path is SQLite and a postgres:// URL is PostgreSQL, so an existing
// configuration keeps working untouched and moving to PostgreSQL is one line
// changed rather than a new setting to discover. A gateway on one machine has
// no reason to run a database server, and a gateway that has outgrown that
// should not have to be reconfigured twice.
//
// An address of the form @/path/to/file reads the real address out of that
// file, which is how a DSN carrying a password stays out of a configuration
// file that gets copied, diffed and pasted into issues.
func Open(ctx context.Context, address string) (*DB, error) {
address, err := resolveAddress(address)
if err != nil {
return nil, err
}
if isPostgres(address) {
return OpenPostgres(ctx, address)
}
return OpenSQLite(ctx, address)
}
// isPostgres reports whether an address names a PostgreSQL server.
//
// By scheme, because that is the only part of a DSN that is reliably there.
// A bare host:port would be ambiguous with a relative file path, so it is not
// accepted: an address this cannot classify should fail loudly rather than
// quietly create a SQLite file named after someone's database server.
func isPostgres(address string) bool {
return strings.HasPrefix(address, "postgres://") ||
strings.HasPrefix(address, "postgresql://")
}
// resolveAddress expands an @file indirection.
//
// The file is read whole and trimmed, so a trailing newline — which every
// editor and every `echo` adds — does not become part of a password.
func resolveAddress(address string) (string, error) {
if !strings.HasPrefix(address, "@") {
return address, nil
}
path := strings.TrimPrefix(address, "@")
b, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("reading the store address from %s: %w", path, err)
}
out := strings.TrimSpace(string(b))
if out == "" {
return "", fmt.Errorf("%s is empty, so there is no store address in it", path)
}
return out, nil
}