Skip to content

Commit fb7e9a6

Browse files
committed
change ParseResponse - returns multiple statements from single call
1 parent 778b45c commit fb7e9a6

File tree

7 files changed

+612
-132
lines changed

7 files changed

+612
-132
lines changed

docs/guides/engine-plugins.md

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Data returned by the engine plugin (SQL text, parameters, columns) is passed thr
1616

1717
An engine plugin is an external process that implements one RPC:
1818

19-
- **Parse** — accepts the query text and either schema SQL or connection parameters, and returns processed SQL, parameter list, and result columns.
19+
- **Parse** — accepts the **entire contents** of one query file (e.g. `query.sql`) and either schema SQL or connection parameters; returns **one Statement per query block** in that file (each with sql, parameters, columns, and name/cmd).
2020

2121
Process plugins (e.g. written in Go) talk to sqlc over **stdin/stdout** using **Protocol Buffers**. The protocol is defined in `protos/engine/engine.proto`.
2222

@@ -102,40 +102,54 @@ The engine API exposes only **Parse**. There are no separate methods for catalog
102102

103103
### 2. Parse
104104

105+
sqlc calls Parse **once per query file** (e.g. once for `query.sql`). The plugin receives the full file contents and returns one **Statement** per query block in that file. sqlc then passes each statement to the codegen plugin as a separate query.
106+
105107
**Request**
106108

107-
- `sql` — The query text to parse.
109+
- `sql` — The **entire contents** of one query file (all query blocks, with `-- name: X :one`-style comments).
108110
- `schema_source` — One of:
109-
- `schema_sql`: schema as in a schema.sql file (used for schema-based parsing).
111+
- `schema_sql`: full schema as in schema.sql (for schema-based parsing).
110112
- `connection_params`: DSN and options for database-only mode.
111113

112114
**Response**
113115

114-
- `sql` — Processed query text. Often the same as input; with a schema you may expand `*` into explicit columns.
115-
- `parameters` — List of parameters (position/name, type, nullable, array, etc.).
116-
- `columns` — List of result columns (name, type, nullable, table/schema if known).
116+
Return `statements`: one `Statement` per query block. Each `Statement` has:
117+
118+
- `name` — Query name (from `-- name: GetUser` etc.).
119+
- `cmd` — Command/type: use the `Cmd` enum (`engine.Cmd_CMD_ONE`, `engine.Cmd_CMD_MANY`, `engine.Cmd_CMD_EXEC`, etc.). See `protos/engine/engine.proto` for the full list.
120+
- `sql` — Processed SQL for that block (as-is or with `*` expanded using schema).
121+
- `parameters` — Parameters for this statement.
122+
- `columns` — Result columns (names, types, nullability, etc.) for this statement.
123+
124+
The engine package provides helpers (optional) to split `query.sql` and parse `"-- name: X :cmd"` lines in the same way as the built-in engines:
117125

118-
Example handler:
126+
- `engine.CommentSyntax` — Which comment styles to accept (`Dash`, `SlashStar`, `Hash`).
127+
- `engine.ParseNameAndCmd(line, syntax)` — Parses a single line like `"-- name: ListAuthors :many"` → `(name, cmd, ok)`. `cmd` is `engine.Cmd`.
128+
- `engine.QueryBlocks(content, syntax)` — Splits file content into `[]engine.QueryBlock` (each has `Name`, `Cmd`, `SQL`).
129+
- `engine.StatementMeta(name, cmd, sql)` — Builds a `*engine.Statement` with name/cmd/sql set; you add parameters and columns.
130+
131+
Example handler using helpers:
119132

