A custom emitter

On this page

On SQLite:

  • the wrapper forwards emit.TypeDefaulterPort, which the generator looks for on the emitter it was given
  • the extra file is built from emit.Request's analysed queries and formatted with go/format
  • the committed internal/store comes from go run . -write; the stock sgl generate would prune query_index.go, which only this emitter writes (053 builds an sgl that keeps it)

Seagull splits its work in two. The analyser reads the SQL and works out everything about each query; an emitter turns that analysis into source files. The emitters Seagull ships write Go for database/sql, pgx and ClickHouse's native driver, and the interface they implement is public, so you can write your own: for another driver, another language, or, as here, one extra file beside the usual package.

This example wraps the database/sql emitter and adds query_index.go, a list of every query in the package with its command and whether it only reads. A list like that can feed a documentation page, metric labels or an admin screen, and it can never fall out of step with the SQL.

The interface

An emitter has one method:

type EmitterPort interface {
    Emit(ctx context.Context, request *emit.Request) ([]emit.GeneratedFile, error)
}

emit.Request carries everything the analysis produced: the package name, the analysed queries in file order, the schema catalogue, the type mapping with every override applied, the dialect, and the Style that names things in the generated code. The emitter returns one GeneratedFile per file, a plain name and its full content.

The program

example.go

package main

import (
	"context"
	"fmt"
	"go/format"
	"io"
	"io/fs"
	"strings"

	"seagull.sh/seagull"
	"seagull.sh/seagull/sdk/dialect"
	"seagull.sh/seagull/sdk/dialect/dialect_engine_sqlite"
	"seagull.sh/seagull/sdk/emit"
	"seagull.sh/seagull/sdk/emit/emit_driver_databasesql"
	"seagull.sh/seagull/sdk/emit/emit_kit"
)

const (
	// indexFile is the name of the file the emitter adds.
	indexFile = "query_index.go"

	// infoType is the type the index declares for one query.
	infoType = "QueryInfo"

	// indexVariable is the variable the index declares.
	indexVariable = "QueryIndex"
)

// indexEmitter wraps another emitter and adds query_index.go: a list of every query in
// the package with its method, its command and whether it only reads, for a documentation
// page, metric labels or an admin screen. It passes on every optional interface the
// generator asks the emitter it was given for, adding the names its own file declares to
// the wrapped emitter's reservation.
type indexEmitter struct {
	// base generates the package itself.
	base emit.EmitterPort
}

var (
	_ emit.EmitterPort = (*indexEmitter)(nil)

	_ emit.TypeDefaulterPort = (*indexEmitter)(nil)

	_ emit.ReserverPort = (*indexEmitter)(nil)

	_ emit.CheckerPort = (*indexEmitter)(nil)

	_ emit.NullElementBinderPort = (*indexEmitter)(nil)
)

// Emit generates the wrapped emitter's files and appends the index.
//
// Takes request (*emit.Request) which carries the analysed queries and the style.
//
// Returns []emit.GeneratedFile which holds the wrapped emitter's files and the index.
// Returns error when either cannot be generated.
func (e indexEmitter) Emit(ctx context.Context, request *emit.Request) ([]emit.GeneratedFile, error) {
	files, err := e.base.Emit(ctx, request)
	if err != nil {
		return nil, err
	}
	index, err := queryIndex(request)
	if err != nil {
		return nil, err
	}
	return append(files, index), nil
}

// DefaultTypes passes on the wrapped emitter's type defaults. The generator asks the
// emitter it was given whether it implements emit.TypeDefaulterPort, so without this
// method the wrapper would hide them.
//
// Takes info (*dialect.Facts) which describes the dialect.
//
// Returns seagull.TypeSlots which holds the wrapped emitter's defaults, or none.
func (e indexEmitter) DefaultTypes(info *dialect.Facts) seagull.TypeSlots {
	if defaulter, ok := e.base.(emit.TypeDefaulterPort); ok {
		return defaulter.DefaultTypes(info)
	}
	return seagull.TypeSlots{}
}

// Reserve passes on the names the wrapped emitter's code uses and adds the index's, so
// the naming plan never gives a model, a named type or a query's constant the name
// QueryInfo or QueryIndex, and never names a query file query_index.go.
//
// Takes info (*dialect.Facts) which describes the dialect.
// Takes style (*emit.Style) which holds the style of the generated code.
//
// Returns emit.Reservation which holds the wrapped emitter's names and the index's.
func (e indexEmitter) Reserve(info *dialect.Facts, style *emit.Style) emit.Reservation {
	reservation := emit_kit.ReservationFor(e.base, info, style)
	reservation.Declarations = append(reservation.Declarations, infoType, indexVariable)
	reservation.Files = append(reservation.Files, indexFile)
	return reservation
}

