Help us shape the future of Go! Take the 2026 Go Developer Survey

workspace

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 23 Imported by: 0

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 — per connection it has been on — a result set: result tabs, the last statement, error and plan (results.go). 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

View Source
const CompletionTimeout = DiagramTimeout

CompletionTimeout bounds reading a schema for completion. The ERD's bound: the same three catalog queries.

View Source
const DiagramTimeout = 30 * time.Second

DiagramTimeout bounds reading a schema for a diagram: three catalog queries, which take milliseconds on a normal database and seconds on a catalog of tens of thousands of tables. Longer than SchemaLookupTimeout, because here the user asked for the diagram and is waiting for it, while the assistant's lookup is a side errand that must not hold up a question.

View Source
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.

View Source
const MaxScriptResults = 20

MaxScriptResults caps how many of one script run's s.Show results are kept for a UI to switch between. A script that shows a result per loop pass could otherwise hold every one of them — each possibly max_rows rows — for as long as the tab lives. The newest are kept: the last result shown is usually the one the script was building toward.

View Source
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

func AttachColumns(ctx ai.Context, cols [][]db.Column, err error) (ai.Context, string)

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

func Pick(ed Editor) (stmts []string, tag string)

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

func PickAll(text string) (stmts []string, tag string)

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

func PlanStatus(p *explain.Plan) string

PlanStatus is the status-bar summary of a plan: "plan · 12 steps, cost 431 · 2 findings".

func Preview

func Preview(stmt string) string

Preview is a one-line, length-capped echo of a statement for the log.

func Project

func Project(src *model.Result, rows, cols []int) *model.Result

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

func ResultStatus(r *model.Result, maxRows, shown int) string

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

func SortRows(order []int, r *model.Result, col int, desc bool, numeric []bool)

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

func StmtRange(text string, caret int) [2]int

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.

The range starts at the statement's first code (CodeStart), not at the comments heading it: a separator such as `---` between two statements is left unmarked, so it reads as the gap it is meant to be. Which statement the caret picks is unchanged, so a caret on such a comment still marks (and Ctrl+R still runs) the statement below, with the marker starting under the caret — as a caret on a blank line between statements does.

func WidestNumeric

func WidestNumeric(r *model.Result, c int) int

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
	// Databases and Schemas are the levels above the tables: the server's
	// databases, on a driver with a database picker (db.HasDatabases:
	// Postgres, MySQL), and this database's schemas with their table
	// counts, on one whose sidebar is loaded a schema at a time
	// (db.Navigable: Postgres). Schema is the one whose tables Catalog
	// holds, "" for every schema's. Each is empty on the other drivers,
	// and Databases or Schemas is nil when its listing failed.
	Databases []db.DatabaseInfo
	Schemas   []db.SchemaInfo
	Schema    string
	Err       error // the connect's failure (db.ErrCanceled when stopped); nil on success
	Changed   bool  // the active connection moved to Name
	// Relisted reports the re-read a run's DDL started (RunDone.Relist),
	// not one the user asked for: the status bar belongs to the run that
	// started it, so a UI leaves it as it is rather than saying "ready".
	Relisted bool
	// Left is the connection Changed moved away from, "" when there was
	// none (the first connect, or one after a Disconnect). A UI that knows
	// no other workspace is on it may close its pool once Release has run
	// — see Workspace.Derived for the one kind worth closing.
	Left string
	// 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
	// Counts, when non-nil, fetches the row counts of the catalog's tables
	// for the sidebar; its event is a *RowCounts. It is separate so the
	// tables list draws at once and the numbers fill in after — a count
	// reads a table's rows, and the list must never wait for that. nil
	// while the sidebar's counts are off (Workspace.ShowRowCounts).
	Counts Job
}

Connected lands a connect.

type Editor

type Editor struct {
	Text      string
	Caret     int
	Selection string
}

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.

const (
	Info   Level = iota // neutral progress: "running query on pg — SELECT …"
	Ok                  // something finished well
	Warn                // worth a look, not a failure: stopped, busy, lost state
	Err                 // a failure
	Accent              // a pointer to something new on screen
	Muted               // background detail
)

type Note

type Note struct {
	Level Level
	Text  string
}

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.

func PlanNotes

func PlanNotes(p *explain.Plan, prev *explain.Plan, elapsed time.Duration) []Note

PlanNotes is what the log says when a plan lands: how long the explain took and the plan's headline, how it compares with prev (the last plan of the same statement, or nil), and each finding worth acting on.

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)

	// WholeCatalog is for a UI with no schema picker (the TUI): a connect
	// that names no schema lists every schema's tables, as long as the
	// database has at most db.AllSchemasLimit of them, instead of opening
	// on the search_path's first schema. Past the limit it opens on that
	// schema all the same, with a note: listing a huge catalog is the cost
	// the per-schema load exists to avoid.
	WholeCatalog bool
}

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).

const (
	Busy         Reason = iota + 1 // a run is already in flight
	NoConnection                   // no active connection
	Nothing                        // nothing to run or explain
	Invalid                        // the request cannot be carried out as asked
)

type Refusal

type Refusal struct {
	Reason Reason
	Note   Note
}

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.

func (*Refusal) Error

func (r *Refusal) Error() string

type ResultTab added in v0.9.0

type ResultTab struct {
	ID     int    // stable for the tab's life: what ShowResultTab and the rest take
	Seq    int    // numbers the result on it; moves whenever that result does
	Title  string // what produced it, one line: a statement's preview, "preview cats", "script x.go"
	Stmt   string // the whole statement, for a tooltip ("" for a script's)
	Pinned bool
	// Result is the result the tab shows. Shared: read it, never write it.
	Result *model.Result
	Shows  int  // how many s.Show results of a script it holds (0 for a run's own)
	Shared bool // shared with the assistant (ShareResultTab)
}

ResultTab describes one tab of the active connection's result set, for a UI's strip.

type RowCounts added in v0.3.0

type RowCounts struct {
	Conn   string
	Counts map[db.TableRef]db.RowCount // read-only; see db.Manager.RowCounts
	// Stale reports counts for a connection that is no longer the active
	// one, or a counting a later connect canceled: nothing landed, and a UI
	// draws nothing.
	Stale bool
	Notes []Note
}

RowCounts lands the row counts of the active connection's tables. The counts are also kept in the workspace (see Workspace.RowCounts), so a UI that redraws its tables list later finds them there.

type RunDone

type RunDone struct {
	Tag   string
	Conn  string
	Stmts []string // what ran; nil for a script
	// Result is the result the run put on screen: the last of its
	// statements' that returned rows (each got a result tab, see Tabs), or
	// the last statement's when none did. A failed run still lands the
	// rows of the statements before the failure, so Result can be set
	// beside Err. nil when nothing landed, and for a script.
	Result *model.Result
	// Tabs is how many result tabs the run filled, one per statement that
	// returned rows (results.go); 0 for a script and when nothing landed.
	Tabs    int
	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
	// Wrote reports that the run may have changed rows (db.ChangesRows of
	// a statement it reached, or a script), failed or not. A UI with other
	// workspaces on the same Manager recounts their sidebars on it
	// (Workspace.Recount); Counts alone would not tell it, being nil
	// whenever this workspace's own counts are off.
	Wrote bool
	// Counts, when non-nil, refreshes the sidebar's row counts: the run
	// Wrote, and the sidebar shows counts (Workspace.ShowRowCounts). Its
	// event is a *RowCounts, as Connected.Counts's is.
	Counts Job
	// Relist, when non-nil, reads the sidebar's catalog again — databases,
	// schemas, tables — after a run that may have changed it: a statement
	// that changes the catalog (sqlsplit.ChangesCatalog), a script that ran
	// one on its connection (sdb.S.CatalogChanged), or the COMMIT that ends
	// a transaction one ran in. It is a Refresh in all but its words
	// (relistLocked), its event a *Connected with Changed false, and it
	// counts the rows of the list it lands — so Counts is nil whenever it
	// is set. A UI must run it whenever it is set, as it does Counts: until
	// it lands, the workspace is mid-connect (Connecting), which refuses a
	// Refresh and a schema pick.
	Relist Job
}

