Documentation
¶
Overview ¶
Package handlers implements TableX's HTTP handlers — one file per feature area (auth, home, server, database, table, sql, navigation). Handlers resolve the active Connection from the session's UserContext, call the engine-neutral driver API, build a view model and render via internal/view. They never import a concrete database driver or concatenate user input into SQL.
Index ¶
- Constants
- func BoundedParseForm(r *http.Request) error
- func RequestID(ctx context.Context) string
- func WithListingMemo(ctx context.Context) context.Context
- func WithRequestID(ctx context.Context, id string) context.Context
- type CounterSnapshot
- type Counters
- type DBOpLimiter
- type Handlers
- func (h *Handlers) DBCreateTable(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBDesigner(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBEvents(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBEventsManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBExport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBImport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBOperations(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBPrivileges(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBPrivilegesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBQBE(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBRoutines(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBRoutinesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBSQL(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBSearch(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBStructure(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBTriggers(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) DBTriggersManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) EventEditor(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) Home(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) Login(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) LoginForm(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) NavChildren(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) NavTree(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ObjectDefinition(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) RenderLoginThrottled(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) RenderRestricted(w http.ResponseWriter, r *http.Request, msg string)
- func (h *Handlers) RoutineEditor(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) RoutinePrivileges(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) RoutinePrivilegesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) SSOCallback(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) SSOStart(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerDatabases(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerDatabasesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerExport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerImport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerProcesses(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerProcessesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerSQL(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerStatus(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerUsers(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerUsersManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) ServerVariables(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableBrowse(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableBulkApply(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableBulkRows(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableDelete(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableEdit(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableEditForm(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableExport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableImport(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableInsert(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableInsertForm(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableOperations(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TablePrivileges(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TablePrivilegesManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableSQL(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableSearch(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableStructure(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableTriggers(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TableTriggersManage(w http.ResponseWriter, r *http.Request)
- func (h *Handlers) TriggerEditor(w http.ResponseWriter, r *http.Request)
- type PoolBudget
- type RateLimiter
- type UserContext
- func (uc *UserContext) AddFlash(f view.Flash)
- func (uc *UserContext) AddHistory(q string)
- func (uc *UserContext) Capabilities() driver.Capabilities
- func (uc *UserContext) Close()
- func (uc *UserContext) ClosePool(database string)
- func (uc *UserContext) ConnFor(ctx context.Context, database string) (*driver.Connection, error)
- func (uc *UserContext) Dialect() driver.Dialect
- func (uc *UserContext) ExportConnFor(ctx context.Context, database string) (*driver.Connection, error)
- func (uc *UserContext) History() []string
- func (uc *UserContext) PinnedFor(ctx context.Context, database string) (*driver.Pinned, error)
- func (uc *UserContext) RowsPerPage() int
- func (uc *UserContext) ServerConn() *driver.Connection
- func (uc *UserContext) SetRowsPerPage(n int)
Constants ¶
const ( SSOStartPath = "/auth/sso/start" SSOCallbackPath = "/auth/sso/callback" )
The SSO gate's paths. Exported because three places name them — the route table, the middleware that must let them through, and the handlers that redirect to them — and three literals would drift apart. Both are GET: the provider completes the flow by redirecting the browser, and a redirect cannot be a POST.
const MaxImportBytes = 32 << 20 // 32 MiB
MaxImportBytes caps an import upload. The server package's route-aware limitBody applies this same cap to import routes *before* CSRF parses the multipart body, so the cap holds even on the no-JS path (token in the form field, not a header) where csrf would otherwise parse the whole body under the looser global limit first.
const MaxImportExpandedBytes = 4 * MaxImportBytes
MaxImportExpandedBytes bounds a compressed upload AFTER decompression. The upload cap is not itself a bound on the work: gzip's ratio is unbounded in principle, so 32 MiB of well-chosen input expands to many gigabytes — the classic decompression bomb — and the importer holds the whole script in memory. This constant is therefore a memory budget, not a formality.
It is deliberately larger than MaxImportBytes: a compressed upload that could only expand to its own uploaded size would make the option pointless, since the whole reason to gzip a dump is that it is much bigger than the wire.
Variables ¶
This section is empty.
Functions ¶
func BoundedParseForm ¶
BoundedParseForm parses the request form, dispatching on media type: multipart/form-data goes through ParseMultipartForm (with the small in-memory threshold above), everything else through ParseForm. This closes two gaps: ParseForm never reads a multipart body (so a later PostFormValue would trigger ParseMultipartForm under Go's 32 MiB default), and a prior successful ParseForm leaves r.PostForm non-nil so a multipart body's fields would silently read empty. It is idempotent (a cached form is reused). Exported so the csrf middleware can bound the FIRST parse of a no-JS (form-token) request before it extracts the token.
func WithListingMemo ¶
WithListingMemo installs a fresh per-request listing memo (called by the server's request middleware alongside the request id).
Types ¶
type CounterSnapshot ¶
type CounterSnapshot struct {
LoginsOK int64
LoginsDenied int64
LoginsThrottled int64
QueryBudgetRefused int64
}
CounterSnapshot is a consistent-enough read of the counters: each value is read atomically, but not all together, which is the right trade for monotonic counters a scraper differentiates anyway.
type Counters ¶
type Counters struct {
// contains filtered or unexported fields
}
Counters are the process-wide numbers only the handlers are in a position to see. The server package renders them at /metrics; it cannot count them itself, because a login rejection and a spent query budget are both invisible from the middleware (the first re-renders a form at 200, the second surfaces as one statement's error inside an otherwise ordinary response).
A nil *Counters is a valid disabled set — every method tolerates it — so the many tests that build a bare &Handlers{} do not have to wire one up.
func (*Counters) Snapshot ¶
func (c *Counters) Snapshot() CounterSnapshot
Snapshot reads every counter. A nil receiver reads as all zeros.
type DBOpLimiter ¶
type DBOpLimiter struct {
// contains filtered or unexported fields
}
DBOpLimiter caps how many requests may simultaneously hold a PRIVATE database connection — a streaming export, a SQL console script or a SQL import. Those deliberately open transient connections outside the cached-pool machinery (they must not share or evict a session pool), which put them outside PoolBudget as well; nothing else bounds them, and the server has no in-flight request cap, so enough parallel exports could exhaust the DATABASE's max_connections and take down every other client of that server.
It refuses rather than queues: a caller that waited would hold an HTTP worker and its session while contributing nothing, and the client has no way to know the request is parked. 503 + Retry-After is the honest answer.
func NewDBOpLimiter ¶
func NewDBOpLimiter(n int) *DBOpLimiter
NewDBOpLimiter allows up to n concurrent private-connection operations; n <= 0 means unlimited.
func (*DBOpLimiter) InFlight ¶
func (l *DBOpLimiter) InFlight() int
InFlight reports how many slots are currently held (0 when unlimited).
func (*DBOpLimiter) Limit ¶
func (l *DBOpLimiter) Limit() int
Limit reports the concurrency ceiling, or 0 when unlimited.
func (*DBOpLimiter) Refused ¶
func (l *DBOpLimiter) Refused() int64
Refused reports how many operations have been turned away since startup.
func (*DBOpLimiter) TryAcquire ¶
func (l *DBOpLimiter) TryAcquire() (release func(), ok bool)
TryAcquire reserves a slot, returning a release func and true on success. The release func is idempotent, so `defer release()` is safe alongside an early explicit release. A nil limiter or an unlimited one always succeeds.
type Handlers ¶
type Handlers struct {
View *view.Renderer
Sessions *session.Manager
Cfg config.Config
Rate RateLimiter
Log *slog.Logger
Version string
Pools *PoolBudget // process-wide per-database pool budget (nil = unlimited)
DBOps *DBOpLimiter // in-flight cap on private-connection ops (nil = unlimited)
Proxy *auth.ProxyTrust // trusted-proxy set for client-IP resolution (nil = XFF untrusted)
Audit *audit.Logger // audit trail (nil = not configured; every method is nil-safe)
Counters *Counters // /metrics counters the handlers own (nil = not counted; nil-safe)
// SSO is the discovered OIDC provider, or nil when no gate is configured.
// Nil is the ONLY signal the handlers use: the gate routes 404 and the
// middleware lets everything through, so a deployment without [sso] behaves
// exactly as it did before the feature existed.
SSO *auth.OIDCProvider
// AccountRate throttles failed logins per ACCOUNT NAME, across all source
// addresses. Separate from Rate because it needs its own, higher threshold:
// its key is shared by everyone typing that name, so reusing the per-IP cap
// would let one person's typo lock out a whole team. Nil = disabled.
AccountRate *auth.RateLimiter
}
Handlers carries the shared dependencies for every handler.
func (*Handlers) DBCreateTable ¶
func (h *Handlers) DBCreateTable(w http.ResponseWriter, r *http.Request)
DBCreateTable renders (GET) and runs (POST) the create-table form. It mirrors DBOperations' dispatch shape: validate-first, build via the SchemaEditor, exec via ExecScript, redirect with a flash.
func (*Handlers) DBDesigner ¶
func (h *Handlers) DBDesigner(w http.ResponseWriter, r *http.Request)
DBDesigner renders a read-only schema/relations map of the database: a card per table (columns with primary-key and foreign-key markers) plus a summary of every foreign-key relationship. It is a clean-room visualization built from the neutral introspection model — no .reference/ code is used.
func (*Handlers) DBEvents ¶
func (h *Handlers) DBEvents(w http.ResponseWriter, r *http.Request)
DBEvents lists scheduled events (GET /db/{db}/events).
func (*Handlers) DBEventsManage ¶
func (h *Handlers) DBEventsManage(w http.ResponseWriter, r *http.Request)
DBEventsManage handles POST /db/{db}/events.
func (*Handlers) DBOperations ¶
func (h *Handlers) DBOperations(w http.ResponseWriter, r *http.Request)
DBOperations renders and runs database maintenance (create/drop database).
func (*Handlers) DBPrivileges ¶
func (h *Handlers) DBPrivileges(w http.ResponseWriter, r *http.Request)
DBPrivileges lists the server's accounts and the database's direct grants (GET /db/{db}/privileges); with a PrivilegeManager engine it also renders the grant/revoke controls (POST handled by DBPrivilegesManage in access.go).
func (*Handlers) DBPrivilegesManage ¶
func (h *Handlers) DBPrivilegesManage(w http.ResponseWriter, r *http.Request)
DBPrivilegesManage handles POST /db/{db}/privileges (database-scope grant/revoke).
func (*Handlers) DBQBE ¶
func (h *Handlers) DBQBE(w http.ResponseWriter, r *http.Request)
DBQBE is a single-table query-by-example builder (GET form, POST run): pick a table, choose which columns to project and per-column criteria, and run a parameterized SELECT. It reuses the Search operator whitelist and the same parameter-binding/LIKE-escaping rules — no value is ever concatenated.
func (*Handlers) DBRoutines ¶
func (h *Handlers) DBRoutines(w http.ResponseWriter, r *http.Request)
DBRoutines lists stored procedures/functions (GET /db/{db}/routines).
func (*Handlers) DBRoutinesManage ¶
func (h *Handlers) DBRoutinesManage(w http.ResponseWriter, r *http.Request)
DBRoutinesManage handles POST /db/{db}/routines.
func (*Handlers) DBSQL ¶
func (h *Handlers) DBSQL(w http.ResponseWriter, r *http.Request)
DBSQL is the database-level SQL console (GET/POST /db/{db}/sql).
func (*Handlers) DBSearch ¶
func (h *Handlers) DBSearch(w http.ResponseWriter, r *http.Request)
DBSearch searches every table's text columns for a term (GET form, POST run), reporting per-table match counts.
func (*Handlers) DBStructure ¶
func (h *Handlers) DBStructure(w http.ResponseWriter, r *http.Request)
DBStructure renders a database's Structure tab (GET /db/{db}). For PostgreSQL with no schema selected it lists schemas; otherwise it lists tables/views.
func (*Handlers) DBTriggers ¶
func (h *Handlers) DBTriggers(w http.ResponseWriter, r *http.Request)
DBTriggers lists triggers in the database (GET /db/{db}/triggers).
func (*Handlers) DBTriggersManage ¶
func (h *Handlers) DBTriggersManage(w http.ResponseWriter, r *http.Request)
DBTriggersManage handles POST /db/{db}/triggers.
func (*Handlers) EventEditor ¶
func (h *Handlers) EventEditor(w http.ResponseWriter, r *http.Request)
func (*Handlers) Home ¶
func (h *Handlers) Home(w http.ResponseWriter, r *http.Request)
Home renders the server overview (GET /). Requires an authenticated session (enforced by the auth gate middleware).
func (*Handlers) Login ¶
func (h *Handlers) Login(w http.ResponseWriter, r *http.Request)
Login authenticates (POST /login) by opening a real database connection with the supplied (or predefined-server) parameters. On success it builds the session's UserContext, mints the authenticated session in one atomic step (fresh ID + CSRF — anti-fixation) and redirects home. Failures are rate-limited and reported generically.
func (*Handlers) LoginForm ¶
func (h *Handlers) LoginForm(w http.ResponseWriter, r *http.Request)
LoginForm renders the login page (GET /login). Already-authenticated users are redirected home.
func (*Handlers) Logout ¶
func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request)
Logout destroys the session and closes its pools (POST /logout).
func (*Handlers) NavChildren ¶
func (h *Handlers) NavChildren(w http.ResponseWriter, r *http.Request)
NavChildren is the htmx endpoint that lazily loads a node's children (GET /nav/children?db=&schema=).
func (*Handlers) NavTree ¶
func (h *Handlers) NavTree(w http.ResponseWriter, r *http.Request)
NavTree is the htmx endpoint that re-renders the whole top-level database tree (GET /nav), used to refresh the sidebar after a structural change (create / drop / rename database or table) that only swapped #page_content.
func (*Handlers) ObjectDefinition ¶
func (h *Handlers) ObjectDefinition(w http.ResponseWriter, r *http.Request)
ObjectDefinition serves one object's CREATE statement as an htmx fragment (GET /db/{db}/definition?kind=&name=&i=[&table=]). The object is addressed by list position plus name; that rule and its resolvers live in programs.go, shared with the drop actions.
func (*Handlers) RenderLoginThrottled ¶
func (h *Handlers) RenderLoginThrottled(w http.ResponseWriter, r *http.Request)
RenderLoginThrottled renders the generic "too many login attempts" response from the csrf middleware's coarse bare-IP throttle, which fires BEFORE the body is parsed — so no posted sticky values (username/host/server) can be echoed back. It reuses loginThrottled, matching Login's own throttle rejection: a full-page caller gets the login form + HTTP 429 + Retry-After; an htmx caller gets HTTP 200 + HX-Redirect /login.
func (*Handlers) RenderRestricted ¶
RenderRestricted answers a request refused by restricted mode (config [restrict]) with a 403 and an explanation naming the setting responsible. Exported because the enforcement is a middleware in the server package, which owns the policy but not the renderer.
func (*Handlers) RoutineEditor ¶
func (h *Handlers) RoutineEditor(w http.ResponseWriter, r *http.Request)
RoutineEditor opens the definition editor, as TriggerEditor and EventEditor do for their kinds. Separate entry points rather than one kind= route, so each list page's link is a plain URL and the kind cannot be spoofed into administering another kind.
func (*Handlers) RoutinePrivileges ¶
func (h *Handlers) RoutinePrivileges(w http.ResponseWriter, r *http.Request)
RoutinePrivileges lists the grants on one stored routine (GET /db/{db}/routines/privileges?name=…&i=…), with the grant/revoke controls on an engine that has routine grants (POST handled by RoutinePrivilegesManage in access.go).
It shares dbUsersBody and the db_privileges template with the database- and table-scope pages: same grants table, same grantee <select>, same revoke button — only the object and the allowlist differ.
func (*Handlers) RoutinePrivilegesManage ¶
func (h *Handlers) RoutinePrivilegesManage(w http.ResponseWriter, r *http.Request)
RoutinePrivilegesManage handles POST /db/{db}/routines/privileges — grant and revoke EXECUTE (and MySQL's ALTER ROUTINE) on one stored routine.
It mirrors managePrivileges exactly where it can: same grantee decoding, same existence check, same "grant is validated against the allowlist, revoke against a fresh introspection" split. What it cannot share is the object — a routine is addressed by name AND position (and, on PostgreSQL, by the identity arguments only introspection can supply), so resolution goes through the stored-program addressing rule instead of a TableRef.
func (*Handlers) SSOCallback ¶
func (h *Handlers) SSOCallback(w http.ResponseWriter, r *http.Request)
SSOCallback completes the flow. Every failure below lands on the same generic page: the differences matter to the operator's log, not to whoever is holding a bad callback.
func (*Handlers) SSOStart ¶
func (h *Handlers) SSOStart(w http.ResponseWriter, r *http.Request)
SSOStart begins the authorization-code flow: mint a handshake, remember it on this browser's session, and redirect to the provider.
func (*Handlers) ServerDatabases ¶
func (h *Handlers) ServerDatabases(w http.ResponseWriter, r *http.Request)
ServerDatabases renders the server-level database list (GET /server).
func (*Handlers) ServerDatabasesManage ¶
func (h *Handlers) ServerDatabasesManage(w http.ResponseWriter, r *http.Request)
ServerDatabasesManage handles the server-scoped Create-database control on the databases list (POST /server).
func (*Handlers) ServerExport ¶
func (h *Handlers) ServerExport(w http.ResponseWriter, r *http.Request)
ServerExport / DBExport / TableExport: GET renders the form, POST streams the download. Server scope dumps every (non-system) database.
func (*Handlers) ServerImport ¶
func (h *Handlers) ServerImport(w http.ResponseWriter, r *http.Request)
ServerImport / DBImport / TableImport: GET renders the form, POST runs the SQL script (and, for tables, CSV import). Server scope runs the script against the server connection (so CREATE DATABASE / USE work).
func (*Handlers) ServerProcesses ¶
func (h *Handlers) ServerProcesses(w http.ResponseWriter, r *http.Request)
ServerProcesses shows active processes/connections (GET /server/processes).
func (*Handlers) ServerProcessesManage ¶
func (h *Handlers) ServerProcessesManage(w http.ResponseWriter, r *http.Request)
ServerProcessesManage handles POST /server/processes — terminating a session.
Validate-first, like every other administrative action here: the identifier must parse as an integer (which is also what keeps it out of the statement's syntax — see driver.ProcessManager), and it must appear in a FRESH read of the process list. Whether the account may actually kill that session is the engine's decision; TableX only refuses to name one nobody reported.
func (*Handlers) ServerSQL ¶
func (h *Handlers) ServerSQL(w http.ResponseWriter, r *http.Request)
ServerSQL is the server-level SQL console (GET/POST /server/sql).
func (*Handlers) ServerStatus ¶
func (h *Handlers) ServerStatus(w http.ResponseWriter, r *http.Request)
ServerStatus shows runtime status counters (GET /server/status).
func (*Handlers) ServerUsers ¶
func (h *Handlers) ServerUsers(w http.ResponseWriter, r *http.Request)
ServerUsers lists database accounts/roles (GET /server/users).
func (*Handlers) ServerUsersManage ¶
func (h *Handlers) ServerUsersManage(w http.ResponseWriter, r *http.Request)
ServerUsersManage handles POST /server/users: create_user, drop_user, set_password and (role engines) alter_attrs, dispatched on the action field like DBOperations. The account admin runs on the server connection.
func (*Handlers) ServerVariables ¶
func (h *Handlers) ServerVariables(w http.ResponseWriter, r *http.Request)
ServerVariables shows configuration variables (GET /server/variables).
func (*Handlers) TableBrowse ¶
func (h *Handlers) TableBrowse(w http.ResponseWriter, r *http.Request)
TableBrowse renders the Browse grid (GET /db/{db}/table/{table}).
func (*Handlers) TableBulkApply ¶
func (h *Handlers) TableBulkApply(w http.ResponseWriter, r *http.Request)
TableBulkApply writes the multi-row form (POST .../rows/apply). Mode comes from a hidden field set when the form was RENDERED, not from the button that submitted it: the user already chose Edit or Copy in the browse grid, and a form that could switch between updating and duplicating on submit is one mis-click from doubling a table.
All-or-nothing: every statement runs in one transaction, so a failure halfway through rolls back rather than leaving half the selection written.
func (*Handlers) TableBulkRows ¶
func (h *Handlers) TableBulkRows(w http.ResponseWriter, r *http.Request)
TableBulkRows dispatches a "with selected" action (POST .../rows). The action verb is a form field rather than a route segment because the browse grid is ONE form: a second submit button can add a verb, but it cannot change the form's method or re-check the boxes.
func (*Handlers) TableDelete ¶
func (h *Handlers) TableDelete(w http.ResponseWriter, r *http.Request)
TableDelete deletes the selected rows (POST .../delete). Each token targets one row by its validated key, with values bound as parameters.
func (*Handlers) TableEdit ¶
func (h *Handlers) TableEdit(w http.ResponseWriter, r *http.Request)
TableEdit performs the update (POST .../edit).
func (*Handlers) TableEditForm ¶
func (h *Handlers) TableEditForm(w http.ResponseWriter, r *http.Request)
TableEditForm renders the edit form for one row (GET .../edit?where=token).
func (*Handlers) TableExport ¶
func (h *Handlers) TableExport(w http.ResponseWriter, r *http.Request)
func (*Handlers) TableImport ¶
func (h *Handlers) TableImport(w http.ResponseWriter, r *http.Request)
func (*Handlers) TableInsert ¶
func (h *Handlers) TableInsert(w http.ResponseWriter, r *http.Request)
TableInsert performs the insert (POST .../insert).
func (*Handlers) TableInsertForm ¶
func (h *Handlers) TableInsertForm(w http.ResponseWriter, r *http.Request)
TableInsertForm renders the insert form (GET .../insert). With ?copy=1&where= it prefills from an existing row.
func (*Handlers) TableOperations ¶
func (h *Handlers) TableOperations(w http.ResponseWriter, r *http.Request)
TableOperations renders and runs table maintenance (rename/truncate/drop).
func (*Handlers) TablePrivileges ¶
func (h *Handlers) TablePrivileges(w http.ResponseWriter, r *http.Request)
TablePrivileges lists the grants on a single table (GET .../privileges); with a PrivilegeManager engine it also renders the grant/revoke controls (POST handled by TablePrivilegesManage in access.go).
func (*Handlers) TablePrivilegesManage ¶
func (h *Handlers) TablePrivilegesManage(w http.ResponseWriter, r *http.Request)
TablePrivilegesManage handles POST /db/{db}/table/{table}/privileges (table-scope grant/revoke).
func (*Handlers) TableSQL ¶
func (h *Handlers) TableSQL(w http.ResponseWriter, r *http.Request)
TableSQL is the table-level SQL console (GET/POST /db/{db}/table/{t}/sql).
func (*Handlers) TableSearch ¶
func (h *Handlers) TableSearch(w http.ResponseWriter, r *http.Request)
TableSearch searches a table by per-column conditions (GET form, POST run).
func (*Handlers) TableStructure ¶
func (h *Handlers) TableStructure(w http.ResponseWriter, r *http.Request)
TableStructure serves the Structure tab. GET renders columns, indexes, foreign keys and the reconstructed/native CREATE statement; POST runs a structure-edit operation (add/modify/drop column, add/drop index, add/drop FK).
func (*Handlers) TableTriggers ¶
func (h *Handlers) TableTriggers(w http.ResponseWriter, r *http.Request)
TableTriggers lists triggers, filtered to the current table (GET .../triggers).
func (*Handlers) TableTriggersManage ¶
func (h *Handlers) TableTriggersManage(w http.ResponseWriter, r *http.Request)
TableTriggersManage handles POST /db/{db}/table/{table}/triggers. It shares manageProgram: the table filter comes from the path, which resolveScope already reads, so the database-level and per-table pages need no separate logic.
func (*Handlers) TriggerEditor ¶
func (h *Handlers) TriggerEditor(w http.ResponseWriter, r *http.Request)
type PoolBudget ¶
type PoolBudget struct {
// contains filtered or unexported fields
}
PoolBudget is the process-wide cap on cached per-database pools (config pool_cap), shared by every session. It bounds the unbounded dimension of connection growth: each session caches one pool (up to pool_max_conns connections) per database it touches, so without a budget N sessions × M databases would pile up pools indefinitely. The server-level login pools (one per session) are not counted — the session cap already bounds them — and the short-lived pinned/export connections are bounded by DBOpLimiter instead.
func NewPoolBudget ¶
func NewPoolBudget(capacity int) *PoolBudget
NewPoolBudget builds a budget allowing up to capacity cached per-database pools across all sessions; capacity <= 0 means unlimited.
func (*PoolBudget) InUse ¶
func (b *PoolBudget) InUse() int
InUse reports how many cached pools are currently charged to the budget, and Limit the ceiling (0 when unlimited). Both are for /metrics: pools approaching the cap is the leading indicator of the eviction pressure that makes browsing slow, and it is invisible from the outside.
func (*PoolBudget) Limit ¶
func (b *PoolBudget) Limit() int
Limit reports the pool ceiling, or 0 when unlimited.
type RateLimiter ¶
type RateLimiter interface {
// Reserve atomically checks and consumes one attempt against every key —
// a single critical section, so a concurrent burst cannot pass a separate
// Allowed check before any Record lands.
Reserve(keys ...string) bool
Reset(key string)
}
RateLimiter is the subset of auth.RateLimiter the handlers use (kept as an interface-like alias so tests can substitute a no-op).
type UserContext ¶
type UserContext struct {
ServerID string
ServerName string
Engine string
// contains filtered or unexported fields
}
UserContext is the per-session application payload. It owns the live database pools for one logged-in server connection, the user's preferences and the SQL console history. Credentials live here, in server-side memory only — never in the cookie, never logged, never persisted. Close() (wired to the session) releases every pool on logout or expiry.
func NewUserContext ¶
func NewUserContext(serverID, serverName string, d driver.Dialect, base driver.ConnParams, serverConn *driver.Connection, budget *PoolBudget) *UserContext
NewUserContext builds a context around an already-opened server connection. budget may be nil (no pool cap).
d MUST be serverConn.Dialect() — the copy driver.Open routed through driver.Specialize, carrying the detected flavor, server version and sql_mode — and never the registered singleton returned by driver.Get. The singleton is the zero value, so every version/flavor gate reading uc.Dialect() fails closed: that is precisely how the SQL console came to classify MariaDB's `DELETE … RETURNING` as a non-row statement and discard its rows. (The parameter stays separate from serverConn because tests build connection-less contexts, and because a few deliberately pair a dialect with a stand-in connection of another engine.)
The dialect is fixed for the session's lifetime and read without uc.mu. A later rebindServerConn does NOT refresh it: the maintenance database it swaps to lives on the same server, so the specialization is identical, and a mutable field would race every lock-free uc.Dialect() reader.
func (*UserContext) AddFlash ¶
func (uc *UserContext) AddFlash(f view.Flash)
AddFlash queues a flash message to show on the next full page render (used across HX-Location / 303 redirects, which start a fresh request).
func (*UserContext) AddHistory ¶
func (uc *UserContext) AddHistory(q string)
AddHistory records a SQL statement in the per-session console history (most-recent first, de-duplicated, bounded in count and per-entry size).
func (*UserContext) Capabilities ¶
func (uc *UserContext) Capabilities() driver.Capabilities
Capabilities returns the engine capability flags.
func (*UserContext) Close ¶
func (uc *UserContext) Close()
Close releases every pool. Wired to the session's close hook. The closed flag makes every later (or in-flight, racing) ConnFor/PinnedFor/ExportConnFor fail with errSessionClosed instead of re-opening pools nobody would close; a dial already in flight sees the flag when it re-locks and discards its fresh pool.
func (*UserContext) ClosePool ¶
func (uc *UserContext) ClosePool(database string)
ClosePool closes and forgets the cached per-database pool for database (if any), releasing its budget slot, and bumps the database's generation so an in-flight ConnFor dial for it discards its fresh pool instead of re-caching a pool (and re-leaking a slot) for a database that is being dropped. Safe on every engine.
func (*UserContext) ConnFor ¶
func (uc *UserContext) ConnFor(ctx context.Context, database string) (*driver.Connection, error)
ConnFor returns a connection bound to the given database, opening and caching a pool if necessary. Engines whose databases all live on the one server connection (Capabilities().DatabasesShareConnection — SQLite's single file) and the login database reuse the server connection; other databases get their own pool — required for PostgreSQL (one DB per connection) and convenient for MySQL console ergonomics.
func (*UserContext) Dialect ¶
func (uc *UserContext) Dialect() driver.Dialect
Dialect returns the engine dialect.
func (*UserContext) ExportConnFor ¶
func (uc *UserContext) ExportConnFor(ctx context.Context, database string) (*driver.Connection, error)
ExportConnFor opens a dedicated, transient connection pool for a streaming export, bound to the given database (or the login database when empty). The caller owns it and must Close it when the export finishes — shared ConnFor pools must never be closed (they are cached per session). Dialect-owned export session pinning is applied via driver.ExportConnAdjuster (MySQL pins time_zone, sql_mode and sql_quote_show_create so dumps render restore-parsable); the hook clones the shared Params map before modifying.
func (*UserContext) History ¶
func (uc *UserContext) History() []string
History returns a copy of the console history (most recent first).
func (*UserContext) PinnedFor ¶
PinnedFor opens a dedicated, transient single connection bound to the given database (or the login database when empty) for multi-statement script execution. Unlike ConnFor it never shares or caches: the caller owns the connection and must Close it when the script finishes, which discards any session state (SETs, PRAGMAs) the script established.
func (*UserContext) RowsPerPage ¶
func (uc *UserContext) RowsPerPage() int
RowsPerPage returns the user's browse page size.
func (*UserContext) ServerConn ¶
func (uc *UserContext) ServerConn() *driver.Connection
ServerConn returns the server-level connection (no specific database). The read is a lock-free atomic load: the pointer is normally set once, but a successful DROP DATABASE on the login database rebinds it (under uc.mu) to a maintenance-DB pool. After Close the underlying pool is closed and any query on it returns database/sql's ErrDBClosed — a clean error, not a leak (only ConnFor could re-open pools, and its closed check prevents that).
func (*UserContext) SetRowsPerPage ¶
func (uc *UserContext) SetRowsPerPage(n int)
SetRowsPerPage updates the browse page-size preference, clamping it to an allowed option so a poisoned or out-of-range value cannot persist.
Source Files
¶
- access.go
- auth.go
- browsesort.go
- bulkrows.go
- chrome.go
- columnform.go
- confirm.go
- context.go
- database.go
- definition.go
- designer.go
- edit.go
- export.go
- handlers.go
- home.go
- importer.go
- listings.go
- metadata.go
- metrics.go
- nav.go
- operations.go
- programs.go
- qbe.go
- search.go
- server.go
- server_monitor.go
- sql.go
- sso.go
- structure.go
- table.go
- urls.go