mirror of https://github.com/VinGarcia/ksql.git
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
parent
d275555df5
commit
edecbf8191
4
go.mod
4
go.mod
|
@ -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
69
ksql.go
|
@ -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
|
||||
}
|
||||
|
|
108
ksql_test.go
108
ksql_test.go
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue