Generating from Go

On this page

On SQLite:

  • dialects.New picks the SQLite dialect by name, as a tool reading its own configuration would
  • the default emitter is database/sql, which also writes prepared.go; sgl.yaml sets go.prepared: true so sgl generate agrees
  • the check compares each generated file and looks for files with the generated header that this run would prune

sgl is a thin command over a Go package, seagull.sh/seagull, and anything that can import a package can generate code the way sgl generate does. A build tool, a framework or a project's own generator can hand Seagull a dialect and the SQL, get back the generated files and every diagnostic, and decide for itself what to print, what to fail on and where to write. This example is such a program: it generates a store package from Go, and it has a check mode for CI.

The project

A notes table, with an INSERT and a list. sgl.yaml describes the same project for sgl, so you can compare the two: the program does in Go what this file asks of sgl generate.

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: notes
    dialect: sqlite
    migrations: db/migrations
    queries: db/queries
    go:
      package: store
      out: internal/store
      prepared: true
      types:
        overrides:
          - column: notes.settings
            go: encoding/json.RawMessage

The type override says that notes.settings, a TEXT column holding JSON, should come back as a json.RawMessage. prepared: true matters too, as the end of this page explains.

The program

main.go only parses a -check flag and exits 1 when the package is stale. The work is in run:

example.go

package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"seagull.sh/seagull"
	"seagull.sh/seagull/sdk/dialect/dialect_registry"
)

// run generates the store package from the project's migrations and queries, as a build
// tool that embeds Seagull does. It writes the package into directory or, when check is
// set, writes nothing and reports whether directory is out of date.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
// Takes directory (string) which is the package directory, such as internal/store.
// Takes check (bool) which compares the generated files with directory instead of writing
// them.
// Takes output (io.Writer) which receives the diagnostics and a summary.
//
// Returns bool which is true when check found directory out of date.
// Returns error when generation fails or a query has an error diagnostic.
func run(ctx context.Context, project fs.FS, directory string, check bool, output io.Writer) (bool, error) {
	sqlite, err := dialect_registry.New("sqlite", dialect_registry.Options{})
	if err != nil {
		return false, err
	}
	generator, err := seagull.New(sqlite, seagull.WithTypeOverrides(seagull.TypeOverride{
		SQLTypeName: "",
		Column:      "notes.settings",
		GoPackage:   "encoding/json",
		GoName:      "RawMessage",
	}))
	if err != nil {
		return false, fmt.Errorf("configuring the generator: %w", err)
	}

	result, err := generator.Generate(ctx, seagull.Input{
		Migrations:   project,
		Queries:      nil,
		Package:      "store",
		MigrationDir: "db/migrations",
		QueryDir:     "db/queries",
	})
	if err != nil {
		return false, err
	}
	for _, diagnostic := range result.Diagnostics {
		fmt.Fprintf(output, "%s:%d:%d: %s[%s]: %s\n", diagnostic.Filename, diagnostic.Line, diagnostic.Column,
			diagnostic.Severity, diagnostic.Code, diagnostic.Message)
	}
	if err := result.Err(); err != nil {
		return false, err
	}

	if check {
		stale, err := outOfDate(result, directory)
		if err != nil {
			return false, err
		}
		if len(stale) == 0 {
			fmt.Fprintln(output, "up to date")
			return false, nil
		}
		fmt.Fprintf(output, "out of date: %s\n", strings.Join(stale, ", "))
		return true, nil
	}

	if err := seagull.WriteFiles(directory, result); err != nil {
		return false, err
	}
	fmt.Fprintf(output, "wrote %d files for %d queries\n", len(result.Files), result.QueryCount)
	return false, nil
}

