That String Array

October 11, 2019
golang gqlgen graphql sql cockroachdb

If you are using gqlgen to generate your GraphQL endpoint and sqlx or some other kind of ORM for your database, you can benefit from using one type or struct in both directions. You create one type User struct for GraphQL and it maps to your db schema. Now if this user contains more complex types, things become interesting…

Requirements

Situation

We have a struct with a string slice type:

type User struct {
    Friends []string `db:"friends"`
}

This works quite well with gqlgen as it translates straight forward to a [String] in GraphQL (quick warning here: If the struct doesn’t match the graphql schema, gqlgen just doesn’t generate the code leaving you with a partially implemented interface of your endpoint).

Now let’s have a look at the database side of things. The schema representing our users will look like this:

CREATE TABLE IF NOT EXISTS users (
    friends STRING[]
)

Notice the friends column having the type STRING[] which is equivalent to STRING ARRAY.

If if we now select from that table and want to scan into our User struct, we are greeted with:

Error: sql: Scan error on column index 1, name “friends”: unsupported Scan, storing driver.Value type []uint8 into type *[]string

Apparently the driver returns a byte array.

Solution

This seems like a problem someone else had before, so a quick search suggest pq.StringArray as the matching type. This leaves us with a new struct definition:

type User struct {
    Friends pq.StringArray `db:"friends"`
}

This solves the issue with scanning the column into the struct. Reason for this is the .Value and .Scan methods on the StringArray type instructing sql how to scan the data from the database. More details on this can be found in the sqlx documentation.

Now when regenerating the GraphQL endpoint, our User suddenly disappears. From our gqlgen configuration we know that we provide the model for the User:

models:
  User:
    model: github.com/user/package/model.User

so when we generate the GraphQL endpoint, gqlgen fails as the pq.StringArray doesn’t match the [String] in the schema definition. In order to fix this, we have to update our GraphQL schema:

type User {
  friends: StringArray
}

StringArray is a custom scalar which we can define as scalar StringArray in the schema. StringArray is not defined yet so we point gqlgen to the correct place via the configuration:

StringArray:
    model: github.com/user/package/gql.StringArray

Where qgl is my package containing everything gqlgen. Types are sorted now and we get back our User in the GraphQL schema. The next problem is that gqlgen doesn’t know how to serialize from or to the StringArray type. This problem can be solved by providing an external marshaler which is very well described in the gqlgen documentation.

func MarshalStringArray(a pq.StringArray) graphql.Marshaler {
	return graphql.WriterFunc(func(w io.Writer) {
		data, _ := json.Marshal(a)
		io.WriteString(w, string(data))
	})
}

func UnmarshalStringArray(v interface{}) (pq.StringArray, error) {
	a, ok := v.(pq.StringArray)
	if !ok {
		return nil, errors.New("failed to cast to pq.StringArray")
	}
	return a, nil
}

The data, _ := json.Marshal(a) line is for convenience and asks for a more sophisticated solution.

Now all those steps together will provide you with a single model which can be used by gqlgen to generate the GraphQL endpoint and by sqlx to write and scan.

Headless BPF

December 30, 2023

I’ve been using the Berkeley Packet Filter from Golang’s wrapper around the PCAP library whenever I needed to filter network packets programmatically:

golang pcap bpf

Byte Replace

October 19, 2023

You are probably very familiar with the strings function to replace a substring:

golang benchmark patch