diff --git a/.travis.yml b/.travis.yml index 537dca8d..b120b33a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,8 @@ env: - PGVERSION=9.2 - PGVERSION=9.1 +# The tricky test user, below, has to actually exist so that it can be used in a test +# of aclitem formatting. It turns out aclitems cannot contain non-existing users/roles. before_script: - mv conn_config_test.go.travis conn_config_test.go - psql -U postgres -c 'create database pgx_test' @@ -36,6 +38,7 @@ before_script: - psql -U postgres -c "create user pgx_ssl SUPERUSER PASSWORD 'secret'" - psql -U postgres -c "create user pgx_md5 SUPERUSER PASSWORD 'secret'" - psql -U postgres -c "create user pgx_pw SUPERUSER PASSWORD 'secret'" + - psql -U postgres -c "create user \" tricky, ' } \"\" \\ test user \" superuser password 'secret'" install: - go get -u github.com/shopspring/decimal diff --git a/README.md b/README.md index cf244b04..b78fbc5c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ To setup the normal test environment, first install these dependencies: Then run the following SQL: create user pgx_md5 password 'secret'; + create user " tricky, ' } "" \ test user " superuser password 'secret'; create database pgx_test; Connect to database pgx_test and run: diff --git a/values.go b/values.go index f2e43473..6cb6e429 100644 --- a/values.go +++ b/values.go @@ -44,6 +44,7 @@ const ( Int8ArrayOid = 1016 Float4ArrayOid = 1021 Float8ArrayOid = 1022 + AclItemOid = 1033 InetArrayOid = 1041 VarcharOid = 1043 DateOid = 1082 @@ -89,6 +90,7 @@ func init() { "_timestamp": BinaryFormatCode, "_timestamptz": BinaryFormatCode, "_varchar": BinaryFormatCode, + "aclitem": TextFormatCode, // Pg's src/backend/utils/adt/acl.c has only in/out (text) not send/recv (bin) "bool": BinaryFormatCode, "bytea": BinaryFormatCode, "char": BinaryFormatCode, @@ -263,6 +265,58 @@ func (n NullString) Encode(w *WriteBuf, oid Oid) error { return encodeString(w, oid, n.String) } +// AclItem is used for PostgreSQL's aclitem data type. A sample aclitem +// might look like this: +// +// postgres=arwdDxt/postgres +// +// Note, however, that because the user/role name part of an aclitem is +// an identifier, it follows all the usual formatting rules for SQL +// identifiers: if it contains spaces and other special characters, +// it should appear in double-quotes: +// +// postgres=arwdDxt/"role with spaces" +// +type AclItem string + +// NullAclItem represents a pgx.AclItem that may be null. NullAclItem implements the +// Scanner and Encoder interfaces so it may be used both as an argument to +// Query[Row] and a destination for Scan for prepared and unprepared queries. +// +// If Valid is false then the value is NULL. +type NullAclItem struct { + AclItem AclItem + Valid bool // Valid is true if AclItem is not NULL +} + +func (n *NullAclItem) Scan(vr *ValueReader) error { + if vr.Type().DataType != AclItemOid { + return SerializationError(fmt.Sprintf("NullAclItem.Scan cannot decode OID %d", vr.Type().DataType)) + } + + if vr.Len() == -1 { + n.AclItem, n.Valid = "", false + return nil + } + + n.Valid = true + n.AclItem = AclItem(decodeText(vr)) + return vr.Err() +} + +// Particularly important to return TextFormatCode, seeing as Postgres +// only ever sends aclitem as text, not binary. +func (n NullAclItem) FormatCode() int16 { return TextFormatCode } + +func (n NullAclItem) Encode(w *WriteBuf, oid Oid) error { + if !n.Valid { + w.WriteInt32(-1) + return nil + } + + return encodeString(w, oid, string(n.AclItem)) +} + // Name is a type used for PostgreSQL's special 63-byte // name data type, used for identifiers like table names. // The pg_class.relname column is a good example of where the @@ -964,6 +1018,10 @@ func Encode(wbuf *WriteBuf, oid Oid, arg interface{}) error { return encodeUInt(wbuf, oid, arg) case Char: return encodeChar(wbuf, oid, arg) + case AclItem: + // The aclitem data type goes over the wire using the same format as string, + // so just cast to string and use encodeString + return encodeString(wbuf, oid, string(arg)) case Name: // The name data type goes over the wire using the same format as string, // so just cast to string and use encodeString @@ -1146,6 +1204,9 @@ func Decode(vr *ValueReader, d interface{}) error { *v = uint64(n) case *Char: *v = decodeChar(vr) + case *AclItem: + // aclitem goes over the wire just like text + *v = AclItem(decodeText(vr)) case *Name: // name goes over the wire just like text *v = Name(decodeText(vr)) diff --git a/values_test.go b/values_test.go index ab40c65c..8b85ceef 100644 --- a/values_test.go +++ b/values_test.go @@ -562,6 +562,7 @@ func TestNullX(t *testing.T) { i16 pgx.NullInt16 i32 pgx.NullInt32 c pgx.NullChar + a pgx.NullAclItem n pgx.NullName oid pgx.NullOid xid pgx.NullXid @@ -599,6 +600,10 @@ func TestNullX(t *testing.T) { {"select $1::\"char\"", []interface{}{pgx.NullChar{Char: 255, Valid: true}}, []interface{}{&actual.c}, allTypes{c: pgx.NullChar{Char: 255, Valid: true}}}, {"select $1::name", []interface{}{pgx.NullName{Name: "foo", Valid: true}}, []interface{}{&actual.n}, allTypes{n: pgx.NullName{Name: "foo", Valid: true}}}, {"select $1::name", []interface{}{pgx.NullName{Name: "foo", Valid: false}}, []interface{}{&actual.n}, allTypes{n: pgx.NullName{Name: "", Valid: false}}}, + {"select $1::aclitem", []interface{}{pgx.NullAclItem{AclItem: "postgres=arwdDxt/postgres", Valid: true}}, []interface{}{&actual.a}, allTypes{a: pgx.NullAclItem{AclItem: "postgres=arwdDxt/postgres", Valid: true}}}, + {"select $1::aclitem", []interface{}{pgx.NullAclItem{AclItem: "postgres=arwdDxt/postgres", Valid: false}}, []interface{}{&actual.a}, allTypes{a: pgx.NullAclItem{AclItem: "", Valid: false}}}, + // A tricky (and valid) aclitem can still be used, especially with Go's useful backticks + {"select $1::aclitem", []interface{}{pgx.NullAclItem{AclItem: `postgres=arwdDxt/" tricky, ' } "" \ test user "`, Valid: true}}, []interface{}{&actual.a}, allTypes{a: pgx.NullAclItem{AclItem: `postgres=arwdDxt/" tricky, ' } "" \ test user "`, Valid: true}}}, {"select $1::cid", []interface{}{pgx.NullCid{Cid: 1, Valid: true}}, []interface{}{&actual.cid}, allTypes{cid: pgx.NullCid{Cid: 1, Valid: true}}}, {"select $1::cid", []interface{}{pgx.NullCid{Cid: 1, Valid: false}}, []interface{}{&actual.cid}, allTypes{cid: pgx.NullCid{Cid: 0, Valid: false}}}, {"select $1::cid", []interface{}{pgx.NullCid{Cid: 4294967295, Valid: true}}, []interface{}{&actual.cid}, allTypes{cid: pgx.NullCid{Cid: 4294967295, Valid: true}}},