snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
{
"TableName": "\"test_models\"",
"ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"",
"Columns": [
"created_at",
"id",
"int",
"nid",
"null_time_ptr",
"string",
"updated_at"
],
"Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)"
}

View File

@@ -0,0 +1,14 @@
{
"TableName": "\"test_models\"",
"ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"",
"Columns": [
"created_at",
"id",
"int",
"nid",
"null_time_ptr",
"string",
"updated_at"
],
"Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)"
}

View File

@@ -0,0 +1,16 @@
[
"0001-01-01T00:00:00Z",
"0001-01-01T00:00:00Z",
"string",
42,
null,
{
"ID": "00000000-0000-0000-0000-000000000000",
"NID": "00000000-0000-0000-0000-000000000000",
"String": "string",
"Int": 42,
"NullTimePtr": null,
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z"
}
]

298
oryx/sqlxx/batch/create.go Normal file
View File

@@ -0,0 +1,298 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package batch
import (
"context"
"database/sql"
"fmt"
"reflect"
"sort"
"strings"
"time"
"github.com/jmoiron/sqlx/reflectx"
"github.com/ory/x/dbal"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/ory/pop/v6"
"github.com/ory/x/otelx"
"github.com/ory/x/sqlcon"
"github.com/ory/x/sqlxx"
)
type (
insertQueryArgs struct {
TableName string
ColumnsDecl string
Columns []string
Placeholders string
}
quoter interface {
Quote(key string) string
}
TracerConnection struct {
Tracer *otelx.Tracer
Connection *pop.Connection
}
)
func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *reflectx.Mapper, quoter quoter, models []*T) insertQueryArgs {
var (
v T
model = pop.NewModel(v, ctx)
columns []string
quotedColumns []string
placeholders []string
placeholderRow []string
)
for _, col := range model.Columns().Cols {
columns = append(columns, col.Name)
placeholderRow = append(placeholderRow, "?")
}
// We sort for the sole reason that the test snapshots are deterministic.
sort.Strings(columns)
for _, col := range columns {
quotedColumns = append(quotedColumns, quoter.Quote(col))
}
// We generate a list (for every row one) of VALUE statements here that
// will be substituted by their column values later:
//
// (?, ?, ?, ?),
// (?, ?, ?, ?),
// (?, ?, ?, ?)
for _, m := range models {
m := reflect.ValueOf(m)
pl := make([]string, len(placeholderRow))
copy(pl, placeholderRow)
// PostgreSQL-compatible databases can generate UUIDs using
// "gen_random_uuid()", which ends up in a VALUE statement of:
//
// (gen_random_uuid(), ?, ?, ?),
for k := range placeholderRow {
if columns[k] != "id" {
continue
}
field := mapper.FieldByName(m, columns[k])
val, ok := field.Interface().(uuid.UUID)
if !ok {
continue
}
if val == uuid.Nil && dbal.IsPostgresCompatible(dialect) {
pl[k] = "gen_random_uuid()"
break
}
}
placeholders = append(placeholders, fmt.Sprintf("(%s)", strings.Join(pl, ", ")))
}
return insertQueryArgs{
TableName: quoter.Quote(model.TableName()),
ColumnsDecl: strings.Join(quotedColumns, ", "),
Columns: columns,
Placeholders: strings.Join(placeholders, ",\n"),
}
}
func buildInsertQueryValues[T any](dialect string, mapper *reflectx.Mapper, columns []string, models []*T, nowFunc func() time.Time) (values []any, err error) {
for _, m := range models {
m := reflect.ValueOf(m)
now := nowFunc()
// Append model fields to args
for _, c := range columns {
field := mapper.FieldByName(m, c)
switch c {
case "created_at":
if pop.IsZeroOfUnderlyingType(field.Interface()) {
field.Set(reflect.ValueOf(now))
}
case "updated_at":
field.Set(reflect.ValueOf(now))
case "id":
if value, ok := field.Interface().(uuid.UUID); ok && value != uuid.Nil {
break // breaks switch, not for
} else if value, ok := field.Interface().(string); ok && len(value) > 0 {
break // breaks switch, not for
} else if dbal.IsPostgresCompatible(dialect) {
// This is a special case:
// 1. We're using a PostgreSQL-compatible database.
// 2. It's the primary key field ("ID")
// 3. A UUID was not yet set.
//
// If all these conditions meet, the VALUE statement will look as such:
//
// (gen_random_uuid(), ?, ?, ?, ...)
//
// For that reason, we do not add the ID value to the list of arguments,
// because one of the arguments is using a built-in and thus doesn't need a value.
continue // break switch, not for
}
id, err := uuid.NewV4()
if err != nil {
return nil, err
}
field.Set(reflect.ValueOf(id))
}
values = append(values, field.Interface())
// Special-handling for *sqlxx.NullTime: mapper.FieldByName sets this to a zero time.Time,
// but we want a nil pointer instead.
if i, ok := field.Interface().(*sqlxx.NullTime); ok {
if time.Time(*i).IsZero() {
field.Set(reflect.Zero(field.Type()))
}
}
}
}
return values, nil
}
type createOptions struct {
onConflict string
}
type option func(*createOptions)
func OnConflictDoNothing() func(*createOptions) {
return func(o *createOptions) {
o.onConflict = "ON CONFLICT DO NOTHING"
}
}
// CreateFromSlice is a helper around Create that accepts a slice of models
// instead of a slice of model pointers.
func CreateFromSlice[T any](ctx context.Context, p *TracerConnection, models []T, opts ...option) (err error) {
var ptrs []*T
for k := range models {
ptrs = append(ptrs, &models[k])
}
return Create(ctx, p, ptrs, opts...)
}
// Create batch-inserts the given models into the database using a single INSERT statement.
// The models are either all created or none.
func Create[T any](ctx context.Context, p *TracerConnection, models []*T, opts ...option) (err error) {
ctx, span := p.Tracer.Tracer().Start(ctx, "persistence.sql.batch.Create")
defer otelx.End(span, &err)
if len(models) == 0 {
return nil
}
options := &createOptions{}
for _, opt := range opts {
opt(options)
}
var v T
model := pop.NewModel(v, ctx)
conn := p.Connection
quoter, ok := conn.Dialect.(quoter)
if !ok {
return errors.Errorf("store is not a quoter: %T", conn.Store)
}
queryArgs := buildInsertQueryArgs(ctx, conn.Dialect.Name(), conn.TX.Mapper, quoter, models)
values, err := buildInsertQueryValues(conn.Dialect.Name(), conn.TX.Mapper, queryArgs.Columns, models, func() time.Time { return time.Now().UTC().Truncate(time.Microsecond) })
if err != nil {
return err
}
var returningClause string
if conn.Dialect.Name() != dbal.DriverMySQL {
// PostgreSQL, CockroachDB, YugabyteDB, and SQLite support RETURNING.
returningClause = fmt.Sprintf("RETURNING %s", model.IDField())
}
query := conn.Dialect.TranslateSQL(fmt.Sprintf(
"INSERT INTO %s (%s) VALUES\n%s\n%s\n%s",
queryArgs.TableName,
queryArgs.ColumnsDecl,
queryArgs.Placeholders,
options.onConflict,
returningClause,
))
rows, err := conn.TX.QueryContext(ctx, query, values...)
if err != nil {
return sqlcon.HandleError(err)
}
defer rows.Close()
// Hydrate the models from the RETURNING clause.
//
// Databases not supporting RETURNING will just return 0 rows.
count := 0
for rows.Next() {
if err := setModelID(rows, pop.NewModel(models[count], ctx)); err != nil {
return err
}
count++
}
if err := rows.Err(); err != nil {
return sqlcon.HandleError(err)
}
return sqlcon.HandleError(err)
}
// setModelID was copy & pasted from pop. It basically sets
// the primary key to the given value read from the SQL row.
func setModelID(row *sql.Rows, model *pop.Model) error {
el := reflect.ValueOf(model.Value).Elem()
fbn := el.FieldByName("ID")
if !fbn.IsValid() {
return errors.New("model does not have a field named id")
}
pkt, err := model.PrimaryKeyType()
if err != nil {
return errors.WithStack(err)
}
switch pkt {
case "UUID":
var id uuid.UUID
if err := row.Scan(&id); err != nil {
return errors.WithStack(err)
}
fbn.Set(reflect.ValueOf(id))
default:
var id interface{}
if err := row.Scan(&id); err != nil {
return errors.WithStack(err)
}
v := reflect.ValueOf(id)
switch fbn.Kind() {
case reflect.Int, reflect.Int64:
fbn.SetInt(v.Int())
default:
fbn.Set(reflect.ValueOf(id))
}
}
return nil
}