RunDone lands a run of statements, or of a script.

type SchemaLoaded added in v0.3.0

type SchemaLoaded struct {
	Conn    string
	Schema  string        // the schema loaded; "" for every schema
	Catalog *model.Result // its tables; nil when the load failed, and the old list stays
	// Stale reports a load superseded by a later pick or connect: nothing
	// landed, and a UI draws nothing.
	Stale bool
	Notes []Note
	// Counts is the row counting of the new tables, as on Connected.
	Counts Job
}

SchemaLoaded lands a schema pick (Workspace.PickSchema): the tables of another schema of the active database, in place of the ones the sidebar listed.

type SchemaPick added in v0.3.0

type SchemaPick struct {
	// Name is the schema to list. One the database no longer has falls
	// back to the default rather than to an empty list.
	Name string
	// All lists every schema's tables, where the database has at most
	// db.AllSchemasLimit of them; past that, the default schema, with a
	// note saying why.
	All bool
}

SchemaPick says which tables a sidebar on a db.Navigable driver lists (Postgres: see db/navigate.go). The zero value is the default: the first schema on search_path, which is what a bare table name means there.

type ScriptPrint

type ScriptPrint struct {
	Text string
	Conn string
}

ScriptPrint is a script's s.Print, mid-run: a line for the log of Conn, the connection the script run started on.

type ScriptShow

type ScriptShow struct {
	Result *model.Result
	Conn   string
}

ScriptShow is a script's s.Show, mid-run: a result to put in the grid — when Conn, the connection the script run started on (whose result set it landed in), is still the active one.

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

func New(cfg *config.Config, mgr *db.Manager, hist *userdata.History, opt Options) *Workspace

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) Active

func (w *Workspace) Active() string

Active is the active connection's name.

func (*Workspace) Busy

func (w *Workspace) Busy() bool

Busy reports whether a run (statements, an explain or a script) is in flight.

func (*Workspace) CanShareResults added in v0.9.0

func (w *Workspace) CanShareResults() bool

CanShareResults reports whether the active connection lets the assistant see result rows (ai_rows), which is what sharing a result tab needs — for a UI to offer "Share with the assistant", or say why not. The config is read live: dbc web can change ai_rows on a connection at any time.

func (*Workspace) Cancel

func (w *Workspace) Cancel() (n Note, status string)

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

func (w *Workspace) CancelConnect() (Note, bool)

CancelConnect abandons the connect in flight. It reports false when there is none. The connect still lands, as canceled.

func (*Workspace) Catalog

func (w *Workspace) Catalog() *model.Result

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) CatalogSchema added in v0.3.0

func (w *Workspace) CatalogSchema() string

CatalogSchema is the schema whose tables Catalog holds, or "" when it holds every schema's (always, on a driver that is not db.Navigable).

func (*Workspace) ChatContext

