White-label presets
On this page
On SQLite:
- the preset starts from DefaultPreset; sgl becomes a foreign prefix, so an unconverted directive draws Q051
- helpers become acme..., sentinel errors ErrAcme..., and error text starts acme:
- query_names.go was renamed acme_query_names.go, so only WithLegacyHeaders lets WriteFiles prune the old file
- the acme package is written to a temporary directory, since its header is not sgl's
A framework or code generator that embeds Seagull may not want its users to see Seagull at all. Its queries should say acme.query, not sgl.query; its generated files should say they were generated by Acme; its helpers and errors should carry Acme's name. A seagull.Preset holds all of that vocabulary in one value, and seagull.WithPreset hands it to the generator. This example moves an existing sgl project to a fictional tool called Acme, from its query files to the generated package on disk.
The project
The queries take a Go slice and build a filter at run time, chosen because both make Seagull generate helper code, and helper names are part of the vocabulary:
db/queries/parcels.sql
-- sgl.query(ListParcelsByStatus, many)
-- ?1 as sgl.param(statuses, kind: slice)
SELECT id, recipient, status FROM parcels WHERE status IN (?) ORDER BY id;
-- sgl.query(SearchParcels, many, dynamic: runtime)
SELECT id, recipient, status, weight_grams FROM parcels
sgl.yaml turns on query_names, so the package also has a file mapping each SQL string to its query name. That file's name and function are part of the preset too.
The preset
acmePreset starts from seagull.DefaultPreset(), the vocabulary sgl uses, and replaces each part:
Directivesmakesacmethe canonical prefix with no aliases. It also listssglasForeign: another tool's prefix, so a directive nobody converted draws a Q051 warning instead of being silently ignored.Headeris the first line of every generated file.WriteFilesuses it to recognise the files it may prune.HelperPrefixandErrorPrefixname the generated helpers and errors.QueryNamesrenames the query-names file and its function.
seagull.New checks the preset and refuses one it cannot use, such as a prefix of the wrong shape, with an error wrapping ErrInvalidConfig.
The program
example.go
package main
import (
"bufio"
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"slices"
"strings"
"testing/fstest"
"seagull.sh/seagull"
"seagull.sh/seagull/sdk/dialect/dialect_engine_sqlite"
)
var (
// acmeIdentifier matches the helper and error names the acme preset produces.
acmeIdentifier = regexp.MustCompile(`\b(?:acme|ErrAcme)[A-Za-z]+`)
// stockVocabulary matches any trace of the stock vocabulary.
stockVocabulary = regexp.MustCompile(`(?i)sgl`)
)
// acmePreset is the vocabulary of Acme's code generator, which embeds Seagull: its own
// directive prefix, generated-file header, helper and error prefixes and query-names
// file. sgl becomes a foreign prefix, so a directive nobody converted is reported with
// Q051 rather than ignored.
//
// Returns seagull.Preset which is the acme preset.
func acmePreset() seagull.Preset {
preset := seagull.DefaultPreset()
preset.Directives = seagull.Directives{
Canonical: "acme",
Aliases: nil,
Foreign: []string{"sgl"},
RequireCanonical: false,
}
preset.Header = "// Code generated by acme-gen. DO NOT EDIT."
preset.HelperPrefix = "acme"
preset.ErrorPrefix = "acme"
preset.QueryNames = seagull.QueryNames{Enabled: true, File: "acme_query_names.go", Function: "QueryName"}
return preset
}
// run moves a store package from the stock vocabulary to Acme's. It generates the package
// as sgl would into directory, rewrites the query files to the acme prefix with
// FormatDirectives, generates again with the acme preset, checks the result never says
// sgl, and writes it over the old package, pruning the files the old header marked.
//
// Takes project (fs.FS) which holds db/migrations and db/queries, written for sgl.
// Takes directory (string) which receives the generated package.
// Takes output (io.Writer) which receives each step.
//
// Returns error when a step fails.
func run(ctx context.Context, project fs.FS, directory string, output io.Writer) error {
stock := seagull.DefaultPreset()
stock.QueryNames.Enabled = true
before, err := generate(ctx, &stock, project)
if err != nil {
return err
}
if err := seagull.WriteFiles(directory, before); err != nil {
return err
}
fmt.Fprintln(output, "generated with the stock preset:")
if err := list(output, directory); err != nil {
return err
}
converted, err := convert(project, output)
if err != nil {
return err
}
after, err := generate(ctx, new(acmePreset()), converted)
if err != nil {
return err
}
var helpers []string
for _, file := range after.Files {
if stockVocabulary.Match(file.Content) {
return fmt.Errorf("%s still mentions sgl", file.Name)
}
helpers = append(helpers, acmeIdentifier.FindAllString(string(file.Content), -1)...)
}
slices.Sort(helpers)
fmt.Fprintf(output, "\nno file of the acme generation mentions sgl; its helpers are\n %s\n",
strings.Join(slices.Compact(helpers), "\n "))
if err := seagull.WriteFiles(directory, after, seagull.WithLegacyHeaders(stock.Header)); err != nil {
return err
}
fmt.Fprintln(output, "\nwritten with the acme preset, pruning files with the stock header:")
return list(output, directory)
}
// generate runs the generator with a preset over a project.
//
// Takes preset (*seagull.Preset) which is the vocabulary.
// 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, preset *seagull.Preset, project fs.FS) (*seagull.Result, error) {
generator, err := seagull.New(dialect_engine_sqlite.New(), seagull.WithPreset(*preset))
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
}
// convert rewrites the directives of every query file from sgl to acme, as Acme's tool
// would when a project adopts it, and prints the first converted file.
//
// Takes project (fs.FS) which holds the sgl project.
// Takes output (io.Writer) which receives the converted file.
//
// Returns fs.FS which holds the migrations unchanged and the converted queries.
// Returns error when a file cannot be read or formatted.
func convert(project fs.FS, output io.Writer) (fs.FS, error) {
converted := fstest.MapFS{}
err := fs.WalkDir(project, "db", func(name string, entry fs.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return err
}
content, err := fs.ReadFile(project, name)
if err != nil {
return err
}
if path.Dir(name) == "db/queries" {
if content, err = seagull.FormatDirectives(content, "acme", "sgl"); err != nil {
return err
}
fmt.Fprintf(output, "\n%s, rewritten with FormatDirectives:\n%s", name, content)
}
converted[name] = &fstest.MapFile{Data: content}
return nil
})
return converted, err
}
// list prints each file in directory with its first line, the header that marks a
// generated file.
//
// Takes output (io.Writer) which receives the listing.
// Takes directory (string) which is the package directory.
//
// Returns error when the directory cannot be read.
func list(output io.Writer, directory string) error {
entries, err := os.ReadDir(directory)
if err != nil {
return err
}
for _, entry := range entries {
file, err := os.Open(filepath.Join(directory, entry.Name())) //nolint:gosec // a file of the package directory
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
scanner.Scan()
_ = file.Close()
fmt.Fprintf(output, " %-22s %s\n", entry.Name(), scanner.Text())
}
return nil
}
run does the move in four steps.
It generates with the stock preset, as sgl would for this sgl.yaml, and writes the package. Every file starts with sgl's header.
It converts the queries with seagull.FormatDirectives(content, "acme", "sgl"): the first prefix is the canonical one to write, and the rest are the prefixes to rewrite to it. This is the function behind sgl fmt. It changes only directives, never the SQL, and running it twice changes nothing more.
It generates again with the Acme preset, from the converted queries, and checks that no generated file mentions sgl in any case. It collects the helper and error names to print.
It writes over the old package with seagull.WithLegacyHeaders(stock.Header). Most files keep their names and are simply replaced. The query-names file does not: it is now acme_query_names.go, so the old query_names.go is left over, and it carries a header the Acme preset does not use. Naming the old header as a legacy one tells WriteFiles to prune files that carry it too.
What it prints
generated with the stock preset:
bind_limits.go // Code generated by sgl. DO NOT EDIT.
models.go // Code generated by sgl. DO NOT EDIT.
parcels.sql.go // Code generated by sgl. DO NOT EDIT.
prepared.go // Code generated by sgl. DO NOT EDIT.
querier.go // Code generated by sgl. DO NOT EDIT.
query_names.go // Code generated by sgl. DO NOT EDIT.
runtime_helpers.go // Code generated by sgl. DO NOT EDIT.
slice_helpers.go // Code generated by sgl. DO NOT EDIT.
db/queries/parcels.sql, rewritten with FormatDirectives:
-- acme.query(ListParcelsByStatus, many)
-- ?1 as acme.param(statuses, kind: slice)
SELECT id, recipient, status FROM parcels WHERE status IN (?) ORDER BY id;
-- acme.query(SearchParcels, many, dynamic: runtime)
SELECT id, recipient, status, weight_grams FROM parcels
no file of the acme generation mentions sgl; its helpers are
ErrAcmeTooManyBindVariables
ErrAcmeUnknownColumn
ErrAcmeUnknownDirection
ErrAcmeUnknownOperator
acmeAllowedDirections
acmeAllowedOperators
acmeAt
acmeBoxSlice
acmeCastAfter
acmeCastBefore
acmeCastMiddle
acmeCastPatterns
acmeCastTypes
acmeCheckBinds
acmeCloseStatements
acmeColumnExpressionRegex
acmeEdit
acmeEmptyExclusion
acmeEmptyMembership
acmeExtractColumnRoot
acmeFilter
acmeFilterCount
acmeFilterInto
acmeIsMultiOperator
acmeIsUnaryOperator
acmeLimitAfter
acmeLimitBefore
acmeList
acmeListMark
acmeMaxBindVariables
acmeMaxColumnExpressionLength
acmeMultiValueLen
acmeNewOrder
acmeNewPredicate
acmeNormaliseDirection
acmeOffsetAfter
acmeOffsetBefore
acmeOrder
acmePagination
acmePaginationCount
acmePaginationInto
acmePredicate
acmeReflectSlice
acmeResolveColumn
acmeSimpleColumn
acmeSplice
acmeSplitCast
acmeUnboundedLimit
acmeWhereClose
acmeWhereOpen
acmeWriteBindPlaceholder
written with the acme preset, pruning files with the stock header:
acme_query_names.go // Code generated by acme-gen. DO NOT EDIT.
bind_limits.go // Code generated by acme-gen. DO NOT EDIT.
models.go // Code generated by acme-gen. DO NOT EDIT.
parcels.sql.go // Code generated by acme-gen. DO NOT EDIT.
prepared.go // Code generated by acme-gen. DO NOT EDIT.
querier.go // Code generated by acme-gen. DO NOT EDIT.
runtime_helpers.go // Code generated by acme-gen. DO NOT EDIT.
slice_helpers.go // Code generated by acme-gen. DO NOT EDIT.
The first listing is the package sgl would write. Then comes the converted query file, with acme. directives and the SQL untouched. The helper names follow the preset: helpers start acme, unexported errors errAcme and exported ones ErrAcme, such as ErrAcmeUnknownColumn, which the run-time filter builder returns for a column it does not allow. The last listing has Acme's header on every file and no query_names.go: the legacy header let it go.
sgl offers the same to a project arriving from another generator: list that generator's header under go.legacy_headers in sgl.yaml, and sgl generate prunes its old files as well. Embedding Seagull describes every field of a preset, including the type slots that replace whole categories of Go types.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/embedding && go run ./049_presets
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/embedding && go test ./049_presets
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: parcels
dialect: sqlite
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
query_names:
enabled: true
db/migrations/0001_init.up.sql
CREATE TABLE parcels (
id INTEGER PRIMARY KEY,
recipient TEXT NOT NULL,
status TEXT NOT NULL,
weight_grams INTEGER NOT NULL
);
db/migrations/0001_init.down.sql
DROP TABLE parcels;
db/queries/parcels.sql
-- sgl.query(ListParcelsByStatus, many)
-- ?1 as sgl.param(statuses, kind: slice)
SELECT id, recipient, status FROM parcels WHERE status IN (?) ORDER BY id;
-- sgl.query(SearchParcels, many, dynamic: runtime)
SELECT id, recipient, status, weight_grams FROM parcels
example.go
package main
import (
"bufio"
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"slices"
"strings"
"testing/fstest"
"seagull.sh/seagull"
"seagull.sh/seagull/sdk/dialect/dialect_engine_sqlite"
)
var (
// acmeIdentifier matches the helper and error names the acme preset produces.
acmeIdentifier = regexp.MustCompile(`\b(?:acme|ErrAcme)[A-Za-z]+`)
// stockVocabulary matches any trace of the stock vocabulary.
stockVocabulary = regexp.MustCompile(`(?i)sgl`)
)
// acmePreset is the vocabulary of Acme's code generator, which embeds Seagull: its own
// directive prefix, generated-file header, helper and error prefixes and query-names
// file. sgl becomes a foreign prefix, so a directive nobody converted is reported with
// Q051 rather than ignored.
//
// Returns seagull.Preset which is the acme preset.
func acmePreset() seagull.Preset {
preset := seagull.DefaultPreset()
preset.Directives = seagull.Directives{
Canonical: "acme",
Aliases: nil,
Foreign: []string{"sgl"},
RequireCanonical: false,
}
preset.Header = "// Code generated by acme-gen. DO NOT EDIT."
preset.HelperPrefix = "acme"
preset.ErrorPrefix = "acme"
preset.QueryNames = seagull.QueryNames{Enabled: true, File: "acme_query_names.go", Function: "QueryName"}
return preset
}
// run moves a store package from the stock vocabulary to Acme's. It generates the package
// as sgl would into directory, rewrites the query files to the acme prefix with
// FormatDirectives, generates again with the acme preset, checks the result never says
// sgl, and writes it over the old package, pruning the files the old header marked.
//
// Takes project (fs.FS) which holds db/migrations and db/queries, written for sgl.
// Takes directory (string) which receives the generated package.
// Takes output (io.Writer) which receives each step.
//
// Returns error when a step fails.
func run(ctx context.Context, project fs.FS, directory string, output io.Writer) error {
stock := seagull.DefaultPreset()
stock.QueryNames.Enabled = true
before, err := generate(ctx, &stock, project)
if err != nil {
return err
}
if err := seagull.WriteFiles(directory, before); err != nil {
return err
}
fmt.Fprintln(output, "generated with the stock preset:")
if err := list(output, directory); err != nil {
return err
}
converted, err := convert(project, output)
if err != nil {
return err
}
after, err := generate(ctx, new(acmePreset()), converted)
if err != nil {
return err
}
var helpers []string
for _, file := range after.Files {
if stockVocabulary.Match(file.Content) {
return fmt.Errorf("%s still mentions sgl", file.Name)
}
helpers = append(helpers, acmeIdentifier.FindAllString(string(file.Content), -1)...)
}
slices.Sort(helpers)
fmt.Fprintf(output, "\nno file of the acme generation mentions sgl; its helpers are\n %s\n",
strings.Join(slices.Compact(helpers), "\n "))
if err := seagull.WriteFiles(directory, after, seagull.WithLegacyHeaders(stock.Header)); err != nil {
return err
}
fmt.Fprintln(output, "\nwritten with the acme preset, pruning files with the stock header:")
return list(output, directory)
}
// generate runs the generator with a preset over a project.
//
// Takes preset (*seagull.Preset) which is the vocabulary.
// 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, preset *seagull.Preset, project fs.FS) (*seagull.Result, error) {
generator, err := seagull.New(dialect_engine_sqlite.New(), seagull.WithPreset(*preset))
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
}
// convert rewrites the directives of every query file from sgl to acme, as Acme's tool
// would when a project adopts it, and prints the first converted file.
//
// Takes project (fs.FS) which holds the sgl project.
// Takes output (io.Writer) which receives the converted file.
//
// Returns fs.FS which holds the migrations unchanged and the converted queries.
// Returns error when a file cannot be read or formatted.
func convert(project fs.FS, output io.Writer) (fs.FS, error) {
converted := fstest.MapFS{}
err := fs.WalkDir(project, "db", func(name string, entry fs.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return err
}
content, err := fs.ReadFile(project, name)
if err != nil {
return err
}
if path.Dir(name) == "db/queries" {
if content, err = seagull.FormatDirectives(content, "acme", "sgl"); err != nil {
return err
}
fmt.Fprintf(output, "\n%s, rewritten with FormatDirectives:\n%s", name, content)
}
converted[name] = &fstest.MapFile{Data: content}
return nil
})
return converted, err
}
// list prints each file in directory with its first line, the header that marks a
// generated file.
//
// Takes output (io.Writer) which receives the listing.
// Takes directory (string) which is the package directory.
//
// Returns error when the directory cannot be read.
func list(output io.Writer, directory string) error {
entries, err := os.ReadDir(directory)
if err != nil {
return err
}
for _, entry := range entries {
file, err := os.Open(filepath.Join(directory, entry.Name())) //nolint:gosec // a file of the package directory
if err != nil {
return err
}
scanner := bufio.NewScanner(file)
scanner.Scan()
_ = file.Close()
fmt.Fprintf(output, " %-22s %s\n", entry.Name(), scanner.Text())
}
return nil
}
main.go
// Command 049_presets white-labels Seagull: it generates this example's store package
// with the stock vocabulary, converts the queries to the acme. prefix, and generates
// again with Acme's preset, its own header, helper names and query-names file.
//
// Run it with `go run .`; it writes into a temporary directory, or into the directory
// given with -out.
package main
import (
"context"
"flag"
"log"
"os"
)
func main() {
out := flag.String("out", "", "directory to write the generated package into; a temporary one when empty")
flag.Parse()
if err := execute(context.Background(), *out); err != nil {
log.Fatal(err)
}
}
// execute runs the example, writing into directory or a temporary one when it is empty.
//
// Takes directory (string) which is the output directory, or empty.
//
// Returns error when the example fails.
func execute(ctx context.Context, directory string) error {
if directory == "" {
temporary, err := os.MkdirTemp("", "seagull-example-")
if err != nil {
return err
}
defer func() { _ = os.RemoveAll(temporary) }()
directory = temporary
}
return run(ctx, os.DirFS("."), directory, os.Stdout)
}
output.txt
generated with the stock preset:
bind_limits.go // Code generated by sgl. DO NOT EDIT.
models.go // Code generated by sgl. DO NOT EDIT.
parcels.sql.go // Code generated by sgl. DO NOT EDIT.
prepared.go // Code generated by sgl. DO NOT EDIT.
querier.go // Code generated by sgl. DO NOT EDIT.
query_names.go // Code generated by sgl. DO NOT EDIT.
runtime_helpers.go // Code generated by sgl. DO NOT EDIT.
slice_helpers.go // Code generated by sgl. DO NOT EDIT.
db/queries/parcels.sql, rewritten with FormatDirectives:
-- acme.query(ListParcelsByStatus, many)
-- ?1 as acme.param(statuses, kind: slice)
SELECT id, recipient, status FROM parcels WHERE status IN (?) ORDER BY id;
-- acme.query(SearchParcels, many, dynamic: runtime)
SELECT id, recipient, status, weight_grams FROM parcels
no file of the acme generation mentions sgl; its helpers are
ErrAcmeTooManyBindVariables
ErrAcmeUnknownColumn
ErrAcmeUnknownDirection
ErrAcmeUnknownOperator
acmeAllowedDirections
acmeAllowedOperators
acmeAt
acmeBoxSlice
acmeCastAfter
acmeCastBefore
acmeCastMiddle
acmeCastPatterns
acmeCastTypes
acmeCheckBinds
acmeCloseStatements
acmeColumnExpressionRegex
acmeEdit
acmeEmptyExclusion
acmeEmptyMembership
acmeExtractColumnRoot
acmeFilter
acmeFilterCount
acmeFilterInto
acmeIsMultiOperator
acmeIsUnaryOperator
acmeLimitAfter
acmeLimitBefore
acmeList
acmeListMark
acmeMaxBindVariables
acmeMaxColumnExpressionLength
acmeMultiValueLen
acmeNewOrder
acmeNewPredicate
acmeNormaliseDirection
acmeOffsetAfter
acmeOffsetBefore
acmeOrder
acmePagination
acmePaginationCount
acmePaginationInto
acmePredicate
acmeReflectSlice
acmeResolveColumn
acmeSimpleColumn
acmeSplice
acmeSplitCast
acmeUnboundedLimit
acmeWhereClose
acmeWhereOpen
acmeWriteBindPlaceholder
written with the acme preset, pruning files with the stock header:
acme_query_names.go // Code generated by acme-gen. DO NOT EDIT.
bind_limits.go // Code generated by acme-gen. DO NOT EDIT.
models.go // Code generated by acme-gen. DO NOT EDIT.
parcels.sql.go // Code generated by acme-gen. DO NOT EDIT.
prepared.go // Code generated by acme-gen. DO NOT EDIT.
querier.go // Code generated by acme-gen. DO NOT EDIT.
runtime_helpers.go // Code generated by acme-gen. DO NOT EDIT.
slice_helpers.go // Code generated by acme-gen. DO NOT EDIT.