-
-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pgxmock batch example in Go #212
Changes from 19 commits
3a09c76
8e2db26
d36e83e
1762d12
1a89fce
b3539db
a36b504
3255207
886f321
49f014d
4841976
b0d1e2a
e8cf2a8
0768aaf
bd3d7cd
8a03813
84a0ec5
ad1dad8
2f03bb6
117f4c9
efee7a3
4a4e1d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
pgx "github.com/jackc/pgx/v5" | ||
pgconn "github.com/jackc/pgx/v5/pgconn" | ||
pgxpool "github.com/jackc/pgx/v5/pgxpool" | ||
) | ||
|
||
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 | ||
} | ||
|
||
func (ex *ExampleBatch) insertRow(fields ...string) (err error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any value in |
||
for i, value := range fields { | ||
fields[i] = "$$" + value + "$$" | ||
} | ||
sql := `INSERT INTO metadata (title, authors, subject, description) | ||
VALUES | ||
(` + strings.Join(fields[:4], ", ") + ");" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't do parameters concatenation, this is the possible place for SQL injection. We should use parameterized queries. |
||
_, err = ex.db.Exec(context.Background(), sql) | ||
return err | ||
} | ||
|
||
func (ex *ExampleBatch) databaseSetup() (err error) { | ||
// 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(w http.ResponseWriter, req *http.Request) { | ||
|
||
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)) | ||
|
||
// 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 ex.output.Len() == 0 { | ||
ex.output = temp | ||
} | ||
fmt.Fprint(w, ex.output.String()) | ||
|
||
} | ||
|
||
func (ex *ExampleBatch) requestBatch() (err error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any need in this function. Let's keep it simple |
||
|
||
// 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 { | ||
return errors.New("SendBatch returns a NIL object") | ||
} | ||
|
||
return err | ||
} | ||
|
||
func (ex *ExampleBatch) Close() (err error) { | ||
|
||
// Close batch results object | ||
ex.br.Close() | ||
|
||
// Close connection to database | ||
ex.db.Close() | ||
|
||
return err | ||
} | ||
|
||
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) | ||
} | ||
|
||
// Setup the database | ||
var example = ExampleBatch{ | ||
db: db, | ||
batch: &pgx.Batch{}, | ||
} | ||
if err = example.databaseSetup(); err != nil { | ||
panic(err) | ||
} | ||
defer example.Close() | ||
|
||
// Add an example to database table | ||
example.insertRow( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we insert here and in |
||
"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 ... | ||
http.HandleFunc("/", example.printResults) | ||
_ = http.ListenAndServe(":8080", nil) | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
pgx "github.com/jackc/pgx/v5" | ||
"github.com/pashagolub/pgxmock/v4" | ||
) | ||
|
||
// a successful test case | ||
func TestExpectBatch(t *testing.T) { | ||
mock, err := pgxmock.NewPool() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer mock.Close() | ||
|
||
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 TestExpectBegin(t *testing.T) { | ||
mock, err := pgxmock.NewPool() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer mock.Close() | ||
|
||
mock.ExpectBegin() | ||
|
||
// 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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nor
Begin
neitherExec
used anywhere in code. Although this is not an error, we want to show our user that interfaces must be minimal