From 3a09c76014e3218c343976c18d1ed9c3f698a0c8 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:26:52 +0100 Subject: [PATCH 01/18] Batch example draft. --- examples/batch/batch.go | 59 ++++++++++++++++++++++++++++++++++++ examples/batch/batch_test.go | 41 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 examples/batch/batch.go create mode 100644 examples/batch/batch_test.go diff --git a/examples/batch/batch.go b/examples/batch/batch.go new file mode 100644 index 0000000..52d8e6b --- /dev/null +++ b/examples/batch/batch.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "errors" + + pgx "github.com/jackc/pgx/v5" + pgxpool "github.com/jackc/pgx/v5/pgxpool" +) + +type PgxIface interface { + Begin(context.Context) (pgx.Tx, error) + Close() +} + +func selectBatch(db PgxIface) (err error) { + tx, err := db.Begin(context.Background()) + if err != nil { + return + } + defer func() { + switch err { + case nil: + err = tx.Commit(context.Background()) + default: + _ = tx.Rollback(context.Background()) + } + }() + + batch := &pgx.Batch{} + batch.Queue("insert into ledger(description, amount) values($1, $2)", "q1", 1) + batch.Queue("insert into ledger(description, amount) values($1, $2)", "q2", 2) + batch.Queue("insert into ledger(description, amount) values($1, $2)", "q3", 3) + batch.Queue("select id, description, amount from ledger order by id") + batch.Queue("select id, description, amount from ledger order by id") + batch.Queue("select * from ledger where false") + batch.Queue("select sum(amount) from ledger") + + if br := tx.SendBatch(context.Background(), batch); br == nil { + return errors.New("SendBatch returns a NIL object") + } + + // TODO : call BatchResults.Exec method + // _, err = br.Exec() + return +} + +func main() { + // @NOTE: the real connection is not required for tests + db, err := pgxpool.New(context.Background(), "postgres://rolname@hostname/dbname") + if err != nil { + panic(err) + } + defer db.Close() + + if err = selectBatch(db); err != nil { + panic(err) + } +} diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go new file mode 100644 index 0000000..64b010c --- /dev/null +++ b/examples/batch/batch_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "github.com/pashagolub/pgxmock/v3" +) + +// a successful case +func TestShouldSelectRows(t *testing.T) { + mock, err := pgxmock.NewPool() + if err != nil { + t.Fatal(err) + } + defer mock.Close() + + // TODO +} + +// a failing test case +func TestShouldRollbackOnFailure(t *testing.T) { + mock, err := pgxmock.NewPool() + if err != nil { + t.Fatal(err) + } + defer mock.Close() + + mock.ExpectBegin() + // TODO: mock.ExpectBatch() + mock.ExpectRollback() + + // now we execute our method + if err = selectBatch(mock); err == nil { + t.Errorf("was expecting an error, but there was none") + } + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} From 8e2db2663a616b86122e3c1694cc9fa4087f67a3 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:38:36 +0100 Subject: [PATCH 02/18] Working example with pgx.Batch --- examples/batch/batch.go | 84 ++++++++++++++++++++++++++++++++---- examples/batch/batch_test.go | 2 +- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 52d8e6b..b25d842 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" pgx "github.com/jackc/pgx/v5" pgxpool "github.com/jackc/pgx/v5/pgxpool" @@ -13,10 +14,38 @@ type PgxIface interface { Close() } -func selectBatch(db PgxIface) (err error) { +func databaseSetup(db PgxIface) (err error) { tx, err := db.Begin(context.Background()) if err != nil { - return + return fmt.Errorf("databaseSetup: %s", err) + } + defer func() { + switch err { + case nil: + err = tx.Commit(context.Background()) + default: + _ = tx.Rollback(context.Background()) + } + }() + + // Create table + sql := `CREATE TABLE IF NOT EXISTS ledger ( + id SERIAL PRIMARY KEY, + description VARCHAR(255) NOT NULL, + amount INTEGER NOT NULL);` + + _, err = tx.Exec(context.Background(), sql) + if err != nil { + return fmt.Errorf("databaseSetup: %s", err) + } + + return +} + +func requestBatch(db PgxIface) (err error) { + tx, err := db.Begin(context.Background()) + if err != nil { + return fmt.Errorf("requestBatch: %s", err) } defer func() { switch err { @@ -32,28 +61,67 @@ func selectBatch(db PgxIface) (err error) { batch.Queue("insert into ledger(description, amount) values($1, $2)", "q2", 2) batch.Queue("insert into ledger(description, amount) values($1, $2)", "q3", 3) batch.Queue("select id, description, amount from ledger order by id") - batch.Queue("select id, description, amount from ledger order by id") + batch.Queue("select id, description, amount from ledger order by amount") batch.Queue("select * from ledger where false") batch.Queue("select sum(amount) from ledger") - if br := tx.SendBatch(context.Background(), batch); br == nil { + br := tx.SendBatch(context.Background(), batch) + if br == nil { return errors.New("SendBatch returns a NIL object") } + defer br.Close() + + _, err = br.Exec() + + if err != nil { + return fmt.Errorf("requestBatch: %s", err) + } + + return +} + +func databaseCleanup(db PgxIface) (err error) { + tx, err := db.Begin(context.Background()) + if err != nil { + return fmt.Errorf("databaseCleanup: %s", err) + } + defer func() { + switch err { + case nil: + err = tx.Commit(context.Background()) + default: + _ = tx.Rollback(context.Background()) + } + }() + + // Delete all rows in table + sql := `DELETE FROM ledger ;` + + _, err = tx.Exec(context.Background(), sql) + if err != nil { + return fmt.Errorf("databaseCleanup: %s", err) + } - // TODO : call BatchResults.Exec method - // _, err = br.Exec() return } func main() { // @NOTE: the real connection is not required for tests - db, err := pgxpool.New(context.Background(), "postgres://rolname@hostname/dbname") + db, err := pgxpool.New(context.Background(), "postgres://:@/") if err != nil { panic(err) } defer db.Close() - if err = selectBatch(db); err != nil { + if err = databaseSetup(db); err != nil { + panic(err) + } + + if err = requestBatch(db); err != nil { + panic(err) + } + + if err = databaseCleanup(db); err != nil { panic(err) } } diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go index 64b010c..30236bf 100644 --- a/examples/batch/batch_test.go +++ b/examples/batch/batch_test.go @@ -30,7 +30,7 @@ func TestShouldRollbackOnFailure(t *testing.T) { mock.ExpectRollback() // now we execute our method - if err = selectBatch(mock); err == nil { + if err = requestBatch(mock); err == nil { t.Errorf("was expecting an error, but there was none") } From d36e83e22357e7f331463f8d5f04dcd4ee74a884 Mon Sep 17 00:00:00 2001 From: "Giulio C.n" <57756052+giuliocn@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:23:38 +0100 Subject: [PATCH 03/18] Create README.md --- examples/batch/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 examples/batch/README.md diff --git a/examples/batch/README.md b/examples/batch/README.md new file mode 100644 index 0000000..6825b47 --- /dev/null +++ b/examples/batch/README.md @@ -0,0 +1,13 @@ +## PGX working example with PGX.batch + +These example is subdivided into three steps. +1. databaseSetup +2. requestBatch, where a list of sql queries is executed. +3. databaseCleanup + +## Video + + +https://github.com/giuliocn/pgxmock/assets/57756052/e41e5f5a-c4fc-40e8-8653-99d84a3468ff + + From 1762d12d9e17397139fbcb8ba2b7065d0cb09392 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Thu, 2 May 2024 18:44:49 +0200 Subject: [PATCH 04/18] Added batch example with comments. --- examples/batch/batch.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index b25d842..fefa4a3 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -15,10 +15,13 @@ type PgxIface interface { } func databaseSetup(db PgxIface) (err error) { + + // Initialize a database object tx, err := db.Begin(context.Background()) if err != nil { return fmt.Errorf("databaseSetup: %s", err) } + // Finally, commit changes or rollback defer func() { switch err { case nil: @@ -28,12 +31,13 @@ func databaseSetup(db PgxIface) (err error) { } }() - // Create table + // Create a new table 'ledger' sql := `CREATE TABLE IF NOT EXISTS ledger ( id SERIAL PRIMARY KEY, description VARCHAR(255) NOT NULL, amount INTEGER NOT NULL);` + // Execute SQL commands _, err = tx.Exec(context.Background(), sql) if err != nil { return fmt.Errorf("databaseSetup: %s", err) @@ -43,10 +47,13 @@ func databaseSetup(db PgxIface) (err error) { } func requestBatch(db PgxIface) (err error) { + + // Initialize a database object tx, err := db.Begin(context.Background()) if err != nil { return fmt.Errorf("requestBatch: %s", err) } + // Finally, commit changes or rollback defer func() { switch err { case nil: @@ -56,7 +63,10 @@ func requestBatch(db PgxIface) (err error) { } }() + // Create a Batch object batch := &pgx.Batch{} + + // Add SQL commands to queue batch.Queue("insert into ledger(description, amount) values($1, $2)", "q1", 1) batch.Queue("insert into ledger(description, amount) values($1, $2)", "q2", 2) batch.Queue("insert into ledger(description, amount) values($1, $2)", "q3", 3) @@ -65,14 +75,15 @@ func requestBatch(db PgxIface) (err error) { batch.Queue("select * from ledger where false") batch.Queue("select sum(amount) from ledger") + // Create a BatchRequest object br := tx.SendBatch(context.Background(), batch) if br == nil { return errors.New("SendBatch returns a NIL object") } defer br.Close() + // Execute a BatchRequest _, err = br.Exec() - if err != nil { return fmt.Errorf("requestBatch: %s", err) } @@ -81,10 +92,13 @@ func requestBatch(db PgxIface) (err error) { } func databaseCleanup(db PgxIface) (err error) { + + // Initialize a database object tx, err := db.Begin(context.Background()) if err != nil { return fmt.Errorf("databaseCleanup: %s", err) } + // Finally, commit changes or rollback defer func() { switch err { case nil: @@ -94,9 +108,10 @@ func databaseCleanup(db PgxIface) (err error) { } }() - // Delete all rows in table + // Delete all rows in a table sql := `DELETE FROM ledger ;` + // Execute SQL commands _, err = tx.Exec(context.Background(), sql) if err != nil { return fmt.Errorf("databaseCleanup: %s", err) @@ -106,6 +121,7 @@ func databaseCleanup(db PgxIface) (err error) { } func main() { + // @NOTE: the real connection is not required for tests db, err := pgxpool.New(context.Background(), "postgres://:@/") if err != nil { @@ -113,14 +129,17 @@ func main() { } defer db.Close() + // Create a database table if err = databaseSetup(db); err != nil { panic(err) } + // Create and send a batch request if err = requestBatch(db); err != nil { panic(err) } + // Delete all rows in table if err = databaseCleanup(db); err != nil { panic(err) } From 1a89fcec607ec2acb7632c1a48404b841303d0a8 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Fri, 3 May 2024 15:49:12 +0200 Subject: [PATCH 05/18] Resolved comments from author. --- examples/batch/batch.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index fefa4a3..480607a 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -33,9 +33,9 @@ func databaseSetup(db PgxIface) (err error) { // Create a new table 'ledger' sql := `CREATE TABLE IF NOT EXISTS ledger ( - id SERIAL PRIMARY KEY, - description VARCHAR(255) NOT NULL, - amount INTEGER NOT NULL);` + BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + description TEXT NOT NULL, + amount BIGINT NOT NULL);` // Execute SQL commands _, err = tx.Exec(context.Background(), sql) From b3539db1def2bd3bdc603086a11df32f130bc608 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Fri, 3 May 2024 15:53:05 +0200 Subject: [PATCH 06/18] Resolved comments from author. --- examples/batch/batch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 480607a..37718da 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -33,7 +33,7 @@ func databaseSetup(db PgxIface) (err error) { // Create a new table 'ledger' sql := `CREATE TABLE IF NOT EXISTS ledger ( - BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, description TEXT NOT NULL, amount BIGINT NOT NULL);` @@ -123,7 +123,7 @@ func databaseCleanup(db PgxIface) (err error) { func main() { // @NOTE: the real connection is not required for tests - db, err := pgxpool.New(context.Background(), "postgres://:@/") + db, err := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/postgres") if err != nil { panic(err) } From a36b504142e198067480ad139219bf6974523e5c Mon Sep 17 00:00:00 2001 From: "Giulio C.n" <57756052+giuliocn@users.noreply.github.com> Date: Thu, 2 May 2024 18:59:21 +0200 Subject: [PATCH 07/18] Update README.md --- examples/batch/README.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/examples/batch/README.md b/examples/batch/README.md index 6825b47..dedec14 100644 --- a/examples/batch/README.md +++ b/examples/batch/README.md @@ -1,13 +1,44 @@ -## PGX working example with PGX.batch +## PostgreSQL Batch Example in Go -These example is subdivided into three steps. +This example is subdivided into three steps. 1. databaseSetup 2. requestBatch, where a list of sql queries is executed. 3. databaseCleanup -## Video +**Description:** +This Go program demonstrates interacting with a PostgreSQL database using batch operations for improved efficiency. It leverages the `pgx/v5` library for database connectivity. -https://github.com/giuliocn/pgxmock/assets/57756052/e41e5f5a-c4fc-40e8-8653-99d84a3468ff +**Features:** +- **Database Setup:** Creates a table named `ledger` to store data (description and amount). +- **Batch Requests:** Executes multiple SQL statements within a single database transaction using `pgx.Batch`. This includes inserts (`INSERT`), selects (`SELECT`), and an aggregate function (`SUM`). +- **Error Handling:** Gracefully handles potential errors during database operations and panics for critical failures. +- **Transaction Management:** Ensures data consistency by using transactions with `Begin`, `Commit`, and `Rollback`. +- **Cleanup:** Deletes all rows from the `ledger` table after processing. +**Requirements:** + +- Go programming language installed (`https://golang.org/doc/install`) +- PostgreSQL database server running +- `pgx/v5` library (`go get -u github.com/jackc/pgx/v5`) + +**Instructions:** + +1. **Configure Database Connection:** + - Replace placeholders in the `pgxpool.New` connection string with your actual database credentials (``, ``, ``, and ``). +2. **Run the Program:** + - Execute the Go program using `go run .` from the terminal. + +**Note:** + +- This example is for demonstration purposes and might require adjustments for production use cases. +- Consider implementing proper configuration management for database credentials. + +**Additional Information:** + +- Refer to the `pgx/v5` documentation for detailed API usage: [https://github.com/jackc/pgx](https://github.com/jackc/pgx) + +**Disclaimer:** + +- This example assumes a basic understanding of Go and PostgreSQL. From 325520764f56acdd5ca3eb8061a8b292696bb472 Mon Sep 17 00:00:00 2001 From: "Giulio C.n" <57756052+giuliocn@users.noreply.github.com> Date: Thu, 2 May 2024 19:00:26 +0200 Subject: [PATCH 08/18] Update batch_test.go --- examples/batch/batch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go index 30236bf..92ff0a0 100644 --- a/examples/batch/batch_test.go +++ b/examples/batch/batch_test.go @@ -6,7 +6,7 @@ import ( "github.com/pashagolub/pgxmock/v3" ) -// a successful case +// a successful test case func TestShouldSelectRows(t *testing.T) { mock, err := pgxmock.NewPool() if err != nil { From 886f321ad9ef89e726f8a4e0848793ecfb57b2ed Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Thu, 9 May 2024 15:57:39 +0200 Subject: [PATCH 09/18] Reads results and print rows to stdout. --- examples/batch/batch.go | 57 +++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 37718da..73137b4 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -43,7 +43,7 @@ func databaseSetup(db PgxIface) (err error) { return fmt.Errorf("databaseSetup: %s", err) } - return + return err } func requestBatch(db PgxIface) (err error) { @@ -67,28 +67,47 @@ func requestBatch(db PgxIface) (err error) { batch := &pgx.Batch{} // Add SQL commands to queue - batch.Queue("insert into ledger(description, amount) values($1, $2)", "q1", 1) - batch.Queue("insert into ledger(description, amount) values($1, $2)", "q2", 2) - batch.Queue("insert into ledger(description, amount) values($1, $2)", "q3", 3) - batch.Queue("select id, description, amount from ledger order by id") - batch.Queue("select id, description, amount from ledger order by amount") - batch.Queue("select * from ledger where false") - batch.Queue("select sum(amount) from ledger") - - // Create a BatchRequest object + batch.Queue( + `INSERT INTO ledger(description, amount) VALUES ($1, $2), ($3, $4)`, + "first item", 1, "second item", 2) + + batch.Queue("SELECT * FROM ledger") + batch.Queue("SELECT * FROM ledger WHERE amount = 1") + + // Efficiently transmits queued queries as a single transaction. + // After the queries are run, a BatchResults object is returned. br := tx.SendBatch(context.Background(), batch) if br == nil { return errors.New("SendBatch returns a NIL object") } defer br.Close() - // Execute a BatchRequest - _, err = br.Exec() - if err != nil { - return fmt.Errorf("requestBatch: %s", err) - } + // Iterate over a batch of queued queries + for _, query := range batch.QueuedQueries { - return + // Print SQL statement of a queued query + fmt.Println(query.SQL) + + // BatchResult.Query reads results from a queued query + rows, err := br.Query() + if err != nil { + return fmt.Errorf("requestBatch: %s", err) + } + + // Iterate over results to print each row + var id, qt int64 + var descr string + _, err = pgx.ForEachRow(rows, []any{&id, &descr, &qt}, func() error { + fmt.Printf(" (%v, \"%v\", %v)\n", id, descr, qt) + return nil + }) + fmt.Println("") + + if err != nil { + return fmt.Errorf("requestBatch: %s", err) + } + } + return err } func databaseCleanup(db PgxIface) (err error) { @@ -108,7 +127,7 @@ func databaseCleanup(db PgxIface) (err error) { } }() - // Delete all rows in a table + // Delete all rows in table ledger sql := `DELETE FROM ledger ;` // Execute SQL commands @@ -117,7 +136,7 @@ func databaseCleanup(db PgxIface) (err error) { return fmt.Errorf("databaseCleanup: %s", err) } - return + return err } func main() { @@ -139,7 +158,7 @@ func main() { panic(err) } - // Delete all rows in table + // Delete all rows in table ladger if err = databaseCleanup(db); err != nil { panic(err) } From 49f014dbc6815814a59b8915068fa88a0624efdd Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Thu, 9 May 2024 16:19:31 +0200 Subject: [PATCH 10/18] Fix typos and update README. --- examples/batch/README.md | 19 ++++++------------- examples/batch/batch.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/examples/batch/README.md b/examples/batch/README.md index dedec14..d77209f 100644 --- a/examples/batch/README.md +++ b/examples/batch/README.md @@ -1,21 +1,14 @@ ## PostgreSQL Batch Example in Go -This example is subdivided into three steps. -1. databaseSetup -2. requestBatch, where a list of sql queries is executed. -3. databaseCleanup - -**Description:** - -This Go program demonstrates interacting with a PostgreSQL database using batch operations for improved efficiency. It leverages the `pgx/v5` library for database connectivity. +This Go program demonstrates interacting with a PostgreSQL database using batch operations for improved efficiency. It leverages the `pgx/v5` library. **Features:** -- **Database Setup:** Creates a table named `ledger` to store data (description and amount). -- **Batch Requests:** Executes multiple SQL statements within a single database transaction using `pgx.Batch`. This includes inserts (`INSERT`), selects (`SELECT`), and an aggregate function (`SUM`). -- **Error Handling:** Gracefully handles potential errors during database operations and panics for critical failures. -- **Transaction Management:** Ensures data consistency by using transactions with `Begin`, `Commit`, and `Rollback`. -- **Cleanup:** Deletes all rows from the `ledger` table after processing. +* Creates a table named `ledger` to store data (description and amount). +* Executes multiple SQL statements within a single database transaction using `pgx.Batch`. This includes inserts (`INSERT`), selects (`SELECT`), and iterating over results. +* Handles potential errors during database operations. +* Ensures data consistency by using transactions with `Begin`, `Commit`, and `Rollback`. +* Deletes all rows from the `ledger` table after processing. **Requirements:** diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 73137b4..6b3c0dd 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -85,20 +85,20 @@ func requestBatch(db PgxIface) (err error) { // Iterate over a batch of queued queries for _, query := range batch.QueuedQueries { - // Print SQL statement of a queued query + // Print SQL statement of the current query fmt.Println(query.SQL) - // BatchResult.Query reads results from a queued query + // BatchResult.Query reads results from the current query rows, err := br.Query() if err != nil { return fmt.Errorf("requestBatch: %s", err) } - // Iterate over results to print each row - var id, qt int64 + // Iterate over the results to print each selected row + var id, amount int64 var descr string - _, err = pgx.ForEachRow(rows, []any{&id, &descr, &qt}, func() error { - fmt.Printf(" (%v, \"%v\", %v)\n", id, descr, qt) + _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { + fmt.Printf(" (%v, \"%v\", %v)\n", id, descr, amount) return nil }) fmt.Println("") @@ -142,7 +142,7 @@ func databaseCleanup(db PgxIface) (err error) { func main() { // @NOTE: the real connection is not required for tests - db, err := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/postgres") + db, err := pgxpool.New(context.Background(), "postgres://:@/") if err != nil { panic(err) } @@ -158,7 +158,7 @@ func main() { panic(err) } - // Delete all rows in table ladger + // Delete all rows in table ledger if err = databaseCleanup(db); err != nil { panic(err) } From 484197683d09619010cd0573f6cfe0913f0aefe3 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Mon, 13 May 2024 15:05:05 +0200 Subject: [PATCH 11/18] Create OUTPUT.md and formatting. --- examples/batch/OUTPUT.md | 8 ++++ examples/batch/batch.go | 79 +++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 examples/batch/OUTPUT.md diff --git a/examples/batch/OUTPUT.md b/examples/batch/OUTPUT.md new file mode 100644 index 0000000..3a66065 --- /dev/null +++ b/examples/batch/OUTPUT.md @@ -0,0 +1,8 @@ +## PostgreSQL Batch Example output +### SELECT * FROM ledger +- *DESCRIPTION* , *AMOUNT* +- "first item" , 1 +- "second item" , 2 +### SELECT * FROM ledger WHERE amount = 1 +- *DESCRIPTION* , *AMOUNT* +- "first item" , 1 diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 6b3c0dd..ec55cf8 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" pgx "github.com/jackc/pgx/v5" pgxpool "github.com/jackc/pgx/v5/pgxpool" @@ -37,10 +38,46 @@ func databaseSetup(db PgxIface) (err error) { description TEXT NOT NULL, amount BIGINT NOT NULL);` - // Execute SQL commands + // Execute SQL statement _, err = tx.Exec(context.Background(), sql) + + return err +} + +func iterateResults(br pgx.BatchResults, QueuedQueries []*pgx.QueuedQuery) error { + + file, err := os.Create("OUTPUT.md") if err != nil { - return fmt.Errorf("databaseSetup: %s", err) + return fmt.Errorf("iterateResults: %s", err) + } + defer file.Close() + + fmt.Fprintf(file, "## PostgreSQL Batch Example output\n") + + // Iterate over a batch of queued queries + for _, query := range QueuedQueries { + + // Print SQL field of the current query + fmt.Fprintf(file, "### %v \n", query.SQL) + + // reads results from the current query + rows, err := br.Query() + if err != nil { + return fmt.Errorf("iterateResults: %s", err) + } + + // Print column headers + fmt.Fprintf(file, "- *DESCRIPTION* , *AMOUNT* \n") + // Iterate over the resulted rows + // + var id, amount, descr = int64(0), int64(0), string("") + _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { + fmt.Fprintf(file, "- \"%v\" , %d \n", descr, amount) + return nil + }) + if err != nil { + return fmt.Errorf("iterateResults: %s", err) + } } return err @@ -76,38 +113,23 @@ func requestBatch(db PgxIface) (err error) { // Efficiently transmits queued queries as a single transaction. // After the queries are run, a BatchResults object is returned. + // br := tx.SendBatch(context.Background(), batch) if br == nil { return errors.New("SendBatch returns a NIL object") } defer br.Close() - // Iterate over a batch of queued queries - for _, query := range batch.QueuedQueries { - - // Print SQL statement of the current query - fmt.Println(query.SQL) - - // BatchResult.Query reads results from the current query - rows, err := br.Query() - if err != nil { - return fmt.Errorf("requestBatch: %s", err) - } - - // Iterate over the results to print each selected row - var id, amount int64 - var descr string - _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { - fmt.Printf(" (%v, \"%v\", %v)\n", id, descr, amount) - return nil - }) - fmt.Println("") - - if err != nil { - return fmt.Errorf("requestBatch: %s", err) - } + // Read the first query + _, err = br.Exec() + if err != nil { + return err } - return err + // Iterate over batch results and queries. + // Note: the first query is left out of the queue. + // + return iterateResults(br, batch.QueuedQueries[1:]) + } func databaseCleanup(db PgxIface) (err error) { @@ -132,9 +154,6 @@ func databaseCleanup(db PgxIface) (err error) { // Execute SQL commands _, err = tx.Exec(context.Background(), sql) - if err != nil { - return fmt.Errorf("databaseCleanup: %s", err) - } return err } From b0d1e2ad325be0b57e66cbc370289fbc0d5b9d98 Mon Sep 17 00:00:00 2001 From: Pavlo Golub Date: Fri, 31 May 2024 11:46:31 +0200 Subject: [PATCH 12/18] bump to v4 --- examples/batch/batch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go index 92ff0a0..84f27d8 100644 --- a/examples/batch/batch_test.go +++ b/examples/batch/batch_test.go @@ -3,7 +3,7 @@ package main import ( "testing" - "github.com/pashagolub/pgxmock/v3" + "github.com/pashagolub/pgxmock/v4" ) // a successful test case From e8cf2a8c980204592fb540503252ece5a6eec716 Mon Sep 17 00:00:00 2001 From: Pavlo Golub Date: Fri, 31 May 2024 11:52:17 +0200 Subject: [PATCH 13/18] one-statement queries are automatically executed in the transaction --- examples/batch/batch.go | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index ec55cf8..8693fab 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -7,40 +7,23 @@ import ( "os" pgx "github.com/jackc/pgx/v5" + pgconn "github.com/jackc/pgx/v5/pgconn" pgxpool "github.com/jackc/pgx/v5/pgxpool" ) type PgxIface interface { Begin(context.Context) (pgx.Tx, error) + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) Close() } func databaseSetup(db PgxIface) (err error) { - - // Initialize a database object - tx, err := db.Begin(context.Background()) - if err != nil { - return fmt.Errorf("databaseSetup: %s", err) - } - // Finally, commit changes or rollback - defer func() { - switch err { - case nil: - err = tx.Commit(context.Background()) - default: - _ = tx.Rollback(context.Background()) - } - }() - // Create a new table 'ledger' sql := `CREATE TABLE IF NOT EXISTS ledger ( id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, description TEXT NOT NULL, amount BIGINT NOT NULL);` - - // Execute SQL statement - _, err = tx.Exec(context.Background(), sql) - + _, err = db.Exec(context.Background(), sql) return err } From bd3d7cd7985a7d5bfe6fc5469ef8685a586762e8 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:36:57 +0200 Subject: [PATCH 14/18] Delete .md files from example tree. --- examples/batch/OUTPUT.md | 8 -------- examples/batch/README.md | 37 ------------------------------------- 2 files changed, 45 deletions(-) delete mode 100644 examples/batch/OUTPUT.md delete mode 100644 examples/batch/README.md diff --git a/examples/batch/OUTPUT.md b/examples/batch/OUTPUT.md deleted file mode 100644 index 3a66065..0000000 --- a/examples/batch/OUTPUT.md +++ /dev/null @@ -1,8 +0,0 @@ -## PostgreSQL Batch Example output -### SELECT * FROM ledger -- *DESCRIPTION* , *AMOUNT* -- "first item" , 1 -- "second item" , 2 -### SELECT * FROM ledger WHERE amount = 1 -- *DESCRIPTION* , *AMOUNT* -- "first item" , 1 diff --git a/examples/batch/README.md b/examples/batch/README.md deleted file mode 100644 index d77209f..0000000 --- a/examples/batch/README.md +++ /dev/null @@ -1,37 +0,0 @@ -## PostgreSQL Batch Example in Go - -This Go program demonstrates interacting with a PostgreSQL database using batch operations for improved efficiency. It leverages the `pgx/v5` library. - -**Features:** - -* Creates a table named `ledger` to store data (description and amount). -* Executes multiple SQL statements within a single database transaction using `pgx.Batch`. This includes inserts (`INSERT`), selects (`SELECT`), and iterating over results. -* Handles potential errors during database operations. -* Ensures data consistency by using transactions with `Begin`, `Commit`, and `Rollback`. -* Deletes all rows from the `ledger` table after processing. - -**Requirements:** - -- Go programming language installed (`https://golang.org/doc/install`) -- PostgreSQL database server running -- `pgx/v5` library (`go get -u github.com/jackc/pgx/v5`) - -**Instructions:** - -1. **Configure Database Connection:** - - Replace placeholders in the `pgxpool.New` connection string with your actual database credentials (``, ``, ``, and ``). -2. **Run the Program:** - - Execute the Go program using `go run .` from the terminal. - -**Note:** - -- This example is for demonstration purposes and might require adjustments for production use cases. -- Consider implementing proper configuration management for database credentials. - -**Additional Information:** - -- Refer to the `pgx/v5` documentation for detailed API usage: [https://github.com/jackc/pgx](https://github.com/jackc/pgx) - -**Disclaimer:** - -- This example assumes a basic understanding of Go and PostgreSQL. From 8a03813221820ca70aac287fd5c33653f3f86d9a Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:08:44 +0200 Subject: [PATCH 15/18] Example Batch new struct added and sample tests. --- examples/batch/batch.go | 153 ++++++++++++++--------------------- examples/batch/batch_test.go | 27 +++++-- 2 files changed, 78 insertions(+), 102 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 8693fab..d4cc728 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "os" pgx "github.com/jackc/pgx/v5" @@ -11,132 +12,82 @@ import ( pgxpool "github.com/jackc/pgx/v5/pgxpool" ) -type PgxIface interface { +type PgxPoolInterface interface { Begin(context.Context) (pgx.Tx, error) Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults Close() } -func databaseSetup(db PgxIface) (err error) { +type ExampleBatch struct { + batch *pgx.Batch + br pgx.BatchResults + db PgxPoolInterface +} + +func (ex *ExampleBatch) databaseSetup() (err error) { // Create a new table 'ledger' sql := `CREATE TABLE IF NOT EXISTS ledger ( id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, description TEXT NOT NULL, amount BIGINT NOT NULL);` - _, err = db.Exec(context.Background(), sql) + _, err = ex.db.Exec(context.Background(), sql) return err } -func iterateResults(br pgx.BatchResults, QueuedQueries []*pgx.QueuedQuery) error { - - file, err := os.Create("OUTPUT.md") - if err != nil { - return fmt.Errorf("iterateResults: %s", err) - } - defer file.Close() - - fmt.Fprintf(file, "## PostgreSQL Batch Example output\n") +func (ex *ExampleBatch) printResults(file io.Writer) (err error) { // Iterate over a batch of queued queries - for _, query := range QueuedQueries { - - // Print SQL field of the current query - fmt.Fprintf(file, "### %v \n", query.SQL) + for index, query := range ex.batch.QueuedQueries { - // reads results from the current query - rows, err := br.Query() + // Read results from the current query + rows, err := ex.br.Query() if err != nil { return fmt.Errorf("iterateResults: %s", err) } - // Print column headers - fmt.Fprintf(file, "- *DESCRIPTION* , *AMOUNT* \n") - // Iterate over the resulted rows - // - var id, amount, descr = int64(0), int64(0), string("") - _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { - fmt.Fprintf(file, "- \"%v\" , %d \n", descr, amount) - return nil - }) - if err != nil { - return fmt.Errorf("iterateResults: %s", err) + if index > 0 { + // Print SQL field of the current query + fmt.Fprintf(file, "%v \n", query.SQL) + + // Iterate over the selected records + // + var id, amount, descr = int64(0), int64(0), string("") + _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { + descr = "\"" + descr + "\"" + fmt.Fprintf(file, "- %24v, amount: %d \n", descr, amount) + return err + }) } } return err } -func requestBatch(db PgxIface) (err error) { - - // Initialize a database object - tx, err := db.Begin(context.Background()) - if err != nil { - return fmt.Errorf("requestBatch: %s", err) - } - // Finally, commit changes or rollback - defer func() { - switch err { - case nil: - err = tx.Commit(context.Background()) - default: - _ = tx.Rollback(context.Background()) - } - }() - - // Create a Batch object - batch := &pgx.Batch{} - - // Add SQL commands to queue - batch.Queue( - `INSERT INTO ledger(description, amount) VALUES ($1, $2), ($3, $4)`, - "first item", 1, "second item", 2) - - batch.Queue("SELECT * FROM ledger") - batch.Queue("SELECT * FROM ledger WHERE amount = 1") +func (ex *ExampleBatch) requestBatch() (err error) { // Efficiently transmits queued queries as a single transaction. // After the queries are run, a BatchResults object is returned. // - br := tx.SendBatch(context.Background(), batch) - if br == nil { + ex.br = ex.db.SendBatch(context.Background(), ex.batch) + if ex.br == nil { return errors.New("SendBatch returns a NIL object") } - defer br.Close() - - // Read the first query - _, err = br.Exec() - if err != nil { - return err - } - // Iterate over batch results and queries. - // Note: the first query is left out of the queue. - // - return iterateResults(br, batch.QueuedQueries[1:]) + return err } -func databaseCleanup(db PgxIface) (err error) { - - // Initialize a database object - tx, err := db.Begin(context.Background()) - if err != nil { - return fmt.Errorf("databaseCleanup: %s", err) - } - // Finally, commit changes or rollback - defer func() { - switch err { - case nil: - err = tx.Commit(context.Background()) - default: - _ = tx.Rollback(context.Background()) - } - }() +func (ex *ExampleBatch) Close() (err error) { // Delete all rows in table ledger sql := `DELETE FROM ledger ;` + _, err = ex.db.Exec(context.Background(), sql) - // Execute SQL commands - _, err = tx.Exec(context.Background(), sql) + // Close batch results object + ex.br.Close() + + // Close connection to database + ex.db.Close() return err } @@ -144,24 +95,38 @@ func databaseCleanup(db PgxIface) (err error) { func main() { // @NOTE: the real connection is not required for tests - db, err := pgxpool.New(context.Background(), "postgres://:@/") + db, err := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/batch") if err != nil { panic(err) } defer db.Close() - // Create a database table - if err = databaseSetup(db); err != nil { + // Setup the example + var example = ExampleBatch{db: db, batch: &pgx.Batch{}} + if err = example.databaseSetup(); err != nil { panic(err) } - // Create and send a batch request - if err = requestBatch(db); err != nil { + // Add SQL queries to the queue + example.batch.Queue( + `INSERT INTO ledger(description, amount) VALUES ($1, $2), ($3, $4)`, + "first item", 1, "second item", 2) + + example.batch.Queue("SELECT * FROM ledger") + example.batch.Queue("SELECT * FROM ledger WHERE amount = 1") + + // Send a batch request and store results + if err = example.requestBatch(); err != nil { panic(err) } - // Delete all rows in table ledger - if err = databaseCleanup(db); err != nil { + // Print batch result to ... + if err = example.printResults(os.Stdout); err != nil { + panic(err) + } + + // Close the example + if err = example.Close(); err != nil { panic(err) } } diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go index 84f27d8..7c279ee 100644 --- a/examples/batch/batch_test.go +++ b/examples/batch/batch_test.go @@ -3,22 +3,34 @@ package main import ( "testing" + pgx "github.com/jackc/pgx/v5" "github.com/pashagolub/pgxmock/v4" ) // a successful test case -func TestShouldSelectRows(t *testing.T) { +func TestExpectBatch(t *testing.T) { mock, err := pgxmock.NewPool() if err != nil { t.Fatal(err) } defer mock.Close() - // TODO + mock.ExpectBatch() + + // Setup the example + var example = ExampleBatch{db: mock, batch: &pgx.Batch{}} + + // now we execute our method + example.requestBatch() + + // we make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } } // a failing test case -func TestShouldRollbackOnFailure(t *testing.T) { +func TestExpectBegin(t *testing.T) { mock, err := pgxmock.NewPool() if err != nil { t.Fatal(err) @@ -26,13 +38,12 @@ func TestShouldRollbackOnFailure(t *testing.T) { defer mock.Close() mock.ExpectBegin() - // TODO: mock.ExpectBatch() - mock.ExpectRollback() + + // Setup the example + var example = ExampleBatch{db: mock, batch: &pgx.Batch{}} // now we execute our method - if err = requestBatch(mock); err == nil { - t.Errorf("was expecting an error, but there was none") - } + example.requestBatch() // we make sure that all expectations were met if err := mock.ExpectationsWereMet(); err != nil { From ad1dad8ec7c94dac5dda3a060d009b68056a8319 Mon Sep 17 00:00:00 2001 From: giuliocn <57756052+giuliocn@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:45:29 +0200 Subject: [PATCH 16/18] Batch results at localhost in raw text format. --- examples/batch/batch.go | 136 ++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 53 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index d4cc728..65e5334 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" - "io" - "os" + "net/http" + "strings" pgx "github.com/jackc/pgx/v5" pgconn "github.com/jackc/pgx/v5/pgconn" @@ -20,48 +20,79 @@ type PgxPoolInterface interface { } type ExampleBatch struct { - batch *pgx.Batch - br pgx.BatchResults - db PgxPoolInterface + batch *pgx.Batch + br pgx.BatchResults + db PgxPoolInterface + output strings.Builder +} + +func (ex *ExampleBatch) insertRow(fields ...string) (err error) { + for i, value := range fields { + fields[i] = "$$" + value + "$$" + } + sql := `INSERT INTO metadata (title, authors, subject, description) + VALUES + (` + strings.Join(fields[:4], ", ") + ");" + _, err = ex.db.Exec(context.Background(), sql) + return err } func (ex *ExampleBatch) databaseSetup() (err error) { - // Create a new table 'ledger' - sql := `CREATE TABLE IF NOT EXISTS ledger ( - id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, - description TEXT NOT NULL, - amount BIGINT NOT NULL);` + // Create a new table 'metadata' + sql := ` + CREATE TABLE IF NOT EXISTS metadata ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + title TEXT NOT NULL, + authors TEXT NOT NULL, + subject TEXT NOT NULL, + description TEXT NOT NULL); + DELETE FROM metadata ; + INSERT INTO metadata (title, authors, subject, description) + VALUES + ($$A Journey Through the Alps$$, $$John Doe, Jane Smith$$, $$Hiking, Nature, Travel$$, $$Explore the breathtaking beauty of the Alps on a scenic hiking adventure. Discover hidden trails, alpine lakes, and charming mountain villages.$$), + ($$The Art of Baking: A Beginner's Guide$$, $$Emily Baker$$, $$Cooking, Baking, Food$$, $$Learn the fundamentals of baking and create delicious treats from scratch. Master techniques like kneading dough, frosting cakes, and piping cookies.$$), + ($$The History of Ancient Rome$$, $$David Johnson$$, $$History, Ancient Rome, Civilization$$, $$Delve into the fascinating history of the Roman Empire. Explore its rise to power, its cultural achievements, and its eventual decline.$$); + ` _, err = ex.db.Exec(context.Background(), sql) return err } -func (ex *ExampleBatch) printResults(file io.Writer) (err error) { +func (ex *ExampleBatch) printResults(w http.ResponseWriter, req *http.Request) { + + var temp strings.Builder // Iterate over a batch of queued queries - for index, query := range ex.batch.QueuedQueries { + for _, query := range ex.batch.QueuedQueries { // Read results from the current query - rows, err := ex.br.Query() - if err != nil { - return fmt.Errorf("iterateResults: %s", err) + rows, _ := ex.br.Query() + + // Print SQL field of the current query + temp.WriteString(fmt.Sprintf("%v \n", query.SQL)) + + // Iterate over the selected records + // + for rows.Next() { + err := rows.Err() + if err != nil { + return + } + + values, err := rows.Values() + if err != nil { + return + } + + // Convert values to a string + temp.WriteString(fmt.Sprintf("%v \n", values)) } + } - if index > 0 { - // Print SQL field of the current query - fmt.Fprintf(file, "%v \n", query.SQL) - - // Iterate over the selected records - // - var id, amount, descr = int64(0), int64(0), string("") - _, err = pgx.ForEachRow(rows, []any{&id, &descr, &amount}, func() error { - descr = "\"" + descr + "\"" - fmt.Fprintf(file, "- %24v, amount: %d \n", descr, amount) - return err - }) - } + if ex.output.Len() == 0 { + ex.output = temp } + fmt.Fprint(w, ex.output.String()) - return err } func (ex *ExampleBatch) requestBatch() (err error) { @@ -79,10 +110,6 @@ func (ex *ExampleBatch) requestBatch() (err error) { func (ex *ExampleBatch) Close() (err error) { - // Delete all rows in table ledger - sql := `DELETE FROM ledger ;` - _, err = ex.db.Exec(context.Background(), sql) - // Close batch results object ex.br.Close() @@ -99,34 +126,37 @@ func main() { if err != nil { panic(err) } - defer db.Close() - // Setup the example - var example = ExampleBatch{db: db, batch: &pgx.Batch{}} + // Setup the database + var example = ExampleBatch{ + db: db, + batch: &pgx.Batch{}, + } if err = example.databaseSetup(); err != nil { panic(err) } - - // Add SQL queries to the queue - example.batch.Queue( - `INSERT INTO ledger(description, amount) VALUES ($1, $2), ($3, $4)`, - "first item", 1, "second item", 2) - - example.batch.Queue("SELECT * FROM ledger") - example.batch.Queue("SELECT * FROM ledger WHERE amount = 1") - - // Send a batch request and store results + defer example.Close() + + // Add an example to database table + example.insertRow( + "The Adventures of Captain Pickle", + "Timothy T. Turtle", + "Adventure, Fantasy", + "Join Captain Pickle...", + ) + + // Add SQL queries to the queue for a batch operation + example.batch.Queue("SELECT title FROM metadata") + example.batch.Queue("SELECT authors FROM metadata") + example.batch.Queue("SELECT subject, description FROM metadata") + + // Send the batch request to database if err = example.requestBatch(); err != nil { panic(err) } // Print batch result to ... - if err = example.printResults(os.Stdout); err != nil { - panic(err) - } + http.HandleFunc("/", example.printResults) + _ = http.ListenAndServe(":8080", nil) - // Close the example - if err = example.Close(); err != nil { - panic(err) - } } From efee7a35c63b1d386aa58df574d1639874bd6424 Mon Sep 17 00:00:00 2001 From: "Giulio C.n" <57756052+giuliocn@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:26:29 +0200 Subject: [PATCH 17/18] Complete example with revised structure. --- examples/batch/batch.go | 189 +++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 98 deletions(-) diff --git a/examples/batch/batch.go b/examples/batch/batch.go index 65e5334..db4e8be 100644 --- a/examples/batch/batch.go +++ b/examples/batch/batch.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "net/http" - "strings" pgx "github.com/jackc/pgx/v5" pgconn "github.com/jackc/pgx/v5/pgconn" @@ -13,105 +11,105 @@ import ( ) type PgxPoolInterface interface { - Begin(context.Context) (pgx.Tx, error) Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) SendBatch(context.Context, *pgx.Batch) pgx.BatchResults Close() } -type ExampleBatch struct { - batch *pgx.Batch - br pgx.BatchResults - db PgxPoolInterface - output strings.Builder +type exampleBatch struct { + batch *pgx.Batch // batch pointer + results pgx.BatchResults // query results + db PgxPoolInterface // database connection } -func (ex *ExampleBatch) insertRow(fields ...string) (err error) { - for i, value := range fields { - fields[i] = "$$" + value + "$$" - } - sql := `INSERT INTO metadata (title, authors, subject, description) - VALUES - (` + strings.Join(fields[:4], ", ") + ");" - _, err = ex.db.Exec(context.Background(), sql) - return err +type metadata struct { + title string + authors string + subject string + description string } -func (ex *ExampleBatch) databaseSetup() (err error) { - // Create a new table 'metadata' - sql := ` - CREATE TABLE IF NOT EXISTS metadata ( +func NewExample(db PgxPoolInterface) (example exampleBatch, err error) { + + example = exampleBatch{ + db: db, // database + batch: &pgx.Batch{}, // batch struct address + } + + sql := `CREATE TABLE IF NOT EXISTS metadata ( id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, title TEXT NOT NULL, authors TEXT NOT NULL, subject TEXT NOT NULL, description TEXT NOT NULL); - DELETE FROM metadata ; - INSERT INTO metadata (title, authors, subject, description) - VALUES - ($$A Journey Through the Alps$$, $$John Doe, Jane Smith$$, $$Hiking, Nature, Travel$$, $$Explore the breathtaking beauty of the Alps on a scenic hiking adventure. Discover hidden trails, alpine lakes, and charming mountain villages.$$), - ($$The Art of Baking: A Beginner's Guide$$, $$Emily Baker$$, $$Cooking, Baking, Food$$, $$Learn the fundamentals of baking and create delicious treats from scratch. Master techniques like kneading dough, frosting cakes, and piping cookies.$$), - ($$The History of Ancient Rome$$, $$David Johnson$$, $$History, Ancient Rome, Civilization$$, $$Delve into the fascinating history of the Roman Empire. Explore its rise to power, its cultural achievements, and its eventual decline.$$); - ` - _, err = ex.db.Exec(context.Background(), sql) - return err -} - -func (ex *ExampleBatch) printResults(w http.ResponseWriter, req *http.Request) { + DELETE FROM metadata ;` + _, err = example.db.Exec(context.Background(), sql) // Execute sql - var temp strings.Builder - - // Iterate over a batch of queued queries - for _, query := range ex.batch.QueuedQueries { - - // Read results from the current query - rows, _ := ex.br.Query() - - // Print SQL field of the current query - temp.WriteString(fmt.Sprintf("%v \n", query.SQL)) + return example, err +} - // Iterate over the selected records - // - for rows.Next() { - err := rows.Err() - if err != nil { - return - } - - values, err := rows.Values() - if err != nil { - return - } - - // Convert values to a string - temp.WriteString(fmt.Sprintf("%v \n", values)) +func (ex *exampleBatch) BulkInsertMetadata(data []metadata) error { + query := `INSERT INTO metadata (title, authors, subject, description) + VALUES + (@title, @authors, @subject, @description) + ` + batch := &pgx.Batch{} + for _, m := range data { + args := pgx.NamedArgs{ + "title": m.title, + "authors": m.authors, + "subject": m.subject, + "description": m.description, } + batch.Queue(query, args) } - if ex.output.Len() == 0 { - ex.output = temp + results := ex.db.SendBatch(context.Background(), batch) + defer results.Close() + + for index := range data { + _, err := results.Exec() + if err != nil { + return fmt.Errorf("unable to insert row %d: %w", index, err) + } } - fmt.Fprint(w, ex.output.String()) + return results.Close() } -func (ex *ExampleBatch) requestBatch() (err error) { +func (ex *exampleBatch) SendCustomBatch(queries []string) (err error) { + + // Add SQL queries to the queue for a custom batch + for _, query := range queries { + ex.batch.Queue(query) + } // Efficiently transmits queued queries as a single transaction. // After the queries are run, a BatchResults object is returned. - // - ex.br = ex.db.SendBatch(context.Background(), ex.batch) - if ex.br == nil { + ex.results = ex.db.SendBatch(context.Background(), ex.batch) + if ex.results == nil { return errors.New("SendBatch returns a NIL object") } return err } -func (ex *ExampleBatch) Close() (err error) { +func (ex *exampleBatch) TestCustomResults() (err error) { + + for index := range ex.batch.QueuedQueries { + _, err := ex.results.Exec() + if err != nil { + return fmt.Errorf("unable to run query %d: %w", index, err) + } + } + + return ex.results.Close() +} + +func (ex *exampleBatch) Close() (err error) { // Close batch results object - ex.br.Close() + ex.results.Close() // Close connection to database ex.db.Close() @@ -122,41 +120,36 @@ func (ex *ExampleBatch) Close() (err error) { func main() { // @NOTE: the real connection is not required for tests - db, err := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/batch") - if err != nil { - panic(err) - } + db, _ := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/batch") - // Setup the database - var example = ExampleBatch{ - db: db, - batch: &pgx.Batch{}, - } - if err = example.databaseSetup(); err != nil { + // Create a new instance of example struct + example, err := NewExample(db) + if err != nil { panic(err) } defer example.Close() - // Add an example to database table - example.insertRow( - "The Adventures of Captain Pickle", - "Timothy T. Turtle", - "Adventure, Fantasy", - "Join Captain Pickle...", - ) - - // Add SQL queries to the queue for a batch operation - example.batch.Queue("SELECT title FROM metadata") - example.batch.Queue("SELECT authors FROM metadata") - example.batch.Queue("SELECT subject, description FROM metadata") - - // Send the batch request to database - if err = example.requestBatch(); err != nil { - panic(err) + // Insert multiple rows into the database + example.BulkInsertMetadata([]metadata{ + {`A Journey Through the Alps`, `John Doe, Jane Smith`, `Hiking, Nature, Travel`, `Explore the breathtaking beauty of the Alps on a scenic hiking adventure. Discover hidden trails, alpine lakes, and charming mountain villages.`}, + {`The Art of Baking: A Beginner's Guide`, `Emily Baker`, `Cooking, Baking, Food`, `Learn the fundamentals of baking and create delicious treats from scratch. Master techniques like kneading dough, frosting cakes, and piping cookies.`}, + {`The History of Ancient Rome`, `David Johnson`, `History, Ancient Rome, Civilization`, `Delve into the fascinating history of the Roman Empire. Explore its rise to power, its cultural achievements, and its eventual decline.`}, + }) + + // Send a custom batch operation to database + example.SendCustomBatch([]string{ + "SELECT title FROM metadata", + "SELECT authors FROM metadata", + "SELECT subject, description FROM metadata", + }) + + // Print batch results + for _, query := range example.batch.QueuedQueries { + fmt.Println(query.SQL) + rows, _ := example.results.Query() + for rows.Next() { + values, _ := rows.Values() + fmt.Println(values) + } } - - // Print batch result to ... - http.HandleFunc("/", example.printResults) - _ = http.ListenAndServe(":8080", nil) - } From 4a4e1d9439fb1832a115c2bf60ca1d8ac72de281 Mon Sep 17 00:00:00 2001 From: "Giulio C.n" <57756052+giuliocn@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:28:45 +0200 Subject: [PATCH 18/18] Failing tests due to expected queries. --- examples/batch/batch_test.go | 80 ++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/examples/batch/batch_test.go b/examples/batch/batch_test.go index 7c279ee..7135d0e 100644 --- a/examples/batch/batch_test.go +++ b/examples/batch/batch_test.go @@ -3,49 +3,93 @@ package main import ( "testing" - pgx "github.com/jackc/pgx/v5" - "github.com/pashagolub/pgxmock/v4" + pgxmock "github.com/pashagolub/pgxmock/v4" ) -// a successful test case -func TestExpectBatch(t *testing.T) { +func TestNewExample(t *testing.T) { mock, err := pgxmock.NewPool() if err != nil { t.Fatal(err) } defer mock.Close() + // Expect a call to Exec + mock.ExpectExec(`^CREATE TABLE IF NOT EXISTS (.+)`). + WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) + + _, err = NewExample(mock) + if err != nil { + t.Errorf("creating new example error: %s", err) + } + + // We make sure that all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} + +func TestSendCustomBatch(t *testing.T) { + mock, err := pgxmock.NewPool() + if err != nil { + t.Fatal(err) + } + defer mock.Close() + + // Expect a call to Exec and pgx.Batch + mock.ExpectExec(`^CREATE TABLE IF NOT EXISTS (.+)`). + WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) mock.ExpectBatch() - // Setup the example - var example = ExampleBatch{db: mock, batch: &pgx.Batch{}} + example, err := NewExample(mock) + if err != nil { + t.Errorf("creating new example error: %s", err) + } - // now we execute our method - example.requestBatch() + err = example.SendCustomBatch([]string{ + "SELECT title FROM metadata", + "SELECT authors FROM metadata", + "SELECT subject, description FROM metadata", + }) + if err != nil { + t.Errorf("SendCustomBatch error: %s", err) + } + + err = example.TestCustomResults() + if err != nil { + t.Errorf("TestCustomResults error: %s", err) + } - // we make sure that all expectations were met + // We make sure that all expectations were met if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } } - -// a failing test case -func TestExpectBegin(t *testing.T) { +func TestBulkInsert(t *testing.T) { mock, err := pgxmock.NewPool() if err != nil { t.Fatal(err) } defer mock.Close() - mock.ExpectBegin() + // Expect a call to Exec and pgx.Batch + mock.ExpectExec(`^CREATE TABLE IF NOT EXISTS (.+)`). + WillReturnResult(pgxmock.NewResult("CREATE TABLE", 0)) + mock.ExpectBatch() - // Setup the example - var example = ExampleBatch{db: mock, batch: &pgx.Batch{}} + example, err := NewExample(mock) + if err != nil { + t.Errorf("creating new example error: %s", err) + } - // now we execute our method - example.requestBatch() + // Insert multiple rows into the database + err = example.BulkInsertMetadata([]metadata{ + {`title`, `author`, `subject`, `description`}, + }) + if err != nil { + t.Errorf("bulk insert error: %s", err) + } - // we make sure that all expectations were met + // We make sure that all expectations were met if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) }