WIP named
This commit is contained in:
80
named.go
Normal file
80
named.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package gocqlx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"github.com/gocql/gocql"
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Queryx struct {
|
||||||
|
*gocql.Query
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow digits and letters in bind params; additionally runes are
|
||||||
|
// checked against underscores, meaning that bind params can have be
|
||||||
|
// alphanumeric with underscores. Mind the difference between unicode
|
||||||
|
// digits and numbers, where '5' is a digit but '五' is not.
|
||||||
|
var allowedBindRunes = []*unicode.RangeTable{unicode.Letter, unicode.Digit}
|
||||||
|
|
||||||
|
// CompileNamedQuery compiles a named query into an unbound query using the
|
||||||
|
// '?' bindvar and a list of names.
|
||||||
|
func CompileNamedQuery(qs []byte) (cql string, names []string, err error) {
|
||||||
|
// guess number of names
|
||||||
|
n := bytes.Count(qs, []byte(":"))
|
||||||
|
if n == 0 {
|
||||||
|
return "", nil, errors.New("expected a named query")
|
||||||
|
}
|
||||||
|
names = make([]string, 0, n)
|
||||||
|
rebound := make([]byte, 0, len(qs))
|
||||||
|
|
||||||
|
inName := false
|
||||||
|
last := len(qs) - 1
|
||||||
|
name := make([]byte, 0, 10)
|
||||||
|
|
||||||
|
for i, b := range qs {
|
||||||
|
// a ':' while we're in a name is an error
|
||||||
|
if b == ':' {
|
||||||
|
// if this is the second ':' in a '::' escape sequence, append a ':'
|
||||||
|
if inName && i > 0 && qs[i-1] == ':' {
|
||||||
|
rebound = append(rebound, ':')
|
||||||
|
inName = false
|
||||||
|
continue
|
||||||
|
} else if inName {
|
||||||
|
err = errors.New("unexpected `:` while reading named param at " + strconv.Itoa(i))
|
||||||
|
return cql, names, err
|
||||||
|
}
|
||||||
|
inName = true
|
||||||
|
name = []byte{}
|
||||||
|
// if we're in a name, and this is an allowed character, continue
|
||||||
|
} else if inName && (unicode.IsOneOf(allowedBindRunes, rune(b)) || b == '_' || b == '.') && i != last {
|
||||||
|
// append the byte to the name if we are in a name and not on the last byte
|
||||||
|
name = append(name, b)
|
||||||
|
// if we're in a name and it's not an allowed character, the name is done
|
||||||
|
} else if inName {
|
||||||
|
inName = false
|
||||||
|
// if this is the final byte of the string and it is part of the name, then
|
||||||
|
// make sure to add it to the name
|
||||||
|
if i == last && unicode.IsOneOf(allowedBindRunes, rune(b)) {
|
||||||
|
name = append(name, b)
|
||||||
|
}
|
||||||
|
// add the string representation to the names list
|
||||||
|
names = append(names, string(name))
|
||||||
|
// add a proper bindvar for the bindType
|
||||||
|
rebound = append(rebound, '?')
|
||||||
|
// add this byte to string unless it was not part of the name
|
||||||
|
if i != last {
|
||||||
|
rebound = append(rebound, b)
|
||||||
|
} else if !unicode.IsOneOf(allowedBindRunes, rune(b)) {
|
||||||
|
rebound = append(rebound, b)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is a normal byte and should just go onto the rebound query
|
||||||
|
rebound = append(rebound, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(rebound), names, err
|
||||||
|
}
|
||||||
60
named_test.go
Normal file
60
named_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package gocqlx
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCompileQuery(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
Q, R string
|
||||||
|
V []string
|
||||||
|
}{
|
||||||
|
// basic test for named parameters, invalid char ',' terminating
|
||||||
|
{
|
||||||
|
Q: `INSERT INTO foo (a,b,c,d) VALUES (:name, :age, :first, :last)`,
|
||||||
|
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||||
|
V: []string{"name", "age", "first", "last"},
|
||||||
|
},
|
||||||
|
// This query tests a named parameter ending the string as well as numbers
|
||||||
|
{
|
||||||
|
Q: `SELECT * FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||||
|
R: `SELECT * FROM a WHERE first_name=? AND last_name=?`,
|
||||||
|
V: []string{"name1", "name2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: `SELECT "::foo" FROM a WHERE first_name=:name1 AND last_name=:name2`,
|
||||||
|
R: `SELECT ":foo" FROM a WHERE first_name=? AND last_name=?`,
|
||||||
|
V: []string{"name1", "name2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: `SELECT 'a::b::c' || first_name, '::::ABC::_::' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
|
||||||
|
R: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=? AND last_name=?`,
|
||||||
|
V: []string{"first_name", "last_name"},
|
||||||
|
},
|
||||||
|
/* This unicode awareness test sadly fails, because of our byte-wise worldview.
|
||||||
|
* We could certainly iterate by Rune instead, though it's a great deal slower,
|
||||||
|
* it's probably the RightWay(tm)
|
||||||
|
{
|
||||||
|
Q: `INSERT INTO foo (a,b,c,d) VALUES (:あ, :b, :キコ, :名前)`,
|
||||||
|
R: `INSERT INTO foo (a,b,c,d) VALUES (?, ?, ?, ?)`,
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
qr, names, err := CompileNamedQuery([]byte(test.Q))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if qr != test.R {
|
||||||
|
t.Errorf("expected %s, got %s", test.R, qr)
|
||||||
|
}
|
||||||
|
if len(names) != len(test.V) {
|
||||||
|
t.Errorf("expected %#v, got %#v", test.V, names)
|
||||||
|
} else {
|
||||||
|
for i, name := range names {
|
||||||
|
if name != test.V[i] {
|
||||||
|
t.Errorf("expected %dth name to be %s, got %s", i+1, test.V[i], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user