Skip to content

Commit 0ae90f4

Browse files
authored
Merge pull request #1274 from planetscale/bwarminski/branch-connections
Implement Branch Connection View for Postgres and Vitess
2 parents 325aeee + b9bdf39 commit 0ae90f4

54 files changed

Lines changed: 15999 additions & 297 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dist/
1616
completions/
1717
vendor/
1818
.idea
19+
.worktrees/
1920

2021
# Output of running go build cmd/pscale/main.go
2122
main

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ require (
99
github.com/adrg/xdg v0.5.3
1010
github.com/benbjohnson/clock v1.3.5
1111
github.com/briandowns/spinner v1.23.2
12+
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
13+
github.com/charmbracelet/bubbletea v1.3.10
1214
github.com/charmbracelet/huh v1.0.0
15+
github.com/charmbracelet/lipgloss v1.1.0
16+
github.com/charmbracelet/x/ansi v0.11.2
1317
github.com/fatih/color v1.19.0
1418
github.com/frankban/quicktest v1.14.6
1519
github.com/go-sql-driver/mysql v1.9.3
@@ -24,6 +28,7 @@ require (
2428
github.com/mattn/go-isatty v0.0.20
2529
github.com/mattn/go-shellwords v1.0.12
2630
github.com/mitchellh/go-homedir v1.1.0
31+
github.com/muesli/termenv v0.16.0
2732
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
2833
github.com/planetscale/planetscale-go v0.168.1
2934
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4
@@ -53,11 +58,7 @@ require (
5358
github.com/atotto/clipboard v0.1.4 // indirect
5459
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
5560
github.com/catppuccin/go v0.3.0 // indirect
56-
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
57-
github.com/charmbracelet/bubbletea v1.3.10 // indirect
5861
github.com/charmbracelet/colorprofile v0.3.3 // indirect
59-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
60-
github.com/charmbracelet/x/ansi v0.11.2 // indirect
6162
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
6263
github.com/charmbracelet/x/exp/strings v0.0.0-20251201173703-9f73bfd934ff // indirect
6364
github.com/charmbracelet/x/term v0.2.2 // indirect
@@ -93,7 +94,6 @@ require (
9394
github.com/mtibben/percent v0.2.1 // indirect
9495
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
9596
github.com/muesli/cancelreader v0.2.2 // indirect
96-
github.com/muesli/termenv v0.16.0 // indirect
9797
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
9898
github.com/pires/go-proxyproto v0.8.1 // indirect
9999
github.com/planetscale/vitess-types v0.0.0-20250728133330-81b28fd54ee5 // indirect

internal/cmd/branch/branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func BranchCmd(ch *cmdutil.Helper) *cobra.Command {
3535
cmd.AddCommand(RoutingRulesCmd(ch))
3636
cmd.AddCommand(SafeMigrationsCmd(ch))
3737
cmd.AddCommand(LintCmd(ch))
38+
cmd.AddCommand(ConnectionsCmd(ch))
3839
cmd.AddCommand(ProcesslistCmd(ch))
3940
cmd.AddCommand(vtctld.VtctldCmd(ch))
4041
cmd.AddCommand(InfraCmd(ch))

internal/cmd/branch/connections.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package branch
2+
3+
import (
4+
"github.com/planetscale/cli/internal/cmd/branch/connections"
5+
"github.com/planetscale/cli/internal/cmdutil"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// ConnectionsCmd manages branch connections across supported database engines.
10+
func ConnectionsCmd(ch *cmdutil.Helper) *cobra.Command {
11+
cmd := &cobra.Command{
12+
Use: "connections <command>",
13+
Short: "Show and kill branch connections",
14+
Long: `Show and kill branch connections.
15+
16+
Agent workflow:
17+
1. Run: pscale branch connections show <database> <branch> --format json
18+
2. Inspect query_id, transaction_id, and connection_id from the selected row.
19+
3. Explain the proposed action and wait for user approval before running it.
20+
4. Run exactly one action command with the matching ID.
21+
5. Run show again to verify the result.
22+
23+
Action semantics:
24+
kill <database> <branch> <query-id> --query Cancels the listed query_id.
25+
kill-transaction <database> <branch> <transaction-id>
26+
Postgres only. destructive. Terminates the listed transaction_id if it still matches server state.
27+
kill <database> <branch> <connection-id> destructive. Terminates the listed connection_id.
28+
29+
Use --format json when an agent or script needs to inspect query_id,
30+
transaction_id, and connection_id fields. Human output uses vertical records so
31+
query text and action IDs are not truncated.`,
32+
}
33+
34+
cmd.AddCommand(ConnectionsShowCmd(ch))
35+
cmd.AddCommand(ConnectionsKillCmd(ch))
36+
cmd.AddCommand(ConnectionsKillTransactionCmd(ch))
37+
cmd.AddCommand(connections.TopCmd(ch))
38+
39+
return cmd
40+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package connections
2+
3+
import (
4+
"context"
5+
"errors"
6+
"strings"
7+
8+
"github.com/planetscale/cli/internal/cmdutil"
9+
live "github.com/planetscale/cli/internal/connections"
10+
"github.com/planetscale/cli/internal/printer"
11+
ps "github.com/planetscale/planetscale-go/planetscale"
12+
)
13+
14+
type actionResult struct {
15+
Success bool `csv:"success" header:"success" json:"success"`
16+
Keyspace string `csv:"keyspace" header:"keyspace" json:"keyspace,omitempty"`
17+
Shard string `csv:"shard" header:"shard" json:"shard,omitempty"`
18+
Tablet string `csv:"tablet" header:"tablet" json:"tablet,omitempty"`
19+
ID int64 `csv:"id" header:"id,text" json:"id,omitempty"`
20+
Kind string `csv:"kind" header:"kind" json:"kind,omitempty"`
21+
}
22+
23+
func (a *actionResult) MarshalCSVValue() interface{} {
24+
return []*actionResult{a}
25+
}
26+
27+
type compactActionResult struct {
28+
Success bool `csv:"success" header:"success" json:"success"`
29+
ID int64 `csv:"id" header:"id,text" json:"id,omitempty"`
30+
Kind string `csv:"kind" header:"kind" json:"kind,omitempty"`
31+
}
32+
33+
func (a *compactActionResult) MarshalCSVValue() interface{} {
34+
return []*compactActionResult{a}
35+
}
36+
37+
func toActionResult(result live.ActionResult) *actionResult {
38+
return &actionResult{
39+
Success: result.Success,
40+
Keyspace: result.Keyspace,
41+
Shard: result.Shard,
42+
Tablet: result.Tablet,
43+
ID: result.ID,
44+
Kind: result.Kind,
45+
}
46+
}
47+
48+
func toCompactActionResult(result live.ActionResult) *compactActionResult {
49+
return &compactActionResult{
50+
Success: result.Success,
51+
ID: result.ID,
52+
Kind: result.Kind,
53+
}
54+
}
55+
56+
// RunCancelQuery cancels the active query identified by a live connection query ID.
57+
func RunCancelQuery(ctx context.Context, ch *cmdutil.Helper, database, branch, queryID string, target ConnectionTarget) error {
58+
return RunCancelQueryForEngine(ctx, ch, database, branch, queryID, ps.DatabaseEngineMySQL, target)
59+
}
60+
61+
// RunCancelQueryForEngine cancels the active query and prints output for the resolved database engine.
62+
func RunCancelQueryForEngine(ctx context.Context, ch *cmdutil.Helper, database, branch, queryID string, engine ps.DatabaseEngine, target ConnectionTarget) error {
63+
return runAction(ctx, ch, database, branch, "query-id", queryID, target, func(ctx context.Context, client *live.Client, id string) (live.ActionResult, error) {
64+
return client.CancelQueryResult(ctx, live.ActionTarget{QueryID: &id})
65+
}, engine)
66+
}
67+
68+
// RunKillTransaction terminates the connection identified by a live connection transaction ID.
69+
func RunKillTransaction(ctx context.Context, ch *cmdutil.Helper, database, branch, transactionID string, target ConnectionTarget) error {
70+
return RunKillTransactionForEngine(ctx, ch, database, branch, transactionID, ps.DatabaseEnginePostgres, target)
71+
}
72+
73+
// RunKillTransactionForEngine terminates a transaction and prints output for the resolved database engine.
74+
func RunKillTransactionForEngine(ctx context.Context, ch *cmdutil.Helper, database, branch, transactionID string, engine ps.DatabaseEngine, target ConnectionTarget) error {
75+
return runAction(ctx, ch, database, branch, "transaction-id", transactionID, target, func(ctx context.Context, client *live.Client, id string) (live.ActionResult, error) {
76+
return client.TerminateTransactionResult(ctx, live.ActionTarget{TransactionID: &id})
77+
}, engine)
78+
}
79+
80+
// RunKillConnection terminates the connection identified by a live connection_id.
81+
func RunKillConnection(ctx context.Context, ch *cmdutil.Helper, database, branch, connectionID string, target ConnectionTarget) error {
82+
return RunKillConnectionForEngine(ctx, ch, database, branch, connectionID, ps.DatabaseEngineMySQL, target)
83+
}
84+
85+
// RunKillConnectionForEngine terminates a connection and prints output for the resolved database engine.
86+
func RunKillConnectionForEngine(ctx context.Context, ch *cmdutil.Helper, database, branch, connectionID string, engine ps.DatabaseEngine, target ConnectionTarget) error {
87+
return runAction(ctx, ch, database, branch, "connection-id", connectionID, target, func(ctx context.Context, client *live.Client, id string) (live.ActionResult, error) {
88+
return client.TerminateConnectionResult(ctx, live.ActionTarget{ConnectionID: &id})
89+
}, engine)
90+
}
91+
92+
func runAction(ctx context.Context, ch *cmdutil.Helper, database, branch, idName, id string, target ConnectionTarget, runAction func(context.Context, *live.Client, string) (live.ActionResult, error), engine ps.DatabaseEngine) error {
93+
if err := validateActionID(idName, id); err != nil {
94+
return err
95+
}
96+
id = strings.TrimSpace(id)
97+
98+
client, err := newConnectionsClient(ch, database, branch, target)
99+
if err != nil {
100+
return err
101+
}
102+
103+
result, err := runAction(ctx, client, id)
104+
if err != nil {
105+
return err
106+
}
107+
return printActionResult(ch, result, engine, idName)
108+
}
109+
110+
// ValidateConnectionID checks the connection action identifier without making network calls.
111+
func ValidateConnectionID(id string) error {
112+
return validateActionID("connection-id", id)
113+
}
114+
115+
// ValidateQueryID checks the query action identifier without making network calls.
116+
func ValidateQueryID(id string) error {
117+
return validateActionID("query-id", id)
118+
}
119+
120+
// ValidateTransactionID checks the transaction action identifier without making network calls.
121+
func ValidateTransactionID(id string) error {
122+
return validateActionID("transaction-id", id)
123+
}
124+
125+
func validateActionID(idName, id string) error {
126+
if strings.TrimSpace(id) == "" {
127+
return errors.New(idName + " is required")
128+
}
129+
return nil
130+
}
131+
132+
func printActionResult(ch *cmdutil.Helper, result live.ActionResult, engine ps.DatabaseEngine, idName string) error {
133+
if ch.Printer.Format() == printer.Human {
134+
ch.Printer.Printf("%s.\n", actionResultMessage(result, idName))
135+
return nil
136+
}
137+
if ch.Printer.Format() == printer.JSON {
138+
return ch.Printer.PrintResource(toActionResult(result))
139+
}
140+
if engine == ps.DatabaseEnginePostgres {
141+
return ch.Printer.PrintResource(toCompactActionResult(result))
142+
}
143+
return ch.Printer.PrintResource(toActionResult(result))
144+
}
145+
146+
func actionResultMessage(result live.ActionResult, idName string) string {
147+
var message string
148+
switch idName {
149+
case "query-id":
150+
message = "Cancelled query"
151+
case "transaction-id":
152+
message = "Killed transaction"
153+
case "connection-id":
154+
message = "Killed connection"
155+
default:
156+
message = "Action sent"
157+
}
158+
if result.Tablet != "" {
159+
message += " on " + result.Tablet
160+
}
161+
return message
162+
}

0 commit comments

Comments
 (0)