@@ -9,12 +9,12 @@ package cmd
99// mock → codegen request) and that the plugin package is used when PluginParseFunc is nil.
1010//
1111// Proof that the technology works:
12- // - TestPluginPipeline_FullPipeline: with PluginParseFunc set, the pipeline sends schema+query into the mock,
13- // takes Sql/Params/Columns from it, builds compiler.Result → plugin.GenerateRequest, and the codegen mock
14- // receives that. So "plugin engine → ParseRequest contract → codegen" is validated.
12+ // - TestPluginPipeline_FullPipeline: one block → one Parse call; that call receives schema; codegen gets the result.
13+ // - TestPluginPipeline_NBlocksNCalls: N blocks in query.sql → exactly N Parse calls; each call receives schema.
14+ // - TestPluginPipeline_DatabaseOnly_ReceivesNoSchema: with analyzer.database: only + database.uri, each Parse
15+ // call receives empty schema (the real runner would get connection_params in ParseRequest).
1516// - TestPluginPipeline_WithoutOverride_UsesPluginPackage: with PluginParseFunc nil, generate fails with an error
1617// that is NOT "unknown engine", so we did enter runPluginQuerySet and call the engine process runner.
17- // The plugin package is required for that path. Vet does not support plugin engines.
1818
1919import (
2020 "bytes"
@@ -50,10 +50,13 @@ engines:
5050// engineMockRecord holds what the engine-plugin mock received and returned.
5151// Used to validate that the pipeline passes schema + raw query in, and that
5252// the plugin's Sql/Parameters/Columns are what later reach codegen.
53+ // CalledWith records each Parse call so we can assert N blocks → N calls and
54+ // that every call received schema (or "" in databaseOnly mode).
5355type engineMockRecord struct {
5456 Calls int
55- SchemaSQL string
56- QuerySQL string
57+ SchemaSQL string // last call (backward compat)
58+ QuerySQL string // last call (backward compat)
59+ CalledWith []struct { SchemaSQL , QuerySQL string }
5760 ReturnedSQL string
5861 ReturnedParams []* pb.Parameter
5962 ReturnedCols []* pb.Column
@@ -101,6 +104,7 @@ func TestPluginPipeline_FullPipeline(t *testing.T) {
101104 engineRecord .Calls ++
102105 engineRecord .SchemaSQL = schemaSQL
103106 engineRecord .QuerySQL = querySQL
107+ engineRecord .CalledWith = append (engineRecord .CalledWith , struct { SchemaSQL , QuerySQL string }{schemaSQL , querySQL })
104108 return & pb.ParseResponse {
105109 Sql : engineRecord .ReturnedSQL ,
106110 Parameters : engineRecord .ReturnedParams ,
@@ -146,12 +150,20 @@ func TestPluginPipeline_FullPipeline(t *testing.T) {
146150 }
147151
148152 // ---- 1) Validate what was sent INTO the engine plugin ----
153+ // N blocks in query.sql must yield N Parse calls; each call must receive schema (or connection in databaseOnly).
149154 if engineRecord .Calls != 1 {
150- t .Errorf ("engine mock called %d times, want 1" , engineRecord .Calls )
155+ t .Errorf ("engine mock called %d times, want 1 (one block → one Parse call)" , engineRecord .Calls )
156+ }
157+ if len (engineRecord .CalledWith ) != 1 {
158+ t .Errorf ("engine mock CalledWith has %d entries, want 1" , len (engineRecord .CalledWith ))
159+ }
160+ if len (engineRecord .CalledWith ) > 0 && engineRecord .CalledWith [0 ].SchemaSQL != schemaContent {
161+ t .Errorf ("every Parse call must receive schema: got %q" , engineRecord .CalledWith [0 ].SchemaSQL )
151162 }
152163 if engineRecord .SchemaSQL != schemaContent {
153164 t .Errorf ("engine received schema:\n got: %q\n want: %q" , engineRecord .SchemaSQL , schemaContent )
154165 }
166+ // With one block, query SQL is the whole block (same as queryContent)
155167 if engineRecord .QuerySQL != queryContent {
156168 t .Errorf ("engine received query:\n got: %q\n want: %q" , engineRecord .QuerySQL , queryContent )
157169 }
@@ -265,6 +277,124 @@ func TestPluginPipeline_WithoutOverride_UsesPluginPackage(t *testing.T) {
265277 }
266278}
267279
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).
282+ func TestPluginPipeline_NBlocksNCalls (t * testing.T ) {
283+ const (
284+ schemaContent = "CREATE TABLE users (id INT, name TEXT);"
285+ block1 = "-- name: GetUser :one\n SELECT id, name FROM users WHERE id = $1"
286+ block2 = "-- name: ListUsers :many\n SELECT id, name FROM users ORDER BY id"
287+ )
288+ 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
292+
293+ engineRecord := & engineMockRecord {
294+ ReturnedSQL : "SELECT id, name FROM users WHERE id = $1" ,
295+ ReturnedParams : []* pb.Parameter {{Position : 1 , DataType : "int" , Nullable : false }},
296+ ReturnedCols : []* pb.Column {{Name : "id" , DataType : "int" , Nullable : false }, {Name : "name" , DataType : "text" , Nullable : false }},
297+ }
298+ pluginParse := func (schemaSQL , querySQL string ) (* pb.ParseResponse , error ) {
299+ engineRecord .Calls ++
300+ engineRecord .CalledWith = append (engineRecord .CalledWith , struct { SchemaSQL , QuerySQL string }{schemaSQL , querySQL })
301+ return & pb.ParseResponse {Sql : querySQL , Parameters : engineRecord .ReturnedParams , Columns : engineRecord .ReturnedCols }, nil
302+ }
303+ conf , _ := config .ParseConfig (strings .NewReader (testPluginPipelineConfig ))
304+ inputs := & sourceFiles {
305+ Config : & conf , ConfigPath : "sqlc.yaml" , Dir : "." ,
306+ FileContents : map [string ][]byte {"schema.sql" : []byte (schemaContent ), "query.sql" : []byte (queryContent )},
307+ }
308+ debug := opts .DebugFromString ("" )
309+ debug .ProcessPlugins = true
310+ o := & Options {
311+ 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 }),
313+ }
314+ _ , err := generate (context .Background (), inputs , o )
315+ if err != nil {
316+ t .Fatalf ("generate failed: %v" , err )
317+ }
318+ if n := len (engineRecord .CalledWith ); n != 2 {
319+ t .Errorf ("expected 2 Parse calls (2 blocks), got %d" , n )
320+ }
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+ }
325+ }
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 )
328+ }
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 )
331+ }
332+ }
333+
334+ const testPluginPipelineConfigDatabaseOnly = `version: "2"
335+ sql:
336+ - engine: "testeng"
337+ schema: ["schema.sql"]
338+ queries: ["query.sql"]
339+ analyzer:
340+ database: only
341+ database:
342+ uri: "postgres://localhost/test"
343+ codegen:
344+ - plugin: "mock"
345+ out: "."
346+ plugins:
347+ - name: "mock"
348+ process:
349+ cmd: "echo"
350+ engines:
351+ - name: "testeng"
352+ process:
353+ cmd: "echo"
354+ `
355+
356+ // TestPluginPipeline_DatabaseOnly_ReceivesNoSchema verifies that in databaseOnly mode (analyzer.database: only +
357+ // database.uri) the plugin receives empty schema and the core would pass connection_params to the real runner.
358+ // The mock only sees (schemaSQL, querySQL); in databaseOnly we pass schemaSQL="".
359+ func TestPluginPipeline_DatabaseOnly_ReceivesNoSchema (t * testing.T ) {
360+ const queryContent = "-- name: GetOne :one\n SELECT 1"
361+ engineRecord := & engineMockRecord {
362+ ReturnedSQL : "SELECT 1" , ReturnedParams : nil , ReturnedCols : []* pb.Column {{Name : "?column?" , DataType : "int" , Nullable : true }},
363+ }
364+ pluginParse := func (schemaSQL , querySQL string ) (* pb.ParseResponse , error ) {
365+ engineRecord .Calls ++
366+ engineRecord .CalledWith = append (engineRecord .CalledWith , struct { SchemaSQL , QuerySQL string }{schemaSQL , querySQL })
367+ return & pb.ParseResponse {Sql : querySQL , Parameters : nil , Columns : engineRecord .ReturnedCols }, nil
368+ }
369+ conf , err := config .ParseConfig (strings .NewReader (testPluginPipelineConfigDatabaseOnly ))
370+ if err != nil {
371+ t .Fatalf ("parse config: %v" , err )
372+ }
373+ inputs := & sourceFiles {
374+ Config : & conf , ConfigPath : "sqlc.yaml" , Dir : "." ,
375+ FileContents : map [string ][]byte {"schema.sql" : []byte ("CREATE TABLE t (id INT);" ), "query.sql" : []byte (queryContent )},
376+ }
377+ debug := opts .DebugFromString ("" )
378+ debug .ProcessPlugins = true
379+ o := & Options {
380+ Env : Env {Debug : debug }, Stderr : & bytes.Buffer {}, PluginParseFunc : pluginParse ,
381+ CodegenHandlerOverride : ext .HandleFunc (func (_ context.Context , req * plugin.GenerateRequest ) (* plugin.GenerateResponse , error ) { return & plugin.GenerateResponse {}, nil }),
382+ }
383+ _ , err = generate (context .Background (), inputs , o )
384+ if err != nil {
385+ t .Fatalf ("generate failed: %v" , err )
386+ }
387+ if len (engineRecord .CalledWith ) != 1 {
388+ t .Errorf ("expected 1 Parse call, got %d" , len (engineRecord .CalledWith ))
389+ }
390+ if len (engineRecord .CalledWith ) > 0 && engineRecord .CalledWith [0 ].SchemaSQL != "" {
391+ t .Errorf ("databaseOnly mode: each Parse call must receive empty schema (connection_params are used by real runner); got %q" , engineRecord .CalledWith [0 ].SchemaSQL )
392+ }
393+ if len (engineRecord .CalledWith ) > 0 && engineRecord .CalledWith [0 ].QuerySQL != queryContent {
394+ t .Errorf ("query SQL must still be passed; got %q" , engineRecord .CalledWith [0 ].QuerySQL )
395+ }
396+ }
397+
268398// TestPluginPipeline_OptionsOverrideNil ensures default Options do not inject mocks.
269399func TestPluginPipeline_OptionsOverrideNil (t * testing.T ) {
270400 o := & Options {}
0 commit comments