🐘
pgx/v5で配列型・任意の型を受け取る
配列型を受け取る
pgx/v5のstdlibを使っている場合、PostgreSQLの配列に対してGolangのスライスを渡すのは問題ないのですが、PostgreSQLの配列を受け取るときにGolangのスライスをそのまま渡すとエラーになります。
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
conn, err := sql.Open("pgx", "postgres://postgres@localhost:5432/postgres")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
var val []int
err = conn.QueryRow("SELECT $1::int[]", []int{1, 2, 3}).Scan(&val)
//=> 2025/09/15 11:22:53 sql: Scan error on column index 0, name "int4": unsupported Scan, storing driver.Value type string into type *[]int
if err != nil {
log.Fatal(err)
}
fmt.Println(val)
}
エラーを回避するにはpgtype.NewMap()を使う必要があります。
- https://github.com/jackc/pgx/blob/61d3c965ad442cc14d6b0e39e0ab3821f3684c03/stdlib/sql.go#L58-L65
-
Support StringArray? · Issue #771 · jackc/pgx ※
pgtype.FlatArrayは不要でした
err = conn.QueryRow("SELECT $1::int[]", []int{1, 2, 3}).Scan(pgtype.NewMap().SQLScanner(&val))
if err != nil {
log.Fatal(err)
}
fmt.Println(val) //=> [1, 2, 3]
}
また、ドライバが混在するのですがpq.Array()でも受け取れます。
var val []sql.NullInt32
err = conn.QueryRow("SELECT $1::int[]", []int{1, 2, 3}).Scan(pq.Array(&val))
if err != nil {
log.Fatal(err)
}
fmt.Println(val) //=> [{1 true} {2 true} {3 true}]
sql.Row#Scan()のコードを追ってみると、引数がsql.Scannerを実装していた場合、そちらに処理を委譲するようになっていました。
pgtype.NewMap().SQLScanner()は、まずNewMap()で*pgtype.Mapを生成し、SQLScanner()がマッピングに従ってPostgreSQLの型をGolangの型に変換するsql.Scannerを返すことで、配列をスライスとして受け取れるようです。
ドメイン型をurl.URLで受け取る
pgtype.NewMap()が返すマップに新しい型を登録すれば、任意のPostgreSQLの型をGolangの型で受け取ることができるはずです。
ためしに、URLのドメイン型を定義して、url.URLで受け取ってみました。
URLドメイン型とテーブルを定義します。
CREATE DOMAIN url AS text;
CREATE TABLE urls (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
url url
);
INSERT INTO urls (name, url) VALUES ('example', 'https://example.com');
URLドメイン型のOIDを調べます。
postgres=# SELECT oid FROM pg_type WHERE typname = 'url';
oid
-------
16403
(1 row)
GolangでURLドメイン型のコーデックを作成します。
package main
import (
"database/sql/driver"
"fmt"
"net/url"
"github.com/jackc/pgx/v5/pgtype"
)
type URLCodec struct{}
func (URLCodec) FormatSupported(format int16) bool {
return format == pgtype.TextFormatCode
}
func (URLCodec) PreferredFormat() int16 {
return pgtype.TextFormatCode
}
func (URLCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan {
switch format {
case pgtype.TextFormatCode:
switch value.(type) {
case string:
return encodePlanURLCodecString{}
}
}
return nil
}
type encodePlanURLCodecString struct{}
func (encodePlanURLCodecString) Encode(value any, buf []byte) (newBuf []byte, err error) {
s := value.(string)
buf = append(buf, s...)
return buf, nil
}
func (URLCodec) PlanScan(m *pgtype.Map, oid uint32, format int16, target any) pgtype.ScanPlan {
switch format {
case pgtype.TextFormatCode:
switch target.(type) {
case **url.URL:
return scanPlanTextAnyToURL{}
}
}
return nil
}
func (c URLCodec) DecodeDatabaseSQLValue(m *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) {
return c.DecodeValue(m, oid, format, src)
}
func (c URLCodec) DecodeValue(m *pgtype.Map, oid uint32, format int16, src []byte) (any, error) {
if src == nil {
return nil, nil
}
return url.Parse(string(src))
}
type scanPlanTextAnyToURL struct{}
func (scanPlanTextAnyToURL) Scan(src []byte, dst any) error {
if src == nil {
return fmt.Errorf("cannot scan NULL into %T", dst)
}
u, err := url.Parse(string(src))
if err != nil {
return err
}
p := (dst).(**url.URL)
*p = u
return nil
}
pgtype.MapにOID・コーデック・Golangの型を登録すると、url.URLでカラムの値を受け取れます。
package main
import (
"database/sql"
"fmt"
"log"
"net/url"
"github.com/jackc/pgx/v5/pgtype"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
conn, err := sql.Open("pgx", "postgres://postgres@localhost:5432/postgres")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
m := pgtype.NewMap()
m.RegisterType(&pgtype.Type{Name: "url", OID: 16403, Codec: URLCodec{}})
m.RegisterDefaultPgType((**url.URL)(nil), "url")
url := &url.URL{}
err = conn.QueryRow("SELECT url FROM urls WHERE name = $1", "example").Scan(m.SQLScanner(&url))
if err != nil {
log.Fatal(err)
}
fmt.Printf("%[1]s (%[1]T)\n", url) //=> https://example.com (*url.URL)
}
Discussion