Prepared statements
On this page
On SQLite:
- Prepare takes the *sql.DB; RunInTx takes it back through UnderlyingDB
- inside RunInTx the queries still use the prepared statements: PreparedDBTX's WrapDBTX binds each one to the *sql.Tx with StmtContext
- SQLite prepares a statement per connection, and database/sql re-prepares it on each pooled connection it uses
A prepared statement is parsed and planned by the database once, then run as often as you like with different values. With prepared: true under go: in sgl.yaml, sgl generates a Prepare function that prepares every static query in the package when the program starts. You get two things from it: a query run over and over skips the parsing each time, and a query that no longer fits the schema fails at start-up, where you will see it, rather than on the first request that happens to use it.
The example keeps a table of high-tide times for three harbours, loads three days of them, and looks each one up with the same query.
Turning it on
The option is one line in the go: section:
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: tides
dialect: sqlite
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
prepared: true
Nothing changes in the SQL. The queries are written exactly as they would be without it:
db/queries/tide_times.sql
-- sgl.query(HighTide, one)
SELECT high_at FROM tide_times WHERE port = ? AND day = ?;
What sgl generates
The generated methods do not change either: HighTide still passes its SQL text to QueryRowContext. What is new is a file, prepared.go, with a Prepare function and a PreparedDBTX type.
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{addTideTimeSQL, highTideSQL}
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...)
}
Prepare prepares each static query and keeps the statements in a map keyed by their SQL text. If any query fails to prepare, it closes the ones it has made and returns the error. PreparedDBTX has the same three methods as any DBTX, so you pass it to store.New in place of the *sql.DB. When a method runs a query, PreparedDBTX looks the text up and runs the prepared statement instead; text it did not prepare, such as a runtime builder's SQL, goes straight to the database. Close releases the statements.
The database holds prepared statements per connection, and database/sql keeps a pool of connections, so it prepares each statement again on every pooled connection it uses. That happens behind the *sql.Stmt, without any code of yours.
Transactions
RunInTx needs the *sql.DB itself to begin a transaction, and UnderlyingDB hands it back.
The transaction keeps the prepared statements. PreparedDBTX has a WrapDBTX method, which store.New notices and remembers. When RunInTx or WithTx binds the queries to a transaction, it passes the *sql.Tx through WrapDBTX, which returns a preparedTx: for each query it finds the prepared statement and binds it to the transaction with StmtContext, so the statement runs inside the transaction without being prepared again.
Using it
The program prepares the queries once, loads the tide times in a transaction, then runs HighTide nine times, once for each harbour and day.
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"seagull.sh/seagull/examples/sqlite/025_prepared_statements/internal/store"
)
const (
// days is how many days of tide times the example loads for each port.
days = 3
// minutesPerDay wraps tide times past midnight.
minutesPerDay = 24 * 60
// tideDrift is roughly how much later high tide comes each day, in minutes.
tideDrift = 50
)
// ports lists the harbours and the minute of their first high tide.
var ports = []struct {
name string
first int
}{
{name: "Padstow", first: 5*60 + 12},
{name: "Tenby", first: 6*60 + 40},
{name: "Whitby", first: 11*60 + 3},
}
// run prepares every query once, loads a few days of tide times in a transaction, then
// looks up each port's high tide for each day through the prepared statements.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives the tide table.
//
// Returns error when preparing or a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
// With go.prepared: true, sgl generates Prepare. It prepares every static query in the
// package up front, so a query that no longer matches the schema fails here, at
// start-up, rather than on first use. Close releases the statements.
prepared, err := store.Prepare(ctx, database)
if err != nil {
return fmt.Errorf("preparing queries: %w", err)
}
defer func() { _ = prepared.Close() }()
// *store.PreparedDBTX is a DBTX: every method of these Queries runs its prepared
// statement instead of sending the SQL text again.
queries := store.New(prepared)
// RunInTx needs the *sql.DB itself, which UnderlyingDB returns. PreparedDBTX also has a
// WrapDBTX method, so the Queries that RunInTx binds to the transaction keep using the
// prepared statements, each bound to the *sql.Tx with StmtContext.
err = queries.RunInTx(ctx, prepared.UnderlyingDB(), func(inTransaction *store.Queries) error {
return loadTideTimes(ctx, inTransaction)
})
if err != nil {
return fmt.Errorf("loading tide times: %w", err)
}
fmt.Fprint(output, " ")
for day := 1; day <= days; day++ {
fmt.Fprintf(output, " day %d", day)
}
fmt.Fprintln(output)
// The same HighTide statement runs once per port and day.
for _, port := range ports {
fmt.Fprintf(output, "%-9s", port.name)
for day := int64(1); day <= days; day++ {
tide, err := queries.HighTide(ctx, store.HighTideParams{Port: port.name, Day: day})
if err != nil {
return fmt.Errorf("looking up %s on day %d: %w", port.name, day, err)
}
fmt.Fprintf(output, " %s", tide.HighAt)
}
fmt.Fprintln(output)
}
return nil
}
// loadTideTimes adds a high tide for every port on every day, each a little later than
// the day before.
//
// Takes queries (*store.Queries) which add the rows.
//
// Returns error when a row cannot be added.
func loadTideTimes(ctx context.Context, queries *store.Queries) error {
for _, port := range ports {
for day := range days {
minute := (port.first + day*tideDrift) % minutesPerDay
err := queries.AddTideTime(ctx, store.AddTideTimeParams{
Port: port.name,
Day: int64(day + 1),
HighAt: fmt.Sprintf("%02d:%02d", minute/60, minute%60),
})
if err != nil {
return err
}
}
}
return nil
}
What it prints
day 1 day 2 day 3
Padstow 05:12 06:02 06:52
Tenby 06:40 07:30 08:20
Whitby 11:03 11:53 12:43
Try it
Remove prepared: true and run sgl generate. prepared.go is deleted and nothing else in the package changes, which shows that preparing is entirely a matter of the DBTX you pass to store.New. The program then stops compiling at store.Prepare; pass the *sql.DB to store.New directly and it runs as before, without prepared statements.
Across engines
HighTide has the same Go types on every engine that has this example.
CockroachDB and TimescaleDB run the PostgreSQL version unchanged.
Compare the whole example side by side with PostgreSQL.
Where the example is missing:
- Not on ClickHouse. ClickHouse prepares only INSERT batches.
Run it locally
From the directory you cloned the Seagull repository into:
cd seagull/examples/sqlite && go run ./025_prepared_statements
Its test runs it too, and checks what it prints against output.txt:
cd seagull/examples/sqlite && go test ./025_prepared_statements
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: tides
dialect: sqlite
migrations: db/migrations
queries: db/queries
go:
package: store
out: internal/store
prepared: true
db/migrations/0001_tide_times.up.sql
CREATE TABLE tide_times (
id INTEGER PRIMARY KEY,
port TEXT NOT NULL,
day INTEGER NOT NULL,
high_at TEXT NOT NULL,
UNIQUE (port, day)
);
db/migrations/0001_tide_times.down.sql
DROP TABLE tide_times;
db/queries/tide_times.sql
-- sgl.query(AddTideTime, exec)
INSERT INTO tide_times (port, day, high_at) VALUES (?, ?, ?);
-- sgl.query(HighTide, one)
SELECT high_at FROM tide_times WHERE port = ? AND day = ?;
example.go
package main
import (
"context"
"database/sql"
"fmt"
"io"
"seagull.sh/seagull/examples/sqlite/025_prepared_statements/internal/store"
)
const (
// days is how many days of tide times the example loads for each port.
days = 3
// minutesPerDay wraps tide times past midnight.
minutesPerDay = 24 * 60
// tideDrift is roughly how much later high tide comes each day, in minutes.
tideDrift = 50
)
// ports lists the harbours and the minute of their first high tide.
var ports = []struct {
name string
first int
}{
{name: "Padstow", first: 5*60 + 12},
{name: "Tenby", first: 6*60 + 40},
{name: "Whitby", first: 11*60 + 3},
}
// run prepares every query once, loads a few days of tide times in a transaction, then
// looks up each port's high tide for each day through the prepared statements.
//
// Takes database (*sql.DB) which is a migrated database.
// Takes output (io.Writer) which receives the tide table.
//
// Returns error when preparing or a query fails.
func run(ctx context.Context, database *sql.DB, output io.Writer) error {
// With go.prepared: true, sgl generates Prepare. It prepares every static query in the
// package up front, so a query that no longer matches the schema fails here, at
// start-up, rather than on first use. Close releases the statements.
prepared, err := store.Prepare(ctx, database)
if err != nil {
return fmt.Errorf("preparing queries: %w", err)
}
defer func() { _ = prepared.Close() }()
// *store.PreparedDBTX is a DBTX: every method of these Queries runs its prepared
// statement instead of sending the SQL text again.
queries := store.New(prepared)
// RunInTx needs the *sql.DB itself, which UnderlyingDB returns. PreparedDBTX also has a
// WrapDBTX method, so the Queries that RunInTx binds to the transaction keep using the
// prepared statements, each bound to the *sql.Tx with StmtContext.
err = queries.RunInTx(ctx, prepared.UnderlyingDB(), func(inTransaction *store.Queries) error {
return loadTideTimes(ctx, inTransaction)
})
if err != nil {
return fmt.Errorf("loading tide times: %w", err)
}
fmt.Fprint(output, " ")
for day := 1; day <= days; day++ {
fmt.Fprintf(output, " day %d", day)
}
fmt.Fprintln(output)
// The same HighTide statement runs once per port and day.
for _, port := range ports {
fmt.Fprintf(output, "%-9s", port.name)
for day := int64(1); day <= days; day++ {
tide, err := queries.HighTide(ctx, store.HighTideParams{Port: port.name, Day: day})
if err != nil {
return fmt.Errorf("looking up %s on day %d: %w", port.name, day, err)
}
fmt.Fprintf(output, " %s", tide.HighAt)
}
fmt.Fprintln(output)
}
return nil
}
// loadTideTimes adds a high tide for every port on every day, each a little later than
// the day before.
//
// Takes queries (*store.Queries) which add the rows.
//
// Returns error when a row cannot be added.
func loadTideTimes(ctx context.Context, queries *store.Queries) error {
for _, port := range ports {
for day := range days {
minute := (port.first + day*tideDrift) % minutesPerDay
err := queries.AddTideTime(ctx, store.AddTideTimeParams{
Port: port.name,
Day: int64(day + 1),
HighAt: fmt.Sprintf("%02d:%02d", minute/60, minute%60),
})
if err != nil {
return err
}
}
}
return nil
}
main.go
// Command 025_prepared_statements applies its migrations to a SQLite database, then runs
// the example in example.go: every query is prepared once with the generated Prepare, and
// the tide-table lookups reuse the prepared statements.
//
// Run it with `go run .`, or `go run . -db tides.db` to keep the database.
package main
import (
"context"
"database/sql"
"embed"
"flag"
"fmt"
"log"
"os"
"path/filepath"
_ "modernc.org/sqlite"
"seagull.sh/seagull/sdk/migrate"
"seagull.sh/seagull/sdk/migrate/migrate_backend_sqlite"
)
// migrations holds the migration files, compiled into the binary.
//
//go:embed db/migrations
var migrations embed.FS
func main() {
path := flag.String("db", "", "SQLite database file; a temporary one when empty")
flag.Parse()
if err := execute(context.Background(), *path); err != nil {
log.Fatal(err)
}
}
// execute opens and migrates the database at path, or a temporary one when path is empty,
// and runs the example against it.
//
// Takes path (string) which is the database file, or empty.
//
// Returns error when the database cannot be prepared or the example fails.
func execute(ctx context.Context, path string) error {
if path == "" {
directory, err := os.MkdirTemp("", "seagull-example-")
if err != nil {
return err
}
defer func() { _ = os.RemoveAll(directory) }()
path = filepath.Join(directory, "example.db")
}
database, err := sql.Open("sqlite", "file:"+path)
if err != nil {
return err
}
defer func() { _ = database.Close() }()
migrator, err := migrate.NewMigrator(database, migrate_backend_sqlite.New(), migrations, "db/migrations")
if err != nil {
return err
}
if _, err := migrator.Up(ctx); err != nil {
return fmt.Errorf("migrating: %w", err)
}
return run(ctx, database, os.Stdout)
}
internal/store/models.go
// Code generated by sgl. DO NOT EDIT.
package store
type TideTimes struct {
ID int64 `json:"id"`
Port string `json:"port"`
Day int64 `json:"day"`
HighAt string `json:"high_at"`
}
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{addTideTimeSQL, highTideSQL}
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/tide_times.sql.go
// Code generated by sgl. DO NOT EDIT.
package store
import "context"
const addTideTimeSQL = `INSERT INTO tide_times (port, day, high_at) VALUES (?1, ?2, ?3);`
type AddTideTimeParams struct {
Port string
Day int64
HighAt string
}
func (queries *Queries) AddTideTime(ctx context.Context, params AddTideTimeParams) error {
_, err := queries.writer.ExecContext(ctx, addTideTimeSQL, params.Port, params.Day, params.HighAt)
return err
}
const highTideSQL = `SELECT high_at FROM tide_times WHERE port = ?1 AND day = ?2;`
type HighTideParams struct {
Port string
Day int64
}
type HighTideRow struct {
HighAt string `json:"high_at"`
}
func (queries *Queries) HighTide(ctx context.Context, params HighTideParams) (HighTideRow, error) {
var row HighTideRow
err := queries.reader.QueryRowContext(ctx, highTideSQL, params.Port, params.Day).Scan(&row.HighAt)
if err != nil {
return HighTideRow{}, err
}
return row, nil
}
output.txt
day 1 day 2 day 3
Padstow 05:12 06:02 06:52
Tenby 06:40 07:30 08:20
Whitby 11:03 11:53 12:43