func (w *Workspace) ChatContext(question string, ed Editor, views ...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. A result goes only from the tab on screen, and only when that tab holds the statement's result (attachTabLocked) — which after a run of several statements may be an earlier statement's tab.

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.

A result tab the user SHARED (ShareResultTab) changes which result goes: the shared one, whatever the caret is on, framed as shared (ai.Context Shared) — explicit beats implied. The statement under the caret still goes as "this query", with its error when it is the one that just failed; the shared result's own statement goes along to say what the rows are of, and names tables for the schema lookup too.

views are the UI's grids: the one on screen and, when the shared tab is not on screen, that tab's (its parked grid, or the page's snapshot of it). Each is matched to a result by GridView.Result, so passing one that matches nothing is harmless. A shared result with no matching view goes unsorted and with nothing hidden — a UI that kept a view for it must pass it, or columns the user hid would reach the model.

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) CloseResultTab added in v0.9.0

func (w *Workspace) CloseResultTab(id int) error

CloseResultTab closes result tab id, pinned or not. When it was on screen, the tab to its right takes its place (its left, at the end of the strip), as a browser's tabs do; the last one closed leaves the pane empty. A run in flight that picked it lands in a new tab instead.

func (*Workspace) CloseUnpinnedResultTabs added in v0.9.0

func (w *Workspace) CloseUnpinnedResultTabs() int

CloseUnpinnedResultTabs closes every unpinned tab of the active connection's result set — except the one shared with the assistant, which the user is still using — and reports how many went. The current tab, if kept, stays current; otherwise the last kept tab comes on screen.

func (*Workspace) Complete added in v0.4.0

func (w *Workspace) Complete(buffer string, caret int) (res sqlcomplete.Result, ready bool)

Complete suggests what to type at caret (a byte offset into buffer), from the active connection's cached schema. It never blocks on the database: ready is false when the schema is not cached yet, and the caller then runs LoadCompletions off its event loop and asks again. With no connection, or after a failed load, it is ready with the dialect's vocabulary alone.

func (*Workspace) Config

func (w *Workspace) Config() *config.Config

Config is the configuration the workspace was built with.

func (*Workspace) Connect

func (w *Workspace) Connect(name string) Start

Connect opens the connection and loads its sidebar, opening on the default schema (see SchemaPick, Options.WholeCatalog).

func (*Workspace) ConnectPick added in v0.3.0

func (w *Workspace) ConnectPick(name string, pick SchemaPick) Start

ConnectPick 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.

On a db.Navigable driver the catalog is three short reads rather than one long one — the server's databases, this database's schemas, and the tables of the one schema pick names — so a connect to a big server costs what its sidebar shows, not what the server holds. Each has its own budget (navListTimeout, schemaSummaryTimeout, catalogTimeout), and a schema list too slow to count its tables falls back to their names (listSchemas). Elsewhere it is the whole table list, as it always was — after, on MySQL, the server's databases (db.HasDatabases), for the database picker.

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

func (w *Workspace) Connecting() (name string, ok bool)

Connecting reports whether a connect is in flight, and to what.

func (*Workspace) Databases added in v0.3.0

func (w *Workspace) Databases() []db.DatabaseInfo

Databases is the active connection's server's databases, for a database picker; nil on a driver without databases (db.HasDatabases), or when the listing failed. The slice is shared: read it, never write to it.

func (*Workspace) Derived added in v0.3.0

func (w *Workspace) Derived(name string) bool

Derived reports whether name is a connection derived onto another of its base's databases ("<conn>/<database>", config.DatabaseSep) rather than a configured one.

It is what a UI asks of Connected.Left before closing the pool a switch left. A derived pool is the one worth closing: browsing a server's databases opens one per database picked, and without a close each stays open (its *sql.DB until dbc exits, its idle connections until conn_idle_timeout), so the count grows with the browsing rather than with the configured connections. A configured connection's pool is kept, as it always was: there is one per row of the Connections list, and switching back to it should not dial again.

func (*Workspace) Diagram added in v0.3.0

func (w *Workspace) Diagram(ctx context.Context, sel erd.Selection) (*erd.Schema, error)

Diagram reads the active connection's schema and narrows it to sel — the entity-relationship diagram both UIs draw (the TUI saves and opens it, dbc web shows it and serves its files). It blocks for the catalog round trips, so a UI calls it off its event loop.

Like LookupColumns it runs on the POOL, not the pinned session: a diagram must not wait for a long query the user is running, nor read the schema from inside their open transaction.

It does not take the run slot either — reading the catalog is not a run, and a diagram while a query runs is a reasonable thing to want.

A table in sel that is not in the schema is refused (Invalid) rather than dropped quietly: a diagram "around orders" that silently shows the whole database would be a confusing answer.

A diagram of everything (sel naming no tables) is of the schema the sidebar lists, when it lists one (CatalogSchema): on a big database "all" means what the user is looking at, not every table the server holds.

A CATALOG TOO BIG TO READ WHOLE (db.ErrCatalogTooBig) is read SCOPED instead, on Postgres, as completion does (complete.go): Manager.SchemaIn over the schemas the selection can be about (diagramScope). The connection is then remembered as too big (complScoped, shared with completion — both read the same Manager.Schema, so one finding it overflow is the other's answer too), and later diagrams and completion loads skip the failing whole read.

whole (Manager.Schema) ──ok────────────────────────────► Select
  │ ErrCatalogTooBig            (or complScoped[conn] already)
  ▼
complScoped[conn] = true
  ▼
scoped (Manager.SchemaIn: diagramScope) ──► Select

What a scoped diagram misses: a neighbour (sel.Depth) in a schema outside the scope, since SchemaIn leaves out keys to tables it did not read; and a bare name that is unique in the scope but not in the database resolves rather than being refused as ambiguous. Both are the price of drawing anything at all where the whole read cannot.

func (*Workspace) Disconnect added in v0.3.0

func (w *Workspace) Disconnect() (left string, st Start, err error)

Disconnect leaves the active connection without picking another: no active connection, no catalog, and — through the Job — the pinned session closed, rolling back whatever it left open, as a switch does. A connect in flight is abandoned with it (connGen moves on, so it lands Stale), as is the sidebar's counting and schema loading. It returns the connection left: on a connect still dialing, the one being dialed when there was no connection before it.

A run in flight is refused rather than canceled: its session is the one the disconnect would close, and a Stop first leaves the user to decide what becomes of the statement. Nothing to disconnect from is refused too.

The pool is not this workspace's to close — other workspaces may be on the same connection (db.Manager is shared); a UI that knows they are not closes it with db.Manager.Disconnect once the Job has run.

The Job's event is a *SessionReleased, or nil when no session was open.

func (*Workspace) DropResults added in v0.9.0

func (w *Workspace) DropResults(conn string)

DropResults forgets conn's result set — after the connection is removed, so its results do not stay alive in every query tab that visited it.

func (*Workspace) Explain

func (w *Workspace) Explain(stmt, where string, analyze bool) (Start, error)

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

func (w *Workspace) ExplainEditor(ed Editor, analyze bool) (Start, error)

ExplainEditor is Ctrl+X (estimate) and Alt+X (analyze): explain the statement under the caret, or the selected one.

func (*Workspace) History

func (w *Workspace) History() *userdata.History

History is the query history runs are recorded in.

func (*Workspace) HistoryKey added in v0.8.0

func (w *Workspace) HistoryKey() string

HistoryKey is the active connection's database as history records it — what the history pickers (the TUI's Ctrl+P, dbc web's) scope to when they open on "this database".

func (*Workspace) LastErr

func (w *Workspace) LastErr() string

LastErr is what the last run on the active connection 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

func (w *Workspace) LastResult() *model.Result

LastResult is the result on screen: the active connection's current result tab's — a run's, or a script's s.Show. nil when that set has none.

func (*Workspace) LastResultSeq added in v0.9.0

func (w *Workspace) LastResultSeq() (*model.Result, int)

LastResultSeq is LastResult with its seq (see results.go), which moves whenever the result on screen does — a run, a show, another result tab or connection — and only then. 0 with no result.

func (*Workspace) LastStmt

func (w *Workspace) LastStmt() string

LastStmt is the statement the last run on the active connection executed ("" before any, and after a script run: what a script ran is not known).

func (*Workspace) ListTables

func (w *Workspace) ListTables() (Start, error)

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) LoadCompletions added in v0.4.0

func (w *Workspace) LoadCompletions(ctx context.Context) error

LoadCompletions reads the active connection's schema into the completion cache, unless it is cached already. It blocks for the catalog round trips (bounded by CompletionTimeout and ctx), so a UI calls it off its event loop. Callers that arrive while a load is in flight wait for that one rather than starting their own.

The error is the load's, for the UI to report once; completion goes on with the vocabulary alone either way.

func (*Workspace) LookupColumns

func (w *Workspace) LookupColumns(refs []db.TableRef) ([][]db.Column, error)

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) Manager

