title: How to implement nested composite types with pgx/pgtype
body: <p>To avoid complex 1-to-1 mappings between Postgres tables, I thought it would be a good idea to use composite types instead. So I created these two types, and a table to hold them:</p>
  <pre class="lang-sql prettyprint-override"><code>CREATE TYPE my_point AS (x INTEGER, y INTEGER);
  CREATE TYPE my_rectangle AS (p1 my_point, p2 my_point);
  CREATE TABLE zoo (id INTEGER, r my_rectangle, p my_point);
  </code></pre>
  <p><em>(The types and tables are just for the example. In the real project, they are quite different, so there's no point in proposing alternative solutions, such as using the built-in Postgres <code>point</code> type)</em></p>
  <p>So far so good. It works as expected when doing manual queries in <code>psql</code> etc. Now to the problem:</p>
  <p>I'm trying to access these types using <code>go</code>, <code>pgx</code> and <code>sqlx</code>. For the <code>my_point</code> type, I got a working solution quite easily by mostly copying the <code>point</code> implementation from <code>pgtype</code>. But no such luck for the <code>my_rectangle</code> type. I've tried to come up with multiple solutions, but run into different problems with each. The error I get with the code below for example is:</p>
  <blockquote>
  <p>Unknown OID for model.MyPointType{Point:model.MyPoint{X:20, Y:21}, Status:0x2}</p>
  </blockquote>
  <p>Here's my test function:</p>
  <pre class="lang-golang prettyprint-override"><code>
  func TestCompositeTypes(t *testing.T) {
      // Setup database connection ...
      conn := ...
      ci := conn.ConnInfo()
  
      // Get OID for &quot;my_point&quot;
      var myPointOID uint32
      err = conn.QueryRow(context.Background(), `select 'my_point'::regtype::oid`).Scan(&amp;myPointOID)
      require.NoError(t, err)
  
      // Get OID for &quot;my_rectangle&quot;
      var myRectangleOID uint32
      err = conn.QueryRow(context.Background(), `select 'my_rectangle'::regtype::oid`).Scan(&amp;myRectangleOID)
      require.NoError(t, err)
  
      ci.RegisterDataType(pgtype.DataType{
          Value: &amp;MyPointType{},
          Name: &quot;my_point&quot;,
          OID: myPointOID,
      })
  
      ci.RegisterDataType(pgtype.DataType{
          Value: &amp;MyRectangleType{},
          Name: &quot;my_rectangle&quot;,
          OID: myRectangleOID,
      })
  
      p := MyPointType{
          Point:  MyPoint{50, 100},
          Status: pgtype.Present,
      }
  
      // This works!
      _, err = db.Connection.Exec(
          &quot;INSERT INTO zoo (id, p) VALUES ($1, $2::my_point)&quot;,
          100,
          p
      )
      require.NoError(t, err)
  
      r := MyRectangleType{
          P1:     MyPointType{Point: MyPoint{20, 21}, Status: pgtype.Present},
          P2:     MyPointType{Point: MyPoint{30, 31}, Status: pgtype.Present},
          Status: pgtype.Present,
      }
  
      // This does *not* work! (see error above)
      _, err = db.Connection.Exec(
          &quot;INSERT INTO zoo (id, r) VALUES ($1, $2::my_rectangle)&quot;,
          100,
          r
      )
      require.NoError(t, err)
  }
  </code></pre>
  <p>Here's the implementation of <code>MyPoint</code>:</p>
  <pre class="lang-golang prettyprint-override"><code>type MyPoint struct {
      X int
      Y int
  }
  
  type MyPointType struct {
      Point  MyPoint
      Status pgtype.Status
  }
  
  func (p *MyPointType) AssignTo(dst interface{}) error {
      return fmt.Errorf(&quot;cannot assign %v to %T&quot;, p, dst)
  }
  
  func (p MyPointType) Get() interface{} {
      switch p.Status {
      case pgtype.Present:
          return p
      case pgtype.Null:
          return nil
      default:
          return p.Status
      }
  }
  
  func parsePoint(src []byte) (*MyPointType, error) {
      if src == nil || bytes.Compare(src, []byte(&quot;null&quot;)) == 0 {
          return &amp;MyPointType{Status: pgtype.Null}, nil
      }
  
      if len(src) &lt; 5 {
          return nil, fmt.Errorf(&quot;invalid length for my_point: %v&quot;, len(src))
      }
  
      if src[0] == '&quot;' &amp;&amp; src[len(src)-1] == '&quot;' {
          src = src[1 : len(src)-1]
      }
  
      parts := strings.SplitN(string(src[1:len(src)-1]), &quot;,&quot;, 2)
      if len(parts) &lt; 2 {
          return nil, fmt.Errorf(&quot;invalid format for my_point&quot;)
      }
  
      x, err := strconv.ParseInt(parts[0], 10, 32)
      if err != nil {
          return nil, err
      }
  
      y, err := strconv.ParseInt(parts[1], 10, 32)
      if err != nil {
          return nil, err
      }
  
      return &amp;MyPointType{Point: MyPoint{int(x), int(y)}, Status: pgtype.Present}, nil
  }
  
  func (p *MyPointType) Set(src interface{}) error {
      if src == nil {
          p.Status = pgtype.Null
          return nil
      }
      err := fmt.Errorf(&quot;cannot convert %v to MyPointType&quot;, src)
      var parsed *MyPointType
      switch value := src.(type) {
      case string:
          parsed, err = parsePoint([]byte(value))
      case []byte:
          parsed, err = parsePoint(value)
      default:
          return err
      }
      if err != nil {
          return err
      }
      *p = *parsed
      return nil
  }
  
  func (p MyPointType) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
      switch p.Status {
      case pgtype.Null:
          return nil, nil
      case pgtype.Undefined:
          return nil, errUndefined
      }
  
      // This is how pgtype.Point does it, but I think my implementation below
      // works similarly and is more safe...?
      //
      // buf = pgio.AppendUint64(buf, math.Float64bits(src.P.X))
      // buf = pgio.AppendUint64(buf, math.Float64bits(src.P.Y))
      // return buf, nil
  
      return (pgtype.CompositeFields{int32(p.Point.X), int32(p.Point.Y)}).EncodeBinary(ci, buf)
  }
  </code></pre>
  <p>And here's the implementation of <code>MyRectangleType</code>:</p>
  <pre class="lang-golang prettyprint-override"><code>type MyRectangleType struct {
      P1     MyPointType
      P2     MyPointType
      Status pgtype.Status
  }
  
  func (r *MyRectangleType) AssignTo(dst interface{}) error {
      return fmt.Errorf(&quot;cannot assign %v to %T&quot;, r, dst)
  }
  
  func (r MyRectangleType) Get() interface{} {
      switch r.Status {
      case pgtype.Present:
          return r
      case pgtype.Null:
          return nil
      default:
          return r.Status
      }
  }
  
  
  func parseRectangle(src []byte) (*MyRectangleType, error) {
      if src == nil || bytes.Compare(src, []byte(&quot;null&quot;)) == 0 {
          return &amp;MyRectangleType{Status: pgtype.Null}, nil
      }
  
      if len(src) &lt; 5 {
          return nil, fmt.Errorf(&quot;invalid length for my_rectangle: %v&quot;, len(src))
      }
  
      if src[0] == '&quot;' &amp;&amp; src[len(src)-1] == '&quot;' {
          src = src[1 : len(src)-1]
      }
  
      parts := strings.SplitN(string(src[1:len(src)-1]), &quot;,&quot;, 2)
      if len(parts) &lt; 2 {
          return nil, fmt.Errorf(&quot;invalid format for my_rectangle&quot;)
      }
  
      p1, err := parsePoint([]byte(parts[0]))
      if err != nil {
          return nil, err
      }
  
      p2, err := parsePoint([]byte(parts[1]))
      if err != nil {
          return nil, err
      }
  
      return &amp;MyRectangleType{P1: *p1, P2: *p2, Status: pgtype.Present}, nil
  }
  
  func (r *MyRectangleType) Set(src interface{}) error {
      if src == nil {
          r.Status = pgtype.Null
          return nil
      }
      err := fmt.Errorf(&quot;cannot convert %v to MyRectangleType&quot;, src)
      var parsed *MyRectangleType
      switch value := src.(type) {
      case string:
          parsed, err = parseRectangle([]byte(value))
      case []byte:
          parsed, err = parseRectangle(value)
      default:
          return err
      }
      if err != nil {
          return err
      }
      *r = *parsed
      return nil
  }
  
  func (r MyRectangleType) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
      switch r.Status {
      case pgtype.Null:
          return nil, nil
      case pgtype.Undefined:
          return nil, errUndefined
      }
  
      return (pgtype.CompositeFields{
          r.P1,
          r.P2,
      }).EncodeBinary(ci, buf)
  }
  </code></pre>
  <p>What do I do wrong? What can be improved? I'm more concerned about readability than performance.</p>