120133
```go
121134
func handleParse(req *engine.ParseRequest) (*engine.ParseResponse, error) {
122-
sql := req.GetSql()
135+
queryFileContent := req.GetSql()
136+
syntax := engine.CommentSyntax{Dash: true, SlashStar: true, Hash: true}
123137
124138
var schema *SchemaInfo
125139
if s := req.GetSchemaSql(); s != "" {
126140
schema = parseSchema(s)
127141
}
128142
// Or use req.GetConnectionParams() for database-only mode.
129143
130-
parameters := extractParameters(sql)
131-
columns := extractColumns(sql, schema)
132-
processedSQL := processSQL(sql, schema) // e.g. expand SELECT *
133-
134-
return &engine.ParseResponse{
135-
Sql: processedSQL,
136-
Parameters: parameters,
137-
Columns: columns,
138-
}, nil
144+
blocks, _ := engine.QueryBlocks(queryFileContent, syntax)
145+
var statements []*engine.Statement
146+
for _, b := range blocks {
147+
st := engine.StatementMeta(b.Name, b.Cmd, processSQL(b.SQL, schema))
148+
st.Parameters = extractParameters(b.SQL)
149+
st.Columns = extractColumns(b.SQL, schema)
150+
statements = append(statements, st)
151+
}
152+
return &engine.ParseResponse{Statements: statements}, nil
139153
}
140154
```
141155

@@ -164,28 +178,29 @@ Invocation:
164178
sqlc-engine-mydb parse # stdin: ParseRequest, stdout: ParseResponse
165179
```
166180

167-
The definition lives in `engine/engine.proto` (and generated Go in `pkg/engine`).
181+
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.
168182

169183
## Example
170184

171185
The protocol and Go SDK are in this repository: `protos/engine/engine.proto` and `pkg/engine/` (including `sdk.go` with `engine.Run` and `engine.Handler`). Use them to build a binary that implements the Parse RPC; register it under `engines` in sqlc.yaml as shown above.
172186

173187
## Architecture
174188

175-
For each `sql[]` block, `sqlc generate` branches on the configured engine: built-in (postgresql, mysql, sqlite) use the compiler and catalog; any engine listed under `engines:` in sqlc.yaml uses the plugin path (no compiler, schema + queries go to the plugin's Parse RPC, then output goes to codegen).
189+
For each `sql[]` block, `sqlc generate` branches on the configured engine: built-in (postgresql, mysql, sqlite) use the compiler and catalog; any engine listed under `engines:` in sqlc.yaml uses the plugin path (no compiler). For the plugin path, sqlc calls Parse **once per query file**, sending the full file contents and schema (or connection params). The plugin returns **N statements** (one per query block); sqlc passes each statement to codegen as a separate query.
176190

177191
```
178192
┌─────────────────────────────────────────────────────────────────┐
179-
│ sqlc generate │
180-
│ 1. Read sqlc.yaml, find engine for this sql block │
181-
│ 2. If plugin engine: call plugin parse (sql + schema_sql etc.) │
182-
│ 3. Use returned sql, parameters, columns in codegen │
193+
│ sqlc generate (plugin engine) │
194+
│ 1. Per query file: one Parse(schema_sql|connection_params, │
195+
│ full query file content) │
196+
│ 2. ParseResponse.statements = one Statement per query block │
197+
│ 3. Each statement → one codegen query (N helpers) │
183198
└─────────────────────────────────────────────────────────────────┘
184199
185200
sqlc sqlc-engine-mydb
186201
│──── spawn, args: ["parse"] ──────────────────────────────► │
187-
│──── stdin: ParseRequest{sql, schema_sql|connection_params} ► │
188-
│◄─── stdout: ParseResponse{sql, parameters, columns} ─────── │
202+
│──── stdin: ParseRequest{sql=full query.sql, schema_sql|…} ► │
203+
│◄─── stdout: ParseResponse{statements: [stmt1, stmt2, …]} ── │
189204
```
190205

191206
## See also

internal/cmd/plugin_engine.go

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/sqlc-dev/sqlc/internal/config"
1818
"github.com/sqlc-dev/sqlc/internal/metadata"
1919
"github.com/sqlc-dev/sqlc/internal/multierr"
20-
"github.com/sqlc-dev/sqlc/internal/source"
2120
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
2221
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
2322
"google.golang.org/protobuf/proto"
@@ -86,9 +85,6 @@ func (r *engineProcessRunner) parseRequest(ctx context.Context, req *pb.ParseReq
8685
return resp, nil
8786
}
8887

89-
// defaultCommentSyntax is used when parsing query names from plugin-engine query files.
90-
var defaultCommentSyntax = metadata.CommentSyntax(source.CommentSyntax{Dash: true, SlashStar: true, Hash: false})
91-
9288
// runPluginQuerySet runs the plugin-engine path: schema and queries are sent to the
9389
// engine plugin via ParseRequest; the responses are turned into compiler.Result and
9490
// passed to ProcessResult. No AST or compiler parsing is used.
@@ -177,26 +173,24 @@ func runPluginQuerySet(ctx context.Context, rp resultProcessor, name, dir string
177173
continue
178174
}
179175
queryContent := string(blob)
180-
blocks, err := metadata.QueryBlocks(queryContent, defaultCommentSyntax)
176+
resp, err := parseFn(queryContent)
181177
if err != nil {
182178
merr.Add(filename, queryContent, 0, err)
183179
continue
184180
}
185-
for _, b := range blocks {
186-
resp, err := parseFn(b.SQL)
187-
if err != nil {
188-
merr.Add(filename, queryContent, 0, err)
189-
continue
190-
}
191-
q := pluginResponseToCompilerQuery(b.Name, b.Cmd, filepath.Base(filename), resp)
181+
baseName := filepath.Base(filename)
182+
stmts := resp.GetStatements()
183+
for _, st := range stmts {
184+
q := statementToCompilerQuery(st, baseName)
192185
if q == nil {
193186
continue
194187
}
195-
if _, exists := set[b.Name]; exists {
196-
merr.Add(filename, queryContent, 0, fmt.Errorf("duplicate query name: %s", b.Name))
188+
qName := st.GetName()
189+
if _, exists := set[qName]; exists {
190+
merr.Add(filename, queryContent, 0, fmt.Errorf("duplicate query name: %s", qName))
197191
continue
198192
}
199-
set[b.Name] = struct{}{}
193+
set[qName] = struct{}{}
200194
queries = append(queries, q)
201195
}
202196
}
@@ -236,14 +230,17 @@ func loadSchemaSQL(schemaPaths []string, readFile func(string) ([]byte, error))
236230
return strings.Join(parts, "\n"), nil
237231
}
238232

239-
func pluginResponseToCompilerQuery(name, cmd, filename string, resp *pb.ParseResponse) *compiler.Query {
240-
sqlTrimmed := strings.TrimSpace(resp.GetSql())
233+
// statementToCompilerQuery converts one engine.Statement from the plugin into a compiler.Query.
234+
func statementToCompilerQuery(st *pb.Statement, filename string) *compiler.Query {
235+
if st == nil {
236+
return nil
237+
}
238+
sqlTrimmed := strings.TrimSpace(st.GetSql())
241239
if sqlTrimmed == "" {
242240
return nil
243241
}
244-
245242
var params []compiler.Parameter
246-
for _, p := range resp.GetParameters() {
243+
for _, p := range st.GetParameters() {
247244
col := &compiler.Column{
248245
DataType: p.GetDataType(),
249246
NotNull: !p.GetNullable(),
@@ -256,9 +253,8 @@ func pluginResponseToCompilerQuery(name, cmd, filename string, resp *pb.ParseRes
256253
}
257254
params = append(params, compiler.Parameter{Number: pos, Column: col})
258255
}
259-
260256
var columns []*compiler.Column
261-
for _, c := range resp.GetColumns() {
257+
for _, c := range st.GetColumns() {
262258
columns = append(columns, &compiler.Column{
263259
Name: c.GetName(),
264260
DataType: c.GetDataType(),
@@ -267,12 +263,11 @@ func pluginResponseToCompilerQuery(name, cmd, filename string, resp *pb.ParseRes
267263
ArrayDims: int(c.GetArrayDims()),
268264
})
269265
}
270-
271266
return &compiler.Query{
272267
SQL: sqlTrimmed,
273268
Metadata: metadata.Metadata{
274-
Name: name,
275-
Cmd: cmd,
269+
Name: st.GetName(),
270+
Cmd: pb.CmdToString(st.GetCmd()),
276271
Filename: filename,
277272
},
278273
Params: params,

internal/cmd/plugin_engine_test.go

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,10 @@ func TestPluginPipeline_FullPipeline(t *testing.T) {
106106
engineRecord.QuerySQL = querySQL
107107
engineRecord.CalledWith = append(engineRecord.CalledWith, struct{ SchemaSQL, QuerySQL string }{schemaSQL, querySQL})
108108
return &pb.ParseResponse{
109-
Sql: engineRecord.ReturnedSQL,
110-
Parameters: engineRecord.ReturnedParams,
111-
Columns: engineRecord.ReturnedCols,
109+
Statements: []*pb.Statement{{
110+
Name: "GetUser", Cmd: pb.Cmd_CMD_ONE, Sql: engineRecord.ReturnedSQL,
111+
Parameters: engineRecord.ReturnedParams, Columns: engineRecord.ReturnedCols,
112+
}},
112113
}, nil
113114
}
114115

@@ -277,28 +278,30 @@ func TestPluginPipeline_WithoutOverride_UsesPluginPackage(t *testing.T) {
277278
}
278279
}
279280

280-
// TestPluginPipeline_NBlocksNCalls verifies that N sqlc blocks in query.sql yield N plugin Parse calls,
281-
// and each call receives the schema (or connection params in databaseOnly mode).
281+
// TestPluginPipeline_NBlocksNCalls verifies that one Parse call receives full query.sql and schema,
282+
// and the plugin returns N statements (one per query block); sqlc then generates N helpers.
282283
func TestPluginPipeline_NBlocksNCalls(t *testing.T) {
283284
const (
284285
schemaContent = "CREATE TABLE users (id INT, name TEXT);"
285286
block1 = "-- name: GetUser :one\nSELECT id, name FROM users WHERE id = $1"
286287
block2 = "-- name: ListUsers :many\nSELECT id, name FROM users ORDER BY id"
287288
)
288289
queryContent := block1 + "\n\n" + block2
289-
// QueryBlocks slices from " name: " line to the next " name: " (exclusive), so first block includes "\n\n".
290-
expectedBlock1 := block1 + "\n\n"
291-
expectedBlock2 := block2
292290

293291
engineRecord := &engineMockRecord{
294-
ReturnedSQL: "SELECT id, name FROM users WHERE id = $1",
295292
ReturnedParams: []*pb.Parameter{{Position: 1, DataType: "int", Nullable: false}},
296293
ReturnedCols: []*pb.Column{{Name: "id", DataType: "int", Nullable: false}, {Name: "name", DataType: "text", Nullable: false}},
297294
}
295+
var codegenReq *plugin.GenerateRequest
298296
pluginParse := func(schemaSQL, querySQL string) (*pb.ParseResponse, error) {
299297
engineRecord.Calls++
300298
engineRecord.CalledWith = append(engineRecord.CalledWith, struct{ SchemaSQL, QuerySQL string }{schemaSQL, querySQL})
301-
return &pb.ParseResponse{Sql: querySQL, Parameters: engineRecord.ReturnedParams, Columns: engineRecord.ReturnedCols}, nil
299+
return &pb.ParseResponse{
300+
Statements: []*pb.Statement{
301+
{Name: "GetUser", Cmd: pb.Cmd_CMD_ONE, Sql: "SELECT id, name FROM users WHERE id = $1", Parameters: engineRecord.ReturnedParams, Columns: engineRecord.ReturnedCols},
302+
{Name: "ListUsers", Cmd: pb.Cmd_CMD_MANY, Sql: "SELECT id, name FROM users ORDER BY id", Parameters: nil, Columns: engineRecord.ReturnedCols},
303+
},
304+
}, nil
302305
}
303306
conf, _ := config.ParseConfig(strings.NewReader(testPluginPipelineConfig))
304307
inputs := &sourceFiles{
@@ -309,25 +312,29 @@ func TestPluginPipeline_NBlocksNCalls(t *testing.T) {
309312
debug.ProcessPlugins = true
310313
o := &Options{
311314
Env: Env{Debug: debug}, Stderr: &bytes.Buffer{}, PluginParseFunc: pluginParse,
312-
CodegenHandlerOverride: ext.HandleFunc(func(_ context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) { return &plugin.GenerateResponse{}, nil }),
315+
CodegenHandlerOverride: ext.HandleFunc(func(_ context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
316+
codegenReq = req
317+
return &plugin.GenerateResponse{}, nil
318+
}),
313319
}
314320
_, err := generate(context.Background(), inputs, o)
315321
if err != nil {
316322
t.Fatalf("generate failed: %v", err)
317323
}
318-
if n := len(engineRecord.CalledWith); n != 2 {
319-
t.Errorf("expected 2 Parse calls (2 blocks), got %d", n)
324+
if engineRecord.Calls != 1 {
325+
t.Errorf("expected 1 Parse call (full query.sql), got %d", engineRecord.Calls)
320326
}
321-
for i, call := range engineRecord.CalledWith {
322-
if call.SchemaSQL != schemaContent {
323-
t.Errorf("Parse call %d: every call must receive schema; got schemaSQL %q", i+1, call.SchemaSQL)
324-
}
327+
if len(engineRecord.CalledWith) != 1 {
328+
t.Fatalf("expected 1 CalledWith, got %d", len(engineRecord.CalledWith))
325329
}
326-
if len(engineRecord.CalledWith) >= 1 && engineRecord.CalledWith[0].QuerySQL != expectedBlock1 {
327-
t.Errorf("Parse call 1: query must be first block; got %q", engineRecord.CalledWith[0].QuerySQL)
330+
if engineRecord.CalledWith[0].SchemaSQL != schemaContent {
331+
t.Errorf("Parse must receive schema; got %q", engineRecord.CalledWith[0].SchemaSQL)
328332
}
329-
if len(engineRecord.CalledWith) >= 2 && engineRecord.CalledWith[1].QuerySQL != expectedBlock2 {
330-
t.Errorf("Parse call 2: query must be second block; got %q", engineRecord.CalledWith[1].QuerySQL)
333+
if engineRecord.CalledWith[0].QuerySQL != queryContent {
334+
t.Errorf("Parse must receive full query.sql; got %q", engineRecord.CalledWith[0].QuerySQL)
335+
}
336+
if codegenReq == nil || len(codegenReq.Queries) != 2 {
337+
t.Errorf("codegen must receive 2 queries (from 2 statements); got %d", len(codegenReq.Queries))
331338
}
332339
}
333340

@@ -364,7 +371,9 @@ func TestPluginPipeline_DatabaseOnly_ReceivesNoSchema(t *testing.T) {
364371
pluginParse := func(schemaSQL, querySQL string) (*pb.ParseResponse, error) {
365372
engineRecord.Calls++
366373
engineRecord.CalledWith = append(engineRecord.CalledWith, struct{ SchemaSQL, QuerySQL string }{schemaSQL, querySQL})
367-
return &pb.ParseResponse{Sql: querySQL, Parameters: nil, Columns: engineRecord.ReturnedCols}, nil
374+
return &pb.ParseResponse{
375+
Statements: []*pb.Statement{{Name: "GetOne", Cmd: pb.Cmd_CMD_ONE, Sql: "SELECT 1", Parameters: nil, Columns: engineRecord.ReturnedCols}},
376+
}, nil
368377
}
369378
conf, err := config.ParseConfig(strings.NewReader(testPluginPipelineConfigDatabaseOnly))
370379
if err != nil {

0 commit comments

Comments
 (0)