// Check passes on what the wrapped emitter refuses, which the generator reports as
// diagnostics at the query before it emits anything.
//
// Takes request (*emit.Request) which is the request about to be emitted.
//
// Returns []emit.Problem which holds the wrapped emitter's problems, or none.
func (e indexEmitter) Check(request *emit.Request) []emit.Problem {
	if checker, ok := e.base.(emit.CheckerPort); ok {
		return checker.Check(request)
	}
	return nil
}

// BindsNullElements passes on whether the wrapped emitter's driver binds a NULL element
// of an array parameter. Without it the generator would take the wrapper for one that
// does, and give array parameters nullable elements on a driver that panics on them, such
// as DuckDB's.
//
// Takes info (*dialect.Facts) which describes the dialect.
//
// Returns bool which is the wrapped emitter's answer, or true when it gives none.
func (e indexEmitter) BindsNullElements(info *dialect.Facts) bool {
	if binder, ok := e.base.(emit.NullElementBinderPort); ok {
		return binder.BindsNullElements(info)
	}
	return true
}

// queryIndex writes query_index.go. It starts with the style's header, so WriteFiles
// recognises it as generated, and is formatted with go/format. Each query's method is the
// name the naming plan gives it, which the wrapped emitter declares, rather than one
// derived from the query's name.
//
// Takes request (*emit.Request) which carries the package name, queries, naming plan and
// style.
//
// Returns emit.GeneratedFile which is the index.
// Returns error when the source does not format.
func queryIndex(request *emit.Request) (emit.GeneratedFile, error) {
	var source strings.Builder
	fmt.Fprintf(&source, "%s\n\npackage %s\n\n", request.Style.Header, request.Package)
	fmt.Fprintf(&source, "// %s describes one generated query method.\n", infoType)
	fmt.Fprintf(&source, "type %s struct {\n\tName string\n\tMethod string\n\tCommand string\n\tReadOnly bool\n}\n\n", infoType)
	fmt.Fprintf(&source, "// %s lists every query of the package in source order.\n", indexVariable)
	fmt.Fprintf(&source, "var %s = []%s{\n", indexVariable, infoType)
	for index, query := range request.Queries {
		fmt.Fprintf(&source, "\t{Name: %q, Method: %q, Command: %q, ReadOnly: %t},\n",
			query.Name, request.Naming.Queries[index].Method, query.Command, query.ReadOnly)
	}
	source.WriteString("}\n")

	formatted, err := format.Source([]byte(source.String()))
	if err != nil {
		return emit.GeneratedFile{}, fmt.Errorf("formatting %s: %w", indexFile, err)
	}
	return emit.GeneratedFile{Name: indexFile, Content: formatted}, nil
}

// generate runs the generator with the index emitter wrapped around the database/sql
// emitter.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
//
// Returns *seagull.Result which holds the generated files.
// Returns error when generation fails or a query has an error diagnostic.
func generate(ctx context.Context, project fs.FS) (*seagull.Result, error) {
	emitter := indexEmitter{base: emit_driver_databasesql.New(emit_driver_databasesql.WithPrepared(false))}
	generator, err := seagull.New(dialect_engine_sqlite.New(), seagull.WithEmitter(emitter))
	if err != nil {
		return nil, err
	}
	result, err := generator.Generate(ctx, seagull.Input{
		Migrations:   project,
		Queries:      nil,
		Package:      "store",
		MigrationDir: "db/migrations",
		QueryDir:     "db/queries",
	})
	if err != nil {
		return nil, err
	}
	if err := result.Err(); err != nil {
		return nil, err
	}
	return result, nil
}

// run generates the package with the index emitter and prints the files it produced and
// the index itself.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
// Takes output (io.Writer) which receives the file list and the index.
//
// Returns error when generation fails.
func run(ctx context.Context, project fs.FS, output io.Writer) error {
	result, err := generate(ctx, project)
	if err != nil {
		return err
	}
	for _, file := range result.Files {
		fmt.Fprintln(output, file.Name)
		if file.Name == indexFile {
			fmt.Fprintf(output, "\n%s", file.Content)
		}
	}
	return nil
}