// outOfDate lists the files WriteFiles would change: a generated file that is missing or
// differs, and a file an earlier run generated that this run no longer does, such as the
// file of a deleted query, which WriteFiles would remove.
//
// Takes result (*seagull.Result) which is the generation to compare.
// Takes directory (string) which holds the package as it is now.
//
// Returns []string which holds the names of the files that would change, sorted.
// Returns error when the directory cannot be read.
func outOfDate(result *seagull.Result, directory string) ([]string, error) {
	var stale []string
	generated := make(map[string]bool, len(result.Files))
	for _, file := range result.Files {
		generated[file.Name] = true
		existing, err := os.ReadFile(filepath.Join(directory, file.Name)) //nolint:gosec // a file of the package directory
		if err != nil || !bytes.Equal(existing, file.Content) {
			stale = append(stale, file.Name)
		}
	}

	entries, err := os.ReadDir(directory)
	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}
	for _, entry := range entries {
		if generated[entry.Name()] || entry.IsDir() {
			continue
		}
		if firstLine(filepath.Join(directory, entry.Name())) == result.Header {
			stale = append(stale, entry.Name())
		}
	}
	slices.Sort(stale)
	return stale, nil
}

// firstLine reads the first line of a file, where a generated file carries its header.
//
// Takes path (string) which is the file.
//
// Returns string which is the first line, or empty when the file cannot be read.
func firstLine(path string) string {
	file, err := os.Open(path) //nolint:gosec // a file in the package directory
	if err != nil {
		return ""
	}
	defer func() { _ = file.Close() }()
	scanner := bufio.NewScanner(file)
	scanner.Scan()
	return scanner.Text()
}

Choosing the dialect. dialect_registry.New("sqlite", dialect_registry.Options{}) builds a dialect from its name, which is how a tool that reads its own configuration picks one. A program that only ever targets SQLite could import seagull.sh/seagull/sdk/dialect/dialect_engine_sqlite and call dialect_engine_sqlite.New() instead. An unknown name is an error that lists the names it does know.

Building the generator. seagull.New takes the dialect as its first argument, so forgetting it is a compile error, and every other setting as an option with a default. WithTypeOverrides is the Go form of the override in sgl.yaml: the column, and the package and name of the Go type.

Generating. Generate reads the SQL through seagull.Input. Migrations is any fs.FS, here the working directory, so an embed.FS or an in-memory fstest.MapFS work as well. Queries is nil, which means "the same file system as the migrations", and the two directory fields are paths inside it.

Errors and diagnostics. The two are kept apart. Generate returns an error only when the run cannot go on: a file it cannot read, a limit exceeded, a cancelled context. Problems in your SQL come back as result.Diagnostics, each with a file, position, severity, code and message, and the program prints each one in the shape of the first line sgl prints for a diagnostic. result.Err() is non-nil when any of them is an error, and then the result holds no files.

Writing. seagull.WriteFiles writes the files into the directory, creating it if needed and replacing each file atomically. It also prunes: a file whose first line is the generated header, result.Header, but which this run did not produce, such as the file of a deleted query, is removed. Files without the header are never touched, so hand-written code can live beside the generated package.

Checking. In check mode the program writes nothing. outOfDate compares each generated file with the one on disk, and looks for files that carry the header but that WriteFiles would prune. Any difference means the committed package is stale.

What it generates

The override shows in the row struct: Settings is a *json.RawMessage, a pointer because the column can be NULL. A column override changes result rows only, so AddNote still takes the setting as a *string.

internal/store

type ListNotesRow struct {
	ID       int64            `json:"id"`
	Body     string           `json:"body"`
	Settings *json.RawMessage `json:"settings"`
}

