Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/felixdotgo/querybox/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This guide covers best practices for developing secure, performant, and maintainable QueryBox plugins.

Security

Never Log Credentials

Avoid logging connection parameters or credentials:
// ❌ Dangerous - exposes credentials in logs
fmt.Fprintf(os.Stderr, "Connecting with: %+v\n", req.Connection)

// ✅ Safe - log without sensitive data
fmt.Fprintf(os.Stderr, "Connecting to %s\n", host)

Sanitize Query Parameters

When constructing queries from user input, use parameterized queries:
// ❌ SQL injection risk
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)

// ✅ Use parameterized queries
stmt, err := db.Prepare("SELECT * FROM users WHERE name = ?")
rows, err := stmt.Query(userInput)

Validate Connection Parameters

Reject obviously invalid inputs early:
func buildDSN(connection map[string]string) (string, error) {
    host := connection["host"]
    if host == "" {
        return "", fmt.Errorf("host is required")
    }
    // Reject localhost bypass attempts in production
    if strings.Contains(host, "@") {
        return "", fmt.Errorf("invalid host format")
    }
    // ...
}

Use TLS by Default

Enable encrypted connections when possible:
// MySQL example from plugins/mysql/main.go:69-75
func init() {
    // Register TLS config with embedded root certificates
    if pool, err := certs.RootCertPool(); err == nil {
        mysql.RegisterTLSConfig("querybox", &tls.Config{RootCAs: pool})
    }
}

// Convert generic TLS flags to registered config
if t := params.Get("tls"); t == "true" || t == "preferred" {
    params.Set("tls", "querybox")
}

Avoid Hardcoded Secrets

Never embed API keys or credentials in plugin code:
// ❌ Hardcoded secret
const apiKey = "sk-1234567890abcdef"

// ✅ Accept from connection parameters
apiKey := connection["api_key"]
if apiKey == "" {
    return fmt.Errorf("api_key is required")
}

Limit Resource Access

Restrict file system access when using FILE_PATH fields:
// Validate file path is within allowed directories
dbPath := connection["db_path"]
absPath, err := filepath.Abs(dbPath)
if err != nil {
    return fmt.Errorf("invalid path: %w", err)
}

// Prevent directory traversal
if strings.Contains(absPath, "..") {
    return fmt.Errorf("path traversal not allowed")
}

Error Handling

Return Errors in Response, Not as Go Errors

From pkg/plugin/plugin.go:137-220, the CLI protocol expects all results on stdout:
// ✅ Correct - error in response
func (p *myPlugin) Exec(ctx context.Context, req *plugin.ExecRequest) (*plugin.ExecResponse, error) {
    if err := validateQuery(req.Query); err != nil {
        return &plugin.ExecResponse{
            Error: fmt.Sprintf("invalid query: %v", err),
        }, nil  // Always return nil as Go error
    }
    // ...
}

// ❌ Incorrect - breaks protocol
func (p *myPlugin) Exec(ctx context.Context, req *plugin.ExecRequest) (*plugin.ExecResponse, error) {
    if err := validateQuery(req.Query); err != nil {
        return nil, err  // Host cannot parse this
    }
}

Provide Helpful Error Messages

Include actionable information:
// ❌ Vague
return &plugin.ExecResponse{Error: "connection failed"}, nil

// ✅ Actionable
return &plugin.ExecResponse{
    Error: fmt.Sprintf("connection failed: %v. Check host and port are correct", err),
}, nil

Close Resources Properly

Always use defer to close connections:
func (p *myPlugin) Exec(ctx context.Context, req *plugin.ExecRequest) (*plugin.ExecResponse, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return &plugin.ExecResponse{Error: fmt.Sprintf("open error: %v", err)}, nil
    }
    defer db.Close()  // ✅ Always close

    rows, err := db.Query(req.Query)
    if err != nil {
        return &plugin.ExecResponse{Error: fmt.Sprintf("query error: %v", err)}, nil
    }
    defer rows.Close()  // ✅ Close result set
    // ...
}

Performance

Respect Context Deadlines

From docs/features/02-plugin-system.md:14-21, commands have timeouts:
CommandTimeout
info2s
exec30s
authforms30s
connection-tree30s
test-connection15s
Respect the provided context:
func (p *myPlugin) Exec(ctx context.Context, req *plugin.ExecRequest) (*plugin.ExecResponse, error) {
    // ✅ Pass context to database operations
    rows, err := db.QueryContext(ctx, req.Query)
    if err != nil {
        return &plugin.ExecResponse{Error: fmt.Sprintf("query error: %v", err)}, nil
    }
    // ...
}

Limit Result Set Size

Prevent memory exhaustion from large queries:
// Add LIMIT clause if missing
query := req.Query
if !strings.Contains(strings.ToUpper(query), "LIMIT") {
    query = query + " LIMIT 10000"
}
Or stream results instead of buffering:
var rowResults []*plugin.Row
for rows.Next() {
    if len(rowResults) >= 10000 {
        break  // Stop after reasonable limit
    }
    // ... scan row ...
    rowResults = append(rowResults, row)
}

