pgx icon indicating copy to clipboard operation
pgx copied to clipboard

How to use arrays with v5/stdlib

Open regeda opened this issue 1 year ago • 7 comments

The documentation declares the following:

ArrayCodec implements support for arrays. If pgtype supports type T then it can easily support []T by registering an ArrayCodec for the appropriate PostgreSQL OID. In addition, Array[T] type can support multi-dimensional arrays.

I'm trying to use the following:

type X struct {
  ids []pgtype.UUID
}

func smth(x X) error {
  // "conn" is established using `v5/stdlib`
  _, err := conn.ExecContext("....$1....", x.ids)
  return err
}

However, I'm getting the following:

sql: converting argument $1 type: unsupported type []pgtype.UUID, a slice of struct

How to use arrays in both ways Valuer and Scanner?

regeda avatar Oct 24 '23 16:10 regeda

You can pass Go arrays directly as query arguments. For scanning you need to use pgtype.Map.SQLScanner.

package main

import (
	"database/sql"
	"log"
	"os"

	"github.com/jackc/pgx/v5/pgtype"
	_ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
	db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
	m := pgtype.NewMap()

	uuidsIn := []pgtype.UUID{
		{Bytes: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Valid: true},
		{Bytes: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Valid: true},
	}

	var uuidsOut []pgtype.UUID

	err = db.QueryRow("select $1::uuid[]", uuidsIn).Scan(m.SQLScanner(&uuidsOut))
	if err != nil {
		log.Fatal(err)
	}

	log.Println(uuidsOut)
}

Output:

2023/10/26 20:38:15 [{[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0] true} {[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0] true}]
``

jackc avatar Oct 27 '23 01:10 jackc

Got it!

@jackc Do you have any plans to avoid using SQLScanner in future? It makes the data model layer not interoperable if different SQL drivers are in place.

regeda avatar Oct 27 '23 15:10 regeda

Do you have any plans to avoid using SQLScanner in future? It makes the data model layer not interoperable if different SQL drivers are in place.

pgx supports []T as long as it understands T. But database/sql requires the type implement sql.Scanner. The only options I know of are using something like SQLScanner or creating a specific type for each array that implements sql.Scanner. See https://github.com/jackc/pgx/issues/1781 for an example.

jackc avatar Oct 27 '23 21:10 jackc

Maybe the following code could help smbd:


type Array[T any] []T

func (a *Array[T]) Scan(src any) error {
	m := pgtype.NewMap()

	v := (*[]T)(a)

	t, ok := m.TypeForValue(v)
	if !ok {
		return fmt.Errorf("cannot convert to sql.Scanner: cannot find registered type for %T", a)
	}

	var bufSrc []byte
	if src != nil {
		switch src := src.(type) {
		case string:
			bufSrc = []byte(src)
		case []byte:
			bufSrc = src
		default:
			bufSrc = []byte(fmt.Sprint(bufSrc))
		}
	}

	return m.Scan(t.OID, pgtype.TextFormatCode, bufSrc, v)
}

regeda avatar Oct 31 '23 12:10 regeda

Thanks @regeda, this works great for pgtype.FlatArray[T] and pgtype.Multirange[T] because both are type aliases for []T which represents a builtin type. In case anyone is trying to make it work with pgtype.Range[T], the following works. A similar approach should work for pgtype.Array.

type Range[T any] struct {
        pgtype.Range[T]
}

func (a *Range[T]) Scan(src any) error {
        m := pgtype.NewMap()

        v := (*pgtype.Range[T])(&a.Range)

        t, ok := m.TypeForValue(v)
        if !ok {
                return fmt.Errorf("cannot convert to sql.Scanner: cannot find registered type for %T", a)
        }

        var bufSrc []byte
        if src != nil {
                switch src := src.(type) {
                case string:
                        bufSrc = []byte(src)
                case []byte:
                        bufSrc = src
                default:
                        bufSrc = []byte(fmt.Sprint(bufSrc))
                }
        }

        return m.Scan(t.OID, pgtype.TextFormatCode, bufSrc, v)
}

gspeicher avatar Mar 25 '24 21:03 gspeicher

I have queries that use jmoiron/sqlx so I cannot use pgtype.Map.SQLScanner on arrays. I was previously using the type specific array structs on pgx-v4. I managed to workaround the issue by using to_jsonb(my_array) in the query, scanning it to a []byte and then calling json.Unmarshal() to parse it to the Go type.

kirk-anchor avatar May 09 '24 12:05 kirk-anchor

Candidate solution: https://github.com/jackc/pgx/pull/2020

jackc avatar May 19 '24 13:05 jackc