Wrapping. indexEmitter holds another emitter, here emit_driver_databasesql.New(emit_driver_databasesql.WithPrepared(false)). Its Emit lets that emitter generate the package, then appends one more file. Everything the wrapped emitter writes is untouched.

Forwarding the type defaults. An emitter may also implement emit.TypeDefaulterPort, to supply the Go types its driver scans into. The generator asks the emitter it was given whether it implements that interface, so a wrapper that did not forward DefaultTypes would hide the wrapped emitter's defaults, and the generated types could quietly change.

Building the file. queryIndex writes ordinary Go source from request.Queries, reading each query's Name, Command and ReadOnly. Two details make it behave like any other generated file. It starts with request.Style.Header, the generated-file header, which is how WriteFiles recognises the files it may prune later. And it is formatted with go/format, so it reads like the rest of the package.

Using it. seagull.WithEmitter replaces the default emitter, and Generate returns the extra file in Result.Files with all the others.

What it prints

The program lists the files the emitter produced, then prints the index:

models.go
tasks.sql.go
querier.go
query_index.go

// Code generated by sgl. DO NOT EDIT.

package store

// QueryInfo describes one generated query method.
type QueryInfo struct {
	Name     string
	Method   string
	Command  string
	ReadOnly bool
}

// QueryIndex lists every query of the package in source order.
var QueryIndex = []QueryInfo{
	{Name: "CreateTask", Method: "CreateTask", Command: "one", ReadOnly: false},
	{Name: "ListTasks", Method: "ListTasks", Command: "many", ReadOnly: true},
	{Name: "CompleteTask", Method: "CompleteTask", Command: "execrows", ReadOnly: false},
}

The index uses the same header as the rest of the package, and ListTasks is the only query marked read-only. The three files before it are exactly what the database/sql emitter writes without the wrapper.

The generated method for ListTasks is unchanged by the wrapper:

internal/store

type ListTasksRow struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  int64  `json:"done"`
}