Reuse Connection Pools

For plugins that maintain state (not recommended for the current stateless model), reuse connections:
// Not applicable to current stateless plugin model,
// but useful if QueryBox adds persistent plugin processes in the future
var dbPool *sql.DB

func getDB(dsn string) (*sql.DB, error) {
    if dbPool == nil {
        var err error
        dbPool, err = sql.Open("mysql", dsn)
        if err != nil {
            return nil, err
        }
        dbPool.SetMaxOpenConns(10)
    }
    return dbPool, nil
}
Note: The current plugin model spawns one process per request, so connection pooling has no effect. This may change in future versions.

Avoid Expensive Operations in Info

The info command has a 2-second timeout and is called frequently:
// ❌ Slow - queries external service
func (p *myPlugin) Info(ctx context.Context, _ *pluginpb.PluginV1_InfoRequest) (*plugin.InfoResponse, error) {
    latestVersion := fetchLatestVersionFromGitHub()  // Network call
    return &plugin.InfoResponse{
        Version: latestVersion,
        // ...
    }, nil
}

// ✅ Fast - returns static metadata
func (p *myPlugin) Info(ctx context.Context, _ *pluginpb.PluginV1_InfoRequest) (*plugin.InfoResponse, error) {
    return &plugin.InfoResponse{
        Type:        plugin.TypeDriver,
        Name:        "myplugin",
        Version:     "1.0.0",  // Hardcoded at build time
        Description: "My database driver",
    }, nil
}

Code Quality

Use the Unimplemented Server Stub

Embed the generated stub for forward compatibility:
type myPlugin struct {
    pluginpb.UnimplementedPluginServiceServer  // ✅ Future-proof
}
This ensures new RPC methods added to the protocol have no-op defaults.

Implement All Core Methods

From docs/features/02-plugin-system.md:14-21, required and optional methods: Required:
  • Info - Plugin metadata
  • Exec - Query execution
  • AuthForms - Connection form definitions
Optional:
  • ConnectionTree - Database browsing
  • TestConnection - Connection validation
Always implement TestConnection to improve UX:
func (p *myPlugin) TestConnection(ctx context.Context, req *plugin.TestConnectionRequest) (*plugin.TestConnectionResponse, error) {
    // Attempt to connect and ping
    db, err := sql.Open("driver", buildDSN(req.Connection))
    if err != nil {
        return &plugin.TestConnectionResponse{Ok: false, Message: err.Error()}, nil
    }
    defer db.Close()

    if err := db.PingContext(ctx); err != nil {
        return &plugin.TestConnectionResponse{Ok: false, Message: err.Error()}, nil
    }

    return &plugin.TestConnectionResponse{Ok: true, Message: "Connection successful"}, nil
}

Handle Empty Results Gracefully

Always return empty collections, never nil:
// ✅ Return empty array
if docs == nil {
    docs = []*structpb.Struct{}
}

// ✅ Initialize with empty slice
rows := []*plugin.Row{}

// ❌ Return nil (causes UI issues)
return &plugin.DocumentResult{Documents: nil}

Support Explain Query Capability

For SQL plugins, implement the explain-query capability:
func (p *myPlugin) Info(ctx context.Context, _ *pluginpb.PluginV1_InfoRequest) (*plugin.InfoResponse, error) {
    return &plugin.InfoResponse{
        // ...
        Capabilities: []string{"query", "explain-query"},  // ✅ Advertise capability
    }, nil
}

func (p *myPlugin) Exec(ctx context.Context, req *plugin.ExecRequest) (*plugin.ExecResponse, error) {
    query := req.Query

    // ✅ Handle explain-query option
    if req.Options != nil {
        if v, ok := req.Options["explain-query"]; ok && v == "yes" {
            query = "EXPLAIN " + query
        }
    }

    // ... execute query ...
}

Testing

Write Unit Tests

Test core functionality without external dependencies:
func TestFormatSQLValue(t *testing.T) {
    tests := []struct {
        input    interface{}
        expected string
    }{
        {nil, ""},
        {"hello", "hello"},
        {[]byte("world"), "world"},
        {[]byte{0xDE, 0xAD, 0xBE, 0xEF}, "0xdeadbeef"},
        {42, "42"},
    }

    for _, tt := range tests {
        result := plugin.FormatSQLValue(tt.input)
        if result != tt.expected {
            t.Errorf("FormatSQLValue(%v) = %q, want %q", tt.input, result, tt.expected)
        }
    }
}

Test with Real Databases

Use Docker for integration tests:
func TestMySQLExec(t *testing.T) {
    // Skip if MySQL not available
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    // Assumes docker-compose with MySQL running
    p := &mysqlPlugin{}
    resp, err := p.Exec(context.Background(), &plugin.ExecRequest{
        Connection: map[string]string{"dsn": "root@tcp(localhost:3306)/test"},
        Query:      "SELECT 1 AS num",
    })

    if err != nil {
        t.Fatalf("Exec failed: %v", err)
    }
    if resp.Error != "" {
        t.Fatalf("Exec returned error: %s", resp.Error)
    }
    // ... validate result ...
}
Run integration tests:
go test ./... -short          # Skip integration tests
go test ./...                 # Run all tests