func (w *Workspace) Manager() *db.Manager

Manager is the connection manager the workspace runs on.

func (*Workspace) PickSchema added in v0.3.0

func (w *Workspace) PickSchema(pick SchemaPick) (Start, error)

PickSchema lists another schema's tables in the sidebar, on the active connection, in place of the ones listed now. The tables load off the UI's event loop (the Job), like a connect's; the event is a *SchemaLoaded, with a Counts job for the new tables' row counts.

It shares connGen with Connect, so whichever of a pick and a connect starts later wins: a connect supersedes a pick in flight (another database's schemas are not this one's), and a pick supersedes the old list's counting and any earlier pick. A pick while a connect is still in flight is refused — the schema list it would pick from is being replaced.

pick sales ──► gen=5, load sales ───────────── (canceled) ─► Stale
pick hr    ──► gen=6, cancel sales, load hr ─► lands, schema = hr

func (*Workspace) PinResultTab added in v0.9.0

func (w *Workspace) PinResultTab(id int, pin bool) error

PinResultTab pins or unpins result tab id. A pinned tab keeps its result: the next run that would have replaced it opens a new tab.

func (*Workspace) Plan

func (w *Workspace) Plan() *explain.Plan

Plan is the active connection's last plan: an explain's, or one recognized in a result.

