Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b6aa62d
Added support for the database engine plugin system for extending sql…
asmyasnikov Dec 28, 2025
5336821
Fix of endtoend tests
asmyasnikov Dec 28, 2025
2b88994
added install plugin-based-codegen's
asmyasnikov Dec 28, 2025
b1d156d
remove tmp file
asmyasnikov Dec 28, 2025
9f65d4f
removed go.{mod,sum}
asmyasnikov Dec 28, 2025
74b621f
SQLCDEBUG=processplugins=1
asmyasnikov Dec 28, 2025
cede5d3
Fix
asmyasnikov Dec 28, 2025
15b240d
Fix
asmyasnikov Dec 28, 2025
0b3b165
Apply suggestions from code review
asmyasnikov Dec 28, 2025
6c5b9a6
revert Combine
asmyasnikov Dec 28, 2025
7609ebc
.gitignore + README
asmyasnikov Jan 10, 2026
2c74313
simplified engine API
asmyasnikov Jan 27, 2026
88e6082
Apply suggestions from code review
asmyasnikov Jan 27, 2026
f39ae4a
Delete protos/engine/engine_grpc.pb.go
asmyasnikov Jan 27, 2026
18f5368
Delete protos/engine/engine.pb.go
asmyasnikov Jan 27, 2026
8eaef3c
Delete pkg/plugin/sdk.go
asmyasnikov Jan 27, 2026
fbaf6ba
Delete pkg/engine/engine.pb.go
asmyasnikov Jan 27, 2026
ce385ae
Delete pkg/plugin/codegen.pb.go
asmyasnikov Jan 27, 2026
a024d3e
Delete examples/plugin-based-codegen/README.md
asmyasnikov Jan 27, 2026
fbd5b43
Delete examples/plugin-based-codegen/gen/rust/queries.rs
asmyasnikov Jan 27, 2026
e6a730a
docs
asmyasnikov Jan 27, 2026
c8831c7
removed example
asmyasnikov Jan 27, 2026
6d5770f
fix
asmyasnikov Jan 27, 2026
d2417e8
Update .gitignore
asmyasnikov Jan 27, 2026
c50e9c7
pb.go
asmyasnikov Jan 27, 2026
e9cc264
fix comments
asmyasnikov Jan 27, 2026
ad7bf6c
simplified plugin engine code
asmyasnikov Jan 27, 2026
5d4c8dd
sourceFiles
asmyasnikov Jan 27, 2026
131d7bb
fix
asmyasnikov Jan 27, 2026
048a64d
Apply suggestions from code review
asmyasnikov Jan 27, 2026
79621b0
removed temp file
asmyasnikov Jan 27, 2026
d9df83b
Apply suggestions from code review
asmyasnikov Jan 27, 2026
55760fc
Apply suggestions from code review
asmyasnikov Jan 27, 2026
96dfabd
Apply suggestions from code review
asmyasnikov Jan 27, 2026
85475e2
removed engine interface
asmyasnikov Jan 27, 2026
0f81f5d
merge files
asmyasnikov Jan 27, 2026
7800a42
move md doc
asmyasnikov Jan 27, 2026
f6b34f0
Apply suggestions from code review
asmyasnikov Jan 27, 2026
830767e
revert changes
asmyasnikov Jan 27, 2026
e4667d2
revert
asmyasnikov Jan 27, 2026
9b9b3ed
docs
asmyasnikov Jan 27, 2026
a8fec25
fix
asmyasnikov Jan 27, 2026
778b45c
fixes and tests
asmyasnikov Jan 28, 2026
fb7e9a6
change ParseResponse - returns multiple statements from single call
asmyasnikov Jan 28, 2026
13fc9f3
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Jan 30, 2026
12ffdbb
fix
asmyasnikov Feb 1, 2026
2e280c8
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Feb 5, 2026
a6a4bdb
fix doc
asmyasnikov Feb 8, 2026
2f48010
fix doc
asmyasnikov Feb 8, 2026
bd1f56e
throw error on wrong external plugin options
asmyasnikov Feb 8, 2026
d4ccb4d
Catalog from engine plugin
asmyasnikov Feb 9, 2026
b9d8139
clickhouse + YDB
asmyasnikov Feb 13, 2026
ec0503c
README
asmyasnikov Feb 13, 2026
7f8f44e
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Feb 21, 2026
eedd52c
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Feb 25, 2026
7b46024
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Mar 23, 2026
6b8773a
use EngineService over process.Runner
asmyasnikov Apr 13, 2026
325b16d
Merge branch 'sqlc-dev:main' into engine-plugin
asmyasnikov Apr 23, 2026
737f03e
Merge branch 'main' into engine-plugin
asmyasnikov Apr 28, 2026
b7aec34
merge from main
asmyasnikov Apr 29, 2026
56ea963
refactor(cmd): keep OutputPair and ResultProcessor names from main
asmyasnikov Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
use EngineService over process.Runner
  • Loading branch information
