Help us shape the future of Go! Take the 2026 Go Developer Survey

patch

package
v0.6.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package patch provides the two wrapper types an update input is made of.

Everything else rig generates is a plain Go type — string, time.Time, *string for a nullable column. Update input is the exception, because a PATCH has to distinguish things a plain value cannot: a field the caller left out from one they sent, and, where the column allows it, a field they explicitly cleared.

There are two types rather than one because a column either can be null or it cannot, and the type should say which:

  • Optional is absent or set. It is what a NOT NULL column gets, and there is no way to write null into one.
  • Nullable is absent, null, or set. It is what a nullable column gets.

The alternative — one three-state type everywhere — makes clearing a NOT NULL column something you can write down and have rejected at runtime. This way the compiler rejects it, and the only null that has to be caught is one that arrives over the wire.

The zero value of each is absent, which is what makes them work with encoding/json without any cooperation from the surrounding struct: UnmarshalJSON is only called for keys that are present, so a field the client omitted is never touched.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ErrNullNotAllowed

type ErrNullNotAllowed struct{}

ErrNullNotAllowed is returned when a client sends null for a column that cannot hold one.

It is a distinct type so the decoding layer can turn it into a 400 that names the field, rather than the generic complaint encoding/json would produce.

func (ErrNullNotAllowed) Error

func (ErrNullNotAllowed) Error() string

Error implements error.

type Nullable

type Nullable[T any] struct {
	// contains filtered or unexported fields
}

Nullable is a value that may be left out, cleared, or set.

Three states, matching a nullable column exactly.

Example

A generated update input is a struct of these two types: Optional for a NOT NULL column, Nullable for one that allows null. Decoding a PATCH body into it is the whole point — a field the caller omitted is never touched, so it stays absent, while one they sent as null arrives as null.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/simonjanss/rig/runtime/patch"
)

func main() {
	// What a generated update input for a `todo` table looks like.
	var in struct {
		Title    patch.Optional[string] `json:"title"`
		Notes    patch.Nullable[string] `json:"notes"`
		Priority patch.Nullable[int]    `json:"priority"`
	}

	// The caller renamed the todo, cleared its notes, and said nothing at all
	// about its priority.
	body := `{"title": "Buy milk", "notes": null}`
	if err := json.Unmarshal([]byte(body), &in); err != nil {
		panic(err)
	}

	fmt.Println("title:   ", in.Title, in.Title.IsSet())
	fmt.Println("notes:   ", in.Notes, in.Notes.IsNull())
	fmt.Println("priority:", in.Priority, in.Priority.Touched())

}
Output:
title:    Buy milk true
notes:    null true
priority: absent false
Example (States)

Absent and null are different answers, and only one of them is a change. A repository asks Touched before it writes a column at all, and IsNull to decide whether it writes a value or a NULL.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/patch"
)

func main() {
	set := patch.NewNullable("weekly")
	cleared := patch.Null[string]()
	untouched := patch.Unspecified[string]()

	for _, n := range []patch.Nullable[string]{set, cleared, untouched} {
		fmt.Printf("%-8s touched=%-5t null=%t\n", n, n.Touched(), n.IsNull())
	}

}
Output:
weekly   touched=true  null=false
null     touched=true  null=true
absent   touched=false null=false

func FromPtr

func FromPtr[T any](v *T) Nullable[T]

FromPtr builds a Nullable from a pointer: nil clears, anything else sets.

It is how a previous row's value — which the model holds as *T — becomes an input, which is what merging a partial update against the current state needs.

Example

FromPtr is how the current row — which a generated model holds as *T — becomes an input, so a partial update can be merged against the state it is patching.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/patch"
)

func main() {
	current := "the previous note"

	fmt.Println(patch.FromPtr(&current))
	fmt.Println(patch.FromPtr[string](nil))

}
Output:
the previous note
null

func FromSlice

func FromSlice[T any](v []T) Nullable[[]T]

FromSlice builds a Nullable from a slice: nil clears, anything else sets.

The array counterpart of FromPtr. A nullable array column is held as a plain slice rather than a pointer to one — nil already means "no value" — so the pointer form has nothing to take the address of.

An empty non-nil slice is a value: a column set to {} is not a column set to NULL, and a replay that confused the two would quietly change the row.

func NewNullable

func NewNullable[T any](v T) Nullable[T]

NewNullable builds a Nullable carrying a value.

func Null

func Null[T any]() Nullable[T]