Test CLI Protocol

Test the stdin/stdout protocol:
# Test info command
./bin/plugins/myplugin info | jq .

# Test exec command
echo '{"connection":{"dsn":"test"}, "query":"SELECT 1"}' | \
  ./bin/plugins/myplugin exec | jq .

# Test authforms command
./bin/plugins/myplugin authforms | jq '.forms'

Documentation

Document Connection Parameters

Include examples in auth form placeholders:
{
    Type:        plugin.AuthFieldText,
    Name:        "dsn",
    Label:       "DSN",
    Placeholder: "user:pass@tcp(host:port)/dbname",  // ✅ Shows format
}

Provide Usage Examples

Include examples in error messages:
return &plugin.ExecResponse{
    Error: "unsupported query format\n" +
        "Examples:\n" +
        "  db.users.find({})\n" +
        "  db.users.insertOne({\"name\": \"Alice\"})\n" +
        "  {\"ping\": 1}",
}, nil

Add Plugin Metadata

Provide complete metadata for plugin discovery:
func (p *myPlugin) Info(ctx context.Context, _ *pluginpb.PluginV1_InfoRequest) (*plugin.InfoResponse, error) {
    return &plugin.InfoResponse{
        Type:         plugin.TypeDriver,
        Name:         "MyDB",
        Version:      "1.0.0",
        Description:  "MyDB database driver for QueryBox",
        Url:          "https://mydb.com",
        Author:       "MyDB Inc.",
        License:      "MIT",
        IconUrl:      "https://mydb.com/icon.png",
        Capabilities: []string{"query", "explain-query"},
        Tags:         []string{"nosql", "document"},
        Contact:      "support@mydb.com",
    }, nil
}

Compatibility

Support Legacy Connection Formats

Accept both old DSN keys and new credential_blob format:
func buildDSN(connection map[string]string) (string, error) {
    // ✅ Support legacy DSN key
    if dsn, ok := connection["dsn"]; ok && dsn != "" {
        return dsn, nil
    }

    // ✅ Support new credential_blob format
    if blob, ok := connection["credential_blob"]; ok && blob != "" {
        var payload struct {
            Form   string            `json:"form"`
            Values map[string]string `json:"values"`
        }
        if err := json.Unmarshal([]byte(blob), &payload); err == nil {
            return buildDSNFromValues(payload.Values)
        }
    }

    return "", fmt.Errorf("missing connection parameters")
}

Version Your Plugin

Use semantic versioning and maintain a changelog:
const Version = "1.2.3"

func (p *myPlugin) Info(ctx context.Context, _ *pluginpb.PluginV1_InfoRequest) (*plugin.InfoResponse, error) {
    return &plugin.InfoResponse{
        Version: Version,
        // ...
    }, nil
}

Language-Agnostic Plugins

While the SDK is Go-based, any language can implement the protocol:

Python Example

#!/usr/bin/env python3
import sys
import json

def handle_info():
    return {
        "type": "DRIVER",
        "name": "myplugin",
        "version": "1.0.0",
        "description": "Python-based plugin"
    }

def handle_exec(req):
    query = req.get("query", "")
    return {
        "result": {
            "kv": {
                "data": {"result": f"Executed: {query}"}
            }
        }
    }

if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""

    if cmd == "info":
        print(json.dumps(handle_info()))
    elif cmd == "exec":
        req = json.load(sys.stdin)
        print(json.dumps(handle_exec(req)))
    else:
        print("Usage: myplugin info | exec", file=sys.stderr)
        sys.exit(2)
Make it executable:
chmod +x myplugin
cp myplugin ~/.config/querybox/plugins/

Common Pitfalls

Don’t Use Interactive Input

Plugins communicate via stdin/stdout, not TTY:
// ❌ Will hang - stdin is reserved for requests
fmt.Println("Enter password:")
password, _ := bufio.NewReader(os.Stdin).ReadString('\n')

// ✅ Accept password from connection parameters
password := connection["password"]

Don’t Write to Stdout Except for Responses

// ❌ Breaks protocol - stdout is for JSON responses only
fmt.Println("Connecting to database...")

// ✅ Log to stderr instead
fmt.Fprintln(os.Stderr, "Connecting to database...")

Don’t Assume Plugin Working Directory

// ❌ Relative path may not work
data, err := os.ReadFile("config.json")

// ✅ Use absolute paths or embed resources
//go:embed config.json
var configData []byte

Don’t Ignore Connection Tree Type Hints

Use appropriate node types for better UI integration:
// ✅ Use semantic node types
&plugin.ConnectionTreeNode{
    Key:      "mydb.users",
    Label:    "users",
    NodeType: plugin.ConnectionTreeNodeTypeTable,  // Not generic/empty
}

Build docs developers (and LLMs) love