From 40876a6a3214d0decb9f749aade08732b395f35c Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 19:46:55 +0800 Subject: [PATCH 1/2] docs: add CONTRIBUTING.md for contributors Document issue/PR expectations and local test workflow for new contributors. --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1dc125424 --- /dev/null +++ b/CONTRIBUTING.md @@ -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). From 9e3a5ed8ebb4895b2745a05917f7ee798add068d Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 21:07:56 +0800 Subject: [PATCH 2/2] fix: preserve PostgreSQL :: casts in Named queries Fixes #956 by treating :: after identifiers and after named parameters as PostgreSQL type casts instead of consuming a colon. Also covers :param::type patterns (e.g. #983). --- named.go | 69 ++++++++++++++++++++++++++++-------------- named_issue956_test.go | 33 ++++++++++++++++++++ named_test.go | 8 ++--- 3 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 named_issue956_test.go diff --git a/named.go b/named.go index 6ac447771..7ac01dcb7 100644 --- a/named.go +++ b/named.go @@ -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, ':') @@ -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 @@ -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) diff --git a/named_issue956_test.go b/named_issue956_test.go new file mode 100644 index 000000000..46517492d --- /dev/null +++ b/named_issue956_test.go @@ -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) + } +} diff --git a/named_test.go b/named_test.go index 0ee5b85fa..945ec903e 100644 --- a/named_test.go +++ b/named_test.go @@ -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"}, }, {