func (queries *Queries) ListTasks(ctx context.Context) ([]ListTasksRow, error) {
	rows, err := queries.reader.QueryContext(ctx, listTasksSQL)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var results []ListTasksRow
	if rows.Next() {
		var row ListTasksRow
		scanArgs := []any{&row.ID, &row.Title, &row.Done}
		for more := true; more; more = rows.Next() {
			row = ListTasksRow{}
			if err := rows.Scan(scanArgs...); err != nil {
				return nil, err
			}
			results = append(results, row)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return results, nil
}

Keeping the file

go run . -write writes the package into internal/store with WriteFiles, and that is how the committed copy was made. Notice what would happen if you ran the stock sgl generate here: query_index.go carries sgl's header, but the stock emitter does not produce it, so sgl would prune it as a stale generated file. To keep it under sgl, the emitter has to be part of sgl itself, which is what Your own sgl builds.

Writing an emitter describes every field of the request and how to choose Go types for a new driver, and the emit package documents the interface.

Run it locally

From the directory you cloned the Seagull repository into:

cd seagull/examples/embedding && go run ./052_custom_emitter

Its test runs it too, and checks what it prints against output.txt:

cd seagull/examples/embedding && go test ./052_custom_emitter

The files

Every file of the example, as it is in the Seagull repository.

sgl.yaml

# Copyright 2026 PolitePixels Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This project stands against fascism, authoritarianism, and all forms of
# oppression. We built this to empower people, not to enable those who would
# strip others of their rights and dignity.

version: 1

databases:
  - name: tasks
    dialect: sqlite
    migrations: db/migrations
    queries: db/queries
    go:
      package: store
      out: internal/store

db/migrations/0001_init.up.sql

CREATE TABLE tasks (
    id    INTEGER PRIMARY KEY,
    title TEXT    NOT NULL,
    done  INTEGER NOT NULL DEFAULT 0
);

db/migrations/0001_init.down.sql

DROP TABLE tasks;

db/queries/tasks.sql

-- sgl.query(CreateTask, one)
INSERT INTO tasks (title) VALUES (?) RETURNING id, title, done;

-- sgl.query(ListTasks, many)
SELECT id, title, done FROM tasks ORDER BY id;

-- sgl.query(CompleteTask, execrows)
UPDATE tasks SET done = 1 WHERE id = ?;

example.go

package main

import (
	"context"
	"fmt"
	"go/format"
	"io"
	"io/fs"
	"strings"

	"seagull.sh/seagull"
	"seagull.sh/seagull/sdk/dialect"
	"seagull.sh/seagull/sdk/dialect/dialect_engine_sqlite"
	"seagull.sh/seagull/sdk/emit"
	"seagull.sh/seagull/sdk/emit/emit_driver_databasesql"
	"seagull.sh/seagull/sdk/emit/emit_kit"
)

const (
	// indexFile is the name of the file the emitter adds.
	indexFile = "query_index.go"

	// infoType is the type the index declares for one query.
	infoType = "QueryInfo"

	// indexVariable is the variable the index declares.
	indexVariable = "QueryIndex"
)

// indexEmitter wraps another emitter and adds query_index.go: a list of every query in
// the package with its method, its command and whether it only reads, for a documentation
// page, metric labels or an admin screen. It passes on every optional interface the
// generator asks the emitter it was given for, adding the names its own file declares to
// the wrapped emitter's reservation.
type indexEmitter struct {
	// base generates the package itself.
	base emit.EmitterPort
}

var (
	_ emit.EmitterPort = (*indexEmitter)(nil)

	_ emit.TypeDefaulterPort = (*indexEmitter)(nil)

	_ emit.ReserverPort = (*indexEmitter)(nil)

	_ emit.CheckerPort = (*indexEmitter)(nil)

	_ emit.NullElementBinderPort = (*indexEmitter)(nil)
)

// Emit generates the wrapped emitter's files and appends the index.
//
// Takes request (*emit.Request) which carries the analysed queries and the style.
//
// Returns []emit.GeneratedFile which holds the wrapped emitter's files and the index.
// Returns error when either cannot be generated.
func (e indexEmitter) Emit(ctx context.Context, request *emit.Request) ([]emit.GeneratedFile, error) {
	files, err := e.base.Emit(ctx, request)
	if err != nil {
		return nil, err
	}
	index, err := queryIndex(request)
	if err != nil {
		return nil, err
	}
	return append(files, index), nil
}

// DefaultTypes passes on the wrapped emitter's type defaults. The generator asks the
// emitter it was given whether it implements emit.TypeDefaulterPort, so without this
// method the wrapper would hide them.
//
// Takes info (*dialect.Facts) which describes the dialect.
//
// Returns seagull.TypeSlots which holds the wrapped emitter's defaults, or none.
func (e indexEmitter) DefaultTypes(info *dialect.Facts) seagull.TypeSlots {
	if defaulter, ok := e.base.(emit.TypeDefaulterPort); ok {
		return defaulter.DefaultTypes(info)
	}
	return seagull.TypeSlots{}
}

// Reserve passes on the names the wrapped emitter's code uses and adds the index's, so
// the naming plan never gives a model, a named type or a query's constant the name
// QueryInfo or QueryIndex, and never names a query file query_index.go.
//
// Takes info (*dialect.Facts) which describes the dialect.
// Takes style (*emit.Style) which holds the style of the generated code.
//
// Returns emit.Reservation which holds the wrapped emitter's names and the index's.
func (e indexEmitter) Reserve(info *dialect.Facts, style *emit.Style) emit.Reservation {
	reservation := emit_kit.ReservationFor(e.base, info, style)
	reservation.Declarations = append(reservation.Declarations, infoType, indexVariable)
	reservation.Files = append(reservation.Files, indexFile)
	return reservation
}

// Check passes on what the wrapped emitter refuses, which the generator reports as
// diagnostics at the query before it emits anything.
//
// Takes request (*emit.Request) which is the request about to be emitted.
//
// Returns []emit.Problem which holds the wrapped emitter's problems, or none.
func (e indexEmitter) Check(request *emit.Request) []emit.Problem {
	if checker, ok := e.base.(emit.CheckerPort); ok {
		return checker.Check(request)
	}
	return nil
}

// BindsNullElements passes on whether the wrapped emitter's driver binds a NULL element
// of an array parameter. Without it the generator would take the wrapper for one that
// does, and give array parameters nullable elements on a driver that panics on them, such
// as DuckDB's.
//
// Takes info (*dialect.Facts) which describes the dialect.
//
// Returns bool which is the wrapped emitter's answer, or true when it gives none.
func (e indexEmitter) BindsNullElements(info *dialect.Facts) bool {
	if binder, ok := e.base.(emit.NullElementBinderPort); ok {
		return binder.BindsNullElements(info)
	}
	return true
}

// queryIndex writes query_index.go. It starts with the style's header, so WriteFiles
// recognises it as generated, and is formatted with go/format. Each query's method is the
// name the naming plan gives it, which the wrapped emitter declares, rather than one
// derived from the query's name.
//
// Takes request (*emit.Request) which carries the package name, queries, naming plan and
// style.
//
// Returns emit.GeneratedFile which is the index.
// Returns error when the source does not format.
func queryIndex(request *emit.Request) (emit.GeneratedFile, error) {
	var source strings.Builder
	fmt.Fprintf(&source, "%s\n\npackage %s\n\n", request.Style.Header, request.Package)
	fmt.Fprintf(&source, "// %s describes one generated query method.\n", infoType)
	fmt.Fprintf(&source, "type %s struct {\n\tName string\n\tMethod string\n\tCommand string\n\tReadOnly bool\n}\n\n", infoType)
	fmt.Fprintf(&source, "// %s lists every query of the package in source order.\n", indexVariable)
	fmt.Fprintf(&source, "var %s = []%s{\n", indexVariable, infoType)
	for index, query := range request.Queries {
		fmt.Fprintf(&source, "\t{Name: %q, Method: %q, Command: %q, ReadOnly: %t},\n",
			query.Name, request.Naming.Queries[index].Method, query.Command, query.ReadOnly)
	}
	source.WriteString("}\n")

	formatted, err := format.Source([]byte(source.String()))
	if err != nil {
		return emit.GeneratedFile{}, fmt.Errorf("formatting %s: %w", indexFile, err)
	}
	return emit.GeneratedFile{Name: indexFile, Content: formatted}, nil
}

// generate runs the generator with the index emitter wrapped around the database/sql
// emitter.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
//
// Returns *seagull.Result which holds the generated files.
// Returns error when generation fails or a query has an error diagnostic.
func generate(ctx context.Context, project fs.FS) (*seagull.Result, error) {
	emitter := indexEmitter{base: emit_driver_databasesql.New(emit_driver_databasesql.WithPrepared(false))}
	generator, err := seagull.New(dialect_engine_sqlite.New(), seagull.WithEmitter(emitter))
	if err != nil {
		return nil, err
	}
	result, err := generator.Generate(ctx, seagull.Input{
		Migrations:   project,
		Queries:      nil,
		Package:      "store",
		MigrationDir: "db/migrations",
		QueryDir:     "db/queries",
	})
	if err != nil {
		return nil, err
	}
	if err := result.Err(); err != nil {
		return nil, err
	}
	return result, nil
}

// run generates the package with the index emitter and prints the files it produced and
// the index itself.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
// Takes output (io.Writer) which receives the file list and the index.
//
// Returns error when generation fails.
func run(ctx context.Context, project fs.FS, output io.Writer) error {
	result, err := generate(ctx, project)
	if err != nil {
		return err
	}
	for _, file := range result.Files {
		fmt.Fprintln(output, file.Name)
		if file.Name == indexFile {
			fmt.Fprintf(output, "\n%s", file.Content)
		}
	}
	return nil
}

main.go

// Command 052_custom_emitter generates this example's store package with an emitter that
// wraps the database/sql one and adds query_index.go, and prints the result.
//
// Run it with `go run .`, or `go run . -write` to write the package into internal/store,
// where it is committed so the build checks the generated code.
package main

import (
	"context"
	"flag"
	"log"
	"os"

	"seagull.sh/seagull"
)

// outputDirectory is where -write puts the generated package.
const outputDirectory = "internal/store"

func main() {
	write := flag.Bool("write", false, "write the package into internal/store instead of printing it")
	flag.Parse()
	if err := execute(context.Background(), *write); err != nil {
		log.Fatal(err)
	}
}

// execute prints the generated package, or writes it into internal/store.
//
// Takes write (bool) which writes instead of printing.
//
// Returns error when generation or writing fails.
func execute(ctx context.Context, write bool) error {
	project := os.DirFS(".")
	if !write {
		return run(ctx, project, os.Stdout)
	}
	result, err := generate(ctx, project)
	if err != nil {
		return err
	}
	return seagull.WriteFiles(outputDirectory, result)
}

internal/store/models.go

// Code generated by sgl. DO NOT EDIT.

package store

type Tasks struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  int64  `json:"done"`
}

internal/store/querier.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"context"
	"database/sql"
)