34
oryx/sqlxx/expand.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package sqlxx
// Expandable controls what fields to expand for projects.
type Expandable string
// Expandables is a list of Expandable values.
type Expandables []Expandable
// String returns a string representation of the Expandable.
func (e Expandable) String() string {
return string(e)
}
// ToEager returns the fields used by pop's Eager command.
func (e Expandables) ToEager() []string {
var s []string
for _, e := range e {
s = append(s, e.String())
}
return s
}
// Has returns true if the Expandable is in the list.
func (e Expandables) Has(search Expandable) bool {
for _, e := range e {
if e == search {
return true
}
}
return false
}

173
oryx/sqlxx/sqlxx.go Normal file
View File

@@ -0,0 +1,173 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package sqlxx
import (
"fmt"
"net/url"
"reflect"
"slices"
"strings"
"github.com/pkg/errors"
)
func keys(t any, exclude []string) []string {
tt := reflect.TypeOf(t)
if tt.Kind() == reflect.Pointer {
tt = tt.Elem()
}
ks := make([]string, 0, tt.NumField())
for i := range tt.NumField() {
f := tt.Field(i)
key, _, _ := strings.Cut(f.Tag.Get("db"), ",")
if key != "" && key != "-" && !slices.Contains(exclude, key) {
ks = append(ks, key)
}
}
return ks
}
// NamedInsertArguments returns columns and arguments for SQL INSERT statements based on a struct's tags. Does
// not work with nested structs or maps!
//
// type st struct {
// Foo string `db:"foo"`
// Bar string `db:"bar,omitempty"`
// Baz string `db:"-"`
// Zab string
// }
// columns, arguments := NamedInsertArguments(new(st))
// query := fmt.Sprintf("INSERT INTO foo (%s) VALUES (%s)", columns, arguments)
// // INSERT INTO foo (foo, bar) VALUES (:foo, :bar)
func NamedInsertArguments(t any, exclude ...string) (columns string, arguments string) {
keys := keys(t, exclude)
return strings.Join(keys, ", "),
":" + strings.Join(keys, ", :")
}
// NamedUpdateArguments returns columns and arguments for SQL UPDATE statements based on a struct's tags. Does
// not work with nested structs or maps!
//
// type st struct {
// Foo string `db:"foo"`
// Bar string `db:"bar,omitempty"`
// Baz string `db:"-"`
// Zab string
// }
// query := fmt.Sprintf("UPDATE foo SET %s", NamedUpdateArguments(new(st)))
// // UPDATE foo SET foo=:foo, bar=:bar
func NamedUpdateArguments(t any, exclude ...string) string {
keys := keys(t, exclude)
statements := make([]string, len(keys))
for k, key := range keys {
statements[k] = fmt.Sprintf("%s=:%s", key, key)
}
return strings.Join(statements, ", ")
}
func OnConflictDoNothing(dialect string, columnNoop string) string {
if dialect == "mysql" {
return fmt.Sprintf(" ON DUPLICATE KEY UPDATE `%s` = `%s` ", columnNoop, columnNoop)
} else {
return ` ON CONFLICT DO NOTHING `
}
}
// ExtractSchemeFromDSN returns the scheme (e.g. `mysql`, `postgres`, etc) component in a DSN string,
// as well as the remaining part of the DSN after the scheme separator.
// It is an error to not have a scheme present.
// This makes sense in the context of a DSN to be able to identify which database is in use.
func ExtractSchemeFromDSN(dsn string) (string, string, error) {
scheme, afterSchemeSeparator, schemeSeparatorFound := strings.Cut(dsn, "://")
if !schemeSeparatorFound {
return "", "", errors.New("invalid DSN: missing scheme separator")
}
if scheme == "" {
return "", "", errors.New("invalid DSN: empty scheme")
}
return scheme, afterSchemeSeparator, nil
}
// ExtractDbNameFromDSN returns the database name component in a DSN string.
func ExtractDbNameFromDSN(dsn string) (string, error) {
_, afterScheme, err := ExtractSchemeFromDSN(dsn)
if err != nil {
return "", err
}
_, afterSlash, slashFound := strings.Cut(afterScheme, "/")
if !slashFound {
return "", nil
}
dbName, _, _ := strings.Cut(afterSlash, "?")
return dbName, nil
}
// ReplaceSchemeInDSN replaces the scheme (e.g. `mysql`, `postgres`, etc) in a DSN string with another one.
// This is necessary for example when using `cockroach` as a scheme, but using the postgres driver to connect to the database,
// and this driver only accepts `postgres` as a scheme.
func ReplaceSchemeInDSN(dsn string, newScheme string) (string, error) {
_, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn)
if err != nil {
return "", errors.WithStack(err)
}
return newScheme + "://" + afterSchemeSeparator, nil
}
// DSNRedacted parses a database DSN and returns a redacted form as a string.
// It replaces any password with "xxxxx" just like `url.Redacted()`.
// Only the password is redacted, not the username.
// This function is necessary because MySQL uses a DSN format not compatible with `url.Parse`.
// Additionally and as a consequence of the point above, the scheme is expected to be present and non-empty.
// This function is less strict that `url.Parse` in the case of MySQL.
// It also does not escape any characters in the username, whereas `url.String()`/`url.Redacted` does.
func DSNRedacted(dsn string) (string, error) {
scheme, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn)
if err != nil {
return "", errors.WithStack(err)
}
// If this is not MySQL, we simply delegate the work to `url.Parse`.
if scheme != "mysql" {
u, err := url.Parse(dsn)
if err != nil {
return "", errors.WithStack(err)
}
return u.Redacted(), nil
}
// MySQL has a weird DSN syntax not conforming to a standard URL, of the form:
// `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]`
// We only need to parse up to `@` in order to redact the password. The rest is left as-is.
usernamePassword, afterUsernamePassword, usernamePasswordSeparatorFound := strings.Cut(afterSchemeSeparator, "@")
if !usernamePasswordSeparatorFound {
afterUsernamePassword = afterSchemeSeparator
}
username, password, hasPassword := strings.Cut(usernamePassword, ":")
// We only insert a redacted password in the final result if a password was provided in the input.
// This behavior matches the one of `url.Redacted()`.
if hasPassword {
password = ":xxxxx"
}
res := scheme + "://"
if usernamePasswordSeparatorFound {
res += username + password + "@"
}
res += afterUsernamePassword
return res, nil
}
func StripQueryParamsFromDSN(dsn string) string {
before, _, _ := strings.Cut(dsn, "?")
return before
}

