Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "bank"
version = "0.1.0"
edition = "2021"

[dependencies]
openssl = "0.10.41"
postgres-openssl = "0.5.0"
chrono = "0.4.22"

[dependencies.postgres]
version = "0.19.3"
features = [
"with-uuid-1"
]

[dependencies.uuid]
version = "1.1.2"
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// Runs op inside a transaction and retries it as needed.
/// On non-retryable failures, the transaction is aborted and
/// rolled back; on success, the transaction is committed.
fn execute_txn<T, F>(client: &mut Client, op: F) -> Result<T, Error>
where
F: Fn(&mut Transaction) -> Result<T, Error>,
{
let mut txn = client.transaction()?;
loop {
// Set a retry savepoint
// See https://www.cockroachlabs.com/docs/stable/advanced-client-side-transaction-retries
let mut sp = txn.savepoint("cockroach_restart")?;
match op(&mut sp).and_then(|t| sp.commit().map(|_| t)) {
Err(ref err)
if err
.code()
.map(|e| *e == SqlState::T_R_SERIALIZATION_FAILURE)
.unwrap_or(false) => {}
r => break r,
}
}
.and_then(|t| txn.commit().map(|_| t))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
fn transfer_funds(txn: &mut Transaction, from: Uuid, to: Uuid, amount: i64) -> Result<(), Error> {
// Read the balance.
let from_balance: i64 = txn
.query_one("SELECT balance FROM accounts WHERE id = $1", &[&from])?
.get(0);

assert!(from_balance >= amount);

// Perform the transfer.
txn.execute(
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
&[&amount, &from],
)?;
txn.execute(
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
&[&amount, &to],
)?;
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"context"
"fmt"
"log"
"math/rand"
"os"
"time"

"github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm"
"github.com/google/uuid"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

// Account is our model, which corresponds to the "accounts" table
type Account struct {
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()"`
Balance int
}

// The `acctIDs` global variable tracks the random IDs generated by `addAccounts`
var acctIDs []uuid.UUID

// Insert new rows into the "accounts" table
// This function generates new UUIDs and random balances for each row, and
// then it appends the ID to the `acctIDs`, which other functions use to track the IDs
func addAccounts(db *gorm.DB, numRows int, transferAmount int) error {
log.Printf("Creating %d new accounts...", numRows)
for i := 0; i < numRows; i++ {
newID := uuid.New()
newBalance := rand.Intn(10000) + transferAmount
if err := db.Create(&Account{ID: newID, Balance: newBalance}).Error; err != nil {
return err
}
acctIDs = append(acctIDs, newID)
}
log.Println("Accounts created.")
return nil
}

// Transfer funds between accounts
// This function adds `amount` to the "balance" column of the row with the "id" column matching `toID`,
// and removes `amount` from the "balance" column of the row with the "id" column matching `fromID`
func transferFunds(db *gorm.DB, fromID uuid.UUID, toID uuid.UUID, amount int) error {
log.Printf("Transferring %d from account %s to account %s...", amount, fromID, toID)
var fromAccount Account
var toAccount Account

db.First(&fromAccount, fromID)
db.First(&toAccount, toID)

if fromAccount.Balance < amount {
return fmt.Errorf("account %s balance %d is lower than transfer amount %d", fromAccount.ID, fromAccount.Balance, amount)
}

fromAccount.Balance -= amount
toAccount.Balance += amount

if err := db.Save(&fromAccount).Error; err != nil {
return err
}
if err := db.Save(&toAccount).Error; err != nil {
return err
}
log.Println("Funds transferred.")
return nil
}

// Print IDs and balances for all rows in "accounts" table
func printBalances(db *gorm.DB) {
var accounts []Account
db.Find(&accounts)
fmt.Printf("Balance at '%s':\n", time.Now())
for _, account := range accounts {
fmt.Printf("%s %d\n", account.ID, account.Balance)
}
}

// Delete all rows in "accounts" table inserted by `main` (i.e., tracked by `acctIDs`)
func deleteAccounts(db *gorm.DB, accountIDs []uuid.UUID) error {
log.Println("Deleting accounts created...")
err := db.Where("id IN ?", accountIDs).Delete(Account{}).Error
if err != nil {
return err
}
log.Println("Accounts deleted.")
return nil
}

func main() {

db, err := gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")+"&application_name=$ docs_simplecrud_gorm"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}

// Automatically create the "accounts" table based on the `Account`
// model.
db.AutoMigrate(&Account{})

// The number of initial rows to insert
const numAccts int = 5

// The amount to be transferred between two accounts.
const transferAmt int = 100

// Insert `numAccts` rows into the "accounts" table.
// To handle potential transaction retry errors, we wrap the call
// to `addAccounts` in `crdbgorm.ExecuteTx`, a helper function for
// GORM which implements a retry loop
if err := crdbgorm.ExecuteTx(context.Background(), db, nil,
func(tx *gorm.DB) error {
return addAccounts(db, numAccts, transferAmt)
},
); err != nil {
// For information and reference documentation, see:
// https://www.cockroachlabs.com/docs/stable/error-handling-and-troubleshooting.html
fmt.Println(err)
}

// Print balances before transfer.
printBalances(db)

// Select two account IDs
fromID := acctIDs[0]
toID := acctIDs[0:][rand.Intn(len(acctIDs))]

// Transfer funds between accounts. To handle potential
// transaction retry errors, we wrap the call to `transferFunds`
// in `crdbgorm.ExecuteTx`
if err := crdbgorm.ExecuteTx(context.Background(), db, nil,
func(tx *gorm.DB) error {
return transferFunds(tx, fromID, toID, transferAmt)
},
); err != nil {
// For information and reference documentation, see:
// https://www.cockroachlabs.com/docs/stable/error-handling-and-troubleshooting.html
fmt.Println(err)
}

// Print balances after transfer to ensure that it worked.
printBalances(db)

// Delete all accounts created by the earlier call to `addAccounts`
// To handle potential transaction retry errors, we wrap the call
// to `deleteAccounts` in `crdbgorm.ExecuteTx`
if err := crdbgorm.ExecuteTx(context.Background(), db, nil,
func(tx *gorm.DB) error {
return deleteAccounts(db, acctIDs)
},
); err != nil {
// For information and reference documentation, see:
// https://www.cockroachlabs.com/docs/stable/error-handling-and-troubleshooting.html
fmt.Println(err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"context"
"log"
"os"

"github.com/cockroachdb/cockroach-go/v2/crdb/crdbpgxv5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)

func initTable(ctx context.Context, tx pgx.Tx) error {
// Dropping existing table if it exists
log.Println("Drop existing accounts table if necessary.")
if _, err := tx.Exec(ctx, "DROP TABLE IF EXISTS accounts"); err != nil {
return err
}

// Create the accounts table
log.Println("Creating accounts table.")
if _, err := tx.Exec(ctx,
"CREATE TABLE accounts (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), balance INT8)"); err != nil {
return err
}
return nil
}

func insertRows(ctx context.Context, tx pgx.Tx, accts [4]uuid.UUID) error {
// Insert four rows into the "accounts" table.
log.Println("Creating new rows...")
if _, err := tx.Exec(ctx,
"INSERT INTO accounts (id, balance) VALUES ($1, $2), ($3, $4), ($5, $6), ($7, $8)", accts[0], 250, accts[1], 100, accts[2], 500, accts[3], 300); err != nil {
return err
}
return nil
}

func printBalances(conn *pgx.Conn) error {
rows, err := conn.Query(context.Background(), "SELECT id, balance FROM accounts")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id uuid.UUID
var balance int
if err := rows.Scan(&id, &balance); err != nil {
log.Fatal(err)
}
log.Printf("%s: %d\n", id, balance)
}
return nil
}

func transferFunds(ctx context.Context, tx pgx.Tx, from uuid.UUID, to uuid.UUID, amount int) error {
// Read the balance.
var fromBalance int
if err := tx.QueryRow(ctx,
"SELECT balance FROM accounts WHERE id = $1", from).Scan(&fromBalance); err != nil {
return err
}

if fromBalance < amount {
log.Println("insufficient funds")
}

// Perform the transfer.
log.Printf("Transferring funds from account with ID %s to account with ID %s...", from, to)
if _, err := tx.Exec(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, from); err != nil {
return err
}
if _, err := tx.Exec(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to); err != nil {
return err
}
return nil
}

func deleteRows(ctx context.Context, tx pgx.Tx, one uuid.UUID, two uuid.UUID) error {
// Delete two rows into the "accounts" table.
log.Printf("Deleting rows with IDs %s and %s...", one, two)
if _, err := tx.Exec(ctx,
"DELETE FROM accounts WHERE id IN ($1, $2)", one, two); err != nil {
return err
}
return nil
}

func main() {
// Read in connection string
config, err := pgx.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
config.RuntimeParams["application_name"] = "$ docs_simplecrud_gopgx"
conn, err := pgx.ConnectConfig(context.Background(), config)
if err != nil {
log.Fatal(err)
}
defer conn.Close(context.Background())

// Set up table
err = crdbpgx.ExecuteTx(context.Background(), conn, pgx.TxOptions{}, func(tx pgx.Tx) error {
return initTable(context.Background(), tx)
})

// Insert initial rows
var accounts [4]uuid.UUID
for i := 0; i < len(accounts); i++ {
accounts[i] = uuid.New()
}

err = crdbpgx.ExecuteTx(context.Background(), conn, pgx.TxOptions{}, func(tx pgx.Tx) error {
return insertRows(context.Background(), tx, accounts)
})
if err == nil {
log.Println("New rows created.")
} else {
log.Fatal("error: ", err)
}

// Print out the balances
log.Println("Initial balances:")
printBalances(conn)

// Run a transfer
err = crdbpgx.ExecuteTx(context.Background(), conn, pgx.TxOptions{}, func(tx pgx.Tx) error {
return transferFunds(context.Background(), tx, accounts[2], accounts[1], 100)
})
if err == nil {
log.Println("Transfer successful.")
} else {
log.Fatal("error: ", err)
}

// Print out the balances
log.Println("Balances after transfer:")
printBalances(conn)

// Delete rows
err = crdbpgx.ExecuteTx(context.Background(), conn, pgx.TxOptions{}, func(tx pgx.Tx) error {
return deleteRows(context.Background(), tx, accounts[0], accounts[1])
})
if err == nil {
log.Println("Rows deleted.")
} else {
log.Fatal("error: ", err)
}

// Print out the balances
log.Println("Balances after deletion:")
printBalances(conn)
}
Loading
Loading