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
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Contributing to sqlx

Thank you for your interest in contributing to sqlx.

## Before you open an issue or PR

- Search [existing issues](https://github.com/jmoiron/sqlx/issues) and [pull requests](https://github.com/jmoiron/sqlx/pulls) first.
- For bugs, include a minimal Go program that reproduces the problem, your Go version, and database driver if relevant.
- sqlx extends `database/sql`; confirm the behavior with plain `database/sql` when possible to isolate driver-specific issues.

## Pull requests

- Keep changes focused; avoid unrelated formatting or refactors.
- Add or update tests for behavior changes (`go test ./...`).
- Match existing code style in the files you touch.

## Development

```bash
go test ./...
```

Some tests require database drivers; run only package tests if you do not have databases configured locally.

## License

By contributing, you agree that your contributions are licensed under the same terms as the project (MIT).
69 changes: 46 additions & 23 deletions named.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,52 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
currentVar := 1
name := make([]byte, 0, 10)

for i, b := range qs {
isIdentByteBeforeCast := func(b byte) bool {
return unicode.IsLetter(rune(b)) || unicode.IsDigit(rune(b)) || b == '.'
}

appendBindvar := func(param []byte) {
switch bindType {
case NAMED:
rebound = append(rebound, ':')
rebound = append(rebound, param...)
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++
}
}

for i := 0; i < len(qs); i++ {
b := qs[i]
// a ':' while we're in a name is an error
if b == ':' {
// PostgreSQL type cast after a named parameter, e.g. :boundary::jsonb
if inName && len(name) > 0 && i < last && qs[i+1] == ':' {
names = append(names, string(name))
appendBindvar(name)
name = name[:0]
inName = false
rebound = append(rebound, ':', ':')
i++
continue
}
// PostgreSQL type cast in an identifier, e.g. path::text
if !inName && i > 0 && isIdentByteBeforeCast(qs[i-1]) && i < last && qs[i+1] == ':' {
rebound = append(rebound, ':', ':')
i++
continue
}
// if this is the second ':' in a '::' escape sequence, append a ':'
if inName && i > 0 && qs[i-1] == ':' {
rebound = append(rebound, ':')
Expand All @@ -350,7 +393,7 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
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 @@ -369,27 +412,7 @@ func compileNamedQuery(qs []byte, bindType int) (query string, names []string, e
}
// 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++
}
appendBindvar(name)
// add this byte to string unless it was not part of the name
if i != last {
rebound = append(rebound, b)
Expand Down
33 changes: 33 additions & 0 deletions named_issue956_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package sqlx

import "testing"

func TestNamedPostgresCastInIdentifier(t *testing.T) {
query := `SELECT DISTINCT t.path::text AS catalog_path WHERE t.company_id = :company_id FROM table AS t`
q, args, err := Named(query, map[string]interface{}{"company_id": 555})
if err != nil {
t.Fatal(err)
}
want := `SELECT DISTINCT t.path::text AS catalog_path WHERE t.company_id = ? FROM table AS t`
if q != want {
t.Fatalf("got %q want %q", q, want)
}
if len(args) != 1 || args[0].(int) != 555 {
t.Fatalf("args = %#v", args)
}
}

func TestNamedPostgresCastAfterNamedParam(t *testing.T) {
query := `SELECT :boundary::jsonb AS boundary`
q, args, err := Named(query, map[string]interface{}{"boundary": `{"type":"Polygon"}`})
if err != nil {
t.Fatal(err)
}
want := `SELECT ?::jsonb AS boundary`
if q != want {
t.Fatalf("got %q want %q", q, want)
}
if len(args) != 1 {
t.Fatalf("args = %#v", args)
}
}
8 changes: 4 additions & 4 deletions named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ func TestCompileQuery(t *testing.T) {
},
{
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=?`,
D: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=$1 AND last_name=$2`,
T: `SELECT 'a:b:c' || first_name, '::ABC:_:' FROM person WHERE first_name=@p1 AND last_name=@p2`,
N: `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=?`,
D: `SELECT 'a::b::c' || first_name, '::ABC::_:' FROM person WHERE first_name=$1 AND last_name=$2`,
T: `SELECT 'a::b::c' || first_name, '::ABC::_:' FROM person WHERE first_name=@p1 AND last_name=@p2`,
N: `SELECT 'a::b::c' || first_name, '::ABC::_:' FROM person WHERE first_name=:first_name AND last_name=:last_name`,
V: []string{"first_name", "last_name"},
},
{
Expand Down