jackc / pgx

PostgreSQL driver and toolkit for Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`driver.Value` interface is ignored when uninitialized typed map is passed as an argument to `conn.Exec`

jastkand opened this issue · comments

Describe the bug
Given:

  • the pg database with a table having jsonb column (not null default '{}')
  • a typed map that implements a driver.Valuer interface

When the value is passed as an argument in some scenarios the value doesn't respect the driver.Valuer implementation.

To Reproduce
Runnable example showing the issue:

package main

import (
	"context"
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/jackc/pgx/v5"
)

type overrides map[string]string

func (f overrides) Value() (driver.Value, error) {
	if len(f) == 0 {
		return []byte("{}"), nil
	}
	raw, err := json.Marshal(f)
	if err != nil {
		return nil, err
	}
	return raw, nil
}

func (f *overrides) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return fmt.Errorf("failed to unmarshal JSONB value: %v", value)
	}
	return json.Unmarshal(bytes, f)
}

func main() {
	conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close(context.Background())

	_, err = conn.Exec(context.Background(), "DROP TABLE IF EXISTS typedmapbug")
	if err != nil {
		log.Fatal("drop table", err)
	}
	_, err = conn.Exec(context.Background(), "CREATE TABLE typedmapbug(id serial, overrides jsonb not null default '{}')")
	if err != nil {
		log.Fatal(err)
	}

	_, err = conn.Exec(context.Background(), `INSERT INTO typedmapbug (id, overrides) VALUES (1, '{"a":"a"}'), (2, '{"a":"a"}')`)
	if err != nil {
		log.Fatal(err)
	}

	// works
	newOverrides1 := make(overrides)
	if val, ok := newOverrides1.Value(); ok == nil {
		log.Printf("newOverrides1 to driver.Value %s\n", val)
	}
	tag1, err := conn.Exec(context.Background(), "UPDATE typedmapbug SET overrides = $1 WHERE id = $2", newOverrides1, 1)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("update succeds:", tag1.RowsAffected())

	// doesn't work
	var newOverrides2 overrides
	if val, ok := newOverrides2.Value(); ok == nil {
		log.Printf("newOverrides2 to driver.Value %s\n", val)
	}
	tag2, err := conn.Exec(context.Background(), "UPDATE typedmapbug SET overrides = $1 WHERE id = $2", newOverrides2, 2)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("update fails:", tag2.RowsAffected())
}

Expected behavior
The code shouldn't fail on the second update call and the column had to be updated with the '{}'.

Actual behavior
ERROR: null value in column "overrides" violates not-null constraint (SQLSTATE 23502) error is returned instead

Version

  • Go: go version go1.20.13 darwin/amd64
  • PostgreSQL: PostgreSQL 12.9 (Debian 12.9-1.pgdg110+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
  • pgx: v5.5.2

Additional

While debugging it I've found this line https://github.com/jackc/pgx/blob/v5.5.2/extended_query_builder.go#L26 which converts all typed nils into untyped ones. This seems to be causing the issue and the Value() in never called in second scenario.

Duplicate of #1566.