func (queries *Queries) ListNotes(ctx context.Context) ([]ListNotesRow, error) {
	rows, err := queries.reader.QueryContext(ctx, listNotesSQL)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var results []ListNotesRow
	for rows.Next() {
		var row ListNotesRow
		if err := rows.Scan(&row.ID, &row.Body, sglScanOverride(&row.Settings)); err != nil {
			return nil, err
		}
		results = append(results, row)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return results, nil
}

What it prints

The example's test runs the check against an empty directory, then writes, then checks again:

out of date: driver_composites.go, driver_values.go, models.go, notes.sql.go, prepared.go, querier.go, value_reading.go
wrote 7 files for 2 queries
up to date

A second test runs the check against the committed internal/store, which is how a project using this program would guard its generated code in CI, as sgl generate --check does in Seagull in CI.

The library and sgl agree

The committed package is the same whether go run . or sgl generate wrote it, and one setting makes that so. The library's database/sql emitter writes prepared.go unless told otherwise, while sgl's go.prepared setting defaults to false. This sgl.yaml sets prepared: true, so both write the file. A program that wants the other default passes seagull.WithEmitter(emit_driver_databasesql.New(emit_driver_databasesql.WithPrepared(false))).

Embedding Seagull covers every option of New and WriteFiles, and the package documentation has the full API.

Run it locally

From the directory you cloned the Seagull repository into:

cd seagull/examples/embedding && go run ./046_library_generate

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

cd seagull/examples/embedding && go test ./046_library_generate

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: notes
    dialect: sqlite
    migrations: db/migrations
    queries: db/queries
    go:
      package: store
      out: internal/store
      prepared: true
      types:
        overrides:
          - column: notes.settings
            go: encoding/json.RawMessage

db/migrations/0001_init.up.sql

CREATE TABLE notes (
    id       INTEGER PRIMARY KEY,
    body     TEXT    NOT NULL,
    settings TEXT
);

db/migrations/0001_init.down.sql

DROP TABLE notes;

db/queries/notes.sql

-- sgl.query(AddNote, exec)
INSERT INTO notes (body, settings) VALUES (?, ?);

-- sgl.query(ListNotes, many)
SELECT id, body, settings FROM notes ORDER BY id;

example.go

package main

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"seagull.sh/seagull"
	"seagull.sh/seagull/sdk/dialect/dialect_registry"
)

// run generates the store package from the project's migrations and queries, as a build
// tool that embeds Seagull does. It writes the package into directory or, when check is
// set, writes nothing and reports whether directory is out of date.
//
// Takes project (fs.FS) which holds db/migrations and db/queries.
// Takes directory (string) which is the package directory, such as internal/store.
// Takes check (bool) which compares the generated files with directory instead of writing
// them.
// Takes output (io.Writer) which receives the diagnostics and a summary.
//
// Returns bool which is true when check found directory out of date.
// Returns error when generation fails or a query has an error diagnostic.
func run(ctx context.Context, project fs.FS, directory string, check bool, output io.Writer) (bool, error) {
	sqlite, err := dialect_registry.New("sqlite", dialect_registry.Options{})
	if err != nil {
		return false, err
	}
	generator, err := seagull.New(sqlite, seagull.WithTypeOverrides(seagull.TypeOverride{
		SQLTypeName: "",
		Column:      "notes.settings",
		GoPackage:   "encoding/json",
		GoName:      "RawMessage",
	}))
	if err != nil {
		return false, fmt.Errorf("configuring the generator: %w", err)
	}

	result, err := generator.Generate(ctx, seagull.Input{
		Migrations:   project,
		Queries:      nil,
		Package:      "store",
		MigrationDir: "db/migrations",
		QueryDir:     "db/queries",
	})
	if err != nil {
		return false, err
	}
	for _, diagnostic := range result.Diagnostics {
		fmt.Fprintf(output, "%s:%d:%d: %s[%s]: %s\n", diagnostic.Filename, diagnostic.Line, diagnostic.Column,
			diagnostic.Severity, diagnostic.Code, diagnostic.Message)
	}
	if err := result.Err(); err != nil {
		return false, err
	}

	if check {
		stale, err := outOfDate(result, directory)
		if err != nil {
			return false, err
		}
		if len(stale) == 0 {
			fmt.Fprintln(output, "up to date")
			return false, nil
		}
		fmt.Fprintf(output, "out of date: %s\n", strings.Join(stale, ", "))
		return true, nil
	}

	if err := seagull.WriteFiles(directory, result); err != nil {
		return false, err
	}
	fmt.Fprintf(output, "wrote %d files for %d queries\n", len(result.Files), result.QueryCount)
	return false, nil
}

// outOfDate lists the files WriteFiles would change: a generated file that is missing or
// differs, and a file an earlier run generated that this run no longer does, such as the
// file of a deleted query, which WriteFiles would remove.
//
// Takes result (*seagull.Result) which is the generation to compare.
// Takes directory (string) which holds the package as it is now.
//
// Returns []string which holds the names of the files that would change, sorted.
// Returns error when the directory cannot be read.
func outOfDate(result *seagull.Result, directory string) ([]string, error) {
	var stale []string
	generated := make(map[string]bool, len(result.Files))
	for _, file := range result.Files {
		generated[file.Name] = true
		existing, err := os.ReadFile(filepath.Join(directory, file.Name)) //nolint:gosec // a file of the package directory
		if err != nil || !bytes.Equal(existing, file.Content) {
			stale = append(stale, file.Name)
		}
	}

	entries, err := os.ReadDir(directory)
	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}
	for _, entry := range entries {
		if generated[entry.Name()] || entry.IsDir() {
			continue
		}
		if firstLine(filepath.Join(directory, entry.Name())) == result.Header {
			stale = append(stale, entry.Name())
		}
	}
	slices.Sort(stale)
	return stale, nil
}