type DBTX interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

type dbtxWrapper interface {
	WrapDBTX(inner any) any
}

type Queries struct {
	reader  DBTX
	writer  DBTX
	wrapper dbtxWrapper
}

func New(db DBTX) *Queries {
	wrapper, _ := db.(dbtxWrapper)
	return &Queries{reader: db, writer: db, wrapper: wrapper}
}

func NewWithReplica(writer DBTX, reader DBTX) *Queries {
	wrapper, _ := writer.(dbtxWrapper)
	return &Queries{reader: reader, writer: writer, wrapper: wrapper}
}

func (queries *Queries) WithTx(transaction *sql.Tx) *Queries {
	if queries == nil {
		return &Queries{reader: transaction, writer: transaction, wrapper: nil}
	}
	db := queries.wrapDBTX(transaction)
	return &Queries{reader: db, writer: db, wrapper: queries.wrapper}
}

func (queries *Queries) wrapDBTX(db DBTX) DBTX {
	if queries.wrapper == nil {
		return db
	}
	wrapped, ok := queries.wrapper.WrapDBTX(db).(DBTX)
	if !ok {
		return db
	}
	return wrapped
}

func (queries *Queries) RunInTx(ctx context.Context, db *sql.DB, fn func(*Queries) error) error {
	transaction, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer transaction.Rollback()
	if err = fn(queries.WithTx(transaction)); err != nil {
		return err
	}
	return transaction.Commit()
}

internal/store/query_index.go

// Code generated by sgl. DO NOT EDIT.

package store

// QueryInfo describes one generated query method.
type QueryInfo struct {
	Name     string
	Method   string
	Command  string
	ReadOnly bool
}

// QueryIndex lists every query of the package in source order.
var QueryIndex = []QueryInfo{
	{Name: "CreateTask", Method: "CreateTask", Command: "one", ReadOnly: false},
	{Name: "ListTasks", Method: "ListTasks", Command: "many", ReadOnly: true},
	{Name: "CompleteTask", Method: "CompleteTask", Command: "execrows", ReadOnly: false},
}

internal/store/tasks.sql.go

// Code generated by sgl. DO NOT EDIT.

package store

import "context"

const completeTaskSQL = `UPDATE tasks SET done = 1 WHERE id = ?1;`

func (queries *Queries) CompleteTask(ctx context.Context, id int64) (int64, error) {
	results, err := queries.writer.ExecContext(ctx, completeTaskSQL, id)
	if err != nil {
		return 0, err
	}
	return results.RowsAffected()
}

const createTaskSQL = `INSERT INTO tasks (title) VALUES (?1) RETURNING id, title, done;`

type CreateTaskRow struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  int64  `json:"done"`
}

func (queries *Queries) CreateTask(ctx context.Context, title string) (CreateTaskRow, error) {
	var row CreateTaskRow
	err := queries.writer.QueryRowContext(ctx, createTaskSQL, title).Scan(&row.ID, &row.Title, &row.Done)
	if err != nil {
		return CreateTaskRow{}, err
	}
	return row, nil
}

const listTasksSQL = `SELECT id, title, done FROM tasks ORDER BY id;`

type ListTasksRow struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
	Done  int64  `json:"done"`
}

func (queries *Queries) ListTasks(ctx context.Context) ([]ListTasksRow, error) {
	rows, err := queries.reader.QueryContext(ctx, listTasksSQL)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var results []ListTasksRow
	if rows.Next() {
		var row ListTasksRow
		scanArgs := []any{&row.ID, &row.Title, &row.Done}
		for more := true; more; more = rows.Next() {
			row = ListTasksRow{}
			if err := rows.Scan(scanArgs...); err != nil {
				return nil, err
			}
			results = append(results, row)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return results, nil
}

output.txt

models.go
tasks.sql.go
querier.go
query_index.go

// Code generated by sgl. DO NOT EDIT.

package store

// QueryInfo describes one generated query method.
type QueryInfo struct {
	Name     string
	Method   string
	Command  string
	ReadOnly bool
}

// QueryIndex lists every query of the package in source order.
var QueryIndex = []QueryInfo{
	{Name: "CreateTask", Method: "CreateTask", Command: "one", ReadOnly: false},
	{Name: "ListTasks", Method: "ListTasks", Command: "many", ReadOnly: true},
	{Name: "CompleteTask", Method: "CompleteTask", Command: "execrows", ReadOnly: false},
}