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
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:
@@ -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(?, ?, ?, ?, ?, ?, ?)"
|
||||
}
|
||||
@@ -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(?, ?, ?, ?, ?, ?, ?)"
|
||||
}
|
||||
@@ -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
298
oryx/sqlxx/batch/create.go
Normal 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
34
oryx/sqlxx/expand.go
Normal 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
173
oryx/sqlxx/sqlxx.go
Normal 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&...¶mN=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
632
oryx/sqlxx/types.go
Normal 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 }
|
||||
Reference in New Issue
Block a user