Null builds a Nullable that clears the field.

func Unspecified

func Unspecified[T any]() Nullable[T]

Unspecified builds a Nullable that leaves the field alone. It is the zero value, and exists so the intent can be written down.

func (Nullable[T]) Get

func (n Nullable[T]) Get() (T, bool)

Get returns the value, and whether there is one. It reports false for both absent and null; use Nullable.IsNull when the difference matters.

func (Nullable[T]) IsAbsent

func (n Nullable[T]) IsAbsent() bool

IsAbsent reports whether the field was left out entirely.

func (Nullable[T]) IsNull

func (n Nullable[T]) IsNull() bool

IsNull reports whether the field was explicitly cleared.

func (Nullable[T]) IsSet

func (n Nullable[T]) IsSet() bool

IsSet reports whether the field carries a value.

func (Nullable[T]) IsZero

func (n Nullable[T]) IsZero() bool

IsZero implements the check encoding/json's `omitzero` tag makes, so that a struct field holding an absent value is left out of the encoded object entirely.

An explicit null is not zero, and that is the whole distinction: a client that cleared a column has to put null on the wire, and one that said nothing about it has to put nothing. See Optional.IsZero.

func (Nullable[T]) MarshalJSON

func (n Nullable[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Nullable[T]) Or

func (n Nullable[T]) Or(fallback T) T

Or returns the value, or the fallback when absent or null.

func (Nullable[T]) Ptr

func (n Nullable[T]) Ptr() *T

Ptr returns a pointer to the value, or nil when absent or null.

It is what a repository passes to the driver: a nil pointer is exactly how a NULL is written, so no branch is needed at the call site.

func (Nullable[T]) String

func (n Nullable[T]) String() string

String renders the value for logs and test failures.

func (Nullable[T]) Touched

func (n Nullable[T]) Touched() bool

Touched reports whether the field was mentioned at all, whether it was set to a value or cleared. It is the question a repository asks when deciding which columns belong in an UPDATE.

func (*Nullable[T]) UnmarshalJSON

func (n *Nullable[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

type Optional

type Optional[T any] struct {
	// contains filtered or unexported fields
}

Optional is a value that may be left out of an update.

Two states: absent, or set to a value. A column that is NOT NULL cannot be cleared, so there is nothing here that could express clearing it.

Example

Optional has no third state, which is what makes clearing a NOT NULL column something that cannot be written down rather than something rejected at runtime.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/patch"
)

func main() {
	title := patch.NewOptional("Buy milk")
	if v, ok := title.Get(); ok {
		fmt.Println("set to:", v)
	}
	fmt.Println("left alone:", patch.Absent[string]().IsAbsent())

}
Output:
set to: Buy milk
left alone: true

func Absent

func Absent[T any]() Optional[T]

Absent builds an Optional that leaves the field alone. It is the zero value, and exists so the intent can be written down.

func NewOptional

func NewOptional[T any](v T) Optional[T]

NewOptional builds an Optional carrying a value.

func (Optional[T]) Get

func (o Optional[T]) Get() (T, bool)

Get returns the value, and whether one was given.

func (Optional[T]) IsAbsent

func (o Optional[T]) IsAbsent() bool

IsAbsent reports whether the field was left out.

func (Optional[T]) IsSet

func (o Optional[T]) IsSet() bool

IsSet reports whether the field carries a value.

func (Optional[T]) IsZero

func (o Optional[T]) IsZero() bool

IsZero implements the check encoding/json's `omitzero` tag makes, so that a struct field holding an absent value is left out of the encoded object entirely.

That matters for anything that sends an update rather than receiving one — the generated Go client, or a test building a request by hand. MarshalJSON below has to encode absence as null, because a marshaler cannot remove its own field; `omitzero` is the only thing that can, and this is what it asks.

func (Optional[T]) MarshalJSON

func (o Optional[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

An absent value still encodes as null, because a struct field cannot be omitted from the outside. Generated responses use plain values, so this only matters when a request is echoed back.

func (Optional[T]) Or

func (o Optional[T]) Or(fallback T) T

Or returns the value, or the fallback when absent.

func (Optional[T]) String

func (o Optional[T]) String() string

String renders the value for logs and test failures.

func (*Optional[T]) UnmarshalJSON

func (o *Optional[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

An explicit null is an error rather than a silent absence. The two mean different things — "leave this alone" and "clear this" — and a column that cannot be cleared should say so rather than quietly ignoring the request.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL