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
83 changes: 57 additions & 26 deletions named.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,20 +337,73 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
currentVar := 1
name := make([]byte, 0, 10)

// emitName closes out the named parameter we've been building up in name
// and writes the appropriate bindvar into rebound. Used both when the
// terminating non-name byte is part of the regular text (the original
// else-if-inName branch below) and when a Postgres "::" cast is glued
// directly to a named parameter such as :boundary::jsonb (see #983).
emitName := func() {
names = append(names, string(name))
switch bindType {
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++
}
}

skipNext := false
for i, b := range qs {
if skipNext {
skipNext = false
continue
}
// 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] == ':' {
if inName && i > 0 && qs[i-1] == ':' && len(name) == 0 {
rebound = append(rebound, ':')
inName = false
continue
} else if inName {
}
// A ':' arriving in the middle of a built-up name means we hit
// something like :boundary::jsonb. End the current name first.
// If the next byte is also ':', this is a PostgreSQL cast and
// the whole "::" should pass through to the output (see #983).
// Otherwise treat the trailing colon as literal text.
if inName && len(name) > 0 {
emitName()
if i < last && qs[i+1] == ':' {
rebound = append(rebound, ':', ':')
inName = false
skipNext = true
name = name[:0]
continue
}
rebound = append(rebound, ':')
inName = false
name = name[:0]
continue
}
if inName {
err = errors.New("unexpected `:` while reading named param at " + strconv.Itoa(i))
return query, names, err
}
inName = true
name = []byte{}
name = name[:0]
} else if inName && i > 0 && b == '=' && len(name) == 0 {
rebound = append(rebound, ':', '=')
inName = false
Expand All @@ -367,29 +420,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++
}
emitName()
// add this byte to string unless it was not part of the name
if i != last {
rebound = append(rebound, b)
Expand Down
11 changes: 11 additions & 0 deletions named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ func TestCompileQuery(t *testing.T) {
T: `SELECT @name := "name", @p1, @p2, @p3`,
V: []string{"age", "first", "last"},
},
// Postgres :: cast directly attached to a named parameter
// (see github.com/jmoiron/sqlx#983). The "::" must pass through
// to the rebound query instead of returning "unexpected ':'".
{
Q: `SELECT :boundary::jsonb, :id::text`,
R: `SELECT ?::jsonb, ?::text`,
D: `SELECT $1::jsonb, $2::text`,
N: `SELECT :boundary::jsonb, :id::text`,
T: `SELECT @p1::jsonb, @p2::text`,
V: []string{"boundary", "id"},
},
/* 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