632
oryx/sqlxx/types.go Normal file
View File

@@ -0,0 +1,632 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package sqlxx
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"slices"
"strings"
"time"
"github.com/tidwall/gjson"
"github.com/pkg/errors"
)
// Duration represents a JSON and SQL compatible time.Duration.
// swagger:type string
type Duration time.Duration
// MarshalJSON returns m as the JSON encoding of m.
func (ns Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(ns).String())
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *Duration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
p, err := time.ParseDuration(s)
if err != nil {
return err
}
*ns = Duration(p)
return nil
}
// StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.
// swagger:type array
type StringSliceJSONFormat []string
// Scan implements the Scanner interface.
func (m *StringSliceJSONFormat) Scan(value interface{}) error {
var val string
switch v := value.(type) {
case nil:
*m = StringSliceJSONFormat{}
return nil
case string:
val = v
case []byte:
val = string(v)
default:
return errors.Errorf("cannot scan %#v into StringSliceJSONFormat", value)
}
if len(val) == 0 {
val = "[]"
}
if parsed := gjson.Parse(val); parsed.Type == gjson.Null {
val = "[]"
} else if !parsed.IsArray() {
return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String())
}
return errors.WithStack(json.Unmarshal([]byte(val), &m))
}
// Value implements the driver Valuer interface.
func (m StringSliceJSONFormat) Value() (driver.Value, error) {
if len(m) == 0 {
return "[]", nil
}
encoded, err := json.Marshal(&m)
return string(encoded), errors.WithStack(err)
}
// StringSlicePipeDelimiter de/encodes the string slice to/from a SQL string.
type StringSlicePipeDelimiter []string
// Scan implements the Scanner interface.
func (n *StringSlicePipeDelimiter) Scan(value interface{}) error {
var s sql.NullString
if err := s.Scan(value); err != nil {
return err
}
*n = scanStringSlice('|', s.String)
return nil
}
// Value implements the driver Valuer interface.
func (n StringSlicePipeDelimiter) Value() (driver.Value, error) {
return valueStringSlice('|', n), nil
}
func scanStringSlice(delimiter rune, value interface{}) []string {
escaped := false
s := fmt.Sprintf("%s", value)
splitted := strings.FieldsFunc(s, func(r rune) bool {
if r == '\\' {
escaped = !escaped
} else if escaped && r != delimiter {
escaped = false
}
return !escaped && r == delimiter
})
for k, v := range splitted {
splitted[k] = strings.ReplaceAll(v, "\\"+string(delimiter), string(delimiter))
}
return splitted
}
func valueStringSlice(delimiter rune, value []string) string {
replace := make([]string, len(value))
for k, v := range value {
replace[k] = strings.ReplaceAll(v, string(delimiter), "\\"+string(delimiter))
}
return strings.Join(replace, string(delimiter))
}
// NullBool represents a bool that may be null.
// NullBool implements the Scanner interface so
// swagger:type bool
// swagger:model nullBool
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullBool) Scan(value interface{}) error {
d := sql.NullBool{}
if err := d.Scan(value); err != nil {
return err
}
ns.Bool = d.Bool
ns.Valid = d.Valid
return nil
}
// Value implements the driver Valuer interface.
func (ns NullBool) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.Bool, nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (ns NullBool) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.Bool)
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *NullBool) UnmarshalJSON(data []byte) error {
if ns == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
if len(data) == 0 || string(data) == "null" {
return nil
}
ns.Valid = true
return errors.WithStack(json.Unmarshal(data, &ns.Bool))
}
// FalsyNullBool represents a bool that may be null.
// It JSON decodes to false if null.
//
// swagger:type bool
// swagger:model falsyNullBool
type FalsyNullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
// Scan implements the Scanner interface.
func (ns *FalsyNullBool) Scan(value interface{}) error {
d := sql.NullBool{}
if err := d.Scan(value); err != nil {
return err
}
ns.Bool = d.Bool
ns.Valid = d.Valid
return nil
}
// Value implements the driver Valuer interface.
func (ns FalsyNullBool) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.Bool, nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (ns FalsyNullBool) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("false"), nil
}
return json.Marshal(ns.Bool)
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *FalsyNullBool) UnmarshalJSON(data []byte) error {
if ns == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
if len(data) == 0 || string(data) == "null" {
return nil
}
ns.Valid = true
return errors.WithStack(json.Unmarshal(data, &ns.Bool))
}
// swagger:type string
// swagger:model nullString
type NullString string
// MarshalJSON returns m as the JSON encoding of m.
func (ns NullString) MarshalJSON() ([]byte, error) {
return json.Marshal(string(ns))
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *NullString) UnmarshalJSON(data []byte) error {
if ns == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
if len(data) == 0 {
return nil
}
return errors.WithStack(json.Unmarshal(data, (*string)(ns)))
}
// Scan implements the Scanner interface.
func (ns *NullString) Scan(value interface{}) error {
var v sql.NullString
if err := (&v).Scan(value); err != nil {
return err
}
*ns = NullString(v.String)
return nil
}
// Value implements the driver Valuer interface.
func (ns NullString) Value() (driver.Value, error) {
if len(ns) == 0 {
return sql.NullString{}.Value()
}
return sql.NullString{Valid: true, String: string(ns)}.Value()
}
// String implements the Stringer interface.
func (ns NullString) String() string {
return string(ns)
}
// NullTime implements sql.NullTime functionality.
//
// swagger:model nullTime
// required: false
type NullTime time.Time
// Scan implements the Scanner interface.
func (ns *NullTime) Scan(value interface{}) error {
var v sql.NullTime
if err := (&v).Scan(value); err != nil {
return err
}
*ns = NullTime(v.Time.UTC())
return nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (ns NullTime) MarshalJSON() ([]byte, error) {
var t *time.Time
if !time.Time(ns).IsZero() {
tt := time.Time(ns)
t = &tt
}
return json.Marshal(t)
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *NullTime) UnmarshalJSON(data []byte) error {
var t time.Time
if err := json.Unmarshal(data, &t); err != nil {
return err
}
*ns = NullTime(t)
return nil
}
// Value implements the driver Valuer interface.
func (ns NullTime) Value() (driver.Value, error) {
return sql.NullTime{Valid: !time.Time(ns).IsZero(), Time: time.Time(ns).UTC()}.Value()
}
// DeepCopy implements github.com/mohae/deepcopy.Interface.
//
// NullTime wraps time.Time, whose fields are unexported. The default
// reflection-based deep copy in mohae/deepcopy skips unexported fields, which
// would zero out the timestamp. Returning a pointer to a copy preserves the
// value. The method uses a pointer receiver so that mohae/deepcopy returns
// the matching *NullTime type when it finds one as a struct field.
func (ns *NullTime) DeepCopy() any {
if ns == nil {
return (*NullTime)(nil)
}
cpy := *ns
return &cpy
}
// MapStringInterface represents a map[string]interface that works well with JSON, SQL, and Swagger.
type MapStringInterface map[string]interface{}
// Scan implements the Scanner interface.
func (n *MapStringInterface) Scan(value interface{}) error {
v := fmt.Sprintf("%s", value)
if len(v) == 0 {
return nil
}
return errors.WithStack(json.Unmarshal([]byte(v), n))
}
// Value implements the driver Valuer interface.
func (n MapStringInterface) Value() (driver.Value, error) {
value, err := json.Marshal(n)
if err != nil {
return nil, errors.WithStack(err)
}
return string(value), nil
}
// JSONArrayRawMessage represents a json.RawMessage which only accepts arrays that works well with JSON, SQL, and Swagger.
type JSONArrayRawMessage json.RawMessage
// Scan implements the Scanner interface.
func (m *JSONArrayRawMessage) Scan(value interface{}) error {
val := fmt.Sprintf("%s", value)
if len(val) == 0 {
val = "[]"
}
if parsed := gjson.Parse(val); parsed.Type == gjson.Null {
val = "[]"
} else if !parsed.IsArray() {
return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String())
}
*m = []byte(val)
return nil
}
// Value implements the driver Valuer interface.
func (m JSONArrayRawMessage) Value() (driver.Value, error) {
if len(m) == 0 {
return "[]", nil
}
if parsed := gjson.ParseBytes(m); parsed.Type == gjson.Null {
return "[]", nil
} else if !parsed.IsArray() {
return nil, errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String())
}
return string(m), nil
}
// JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger.
type JSONRawMessage json.RawMessage
// Scan implements the Scanner interface.
func (m *JSONRawMessage) Scan(value interface{}) error {
switch v := value.(type) {
case []byte:
*m = slices.Clone(v)
case string:
*m = JSONRawMessage(v)
case nil:
*m = JSONRawMessage("null")
default:
return errors.Errorf("cannot scan %T into JSONRawMessage", value)
}
return nil
}
// Value implements the driver Valuer interface.
func (m JSONRawMessage) Value() (driver.Value, error) {
if len(m) == 0 {
return "null", nil
}
return string(m), nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (m JSONRawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *JSONRawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}
// NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable-
//
// swagger:model nullJsonRawMessage
type NullJSONRawMessage json.RawMessage
// Scan implements the Scanner interface.
func (m *NullJSONRawMessage) Scan(value any) error {
return (*JSONRawMessage)(m).Scan(value)
}
// Value implements the driver Valuer interface.
func (m NullJSONRawMessage) Value() (driver.Value, error) {
if len(m) == 0 || string(m) == "null" {
return nil, nil
}
return string(m), nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (m NullJSONRawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *NullJSONRawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*m = append((*m)[0:0], data...)
return nil
}
// NullJSONObject represents a nullable JSON object (always key-value pairs).
// Use this for fields that are always objects (e.g., HTTP headers).
// For arbitrary JSON that can be strings, arrays, or objects, use NullJSONRawMessage.
//
// swagger:type object
type NullJSONObject json.RawMessage
// Scan implements the Scanner interface.
func (m *NullJSONObject) Scan(value any) error {
return (*NullJSONRawMessage)(m).Scan(value)
}
// Value implements the driver Valuer interface.
func (m NullJSONObject) Value() (driver.Value, error) {
return NullJSONRawMessage(m).Value()
}
// MarshalJSON returns m as the JSON encoding of m.
func (m NullJSONObject) MarshalJSON() ([]byte, error) {
return NullJSONRawMessage(m).MarshalJSON()
}
// UnmarshalJSON sets *m to a copy of data.
func (m *NullJSONObject) UnmarshalJSON(data []byte) error {
return (*NullJSONRawMessage)(m).UnmarshalJSON(data)
}
// JSONScan is a generic helper for retrieving a SQL JSON-encoded value.
func JSONScan(dst, value any) error {
// Note: raw is a string (not []byte) because the MySQL driver reuses byte slices across scans.
// Using strings avoids the need to manually copy the byte slice.
var raw string
switch v := value.(type) {
case nil:
raw = "null"
case string:
raw = v
case []byte:
raw = string(v)
default:
return fmt.Errorf("unable to scan type %T as JSON into %T", value, dst)
}
if err := json.Unmarshal([]byte(raw), dst); err != nil {
return fmt.Errorf("unable to decode JSON payload into %T: %w", dst, err)
}
return nil
}
// NullInt64 represents an int64 that may be null.
// swagger:type int64
// swagger:model nullInt64
type NullInt64 struct {
Int int64
Valid bool // Valid is true if Duration is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullInt64) Scan(value interface{}) error {
d := sql.NullInt64{}
if err := d.Scan(value); err != nil {
return err
}
ns.Int = d.Int64
ns.Valid = d.Valid
return nil
}
// Value implements the driver Valuer interface.
func (ns NullInt64) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.Int, nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (ns NullInt64) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.Int)
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *NullInt64) UnmarshalJSON(data []byte) error {
if ns == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
if len(data) == 0 || string(data) == "null" {
return nil
}
ns.Valid = true
return errors.WithStack(json.Unmarshal(data, &ns.Int))
}
// NullDuration represents a nullable JSON and SQL compatible time.Duration.
//
// swagger:type string
// swagger:model nullDuration
type NullDuration struct {
Duration time.Duration
Valid bool
}
// Scan implements the Scanner interface.
func (ns *NullDuration) Scan(value interface{}) error {
d := sql.NullInt64{}
if err := d.Scan(value); err != nil {
return err
}
ns.Duration = time.Duration(d.Int64)
ns.Valid = d.Valid
return nil
}
// Value implements the driver Valuer interface.
func (ns NullDuration) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return int64(ns.Duration), nil
}
// MarshalJSON returns m as the JSON encoding of m.
func (ns NullDuration) MarshalJSON() ([]byte, error) {
if !ns.Valid {
return []byte("null"), nil
}
return json.Marshal(ns.Duration.String())
}
// UnmarshalJSON sets *m to a copy of data.
func (ns *NullDuration) UnmarshalJSON(data []byte) error {
if ns == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
if len(data) == 0 || string(data) == "null" {
return nil
}
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
p, err := time.ParseDuration(s)
if err != nil {
return err
}
ns.Duration = p
ns.Valid = true
return nil
}
func (ns Duration) IsZero() bool { return ns == 0 }
func (m StringSliceJSONFormat) IsZero() bool { return len(m) == 0 }
func (n StringSlicePipeDelimiter) IsZero() bool { return len(n) == 0 }
func (ns NullBool) IsZero() bool { return !ns.Valid || !ns.Bool }
func (ns FalsyNullBool) IsZero() bool { return !ns.Valid || !ns.Bool }
func (ns NullString) IsZero() bool { return len(ns) == 0 }
func (ns NullTime) IsZero() bool { return time.Time(ns).IsZero() }
func (n MapStringInterface) IsZero() bool { return len(n) == 0 }
func (m JSONArrayRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "[]" }
func (m JSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" }
func (m NullJSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" }
func (ns NullInt64) IsZero() bool { return !ns.Valid || ns.Int == 0 }
func (ns NullDuration) IsZero() bool { return !ns.Valid || ns.Duration == 0 }