Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jastkand opened this issue Jan 29, 2024 · 1 comment
Labels

Comments

@jastkand
Copy link

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.

@jastkand jastkand added the bug label Jan 29, 2024
@jackc
Copy link
Owner

jackc commented Jan 29, 2024

Duplicate of #1566.

@jackc jackc closed this as not planned Won't fix, can't repro, duplicate, stale Jan 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants