Transactions: PostgreSQL and SQLite
Every file of the PostgreSQL and SQLite versions side by side, with the lines that differ marked. 4 parameter or column types differ in Go.
Configuration
sgl.yamldiffers
PostgreSQL
SQLite
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: bank4
- name: bank5
dialect: postgres5
dialect: sqlite6
migrations: db/migrations6
migrations: db/migrations7
queries: db/queries7
queries: db/queries8
go:8
go:9
package: store9
package: store10
out: internal/store10
out: internal/store11
driver: database/sql12
interface:13
enabled: true14
migrate:15
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_accounts.up.sqldiffers
PostgreSQL
SQLite
1
CREATE TABLE accounts (1
CREATE TABLE accounts (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id INTEGER PRIMARY KEY,3
owner TEXT NOT NULL,3
owner TEXT NOT NULL,4
balance INTEGER NOT NULL CHECK (balance >= 0)4
balance INTEGER NOT NULL CHECK (balance >= 0)5
);5
);db/migrations/0001_accounts.down.sqlidentical
PostgreSQL
SQLite
1
DROP TABLE accounts;1
DROP TABLE accounts;Queries
OpenAccountdiffers
PostgreSQL · db/queries/accounts.sql
SQLite · db/queries/accounts.sql
1
-- sgl.query(OpenAccount, one)1
-- sgl.query(OpenAccount, one)2
INSERT INTO accounts (owner, balance) VALUES ($1, $2) RETURNING id;2
INSERT INTO accounts (owner, balance) VALUES (?, ?) RETURNING id;Depositidentical
PostgreSQL · db/queries/accounts.sql
SQLite · db/queries/accounts.sql
1
-- sgl.query(Deposit, exec)1
-- sgl.query(Deposit, exec)2
UPDATE accounts SET balance = balance + :amount WHERE id = :id;2
UPDATE accounts SET balance = balance + :amount WHERE id = :id;Withdrawidentical
PostgreSQL · db/queries/accounts.sql
SQLite · db/queries/accounts.sql
1
-- sgl.query(Withdraw, exec)1
-- sgl.query(Withdraw, exec)2
UPDATE accounts SET balance = balance - :amount WHERE id = :id;2
UPDATE accounts SET balance = balance - :amount WHERE id = :id;ListAccountsidentical
PostgreSQL · db/queries/accounts.sql
SQLite · db/queries/accounts.sql
1
-- sgl.query(ListAccounts, many)1
-- sgl.query(ListAccounts, many)2
SELECT id, owner, balance FROM accounts ORDER BY id;2
SELECT id, owner, balance FROM accounts ORDER BY id;What sgl generates
OpenAccountdiffers
PostgreSQL
SQLite
1
func (queries *Queries) OpenAccount(ctx context.Context, params OpenAccountParams) (OpenAccountRow, error)1
func (queries *Queries) OpenAccount(ctx context.Context, params OpenAccountParams) (OpenAccountRow, error)2
2
3
type OpenAccountParams struct {3
type OpenAccountParams struct {4
Owner string4
Owner string5
Balance int325
Balance int646
}6
}7
7
8
type OpenAccountRow struct {8
type OpenAccountRow struct {9
ID int64 `json:"id"`9
ID int64 `json:"id"`10
}10
}Depositdiffers
PostgreSQL
SQLite
1
func (queries *Queries) Deposit(ctx context.Context, params DepositParams) error1
func (queries *Queries) Deposit(ctx context.Context, params DepositParams) error2
2
3
type DepositParams struct {3
type DepositParams struct {4
Amount int324
Amount int645
ID int645
ID int646
}6
}Withdrawdiffers
PostgreSQL
SQLite
1
func (queries *Queries) Withdraw(ctx context.Context, params WithdrawParams) error1
func (queries *Queries) Withdraw(ctx context.Context, params WithdrawParams) error2
2
3
type WithdrawParams struct {3
type WithdrawParams struct {4
Amount int324
Amount int645
ID int645
ID int646
}6
}ListAccountsdiffers
PostgreSQL
SQLite
1
func (queries *Queries) ListAccounts(ctx context.Context) ([]ListAccountsRow, error)1
func (queries *Queries) ListAccounts(ctx context.Context) ([]ListAccountsRow, error)2
2
3
type ListAccountsRow struct {3
type ListAccountsRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Owner string `json:"owner"`5
Owner string `json:"owner"`6
Balance int32 `json:"balance"`6
Balance int64 `json:"balance"`7
}7
}The program
example.godiffers
PostgreSQL
SQLite
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"errors"6
"errors"7
"fmt"7
"fmt"8
"io"8
"io"9
9
10
"seagull.sh/seagull/examples/postgres/023_transactions/internal/store"10
"seagull.sh/seagull/examples/sqlite/023_transactions/internal/store"11
)11
)12
12
13
// sqlStateError is a server error that carries a SQLSTATE code, as pgx's *pgconn.PgError14
// does.15
type sqlStateError interface {16
error17
18
// SQLState returns the five-character SQLSTATE code.19
SQLState() string20
}21
22
// run opens two accounts in a transaction it manages itself, then makes two transfers13
// run opens two accounts in a transaction it manages itself, then makes two transfers23
// with RunInTx: one commits, and one breaks the balance check part-way and rolls back.14
// with RunInTx: one commits, and one breaks the balance check part-way and rolls back.24
//15
//25
// sgl.yaml sets go.interface, so the generated package also declares store.Querier, an26
// interface of every query method, which *store.Queries implements whether it runs on the27
// database or inside a transaction. printBalances takes a store.Querier, so the same28
// function reads the committed balances and, inside each transfer, the balances the29
// transaction sees before it commits or rolls back; a test could pass it a fake instead.30
//31
// Takes database (*sql.DB) which is a migrated database.16
// Takes database (*sql.DB) which is a migrated database.32
// Takes output (io.Writer) which receives each step and the balances.17
// Takes output (io.Writer) which receives each step and the balances.33
//18
//34
// Returns error when a query fails other than as the example expects.19
// Returns error when a query fails other than as the example expects.35
func run(ctx context.Context, database *sql.DB, output io.Writer) error {20
func run(ctx context.Context, database *sql.DB, output io.Writer) error {36
queries := store.New(database)21
queries := store.New(database)37
22
38
pip, gull, err := openAccounts(ctx, database, queries)23
pip, gull, err := openAccounts(ctx, database, queries)39
if err != nil {24
if err != nil {40
return fmt.Errorf("opening accounts: %w", err)25
return fmt.Errorf("opening accounts: %w", err)41
}26
}42
fmt.Fprintln(output, "opened accounts for Pip and Gull: committed")27
fmt.Fprintln(output, "opened accounts for Pip and Gull: committed")43
if err := printBalances(ctx, queries, output, "balances"); err != nil {28
if err := printBalances(ctx, queries, output); err != nil {44
return err29
return err45
}30
}46
31
47
if err := transfer(ctx, database, queries, output, pip, gull, 1500); err != nil {32
if err := transfer(ctx, database, queries, pip, gull, 1500); err != nil {48
return fmt.Errorf("first transfer: %w", err)33
return fmt.Errorf("first transfer: %w", err)49
}34
}50
fmt.Fprintln(output, "transferred 1500 from Pip to Gull: committed")35
fmt.Fprintln(output, "transferred 1500 from Pip to Gull: committed")51
if err := printBalances(ctx, queries, output, "balances"); err != nil {36
if err := printBalances(ctx, queries, output); err != nil {52
return err37
return err53
}38
}54
39
55
// Gull holds 3500, so this transfer credits Pip and then fails the CHECK constraint when40
// Gull holds 3500, so this transfer credits Pip and then fails the CHECK constraint when56
// it debits Gull. RunInTx rolls back, and the credit to Pip that had already run is41
// it debits Gull. RunInTx rolls back, and the credit to Pip that had already run is57
// undone with it.42
// undone with it.58
err = transfer(ctx, database, queries, output, gull, pip, 9000)43
err = transfer(ctx, database, queries, gull, pip, 9000)59
if err == nil {44
if err == nil {60
return errors.New("an overdrawing transfer committed")45
return errors.New("an overdrawing transfer committed")61
}46
}62
47
fmt.Fprintf(output, "transferred 9000 from Gull to Pip: rolled back (%v)\n", err)63
// pgx reports a server error as a *pgconn.PgError, whose SQLState method gives the48
return printBalances(ctx, queries, output)64
// standard five-character code: 23514 for a failed CHECK constraint. Match on the code,65
// not the message, as the wording differs between servers and versions.66
failure, hasState := errors.AsType[sqlStateError](err)67
if !hasState {68
return fmt.Errorf("an overdrawing transfer failed without a SQLSTATE: %w", err)69
}70
fmt.Fprintf(output, "transferred 9000 from Gull to Pip: rolled back (SQLSTATE %s)\n", failure.SQLState())71
return printBalances(ctx, queries, output, "balances")72
}49
}73
50
74
// openAccounts opens both accounts in one transaction that it begins and commits itself.51
// openAccounts opens both accounts in one transaction that it begins and commits itself.75
// WithTx returns a copy of queries bound to that transaction, which suits a transaction52
// WithTx returns a copy of queries bound to that transaction, which suits a transaction76
// begun with options or shared with code that does not use Seagull.53
// begun with options or shared with code that does not use Seagull.77
//54
//78
// Takes database (*sql.DB) which begins the transaction.55
// Takes database (*sql.DB) which begins the transaction.79
// Takes queries (*store.Queries) which are bound to it.56
// Takes queries (*store.Queries) which are bound to it.80
//57
//81
// Returns pipID (int64) which is Pip's account ID.58
// Returns pipID (int64) which is Pip's account ID.82
// Returns gullID (int64) which is Gull's account ID.59
// Returns gullID (int64) which is Gull's account ID.83
// Returns err (error) when an account cannot be opened or the commit fails.60
// Returns err (error) when an account cannot be opened or the commit fails.84
func openAccounts(ctx context.Context, database *sql.DB, queries *store.Queries) (pipID, gullID int64, err error) {61
func openAccounts(ctx context.Context, database *sql.DB, queries *store.Queries) (pipID, gullID int64, err error) {85
transaction, err := database.BeginTx(ctx, nil)62
transaction, err := database.BeginTx(ctx, nil)86
if err != nil {63
if err != nil {87
return 0, 0, err64
return 0, 0, err88
}65
}89
// Rollback after a successful Commit does nothing, so deferring it covers every early66
// Rollback after a successful Commit does nothing, so deferring it covers every early90
// return.67
// return.91
defer func() { _ = transaction.Rollback() }()68
defer func() { _ = transaction.Rollback() }()92
69
93
inTransaction := queries.WithTx(transaction)70
inTransaction := queries.WithTx(transaction)94
pip, err := inTransaction.OpenAccount(ctx, store.OpenAccountParams{Owner: "Pip", Balance: 5000})71
pip, err := inTransaction.OpenAccount(ctx, store.OpenAccountParams{Owner: "Pip", Balance: 5000})95
if err != nil {72
if err != nil {96
return 0, 0, err73
return 0, 0, err97
}74
}98
gull, err := inTransaction.OpenAccount(ctx, store.OpenAccountParams{Owner: "Gull", Balance: 2000})75
gull, err := inTransaction.OpenAccount(ctx, store.OpenAccountParams{Owner: "Gull", Balance: 2000})99
if err != nil {76
if err != nil {100
return 0, 0, err77
return 0, 0, err101
}78
}102
return pip.ID, gull.ID, transaction.Commit()79
return pip.ID, gull.ID, transaction.Commit()103
}80
}104
81
105
// transfer moves an amount between two accounts with RunInTx, which begins a transaction,82
// transfer moves an amount between two accounts with RunInTx, which begins a transaction,106
// passes fn a Queries bound to it, and commits when fn returns nil or rolls back when it83
// passes fn a Queries bound to it, and commits when fn returns nil or rolls back when it107
// returns an error. After the credit it prints the balances the transaction sees, through84
// returns an error.108
// the same printBalances the caller uses.109
//85
//110
// Takes database (*sql.DB) which begins the transaction.86
// Takes database (*sql.DB) which begins the transaction.111
// Takes queries (*store.Queries) which run the transfer.87
// Takes queries (*store.Queries) which run the transfer.112
// Takes output (io.Writer) which receives the balances inside the transaction.113
// Takes from (int64) which is the account to debit.88
// Takes from (int64) which is the account to debit.114
// Takes to (int64) which is the account to credit.89
// Takes to (int64) which is the account to credit.115
// Takes amount (int32) which is the sum to move, in pence.90
// Takes amount (int64) which is the sum to move, in pence.116
//91
//117
// Returns error when either update fails; the transaction is then rolled back.92
// Returns error when either update fails; the transaction is then rolled back.118
func transfer(ctx context.Context, database *sql.DB, queries *store.Queries, output io.Writer, from, to int64, amount int32) error {93
func transfer(ctx context.Context, database *sql.DB, queries *store.Queries, from, to, amount int64) error {119
return queries.RunInTx(ctx, database, func(inTransaction *store.Queries) error {94
return queries.RunInTx(ctx, database, func(inTransaction *store.Queries) error {120
if err := inTransaction.Deposit(ctx, store.DepositParams{Amount: amount, ID: to}); err != nil {95
if err := inTransaction.Deposit(ctx, store.DepositParams{Amount: amount, ID: to}); err != nil {121
return err96
return err122
}97
}123
if err := printBalances(ctx, inTransaction, output, "inside the transaction"); err != nil {124
return err125
}126
return inTransaction.Withdraw(ctx, store.WithdrawParams{Amount: amount, ID: from})98
return inTransaction.Withdraw(ctx, store.WithdrawParams{Amount: amount, ID: from})127
})99
})128
}100
}129
101
130
// printBalances writes every account's balance on one line. It takes the generated102
// printBalances writes every account's balance on one line.131
// store.Querier rather than *store.Queries, so it reads through whatever it is given: the132
// queries on the database, the queries bound to a transaction, or a fake in a test.133
//103
//134
// Takes queries (store.Querier) which read the accounts.104
// Takes queries (*store.Queries) which read the accounts.135
// Takes output (io.Writer) which receives the line.105
// Takes output (io.Writer) which receives the line.136
// Takes label (string) which says whose view of the balances the line shows.137
//106
//138
// Returns error when the accounts cannot be listed.107
// Returns error when the accounts cannot be listed.139
func printBalances(ctx context.Context, queries store.Querier, output io.Writer, label string) error {108
func printBalances(ctx context.Context, queries *store.Queries, output io.Writer) error {140
accounts, err := queries.ListAccounts(ctx)109
accounts, err := queries.ListAccounts(ctx)141
if err != nil {110
if err != nil {142
return fmt.Errorf("listing accounts: %w", err)111
return fmt.Errorf("listing accounts: %w", err)143
}112
}144
fmt.Fprintf(output, " %s:", label)113
fmt.Fprint(output, " balances:")145
for _, account := range accounts {114
for _, account := range accounts {146
fmt.Fprintf(output, " %s %d", account.Owner, account.Balance)115
fmt.Fprintf(output, " %s %d", account.Owner, account.Balance)147
}116
}148
fmt.Fprintln(output)117
fmt.Fprintln(output)149
return nil118
return nil150
}119
}Go type differences
The same SQL types map to different Go types, because each database reports them differently.
| Query | Field | PostgreSQL | SQLite |
|---|---|---|---|
| OpenAccount | parameter balance | int32 | int64 |
| Deposit | parameter amount | int32 | int64 |
| Withdraw | parameter amount | int32 | int64 |
| ListAccounts | column balance | int32 | int64 |