func (*Workspace) Recount added in v0.4.0

func (w *Workspace) Recount(conn string) Job

Recount refreshes the sidebar's row counts after another workspace's run on conn may have changed rows, or returns nil when this sidebar is not showing conn's tables. A UI that hosts several workspaces on one Manager (dbc web's query tabs and windows) calls it on the others when a RunDone reports Wrote, so every sidebar on the connection shows the write, not only the one that made it — whether or not the writer's own sidebar counts. It returns nil, too, when this sidebar's counts are off.

Unlike recountLocked it does not drop the Manager's cached counts: the writer's landRun already did, before its RunDone was delivered, so what is cached now was counted after the write. Dropping again would only make each workspace count afresh instead of sharing the writer's counting (db.Manager.RowCounts waits for one in flight).

func (*Workspace) Refresh added in v0.9.0

func (w *Workspace) Refresh() (Start, error)

Refresh re-reads the active connection's catalog — the server's databases, this database's schemas, and the tables of the schema the sidebar lists now — for when another client has created, dropped or renamed something since the connect. It is the connect a Switch to the connection already active skips (SwitchPick), forced, and differs from one only in what it keeps:

  • the session: the connection does not change, so there is no Release and a transaction open on the pinned session stays open;
  • the schema pick: the sidebar comes back on the schema it lists now (refreshPickLocked), not the default one;
  • the old list, when the re-read cannot list the tables: an empty sidebar would be no truer than the stale one (landConnect).

The Manager's cached row counts for the connection are dropped too, so a sidebar with its counts on counts afresh rather than serving numbers from before the change that prompted the refresh. The completion cache goes with the landing, as on any connect (landConnect).

It runs as a connect does — on the pool, under connGen, cancelable with Cancel — so a connect started after it supersedes it, and it supersedes a schema pick or a counting in flight. Refused with no active connection, and while a connect is still in flight (the catalog it would re-read is about to be replaced).

The Job's event is a *Connected, with Changed false.

func (*Workspace) Rename added in v0.7.0

func (w *Workspace) Rename(buffer string, caret int, name string) ([]sqlcomplete.Edit, error)

