diff --git a/.gitignore b/.gitignore index fb78403..b86a3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ *.o *.a *.so +cover.out # Folders _obj _test +.idea # Architecture specific extensions/prefixes *.[568vq] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39858dc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -go: - - 1.0 - - 1.1 - - 1.2 - - 1.3 - - tip diff --git a/LICENSE b/LICENSE index 7431811..cc317d9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2015 George Lester +Copyright (c) 2023 Daniel Munoz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 678f604..4e54ed8 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,40 @@ -NamedParameterQuery -==== - -[![Build Status](https://travis-ci.org/Knetic/go-namedParameterQuery.svg?branch=master)](https://travis-ci.org/Knetic/go-namedParameterQuery) -[![Godoc](https://godoc.org/github.com/Knetic/go-namedParameterQuery?status.png)](https://godoc.org/github.com/Knetic/go-namedParameterQuery) - +# NamedParameterQuery Provides support for named parameters in SQL queries used by Go / golang programs and libraries. SQL query parameters in go are positional. This means that when writing a query, you'll need to do it like this: - SELECT * FROM table - WHERE col1 = ? - AND col2 IN(?, ?, ?) - AND col3 = ? + SELECT * FROM table + WHERE col1 = ? + AND col2 IN(?, ?, ?) + AND col3 = ? Where "?" is a parameter that you want to replace with an actual value at runtime. Your code would need to look like this: - sql.QueryRow(queryText, "foo", "bar", "baz", "woot", "bar") + sql.QueryRow(queryText, "foo", "bar", "baz", "woot", "bar") As you can probably guess, this can lead to very unwieldy code in large queries. You wind up needing to keep track not only of how many parameters you have, but in what -order the query expects them. Sometimes you want to reference the same variable in more than one place in your query, which requires you to specify it more than once in your code! Refactoring your queries even once can lead to disastrous +order the query expects them. Sometimes you want to reference the same variable in more +than one place in your query, which requires you to specify it more than once in your code! +Refactoring your queries even once can lead to disastrous and annoying results. The answer to this is to use named parameters, which would look like this: - SELECT * FROM table - WHERE col1 = :userName - AND col2 IN(:firstName, :lastName, :middleName) - AND col3 = :firstname + SELECT * FROM table + WHERE col1 = :userName + AND col2 IN(:firstName, :lastName, :middleName) + AND col3 = :firstname -You would then add parameters to your query by name. This means you won't need to worry about what order your parameters are specified, nor how many times they appear. +You would then add parameters to your query by name. This means you won't need to worry about what +order your parameters are specified, nor how many times they appear. But golang doesn't support named parameters! That's what this library is for. -Why doesn't Go support this normally? --- +## Why doesn't Go support this normally? Go needs to support every kind of SQL server - and not all SQL servers support named parameters. @@ -50,39 +47,45 @@ on their own, but this polyfill works fine anyway. It's possible that someone else already implemented this, but I sure couldn't find a pre-existing solution when I needed it. -Isn't there a better way? --- +## Isn't there a better way? + In short, not across all databases, and not without complicating your query. -There are other ways to achieve the same effect on some databases. You can [register stored procedures which take positional parameters](http://www.mysqltutorial.org/stored-procedures-parameters.aspx), then call that procedure instead of writing a query. However that's a fairly specific use case - you don't always want to store your query permanently on the server; that means you have to worry about query versioning on the server, and complicates updates to queries during deployment, and precludes you from easily deploying new queries without damaging processes relying on the old ones. For most cases, sending the entire query every time you want to use it is the better option. +There are other ways to achieve the same effect on some databases. You can +[register stored procedures which take positional parameters](http://www.mysqltutorial.org/stored-procedures-parameters.aspx), +then call that procedure instead of writing a query. However that's a fairly specific use +case - you don't always want to store your query permanently on the server; that means you have to worry about query versioning on the server, and complicates updates to queries during deployment, and precludes you from easily deploying new queries without damaging processes relying on the old ones. For most cases, sending the entire query every time you want to use it is the better option. -Or, if your database supports it, you could [define user-local variables in your query](http://stackoverflow.com/questions/5154246/mysql-connector-j-allow-user-variables). Usually this requires a change to your DB, connectionstring, and queries. The syntax also varies across databases in unpredictable ways - meaning you're going to write less portable queries. +Or, if your database supports it, you could [define user-local variables in your query](http://stackoverflow.com/questions/5154246/mysql-connector-j-allow-user-variables). +Usually this requires a change to your DB, connectionstring, and queries. The syntax also varies across +databases in unpredictable ways - meaning you're going to write less portable queries. -Personally I don't find those options attractive. To me, a query ought to support named parameters without edits to your database. That's why this library exists. +Personally I don't find those options attractive. To me, a query ought to support named parameters +without edits to your database. That's why this library exists. -How do I use this? --- +## How do I use this? -Probably best to check out the API docs -http://godoc.org/github.com/Knetic/go-namedParameterQuery +Probably best to check out the API docs But here are some quick examples of the main use cases. - query := NewNamedParameterQuery(" - SELECT * FROM table - WHERE col1 = :foo - AND col2 IN(:firstName, :middleName, :lastName) - ") +### Using the parser directly + + query := namedparameter.NewQuery(` + SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName) + `) - query.SetValue("foo", "bar") - query.SetValue("firstName", "Alice") - query.SetValue("lastName", "Bob") - query.SetValue("middleName", "Eve") + query.SetValue("foo", "bar") + query.SetValue("firstName", "Alice") + query.SetValue("lastName", "Bob") + query.SetValue("middleName", "Eve") - connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") - connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) + connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) It doesn't matter what order you specify the parameters, or how many times they appear in the query, they're replaced as expected. @@ -90,23 +93,23 @@ they're replaced as expected. That looks a little tedious, and feels a lot like JDBC, where each parameter is given one line. But you can also add groups of parameters with a map: - query := NewNamedParameterQuery(" - SELECT * FROM table - WHERE col1 = :foo - AND col2 IN(:firstName, :middleName, :lastName) - ") + query := namedparameter.NewQuery(` + SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName) + `) - var parameterMap = map[string]interface{} { - "foo": "bar", - "firstName": "Alice", - "lastName": "Bob" - "middleName": "Eve", - } + var parameterMap = map[string]any { + "foo": "bar", + "firstName": "Alice", + "lastName": "Bob" + "middleName": "Eve", + } - query.SetValuesFromMap(parameterMap) + query.SetValuesFromMap(parameterMap) - connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") - connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) + connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) That example doesn't save any space because it defines the map immediately before using it, but if you already have a map of parameters available, this is easier. @@ -114,36 +117,110 @@ but if you already have a map of parameters available, this is easier. But maybe you know the benefits of strong typing, and want to add entire structs as parameters. No problem. - type QueryValues struct { - Foo string `sqlParameterName:"foo"` - FirstName string `sqlParameterName:"firstName"` - MiddleName string `sqlParameterName:"middleName"` - LastName string `sqlParameterName:"lirstName"` - } + type QueryValues struct { + Foo string `sqlParameterName:"foo"` + FirstName string `sqlParameterName:"firstName"` + MiddleName string `sqlParameterName:"middleName"` + LastName string `sqlParameterName:"lastName"` + } - query := NewNamedParameterQuery(" - SELECT * FROM table - WHERE col1 = :foo - AND col2 IN(:firstName, :middleName, :lastName) - ") + query := namedparameter.NewQuery(` + SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName) + `) - parameter = new(QueryValues) - query.SetValuesFromStruct(parameter) + parameter = new(QueryValues) + query.SetValuesFromStruct(parameter) - connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") - connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) + connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) When defining your struct, you don't *need* to add the "sqlParameterName" tags. But if your query uses lowercase variable names (as mine did), your struct will need to have exportable field names (as above) you can translate between the two with a tag. -Activity --- +### Using the wrappers + + db, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + defer db.Close() + + query := `SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName)` + + rows, err := namedparameter.Using(db).Query(query, + "foo", "bar", + "firstName", "Alice", + "lastName", "Smith", + "middleName", "Eve", + ) + +The order in which the parameters are passed doesn't matter, but they need to be passed in pairs key/value, +where the key has to be a string. The values can be any type supported by the driver in use. + +The arguments can be passed using a `map[string]any`: + + db, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + defer db.Close() + + query := `SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName)` + + var parameterMap = map[string]any { + "foo": "bar", + "firstName": "Alice", + "lastName": "Bob" + "middleName": "Eve", + } + + rows, err := namedparameter.Using(db).Query(query, parameterMap) + +`namedparameter.Using(..)` can wrap either a `*sql.DB` or a `*sql.Tx`, the methods supported by the wrapper are: + +```go +Query(string, ...args) (*sql.Rows, error) +QueryContext(context.Context, string, ...args) (*sql.Rows, error) +QueryRow(string, ...args) (*sql.Row, error) +QueryRowContext(context.Context, string, ...args) (*sql.Row, error) +Exec(string, ...args) (sql.Result, error) +ExecContext(context.Context, string, ...args) (sql.Result, error) +``` + +Notice that `QueryRow` and `QueryRowContext` can return an error (unlike the equivalent methods in `sql.DB` and +`sql.Tx`), this is because both the query parsing and the parameters processing can result in errors. + +There is also support for queries directly from a `*sql.Conn` and the use of prepared statements. + + db, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") + defer db.Close() + + query := `SELECT * FROM table + WHERE col1 = :foo + AND col2 IN(:firstName, :middleName, :lastName)` + + var parameterMap = map[string]any { + "foo": "bar", + "firstName": "Alice", + "lastName": "Bob" + "middleName": "Eve", + } + + conn, _ := db.Conn(context.Background()) + + stmt, _ := namedparameter.UsingConnection(conn).PrepareContext(context.Background, query) + + rows, err := stmt.QueryContext(context.Background(), query, parameterMap) -If this repository hasn't been updated in a while, it's probably because I don't have any outstanding issues to work on - it's not because I've abandoned the project. If you have questions, issues, or patches; I'm completely open to pull requests, issues opened on github, or emails from out of the blue. +`namedparameter.UsingConnection` supports all the context methods listed previously, and a wrapped +prepared statement will support all six methods. -License --- +## License -This implementation of Go named parameter queries is licensed under the MIT general use license. You're free to integrate, fork, and play with this code as you feel fit without consulting the author, as long as you provide proper credit to the author in your works. If you have questions, issues, or patches, I'm completely open to pull requests, issues opened on github, or emails from out of the blue. +This implementation of Go named parameter queries is licensed under the MIT general use license. +You're free to integrate, fork, and play with this code as you feel fit without consulting the +author, as long as you provide proper credit to the author in your works. If you have questions, +issues, or patches, I'm completely open to pull requests, issues opened on github, or +emails from out of the blue. diff --git a/benchmarks_test.go b/benchmarks_test.go index 4abe2ac..ce58a1c 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -1,42 +1,44 @@ package namedParameterQuery import ( - "testing" - "bytes" - "fmt" + "bytes" + "fmt" + "testing" + + "github.com/daniel-munoz/go-namedParameterQuery/namedparameter" ) func BenchmarkSimpleParsing(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux" - for i := 0; i < bench.N; i++ { + query := "SELECT [foo] FROM bar WHERE [baz] = :quux" + for i := 0; i < bench.N; i++ { - NewNamedParameterQuery(query) - } + namedparameter.NewQuery(query) + } } func BenchmarkMultiOccurrenceParsing(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + - "AND [something] = :quux " + - "OR [otherStuff] NOT :quux" + query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + + "AND [something] = :quux " + + "OR [otherStuff] NOT :quux" - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - NewNamedParameterQuery(query) - } + namedparameter.NewQuery(query) + } } func BenchmarkMultiParameterParsing(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + - "AND [something] = :quux2 " + - "OR [otherStuff] NOT :quux3" + query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + + "AND [something] = :quux2 " + + "OR [otherStuff] NOT :quux3" - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - NewNamedParameterQuery(query) - } + namedparameter.NewQuery(query) + } } /* @@ -44,13 +46,13 @@ func BenchmarkMultiParameterParsing(bench *testing.B) { */ func BenchmarkNoReplacement(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = quux" - replacer := NewNamedParameterQuery(query) + query := "SELECT [foo] FROM bar WHERE [baz] = quux" + replacer := namedparameter.NewQuery(query) - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - replacer.GetParsedParameters() - } + replacer.GetParsedParameters() + } } /* @@ -58,14 +60,14 @@ func BenchmarkNoReplacement(bench *testing.B) { */ func BenchmarkSingleReplacement(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux" - replacer := NewNamedParameterQuery(query) + query := "SELECT [foo] FROM bar WHERE [baz] = :quux" + replacer := namedparameter.NewQuery(query) - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - replacer.SetValue("quux", bench.N) - replacer.GetParsedParameters() - } + replacer.SetValue("quux", bench.N) + replacer.GetParsedParameters() + } } /* @@ -73,16 +75,16 @@ func BenchmarkSingleReplacement(bench *testing.B) { */ func BenchmarkMultiOccurrenceReplacement(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + - "AND [something] = :quux " + - "OR [otherStuff] NOT :quux" - replacer := NewNamedParameterQuery(query) + query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + + "AND [something] = :quux " + + "OR [otherStuff] NOT :quux" + replacer := namedparameter.NewQuery(query) - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - replacer.SetValue("quux", bench.N) - replacer.GetParsedParameters() - } + replacer.SetValue("quux", bench.N) + replacer.GetParsedParameters() + } } /* @@ -90,34 +92,34 @@ func BenchmarkMultiOccurrenceReplacement(bench *testing.B) { */ func BenchmarkMultiParameterReplacement(bench *testing.B) { - query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + - "AND [something] = :quux2 " + - "OR [otherStuff] NOT :quux3 " - replacer := NewNamedParameterQuery(query) + query := "SELECT [foo] FROM bar WHERE [baz] = :quux " + + "AND [something] = :quux2 " + + "OR [otherStuff] NOT :quux3 " + replacer := namedparameter.NewQuery(query) - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - replacer.SetValue("quux", bench.N) - replacer.SetValue("quux2", bench.N) - replacer.SetValue("quux3", bench.N) - replacer.GetParsedParameters() - } + replacer.SetValue("quux", bench.N) + replacer.SetValue("quux2", bench.N) + replacer.SetValue("quux3", bench.N) + replacer.GetParsedParameters() + } } func Benchmark16ParameterReplacement(bench *testing.B) { - benchmarkMultiParameter(bench, 16) + benchmarkMultiParameter(bench, 16) } func Benchmark32ParameterReplacement(bench *testing.B) { - benchmarkMultiParameter(bench, 32) + benchmarkMultiParameter(bench, 32) } func Benchmark64ParameterReplacement(bench *testing.B) { - benchmarkMultiParameter(bench, 64) + benchmarkMultiParameter(bench, 64) } func Benchmark128ParameterReplacement(bench *testing.B) { - benchmarkMultiParameter(bench, 128) + benchmarkMultiParameter(bench, 128) } /* @@ -125,26 +127,26 @@ func Benchmark128ParameterReplacement(bench *testing.B) { */ func benchmarkMultiParameter(bench *testing.B, parameterCount int) { - var queryBuffer bytes.Buffer - var parameterName string + var queryBuffer bytes.Buffer + var parameterName string - queryBuffer.WriteString("SELECT [foo] FROM bar WHERE [baz] = :quux ") - queryLine := "AND [something] = :quux%d " + queryBuffer.WriteString("SELECT [foo] FROM bar WHERE [baz] = :quux ") + queryLine := "AND [something] = :quux%d " - for i := 0; i < parameterCount; i++ { + for i := 0; i < parameterCount; i++ { - queryBuffer.WriteString(fmt.Sprintf(queryLine, i)) - } + queryBuffer.WriteString(fmt.Sprintf(queryLine, i)) + } - replacer := NewNamedParameterQuery(queryBuffer.String()) + replacer := namedparameter.NewQuery(queryBuffer.String()) - for i := 0; i < bench.N; i++ { + for i := 0; i < bench.N; i++ { - for n := 0; n < parameterCount; n++ { - parameterName = fmt.Sprintf("quux%d", n) - replacer.SetValue(parameterName, bench.N) - } + for n := 0; n < parameterCount; n++ { + parameterName = fmt.Sprintf("quux%d", n) + replacer.SetValue(parameterName, bench.N) + } - replacer.GetParsedParameters() - } + replacer.GetParsedParameters() + } } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1d0817d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/daniel-munoz/go-namedParameterQuery + +require github.com/DATA-DOG/go-sqlmock v1.5.0 + +go 1.18 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0db0763 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= diff --git a/namedparameter/common.go b/namedparameter/common.go new file mode 100644 index 0000000..3a93c19 --- /dev/null +++ b/namedparameter/common.go @@ -0,0 +1,51 @@ +package namedparameter + +import ( + "errors" +) + +// convertArgsToMap converts a list of arguments into a key value map, where the keys are the parameter names. +// This method expects the parameter arguments to either being just a map[string]any or to come in pairs, in which case +// it processes them as key, value pairs, keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func convertArgsToMap(args []any) (map[string]any, error) { + if len(args) == 0 { + return nil, nil + } + if len(args) == 1 { + if params, ok := args[0].(map[string]any); ok { + return params, nil + } + } + if len(args)%2 != 0 { + return nil, errors.New("number of arguments passed to parameterized query is not correct, expected an even number of arguments") + } + params := make(map[string]any) + for i := 0; i < len(args); i += 2 { + key, val := args[i], args[i+1] + if keyStr, ok := key.(string); !ok { + return nil, errors.New("parameter representing a key needs to be a string") + } else { + params[keyStr] = val + } + } + return params, nil +} + +// parse automates all the process of processing the query and arguments in one place, in order to avoid +// doing this in every other method. +func parse(query string, args []any) (parsedQuery string, params []any, err error) { + var mappedArgs map[string]any + + mappedArgs, err = convertArgsToMap(args) + if err != nil { + return + } + paramQuery := NewQuery(query) + paramQuery.SetValuesFromMap(mappedArgs) + + parsedQuery = paramQuery.GetParsedQuery() + params = paramQuery.GetParsedParameters() + + return +} diff --git a/namedparameter/common_test.go b/namedparameter/common_test.go new file mode 100644 index 0000000..686953b --- /dev/null +++ b/namedparameter/common_test.go @@ -0,0 +1,69 @@ +package namedparameter + +import ( + "reflect" + "testing" +) + +func Test_convertArgsToMap(t *testing.T) { + type args []any + tests := []struct { + name string + args args + want map[string]any + wantErr bool + }{ + { + name: "Simple map case", + args: args{map[string]any{"id": 1, "name": "John", "job": nil}}, + want: map[string]any{"id": 1, "name": "John", "job": nil}, + }, + { + name: "Simple list of params", + args: args{"id", 1, "name", "John", "job", nil}, + want: map[string]any{"id": 1, "name": "John", "job": nil}, + }, + { + name: "Simple list of params", + args: args{"id", 1, "name", "John", "job", nil}, + want: map[string]any{"id": 1, "name": "John", "job": nil}, + }, + { + name: "nil args", + args: nil, + want: nil, + }, + { + name: "Empty args", + args: args{}, + want: nil, + }, + { + name: "One argument, not a map", + args: args{"id"}, + wantErr: true, + }, + { + name: "Odd number of arguments, not a map", + args: args{"id", 1, "name"}, + wantErr: true, + }, + { + name: "Key not a string", + args: args{"id", 1, 25, "name"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := convertArgsToMap(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("convertArgsToMap() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertArgsToMap() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/namedparameter/conn.go b/namedparameter/conn.go new file mode 100644 index 0000000..8613f14 --- /dev/null +++ b/namedparameter/conn.go @@ -0,0 +1,61 @@ +package namedparameter + +import ( + "context" + "database/sql" +) + +// ConnectionWrapper wraps a sql.Conn and adds methods that can be used with parameterized queries. +type ConnectionWrapper struct { + conn *sql.Conn +} + +// UsingConnection wraps a *sql.Conn, in order to decorate it with parameterized methods. +func UsingConnection(conn *sql.Conn) *ConnectionWrapper { + return &ConnectionWrapper{conn: conn} +} + +// QueryContext performs a parameterized query using the expanded args to feed the parameter values. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *ConnectionWrapper) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.conn.QueryContext(ctx, parsedQuery) + } + return w.conn.QueryContext(ctx, parsedQuery, params...) +} + +// QueryRowContext performs a parameterized query using the expanded args to feed the parameter values and returns one row. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *ConnectionWrapper) QueryRowContext(ctx context.Context, query string, args ...any) (*sql.Row, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.conn.QueryRowContext(ctx, parsedQuery), nil + } + return w.conn.QueryRowContext(ctx, parsedQuery, params...), nil +} + +// ExecContext executes a parameterized sql instruction using the expanded args to feed the parameter values and returns the results. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *ConnectionWrapper) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.conn.ExecContext(ctx, parsedQuery) + } + return w.conn.ExecContext(ctx, parsedQuery, params...) +} diff --git a/namedparameter/conn_test.go b/namedparameter/conn_test.go new file mode 100644 index 0000000..bd2889f --- /dev/null +++ b/namedparameter/conn_test.go @@ -0,0 +1,385 @@ +package namedparameter + +import ( + "context" + "database/sql" + "database/sql/driver" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestConnectionWrapper_QueryContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + ctx := context.Background() + conn, dbErr := db.Conn(ctx) + if dbErr != nil { + t.Errorf("Error obtaining connection - error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := UsingConnection(conn).QueryContext(ctx, tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestConnectionWrapper_QueryRowContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + ctx := context.Background() + conn, dbErr := db.Conn(ctx) + if dbErr != nil { + t.Errorf("Error obtaining connection - error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := UsingConnection(conn).QueryRowContext(ctx, tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRowContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestConnectionWrapper_ExecContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + ctx := context.Background() + conn, dbErr := db.Conn(ctx) + if dbErr != nil { + t.Errorf("Error obtaining connection - error = %v", dbErr) + } + + mock.ExpectExec(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + + _, err := UsingConnection(conn).ExecContext(context.Background(), tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("ExecContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} diff --git a/namedparameter/db_tx.go b/namedparameter/db_tx.go new file mode 100644 index 0000000..7a5128c --- /dev/null +++ b/namedparameter/db_tx.go @@ -0,0 +1,119 @@ +package namedparameter + +import ( + "context" + "database/sql" +) + +// WrappableDBObject is an interface that can cover both a sql.DB or a sql.Tx object, making parameterized +// queries usable for any of these without having to implement wrappers for each. +type WrappableDBObject interface { + Exec(string, ...any) (sql.Result, error) + ExecContext(context.Context, string, ...any) (sql.Result, error) + Query(string, ...any) (*sql.Rows, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRow(string, ...any) *sql.Row + QueryRowContext(context.Context, string, ...any) *sql.Row + Prepare(string) (*sql.Stmt, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) +} + +// DBObjectWrapper wraps either a sql.DB or a sql.Tx and adds methods that can be used with parameterized queries. +type DBObjectWrapper struct { + wrappedDBObject WrappableDBObject +} + +// Using wraps a *sql.DB or a *sql.Tx, in order to decorate them with parameterized methods. +func Using(dbObject WrappableDBObject) *DBObjectWrapper { + return &DBObjectWrapper{wrappedDBObject: dbObject} +} + +// Query performs a parameterized query using the expanded args to feed the parameter values. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) Query(query string, args ...any) (*sql.Rows, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.Query(parsedQuery) + } + return w.wrappedDBObject.Query(parsedQuery, params...) +} + +// QueryContext performs a parameterized query using the expanded args to feed the parameter values. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.QueryContext(ctx, parsedQuery) + } + return w.wrappedDBObject.QueryContext(ctx, parsedQuery, params...) +} + +// QueryRow performs a parameterized query using the expanded args to feed the parameter values and returns one row. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) QueryRow(query string, args ...any) (*sql.Row, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.QueryRow(parsedQuery), nil + } + return w.wrappedDBObject.QueryRow(parsedQuery, params...), nil +} + +// QueryRowContext performs a parameterized query using the expanded args to feed the parameter values and returns one row. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) QueryRowContext(ctx context.Context, query string, args ...any) (*sql.Row, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.QueryRowContext(ctx, parsedQuery), nil + } + return w.wrappedDBObject.QueryRowContext(ctx, parsedQuery, params...), nil +} + +// Exec executes a parameterized sql instruction using the expanded args to feed the parameter values and returns the results. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) Exec(query string, args ...any) (sql.Result, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.Exec(parsedQuery) + } + return w.wrappedDBObject.Exec(parsedQuery, params...) +} + +// ExecContext executes a parameterized sql instruction using the expanded args to feed the parameter values and returns the results. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *DBObjectWrapper) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { + parsedQuery, params, err := parse(query, args) + if err != nil { + return nil, err + } + if len(params) == 0 { + return w.wrappedDBObject.ExecContext(ctx, parsedQuery) + } + return w.wrappedDBObject.ExecContext(ctx, parsedQuery, params...) +} diff --git a/namedparameter/db_tx_test.go b/namedparameter/db_tx_test.go new file mode 100644 index 0000000..2c89d6d --- /dev/null +++ b/namedparameter/db_tx_test.go @@ -0,0 +1,1461 @@ +package namedparameter + +import ( + "context" + "database/sql" + "database/sql/driver" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestDBObjectWrapper_Query(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := Using(db).Query(tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Query() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := Using(db).QueryContext(context.Background(), tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryRow(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := Using(db).QueryRow(tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRow() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryRowContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + _, err := Using(db).QueryRowContext(context.Background(), tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRowContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_Exec(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectExec(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + + _, err := Using(db).Exec(tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Exec() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_ExecContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectExec(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + + _, err := Using(db).ExecContext(context.Background(), tt.query, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("ExecContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} +func TestDBObjectWrapper_QueryForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).Query(tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Query() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryContextForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).QueryContext(context.Background(), tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryRowForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).QueryRow(tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRow() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_QueryRowContextForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectQuery(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).QueryRowContext(context.Background(), tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRowContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_ExecForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectExec(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).Exec(tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Exec() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} + +func TestDBObjectWrapper_ExecContextForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectExec(tt.wantQuery).WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + tx, _ := db.Begin() + _, err := Using(tx).ExecContext(context.Background(), tt.query, tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("ExecContext() error = %v, dbErr = %v, wantErr %v", err, dbErr, tt.wantErr) + return + } + }) + } +} diff --git a/namedParameterQuery.go b/namedparameter/namedParameterQuery.go similarity index 78% rename from namedParameterQuery.go rename to namedparameter/namedParameterQuery.go index d8cd8f4..9ee2150 100644 --- a/namedParameterQuery.go +++ b/namedparameter/namedParameterQuery.go @@ -8,7 +8,7 @@ Example usage: - query := NewNamedParameterQuery(" + query := NewQuery(" SELECT * FROM table WHERE col1 = :foo ") @@ -26,13 +26,13 @@ It's also possible to pass in a map, instead of defining each parameter individually: - query := NewNamedParameterQuery(" + query := NewQuery(" SELECT * FROM table WHERE col1 = :foo AND col2 IN(:firstName, :middleName, :lastName) ") - var parameterMap = map[string]interface{} { + var parameterMap = map[string]any { "foo": "bar", "firstName": "Alice", "lastName": "Bob" @@ -53,7 +53,7 @@ LastName string `sqlParameterName:"lirstName"` } - query := NewNamedParameterQuery(" + query := NewQuery(" SELECT * FROM table WHERE col1 = :foo AND col2 IN(:firstName, :middleName, :lastName) @@ -65,7 +65,7 @@ connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db") connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...) */ -package namedParameterQuery +package namedparameter import ( "bytes" @@ -78,18 +78,18 @@ import ( /* NamedParameterQuery handles the translation of named parameters to positional parameters, for SQL statements. It is not recommended to create zero-valued NamedParameterQuery objects by yourself; - instead use NewNamedParameterQuery + instead use NewQuery */ -type NamedParameterQuery struct { +type Query struct { // A map of parameter names as keys, with value as a slice of positional indices which match // that parameter. positions map[string][]int // Contains all positional parameters, in order, ready to be used in the positional query. - parameters []interface{} + parameters []any - // The query containing named parameters, as passed in by NewNamedParameterQuery + // The query containing named parameters, as passed in by NewQuery originalQuery string // The query containing positional parameters, as generated by setQuery @@ -97,7 +97,7 @@ type NamedParameterQuery struct { } /* - NewNamedParameterQuery creates a new named parameter query using the given [queryText] as a SQL query which + NewQuery creates a new named parameter query using the given [queryText] as a SQL query which contains named parameters. Named parameters are identified by starting with a ":" e.g., ":name" refers to the parameter "name", and ":foo" refers to the parameter "foo". @@ -105,14 +105,14 @@ type NamedParameterQuery struct { they cannot be inside quoted strings, and cannot inject statements into a query. They can only be used to insert values. */ -func NewNamedParameterQuery(queryText string) (*NamedParameterQuery) { +func NewQuery(queryText string) *Query { - var ret *NamedParameterQuery + var ret *Query // TODO: I don't like using a map for such a small amount of elements. // If this becomes a bottleneck for anyone, the first thing to do would // be to make a slice and search routine for parameter positions. - ret = new(NamedParameterQuery) + ret = new(Query) ret.positions = make(map[string][]int, 8) ret.setQuery(queryText) @@ -123,7 +123,7 @@ func NewNamedParameterQuery(queryText string) (*NamedParameterQuery) { setQuery parses out all named parameters, stores their locations, and builds a "revised" query which uses positional parameters. */ -func (this *NamedParameterQuery) setQuery(queryText string) { +func (q *Query) setQuery(queryText string) { var revisedBuilder bytes.Buffer var parameterBuilder bytes.Buffer @@ -133,7 +133,7 @@ func (this *NamedParameterQuery) setQuery(queryText string) { var width int var positionIndex int - this.originalQuery = queryText + q.originalQuery = queryText positionIndex = 0 for i := 0; i < len(queryText); { @@ -142,9 +142,9 @@ func (this *NamedParameterQuery) setQuery(queryText string) { i += width // if it's a colon, do not write to builder, but grab name - if(character == ':') { + if character == ':' { - for ;; { + for { character, width = utf8.DecodeRuneInString(queryText[i:]) i += width @@ -158,14 +158,14 @@ func (this *NamedParameterQuery) setQuery(queryText string) { // add to positions parameterName = parameterBuilder.String() - position = this.positions[parameterName] - this.positions[parameterName] = append(position, positionIndex) + position = q.positions[parameterName] + q.positions[parameterName] = append(position, positionIndex) positionIndex++ revisedBuilder.WriteString("?") parameterBuilder.Reset() - if(width <= 0) { + if width <= 0 { break } } @@ -174,39 +174,39 @@ func (this *NamedParameterQuery) setQuery(queryText string) { revisedBuilder.WriteString(string(character)) // if it's a quote, continue writing to builder, but do not search for parameters. - if(character == '\'') { + if character == '\'' { - for ;; { + for { character, width = utf8.DecodeRuneInString(queryText[i:]) i += width revisedBuilder.WriteString(string(character)) - if(character == '\'') { + if character == '\'' { break } } } } - this.revisedQuery = revisedBuilder.String() - this.parameters = make([]interface{}, positionIndex) + q.revisedQuery = revisedBuilder.String() + q.parameters = make([]any, positionIndex) } /* GetParsedQuery returns a version of the original query text whose named parameters have been replaced by positional parameters. */ -func (this *NamedParameterQuery) GetParsedQuery() (string) { - return this.revisedQuery +func (q *Query) GetParsedQuery() string { + return q.revisedQuery } /* GetParsedParameters returns an array of parameter objects that match the positional parameter list from GetParsedQuery */ -func (this *NamedParameterQuery) GetParsedParameters() ([]interface{}) { - return this.parameters +func (q *Query) GetParsedParameters() []any { + return q.parameters } /* @@ -214,10 +214,10 @@ func (this *NamedParameterQuery) GetParsedParameters() ([]interface{}) { If the parsed query does not have a placeholder for the given [parameterName], this method does nothing. */ -func (this *NamedParameterQuery) SetValue(parameterName string, parameterValue interface{}) { +func (q *Query) SetValue(parameterName string, parameterValue any) { - for _, position := range this.positions[parameterName] { - this.parameters[position] = parameterValue + for _, position := range q.positions[parameterName] { + q.parameters[position] = parameterValue } } @@ -228,10 +228,10 @@ func (this *NamedParameterQuery) SetValue(parameterName string, parameterValue i If there are any keys/values present in the map that aren't part of the query, they are ignored. */ -func (this *NamedParameterQuery) SetValuesFromMap(parameters map[string]interface{}) { +func (q *Query) SetValuesFromMap(parameters map[string]any) { for name, value := range parameters { - this.SetValue(name, value) + q.SetValue(name, value) } } @@ -248,7 +248,7 @@ func (this *NamedParameterQuery) SetValuesFromMap(parameters map[string]interfac Foo string `sqlParameterName:"foobar"` } */ -func (this *NamedParameterQuery) SetValuesFromStruct(parameters interface{}) (error) { +func (q *Query) SetValuesFromStruct(parameters any) error { var fieldValues reflect.Value var fieldValue reflect.Value @@ -259,8 +259,8 @@ func (this *NamedParameterQuery) SetValuesFromStruct(parameters interface{}) (er fieldValues = reflect.ValueOf(parameters) - if(fieldValues.Kind() != reflect.Struct) { - return errors.New("Unable to add query values from parameter: parameter is not a struct") + if fieldValues.Kind() != reflect.Struct { + return errors.New("unable to add query values from parameter: parameter is not a struct") } parameterType = fieldValues.Type() @@ -273,17 +273,17 @@ func (this *NamedParameterQuery) SetValuesFromStruct(parameters interface{}) (er // public field? visibilityCharacter, _ = utf8.DecodeRuneInString(parameterField.Name[0:]) - if(fieldValue.CanSet() || unicode.IsUpper(visibilityCharacter)) { + if fieldValue.CanSet() || unicode.IsUpper(visibilityCharacter) { // check to see if this has a tag indicating a different query name queryTag = parameterField.Tag.Get("sqlParameterName") // otherwise just add the struct's name. - if(len(queryTag) <= 0) { + if len(queryTag) <= 0 { queryTag = parameterField.Name } - this.SetValue(queryTag, fieldValue.Interface()) + q.SetValue(queryTag, fieldValue.Interface()) } } return nil diff --git a/namedParameterQuery_test.go b/namedparameter/namedParameterQuery_test.go similarity index 53% rename from namedParameterQuery_test.go rename to namedparameter/namedParameterQuery_test.go index 19222be..d234cf8 100644 --- a/namedParameterQuery_test.go +++ b/namedparameter/namedParameterQuery_test.go @@ -1,4 +1,4 @@ -package namedParameterQuery +package namedparameter import ( "testing" @@ -10,9 +10,9 @@ import ( does not match the [Expected] string, the test fails */ type QueryParsingTest struct { - Name string - Input string - Expected string + Name string + Input string + Expected string ExpectedParameters int } @@ -23,104 +23,103 @@ type QueryParsingTest struct { These tests specifically check type of output parameters, too. */ type ParameterParsingTest struct { - - Name string - Query string - Parameters []TestQueryParameter + Name string + Query string + Parameters []TestQueryParameter ExpectedParameters []interface{} } type TestQueryParameter struct { - Name string + Name string Value interface{} } func TestQueryParsing(test *testing.T) { - var query *NamedParameterQuery + var query *Query // Each of these represents a single test. - queryParsingTests := []QueryParsingTest { - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = 1", + queryParsingTests := []QueryParsingTest{ + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = 1", Expected: "SELECT * FROM table WHERE col1 = 1", - Name: "NoParameter", + Name: "NoParameter", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :name", - Expected: "SELECT * FROM table WHERE col1 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :name", + Expected: "SELECT * FROM table WHERE col1 = ?", ExpectedParameters: 1, - Name: "SingleParameter", + Name: "SingleParameter", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :name AND col2 = :occupation", - Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :name AND col2 = :occupation", + Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", ExpectedParameters: 2, - Name: "TwoParameters", + Name: "TwoParameters", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :name AND col2 = :occupation", - Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :name AND col2 = :occupation", + Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", ExpectedParameters: 2, - Name: "OneParameterMultipleTimes", + Name: "OneParameterMultipleTimes", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 IN (:something, :else)", - Expected: "SELECT * FROM table WHERE col1 IN (?, ?)", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 IN (:something, :else)", + Expected: "SELECT * FROM table WHERE col1 IN (?, ?)", ExpectedParameters: 2, - Name: "ParametersInParenthesis", + Name: "ParametersInParenthesis", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = ':literal' AND col2 LIKE ':literal'", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = ':literal' AND col2 LIKE ':literal'", Expected: "SELECT * FROM table WHERE col1 = ':literal' AND col2 LIKE ':literal'", - Name: "ParametersInQuotes", + Name: "ParametersInQuotes", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = ':literal' AND col2 = :literal AND col3 LIKE ':literal'", - Expected: "SELECT * FROM table WHERE col1 = ':literal' AND col2 = ? AND col3 LIKE ':literal'", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = ':literal' AND col2 = :literal AND col3 LIKE ':literal'", + Expected: "SELECT * FROM table WHERE col1 = ':literal' AND col2 = ? AND col3 LIKE ':literal'", ExpectedParameters: 1, - Name: "ParametersInQuotes2", + Name: "ParametersInQuotes2", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :foo AND col2 IN (SELECT id FROM tabl2 WHERE col10 = :bar)", - Expected: "SELECT * FROM table WHERE col1 = ? AND col2 IN (SELECT id FROM tabl2 WHERE col10 = ?)", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :foo AND col2 IN (SELECT id FROM tabl2 WHERE col10 = :bar)", + Expected: "SELECT * FROM table WHERE col1 = ? AND col2 IN (SELECT id FROM tabl2 WHERE col10 = ?)", ExpectedParameters: 2, - Name: "ParametersInSubclause", + Name: "ParametersInSubclause", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :1234567890 AND col2 = :0987654321", - Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :1234567890 AND col2 = :0987654321", + Expected: "SELECT * FROM table WHERE col1 = ? AND col2 = ?", ExpectedParameters: 2, - Name: "NumericParameters", + Name: "NumericParameters", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :ABCDEFGHIJKLMNOPQRSTUVWXYZ", - Expected: "SELECT * FROM table WHERE col1 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :ABCDEFGHIJKLMNOPQRSTUVWXYZ", + Expected: "SELECT * FROM table WHERE col1 = ?", ExpectedParameters: 1, - Name: "CapsParameters", + Name: "CapsParameters", }, - QueryParsingTest { - Input: "SELECT * FROM table WHERE col1 = :abc123ABC098", - Expected: "SELECT * FROM table WHERE col1 = ?", + QueryParsingTest{ + Input: "SELECT * FROM table WHERE col1 = :abc123ABC098", + Expected: "SELECT * FROM table WHERE col1 = ?", ExpectedParameters: 1, - Name: "AltcapsParameters", + Name: "AltcapsParameters", }, } // Run each test. for _, parsingTest := range queryParsingTests { - query = NewNamedParameterQuery(parsingTest.Input) + query = NewQuery(parsingTest.Input) // test query texts - if(query.GetParsedQuery() != parsingTest.Expected) { + if query.GetParsedQuery() != parsingTest.Expected { test.Log("Test '", parsingTest.Name, "': Expected query text did not match actual parsed output") test.Log("Actual: ", query.GetParsedQuery()) test.Fail() } // test parameters - if(len(query.GetParsedParameters()) != parsingTest.ExpectedParameters) { + if len(query.GetParsedParameters()) != parsingTest.ExpectedParameters { test.Log("Test '", parsingTest.Name, "': Expected parameters did not match actual parsed parameter count") test.Fail() } @@ -135,114 +134,114 @@ func TestQueryParsing(test *testing.T) { */ func TestParameterReplacement(test *testing.T) { - var query *NamedParameterQuery + var query *Query var parameterMap map[string]interface{} // note that if you're adding or editing these tests, // you'll also want to edit the associated struct for this test below, // in the next test func. - queryVariableTests := []ParameterParsingTest { - ParameterParsingTest { + queryVariableTests := []ParameterParsingTest{ + ParameterParsingTest{ - Name: "SingleStringParameter", + Name: "SingleStringParameter", Query: "SELECT * FROM table WHERE col1 = :foo", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "foo", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "foo", Value: "bar", }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "bar", }, }, - ParameterParsingTest { + ParameterParsingTest{ - Name: "TwoStringParameter", + Name: "TwoStringParameter", Query: "SELECT * FROM table WHERE col1 = :foo AND col2 = :foo2", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "foo", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "foo", Value: "bar", }, - TestQueryParameter { - Name: "foo2", + TestQueryParameter{ + Name: "foo2", Value: "bart", }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "bar", "bart", }, }, - ParameterParsingTest { + ParameterParsingTest{ - Name: "TwiceOccurringParameter", + Name: "TwiceOccurringParameter", Query: "SELECT * FROM table WHERE col1 = :foo AND col2 = :foo", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "foo", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "foo", Value: "bar", }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "bar", "bar", }, }, - ParameterParsingTest { + ParameterParsingTest{ - Name: "ParameterTyping", + Name: "ParameterTyping", Query: "SELECT * FROM table WHERE col1 = :str AND col2 = :int AND col3 = :pi", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "str", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "str", Value: "foo", }, - TestQueryParameter { - Name: "int", + TestQueryParameter{ + Name: "int", Value: 1, }, - TestQueryParameter { - Name: "pi", + TestQueryParameter{ + Name: "pi", Value: 3.14, }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "foo", 1, 3.14, }, }, - ParameterParsingTest { + ParameterParsingTest{ - Name: "ParameterOrdering", + Name: "ParameterOrdering", Query: "SELECT * FROM table WHERE col1 = :foo AND col2 = :bar AND col3 = :foo AND col4 = :foo AND col5 = :bar", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "foo", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "foo", Value: "something", }, - TestQueryParameter { - Name: "bar", + TestQueryParameter{ + Name: "bar", Value: "else", }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "something", "else", "something", "something", "else", }, }, - ParameterParsingTest { + ParameterParsingTest{ - Name: "ParameterCaseSensitivity", + Name: "ParameterCaseSensitivity", Query: "SELECT * FROM table WHERE col1 = :foo AND col2 = :FOO", - Parameters: []TestQueryParameter { - TestQueryParameter { - Name: "foo", + Parameters: []TestQueryParameter{ + TestQueryParameter{ + Name: "foo", Value: "baz", }, - TestQueryParameter { - Name: "FOO", + TestQueryParameter{ + Name: "FOO", Value: "quux", }, }, - ExpectedParameters: []interface{} { + ExpectedParameters: []interface{}{ "baz", "quux", }, }, @@ -253,7 +252,7 @@ func TestParameterReplacement(test *testing.T) { // parse query and set values. parameterMap = make(map[string]interface{}, 8) - query = NewNamedParameterQuery(variableTest.Query) + query = NewQuery(variableTest.Query) for _, queryVariable := range variableTest.Parameters { query.SetValue(queryVariable.Name, queryVariable.Value) @@ -263,19 +262,19 @@ func TestParameterReplacement(test *testing.T) { // Test outputs for index, queryVariable := range query.GetParsedParameters() { - if(queryVariable != variableTest.ExpectedParameters[index]) { + if queryVariable != variableTest.ExpectedParameters[index] { test.Log("Test '", variableTest.Name, "' did not produce the expected parameter output. Actual: '", queryVariable, "', Expected: '", variableTest.ExpectedParameters[index], "'") test.Fail() } } - query = NewNamedParameterQuery(variableTest.Query) + query = NewQuery(variableTest.Query) query.SetValuesFromMap(parameterMap) // test map parameter outputs. for index, queryVariable := range query.GetParsedParameters() { - if(queryVariable != variableTest.ExpectedParameters[index]) { + if queryVariable != variableTest.ExpectedParameters[index] { test.Log("Test '", variableTest.Name, "' did not produce the expected parameter output when using parameter map. Actual: '", queryVariable, "', Expected: '", variableTest.ExpectedParameters[index], "'") test.Fail() } @@ -289,16 +288,16 @@ func TestParameterReplacement(test *testing.T) { // TODO: Figure out a way to tie this together with tests for maps/singles. // Right now, each test needs to be hand-defined with its own struct and test method. type SingleParameterTest struct { - Foo string - Bar string - Baz int - unexported string + Foo string + Bar string + Baz int + unexported string notExported int } func TestStructParameters(test *testing.T) { - var query *NamedParameterQuery + var query *Query var singleParam SingleParameterTest singleParam.Foo = "foo" @@ -308,20 +307,20 @@ func TestStructParameters(test *testing.T) { singleParam.notExported = -1 // - query = NewNamedParameterQuery("SELECT * FROM table WHERE col1 = :Foo AND col2 = :Bar AND col3 = :Baz") + query = NewQuery("SELECT * FROM table WHERE col1 = :Foo AND col2 = :Bar AND col3 = :Baz") query.SetValuesFromStruct(singleParam) - verifyStructParameters("MultipleStructReplacement", test, query, []interface{} { + verifyStructParameters("MultipleStructReplacement", test, query, []interface{}{ "foo", "bar", 15, }) // - query = NewNamedParameterQuery("SELECT * FROM table WHERE col1 = :Foo AND col2 = :Bar AND col3 = :Foo AND col4 = :Foo AND col5 = :Baz") + query = NewQuery("SELECT * FROM table WHERE col1 = :Foo AND col2 = :Bar AND col3 = :Foo AND col4 = :Foo AND col5 = :Baz") query.SetValuesFromStruct(singleParam) - verifyStructParameters("RecurringStructParameterReplacement", test, query, []interface{} { + verifyStructParameters("RecurringStructParameterReplacement", test, query, []interface{}{ "foo", "bar", "foo", @@ -330,17 +329,22 @@ func TestStructParameters(test *testing.T) { }) // - query = NewNamedParameterQuery("SELECT * FROM table WHERE col1 = :unexported AND col2 = :notExported AND col3 = :Foo") + query = NewQuery("SELECT * FROM table WHERE col1 = :unexported AND col2 = :notExported AND col3 = :Foo") query.SetValuesFromStruct(singleParam) - verifyStructParameters("UnexportedStructReplacement", test, query, []interface{} { + verifyStructParameters("UnexportedStructReplacement", test, query, []interface{}{ nil, nil, "foo", }) + + query = NewQuery("SELECT * FROM table WHERE col1 = :unexported AND col2 = :notExported AND col3 = :Foo") + if query.SetValuesFromStruct("bar") == nil { + test.Fail() + } } -func verifyStructParameters(testName string, test *testing.T, query *NamedParameterQuery, expectedParameters []interface{}) { +func verifyStructParameters(testName string, test *testing.T, query *Query, expectedParameters []interface{}) { var actualParameters []interface{} @@ -349,13 +353,13 @@ func verifyStructParameters(testName string, test *testing.T, query *NamedParame actualParameterLength := len(actualParameters) expectedParameterLength := len(expectedParameters) - if(actualParameterLength != expectedParameterLength) { + if actualParameterLength != expectedParameterLength { test.Log("Test ", testName, ": Actual parameters (", actualParameterLength, ") returned from struct query did not match expected parameters (", expectedParameterLength, ")") test.Fail() } for index, parameter := range actualParameters { - if(parameter != expectedParameters[index]) { + if parameter != expectedParameters[index] { test.Log("Test ", testName, ": Actual parameter at position ", index, " (", parameter, ") did not match expected parameter (", expectedParameters[index], ")") test.Fail() } diff --git a/namedparameter/statement.go b/namedparameter/statement.go new file mode 100644 index 0000000..0833fae --- /dev/null +++ b/namedparameter/statement.go @@ -0,0 +1,141 @@ +package namedparameter + +import ( + "context" + "database/sql" +) + +// StatementWrapper wraps a sql.Stmt and adds methods that can be used with parameterized queries. +type StatementWrapper struct { + stmt *sql.Stmt + paramQuery *Query +} + +// Prepare prepares a statement from a parameterized query and returns a wrapper that can be used with +// named parameters. +func (w *DBObjectWrapper) Prepare(query string) (*StatementWrapper, error) { + paramQuery := NewQuery(query) + if stmt, err := w.wrappedDBObject.Prepare(paramQuery.GetParsedQuery()); err != nil { + return nil, err + } else { + return &StatementWrapper{stmt: stmt, paramQuery: paramQuery}, nil + } +} + +// PrepareContext prepares a statement from a parameterized query and returns a wrapper that can be used with +// named parameters. +func (w *DBObjectWrapper) PrepareContext(ctx context.Context, query string) (*StatementWrapper, error) { + paramQuery := NewQuery(query) + if stmt, err := w.wrappedDBObject.PrepareContext(ctx, paramQuery.GetParsedQuery()); err != nil { + return nil, err + } else { + return &StatementWrapper{stmt: stmt, paramQuery: paramQuery}, nil + } +} + +// PrepareContext prepares a statement from a parameterized query and returns a wrapper that can be used with +// named parameters. +func (w *ConnectionWrapper) PrepareContext(ctx context.Context, query string) (*StatementWrapper, error) { + paramQuery := NewQuery(query) + if stmt, err := w.conn.PrepareContext(ctx, paramQuery.GetParsedQuery()); err != nil { + return nil, err + } else { + return &StatementWrapper{stmt: stmt, paramQuery: paramQuery}, nil + } +} + +// Query performs a parameterized prepared query using the expanded args to feed the parameter values. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) Query(args ...any) (*sql.Rows, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.Query() + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.Query(w.paramQuery.GetParsedParameters()...) +} + +// QueryContext performs a parameterized prepared query using the expanded args to feed the parameter values. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) QueryContext(ctx context.Context, args ...any) (*sql.Rows, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.QueryContext(ctx) + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.QueryContext(ctx, w.paramQuery.GetParsedParameters()...) +} + +// QueryRow performs a parameterized prepared query using the expanded args to feed the parameter values and returns one row. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) QueryRow(args ...any) (*sql.Row, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.QueryRow(), nil + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.QueryRow(w.paramQuery.GetParsedParameters()...), nil +} + +// QueryRowContext performs a parameterized prepared query using the expanded args to feed the parameter values and returns one row. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) QueryRowContext(ctx context.Context, args ...any) (*sql.Row, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.QueryRowContext(ctx), nil + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.QueryRowContext(ctx, w.paramQuery.GetParsedParameters()...), nil +} + +// Exec executes a parameterized prepared sql instruction using the expanded args to feed the parameter values and returns the results. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) Exec(args ...any) (sql.Result, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.Exec() + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.Exec(w.paramQuery.GetParsedParameters()...) +} + +// ExecContext executes a parameterized prepared sql instruction using the expanded args to feed the parameter values and returns the results. +// This method expects the parameter arguments to either be a map[string]any, or to come in pairs, +// which are processed as key, value pairs, and in that case, the keys are expected to be strings. +// If the number of arguments is not even, or a key value is not a string, an error is returned. +func (w *StatementWrapper) ExecContext(ctx context.Context, args ...any) (sql.Result, error) { + mappedParams, err := convertArgsToMap(args) + if err != nil { + return nil, err + } + if mappedParams == nil { + return w.stmt.ExecContext(ctx) + } + w.paramQuery.SetValuesFromMap(mappedParams) + return w.stmt.ExecContext(ctx, w.paramQuery.GetParsedParameters()...) +} diff --git a/namedparameter/statement_test.go b/namedparameter/statement_test.go new file mode 100644 index 0000000..4589e13 --- /dev/null +++ b/namedparameter/statement_test.go @@ -0,0 +1,1057 @@ +package namedparameter + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStatementWrapper_Query(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectQuery().WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + stmt, prepareErr := Using(db).Prepare(tt.query) + _, err := stmt.Query(tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Query() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_QueryContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectQuery().WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + stmt, prepareErr := Using(db).PrepareContext(ctx, tt.query) + _, err := stmt.QueryContext(context.Background(), tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryContext() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_QueryRow(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectQuery().WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + stmt, prepareErr := Using(db).Prepare(tt.query) + _, err := stmt.QueryRow(tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRow() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_QueryRowContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + ctx := context.Background() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectQuery().WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + stmt, prepareErr := Using(db).PrepareContext(ctx, tt.query) + _, err := stmt.QueryRowContext(ctx, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryRowContext() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_Exec(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectExec().WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + + stmt, prepareErr := Using(db).Prepare(tt.query) + _, err := stmt.Exec(tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Exec() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_ExecContext(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectExec().WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + + stmt, prepareErr := Using(db).PrepareContext(ctx, tt.query) + _, err := stmt.ExecContext(ctx, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("ExecContext() error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_QueryForConnection(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "SELECT id, name, age FROM employees", + args: args{}, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "SELECT id, name, age FROM employees", + args: nil, + wantQuery: "SELECT id, name, age FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "SELECT id, name, age FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "SELECT id, name, age FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "SELECT id, name, age FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "SELECT id, name, age FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "SELECT id, name, age FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare(tt.wantQuery).ExpectQuery().WithArgs(tt.wantArgs...).WillReturnRows(sqlmock.NewRows([]string{"id", "name", "age"})) + + conn, _ := db.Conn(ctx) + stmt, prepareErr := UsingConnection(conn).PrepareContext(ctx, tt.query) + _, err := stmt.QueryContext(ctx, tt.args...) + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("QueryContext() - connection case error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestStatementWrapper_ExecForTransaction(t *testing.T) { + type args []any + + tests := []struct { + name string + query string + args args + wantQuery string + wantArgs []driver.Value + wantErr bool + }{ + { + name: "Very simple case", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"name", "%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + }, + { + name: "No parameters", + query: "DELETE FROM employees", + args: args{}, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Nil parameters", + query: "DELETE FROM employees", + args: nil, + wantQuery: "DELETE FROM employees", + wantArgs: []driver.Value{}, + }, + { + name: "Multiple arguments in list mode", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in list mode - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{"lastname", "Smith", "date", "2020-01-01"}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Multiple arguments in map", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + }, + { + name: "Multiple arguments in map - missing argument", + query: "DELETE FROM employees WHERE last_name = :lastname AND salary > :baseSalary AND start_time < :date", + args: args{map[string]any{"date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE last_name = \\? AND salary > \\? AND start_time < \\?", + wantArgs: []driver.Value{"Smith", 100000, "2020-01-01"}, + wantErr: true, + }, + { + name: "Arguments used multiple times - list", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{"lastname", "Smith", "date", "2020-01-01", "baseSalary", 100000}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Arguments used multiple times - map", + query: "DELETE FROM employees WHERE (last_name = :lastname AND salary > :baseSalary) OR (last_name = :lastname AND start_time < :date) OR salary < :baseSalary", + args: args{map[string]any{"lastname": "Smith", "date": "2020-01-01", "baseSalary": 100000}}, + wantQuery: "DELETE FROM employees WHERE \\(last_name = \\? AND salary > \\?\\) OR \\(last_name = \\? AND start_time < \\?\\) OR salary < \\?", + wantArgs: []driver.Value{"Smith", 100000, "Smith", "2020-01-01", 100000}, + }, + { + name: "Only one parameter - no map", + query: "DELETE FROM employees WHERE name LIKE :name", + args: args{"%Smith%"}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\?", + wantArgs: []driver.Value{"%Smith%"}, + wantErr: true, + }, + { + name: "Odd number of parameters", + query: "DELETE FROM employees WHERE name LIKE :name AND salary > :baseSalary", + args: args{"name", "%Smith%", 100000}, + wantQuery: "DELETE FROM employees WHERE name LIKE \\? AND salary > \\?", + wantArgs: []driver.Value{"%Smith%", 100000}, + wantErr: true, + }, + } + + var ( + db *sql.DB + mock sqlmock.Sqlmock + dbErr error + ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, dbErr = sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectPrepare(tt.wantQuery).ExpectExec().WithArgs(tt.wantArgs...).WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + tx, _ := db.Begin() + stmt, prepareErr := Using(tx).Prepare(tt.query) + _, err := stmt.Exec(tt.args...) + tx.Commit() + dbErr = mock.ExpectationsWereMet() + if (prepareErr != nil || err != nil || dbErr != nil) != tt.wantErr { + t.Errorf("Exec() - case for transaction error = %v, dbErr = %v, prepareErr = %v, wantErr %v", err, dbErr, prepareErr, tt.wantErr) + return + } + }) + } +} + +func TestConnectionWrapper_FailPrepare(t *testing.T) { + t.Run("Failed prepare for DB", func(t *testing.T) { + db, mock, dbErr := sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare("SELECT id, name, age FROM employees WHERE name LIKE \\?").WillReturnError(errors.New("failed to prepare")) + + _, prepareErr := Using(db).Prepare("SELECT id, name, age FROM employees WHERE name LIKE :name") + if prepareErr == nil { + t.Errorf("It should have failed to prepare for a DB") + return + } + if dbErr = mock.ExpectationsWereMet(); dbErr != nil { + t.Errorf("Error preparing query for db, err = %v", dbErr) + } + }) + t.Run("Failed prepare for DB with context", func(t *testing.T) { + db, mock, dbErr := sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare("SELECT id, name, age FROM employees WHERE name LIKE \\?").WillReturnError(errors.New("failed to prepare")) + + ctx := context.Background() + _, prepareErr := Using(db).PrepareContext(ctx, "SELECT id, name, age FROM employees WHERE name LIKE :name") + if prepareErr == nil { + t.Errorf("It should have failed to prepare for a DB with context") + return + } + if dbErr = mock.ExpectationsWereMet(); dbErr != nil { + t.Errorf("Error preparing query for db with context, err = %v", dbErr) + } + }) + t.Run("Failed prepare for Tx", func(t *testing.T) { + db, mock, dbErr := sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectBegin() + mock.ExpectPrepare("SELECT id, name, age FROM employees WHERE name LIKE \\?").WillReturnError(errors.New("failed to prepare")) + + tx, _ := db.Begin() + _, prepareErr := Using(tx).Prepare("SELECT id, name, age FROM employees WHERE name LIKE :name") + if prepareErr == nil { + t.Errorf("It should have failed to prepare for a Transaction") + return + } + if dbErr = mock.ExpectationsWereMet(); dbErr != nil { + t.Errorf("Error preparing query for tx, err = %v", dbErr) + } + }) + t.Run("Failed prepare for Connection", func(t *testing.T) { + db, mock, dbErr := sqlmock.New() + if dbErr != nil { + t.Errorf("Error initializing Query test error = %v", dbErr) + } + + mock.ExpectPrepare("SELECT id, name, age FROM employees WHERE name LIKE \\?").WillReturnError(errors.New("failed to prepare")) + + ctx := context.Background() + conn, _ := db.Conn(ctx) + _, prepareErr := UsingConnection(conn).PrepareContext(ctx, "SELECT id, name, age FROM employees WHERE name LIKE :name") + if prepareErr == nil { + t.Errorf("It should have failed to prepare for a Connection") + return + } + if dbErr = mock.ExpectationsWereMet(); dbErr != nil { + t.Errorf("Error preparing query for Connection, err = %v", dbErr) + } + }) +}