sgl vet and diagnostics
On this page
On SQLite:
- warnings exit 0 unless --strict; errors always exit 1
- -c broken/sgl.yaml points sgl at the second project; paths in its output stay relative to where sgl runs
- an error stops only its own query: sgl generate leaves FindTasks out, writes the rest of the package and exits 1
sgl vet runs the same checks as sgl generate, every query against the schema your migrations build, and writes nothing. It is the command for an editor, a pre-commit hook or a CI job: quick, safe to run at any time, and clear about what is wrong. Every problem it finds is a diagnostic with a stable code, and sgl explain turns that code into a full explanation.
This example has two small projects. The main one vets with a warning; the one in broken/ has an error. The session runs sgl vet on each, then looks a code up.
The project
The main project is a tasks table with three queries. The last of them says it returns one row, but an UPDATE without RETURNING returns none:
db/queries/tasks.sql
-- sgl.query(FinishTask, one)
UPDATE tasks SET done = 1 WHERE id = ?;
This is a warning, Q009, not an error: Seagull still generates FinishTask, but its row struct is empty and every call would end in sql.ErrNoRows, even when the update worked. The fix is to give the query a RETURNING clause, or to say what it really does with exec or execrows.
The project in broken/ has the same schema and one query, with a keyword argument of sgl.param misspelt:
broken/db/queries/tasks.sql
-- sgl.query(FindTasks, many)
-- :title as sgl.param(title, optinal: true)
SELECT id, title, done FROM tasks WHERE (:title IS NULL OR title = :title) ORDER BY id;
-- sgl.query(ListTasks, many)
SELECT id, title, done FROM tasks ORDER BY id;
Seagull cannot guess what an unknown argument was meant to do, so this is an error, Q027. While any query has an error, sgl generate writes no code at all for that database.
The session
sgl vet checks every database in sgl.yaml and prints each diagnostic in one format: the file, line and column, then the severity and code in the form warning[Q009], then the message. Below it is the source line with a caret under the place it is about, and a last line counts errors, warnings and hints. The project has a warning and no error, so sgl vet exits 0.
sgl vet --strict prints the same report but exits 1, because --strict treats warnings as findings. Use it in CI when you want a warning to fail the build rather than scroll past in a log.
sgl -c broken/sgl.yaml vet points sgl at the other project's configuration with the global -c flag. The caret now spans the misspelt word, ^^^^^^^ under optinal, and a help: line suggests the keyword sgl expected. An error fails the run with or without --strict.
sgl explain Q027 prints the code's title and severity, what causes it and how to fix it. Every code also has a page on this site, such as Q027, and sgl explain with no code lists them all.
Here is the session as recorded. Each $ line is a command, the lines after it are what it printed, and # exit N is its exit status.
# The project vets with one warning: FinishTask is declared one, but an UPDATE without
# RETURNING produces no row to return. Warnings do not fail the run.
$ sgl vet
db/queries/tasks.sql:7:1: warning[Q009]: query "FinishTask" uses command "one" but produces no output columns
|
7 | -- sgl.query(FinishTask, one)
| ^
0 error(s), 1 warning(s), 0 hint(s)
# exit 0
# --strict treats warnings as findings, so CI can refuse them.
$ sgl vet --strict
db/queries/tasks.sql:7:1: warning[Q009]: query "FinishTask" uses command "one" but produces no output columns
|
7 | -- sgl.query(FinishTask, one)
| ^
0 error(s), 1 warning(s), 0 hint(s)
# exit 1
# broken/ misspells a keyword argument. The caret marks the span and help suggests the
# keyword sgl expected. An error always fails the run.
$ sgl -c broken/sgl.yaml vet
broken/db/queries/tasks.sql:2:31: error[Q027]: unknown keyword argument "optinal" on sgl.param
|
2 | -- :title as sgl.param(title, optinal: true)
| ^^^^^^^
help: did you mean "optional"?
1 error(s), 0 warning(s), 0 hint(s)
# exit 1
# An error stops only the query it belongs to. sgl generate still writes the rest of
# broken/'s package, ListTasks and the model, so code that does not call FindTasks keeps
# building, and exits 1, so CI fails until the error is fixed.
$ sgl -c broken/sgl.yaml generate
broken/db/queries/tasks.sql:2:31: error[Q027]: unknown keyword argument "optinal" on sgl.param
|
2 | -- :title as sgl.param(title, optinal: true)
| ^^^^^^^
help: did you mean "optional"?
tasks: 2 queries, 3 files in broken/internal/store (3 changed)
sgl: database "tasks": 1 error(s); the queries with errors were left out and the rest written
# exit 1
$ cat broken/internal/store/tasks.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import "context"
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
}
# exit 0
# Every diagnostic has a code; sgl explain says what it means and how to fix it.
$ sgl explain Q027
Q027: Unknown keyword argument (error)
A directive is given a keyword argument it does not accept, such as `nullabel:` on `sgl.param`. The message suggests the closest accepted keyword when there is one.
Correct or remove the keyword argument.
# exit 0
Every command answers -h with its usage and flags, so sgl vet -h is the quickest way to see what else it takes. The CLI reference lists the exit codes: 0 for a clean run, 1 for findings, 2 for a usage mistake.
Try it
Change FinishTask to execrows and run sgl vet --strict again: the warning goes and the run exits 0. Then correct optinal in broken/ and vet that project too. Seagull in CI takes the same checks into a pipeline, with GitHub annotations and SARIF.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/tooling/041_vet_and_diagnostics && go run seagull.sh/seagull/cmd/sgl vet
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(FinishTask, one)
UPDATE tasks SET done = 1 WHERE id = ?;
broken/db/migrations/0001_init.up.sql
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0
);
broken/db/queries/tasks.sql
-- sgl.query(FindTasks, many)
-- :title as sgl.param(title, optinal: true)
SELECT id, title, done FROM tasks WHERE (:title IS NULL OR title = :title) ORDER BY id;
-- sgl.query(ListTasks, many)
SELECT id, title, done FROM tasks ORDER BY id;
broken/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
broken/db/migrations/0001_init.down.sql
DROP TABLE tasks;
transcript.txt
# The project vets with one warning: FinishTask is declared one, but an UPDATE without
# RETURNING produces no row to return. Warnings do not fail the run.
$ sgl vet
db/queries/tasks.sql:7:1: warning[Q009]: query "FinishTask" uses command "one" but produces no output columns
|
7 | -- sgl.query(FinishTask, one)
| ^
0 error(s), 1 warning(s), 0 hint(s)
# exit 0
# --strict treats warnings as findings, so CI can refuse them.
$ sgl vet --strict
db/queries/tasks.sql:7:1: warning[Q009]: query "FinishTask" uses command "one" but produces no output columns
|
7 | -- sgl.query(FinishTask, one)
| ^
0 error(s), 1 warning(s), 0 hint(s)
# exit 1
# broken/ misspells a keyword argument. The caret marks the span and help suggests the
# keyword sgl expected. An error always fails the run.
$ sgl -c broken/sgl.yaml vet
broken/db/queries/tasks.sql:2:31: error[Q027]: unknown keyword argument "optinal" on sgl.param
|
2 | -- :title as sgl.param(title, optinal: true)
| ^^^^^^^
help: did you mean "optional"?
1 error(s), 0 warning(s), 0 hint(s)
# exit 1
# An error stops only the query it belongs to. sgl generate still writes the rest of
# broken/'s package, ListTasks and the model, so code that does not call FindTasks keeps
# building, and exits 1, so CI fails until the error is fixed.
$ sgl -c broken/sgl.yaml generate
broken/db/queries/tasks.sql:2:31: error[Q027]: unknown keyword argument "optinal" on sgl.param
|
2 | -- :title as sgl.param(title, optinal: true)
| ^^^^^^^
help: did you mean "optional"?
tasks: 2 queries, 3 files in broken/internal/store (3 changed)
sgl: database "tasks": 1 error(s); the queries with errors were left out and the rest written
# exit 1
$ cat broken/internal/store/tasks.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import "context"
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
}
# exit 0
# Every diagnostic has a code; sgl explain says what it means and how to fix it.
$ sgl explain Q027
Q027: Unknown keyword argument (error)
A directive is given a keyword argument it does not accept, such as `nullabel:` on `sgl.param`. The message suggests the closest accepted keyword when there is one.
Correct or remove the keyword argument.
# exit 0