Files
zitadel/backend/v3/storage/database/operators.go
T
Livio SpringandGitHub 4e6f68c511 feat(relational): implement session repository (#11109)
# Which Problems Are Solved

As part of going to a relation storage model, we need to implement the
repository for handling sessions and their events.

# How the Problems Are Solved

- Adds a migration step to create the necessary tables and types in the
database.
- Adds the repository implementation for sessions.
- Implements a projection handler for session events to handle the
events written by the old command handlers.
- Updates the `Session` type and `SessionRepository` interface:
  - added `CreatorID`
  - Adjusted `Delete` method to return amount of affected columns
  - Removed sub-repository clumns from the main interface
- Added `CreatorIDCondition`, `ExpirationCondition`, `ExistsFactor` and
`ExistsMetadata`
  - Removed `SetUserAgent`
- Methods of `SessionFactor` and `SessionChallenge` have been made
public
- fixed enumer on `SessionFactorType`
- Adds a `JSON` type to the repository package to easy can JSON columns,
resp. jsonb object inside a property of a `JSONArray` object.
- Adds a `NoChange` interface, which can be implemented to signal that a
`Change` interface implementation is not writing any changes to the
statement builder, e.g. `CTEChange`

# Additional Changes

- Fixes the operators enums. `TextOperations` had wrong comments and
`NumberOperations` additionally also had from comparisons.
- Fixes downgrade of migration step 008.

# Additional Context

- closes https://github.com/zitadel/zitadel/issues/10212
2026-01-06 08:08:11 +01:00

187 lines
5.9 KiB
Go

package database
import (
"fmt"
"time"
"golang.org/x/exp/constraints"
)
type wrappedValue[V Value] struct {
value V
fn function
}
func LowerValue[T Value](v T) wrappedValue[T] {
return wrappedValue[T]{value: v, fn: functionLower}
}
func SHA256Value[T Value](v T) wrappedValue[T] {
return wrappedValue[T]{value: v, fn: functionSHA256}
}
func (b wrappedValue[V]) WriteArg(builder *StatementBuilder) {
builder.Grow(len(b.fn) + 5)
builder.WriteString(string(b.fn))
builder.WriteRune('(')
builder.WriteArg(b.value)
builder.WriteRune(')')
}
var _ argWriter = (*wrappedValue[string])(nil)
type Value interface {
Boolean | Number | Text | Instruction | Bytes
}
//go:generate enumer -type NumberOperation,TextOperation,BytesOperation -linecomment -output ./operators_enumer.go
type Operation interface {
NumberOperation | TextOperation | BytesOperation
}
type Text interface {
~string | Bytes
}
// TextOperation are operations that can be performed on text values.
type TextOperation uint8
const (
// TextOperationEqual compares two strings for equality.
TextOperationEqual TextOperation = iota + 1 // =
// TextOperationNotEqual compares two strings for inequality.
TextOperationNotEqual // <>
// TextOperationStartsWith checks if the first string starts with the second.
TextOperationStartsWith // LIKE
// TextOperationContains checks if the first string contains the second
TextOperationContains // LIKE
// TextOperationEndsWith checks if the first string ends with the second.
TextOperationEndsWith // LIKE
// TextOperationContainsIgnoreCase checks if the first string contains the second ignoring case
TextOperationContainsIgnoreCase // LIKE
// TextOperationEndsWithIgnoreCase checks if the first string ends with the second ignoring case
TextOperationEndsWithIgnoreCase // LIKE
// TextOperationStartsWithIgnoreCase checks if the first string starts with the second ignoring case
TextOperationStartsWithIgnoreCase // LIKE
// TextOperationEqualIgnoreCase compares two strings for equality ignoring case
TextOperationEqualIgnoreCase // =
// TextOperationNotEqualIgnoreCase compares two strings for inequality ignoring case
TextOperationNotEqualIgnoreCase // <>
)
func (op TextOperation) IsIgnoreCaseOperation() bool {
return op == TextOperationContainsIgnoreCase ||
op == TextOperationEndsWithIgnoreCase ||
op == TextOperationStartsWithIgnoreCase ||
op == TextOperationEqualIgnoreCase ||
op == TextOperationNotEqualIgnoreCase
}
func (op TextOperation) Value() TextOperation {
switch op {
case TextOperationStartsWith, TextOperationContains, TextOperationEndsWith, TextOperationEqual, TextOperationNotEqual:
return op
case TextOperationContainsIgnoreCase:
return TextOperationContains
case TextOperationEndsWithIgnoreCase:
return TextOperationEndsWith
case TextOperationStartsWithIgnoreCase:
return TextOperationStartsWith
case TextOperationEqualIgnoreCase:
return TextOperationEqual
case TextOperationNotEqualIgnoreCase:
return TextOperationNotEqual
default:
panic(fmt.Sprintf("unexpected database.TextOperation: %#v", op))
}
}
func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOperation, value any) {
switch op {
case TextOperationContains:
writeOperation[T](builder, col, op.String()+" '%' ||", value)
builder.WriteString(" || '%'")
case TextOperationEndsWith:
writeOperation[T](builder, col, op.String()+" '%' ||", value)
case TextOperationEqual, TextOperationNotEqual:
writeOperation[T](builder, col, op.String(), value)
case TextOperationStartsWith:
writeOperation[T](builder, col, op.String(), value)
builder.WriteString(" || '%'")
case TextOperationContainsIgnoreCase, TextOperationEndsWithIgnoreCase, TextOperationStartsWithIgnoreCase, TextOperationEqualIgnoreCase, TextOperationNotEqualIgnoreCase:
fallthrough
default:
panic(fmt.Sprintf("unexpected database.TextOperation: %#v", op))
}
}
type Number interface {
constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration
}
// NumberOperation are operations that can be performed on number values.
type NumberOperation uint8
const (
// NumberOperationEqual compares two numbers for equality.
NumberOperationEqual NumberOperation = iota + 1 // =
// NumberOperationNotEqual compares two numbers for inequality.
NumberOperationNotEqual // <>
// NumberOperationLessThan compares two numbers to check if the first is less than the second.
NumberOperationLessThan // <
// NumberOperationLessThanOrEqual compares two numbers to check if the first is less than or equal to the second.
NumberOperationLessThanOrEqual // <=
// NumberOperationGreaterThan compares two numbers to check if the first is greater than the second.
NumberOperationGreaterThan // >
// NumberOperationGreaterThanOrEqual compares two numbers to check if the first is greater than or equal to the second.
NumberOperationGreaterThanOrEqual // >=
)
func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value any) {
writeOperation[T](builder, col, op.String(), value)
}
type Boolean interface {
~bool
}
func writeBooleanOperation[T Boolean](builder *StatementBuilder, col Column, value any) {
writeOperation[T](builder, col, "=", value)
}
type Bytes interface {
~[]byte
}
// BytesOperation are operations that can be performed on bytea values.
type BytesOperation uint8
const (
BytesOperationEqual BytesOperation = iota + 1 // =
BytesOperationNotEqual // <>
)
func writeBytesOperation[T Bytes](builder *StatementBuilder, col Column, op BytesOperation, value any) {
writeOperation[T](builder, col, op.String(), value)
}
func writeOperation[V Value](builder *StatementBuilder, col Column, op string, value any) {
if op == "" {
panic("unsupported operation")
}
switch value.(type) {
case V, wrappedValue[V], *wrappedValue[V]:
default:
panic("unsupported value type")
}
col.WriteQualified(builder)
builder.WriteRune(' ')
builder.WriteString(op)
builder.WriteRune(' ')
builder.WriteArg(value)
}