// firstLine reads the first line of a file, where a generated file carries its header.
//
// Takes path (string) which is the file.
//
// Returns string which is the first line, or empty when the file cannot be read.
func firstLine(path string) string {
	file, err := os.Open(path) //nolint:gosec // a file in the package directory
	if err != nil {
		return ""
	}
	defer func() { _ = file.Close() }()
	scanner := bufio.NewScanner(file)
	scanner.Scan()
	return scanner.Text()
}

main.go

// Command 046_library_generate generates this example's store package from Go, without
// sgl: it is what a build tool or framework does when it embeds Seagull.
//
// Run it from this directory with `go run .` to write internal/store, or `go run .
// -check` to write nothing and exit 1 when internal/store is out of date.
package main

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

// outputDirectory is where the generated package is written.
const outputDirectory = "internal/store"

func main() {
	check := flag.Bool("check", false, "exit 1 when internal/store is out of date; write nothing")
	flag.Parse()

	stale, err := run(context.Background(), os.DirFS("."), outputDirectory, *check, os.Stdout)
	if err != nil {
		log.Fatal(err)
	}
	if stale {
		os.Exit(1)
	}
}

internal/store/driver_composites.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"database/sql"
	"database/sql/driver"
	"encoding"
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
)

func sglScanOverride[T any](dest *T) sql.Scanner {
	return sglOverrideScanner[T]{dest: dest}
}

type sglOverrideScanner[T any] struct {
	dest *T
}

func (scanner sglOverrideScanner[T]) Scan(source any) error {
	target := reflect.ValueOf(scanner.dest).Elem()
	if source == nil {
		return sglReadDriverNull(target)
	}
	var read sql.Null[T]
	err := read.Scan(source)
	if err == nil {
		*scanner.dest = read.V
		return nil
	}
	if sglReadOverride(target, source) != nil {
		return err
	}
	return nil
}

func sglReadOverride(target reflect.Value, source any) error {
	if target.Kind() == reflect.Pointer {
		element := reflect.New(target.Type().Elem())
		if err := sglReadOverride(element.Elem(), source); err != nil {
			return err
		}
		target.Set(element)
		return nil
	}
	if read, err := sglReadOverrideText(target, source); read {
		return err
	}
	if target.Kind() == reflect.Bool {
		converted, err := driver.Bool.ConvertValue(source)
		if err != nil {
			return err
		}
		isTrue, isBool := converted.(bool)
		target.SetBool(isBool && isTrue)
		return nil
	}
	return sglReadDriverValue(target, source)
}

func sglReadOverrideText(target reflect.Value, source any) (bool, error) {
	var text []byte
	switch typed := source.(type) {
	case encoding.TextMarshaler:
		marshalled, err := typed.MarshalText()
		if err != nil {
			return false, nil
		}
		text = marshalled
	case string:
		text = []byte(typed)
	case []byte:
		text = typed
	default:
		return false, nil
	}
	if unmarshaler, unmarshals := reflect.TypeAssert[encoding.TextUnmarshaler](target.Addr()); unmarshals {
		return true, unmarshaler.UnmarshalText(text)
	}
	switch {
	case target.Kind() == reflect.String:
		target.SetString(string(text))
		return true, nil
	case target.Kind() == reflect.Slice && target.Type().Elem().Kind() == reflect.Uint8:
		target.SetBytes(append([]byte(nil), text...))
		return true, nil
	default:
		return false, nil
	}
}

func sglReadDriverValue(target reflect.Value, source any) error {
	if source == nil {
		return sglReadDriverNull(target)
	}
	value := reflect.ValueOf(source)
	if value.Type().AssignableTo(target.Type()) {
		target.Set(value)
		return nil
	}
	if target.Kind() == reflect.Pointer {
		element := reflect.New(target.Type().Elem())
		if err := sglReadDriverValue(element.Elem(), source); err != nil {
			return err
		}
		target.Set(element)
		return nil
	}
	if read, err := sglReadDriverLeaf(target, source); read {
		return err
	}
	return sglReadDriverShape(target, value)
}

func sglReadDriverNull(target reflect.Value) error {
	switch target.Kind() {
	case reflect.Pointer, reflect.Slice, reflect.Map, reflect.Interface:
		target.SetZero()
		return nil
	}
	if scanner, scans := reflect.TypeAssert[sql.Scanner](target.Addr()); scans {
		return scanner.Scan(nil)
	}
	return fmt.Errorf("converting NULL to %s is unsupported", target.Type())
}

func sglReadDriverShape(target reflect.Value, value reflect.Value) error {
	switch target.Kind() {
	case reflect.Slice:
		return sglReadList(target, value, sglReadDriverValue)
	case reflect.Map:
		return sglReadMap(target, value, sglReadDriverValue)
	case reflect.Struct:
		return sglReadDriverStruct(target, value)
	default:
		return sglReadScalar(target, value)
	}
}

func sglReadDriverLeaf(target reflect.Value, source any) (bool, error) {
	address := target.Addr().Interface()
	if unmarshaler, ok := address.(json.Unmarshaler); ok {
		encoded, err := json.Marshal(source)
		if err != nil {
			return true, err
		}
		return true, unmarshaler.UnmarshalJSON(encoded)
	}
	if scanner, ok := address.(sql.Scanner); ok {
		return true, scanner.Scan(source)
	}
	return false, nil
}

func sglReadDriverStruct(target reflect.Value, value reflect.Value) error {
	source := value.Interface()
	fields, ok := source.(map[string]any)
	if !ok {
		return sglUnreadable(value.Type(), target.Type())
	}
	target.SetZero()
	structType := target.Type()
	for index := range structType.NumField() {
		name, readable := sglDriverFieldName(structType.Field(index))
		if !readable {
			continue
		}
		element, found := sglDriverField(fields, name)
		if !found {
			continue
		}
		if err := sglReadDriverValue(target.Field(index), element); err != nil {
			return fmt.Errorf("field %s: %w", name, err)
		}
	}
	return nil
}

func sglDriverFieldName(field reflect.StructField) (string, bool) {
	if !field.IsExported() {
		return "", false
	}
	name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
	switch name {
	case "-":
		return "", false
	case "":
		return field.Name, true
	default:
		return name, true
	}
}

func sglDriverField(fields map[string]any, name string) (any, bool) {
	if element, found := fields[name]; found {
		return element, true
	}
	for key, element := range fields {
		if strings.EqualFold(key, name) {
			return element, true
		}
	}
	return nil, false
}

internal/store/driver_values.go

// Code generated by sgl. DO NOT EDIT.

package store

import "database/sql/driver"

type sglDriverText struct {
	value any
}

func (wrapped sglDriverText) Value() (driver.Value, error) {
	converted, err := driver.DefaultParameterConverter.ConvertValue(wrapped.value)
	if text, isBytes := converted.([]byte); isBytes && err == nil {
		return string(text), nil
	}
	return converted, err
}

internal/store/models.go

// Code generated by sgl. DO NOT EDIT.

package store

import "encoding/json"

type Notes struct {
	ID       int64            `json:"id"`
	Body     string           `json:"body"`
	Settings *json.RawMessage `json:"settings"`
}

internal/store/notes.sql.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"context"
	"encoding/json"
)

const addNoteSQL = `INSERT INTO notes (body, settings) VALUES (?1, ?2);`

type AddNoteParams struct {
	Body     string
	Settings *json.RawMessage
}

func (queries *Queries) AddNote(ctx context.Context, params AddNoteParams) error {
	_, err := queries.writer.ExecContext(ctx, addNoteSQL, params.Body, sglDriverText{value: params.Settings})
	return err
}

const listNotesSQL = `SELECT id, body, settings FROM notes ORDER BY id;`

type ListNotesRow struct {
	ID       int64            `json:"id"`
	Body     string           `json:"body"`
	Settings *json.RawMessage `json:"settings"`
}

