Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions named.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,47 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
last := len(qs) - 1
currentVar := 1
name := make([]byte, 0, 10)
skipNext := false

appendNamedBind := func() {
names = append(names, string(name))
switch bindType {
// oracle only supports named type bind vars even for positional
case NAMED:
rebound = append(rebound, ':')
rebound = append(rebound, name...)
case QUESTION, UNKNOWN:
rebound = append(rebound, '?')
case DOLLAR:
rebound = append(rebound, '$')
for _, nb := range strconv.Itoa(currentVar) {
rebound = append(rebound, byte(nb))
}
currentVar++
case AT:
rebound = append(rebound, '@', 'p')
for _, nb := range strconv.Itoa(currentVar) {
rebound = append(rebound, byte(nb))
}
currentVar++
}
}

for i, b := range qs {
// a ':' while we're in a name is an error
if skipNext {
skipNext = false
continue
}
// a ':' while we're in a name is an error, except PostgreSQL casts
if b == ':' {
// :name::type is a named param followed by a PostgreSQL type cast
if inName && len(name) > 0 && i < last && qs[i+1] == ':' {
appendNamedBind()
rebound = append(rebound, ':', ':')
inName = false
skipNext = true
continue
}
// if this is the second ':' in a '::' escape sequence, append a ':'
if inName && i > 0 && qs[i-1] == ':' {
rebound = append(rebound, ':')
Expand Down Expand Up @@ -367,29 +404,7 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
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
switch bindType {
// oracle only supports named type bind vars even for positional
case NAMED:
rebound = append(rebound, ':')
rebound = append(rebound, name...)
case QUESTION, UNKNOWN:
rebound = append(rebound, '?')
case DOLLAR:
rebound = append(rebound, '$')
for _, b := range strconv.Itoa(currentVar) {
rebound = append(rebound, byte(b))
}
currentVar++
case AT:
rebound = append(rebound, '@', 'p')
for _, b := range strconv.Itoa(currentVar) {
rebound = append(rebound, byte(b))
}
currentVar++
}
appendNamedBind()
// add this byte to string unless it was not part of the name
if i != last {
rebound = append(rebound, b)
Expand Down
47 changes: 47 additions & 0 deletions named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ func TestCompileQuery(t *testing.T) {
T: `SELECT @name := "name", @p1, @p2, @p3`,
V: []string{"age", "first", "last"},
},
// PostgreSQL type cast immediately after a named parameter (#983)
{
Q: `SELECT :boundary::jsonb AS boundary`,
R: `SELECT ?::jsonb AS boundary`,
D: `SELECT $1::jsonb AS boundary`,
T: `SELECT @p1::jsonb AS boundary`,
N: `SELECT :boundary::jsonb AS boundary`,
V: []string{"boundary"},
},
{
Q: `SELECT :a::uuid, :b::text`,
R: `SELECT ?::uuid, ?::text`,
D: `SELECT $1::uuid, $2::text`,
T: `SELECT @p1::uuid, @p2::text`,
N: `SELECT :a::uuid, :b::text`,
V: []string{"a", "b"},
},
/* 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)
Expand Down Expand Up @@ -121,6 +138,36 @@ func (t Test) Errorf(err error, format string, args ...interface{}) {
}
}

func TestNamedPostgresTypeCast(t *testing.T) {
q, args, err := Named(
`SELECT :boundary::jsonb AS boundary, :id::int AS id`,
map[string]interface{}{
"boundary": `{"type":"Polygon"}`,
"id": 7,
},
)
if err != nil {
t.Fatal(err)
}
want := `SELECT ?::jsonb AS boundary, ?::int AS id`
if q != want {
t.Errorf("query:\n got: %s\nwant: %s", q, want)
}
if len(args) != 2 {
t.Fatalf("got %d args, want 2: %#v", len(args), args)
}
if args[0] != `{"type":"Polygon"}` || args[1] != 7 {
t.Errorf("args: %#v", args)
}
}

func TestNamedParamSingleColonStillErrors(t *testing.T) {
_, _, err := compileNamedQuery([]byte("SELECT :first:name"), QUESTION)
if err == nil {
t.Fatal("expected error for a single colon inside a named param")
}
}

func TestEscapedColons(t *testing.T) {
t.Skip("not sure it is possible to support this in general case without an SQL parser")
var qs = `SELECT * FROM testtable WHERE timeposted BETWEEN (now() AT TIME ZONE 'utc') AND
Expand Down