From c5043c9ff283836f748f22383624fc5f7f172165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= Date: Sun, 13 Nov 2022 10:52:27 -0300 Subject: [PATCH] Automate the process of running the benchmark and saving it to the README file --- .gitignore | 1 + Makefile | 7 +- README.md | 74 +++-- readme.template.md | 427 ++++++++++++++++++++++++++ scripts/build-readme-from-template.go | 45 +++ 5 files changed, 513 insertions(+), 41 deletions(-) create mode 100644 readme.template.md create mode 100755 scripts/build-readme-from-template.go diff --git a/.gitignore b/.gitignore index ba8e34f..86ac994 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/*coverage.txt go.work go.work.sum +benchmark.tmp diff --git a/Makefile b/Makefile index 0db1fa0..202d7b2 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,10 @@ test: setup go-mod-tidy @( cd adapters/ksqlite3 ; $(GOBIN)/richgo test $(path) $(args) ) bench: go-mod-tidy - @make --no-print-directory -C benchmarks TIME=$(TIME) - @echo "Benchmark executed at: $$(date --iso)" - @echo "Benchmark executed on commit: $$(git rev-parse HEAD)" + @make --no-print-directory -C benchmarks TIME=$(TIME) | tee benchmark.tmp + @echo "Benchmark executed at: $$(date --iso)" | tee -a benchmark.tmp + @echo "Benchmark executed on commit: $$(git rev-parse HEAD)" | tee -a benchmark.tmp + go run scripts/build-readme-from-template.go readme.template.md benchmark.tmp lint: setup go-mod-tidy @$(GOBIN)/staticcheck $(path) $(args) diff --git a/README.md b/README.md index 5372ae6..91ebf80 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - [![CI](https://github.com/VinGarcia/ksql/actions/workflows/ci.yml/badge.svg)](https://github.com/VinGarcia/ksql/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/VinGarcia/ksql/branch/master/graph/badge.svg?token=5CNJ867C66)](https://codecov.io/gh/VinGarcia/ksql) [![Go Reference](https://pkg.go.dev/badge/github.com/vingarcia/ksql.svg)](https://pkg.go.dev/github.com/vingarcia/ksql) @@ -363,50 +362,49 @@ Without further ado, here are the results: ```bash $ make bench TIME=5s -find . -name go.mod -execdir go mod tidy \; sqlc generate go test -bench=. -benchtime=5s goos: linux goarch: amd64 pkg: github.com/vingarcia/ksql/benchmarks cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz -BenchmarkInsert/ksql/sql-adapter/insert-one-12 9358 630699 ns/op -BenchmarkInsert/ksql/pgx-adapter/insert-one-12 10000 548489 ns/op -BenchmarkInsert/sql/insert-one-12 9601 631485 ns/op -BenchmarkInsert/sql/prep-stmt/insert-one-12 10000 558009 ns/op -BenchmarkInsert/sqlx/insert-one-12 9688 637395 ns/op -BenchmarkInsert/sqlx/prep-stmt/insert-one-12 10000 555805 ns/op -BenchmarkInsert/pgxpool/insert-one-12 10000 558187 ns/op -BenchmarkInsert/gorm/insert-one-12 8671 690290 ns/op -BenchmarkInsert/sqlc/insert-one-12 9710 636395 ns/op -BenchmarkInsert/sqlc/prep-stmt/insert-one-12 10000 572243 ns/op -BenchmarkInsert/sqlboiler/insert-one-12 9693 645475 ns/op -BenchmarkQuery/ksql/sql-adapter/single-row-12 42440 139913 ns/op -BenchmarkQuery/ksql/sql-adapter/multiple-rows-12 39391 152663 ns/op -BenchmarkQuery/ksql/pgx-adapter/single-row-12 81457 70102 ns/op -BenchmarkQuery/ksql/pgx-adapter/multiple-rows-12 74710 81578 ns/op -BenchmarkQuery/sql/single-row-12 43040 139945 ns/op -BenchmarkQuery/sql/multiple-rows-12 41533 143923 ns/op -BenchmarkQuery/sql/prep-stmt/single-row-12 89296 69676 ns/op -BenchmarkQuery/sql/prep-stmt/multiple-rows-12 78828 75760 ns/op -BenchmarkQuery/sqlx/single-row-12 41727 141424 ns/op -BenchmarkQuery/sqlx/multiple-rows-12 41360 148159 ns/op -BenchmarkQuery/sqlx/prep-stmt/single-row-12 81553 68347 ns/op -BenchmarkQuery/sqlx/prep-stmt/multiple-rows-12 77161 77070 ns/op -BenchmarkQuery/pgxpool/single-row-12 85140 68628 ns/op -BenchmarkQuery/pgxpool/multiple-rows-12 76016 73659 ns/op -BenchmarkQuery/gorm/single-row-12 79791 74779 ns/op -BenchmarkQuery/gorm/multiple-rows-12 63621 94351 ns/op -BenchmarkQuery/sqlc/single-row-12 41904 142480 ns/op -BenchmarkQuery/sqlc/multiple-rows-12 39877 147798 ns/op -BenchmarkQuery/sqlc/prep-stmt/single-row-12 84484 68351 ns/op -BenchmarkQuery/sqlc/prep-stmt/multiple-rows-12 81837 76040 ns/op -BenchmarkQuery/sqlboiler/single-row-12 67383 90907 ns/op -BenchmarkQuery/sqlboiler/multiple-rows-12 64791 91555 ns/op +BenchmarkInsert/ksql/sql-adapter/insert-one-12 9256 626985 ns/op +BenchmarkInsert/ksql/pgx-adapter/insert-one-12 11056 548748 ns/op +BenchmarkInsert/sql/insert-one-12 9565 623659 ns/op +BenchmarkInsert/sql/prep-stmt/insert-one-12 10000 541058 ns/op +BenchmarkInsert/sqlx/insert-one-12 9319 637775 ns/op +BenchmarkInsert/sqlx/prep-stmt/insert-one-12 10000 549806 ns/op +BenchmarkInsert/pgxpool/insert-one-12 10000 546349 ns/op +BenchmarkInsert/gorm/insert-one-12 8859 675650 ns/op +BenchmarkInsert/sqlc/insert-one-12 9889 634589 ns/op +BenchmarkInsert/sqlc/prep-stmt/insert-one-12 10000 552079 ns/op +BenchmarkInsert/sqlboiler/insert-one-12 9536 633515 ns/op +BenchmarkQuery/ksql/sql-adapter/single-row-12 41222 144799 ns/op +BenchmarkQuery/ksql/sql-adapter/multiple-rows-12 38523 156556 ns/op +BenchmarkQuery/ksql/pgx-adapter/single-row-12 85074 72465 ns/op +BenchmarkQuery/ksql/pgx-adapter/multiple-rows-12 70690 84502 ns/op +BenchmarkQuery/sql/single-row-12 41802 144467 ns/op +BenchmarkQuery/sql/multiple-rows-12 39248 147765 ns/op +BenchmarkQuery/sql/prep-stmt/single-row-12 80530 71376 ns/op +BenchmarkQuery/sql/prep-stmt/multiple-rows-12 76730 77769 ns/op +BenchmarkQuery/sqlx/single-row-12 41960 146817 ns/op +BenchmarkQuery/sqlx/multiple-rows-12 39349 152887 ns/op +BenchmarkQuery/sqlx/prep-stmt/single-row-12 81045 73004 ns/op +BenchmarkQuery/sqlx/prep-stmt/multiple-rows-12 75256 78604 ns/op +BenchmarkQuery/pgxpool/single-row-12 82630 72241 ns/op +BenchmarkQuery/pgxpool/multiple-rows-12 81619 74408 ns/op +BenchmarkQuery/gorm/single-row-12 76700 78651 ns/op +BenchmarkQuery/gorm/multiple-rows-12 62342 95746 ns/op +BenchmarkQuery/sqlc/single-row-12 41563 146143 ns/op +BenchmarkQuery/sqlc/multiple-rows-12 40240 149534 ns/op +BenchmarkQuery/sqlc/prep-stmt/single-row-12 83230 72397 ns/op +BenchmarkQuery/sqlc/prep-stmt/multiple-rows-12 79408 78645 ns/op +BenchmarkQuery/sqlboiler/single-row-12 65866 93841 ns/op +BenchmarkQuery/sqlboiler/multiple-rows-12 65091 94486 ns/op PASS -ok github.com/vingarcia/ksql/benchmarks 228.871s -Benchmark executed at: 2022-09-29 -Benchmark executed on commit: fc2b32d91965404c2479aa3d1be904b5a4c217f4 +ok github.com/vingarcia/ksql/benchmarks 226.109s +Benchmark executed at: 2022-11-13 +Benchmark executed on commit: 5bfb5cd92affae29dab3499b07fcd36b70a20057 ``` ## Running the KSQL tests (for contributors) diff --git a/readme.template.md b/readme.template.md new file mode 100644 index 0000000..60e1093 --- /dev/null +++ b/readme.template.md @@ -0,0 +1,427 @@ +[![CI](https://github.com/VinGarcia/ksql/actions/workflows/ci.yml/badge.svg)](https://github.com/VinGarcia/ksql/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/VinGarcia/ksql/branch/master/graph/badge.svg?token=5CNJ867C66)](https://codecov.io/gh/VinGarcia/ksql) +[![Go Reference](https://pkg.go.dev/badge/github.com/vingarcia/ksql.svg)](https://pkg.go.dev/github.com/vingarcia/ksql) +![Go Report Card](https://goreportcard.com/badge/github.com/vingarcia/ksql) + + +# KSQL the Keep it Simple SQL library + +KSQL was created to offer an actually simple and satisfactory +tool for interacting with SQL Databases in Golang. + +The core idea on KSQL is to offer an easy to use interface, +the actual communication with the database is decoupled so we can use +KSQL on top of `pgx`, `database/sql` and possibly other tools. +You can even create you own backend adapter for KSQL which is +useful in some situations. + +In this README you will find examples for "Getting Started" with the library, +for more advanced use-cases [please read our Wiki](https://github.com/VinGarcia/ksql/wiki). + +## Let's start with some Code: + +This short example below is a TLDR version for illustrating how easy it is to use KSQL. + +You will find more complete examples on the sections below. + +```golang +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/vingarcia/ksql" + "github.com/vingarcia/ksql/adapters/kpgx" +) + +var UsersTable = ksql.NewTable("users", "user_id") + +type User struct { + ID int `ksql:"user_id"` + Name string `ksql:"name"` + Type string `ksql:"type"` +} + +func main() { + ctx := context.Background() + db, err := kpgx.New(ctx, os.Getenv("POSTGRES_URL"), ksql.Config{}) + if err != nil { + log.Fatalf("unable connect to database: %s", err) + } + defer db.Close() + + // For querying only some attributes you can + // create a custom struct like this: + var count []struct { + Count string `ksql:"count"` + Type string `ksql:"type"` + } + err = db.Query(ctx, &count, "SELECT type, count(*) as count FROM users WHERE type = $1 GROUP BY type", "admin") + if err != nil { + log.Fatalf("unable to query users: %s", err) + } + + fmt.Println("number of users by type:", count) + + // For loading entities from the database KSQL can build + // the SELECT part of the query for you if you omit it like this: + var users []User + err = db.Query(ctx, &users, "FROM users WHERE type = $1", "admin") + if err != nil { + log.Fatalf("unable to query users: %s", err) + } + + fmt.Println("users:", users) +} +``` + +We currently have 4 constructors available, +one of them is illustrated above (`kpgx.New()`), +the other ones have the exact same signature +but work on different databases, they are: + +- `kpgx.New(ctx, os.Getenv("DATABASE_URL"), ksql.Config{})` for Postgres, it works on top of `pgxpool` +- `kmysql.New(ctx, os.Getenv("DATABASE_URL"), ksql.Config{})` for MySQL, it works on top of `database/sql` +- `ksqlserver.New(ctx, os.Getenv("DATABASE_URL"), ksql.Config{})` for SQLServer, it works on top of `database/sql` +- `ksqlite3.New(ctx, os.Getenv("DATABASE_URL"), ksql.Config{})` for SQLite3, it works on top of `database/sql` + +## The KSQL Interface + +The current interface contains the methods the users are expected to use, +and it is also used for making it easy to mock the whole library if needed. + +This interface is declared in the project as `ksql.Provider` and is displayed below. + +We plan on keeping it very simple with a small number +of well thought functions that cover all use-cases, +so don't expect many additions: + +```go +// Provider describes the KSQL public behavior +// +// The Insert, Patch, Delete and QueryOne functions return `ksql.ErrRecordNotFound` +// if no record was found or no rows were changed during the operation. +type Provider interface { + Insert(ctx context.Context, table Table, record interface{}) error + Patch(ctx context.Context, table Table, record interface{}) error + Delete(ctx context.Context, table Table, idOrRecord interface{}) error + + Query(ctx context.Context, records interface{}, query string, params ...interface{}) error + QueryOne(ctx context.Context, record interface{}, query string, params ...interface{}) error + QueryChunks(ctx context.Context, parser ChunkParser) error + + Exec(ctx context.Context, query string, params ...interface{}) (Result, error) + Transaction(ctx context.Context, fn func(Provider) error) error +} +``` + +## Using KSQL + +In the example below we'll cover all the most common use-cases such as: + +1. Inserting records +2. Updating records +3. Deleting records +4. Querying one or many records +5. Making transactions + +More advanced use cases are illustrated on their own pages on [our Wiki](https://github.com/VinGarcia/ksql/wiki): + +- [Querying in Chunks for Big Queries](https://github.com/VinGarcia/ksql/wiki/Querying-in-Chunks-for-Big-Queries) +- [Avoiding Code Duplication with the Select Builder](https://github.com/VinGarcia/ksql/wiki/Avoiding-Code-Duplication-with-the-Select-Builder) +- [Reusing Existing Structs on Queries with JOINs](https://github.com/VinGarcia/ksql/wiki/Reusing-Existing-Structs-on-Queries-with-JOINs) +- [Testing Tools and `ksql.Mock`](https://github.com/VinGarcia/ksql/wiki/Testing-Tools-and-ksql.Mock) + +For the more common use-cases please read the example below, +which is also available [here](./examples/crud/crud.go) +if you want to compile it yourself. + +```Go +package main + +import ( + "context" + "fmt" + + "github.com/vingarcia/ksql" + "github.com/vingarcia/ksql/adapters/ksqlite3" + "github.com/vingarcia/ksql/nullable" +) + +// User ... +type User struct { + ID int `ksql:"id"` + Name string `ksql:"name"` + Age int `ksql:"age"` + + // This field will be saved as JSON in the database + Address Address `ksql:"address,json"` +} + +// PartialUpdateUser ... +type PartialUpdateUser struct { + ID int `ksql:"id"` + Name *string `ksql:"name"` + Age *int `ksql:"age"` + Address *Address `ksql:"address,json"` +} + +// Address ... +type Address struct { + State string `json:"state"` + City string `json:"city"` +} + +// UsersTable informs KSQL the name of the table and that it can +// use the default value for the primary key column name: "id" +var UsersTable = ksql.NewTable("users") + +func main() { + ctx := context.Background() + + // The available adapters are: + // - kpgx.New(ctx, connURL, ksql.Config{}) + // - kmysql.New(ctx, connURL, ksql.Config{}) + // - ksqlserver.New(ctx, connURL, ksql.Config{}) + // - ksqlite3.New(ctx, connURL, ksql.Config{}) + // + // For more detailed examples see: + // - `./examples/all_adapters/all_adapters.go` + // + // In this example we'll use sqlite3: + db, err := ksqlite3.New(ctx, "/tmp/hello.sqlite", ksql.Config{ + MaxOpenConns: 1, + }) + if err != nil { + panic(err.Error()) + } + defer db.Close() + + // In the definition below, please note that BLOB is + // the only type we can use in sqlite for storing JSON. + _, err = db.Exec(ctx, `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + age INTEGER, + name TEXT, + address BLOB + )`) + if err != nil { + panic(err.Error()) + } + + var alison = User{ + Name: "Alison", + Age: 22, + Address: Address{ + State: "MG", + }, + } + err = db.Insert(ctx, UsersTable, &alison) + if err != nil { + panic(err.Error()) + } + fmt.Println("Alison ID:", alison.ID) + + // Inserting inline: + err = db.Insert(ctx, UsersTable, &User{ + Name: "Cristina", + Age: 27, + Address: Address{ + State: "SP", + }, + }) + if err != nil { + panic(err.Error()) + } + + // Deleting Alison: + err = db.Delete(ctx, UsersTable, alison.ID) + if err != nil { + panic(err.Error()) + } + + // Retrieving Cristina, note that if you omit the SELECT part of the query + // KSQL will build it for you (efficiently) based on the fields from the struct: + var cris User + err = db.QueryOne(ctx, &cris, "FROM users WHERE name = ? ORDER BY id", "Cristina") + if err != nil { + panic(err.Error()) + } + fmt.Printf("Cristina: %#v\n", cris) + + // Updating all fields from Cristina: + cris.Name = "Cris" + err = db.Patch(ctx, UsersTable, cris) + + // Changing the age of Cristina but not touching any other fields: + + // Partial update technique 1: + err = db.Patch(ctx, UsersTable, struct { + ID int `ksql:"id"` + Age int `ksql:"age"` + }{ID: cris.ID, Age: 28}) + if err != nil { + panic(err.Error()) + } + + // Partial update technique 2: + err = db.Patch(ctx, UsersTable, PartialUpdateUser{ + ID: cris.ID, + Age: nullable.Int(28), // (just a pointer to an int, if null it won't be updated) + }) + if err != nil { + panic(err.Error()) + } + + // Listing first 10 users from the database + // (each time you run this example a new Cristina is created) + // + // Note: Using this function it is recommended to set a LIMIT, since + // not doing so can load too many users on your computer's memory or + // cause an Out Of Memory Kill. + // + // If you need to query very big numbers of users we recommend using + // the `QueryChunks` function. + var users []User + err = db.Query(ctx, &users, "FROM users LIMIT 10") + if err != nil { + panic(err.Error()) + } + + fmt.Printf("Users: %#v\n", users) + + // Making transactions: + err = db.Transaction(ctx, func(db ksql.Provider) error { + var cris2 User + err = db.QueryOne(ctx, &cris2, "FROM users WHERE id = ?", cris.ID) + if err != nil { + // This will cause an automatic rollback: + return err + } + + err = db.Patch(ctx, UsersTable, PartialUpdateUser{ + ID: cris2.ID, + Age: nullable.Int(29), + }) + if err != nil { + // This will also cause an automatic rollback and then panic again + // so that we don't hide the panic inside the KSQL library + panic(err.Error()) + } + + // Commits the transaction + return nil + }) + if err != nil { + panic(err.Error()) + } +} +``` + +## Benchmark Comparison + +The results of the benchmark are good: +they show that KSQL is in practical terms, +as fast as sqlx which was our goal from the start. + +To understand the benchmark below you must know +that all tests are performed using Postgres 12.1 and +that we are comparing the following tools: + +- KSQL using the adapter that wraps `database/sql` +- KSQL using the adapter that wraps `pgx` +- `database/sql` +- `sqlx` +- `pgx` (with `pgxpool`) +- `gorm` +- `sqlc` +- `sqlboiler` + +For each of these tools we are running 3 different queries: + +The `insert-one` query looks like: + +`INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id` + +The `single-row` query looks like: + +`SELECT id, name, age FROM users OFFSET $1 LIMIT 1` + +The `multiple-rows` query looks like: + +`SELECT id, name, age FROM users OFFSET $1 LIMIT 10` + +Keep in mind that some of the tools tested (like GORM) actually build +the queries internally so the actual code used for the benchmark +might differ a little bit from the example ones above. + +Without further ado, here are the results: + +```bash +$ make bench TIME=5s +{{- .benchmark -}} +``` + +## Running the KSQL tests (for contributors) + +The tests use `docker-test` for setting up all the supported databases, +which means that: + +- You need to have `docker` installed +- You must be able to run docker without `sudo`, i.e. + if you are not root you should add yourself to the docker group, e.g.: + + ```bash + $ sudo usermod -aG docker + ``` + And then restart your login session (or just reboot) + +After that you can just run the tests by using: + +```bash +make test +``` + +But it is recommended to first download the required images using: + +```bash +docker pull postgres:14.0 +docker pull mysql:8.0.27 +docker pull mcr.microsoft.com/mssql/server:2017-latest +``` + +Otherwise the first attempt to run the tests will +spend a long time downloading these images +and then fail because the `TestMain()` function +is configured to kill the containers after 20 seconds. + +## TODO List + +- Add support for serializing structs as other formats such as YAML +- Update `ksqltest.FillStructWith` to work with `ksql:"..,json"` tagged attributes +- Create a way for users to submit user defined dialects +- Improve error messages (ongoing) +- Add support for the Patch function to work with maps for partial updates +- Add support for the Insert function to work with maps +- Add support for a `ksql.Array(params ...interface{})` for allowing queries like this: + `db.Query(ctx, &user, "SELECT * FROM user WHERE id in (?)", ksql.Array(1,2,3))` +- Improve docs about `ksql.Mock` + +## Optimization Oportunities + +- Test if using a pointer on the field info is faster or not +- Consider passing the cached structInfo as argument for all the functions that use it, + so that we don't need to get it more than once in the same call. +- Use a cache to store often used queries (like pgx) +- Preload the insert method for all dialects inside `ksql.NewTable()` +- Use prepared statements for the helper functions, `Update`, `Insert` and `Delete`. + +## Features for a possible V2 + +- Change the `.Transaction(db ksql.Provider)` to a `.Transaction(ctx context.Context)` +- Make the `.Query()` method to return a `type Query interface { One(); All(); Chunks(); }` +- Have an `Update()` method that updates without ignoring NULLs as `Patch()` does +- Rename `NewTable()` to just `Table()` so it feels right to declare it inline when convenient diff --git a/scripts/build-readme-from-template.go b/scripts/build-readme-from-template.go new file mode 100755 index 0000000..af9dfa2 --- /dev/null +++ b/scripts/build-readme-from-template.go @@ -0,0 +1,45 @@ +package main + +import ( + "log" + "os" + "text/template" +) + +func main() { + if len(os.Args) < 3 { + log.Fatalf( + "USAGE: go run scripts/build-readme-from-template.go TEMPLATE_FILEPATH BENCHMARK_FILEPATH", + ) + } + + templateFilepath := os.Args[1] + benchmarkFilepath := os.Args[2] + + data, err := os.ReadFile(templateFilepath) + if err != nil { + log.Fatalf("unable read template '%s': %s", templateFilepath, err) + } + + t, err := template.New(templateFilepath).Parse(string(data)) + if err != nil { + log.Fatalf("unable to parse README template '%s': %s", templateFilepath, err) + } + + benchmark, err := os.ReadFile(benchmarkFilepath) + if err != nil { + log.Fatalf("unable to read benchmark results '%s': %s", benchmarkFilepath, err) + } + + f, err := os.OpenFile("README.md", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatalf("unable to open README.md for writing: %s", err) + } + + err = t.Execute(f, map[string]interface{}{ + "benchmark": string(benchmark), + }) + if err != nil { + log.Fatalf("error executing template file: %s", err) + } +}