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
12 changes: 12 additions & 0 deletions internal/compiler/output_columns.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,18 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
col.NotNull = false
}
}
// A type cast does not make its argument non-nullable. If the
// wrapped expression is itself nullable, the result of the cast
// is too. Today we cover the common case of JSON accessor
// operators (`->`, `->>`, `#>`, `#>>`) which return NULL when
// the requested key/path is missing — wrapping `(data ->> 'k')`
// in `::text` was previously emitting a non-nullable string.
// See issue #3792.
if argExpr, ok := n.Arg.(*ast.A_Expr); ok {
if lang.IsJSONNullableOperator(astutils.Join(argExpr.Name, "")) {
col.NotNull = false
}
}
cols = append(cols, col)

case *ast.SelectStmt:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- name: ListJobs :many
-- A `data ->> 'key'` lookup can return SQL NULL when the key is missing.
-- A surrounding `::text` cast must preserve that nullability instead of
-- emitting a non-nullable Go string. See issue #3792.
SELECT id,
(data ->> 'PhoneNumber')::text AS phone_number,
(data ->> 'ContactName')::text AS contact_name,
(data ->> 'State')::text AS state
FROM jobs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE jobs (
id BIGSERIAL PRIMARY KEY,
data JSONB NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "1",
"packages": [
{
"path": "go",
"engine": "postgresql",
"sql_package": "pgx/v5",
"name": "querytest",
"schema": "schema.sql",
"queries": "query.sql"
}
]
}
8 changes: 6 additions & 2 deletions internal/engine/postgresql/reserved.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ func (p *Parser) NamedParam(name string) string {
}

// Cast returns a type cast expression.
// PostgreSQL uses expr::type syntax.
// PostgreSQL uses expr::type syntax. Wrap the arg in parens so that
// operator precedence is preserved when the arg is a compound expression
// like a -> b or a ->> b. Parens around a simple expression are a no-op
// to the parser (pg_query normalises them away during fingerprinting),
// so this is safe for the existing simple-arg cases too.
func (p *Parser) Cast(arg, typeName string) string {
return arg + "::" + typeName
return "(" + arg + ")::" + typeName
}

// https://www.postgresql.org/docs/current/sql-keywords-appendix.html
Expand Down
15 changes: 15 additions & 0 deletions internal/sql/lang/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ func IsMathematicalOperator(s string) bool {
}
return true
}

// IsJSONNullableOperator reports whether op is a Postgres JSON / JSONB
// accessor operator that returns SQL NULL when the requested key or path
// is missing from the value. These are: -> (object/array element as jsonb),
// ->> (object/array element as text), #> (path lookup as jsonb), and #>>
// (path lookup as text). Wrapping such an expression in a type cast does
// not make the result non-nullable — the key may still be absent at run
// time. See issue #3792.
func IsJSONNullableOperator(s string) bool {
switch s {
case "->", "->>", "#>", "#>>":
return true
}
return false
}
17 changes: 17 additions & 0 deletions internal/sql/lang/operator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lang

import "testing"

func TestIsJSONNullableOperator(t *testing.T) {
t.Parallel()
for _, op := range []string{"->", "->>", "#>", "#>>"} {
if !IsJSONNullableOperator(op) {
t.Errorf("expected %q to be classified as JSON-nullable", op)
}
}
for _, op := range []string{"", "+", "-", "=", "::", "@>", "<@", "->>>"} {
if IsJSONNullableOperator(op) {
t.Errorf("did not expect %q to be classified as JSON-nullable", op)
}
}
}
Loading