Skip to content

Instantly share code, notes, and snippets.

@alexisvisco
Last active June 25, 2024 06:51
Show Gist options
  • Select an option

  • Save alexisvisco/158d368b16faf2864f0485929a4cd38a to your computer and use it in GitHub Desktop.

Select an option

Save alexisvisco/158d368b16faf2864f0485929a4cd38a to your computer and use it in GitHub Desktop.

Revisions

  1. alexisvisco revised this gist Jan 26, 2024. 1 changed file with 26 additions and 12 deletions.
    38 changes: 26 additions & 12 deletions hstore_gorm.go
    Original file line number Diff line number Diff line change
    @@ -4,15 +4,28 @@ import (
    "context"
    "database/sql"
    "database/sql/driver"
    "encoding/json"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
    "gorm.io/gorm/schema"
    "strings"
    )

    // Hstore is a wrapper for transferring Hstore values back and forth easily.
    type Hstore struct {
    Map map[string]sql.NullString
    type Hstore map[string]sql.NullString

    func (h Hstore) ToMap() map[string]string {
    m := make(map[string]string)
    for k, v := range h {
    if v.Valid {
    m[k] = v.String
    }
    }
    return m
    }

    func (h Hstore) MarshalJSON() ([]byte, error) {
    return json.Marshal(h.ToMap())
    }

    // escapes and quotes hstore keys/values
    @@ -41,18 +54,19 @@ func hQuote(s interface{}) string {
    // hstore column's database value is NULL, then h.Map is set to nil instead.
    func (h *Hstore) Scan(value interface{}) error {
    if value == nil {
    h.Map = nil
    h = nil
    return nil
    }
    h.Map = make(map[string]sql.NullString)
    *h = make(map[string]sql.NullString)
    var b byte
    pair := [][]byte{{}, {}}
    pi := 0
    inQuote := false
    didQuote := false
    sawSlash := false
    bindex := 0
    for bindex, b = range value.([]byte) {

    for bindex, b = range []byte(value.(string)) {
    if sawSlash {
    pair[pi] = append(pair[pi], b)
    sawSlash = false
    @@ -83,9 +97,9 @@ func (h *Hstore) Scan(value interface{}) error {
    case ',':
    s := string(pair[1])
    if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
    h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
    (*h)[string(pair[0])] = sql.NullString{String: "", Valid: false}
    } else {
    h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    (*h)[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    }
    pair[0] = []byte{}
    pair[1] = []byte{}
    @@ -99,9 +113,9 @@ func (h *Hstore) Scan(value interface{}) error {
    if bindex > 0 {
    s := string(pair[1])
    if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
    h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
    (*h)[string(pair[0])] = sql.NullString{String: "", Valid: false}
    } else {
    h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    (*h)[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    }
    }
    return nil
    @@ -110,11 +124,11 @@ func (h *Hstore) Scan(value interface{}) error {
    // Value implements the driver Valuer interface. Note if h.Map is nil, the
    // database column value will be set to NULL.
    func (h Hstore) Value() (driver.Value, error) {
    if h.Map == nil {
    if h == nil {
    return nil, nil
    }
    parts := []string{}
    for key, val := range h.Map {
    for key, val := range h {
    thispart := hQuote(key) + "=>" + hQuote(val)
    parts = append(parts, thispart)
    }
    @@ -136,7 +150,7 @@ func (h Hstore) GormDBDataType(db *gorm.DB, field *schema.Field) string {
    }

    func (h Hstore) GormValue(_ context.Context, db *gorm.DB) clause.Expr {
    if len(h.Map) == 0 {
    if len(h) == 0 {
    return gorm.Expr("NULL")
    }

  2. alexisvisco revised this gist Dec 20, 2023. No changes.
  3. alexisvisco created this gist Dec 20, 2023.
    146 changes: 146 additions & 0 deletions hstore_gorm.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,146 @@
    package hstore

    import (
    "context"
    "database/sql"
    "database/sql/driver"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
    "gorm.io/gorm/schema"
    "strings"
    )

    // Hstore is a wrapper for transferring Hstore values back and forth easily.
    type Hstore struct {
    Map map[string]sql.NullString
    }

    // escapes and quotes hstore keys/values
    // s should be a sql.NullString or string
    func hQuote(s interface{}) string {
    var str string
    switch v := s.(type) {
    case sql.NullString:
    if !v.Valid {
    return "NULL"
    }
    str = v.String
    case string:
    str = v
    default:
    panic("not a string or sql.NullString")
    }

    str = strings.Replace(str, "\\", "\\\\", -1)
    return `"` + strings.Replace(str, "\"", "\\\"", -1) + `"`
    }

    // Scan implements the Scanner interface.
    //
    // Note h.Map is reallocated before the scan to clear existing values. If the
    // hstore column's database value is NULL, then h.Map is set to nil instead.
    func (h *Hstore) Scan(value interface{}) error {
    if value == nil {
    h.Map = nil
    return nil
    }
    h.Map = make(map[string]sql.NullString)
    var b byte
    pair := [][]byte{{}, {}}
    pi := 0
    inQuote := false
    didQuote := false
    sawSlash := false
    bindex := 0
    for bindex, b = range value.([]byte) {
    if sawSlash {
    pair[pi] = append(pair[pi], b)
    sawSlash = false
    continue
    }

    switch b {
    case '\\':
    sawSlash = true
    continue
    case '"':
    inQuote = !inQuote
    if !didQuote {
    didQuote = true
    }
    continue
    default:
    if !inQuote {
    switch b {
    case ' ', '\t', '\n', '\r':
    continue
    case '=':
    continue
    case '>':
    pi = 1
    didQuote = false
    continue
    case ',':
    s := string(pair[1])
    if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
    h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
    } else {
    h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    }
    pair[0] = []byte{}
    pair[1] = []byte{}
    pi = 0
    continue
    }
    }
    }
    pair[pi] = append(pair[pi], b)
    }
    if bindex > 0 {
    s := string(pair[1])
    if !didQuote && len(s) == 4 && strings.ToLower(s) == "null" {
    h.Map[string(pair[0])] = sql.NullString{String: "", Valid: false}
    } else {
    h.Map[string(pair[0])] = sql.NullString{String: string(pair[1]), Valid: true}
    }
    }
    return nil
    }

    // Value implements the driver Valuer interface. Note if h.Map is nil, the
    // database column value will be set to NULL.
    func (h Hstore) Value() (driver.Value, error) {
    if h.Map == nil {
    return nil, nil
    }
    parts := []string{}
    for key, val := range h.Map {
    thispart := hQuote(key) + "=>" + hQuote(val)
    parts = append(parts, thispart)
    }
    return []byte(strings.Join(parts, ",")), nil
    }

    // GormDataType gorm common data type
    func (h Hstore) GormDataType() string {
    return "hstore"
    }

    // GormDBDataType gorm db data type
    func (h Hstore) GormDBDataType(db *gorm.DB, field *schema.Field) string {
    switch db.Dialector.Name() {
    case "postgres":
    return "HSTORE"
    }
    return ""
    }

    func (h Hstore) GormValue(_ context.Context, db *gorm.DB) clause.Expr {
    if len(h.Map) == 0 {
    return gorm.Expr("NULL")
    }

    data, _ := h.Value()

    return gorm.Expr("?", string(data.([]byte)))
    }