How to write Scan() for custom type in PostgreSQL column in Go

Viewed 587

So I have this table Schema that looks like the following

CREATE TABLE artists (
  id SERIAL PRIMARY KEY,
  foo_user_id TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
  time_span TIME_SPAN NOT NULL,
  items artist [] NOT NULL
);

In addition, I also have the custom type created in PostgreSQL that is defined as follows

CREATE TYPE artist AS (
  artist_name TEXT,
  foo_artist_id TEXT,
  artist_image_url TEXT,
  artist_rank int
);

I am trying to query all rows that have the "foo_user_id" equal to what I pass into the function. Here is the sample code.

func GetHistoricalTopArtists(foo_user_id string) ([]TopArtists, error) {
// connect to DB etc..

// create prepared statement
stmtStr := `SELECT * FROM artists WHERE foo_user_id=$1`

// error check...

// iterate through all rows to get an array of []TopArtists
defer rows.Close()
    for rows.Next() {
        topArtist := new(TopArtists)
        err := rows.Scan(&topArtist.Id, &topArtist.FooUserId, &topArtist.CreatedAt, &topArtist.TimeSpan, &topArtist.Artists)
        if err != nil {
            log.Fatalf("Something went wrong %v", err)
        }
        topArtists = append(topArtists, *topArtist)
    }
}

To represent this data in Go I created the following structs

// Represents a row
type TopArtists struct {
    Id        int64    `json:"id" db:"id"`
    FooUserId string   `json:"foo_user_id" db:"foo_user_id"`
    CreatedAt string   `json:"created_at" db:"created_at"`
    TimeSpan  string   `json:"time_span" db:"time_span"`
    Artists   []Artist `json:"items" db:"items"`
}

// Represents the artist column
type Artist struct {
    ArtistName      string `json:"artist_name"`
    ArtistId        string `json:"foo_artist_id"`
    ArtistImageURL  string `json:"artist_image_url"`
    ArtistRank      int    `json:"artist_rank"`
}

When I call the function that does the query (the one I described above). I get the following error.

Scan error on column index 4, name "items": unsupported Scan, storing driver.Value type []uint8 into type *[]database.Artist.

I have a Value() function, but I am unsure how to implement a Scan() function for the array of the custom struct I have made.

Here is my Value() function, I have attempted to read documentation and similar posts on scanning arrays of primitive types (strings, int, etc) but I could not apply the logic to custom PostgreSQL types.

func (a Artist) Value() (driver.Value, error) {
    s := fmt.Sprintf("(%s, %s, %s, %d)",
        a.ArtistName,
        a.FooArtistId,
        a.ArtistImageURL,
        a.ArtistRank)
    return []byte(s), nil
}
1 Answers

@mkopriva - ...You need to declare a slice type, e.g. type ArtistSlice []Artist, use that as the field's type, and implement the Value/Scan methods on that.

Created custom Composite Types Artist in Postgresq has a strict struct as

{(david,38,url,1),(david2,2,"url 2",2)}

then you have to implement Value/Scan method with custom marshal/unmarshal algorithm

For example

type Artists []Artist

func (a *Artists) Scan(value interface{}) error {
    source, ok := value.(string) // input example  {"(david,38,url,1)","(david2,2,\"url 2\",2)"}
    if !ok {
        return errors.New("incompatible type")
    }
    
    var res Artists
    artists := strings.Split(source, "\",\"")
    for _, artist := range artists {
        for _, old := range []string{"\\\"","\"","{", "}","(",")"} {
            artist = strings.ReplaceAll(artist, old, "")
        }
        artistRawData := strings.Split(artist, ",")
        i, err := strconv.Atoi(artistRawData[1])
        if err != nil {
            return fmt.Errorf("parce ArtistRank raw data (%s) in %d iteration error: %v", artist, i, err)
        }
        res = append(res, Artist{
            ArtistName:     artistRawData[0],
            ArtistId:       artistRawData[1],
            ArtistImageURL: artistRawData[2],
            ArtistRank:     i,
        })
    }
    *a = res
    return nil
}
Related