func (queries *Queries) ListNotes(ctx context.Context) ([]ListNotesRow, error) {
	rows, err := queries.reader.QueryContext(ctx, listNotesSQL)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var results []ListNotesRow
	for rows.Next() {
		var row ListNotesRow
		if err := rows.Scan(&row.ID, &row.Body, sglScanOverride(&row.Settings)); err != nil {
			return nil, err
		}
		results = append(results, row)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return results, nil
}

internal/store/prepared.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sync"
)

type PreparedDBTX struct {
	db    *sql.DB
	stmts map[string]*sql.Stmt
	mu    sync.RWMutex
}

func Prepare(ctx context.Context, db *sql.DB) (*PreparedDBTX, error) {
	staticQueries := []string{addNoteSQL, listNotesSQL}
	stmts := make(map[string]*sql.Stmt, len(staticQueries))
	for _, query := range staticQueries {
		statement, err := db.PrepareContext(ctx, query)
		if err != nil {
			return nil, errors.Join(fmt.Errorf("preparing statement: %w", err), sglCloseStatements(stmts))
		}
		stmts[query] = statement
	}
	return &PreparedDBTX{db: db, stmts: stmts, mu: sync.RWMutex{}}, nil
}

func (prepared *PreparedDBTX) cachedStatement(query string) *sql.Stmt {
	prepared.mu.RLock()
	defer prepared.mu.RUnlock()
	return prepared.stmts[query]
}

func (prepared *PreparedDBTX) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {
	statement := prepared.cachedStatement(query)
	if statement == nil {
		return prepared.db.ExecContext(ctx, query, arguments...)
	}
	return statement.ExecContext(ctx, arguments...)
}

func (prepared *PreparedDBTX) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {
	statement := prepared.cachedStatement(query)
	if statement == nil {
		return prepared.db.QueryContext(ctx, query, arguments...)
	}
	return statement.QueryContext(ctx, arguments...)
}

func (prepared *PreparedDBTX) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {
	statement := prepared.cachedStatement(query)
	if statement == nil {
		return prepared.db.QueryRowContext(ctx, query, arguments...)
	}
	return statement.QueryRowContext(ctx, arguments...)
}

func (prepared *PreparedDBTX) BeginTx(ctx context.Context, options *sql.TxOptions) (*sql.Tx, error) {
	return prepared.db.BeginTx(ctx, options)
}

func (prepared *PreparedDBTX) PingContext(ctx context.Context) error {
	return prepared.db.PingContext(ctx)
}

func (prepared *PreparedDBTX) UnderlyingDB() *sql.DB {
	return prepared.db
}

func (prepared *PreparedDBTX) Close() error {
	prepared.mu.Lock()
	defer prepared.mu.Unlock()
	err := sglCloseStatements(prepared.stmts)
	prepared.stmts = make(map[string]*sql.Stmt)
	return err
}

func (prepared *PreparedDBTX) WrapDBTX(inner any) any {
	transaction, ok := inner.(*sql.Tx)
	if !ok {
		return inner
	}
	return &preparedTx{source: prepared, transaction: transaction}
}

type preparedTx struct {
	source      *PreparedDBTX
	transaction *sql.Tx
}

func (bound *preparedTx) ExecContext(ctx context.Context, query string, arguments ...any) (sql.Result, error) {
	statement := bound.source.cachedStatement(query)
	if statement == nil {
		return bound.transaction.ExecContext(ctx, query, arguments...)
	}
	return bound.transaction.StmtContext(ctx, statement).ExecContext(ctx, arguments...)
}

func (bound *preparedTx) QueryContext(ctx context.Context, query string, arguments ...any) (*sql.Rows, error) {
	statement := bound.source.cachedStatement(query)
	if statement == nil {
		return bound.transaction.QueryContext(ctx, query, arguments...)
	}
	return bound.transaction.StmtContext(ctx, statement).QueryContext(ctx, arguments...)
}

func (bound *preparedTx) QueryRowContext(ctx context.Context, query string, arguments ...any) *sql.Row {
	statement := bound.source.cachedStatement(query)
	if statement == nil {
		return bound.transaction.QueryRowContext(ctx, query, arguments...)
	}
	return bound.transaction.StmtContext(ctx, statement).QueryRowContext(ctx, arguments...)
}

