Dew

JSONB

Type-safe PostgreSQL JSONB columns.

Dew supports PostgreSQL JSONB columns with type-safe operators via JSONBColumn[T].

Define a JSONB column

JSONB column types must implement sql.Scanner and driver.Valuer (the dew.JSONB constraint). Due to a Go generics limitation, JSONB columns use a standalone function instead of a table method:

type Settings struct {
    Theme string `json:"theme"`
    Lang  string `json:"lang"`
}

func (s *Settings) Scan(src any) error {
    switch v := src.(type) {
    case []byte:  return json.Unmarshal(v, s)
    case string:  return json.Unmarshal([]byte(v), s)
    default:      return fmt.Errorf("unsupported type %T", src)
    }
}

func (s Settings) Value() (driver.Value, error) {
    return json.Marshal(s)
}

Then define the column in your schema:

var Users = dew.DefineSchema("users", dew.PostgreSQLDialect{}, func(t dew.Table[User]) struct {
    dew.Table[User]
    ID       dew.IntColumn
    Name     dew.StringColumn
    Settings dew.JSONBColumn[*Settings]
} {
    return struct {
        dew.Table[User]
        ID       dew.IntColumn
        Name     dew.StringColumn
        Settings dew.JSONBColumn[*Settings]
    }{
        Table:    t,
        ID:       t.IntColumn("id"),
        Name:     t.StringColumn("name"),
        Settings: dew.DefineJSONBColumn[*Settings](t, "settings"),
    }
})

Containment

Contains (@>)

// WHERE settings @> '{"theme":"dark"}'::jsonb
users, err := Users.From(db).
    Where(Users.Settings.Contains(&Settings{Theme: "dark"})).
    All()

ContainedBy (<@)

// WHERE settings <@ '{"theme":"dark","lang":"en"}'::jsonb
users, err := Users.From(db).
    Where(Users.Settings.ContainedBy(&Settings{Theme: "dark", Lang: "en"})).
    All()

Key existence

HasKey (?)

// WHERE settings ? 'theme'
users, err := Users.From(db).
    Where(Users.Settings.HasKey("theme")).
    All()

HasAnyKey (?|)

// WHERE settings ?| ARRAY['theme', 'lang']
users, err := Users.From(db).
    Where(Users.Settings.HasAnyKey("theme", "lang")).
    All()

HasAllKeys (?&)

// WHERE settings ?& ARRAY['theme', 'lang']
users, err := Users.From(db).
    Where(Users.Settings.HasAllKeys("theme", "lang")).
    All()

Equality

// WHERE settings = '...'::jsonb
users, err := Users.From(db).
    Where(Users.Settings.Eq(&Settings{Theme: "dark", Lang: "en"})).
    All()

// WHERE settings != '...'::jsonb
users, err := Users.From(db).
    Where(Users.Settings.NotEq(&Settings{Theme: "light"})).
    All()

Path navigation

Path (->)

Navigate into nested JSON keys. Returns a JSONBColumn for further chaining:

// WHERE settings->'nested' @> '{"key":"value"}'::jsonb
users, err := Users.From(db).
    Where(Users.Settings.Path("nested").Contains(&Settings{...})).
    All()

PathText (->>)

Extract a value as text. Returns a column usable in SELECT:

// SELECT name, settings->>'theme' FROM users
users, err := Users.From(db).
    Select(Users.Name, Users.Settings.PathText("theme")).
    All()

Deep paths

Chain Path for deep navigation, then use PathText for the final key:

// settings->'address'->'geo'->>'lat'
Users.Settings.Path("address", "geo").PathText("lat")

// settings->'address' @> '{"city":"Portland"}'
Users.Settings.Path("address").Contains(...)

Null checks

users, err := Users.From(db).
    Where(Users.Settings.IsNull()).
    All()

users, err := Users.From(db).
    Where(Users.Settings.IsNotNull()).
    All()

Composing with And/Or

JSONB expressions compose like any other expression:

users, err := Users.From(db).
    Where(dew.And(
        Users.Settings.HasKey("theme"),
        dew.Or(
            Users.Settings.Contains(&Settings{Theme: "dark"}),
            Users.Settings.Contains(&Settings{Theme: "light"}),
        ),
    )).
    All()

On this page