Rename renames the alias or CTE under caret in buffer to name (sqlcomplete.Rename), quoting it as the active connection's dialect needs. It needs no schema, so unlike Complete it never waits on a load. The error is a sentence for the user: why there is nothing to rename.

func (*Workspace) RenameResults added in v0.9.0

func (w *Workspace) RenameResults(from, to string)

RenameResults moves conn from's result sets to to, after a connection rename, so a query tab that visited the old name finds its results under the new one. A set already under to (a name reused) is replaced.

func (*Workspace) ResultTabLimit added in v0.9.0

func (w *Workspace) ResultTabLimit() int

ResultTabLimit is how many tabs one connection's result set holds (result_tabs).

func (*Workspace) ResultTabs added in v0.9.0

func (w *Workspace) ResultTabs() (tabs []ResultTab, cur int)

ResultTabs is the active connection's result set: its tabs in strip order, and the index of the one on screen (-1 when there is none).

func (*Workspace) RowCounts added in v0.3.0

func (w *Workspace) RowCounts() map[db.TableRef]db.RowCount

RowCounts is the row count of each of the catalog's tables that has one (views never do); nil until the connect's counting lands. The map is shared: read it, never write to it.

func (*Workspace) RowCountsShown added in v0.9.0

func (w *Workspace) RowCountsShown() bool

RowCountsShown reports whether the sidebar's row counts are on (ShowRowCounts); they start off.

func (*Workspace) Run

func (w *Workspace) Run(stmts []string, tag string) (Start, error)

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

func (w *Workspace) RunEditor(ed Editor, all bool) (Start, error)

RunEditor is Ctrl+R (all false) or Ctrl+Shift+R (all true) on what the editor holds.

func (*Workspace) RunProgress

func (w *Workspace) RunProgress() (step, steps int)

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

func (w *Workspace) RunScript(path string) (Start, error)

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

func (w *Workspace) RunStmts(stmts []string, tag string) (Start, error)

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

func (w *Workspace) RunTag() string

RunTag names the run in flight ("statement 2/4", "explain", "script x.go"); "" when idle.

func (*Workspace) RunningStatus

func (w *Workspace) RunningStatus() string

RunningStatus is the status line for the run in flight, "" when idle.

func (*Workspace) Schemas added in v0.3.0

func (w *Workspace) Schemas() []db.SchemaInfo

Schemas is every schema of the active database, empty ones included, with its table count; nil on a driver that is not db.Navigable. The slice is shared: read it, never write to it.

func (*Workspace) ScriptChatContext added in v0.9.0

func (w *Workspace) ScriptChatContext(question, name, source string, views ...GridView) (ctx ai.Context, refs []db.TableRef)