asmyasnikov committed Apr 13, 2026
commit 6b8773a01e87e0eb106936621fb5685c0e4c9fb5
17 changes: 10 additions & 7 deletions docs/guides/engine-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ sql:
| Field | Description |
|-------|-------------|
| `name` | Engine name used in `sql[].engine` |
| `process.cmd` | Command to run: executable path and optional arguments (e.g. `sqlc-engine-external-db --dont-open-wildcard-star`). First token is the executable; remaining tokens are passed as arguments before the RPC method. |
| `process.cmd` | Command to run: executable path and optional arguments (e.g. `sqlc-engine-external-db --dont-open-wildcard-star`). First token is the executable; remaining tokens are passed as arguments **before** the full RPC method name `/engine.EngineService/Parse`. |
| `env` | Environment variable names passed to the plugin |

Each engine must define either `process` (with `cmd`) or `wasm` (with `url` and `sha256`). See [Configuration reference](../reference/config.md) for the full `engines` schema.
Expand All @@ -72,7 +72,7 @@ Each engine must define either `process` (with `cmd`) or `wasm` (with `url` and

For an engine with `process.cmd`, sqlc resolves and runs the plugin as follows:

1. **Command parsing** — `process.cmd` is split on whitespace. The first token is the executable; any further tokens are passed as arguments, and sqlc appends the RPC method name (`parse`) when invoking the plugin.
1. **Command parsing** — `process.cmd` is split on whitespace. The first token is the executable; any further tokens are passed as arguments, and sqlc appends the **gRPC full method name** for Parse — the same pattern as for [codegen plugins](plugins.md): `/engine.EngineService/Parse` (the generated client passes this string as the last argv token to the child process).

2. **Executable lookup** — The first token is resolved the same way as in the shell:
- If it contains a path separator (e.g. `/usr/bin/sqlc-engine-external-db` or `./bin/sqlc-engine-external-db`), it is treated as a path. Absolute paths are used as-is; relative paths are taken relative to the **current working directory of the process running sqlc**.
Expand Down Expand Up @@ -178,19 +178,22 @@ go build -o sqlc-engine-external-db .

## Protocol

Process plugins use Protocol Buffers on stdin/stdout:
Process plugins use Protocol Buffers on stdin/stdout — **no TCP gRPC**, but the same **unary RPC shape** as codegen plugins: sqlc uses the generated `EngineServiceClient` with `process.Runner` implementing `grpc.ClientConnInterface`, so the child receives the **full gRPC method path** as the last command-line argument (like `/plugin.CodegenService/Generate` for codegen).

```
sqlc → stdin (protobuf) → plugin → stdout (protobuf) → sqlc
```

Invocation:
Invocation (method name is one argv token, often quoted in shell examples):

```bash
sqlc-engine-external-db parse # stdin: ParseRequest, stdout: ParseResponse
sqlc-engine-external-db --flag /engine.EngineService/Parse
# stdin: ParseRequest, stdout: ParseResponse
```

The definition lives in `protos/engine/engine.proto` (generated Go in `pkg/engine`). After editing the proto, run `make proto-engine-plugin` to regenerate the Go code.
The Go SDK `engine.Run` dispatches **Parse** when `os.Args[1]` is `/engine.EngineService/Parse` or the legacy shorthand `parse` (for older tooling).

The definition lives in `protos/engine/engine.proto` (messages **and** `service EngineService`; generated Go in `pkg/engine`, including `engine_grpc.pb.go` for the client stub). After editing the proto, run `make proto-engine-plugin` to regenerate the Go code.

## Example

Expand All @@ -210,7 +213,7 @@ For each `sql[]` block, `sqlc generate` branches on the configured engine: built
└─────────────────────────────────────────────────────────────────┘

sqlc sqlc-engine-external-db
│──── spawn, args: ["parse"] ──────────────────────────────► │
│──── spawn, argv ends with /engine.EngineService/Parse ─────► │
│──── stdin: ParseRequest{sql=full query.sql, schema_sql|…} ► │
│◄─── stdout: ParseResponse{statements: [stmt1, stmt2, …]} ── │
```
Expand Down
80 changes: 12 additions & 68 deletions internal/cmd/plugin_engine.go
Original file line number Diff line number Diff line change
@@ -1,92 +1,30 @@
// This file runs a database-engine plugin as an external process (parse RPC over stdin/stdout).
// This file runs a database-engine plugin as an external process (EngineService/Parse over stdin/stdout).
// Like codegen plugins, sqlc uses the generated EngineServiceClient with process.Runner implementing
// grpc.ClientConnInterface (full method name /engine.EngineService/Parse as the subprocess argv tail).
// It is used only by the plugin-engine generate path (runPluginQuerySet). Vet does not support plugin engines.

package cmd

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/sqlc-dev/sqlc/internal/compiler"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/ext/process"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/plugin"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
"google.golang.org/protobuf/proto"

"github.com/sqlc-dev/sqlc/internal/info"
pb "github.com/sqlc-dev/sqlc/pkg/engine"
)

// engineProcessRunner runs an engine plugin as an external process.
type engineProcessRunner struct {
Cmd string
Dir string // Working directory for the plugin (config file directory)
Env []string
}

func newEngineProcessRunner(cmd, dir string, env []string) *engineProcessRunner {
return &engineProcessRunner{Cmd: cmd, Dir: dir, Env: env}
}

func (r *engineProcessRunner) invoke(ctx context.Context, method string, req, resp proto.Message) error {
stdin, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("failed to encode request: %w", err)
}

cmdParts := strings.Fields(r.Cmd)
if len(cmdParts) == 0 {
return fmt.Errorf("engine plugin not found: %s\n\nSee the engine plugins documentation: https://docs.sqlc.dev/en/latest/guides/engine-plugins.html", r.Cmd)
}

path, err := exec.LookPath(cmdParts[0])
if err != nil {
return fmt.Errorf("engine plugin not found: %s\n\nSee the engine plugins documentation: https://docs.sqlc.dev/en/latest/guides/engine-plugins.html", r.Cmd)
}

args := append(cmdParts[1:], method)
cmd := exec.CommandContext(ctx, path, args...)
cmd.Stdin = bytes.NewReader(stdin)
if r.Dir != "" {
cmd.Dir = r.Dir
}
cmd.Env = append(os.Environ(), fmt.Sprintf("SQLC_VERSION=%s", info.Version))

out, err := cmd.Output()
if err != nil {
stderr := err.Error()
var exit *exec.ExitError
if errors.As(err, &exit) {
stderr = string(exit.Stderr)
}
return fmt.Errorf("engine plugin error: %s", stderr)
}

if err := proto.Unmarshal(out, resp); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}

// parseRequest invokes the plugin's Parse RPC. Used by runPluginQuerySet.
func (r *engineProcessRunner) parseRequest(ctx context.Context, req *pb.ParseRequest) (*pb.ParseResponse, error) {
resp := &pb.ParseResponse{}
if err := r.invoke(ctx, "parse", req, resp); err != nil {
return nil, err
}
return resp, nil
}

// runPluginQuerySet runs the plugin-engine path: schema and queries are sent to the
// engine plugin via ParseRequest; the responses are turned into compiler.Result and
// passed to ProcessResult. No AST or compiler parsing is used.
Expand Down Expand Up @@ -139,7 +77,13 @@ func runPluginQuerySet(ctx context.Context, rp resultProcessor, name, dir string
return o.PluginParseFunc(schemaStr, querySQL)
}
} else {
r := newEngineProcessRunner(enginePlugin.Process.Cmd, combo.Dir, enginePlugin.Env)
runner := &process.Runner{
Cmd: enginePlugin.Process.Cmd,
Env: enginePlugin.Env,
Dir: combo.Dir,
InheritParentEnv: true,
}
engineClient := pb.NewEngineServiceClient(runner)
parseFn = func(querySQL string) (*pb.ParseResponse, error) {
req := &pb.ParseRequest{Sql: querySQL}
if databaseOnly {
Expand All @@ -149,7 +93,7 @@ func runPluginQuerySet(ctx context.Context, rp resultProcessor, name, dir string
} else {
req.SchemaSource = &pb.ParseRequest_SchemaSql{SchemaSql: schemaSQL}
}
return r.parseRequest(ctx, req)
return engineClient.Parse(ctx, req)
}
}

Expand Down
5 changes: 2 additions & 3 deletions internal/cmd/plugin_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,11 @@ func TestPluginPipeline_FullPipeline(t *testing.T) {
}

// TestPluginPipeline_WithoutOverride_UsesPluginPackage proves that when PluginParseFunc
// is not set, the pipeline calls the engine process runner (newEngineProcessRunner + parseRequest).
// is not set, the pipeline calls process.Runner + NewEngineServiceClient(...).Parse (real subprocess).
// It runs generate with a plugin engine and nil PluginParseFunc; we expect failure
// (e.g. from running "echo" as the engine binary), but the error must NOT be
// "unknown engine" — so we know we went past config lookup and into the plugin path.
// If you add panic("azaza") at the start of newEngineProcessRunner or parseRequest,
// this test will panic, confirming that the plugin package is actually invoked.
// If you add panic at the start of that path, this test will panic, confirming the runner is invoked.
func TestPluginPipeline_WithoutOverride_UsesPluginPackage(t *testing.T) {
ctx := context.Background()
conf, err := config.ParseConfig(strings.NewReader(testPluginPipelineConfig))
Expand Down
26 changes: 21 additions & 5 deletions internal/ext/process/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand All @@ -22,6 +23,12 @@ type Runner struct {
Cmd string
Format string
Env []string
// Dir, if set, is the working directory for the child process (e.g. directory of sqlc.yaml).
Dir string
// InheritParentEnv, if true, starts the child with os.Environ() and appends SQLC_VERSION and
// variables listed in Env (same names as codegen). Use for database engine plugins that need
// a normal shell-like environment (e.g. PATH). Default false matches historical codegen behavior.
InheritParentEnv bool
}

func (r *Runner) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
Expand Down Expand Up @@ -53,16 +60,25 @@ func (r *Runner) Invoke(ctx context.Context, method string, args any, reply any,
return fmt.Errorf("unknown plugin format: %s", r.Format)
}

// Check if the output plugin exists
path, err := exec.LookPath(r.Cmd)
cmdFields := strings.Fields(strings.TrimSpace(r.Cmd))
if len(cmdFields) == 0 {
return fmt.Errorf("process: empty command")
}
exePath, err := exec.LookPath(cmdFields[0])
if err != nil {
return fmt.Errorf("process: %s not found", r.Cmd)
}

cmd := exec.CommandContext(ctx, path, method)
argv := append(append([]string(nil), cmdFields[1:]...), method)
cmd := exec.CommandContext(ctx, exePath, argv...)
cmd.Stdin = bytes.NewReader(stdin)
cmd.Env = []string{
fmt.Sprintf("SQLC_VERSION=%s", info.Version),
if r.Dir != "" {
cmd.Dir = r.Dir
}
if r.InheritParentEnv {
cmd.Env = append(append([]string(nil), os.Environ()...), fmt.Sprintf("SQLC_VERSION=%s", info.Version))
} else {
cmd.Env = []string{fmt.Sprintf("SQLC_VERSION=%s", info.Version)}
}
for _, key := range r.Env {
if key == "SQLC_AUTH_TOKEN" {
Expand Down
12 changes: 8 additions & 4 deletions pkg/engine/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func Run(h Handler) {

func run(h Handler, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
if len(args) < 2 {
return fmt.Errorf("usage: %s <method>", args[0])
return fmt.Errorf("usage: %s <%s|parse>", args[0], EngineService_Parse_FullMethodName)
}

method := args[1]
Expand All @@ -81,8 +81,12 @@ func run(h Handler, args []string, stdin io.Reader, stdout, stderr io.Writer) er

var output proto.Message

switch method {
case "parse":
switch {
// Full gRPC method name (same argv tail as codegen plugins use for CodegenService/Generate).
case method == EngineService_Parse_FullMethodName:
fallthrough
// Legacy shorthand for manual invocation and older sqlc versions.
case method == "parse":
var req ParseRequest
if err := proto.Unmarshal(input, &req); err != nil {
return fmt.Errorf("parsing request: %w", err)
Expand All @@ -93,7 +97,7 @@ func run(h Handler, args []string, stdin io.Reader, stdout, stderr io.Writer) er
output, err = h.Parse(&req)

default:
return fmt.Errorf("unknown method: %s", method)
return fmt.Errorf("unknown method: %q (expected %q or legacy \"parse\")", method, EngineService_Parse_FullMethodName)
}

if err != nil {
Expand Down
57 changes: 57 additions & 0 deletions pkg/engine/sdk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package engine

import (
"bytes"
"io"
"testing"

"google.golang.org/protobuf/proto"
)

func TestRun_acceptsFullGRPCMethodName(t *testing.T) {
called := false
h := Handler{
Parse: func(req *ParseRequest) (*ParseResponse, error) {
called = true
return &ParseResponse{}, nil
},
}
in, err := proto.Marshal(&ParseRequest{Sql: "SELECT 1"})
if err != nil {
t.Fatal(err)
}
var stdout bytes.Buffer
err = run(h, []string{"plugin", EngineService_Parse_FullMethodName}, bytes.NewReader(in), &stdout, io.Discard)
if err != nil {
t.Fatal(err)
}
if !called {
t.Fatal("Parse not invoked for full gRPC method argv")
}
var resp ParseResponse
if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil {
t.Fatalf("stdout protobuf: %v", err)
}
}

func TestRun_acceptsLegacyParseArgv(t *testing.T) {
called := false
h := Handler{
Parse: func(req *ParseRequest) (*ParseResponse, error) {
called = true
return &ParseResponse{}, nil
},
}
in, err := proto.Marshal(&ParseRequest{})
if err != nil {
t.Fatal(err)
}
var stdout bytes.Buffer
err = run(h, []string{"plugin", "parse"}, bytes.NewReader(in), &stdout, io.Discard)
if err != nil {
t.Fatal(err)
}
if !called {
t.Fatal("Parse not invoked for legacy parse argv")
}
}
Loading