Documentation
¶
Index ¶
- Constants
- Variables
- func DemoBytdbPath() (string, error)
- func DerivedName(base, database string) string
- func ExpandDSN(name, driver, dsn string) (string, []string)
- func SavedDir() string
- func SavedFile() string
- func SupportsDatabases(driver string) bool
- type Config
- func (c *Config) AddConn(cn Connection) error
- func (c *Config) ConnByName(name string) (Connection, bool)
- func (c *Config) Conns() []Connection
- func (c *Config) LoadSaved(path string, knownDriver func(string) bool)
- func (c *Config) MergeSaved(saved []SavedConn, knownDriver func(string) bool) []string
- func (c *Config) RemoveConn(name string) bool
- func (c *Config) ReplaceConn(old string, cn Connection) error
- type Connection
- type DemoEngine
- type SavedConn
- type SavedStore
- func (s *SavedStore) Add(c SavedConn) error
- func (s *SavedStore) Delete(name string) error
- func (s *SavedStore) Get(name string) (SavedConn, bool, error)
- func (s *SavedStore) List() ([]SavedConn, error)
- func (s *SavedStore) Path() string
- func (s *SavedStore) Persistent() bool
- func (s *SavedStore) Update(old string, c SavedConn) error
- type TLSOpts
Constants ¶
const ( // DefaultAIContextRows mirrors ai.DefaultContextRows. It is repeated // rather than imported so config stays a leaf package that every other // one can depend on. DefaultAIContextRows = 10 // DefaultConnIdleTimeout is how long a pooled connection may sit unused // before the pool closes it (conn_idle_timeout). database/sql's own // default is forever, which keeps server-side backends — their memory and // a slot in the server's max_connections — alive for hours after the // last query. An hour is long enough that interactive use (run, read, // think, run again) never pays a reconnect, and short enough that a dbc // left open overnight gives its connections back. It is a ceiling, not a // keepalive: a server or proxy that cuts idle connections sooner still // does, and the drivers' liveness checks on checkout replace those // transparently (for Postgres, with db.pgShouldPing's help: pgx alone // skips the check within a second of the last checkout). The editor's // pinned session is checked out, never idle, // so this never expires it. DefaultConnIdleTimeout = time.Hour // DefaultConnectTimeout bounds opening a connection (connect_timeout): // the dial, the handshake and the first ping together. Without it an // unreachable host fails only at the OS's TCP connect timeout — over a // minute — while the UI shows "connecting…". Five seconds is ample for // any reachable server, including one across a VPN, and short enough that // a wrong host or a down VPN is reported while the user is still looking. DefaultConnectTimeout = 5 * time.Second )
const ( // DemoBytdb is the bytdb-backed demo, and the one active by default. DemoBytdb = "demo-bytdb" // DemoSQLite is the in-memory SQLite demo. It was plain "demo" before the // bytdb demo arrived; the engine suffix now names both the same way. The // shipped scripts name it directly — their ? placeholders are SQLite's, so // they could not run on the bytdb demo ($1) under any name. DemoSQLite = "demo-sqlite" )
Names of the built-in demo connections. Both are registered when no config file is found, so the two embedded engines can be queried side by side without writing a config first.
const ( TLSDisable = "disable" TLSPrefer = "prefer" TLSRequire = "require" TLSVerifyCA = "verify-ca" TLSVerifyFull = "verify-full" )
TLS modes, as written in the tls key.
const DatabaseSep = "/"
DatabaseSep joins a configured connection's name to one of its server's databases, in a derived connection's name: "ProdDr/analytics".
A Postgres connection is bound to one database — every catalog the server has is per database, and there is no USE to move a session to another — so reaching a second database on the same host means opening a second pool. A MySQL pool is opened the same way, on the database its sidebar lists (see SupportsDatabases). Naming that pool "<base>/<database>" lets everything keyed by connection name (the Manager's pools and row counts, the pinned session, history, saved tabs, the web layout's per-connection keys) keep working with no second key, and a tab saved on a derived connection reopens on it after a restart, because the name alone says how to rebuild it.
Variables ¶
var ErrConnExists = errors.New("a connection by that name already exists")
ErrConnExists is AddConn refusing a name already in use: ConnByName would always find the first of two, so the second could never be reached.
var ErrConnNotFound = errors.New("no connection by that name")
ErrConnNotFound is ReplaceConn finding no connection by the old name — removed (in another window, say) since the caller looked it up.
var TLSModes = []string{TLSDisable, TLSPrefer, TLSRequire, TLSVerifyCA, TLSVerifyFull}
TLSModes lists the modes in order of strictness, for messages and the web form's select.
Functions ¶
func DemoBytdbPath ¶
DemoBytdbPath returns the file backing the built-in bytdb demo, creating its parent directory. It prefers the OS cache directory and falls back to the temp directory on systems where the cache directory is undefined (a bare container with no $HOME, say).
func DerivedName ¶ added in v0.3.0
DerivedName is the name of base's connection to database.
func ExpandDSN ¶
ExpandDSN expands ${VAR} and $VAR in a DSN from the environment, as Load does for the file's connections. It does this itself rather than via os.ExpandEnv so that a typo'd ${PGPASS} comes back as a warning naming it instead of silently becoming "" and surfacing later as a baffling auth failure. name is the connection's, for the warning.
Each value is escaped for where it lands in the DSN of driver (any alias db.Driver accepts): inside a quoted Postgres value, in a URL's password, in a MySQL param — see dsnexpand.go. So a password kept in the environment may hold any character, as one typed into the DSN form's field may.
func SavedDir ¶ added in v0.3.0
func SavedDir() string
SavedDir is the directory relative tls_* paths of a saved connection (one added in dbc web) resolve against: the saved-connections file's own, as a config file's resolve against its directory. "" when there is no home directory, which leaves them relative to the working directory.
func SavedFile ¶
func SavedFile() string
SavedFile is where saved connections live, beside config.toml; "" when no home directory is known, and they then last only as long as a dbc web.
func SupportsDatabases ¶ added in v0.3.0
SupportsDatabases reports whether a driver's connections can be derived onto another database of their server: Postgres, and MySQL (MariaDB too). Both tie what a connection's sidebar lists to the database its DSN names — Postgres because every catalog is per database, MySQL because dbc scopes its catalog queries to DATABASE() — so on both another database is another connection. MySQL could USE its way across instead, but a pool's connections are interchangeable and a USE would stick to only one of them; setting the database the pool dials with keeps every connection of it on the same one. SQLite and bytdb open one file, with no server to hold a second database.
db.HasDatabases (the database pickers) is this same set, so a picker never offers a database ConnByName could not derive.
Types ¶
type Config ¶
type Config struct {
ScriptsDir string `toml:"scripts_dir"`
MaxRows int `toml:"max_rows"` // rows fetched from the server
// MaxDisplayRows caps how many of those rows the TUI table renders. It is
// separate from MaxRows because the two are limited by different things:
// fetching more is a question of memory and patience, rendering more costs
// a widget per value and shows up as a sluggish scroll. Raising max_rows
// for the sake of an export should not make the table crawl. Zero means no
// display cap.
MaxDisplayRows int `toml:"max_display_rows"`
// The AI assistant (package ai). AIAgent picks the ACP backend —
// "copilot" (the default), "claude" or "gemini"; AIModel is a preferred
// model id, applied when the agent offers it. AIContextRows caps how many
// result rows go with a question on connections that allow rows at all
// (ai_rows); 0 sends none, and unset means ai.DefaultContextRows.
AIAgent string `toml:"ai_agent"`
AIModel string `toml:"ai_model"`
AIContextRows int `toml:"ai_context_rows"`
// ConnIdleTimeout is how long a pooled connection may sit idle before it
// is closed; see DefaultConnIdleTimeout. Written as a duration string in
// the file ("30m", "2h"); 0 keeps idle connections open indefinitely, as
// database/sql does by default. It applies to every connection — it is a
// question of how long dbc sits idle, not of which database it talks to.
ConnIdleTimeout time.Duration `toml:"conn_idle_timeout"`
// ConnectTimeout caps opening a connection; see DefaultConnectTimeout.
// A duration string in the file ("10s"); 0 sets no limit of dbc's own,
// leaving it to the driver (a DSN's own connect_timeout, say) and the OS.
// A DSN-level timeout still applies either way — whichever is shorter
// wins. Global for the same reason as ConnIdleTimeout.
ConnectTimeout time.Duration `toml:"connect_timeout"`
// PlanTheme is the palette of the plan pictures the shell and the TUI
// draw — `dbc explain -t pdf|jpeg|png` and the TUI's Save as PDF /
// JPEG, and the schema diagrams of `dbc erd -t png|jpeg` and the TUI's
// Diagram items: "dark" (the default, dbc's own slate) or "light" (paper, kinder
// to a printer). Load normalizes it to one of the two, so readers can
// hand it to theme.ByName without a second error to handle. dbc web
// ignores it: its files follow the light or dark the view is wearing.
// A string rather than a theme.Palette so config stays a leaf package.
PlanTheme string `toml:"plan_theme"`
DefaultConnection string `toml:"default_connection"`
// Connections is written freely while the config is being built (Load,
// the demo pruning, an ad-hoc --dsn), when nothing else can see it. Once
// something may run beside a change — dbc web adding a connection while
// its tabs connect and run — the change goes through AddConn/RemoveConn/
// ReplaceConn and every reader through Conns/ConnByName, which take connMu.
//
// The writers are copy-on-write: AddConn, RemoveConn and ReplaceConn
// build a new slice rather than edit the old one in place, so a slice a
// reader got from Conns (or a range over the field that began before the
// change) never sees an element change under it.
Connections []Connection `toml:"connection"`
Path string `toml:"-"` // file the config was loaded from ("" if none)
Demo bool `toml:"-"` // true when running with the built-in demo connection
// Warnings are load-time findings worth telling the user about but not
// worth refusing to start over, e.g. a DSN referencing an unset env var.
Warnings []string `toml:"-"`
// contains filtered or unexported fields
}
Config is the application configuration.
func Load ¶
Load reads the config from an explicit path, or searches ./dbc.toml then ~/.config/dbc/config.toml. With no config anywhere, it falls back to the built-in demo connections so the app is usable immediately.
func LoadDemo ¶
func LoadDemo(explicit string, demo DemoEngine) (*Config, error)
LoadDemo is Load with a say in which built-in demo connection starts out active. The choice only matters when there is no config file: a real config names its own default_connection.
func (*Config) AddConn ¶
func (c *Config) AddConn(cn Connection) error
AddConn appends a connection at runtime. The name must be new.
func (*Config) ConnByName ¶
func (c *Config) ConnByName(name string) (Connection, bool)
ConnByName finds a connection config by name. A name no connection has exactly, of the form "<base>/<database>" where base is a Postgres or MySQL connection (SupportsDatabases), is that connection derived onto database (see DatabaseSep): a copy with Name set to name and Base and Database filled in.
The exact match is tried first, so a configured connection whose own name contains a "/" is still found as itself. Then the name is matched against each such connection's name plus the separator, rather than split at a "/": a quoted Postgres database name may contain one, and so may a connection's name. Of two bases that both match ("a" and "a/b" for "a/b/c"), the longer wins, as the more specific.
func (*Config) Conns ¶
func (c *Config) Conns() []Connection
Conns is the connection list as it stands, for a reader that may run beside AddConn/RemoveConn. The slice is the one the writers replace wholesale, never edit, so it is safe to keep and range without the lock — callers must not modify it.
func (*Config) LoadSaved ¶
LoadSaved merges the saved connections at path into c, after what c already holds, recording anything skipped in c.Warnings. A file that cannot be read or parsed is a warning too, not a failure: the config file's connections still work, and refusing to start over a stray character in a file dbc wrote would be out of proportion. See MergeSaved for knownDriver.
Call it once c is complete (after Load, the demos and any ad-hoc connection), before anything else can see c.
func (*Config) MergeSaved ¶
MergeSaved adds saved connections to c, marked Web, and returns warnings for the ones it skips; it does not add them to c.Warnings (dbc web also merges the entries it moves from its old store, and logs them itself).
Skipped, and left in the file for the user to sort out:
- an entry with no name or no driver (a hand edit gone wrong);
- a name c already has — the config file's word wins, it being the user's deliberate one where this is only what was once typed in a form; so does the first of two saved entries by one name;
- a driver knownDriver rejects. config cannot know the drivers itself (it is a leaf; db registers them), so the caller passes db's check. nil accepts every driver, and an unknown one fails at connect;
- TLS settings TLSOpts.Check refuses. Skipped rather than merged without them: a connection that was meant to verify its server must not quietly connect in plaintext instead.
func (*Config) RemoveConn ¶
RemoveConn drops a connection at runtime, reporting whether there was one by that name. A default_connection naming it is left alone: the lookups that honor it (workspace.New, dbc web's defaultConn) already fall back to the first connection when it names nothing.
func (*Config) ReplaceConn ¶
func (c *Config) ReplaceConn(old string, cn Connection) error
ReplaceConn swaps the connection named old for cn, in place: the entry keeps its position, so an edit does not move it down the sidebar. cn may carry a new name; ErrConnExists refuses one another connection already has. Doing the lookup, the clash check and the swap under one lock is what makes two windows editing (or one editing while another removes) safe: the second of them fails here instead of leaving two entries by one name or resurrecting a removed one.
type Connection ¶
type Connection struct {
Name string `toml:"name"`
Driver string `toml:"driver"` // postgres | mysql | sqlite | bytdb
DSN string `toml:"dsn"` // env vars are expanded, e.g. ${PGPASS}
// TLSOpts holds the tls, tls_ca, tls_cert and tls_key keys: TLS for
// postgres and mysql, one spelling for both — see tls.go. Embedded
// untagged so the keys sit flat beside dsn. Load leaves the paths
// absolute (ExpandTLS), so the db package never has to know which file
// they were written in.
TLSOpts
// Migrations is the directory `dbc migrate` reads for this connection,
// relative to the working directory. Per connection rather than global
// because a config typically lists several databases and each has its
// own schema. Empty means the -dir flag (or ".") decides.
Migrations string `toml:"migrations"`
// AIRows lets the AI assistant see this connection's result ROWS (up to
// ai_context_rows of them). Off by default and per connection, because
// rows are the database's contents and sending them to a hosted model is
// a decision about that data: fine for a scratch database, not
// something a production connection should do because a global switch
// was flipped. The query text and errors go regardless — see package ai.
AIRows bool `toml:"ai_rows"`
// Demo marks a built-in demo connection, the only kind db.Manager seeds
// with the demo cats table when it opens it. Set by demoFallback alone,
// never read from a file: seeding runs CREATE TABLE and DELETE FROM cats,
// so it must not be possible to switch on for a real database. It is a
// per-connection flag rather than Config.Demo because a demo config can
// also carry the ad-hoc --dsn connection, which is the user's database.
Demo bool `toml:"-"`
// Web marks a connection added from inside dbc — dbc web's browser UI
// or the TUI's connection form (package connedit) — and kept in the
// saved-connections file (connections.toml, see SavedStore), not in the
// config file. Never read from a file's key — MergeSaved sets it — so
// both UIs can tell which ones they may edit or remove: the config file's
// connections are the file's to change. Every dbc merges the saved ones,
// so the TUI and headless runs list them too.
Web bool `toml:"-"`
// Base and Database describe a connection derived from a configured
// one: the same server, credentials and TLS, opened on another database
// of that server. ConnByName makes one up for a name of the form
// "<base>/<database>" (see DatabaseSep); none is ever stored, in the
// config or the saved-connections file. Database overrides whatever
// database the DSN names when the pool is opened (db.openPool); "" keeps
// the DSN's. Both are "" on a configured connection.
Base string `toml:"-"`
Database string `toml:"-"`
}
Connection describes one database host connection.
type DemoEngine ¶
type DemoEngine string
DemoEngine names which built-in demo connection starts out active. Both connections exist either way — this only picks the default.
const ( DemoDefault DemoEngine = "" // same as DemoEngineBytdb DemoEngineBytdb DemoEngine = "bytdb" DemoEngineSQLite DemoEngine = "sqlite" )
func ParseDemoEngine ¶
func ParseDemoEngine(s string) (DemoEngine, error)
ParseDemoEngine resolves a -demo flag or $DBC_DEMO value. An empty string is the default (bytdb); anything unrecognized is an error rather than a silent fallback, because silently ignoring it would leave the user staring at the engine they just asked not to use.
type SavedConn ¶
type SavedConn struct {
Name string `toml:"name"`
Driver string `toml:"driver"`
DSN string `toml:"dsn"` // as typed: ${VAR}s unexpanded
// TLSOpts is Connection.TLSOpts, with its paths as typed too: ${VAR}s
// unexpanded and a relative path left relative (to SavedDir). Each
// process resolves them as it merges the entry, as it does the DSN.
TLSOpts
// AIRows is Connection.AIRows: the assistant may see result rows.
AIRows bool `toml:"ai_rows"`
// Added orders the sidebar in dbc web (after the config file's). It is
// kept across an edit so an edited entry does not move.
Added time.Time `toml:"added"`
}
SavedConn is one connection in the saved-connections file.
type SavedStore ¶
type SavedStore struct {
// contains filtered or unexported fields
}
SavedStore is the writer of the saved-connections file, for dbc web: each change is a locked read-modify-write of the whole file, so it always starts from what is on disk — another dbc web's change, or a hand edit, included — rather than from what this process read at startup.
With path == "" (no home directory) it keeps the list in memory, lasting as long as the process, and callers do not branch on which kind they have.
func OpenSaved ¶
func OpenSaved(path string) *SavedStore
OpenSaved returns the store for path. Nothing is read or created until the first call.
func (*SavedStore) Add ¶
func (s *SavedStore) Add(c SavedConn) error
Add appends a connection. A name the file already has is ErrConnExists — another dbc web may have added it since this one started — rather than a replacement. A zero Added is now.
func (*SavedStore) Delete ¶
func (s *SavedStore) Delete(name string) error
Delete removes a connection. A missing one is success: the caller wanted it gone.
func (*SavedStore) Get ¶
func (s *SavedStore) Get(name string) (SavedConn, bool, error)
Get finds one saved connection by name.
func (*SavedStore) List ¶
func (s *SavedStore) List() ([]SavedConn, error)
List returns the saved connections, in file order.
func (*SavedStore) Path ¶
func (s *SavedStore) Path() string
Path is the file; "" for a memory-only store.
func (*SavedStore) Persistent ¶
func (s *SavedStore) Persistent() bool
Persistent reports whether changes reach a file.
func (*SavedStore) Update ¶
func (s *SavedStore) Update(old string, c SavedConn) error
Update replaces the connection named old with c, in place (the file's order is the sidebar's), keeping old's Added. c may carry a new name; one another entry has is ErrConnExists. A missing old is ErrConnNotFound: it was removed meanwhile, and writing c would bring it back.
type TLSOpts ¶ added in v0.3.0
type TLSOpts struct {
TLS string `toml:"tls,omitempty" json:"tls,omitempty"` // a mode above; "" leaves TLS to the DSN
TLSCA string `toml:"tls_ca,omitempty" json:"tls_ca,omitempty"` // PEM file of CA certificates to trust
TLSCert string `toml:"tls_cert,omitempty" json:"tls_cert,omitempty"` // PEM client certificate, for servers that ask for one
TLSKey string `toml:"tls_key,omitempty" json:"tls_key,omitempty"` // its PEM private key
// TLSKeyPassword is "${VAR}" (or "$VAR"): the environment variable
// holding tls_key's passphrase, when the key is encrypted. The
// reference, never the passphrase — see "AN ENCRYPTED tls_key" above.
TLSKeyPassword string `toml:"tls_key_password,omitempty" json:"tls_key_password,omitempty"`
}
TLSOpts is a connection's TLS settings. It is embedded (untagged) in Connection and SavedConn, so the TOML keys sit flat beside dsn in both files — a saved entry can still be copied into config.toml as it is. The JSON tags are dbc web's: its connection form and list embed it the same way. None of it is secret (paths, not the keys themselves), so unlike the DSN it does go back to the browser.
func ExpandTLS ¶ added in v0.3.0
ExpandTLS returns t with its file paths made absolute: ${VAR} and $VAR expanded from the environment (as ExpandDSN does, with a warning for an unset one), a leading ~/ taken as the home directory, and a relative path joined to base — the directory of the file that defines the connection.
Relative to that file, not to the working directory: dbc is started from wherever the user happens to be (a project for its scripts, a migrations folder), and "certs/ca.pem" written next to config.toml should mean the same file every time. base "" leaves a relative path relative to the working directory — the ad-hoc --tls-* flags, typed in the shell, mean that. name is the connection's, for the warnings.
func (*TLSOpts) Check ¶ added in v0.3.0
Check normalizes the mode's case and turns away settings that cannot be what was meant. Every rule here fails closed: a typo in a security setting must not become "no TLS".
- an unknown mode ("verify_full", "true") is refused rather than read as unset, which would fall back to the DSN — often plaintext;
- a CA or certificate with no mode is refused: whether it was meant to verify or merely to be offered cannot be guessed;
- so are files beside tls = "disable", which would never be read;
- a client certificate needs its key and the other way round;
- tls_key_password must be an environment reference, and needs a tls_key to unlock. A literal is refused rather than used: accepting it would leave a passphrase in a plain-text file, and in every browser dbc web shows the connection to.
func (TLSOpts) KeyPasswordVar ¶ added in v0.3.0
KeyPasswordVar is the name of the environment variable tls_key_password refers to, or "" when there is none — or when it is not a reference, which Check refuses.