ScriptChatContext is ChatContext for a script tab: the editor holds a Go script (name, its file name; source, the editor's text), not SQL.

What differs, and why:

  • The whole source is "the query" (ctx.Script marks it as Go): a script is one program, and Pick's statement split would cut it at the first semicolon into something neither SQL nor Go.
  • No plan: plans are of statements the editor explained, and a script tab explains nothing.
  • Tables are still looked for, in the source and the question — the SQL a script runs is in its string literals, and a table named there is one the model should see the columns of.
  • The last run's error and results go only when that run was THIS script: its s.Show results are what "why is this row here?" is about, while another script's (or a statement's) would be answered as if this one produced them.

The sdb API summary is not set here: it goes once per conversation, which only the assistant (the caller) knows — see ai.Context.ScriptAPI.

func (*Workspace) ScriptResults added in v0.9.0

func (w *Workspace) ScriptResults() (n, at, cut int)

ScriptResults describes the s.Show results held by the result tab on screen: n of them kept (0 for a tab holding a run's own result), the one the grid is on (at, 0-based), and cut, how many earlier shows were dropped to keep to MaxScriptResults — so a UI can number them as the script showed them (cut+1 … cut+n).

func (*Workspace) Session

func (w *Workspace) Session() (conn string, stateful bool)

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) ShareResultTab added in v0.9.0

func (w *Workspace) ShareResultTab(id int, share bool) error

ShareResultTab shares result tab id with the assistant, or stops sharing it. A shared tab's result goes with every question asked on its connection — in place of the result that would otherwise ride along with the statement under the caret — until it is unshared, closed, or another tab is shared (one at a time). The RESULT is what is shared: a shared tab keeps it as a pinned tab would (resultSet.kept), so the next run opens a new tab rather than change what the assistant sees.

Sharing is refused on a connection without ai_rows: result rows are the database's contents, and sending them is that connection's setting to grant (package ai's DATA RULE), not a menu's. Unsharing always works. A share made while ai_rows was on, then turned off, sends column names only, and the note says the rows were withheld (ai.Build).

func (*Workspace) ShowColumns added in v0.3.0

func (w *Workspace) ShowColumns(qname string) (Start, error)

ShowColumns lists a table's information_schema.columns rows in the grid, where they can be read, sorted and copied like any result. qname is the name the sidebar shows for the table (see db.TableIndex.Lookup).

It goes through RunStmts, like a table preview: the statement is recorded in the history, because it is what the user would have typed and is the easiest way back to it (or to an edited SELECT * of it). It is not put in the editor, whose text is the user's.

Only a table in the active connection's catalog is accepted. The name is inlined into SQL, so a name from anywhere else is refused, not escaped and tried.

func (*Workspace) ShowResultTab added in v0.9.0

func (w *Workspace) ShowResultTab(id int) error

ShowResultTab puts result tab id on screen: its result becomes the last result, which is what the grid, copies, exports and the assistant read. Allowed mid-run — looking back at an earlier result while a slow query runs is what keeping them is for — since the run lands in the tab it picked when it started, not in whichever is on screen.

func (*Workspace) ShowRowCounts added in v0.9.0

func (w *Workspace) ShowRowCounts(on bool) Job

ShowRowCounts turns the sidebar's row counts on or off. They start off: every count is an exact count(*), which reads the whole table (see db/rowcount.go), so it is the user's to ask for.

Turning them on returns the Job that counts the listed catalog, whose event is a *RowCounts as a connect's is; nil when nothing is listed yet (the next connect or schema pick counts, now that the switch is on). From then on every connect, schema pick and write recounts, as before.

Turning them off drops the counts and cancels a counting in flight; that one lands Stale (countGen moves on). It returns nil: there is nothing to wait for, and the caller redraws its list without numbers at once.

off ──ShowRowCounts(true)──► on ─► Counts job ─► *RowCounts
on  ──ShowRowCounts(false)─► off: rowCounts nil, in-flight → Stale

func (*Workspace) ShowScriptResult added in v0.9.0

func (w *Workspace) ShowScriptResult(i int) error

ShowScriptResult puts the current result tab's script result i (0-based, among the ones kept) back on the grid: it becomes the last result, which is what the grid, copies, exports and the assistant all read. Refused while a run is in flight — a script still showing results would move the grid under the pick — and for an i out of range.

func (*Workspace) Stop

func (w *Workspace) Stop()

Stop cancels the run, the connect and the row counting in flight, if any, without a word — the first step of quitting. The canceled work still lands its outcome.

func (*Workspace) Switch

func (w *Workspace) Switch(name string) Start

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) SwitchPick added in v0.3.0

func (w *Workspace) SwitchPick(name string, pick SchemaPick) Start

SwitchPick is Switch, opening the sidebar on pick: a UI that remembers a schema per connection asks for it here, rather than loading the default schema's tables only to replace them with a PickSchema at once.

func (*Workspace) TableIndex

func (w *Workspace) TableIndex() *db.TableIndex

TableIndex is the catalog indexed for name lookups; nil without one.

func (*Workspace) Ticking

func (w *Workspace) Ticking(gen int) (status string, ok bool)

Ticking is the status line for run gen while it is still in flight — its tag and elapsed time — and false once it has ended, which is a ticker's cue to stop re-arming.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL