Documentation
¶
Overview ¶
Package workspace is one person's working state against the databases, and the rules for changing it — with no UI in it.
A Workspace holds the active connection and its catalog, the session pinned to it, the run in flight, and the last statement, error, result and plan. The TUI (package tui) and the browser UI (dbc web) are both clients: they turn keys and clicks into calls here, and draw what comes back. The rules therefore exist once — ONE RUN AT A TIME, A PINNED SESSION per active connection, CANCEL REACHES THE SERVER (see run.go) — rather than as two copies that drift.
Starting work, and where its outcome lands ¶
Methods that start work (Connect, Run, Explain, RunScript) never block. They do the bookkeeping that must happen at once — claim the run slot, record history, bump a generation — and return a Start holding a Job: the blocking half, for the caller to run wherever its UI runs blocking work.
UI goroutine worker goroutine
──────────── ────────────────
st, err := w.Run(...) ── claims the slot, returns st.Job
err != nil → refused (busy, no connection, nothing to run)
log st.Notes
hand st.Job to a worker ─────────► ev := st.Job()
runs the statements
LANDS the outcome under w.mu:
frees the slot, sets last*
◄── returns ev (*RunDone)
draw ev (grid, status, log)
The TUI wraps a Job in a tea.Cmd, and the event comes back to Update as a message; the web layer runs it on a goroutine and writes the event to the workspace's SSE stream. The Job lands its own outcome before returning, so the workspace's state is right no matter when — or whether — a UI gets round to drawing the event.
Why a Job and not a channel of events: Bubble Tea already is an event loop with its own way of running blocking work (tea.Cmd), and the TUI's test harness drives it synchronously — press a key, run the command, feed its message back — so a workspace that ran its own goroutines would make every TUI test a race against a channel. Handing the blocking half back to the caller keeps both UIs in charge of their own concurrency.
Events that happen mid-run rather than at the end — a script's s.Show and s.Print — cannot be a Job's return value, and go to Options.Sink instead.
Locking ¶
mu guards the bookkeeping and is only ever held briefly. sessMu guards the pinned session and is held for as long as a statement runs on it — which may be minutes. The rule that keeps them from deadlocking: never take sessMu while holding mu. (Taking mu under sessMu is not needed today and is avoided too.)
Index ¶
- Constants
- func AttachColumns(ctx ai.Context, cols [][]db.Column, err error) (ai.Context, string)
- func Pick(ed Editor) (stmts []string, tag string)
- func PickAll(text string) (stmts []string, tag string)
- func PlanStatus(p *explain.Plan) string
- func Preview(stmt string) string
- func Project(src *model.Result, rows, cols []int) *model.Result
- func ResultStatus(r *model.Result, maxRows, shown int) string
- func SortRows(order []int, r *model.Result, col int, desc bool, numeric []bool)
- func StmtRange(text string, caret int) [2]int
- func WidestNumeric(r *model.Result, c int) int
- type Connected
- type Editor
- type Event
- type ExplainDone
- type GridView
- type Job
- type Level
- type Note
- type Options
- type Reason
- type Refusal
- type RunDone
- type ScriptPrint
- type ScriptShow
- type SessionReleased
- type Start
- type Workspace
- func (w *Workspace) Active() string
- func (w *Workspace) Busy() bool
- func (w *Workspace) Cancel() (n Note, status string)
- func (w *Workspace) CancelConnect() (Note, bool)
- func (w *Workspace) Catalog() *model.Result
- func (w *Workspace) ChatContext(question string, ed Editor, view GridView) (ctx ai.Context, refs []db.TableRef)
- func (w *Workspace) Close()
- func (w *Workspace) Config() *config.Config
- func (w *Workspace) Connect(name string) Start
- func (w *Workspace) Connecting() (name string, ok bool)
- func (w *Workspace) Explain(stmt, where string, analyze bool) (Start, error)
- func (w *Workspace) ExplainEditor(ed Editor, analyze bool) (Start, error)
- func (w *Workspace) History() *userdata.History
- func (w *Workspace) LastErr() string
- func (w *Workspace) LastResult() *model.Result
- func (w *Workspace) LastStmt() string
- func (w *Workspace) ListTables() (Start, error)
- func (w *Workspace) LookupColumns(refs []db.TableRef) ([][]db.Column, error)
- func (w *Workspace) Manager() *db.Manager
- func (w *Workspace) Plan() *explain.Plan
- func (w *Workspace) Run(stmts []string, tag string) (Start, error)
- func (w *Workspace) RunEditor(ed Editor, all bool) (Start, error)
- func (w *Workspace) RunProgress() (step, steps int)
- func (w *Workspace) RunScript(path string) (Start, error)
- func (w *Workspace) RunStmts(stmts []string, tag string) (Start, error)
- func (w *Workspace) RunTag() string
- func (w *Workspace) RunningStatus() string
- func (w *Workspace) Session() (conn string, stateful bool)
- func (w *Workspace) Stop()
- func (w *Workspace) Switch(name string) Start
- func (w *Workspace) TableIndex() *db.TableIndex
- func (w *Workspace) Ticking(gen int) (status string, ok bool)
Constants ¶
const MaxSchemaTables = 8
MaxSchemaTables caps how many tables' columns go with one question. A query joining more than this is rare; a question whose words happen to name a dozen tables is not asking about all of them.
const SchemaLookupTimeout = 3 * time.Second
SchemaLookupTimeout bounds the catalog query a question's tables cost at send time. Past it the question goes without columns rather than waiting on a busy database: the model answering a little less precisely beats the user watching a spinner for a lookup they never asked for.
Variables ¶
This section is empty.
Functions ¶
func AttachColumns ¶
AttachColumns puts a lookup's columns into the context a question was asked with. Only the tables the catalog could describe are kept, so the transcript's "sent: schema of …" names exactly the tables whose columns went. A failed lookup drops the schema entirely and returns the line the transcript shows about it: the question still goes, and the user is told why the model may be guessing column names rather than reading dbc's.
Both UIs call this, so the rule for what a failed or partial lookup sends cannot differ between the terminal and the browser.
func Pick ¶
Pick is what Ctrl+R executes: the selection's statements when there is a selection, otherwise the statement under the caret. tag names the run for the status bar and log: "query" when the buffer holds one statement, "statement 2/4" when the caret picked one of several, "selection" or "selection (3 statements)".
func PickAll ¶
PickAll is what Ctrl+Shift+R executes: every statement in the buffer, whatever the caret or selection says. A one-statement buffer is tagged "query", as Pick tags it, since "all" of one says nothing more.
func PlanStatus ¶
PlanStatus is the status-bar summary of a plan: "plan · 12 steps, cost 431 · 2 findings".
func Project ¶
Project builds a Result holding the given result rows (in the order given — a slice of a display order) and result columns (likewise), with Raw carried along — so a copied selection exports exactly like a whole result: NULLs stay NULLs, numbers stay right-aligned in HTML.
Only the listed columns are taken, so a column the grid hides is left out by not listing it: a copy takes what the user sees. That is what makes hiding useful for sharing — hide the noisy columns, then copy the table for Teams.
func ResultStatus ¶
ResultStatus is the status-bar summary of a result. shown is how many rows the UI displays (max_display_rows can cap it below len(r.Rows)).
func SortRows ¶
SortRows fills order with the display order of r's rows sorted by result column col (ascending, or descending with desc). col < 0 means the result's own order. numeric says which columns compare as numbers — pass export.NumericColumns(r); it is a parameter because a UI computes it once per result, and this may run on every header click.
NULLs sort LAST in both directions: "biggest first" should not open on a screen of nothing. A numeric column compares numbers, so 10 sorts after 9; anything else compares its text case-insensitively. The sort is stable, so rows that tie keep the result's order.
func StmtRange ¶
StmtRange is the byte range of the statement under the caret, for an editor's gutter marker — zero when the buffer holds fewer than two statements, since marking the only one says nothing.
func WidestNumeric ¶
WidestNumeric is the widest display value of numeric column c over EVERY row of r — for a column export.NumericColumns vouched for.
Auto-sizing measures only a grid's first few hundred rows (a text column's width costs a grapheme walk per value, and a huge result should not pay that on arrival), which is fine for text, whose width varies without pattern, and wrong for numbers, which grow: an id column sized by rows 1–500 shows "10…" at row 1,000. A number's display text is plain ASCII, so its width is len() — no decoding — and scanning every row is a length read per cell. NULLs count as their "NULL" text, which is how both grids draw them.
The widest text is measured rather than derived from the column's min/max: a float's text is not monotonic in its value (0.123456789 is wider than 1000), and a length scan is as cheap as a comparison.
Types ¶
type Connected ¶
type Connected struct {
Name string
Catalog *model.Result // the connection's tables; nil if the catalog query failed
Err error // the connect's failure (db.ErrCanceled when stopped); nil on success
Changed bool // the active connection moved to Name
// Stale reports a connect superseded by a newer one: nothing landed,
// and a UI draws nothing.
Stale bool
Notes []Note
Status string // for a status bar; "" leaves it
// Release, when non-nil, closes the session pinned to the connection
// just left — rolling back what it left open. It is a separate Job
// because it waits for any statement still running on that session,
// and the switch itself must not.
Release Job
}
Connected lands a connect.
type Editor ¶
Editor is what a UI's SQL editor holds at the moment of a request: the buffer, the caret as a byte offset into it, and the selected text ("" when nothing is selected). The UI sends the caret and the workspace picks the statement, so "the statement under the caret" means the same thing in every UI.
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is something that happened in the workspace. The concrete types are the pointers below.
type ExplainDone ¶
type ExplainDone struct {
Tag string
Conn string
Stmt string
Plan *explain.Plan
Err error
Elapsed time.Duration
Stale bool
Notes []Note
Status string // as RunDone's: set on failure only
}
ExplainDone lands an explain.
type GridView ¶
type GridView struct {
Result *model.Result // the result the grid holds
Hidden []int // result columns hidden in the grid, ascending
SortCol int // the result column sorted on; -1 for the result's own order
SortDesc bool
Order []int // display row → result row; read, never kept
}
GridView is how a UI's grid shows the result it holds — presentation, so it stays with each UI, but the assistant's data rule needs it: rows go in the order the user sees them, and hidden columns stay out.
type Job ¶
type Job func() Event
Job is the blocking half of a request: it does the work, lands the outcome in the workspace, and returns an Event describing what landed (nil when there is nothing to tell). Run it off the UI goroutine.
type Level ¶
type Level int
Level is how a Note should be shown — the TUI's log colors, the web log's classes.
type Note ¶
Note is one line for the user's log. The workspace writes the words, so a busy refusal or a stopped run reads the same in the terminal and the browser.
type Options ¶
type Options struct {
// Sink receives the events that happen mid-run, from the run's own
// goroutine: a script's ScriptShow and ScriptPrint. It must not block
// for long, and must not call back into the Workspace's start methods
// synchronously. nil drops them.
Sink func(Event)
}
Options configure New beyond the config.
type Reason ¶
type Reason int
Reason is why a request was refused — what the web layer maps to a status code (Busy → 409, the rest → 400).
type Refusal ¶
Refusal is a request the workspace declined, with the reason in words for the user. It is an error, but not a failure: nothing was attempted, and nothing about the workspace changed.
type RunDone ¶
type RunDone struct {
Tag string
Conn string
Stmts []string // what ran; nil for a script
Result *model.Result // the last statement's result; nil on failure and for a script
Plan *explain.Plan // Result is itself a query plan (an EXPLAIN the user typed)
Err error // the failure, or db.ErrCanceled when stopped
Script bool
Elapsed time.Duration
Stale bool // a straggler from a run already written off: draw nothing
Notes []Note
// Status is for the status bar when the run failed or stopped. On
// success it is "": the summary depends on how many rows the UI shows
// (see ResultStatus).
Status string
}
RunDone lands a run of statements, or of a script.
type ScriptPrint ¶
type ScriptPrint struct{ Text string }
ScriptPrint is a script's s.Print, mid-run: a line for the log.
type ScriptShow ¶
ScriptShow is a script's s.Show, mid-run: a result to put in the grid.
type SessionReleased ¶
type SessionReleased struct {
Conn string
Stateful bool // it held state, now gone
Notes []Note // a warning when it did
}
SessionReleased reports that switching connections closed the session pinned to the previous one.
type Start ¶
type Start struct {
Tag string // names the run: "query", "statement 2/4", "explain", …
Gen int // the run generation, for a ticker (see Ticking)
Notes []Note // what to log now — present even when the request was refused
Job Job // the blocking half; nil when refused or when there is nothing to do
}
Start is what a start method returns.
type Workspace ¶
type Workspace struct {
// contains filtered or unexported fields
}
Workspace is one person's working state. Its methods are safe for concurrent use.
func New ¶
New builds a workspace over cfg's connections. It does no IO: the first connect is the caller's Connect(w.Active()).
The active connection starts as the config's default_connection, or the first connection when that names none — so a UI can draw it before the connect completes.
func (*Workspace) Busy ¶
Busy reports whether a run (statements, an explain or a script) is in flight.
func (*Workspace) Cancel ¶
Cancel is Ctrl+K and the Stop button: it stops the run in flight or, with none, the connect in flight. The Note says what it did; status is for a status bar ("" leaves it). The stopped work still lands, as stopped.
func (*Workspace) CancelConnect ¶
CancelConnect abandons the connect in flight. It reports false when there is none. The connect still lands, as canceled.
func (*Workspace) Catalog ¶
Catalog is the active connection's tables, as its catalog query returned them; nil before the first connect completes, or when the query failed.
func (*Workspace) ChatContext ¶
func (w *Workspace) ChatContext(question string, ed Editor, view GridView) (ctx ai.Context, refs []db.TableRef)
ChatContext gathers what the next question may carry — see package ai for what actually goes, which is decided by the connection's ai_rows.
Which statement is "this query": the one under the editor's caret, since that is what the user is looking at. When it is the statement that last ran, its result (or error) comes along; when the editor is empty, the last run's statement stands in.
question is the question being (or about to be) asked; its words, like the statement's, pick which tables' schema goes along. The Tables come back named but without columns — the caller looks those up (they cost a catalog query) — and refs is the same tables as the catalog knows them, for that lookup.
It is cheap enough to call every frame, as the TUI's context chip does.
func (*Workspace) Close ¶
func (w *Workspace) Close()
Close stops what is in flight and releases the pinned session, rolling back whatever it left open. It waits for a statement still running to return from its cancel.
func (*Workspace) Connect ¶
Connect opens the connection and fetches its catalog for a sidebar. The catalog goes through the pool, not the pinned session: it is the app's query, and must not land inside a transaction the user has open.
The connect runs under its own context, so Cancel can abandon it — without that, only connect_timeout bounds it, and with connect_timeout = "0" an unreachable host would hold "connecting…" until the OS gave up on the TCP connect, over a minute later. Starting a connect cancels any still in flight: the user has changed their mind, and the older one must not land after the newer one and switch the connection back.
pick A ──► connGen=1, dial A ───────────── (canceled) ──► lands Stale pick B ──► connGen=2, cancel A, dial B ─► lands, active = B
The Job's event is a *Connected.
func (*Workspace) Connecting ¶
Connecting reports whether a connect is in flight, and to what.
func (*Workspace) Explain ¶
Explain explains one statement on the active connection. where is how the statement was picked ("statement 2/4", "selection"), folded into the tag; "" or "query" adds nothing. The Job's event is an *ExplainDone.
func (*Workspace) ExplainEditor ¶
ExplainEditor is Ctrl+X (estimate) and Alt+X (analyze): explain the statement under the caret, or the selected one.
func (*Workspace) LastErr ¶
LastErr is what the last run failed with, "" if it worked. A stopped run leaves it alone: stopping is the user's choice, not a failure to explain.
func (*Workspace) LastResult ¶
LastResult is the last result published — by a run, or by a script's s.Show. nil before any.
func (*Workspace) ListTables ¶
ListTables is Ctrl+T: the active driver's catalog query, through the same path a run takes, so it lands in the grid and exports like any result. It is the app's query, not the user's, so it is not recorded.
func (*Workspace) LookupColumns ¶
LookupColumns fetches the columns of the tables a question involves (the refs ChatContext returned) on the active connection, under SchemaLookupTimeout. It blocks for a catalog round trip, so a UI calls it off its event loop; the result goes to AttachColumns.
It runs on the POOL, not the pinned session: the session may be mid-run, and a question about a slow query must not wait for that query to end.
func (*Workspace) Run ¶
Run executes statements in order on the pinned session, stopping at the first failure, and publishes the last result. The Job's event is a *RunDone.
func (*Workspace) RunEditor ¶
RunEditor is Ctrl+R (all false) or Ctrl+Shift+R (all true) on what the editor holds.
func (*Workspace) RunProgress ¶
RunProgress is how far a multi-statement run has got: the statement executing now (1-based) and how many there are. steps is 0 when idle, and for work that is not a list of statements.
func (*Workspace) RunScript ¶
RunScript runs a Go script. Its s.Show and s.Print fire from the script's goroutine mid-run and go to the sink as ScriptShow and ScriptPrint; the Job's event is a *RunDone with Script set.
func (*Workspace) RunStmts ¶
RunStmts records statements in the history and runs them. It is how anything the user asked to run by hand gets run: Ctrl+R, run all, a table preview.
func (*Workspace) RunTag ¶
RunTag names the run in flight ("statement 2/4", "explain", "script x.go"); "" when idle.
func (*Workspace) RunningStatus ¶
RunningStatus is the status line for the run in flight, "" when idle.
func (*Workspace) Session ¶
Session describes the pinned session: the connection it is pinned to ("" when none is) and whether it holds state a close would lose — an open transaction, a SET, a temp table. A UI shows the latter as a "transaction open" badge.
It takes sessMu, so it waits for a statement in flight to finish; call it from a UI only where that is acceptable (not every frame).
func (*Workspace) Stop ¶
func (w *Workspace) Stop()
Stop cancels the run and the connect in flight, if any, without a word — the first step of quitting. The canceled work still lands its outcome.
func (*Workspace) Switch ¶
Switch is picking a connection: a connect with a line for the log, and nothing at all when name is already active and its catalog loaded.
func (*Workspace) TableIndex ¶
func (w *Workspace) TableIndex() *db.TableIndex
TableIndex is the catalog indexed for name lookups; nil without one.