Add feature of omiting the "SELECT" part of the query

Now the 3 functions that allow you to write plain SQL queries
also work if you omit the `SELECT ...` part of the query.

If you do this the code will check and notice that the first
token of the query is a "FROM" token and then automatically
build the SELECT part of the query based on the tags of the struct.

Everything is cached, so the impact on performance should be negligible.

The affected functions are:

- Query()
- QueryOne()
- QueryChunks()
pull/2/head
Vinícius Garcia 2021-05-16 17:05:21 -03:00
parent d275555df5
commit edecbf8191
4 changed files with 687 additions and 562 deletions

4
go.mod
View File

@ -3,9 +3,9 @@ module github.com/vingarcia/ksql
go 1.14
require (
github.com/denisenkom/go-mssqldb v0.10.0 // indirect
github.com/denisenkom/go-mssqldb v0.10.0
github.com/ditointernet/go-assert v0.0.0-20200120164340-9e13125a7018
github.com/go-sql-driver/mysql v1.4.0 // indirect
github.com/go-sql-driver/mysql v1.4.0
github.com/golang/mock v1.5.0
github.com/jmoiron/sqlx v1.2.0
github.com/lib/pq v1.1.1

69
ksql.go
View File

@ -6,11 +6,20 @@ import (
"fmt"
"reflect"
"strings"
"unicode"
"github.com/pkg/errors"
"github.com/vingarcia/ksql/structs"
)
var selectQueryCache = map[string]map[reflect.Type]string{}
func init() {
for dname := range supportedDialects {
selectQueryCache[dname] = map[reflect.Type]string{}
}
}
// DB represents the ksql client responsible for
// interfacing with the "database/sql" package implementing
// the KissSQL interface `SQLProvider`.
@ -124,6 +133,14 @@ func (c DB) Query(
slice = slice.Slice(0, 0)
}
if strings.ToUpper(getFirstToken(query)) == "FROM" {
selectPrefix, err := buildSelectQuery(c.dialect, structType, selectQueryCache[c.dialect.DriverName()])
if err != nil {
return err
}
query = selectPrefix + query
}
rows, err := c.db.QueryContext(ctx, query, params...)
if err != nil {
return fmt.Errorf("error running query: %s", err.Error())
@ -189,6 +206,14 @@ func (c DB) QueryOne(
return fmt.Errorf("ksql: expected to receive a pointer to struct, but got: %T", record)
}
if strings.ToUpper(getFirstToken(query)) == "FROM" {
selectPrefix, err := buildSelectQuery(c.dialect, t, selectQueryCache[c.dialect.DriverName()])
if err != nil {
return err
}
query = selectPrefix + query
}
rows, err := c.db.QueryContext(ctx, query, params...)
if err != nil {
return err
@ -243,6 +268,14 @@ func (c DB) QueryChunks(
return err
}
if strings.ToUpper(getFirstToken(parser.Query)) == "FROM" {
selectPrefix, err := buildSelectQuery(c.dialect, structType, selectQueryCache[c.dialect.DriverName()])
if err != nil {
return err
}
parser.Query = selectPrefix + parser.Query
}
rows, err := c.db.QueryContext(ctx, parser.Query, parser.Params...)
if err != nil {
return err
@ -869,3 +902,39 @@ func buildCompositeKeyDeleteQuery(
strings.Join(values, ","),
), params
}
// We implemented this function instead of using
// a regex or strings.Fields because we wanted
// to preserve the performance of the package.
func getFirstToken(s string) string {
s = strings.TrimLeftFunc(s, unicode.IsSpace)
var token strings.Builder
for _, c := range s {
if unicode.IsSpace(c) {
break
}
token.WriteRune(c)
}
return token.String()
}
func buildSelectQuery(
dialect dialect,
structType reflect.Type,
selectQueryCache map[reflect.Type]string,
) (string, error) {
if selectQuery, found := selectQueryCache[structType]; found {
return selectQuery, nil
}
info := structs.GetTagInfo(structType)
var fields []string
for _, field := range info.Fields() {
fields = append(fields, dialect.Escape(field.Name))
}
query := "SELECT " + strings.Join(fields, ", ") + " "
selectQueryCache[structType] = query
return query, nil
}

View File

@ -37,6 +37,21 @@ type Address struct {
func TestQuery(t *testing.T) {
for driver := range supportedDialects {
t.Run(driver, func(t *testing.T) {
variations := []struct {
desc string
queryPrefix string
}{
{
desc: "with select *",
queryPrefix: "SELECT * ",
},
{
desc: "building the SELECT part of the query internally",
queryPrefix: "",
},
}
for _, variation := range variations {
t.Run(variation.desc, func(t *testing.T) {
t.Run("using slice of structs", func(t *testing.T) {
err := createTable(driver)
if err != nil {
@ -50,14 +65,14 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []User
err := c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
err := c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE id=1;`)
assert.Equal(t, nil, err)
assert.Equal(t, []User(nil), users)
assert.Equal(t, 0, len(users))
users = []User{}
err = c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE id=1;`)
assert.Equal(t, nil, err)
assert.Equal(t, []User{}, users)
assert.Equal(t, 0, len(users))
})
t.Run("should return a user correctly", func(t *testing.T) {
@ -70,7 +85,7 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
assert.Equal(t, nil, err)
assert.Equal(t, 1, len(users))
@ -92,7 +107,7 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia")
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(users))
@ -120,14 +135,14 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []*User
err := c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
err := c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE id=1;`)
assert.Equal(t, nil, err)
assert.Equal(t, []*User(nil), users)
assert.Equal(t, 0, len(users))
users = []*User{}
err = c.Query(ctx, &users, `SELECT * FROM users WHERE id=1;`)
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE id=1;`)
assert.Equal(t, nil, err)
assert.Equal(t, []*User{}, users)
assert.Equal(t, 0, len(users))
})
t.Run("should return a user correctly", func(t *testing.T) {
@ -140,12 +155,13 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []*User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
assert.Equal(t, nil, err)
assert.Equal(t, 1, len(users))
assert.Equal(t, "Bia", users[0].Name)
assert.NotEqual(t, uint(0), users[0].ID)
assert.Equal(t, "Bia", users[0].Name)
assert.Equal(t, "BR", users[0].Address.Country)
})
t.Run("should return multiple users correctly", func(t *testing.T) {
@ -161,20 +177,22 @@ func TestQuery(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var users []*User
err = c.Query(ctx, &users, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia")
err = c.Query(ctx, &users, variation.queryPrefix+`FROM users WHERE name like `+c.dialect.Placeholder(0), "% Garcia")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, "João Garcia", users[0].Name)
assert.NotEqual(t, uint(0), users[0].ID)
assert.Equal(t, "João Garcia", users[0].Name)
assert.Equal(t, "US", users[0].Address.Country)
assert.Equal(t, "Bia Garcia", users[1].Name)
assert.NotEqual(t, uint(0), users[1].ID)
assert.Equal(t, "Bia Garcia", users[1].Name)
assert.Equal(t, "BR", users[1].Address.Country)
})
})
})
}
t.Run("testing error cases", func(t *testing.T) {
err := createTable(driver)
@ -226,11 +244,26 @@ func TestQuery(t *testing.T) {
func TestQueryOne(t *testing.T) {
for driver := range supportedDialects {
t.Run(driver, func(t *testing.T) {
variations := []struct {
desc string
queryPrefix string
}{
{
desc: "with select *",
queryPrefix: "SELECT * ",
},
{
desc: "building the SELECT part of the query internally",
queryPrefix: "",
},
}
for _, variation := range variations {
err := createTable(driver)
if err != nil {
t.Fatal("could not create test table!, reason:", err.Error())
}
t.Run(variation.desc, func(t *testing.T) {
t.Run("should return RecordNotFoundErr when there are no results", func(t *testing.T) {
db := connectDB(t, driver)
defer db.Close()
@ -238,7 +271,7 @@ func TestQueryOne(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
u := User{}
err := c.QueryOne(ctx, &u, `SELECT * FROM users WHERE id=1;`)
err := c.QueryOne(ctx, &u, variation.queryPrefix+`FROM users WHERE id=1;`)
assert.Equal(t, ErrRecordNotFound, err)
})
@ -252,7 +285,7 @@ func TestQueryOne(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
u := User{}
err = c.QueryOne(ctx, &u, `SELECT * FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
err = c.QueryOne(ctx, &u, variation.queryPrefix+`FROM users WHERE name=`+c.dialect.Placeholder(0), "Bia")
assert.Equal(t, nil, err)
assert.NotEqual(t, uint(0), u.ID)
@ -276,7 +309,7 @@ func TestQueryOne(t *testing.T) {
c := newTestDB(db, driver, "users")
var u User
err = c.QueryOne(ctx, &u, `SELECT * FROM users WHERE name like `+c.dialect.Placeholder(0)+` ORDER BY id ASC`, "% Sá")
err = c.QueryOne(ctx, &u, variation.queryPrefix+`FROM users WHERE name like `+c.dialect.Placeholder(0)+` ORDER BY id ASC`, "% Sá")
assert.Equal(t, nil, err)
assert.Equal(t, "Andréa Sá", u.Name)
assert.Equal(t, 0, u.Age)
@ -284,6 +317,8 @@ func TestQueryOne(t *testing.T) {
Country: "US",
}, u.Address)
})
})
}
t.Run("should report error if input is not a pointer to struct", func(t *testing.T) {
db := connectDB(t, driver)
@ -312,7 +347,7 @@ func TestQueryOne(t *testing.T) {
ctx := context.Background()
c := newTestDB(db, driver, "users")
var user User
err = c.QueryOne(ctx, &user, `SELECT * FROM not a valid query`)
err := c.QueryOne(ctx, &user, `SELECT * FROM not a valid query`)
assert.NotEqual(t, nil, err)
})
})
@ -762,6 +797,21 @@ func TestUpdate(t *testing.T) {
func TestQueryChunks(t *testing.T) {
for driver := range supportedDialects {
t.Run(driver, func(t *testing.T) {
variations := []struct {
desc string
queryPrefix string
}{
{
desc: "with select *",
queryPrefix: "SELECT * ",
},
{
desc: "building the SELECT part of the query internally",
queryPrefix: "",
},
}
for _, variation := range variations {
t.Run(variation.desc, func(t *testing.T) {
t.Run("should query a single row correctly", func(t *testing.T) {
err := createTable(driver)
if err != nil {
@ -782,7 +832,7 @@ func TestQueryChunks(t *testing.T) {
var length int
var u User
err = c.QueryChunks(ctx, ChunkParser{
Query: `SELECT * FROM users WHERE name = ` + c.dialect.Placeholder(0),
Query: variation.queryPrefix + `FROM users WHERE name = ` + c.dialect.Placeholder(0),
Params: []interface{}{"User1"},
ChunkSize: 100,
@ -820,7 +870,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -862,7 +912,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 1,
@ -905,7 +955,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -946,7 +996,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -986,7 +1036,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -1028,7 +1078,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -1068,7 +1118,7 @@ func TestQueryChunks(t *testing.T) {
var lengths []int
var users []User
err = c.QueryChunks(ctx, ChunkParser{
Query: `select * from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Query: variation.queryPrefix + `from users where name like ` + c.dialect.Placeholder(0) + ` order by name asc;`,
Params: []interface{}{"User%"},
ChunkSize: 2,
@ -1126,7 +1176,7 @@ func TestQueryChunks(t *testing.T) {
for _, fn := range funcs {
err := c.QueryChunks(ctx, ChunkParser{
Query: `SELECT * FROM users`,
Query: variation.queryPrefix + `FROM users`,
Params: []interface{}{},
ChunkSize: 2,
@ -1155,6 +1205,8 @@ func TestQueryChunks(t *testing.T) {
})
})
}
})
}
}
func TestTransaction(t *testing.T) {

View File

@ -42,6 +42,10 @@ func (s structInfo) Add(field fieldInfo) {
s.byName[field.Name] = &field
}
func (s structInfo) Fields() map[int]*fieldInfo {
return s.byIndex
}
// This cache is kept as a pkg variable
// because the total number of types on a program
// should be finite. So keeping a single cache here