func sglCloseStatements(stmts map[string]*sql.Stmt) error {
	var closeErrors []error
	for _, statement := range stmts {
		if err := statement.Close(); err != nil {
			closeErrors = append(closeErrors, err)
		}
	}
	return errors.Join(closeErrors...)
}

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/value_reading.go

// Code generated by sgl. DO NOT EDIT.

package store

import (
	"fmt"
	"reflect"
)

type sglOrderedMap interface {
	Keys() []any
	Values() []any
}

func sglReadList(target reflect.Value, value reflect.Value, read func(reflect.Value, any) error) error {
	if value.Kind() != reflect.Slice && value.Kind() != reflect.Array {
		return sglUnreadable(value.Type(), target.Type())
	}
	if value.Kind() == reflect.Slice && value.IsNil() {
		target.SetZero()
		return nil
	}
	list := reflect.MakeSlice(target.Type(), value.Len(), value.Len())
	for index := range value.Len() {
		if err := read(list.Index(index), value.Index(index).Interface()); err != nil {
			return fmt.Errorf("element %d: %w", index, err)
		}
	}
	target.Set(list)
	return nil
}

func sglReadMap(target reflect.Value, value reflect.Value, read func(reflect.Value, any) error) error {
	if value.Kind() == reflect.Map && value.IsNil() {
		target.SetZero()
		return nil
	}
	keys, values, ok := sglMapEntries(value)
	if !ok {
		return sglUnreadable(value.Type(), target.Type())
	}
	converted := reflect.MakeMapWithSize(target.Type(), len(keys))
	for index, key := range keys {
		convertedKey := reflect.New(target.Type().Key()).Elem()
		if err := read(convertedKey, key); err != nil {
			return fmt.Errorf("key %v: %w", key, err)
		}
		element := reflect.New(target.Type().Elem()).Elem()
		if err := read(element, values[index]); err != nil {
			return fmt.Errorf("value of key %v: %w", key, err)
		}
		converted.SetMapIndex(convertedKey, element)
	}
	target.Set(converted)
	return nil
}

func sglMapEntries(value reflect.Value) (keys []any, values []any, ok bool) {
	if value.Kind() == reflect.Map {
		keys = make([]any, 0, value.Len())
		values = make([]any, 0, value.Len())
		entries := value.MapRange()
		for entries.Next() {
			keys = append(keys, entries.Key().Interface())
			values = append(values, entries.Value().Interface())
		}
		return keys, values, true
	}
	pointer := reflect.New(value.Type())
	pointer.Elem().Set(value)
	ordered, ordersEntries := reflect.TypeAssert[sglOrderedMap](pointer)
	if !ordersEntries {
		return nil, nil, false
	}
	keys, values = ordered.Keys(), ordered.Values()
	return keys, values, len(keys) == len(values)
}

func sglReadScalar(target reflect.Value, value reflect.Value) error {
	kind := sglScalarKind(value.Kind())
	if kind == "" || kind != sglScalarKind(target.Kind()) || !value.CanConvert(target.Type()) {
		return sglUnreadable(value.Type(), target.Type())
	}
	converted := value.Convert(target.Type())
	if !sglScalarHolds(converted, value) {
		return fmt.Errorf("%v overflows %s", value.Interface(), target.Type())
	}
	target.Set(converted)
	return nil
}

func sglScalarHolds(converted reflect.Value, value reflect.Value) bool {
	switch {
	case !value.CanInt() && !value.CanUint():
		return true
	case !converted.Convert(value.Type()).Equal(value):
		return false
	case value.CanInt() && converted.CanUint():
		return value.Int() >= 0
	case value.CanUint() && converted.CanInt():
		return converted.Int() >= 0
	default:
		return true
	}
}

func sglScalarKind(kind reflect.Kind) string {
	switch kind {
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return "integer"
	case reflect.Float32, reflect.Float64:
		return "float"
	case reflect.String:
		return "string"
	case reflect.Bool:
		return "boolean"
	default:
		return ""
	}
}

func sglUnreadable(source reflect.Type, target reflect.Type) error {
	return fmt.Errorf("cannot read %s into %s", source, target)
}

output.txt

out of date: driver_composites.go, driver_values.go, models.go, notes.sql.go, prepared.go, querier.go, value_reading.go
wrote 7 files for 2 queries
up to date