fix(eventstore): Make Eventstore Compatible with Relational Table Package (#10687)

Improves compatibility of eventstore and related database components
with the new relational table package.

## Which problems are solved

1. **Incompatible Database Interfaces**: The existing eventstore was
tightly coupled to the database package, which is incompatible with the
new, more abstract relational table package in v3. This prevented the
new command-side logic from pushing events to the legacy eventstore.
2. **Missing Health Checks**: The database interfaces in the new package
lacked a Ping method, making it impossible to perform health checks on
database connections.
3. **Event Publishing Logic**: The command handling logic in domain
needed a way to collect and push events to the legacy eventstore after a
command was successfully executed.

## How the problems are solved

1. **`LegacyEventstore` Interface**:
* A new `LegacyEventstore` interface is introduced in the new
`database/eventstore` . This interface exposes a `PushWithNewClient`
method that accepts the new `database.QueryExecutor` interface,
decoupling the v3 domain from the legacy implementation.
* The `internal/eventstore.Eventstore` now implements this interface. A
wrapper, PushWithClient, is added to convert the old database client
types (`*sql.DB`, `*sql.Tx`) into the new `QueryExecutor` types before
calling `PushWithNewClient`.
2. **Database Interface Updates**:
* The `database.Pool` and `database.Client` interfaces in
`storage/eventstore` have been updated to include a Ping method,
allowing for consistent health checks across different database
dialects.
* The `postgres` and `sql` dialect implementations have been updated to
support this new method.
3. **Command and Invoker Refactoring**:
* The `Commander` interface in domain now includes an `Events()
[]legacy_es.Command` method. This allows commands to declare which
events they will generate.
* The `eventCollector` in the invoker logic has been redesigned. It now
ensures a database transaction is started before executing a command.
After successful execution, it calls the `Events()` method on the
command to collect the generated events and appends them to a list.
* The `eventStoreInvoker` then pushes all collected events to the legacy
eventstore using the new `LegacyEventstore` interface, ensuring that
events are only pushed if the entire command (and any sub-commands)
executes successfully within the transaction.
4. **Testing**:
* New unit tests have been added for the invoker to verify that events
are correctly collected from single commands, batched commands, and
nested commands.

These changes create a clean bridge between the new v3 command-side
logic and the existing v1 eventstore, allowing for incremental adoption
of the new architecture while maintaining full functionality.

## Additional Information

closes https://github.com/zitadel/zitadel/issues/10442
This commit is contained in:
Silvan
2025-09-16 18:58:49 +02:00
committed by GitHub
parent 41d04ffa65
commit 22ef817d5c
28 changed files with 705 additions and 644 deletions
+1 -2
View File
@@ -17,7 +17,6 @@ on:
env: env:
cache_path: | cache_path: |
backend
internal/statik/statik.go internal/statik/statik.go
internal/notification/statik/statik.go internal/notification/statik/statik.go
internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css* internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css*
@@ -44,7 +43,7 @@ jobs:
continue-on-error: true continue-on-error: true
id: cache id: cache
with: with:
key: core-${{ hashFiles( 'go.*', 'openapi', 'cmd', 'pkg/grpc/**/*.go', 'proto', 'internal', 'backend/**') }} key: core-${{ hashFiles( 'go.*', 'openapi', 'cmd', 'pkg/grpc/**/*.go', 'proto', 'internal', 'backend') }}
restore-keys: | restore-keys: |
core- core-
path: ${{ env.cache_path }} path: ${{ env.cache_path }}
+126 -111
View File
@@ -1,131 +1,146 @@
package domain package domain
// import ( import (
// "context" "context"
// "fmt" "fmt"
// "github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
// ) legacy_es "github.com/zitadel/zitadel/internal/eventstore"
)
// // Commander is the all it needs to implement the command pattern. // Commander is all that is needed to implement the command pattern.
// // It is the interface all manipulations need to implement. // It is the interface all manipulations need to implement.
// // If possible it should also be used for queries. We will find out if this is possible in the future. // If possible it should also be used for queries. We will find out if this is possible in the future.
// type Commander interface { type Commander interface {
// Execute(ctx context.Context, opts *CommandOpts) (err error) Execute(ctx context.Context, opts *CommandOpts) (err error)
// fmt.Stringer // Events returns the events that should be pushed to the event store after the command is executed.
// } // If the command does not produce events, it should return nil or an empty slice.
Events(ctx context.Context) []legacy_es.Command
fmt.Stringer
}
// // Invoker is part of the command pattern. // Invoker is part of the command pattern.
// // It is the interface that is used to execute commands. // It is the interface that is used to execute commands.
// type Invoker interface { type Invoker interface {
// Invoke(ctx context.Context, command Commander, opts *CommandOpts) error Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
// } }
// // CommandOpts are passed to each command // CommandOpts are passed to each command
// // the provide common fields used by commands like the database client. // it provides common fields used by commands like the database client.
// type CommandOpts struct { type CommandOpts struct {
// DB database.QueryExecutor DB database.QueryExecutor
// Invoker Invoker Invoker Invoker
// } }
// type ensureTxOpts struct { type ensureTxOpts struct {
// *database.TransactionOptions *database.TransactionOptions
// } }
// type EnsureTransactionOpt func(*ensureTxOpts) type EnsureTransactionOpt func(*ensureTxOpts)
// // EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction. // EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction.
// // The returned close function will end the transaction. If the DB is already a transaction, the close function // The returned close function will end the transaction. If the DB is already a transaction, the close function
// // will do nothing because another [Commander] is already responsible for ending the transaction. // will do nothing because another [Commander] is already responsible for ending the transaction.
// func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) { func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) {
// beginner, ok := o.DB.(database.Beginner) beginner, ok := o.DB.(database.Beginner)
// if !ok { if !ok {
// // db is already a transaction // db is already a transaction
// return func(_ context.Context, err error) error { return func(_ context.Context, err error) error {
// return err return err
// }, nil }, nil
// } }
// txOpts := &ensureTxOpts{ txOpts := &ensureTxOpts{
// TransactionOptions: new(database.TransactionOptions), TransactionOptions: new(database.TransactionOptions),
// } }
// for _, opt := range opts { for _, opt := range opts {
// opt(txOpts) opt(txOpts)
// } }
// tx, err := beginner.Begin(ctx, txOpts.TransactionOptions) tx, err := beginner.Begin(ctx, txOpts.TransactionOptions)
// if err != nil { if err != nil {
// return nil, err return nil, err
// } }
// o.DB = tx o.DB = tx
// return func(ctx context.Context, err error) error { return func(ctx context.Context, err error) error {
// return tx.End(ctx, err) return tx.End(ctx, err)
// }, nil }, nil
// } }
// // EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool]. // EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool].
// // The returned close function will release the client. If the o.DB is already a client or transaction, the close function // The returned close function will release the client. If the o.DB is already a client or transaction, the close function
// // will do nothing because another [Commander] is already responsible for releasing the client. // will do nothing because another [Commander] is already responsible for releasing the client.
// func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) { func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) {
// pool, ok := o.DB.(database.Pool) pool, ok := o.DB.(database.Pool)
// if !ok { if !ok {
// // o.DB is already a client // o.DB is already a client
// return func(_ context.Context) error { return func(_ context.Context) error {
// return nil return nil
// }, nil }, nil
// } }
// client, err := pool.Acquire(ctx) client, err := pool.Acquire(ctx)
// if err != nil { if err != nil {
// return nil, err return nil, err
// } }
// o.DB = client o.DB = client
// return func(ctx context.Context) error { return func(ctx context.Context) error {
// return client.Release(ctx) return client.Release(ctx)
// }, nil }, nil
// } }
// func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error { func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error {
// if o.Invoker == nil { if o.Invoker == nil {
// return command.Execute(ctx, o) return command.Execute(ctx, o)
// } }
// return o.Invoker.Invoke(ctx, command, o) return o.Invoker.Invoke(ctx, command, o)
// } }
// func DefaultOpts(invoker Invoker) *CommandOpts { func DefaultOpts(invoker Invoker) *CommandOpts {
// if invoker == nil { if invoker == nil {
// invoker = &noopInvoker{} invoker = &noopInvoker{}
// } }
// return &CommandOpts{ return &CommandOpts{
// DB: pool, DB: pool,
// Invoker: invoker, Invoker: invoker,
// } }
// } }
// // commandBatch is a batch of commands. // commandBatch is a batch of commands.
// // It uses the [Invoker] provided by the opts to execute each command. // It uses the [Invoker] provided by the opts to execute each command.
// type commandBatch struct { type commandBatch struct {
// Commands []Commander Commands []Commander
// } }
// func BatchCommands(cmds ...Commander) *commandBatch { // Events implements Commander.
// return &commandBatch{ func (cmd *commandBatch) Events(ctx context.Context) []legacy_es.Command {
// Commands: cmds, commands := make([]legacy_es.Command, 0, len(cmd.Commands))
// } for _, c := range cmd.Commands {
// } if e := c.Events(ctx); len(e) > 0 {
commands = append(commands, e...)
}
}
return commands
}
// // String implements [Commander]. func BatchCommands(cmds ...Commander) *commandBatch {
// func (cmd *commandBatch) String() string { return &commandBatch{
// return "commandBatch" Commands: cmds,
// } }
}
// func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) { // String implements [Commander].
// for _, cmd := range b.Commands { func (cmd *commandBatch) String() string {
// if err = opts.Invoke(ctx, cmd); err != nil { return "commandBatch"
// return err }
// }
// }
// return nil
// }
// var _ Commander = (*commandBatch)(nil) func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
for _, cmd := range b.Commands {
if err = opts.Invoke(ctx, cmd); err != nil {
return err
}
}
return nil
}
var _ Commander = (*commandBatch)(nil)
+68
View File
@@ -0,0 +1,68 @@
package domain
import (
"log/slog"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// The variables could also be moved to a struct.
// I just started with the singleton pattern and kept it like this.
var (
pool database.Pool
// userCodeAlgorithm crypto.EncryptionAlgorithm
tracer tracing.Tracer
logger logging.Logger = *logging.NewLogger(slog.Default())
legacyEventstore eventstore.LegacyEventstore
// userRepo func(database.QueryExecutor) UserRepository
// instanceRepo func(database.QueryExecutor) InstanceRepository
// cryptoRepo func(database.QueryExecutor) CryptoRepository
// orgRepo func(database.QueryExecutor) OrgRepository
// instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
// orgCache cache.Cache[orgCacheIndex, string, *Org]
// generateID func() (string, error) = func() (string, error) {
// return strconv.FormatUint(rand.Uint64(), 10), nil
// }
)
func SetPool(p database.Pool) {
pool = p
}
// func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
// userCodeAlgorithm = algorithm
// }
func SetTracer(t tracing.Tracer) {
tracer = t
}
func SetLogger(l logging.Logger) {
logger = l
}
func SetLegacyEventstore(es eventstore.LegacyEventstore) {
legacyEventstore = es
}
// func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
// userRepo = repo
// }
// func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
// orgRepo = repo
// }
// func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
// instanceRepo = repo
// }
// func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
// cryptoRepo = repo
// }
-65
View File
@@ -60,68 +60,3 @@ type domainChanges interface {
// This is used for reducing events. // This is used for reducing events.
SetUpdatedAt(t time.Time) database.Change SetUpdatedAt(t time.Time) database.Change
} }
// import (
// "math/rand/v2"
// "strconv"
// "github.com/zitadel/zitadel/backend/v3/storage/cache"
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// // "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// "github.com/zitadel/zitadel/internal/crypto"
// )
// // The variables could also be moved to a struct.
// // I just started with the singleton pattern and kept it like this.
// var (
// pool database.Pool
// userCodeAlgorithm crypto.EncryptionAlgorithm
// tracer tracing.Tracer
// // logger logging.Logger
// userRepo func(database.QueryExecutor) UserRepository
// // instanceRepo func(database.QueryExecutor) InstanceRepository
// cryptoRepo func(database.QueryExecutor) CryptoRepository
// orgRepo func(database.QueryExecutor) OrgRepository
// // instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
// orgCache cache.Cache[orgCacheIndex, string, *Org]
// generateID func() (string, error) = func() (string, error) {
// return strconv.FormatUint(rand.Uint64(), 10), nil
// }
// )
// func SetPool(p database.Pool) {
// pool = p
// }
// func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
// userCodeAlgorithm = algorithm
// }
// func SetTracer(t tracing.Tracer) {
// tracer = t
// }
// // func SetLogger(l logging.Logger) {
// // logger = l
// // }
// func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
// userRepo = repo
// }
// func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
// orgRepo = repo
// }
// // func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
// // instanceRepo = repo
// // }
// func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
// cryptoRepo = repo
// }
+117 -116
View File
@@ -1,142 +1,143 @@
package domain package domain
// import ( import (
// "context" "context"
// "fmt" "fmt"
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore" "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
// ) legacy_es "github.com/zitadel/zitadel/internal/eventstore"
)
// // Invoke provides a way to execute commands within the domain package. // Invoke provides a way to execute commands within the domain package.
// // It uses a chain of responsibility pattern to handle the command execution. // It uses a chain of responsibility pattern to handle the command execution.
// // The default chain includes logging, tracing, and event publishing. // The default chain includes logging, tracing, and event publishing.
// // If you want to invoke multiple commands in a single transaction, you can use the [commandBatch]. // If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
// func Invoke(ctx context.Context, cmd Commander) error { func Invoke(ctx context.Context, cmd Commander) error {
// invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil))) invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
// opts := &CommandOpts{ opts := &CommandOpts{
// Invoker: invoker.collector, Invoker: invoker.collector,
// DB: pool, DB: pool,
// } }
// return invoker.Invoke(ctx, cmd, opts) return invoker.Invoke(ctx, cmd, opts)
// } }
// // eventStoreInvoker checks if the command implements the [eventer] interface. // eventStoreInvoker checks if the [Commander].Events function returns any events.
// // If it does, it collects the events and publishes them to the event store. // If it does, it collects the events and publishes them to the event store.
// type eventStoreInvoker struct { type eventStoreInvoker struct {
// collector *eventCollector collector *eventCollector
// } }
// func newEventStoreInvoker(next Invoker) *eventStoreInvoker { func newEventStoreInvoker(next Invoker) *eventStoreInvoker {
// return &eventStoreInvoker{collector: &eventCollector{next: next}} return &eventStoreInvoker{collector: &eventCollector{next: next}}
// } }
// func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) { func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// err = i.collector.Invoke(ctx, command, opts) err = i.collector.Invoke(ctx, command, opts)
// if err != nil { if err != nil {
// return err return err
// } }
// if len(i.collector.events) > 0 { if len(i.collector.events) > 0 {
// err = eventstore.Publish(ctx, i.collector.events, opts.DB) err = eventstore.Publish(ctx, legacyEventstore, opts.DB, i.collector.events...)
// if err != nil { if err != nil {
// return err return err
// } }
// } }
// return nil return nil
// } }
// // eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed. // eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
// type eventCollector struct { // The events are collected after the command got executed, the collector ensures that the command is executed in the same transaction as writing the events.
// next Invoker type eventCollector struct {
// events []*eventstore.Event next Invoker
// } events []legacy_es.Command
}
// type eventer interface { func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// Events() []*eventstore.Event close, err := opts.EnsureTx(ctx)
// } if err != nil {
return err
}
defer func() { err = close(ctx, err) }()
// func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) { if i.next != nil {
// if e, ok := command.(eventer); ok && len(e.Events()) > 0 { err = i.next.Invoke(ctx, command, opts)
// // we need to ensure all commands are executed in the same transaction } else {
// close, err := opts.EnsureTx(ctx) err = command.Execute(ctx, opts)
// if err != nil { }
// return err if err != nil {
// } return err
// defer func() { err = close(ctx, err) }() }
i.events = append(command.Events(ctx), i.events...)
// i.events = append(i.events, e.Events()...) return
// } }
// if i.next != nil {
// return i.next.Invoke(ctx, command, opts)
// }
// return command.Execute(ctx, opts)
// }
// // traceInvoker decorates each command with tracing. // traceInvoker decorates each command with tracing.
// type traceInvoker struct { type traceInvoker struct {
// next Invoker next Invoker
// } }
// func newTraceInvoker(next Invoker) *traceInvoker { func newTraceInvoker(next Invoker) *traceInvoker {
// return &traceInvoker{next: next} return &traceInvoker{next: next}
// } }
// func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) { func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command)) ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command))
// defer func() { defer func() {
// if err != nil { if err != nil {
// span.RecordError(err) span.RecordError(err)
// } }
// span.End() span.End()
// }() }()
// if i.next != nil { if i.next != nil {
// return i.next.Invoke(ctx, command, opts) return i.next.Invoke(ctx, command, opts)
// } }
// return command.Execute(ctx, opts) return command.Execute(ctx, opts)
// } }
// // loggingInvoker decorates each command with logging. // loggingInvoker decorates each command with logging.
// // It is an example implementation and logs the command name at the beginning and success or failure after the command got executed. // It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
// type loggingInvoker struct { type loggingInvoker struct {
// next Invoker next Invoker
// } }
// func newLoggingInvoker(next Invoker) *loggingInvoker { func newLoggingInvoker(next Invoker) *loggingInvoker {
// return &loggingInvoker{next: next} return &loggingInvoker{next: next}
// } }
// func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) { func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// logger.InfoContext(ctx, "Invoking command", "command", command.String()) logger.InfoContext(ctx, "Invoking command", "command", command.String())
// if i.next != nil { if i.next != nil {
// err = i.next.Invoke(ctx, command, opts) err = i.next.Invoke(ctx, command, opts)
// } else { } else {
// err = command.Execute(ctx, opts) err = command.Execute(ctx, opts)
// } }
// if err != nil { if err != nil {
// logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err) logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err)
// return err return err
// } }
// logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String()) logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String())
// return nil return nil
// } }
// type noopInvoker struct { type noopInvoker struct {
// next Invoker next Invoker
// } }
// func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) error { func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) error {
// if i.next != nil { if i.next != nil {
// return i.next.Invoke(ctx, command, opts) return i.next.Invoke(ctx, command, opts)
// } }
// return command.Execute(ctx, opts) return command.Execute(ctx, opts)
// } }
// // cacheInvoker could be used in the future to do the caching. // cacheInvoker could be used in the future to do the caching.
// // My goal would be to have two interfaces: // My goal would be to have two interfaces:
// // - cacheSetter: which caches an object // - cacheSetter: which caches an object
// // - cacheGetter: which gets an object from the cache, this should also skip the command execution // - cacheGetter: which gets an object from the cache, this should also skip the command execution
// type cacheInvoker struct { // type cacheInvoker struct {
// next Invoker // next Invoker
// } // }
+180
View File
@@ -0,0 +1,180 @@
package domain
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
legacy_es "github.com/zitadel/zitadel/internal/eventstore"
)
type testLegacyEventstore struct{}
func (es *testLegacyEventstore) PushWithNewClient(ctx context.Context, client database.QueryExecutor, commands ...legacy_es.Command) ([]legacy_es.Event, error) {
return nil, nil
}
var _ eventstore.LegacyEventstore = (*testLegacyEventstore)(nil)
type invokeTestCommand struct {
events []legacy_es.Command
execute func(ctx context.Context, opts *CommandOpts) error
}
// Events implements Commander.
func (i *invokeTestCommand) Events(ctx context.Context) []legacy_es.Command {
return i.events
}
// Execute implements Commander.
func (i *invokeTestCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
if i.execute == nil {
return nil
}
return i.execute(ctx, opts)
}
// String implements Commander.
func (i *invokeTestCommand) String() string {
return "invokeTestCommand"
}
var _ Commander = (*invokeTestCommand)(nil)
type invokeTestEvent struct {
id string
}
// Aggregate implements [legacy_es.Command].
func (i *invokeTestEvent) Aggregate() *legacy_es.Aggregate {
return &legacy_es.Aggregate{
ID: i.id,
Type: "test",
ResourceOwner: "test",
InstanceID: "test",
Version: legacy_es.Version("v1"),
}
}
// Creator implements [legacy_es.Command].
func (i *invokeTestEvent) Creator() string {
return "test"
}
// Fields implements [legacy_es.Command].
func (i *invokeTestEvent) Fields() []*legacy_es.FieldOperation {
return nil
}
// Payload implements [legacy_es.Command].
func (i *invokeTestEvent) Payload() any {
return nil
}
// Revision implements [legacy_es.Command].
func (i *invokeTestEvent) Revision() uint16 {
return 1
}
// Type implements [legacy_es.Command].
func (i *invokeTestEvent) Type() legacy_es.EventType {
return "test"
}
// UniqueConstraints implements [legacy_es.Command].
func (i *invokeTestEvent) UniqueConstraints() []*legacy_es.UniqueConstraint {
return nil
}
var _ legacy_es.Command = (*invokeTestEvent)(nil)
func Test_eventCollector_Invoke(t *testing.T) {
tests := []struct {
name string
command Commander
expectedErr error
assertCollectedEvents func(t *testing.T, events []legacy_es.Command)
}{
{
name: "simple command with events",
expectedErr: nil,
command: &invokeTestCommand{
events: []legacy_es.Command{&invokeTestEvent{id: "1"}},
},
assertCollectedEvents: func(t *testing.T, events []legacy_es.Command) {
require.Len(t, events, 1)
assert.Equal(t, "1", events[0].Aggregate().ID)
},
},
{
name: "simple command without events",
expectedErr: nil,
command: &invokeTestCommand{},
assertCollectedEvents: func(t *testing.T, events []legacy_es.Command) {
assert.Len(t, events, 0)
},
},
{
name: "command with sub commands",
expectedErr: nil,
command: &invokeTestCommand{
events: []legacy_es.Command{&invokeTestEvent{id: "1"}},
execute: func(ctx context.Context, opts *CommandOpts) error {
return opts.Invoke(ctx, &invokeTestCommand{
events: []legacy_es.Command{&invokeTestEvent{id: "2"}},
})
},
},
assertCollectedEvents: func(t *testing.T, events []legacy_es.Command) {
require.Len(t, events, 2)
assert.Equal(t, "1", events[0].Aggregate().ID)
assert.Equal(t, "2", events[1].Aggregate().ID)
},
},
{
name: "only sub commands with events",
expectedErr: nil,
command: &invokeTestCommand{
execute: func(ctx context.Context, opts *CommandOpts) error {
return opts.Invoke(ctx, &invokeTestCommand{
events: []legacy_es.Command{&invokeTestEvent{id: "2"}},
})
},
},
assertCollectedEvents: func(t *testing.T, events []legacy_es.Command) {
require.Len(t, events, 1)
assert.Equal(t, "2", events[0].Aggregate().ID)
},
},
}
SetLegacyEventstore(new(testLegacyEventstore))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
pool := dbmock.NewMockPool(ctrl)
tx := dbmock.NewMockTransaction(ctrl)
tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil).AnyTimes()
i := newEventStoreInvoker(nil)
opts := CommandOpts{
Invoker: i,
DB: pool,
}
err := opts.Invoke(t.Context(), tt.command)
require.ErrorIs(t, err, tt.expectedErr)
tt.assertCollectedEvents(t, i.collector.events)
})
}
}
+18 -22
View File
@@ -1,18 +1,19 @@
package domain package domain
// import ( import (
// "context" "context"
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore" legacy_es "github.com/zitadel/zitadel/internal/eventstore"
// ) "github.com/zitadel/zitadel/internal/repository/org"
)
// // AddOrgCommand adds a new organization. // AddOrgCommand adds a new organization.
// // I'm unsure if we should add the Admins here or if this should be a separate command. // I'm unsure if we should add the Admins here or if this should be a separate command.
// type AddOrgCommand struct { type AddOrgCommand struct {
// ID string `json:"id"` ID string `json:"id"`
// Name string `json:"name"` Name string `json:"name"`
// Admins []*AddMemberCommand `json:"admins"` // Admins []*AddMemberCommand `json:"admins"`
// } }
// func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand { // func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
// return &AddOrgCommand{ // return &AddOrgCommand{
@@ -63,17 +64,12 @@ package domain
// return nil // return nil
// } // }
// // Events implements [eventer]. // Events implements [eventer].
// func (cmd *AddOrgCommand) Events() []*eventstore.Event { func (cmd *AddOrgCommand) Events(ctx context.Context) []legacy_es.Command {
// return []*eventstore.Event{ return []legacy_es.Command{
// { org.NewOrgAddedEvent(ctx, &org.NewAggregate(cmd.ID).Aggregate, cmd.Name),
// AggregateType: "org", }
// AggregateID: cmd.ID, }
// Type: "org.added",
// Payload: cmd,
// },
// }
// }
// var ( // var (
// _ Commander = (*AddOrgCommand)(nil) // _ Commander = (*AddOrgCommand)(nil)
+4
View File
@@ -12,6 +12,8 @@ type Pool interface {
Acquire(ctx context.Context) (Client, error) Acquire(ctx context.Context) (Client, error)
Close(ctx context.Context) error Close(ctx context.Context) error
Ping(ctx context.Context) error
} }
type PoolTest interface { type PoolTest interface {
@@ -27,6 +29,8 @@ type Client interface {
Migrator Migrator
Release(ctx context.Context) error Release(ctx context.Context) error
Ping(ctx context.Context) error
} }
// Querier is a database client that can execute queries and return rows. // Querier is a database client that can execute queries and return rows.
@@ -27,7 +27,7 @@ func (c *pgxConn) Begin(ctx context.Context, opts *database.TransactionOptions)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
} }
return &pgxTx{tx}, nil return &Transaction{tx}, nil
} }
// Query implements sql.Client. // Query implements sql.Client.
@@ -56,6 +56,11 @@ func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) (int64, err
return res.RowsAffected(), nil return res.RowsAffected(), nil
} }
// Ping implements [database.Pool].
func (c *pgxConn) Ping(ctx context.Context) error {
return wrapError(c.Conn.Ping(ctx))
}
// Migrate implements [database.Migrator]. // Migrate implements [database.Migrator].
func (c *pgxConn) Migrate(ctx context.Context) error { func (c *pgxConn) Migrate(ctx context.Context) error {
if isMigrated { if isMigrated {
@@ -62,7 +62,7 @@ func (c *pgxPool) Begin(ctx context.Context, opts *database.TransactionOptions)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
} }
return &pgxTx{tx}, nil return &Transaction{tx}, nil
} }
// Close implements [database.Pool]. // Close implements [database.Pool].
@@ -71,6 +71,11 @@ func (c *pgxPool) Close(_ context.Context) error {
return nil return nil
} }
// Ping implements [database.Pool].
func (c *pgxPool) Ping(ctx context.Context) error {
return wrapError(c.Pool.Ping(ctx))
}
// Migrate implements [database.Migrator]. // Migrate implements [database.Migrator].
func (c *pgxPool) Migrate(ctx context.Context) error { func (c *pgxPool) Migrate(ctx context.Context) error {
if isMigrated { if isMigrated {
@@ -9,24 +9,30 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
) )
type pgxTx struct{ pgx.Tx } type Transaction struct{ pgx.Tx }
var _ database.Transaction = (*pgxTx)(nil) func PGxTx(tx pgx.Tx) *Transaction {
return &Transaction{
Tx: tx,
}
}
var _ database.Transaction = (*Transaction)(nil)
// Commit implements [database.Transaction]. // Commit implements [database.Transaction].
func (tx *pgxTx) Commit(ctx context.Context) error { func (tx *Transaction) Commit(ctx context.Context) error {
err := tx.Tx.Commit(ctx) err := tx.Tx.Commit(ctx)
return wrapError(err) return wrapError(err)
} }
// Rollback implements [database.Transaction]. // Rollback implements [database.Transaction].
func (tx *pgxTx) Rollback(ctx context.Context) error { func (tx *Transaction) Rollback(ctx context.Context) error {
err := tx.Tx.Rollback(ctx) err := tx.Tx.Rollback(ctx)
return wrapError(err) return wrapError(err)
} }
// End implements [database.Transaction]. // End implements [database.Transaction].
func (tx *pgxTx) End(ctx context.Context, err error) error { func (tx *Transaction) End(ctx context.Context, err error) error {
if err != nil { if err != nil {
rollbackErr := tx.Rollback(ctx) rollbackErr := tx.Rollback(ctx)
if rollbackErr != nil { if rollbackErr != nil {
@@ -39,7 +45,7 @@ func (tx *pgxTx) End(ctx context.Context, err error) error {
// Query implements [database.Transaction]. // Query implements [database.Transaction].
// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. // Subtle: this method shadows the method (Tx).Query of pgxTx.Tx.
func (tx *pgxTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { func (tx *Transaction) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) {
rows, err := tx.Tx.Query(ctx, sql, args...) rows, err := tx.Tx.Query(ctx, sql, args...)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
@@ -49,13 +55,13 @@ func (tx *pgxTx) Query(ctx context.Context, sql string, args ...any) (database.R
// QueryRow implements [database.Transaction]. // QueryRow implements [database.Transaction].
// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. // Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx.
func (tx *pgxTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { func (tx *Transaction) QueryRow(ctx context.Context, sql string, args ...any) database.Row {
return &Row{tx.Tx.QueryRow(ctx, sql, args...)} return &Row{tx.Tx.QueryRow(ctx, sql, args...)}
} }
// Exec implements [database.Transaction]. // Exec implements [database.Transaction].
// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. // Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool.
func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) (int64, error) { func (tx *Transaction) Exec(ctx context.Context, sql string, args ...any) (int64, error) {
res, err := tx.Tx.Exec(ctx, sql, args...) res, err := tx.Tx.Exec(ctx, sql, args...)
if err != nil { if err != nil {
return 0, wrapError(err) return 0, wrapError(err)
@@ -65,12 +71,12 @@ func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) (int64, erro
// Begin implements [database.Transaction]. // Begin implements [database.Transaction].
// As postgres does not support nested transactions we use savepoints to emulate them. // As postgres does not support nested transactions we use savepoints to emulate them.
func (tx *pgxTx) Begin(ctx context.Context) (database.Transaction, error) { func (tx *Transaction) Begin(ctx context.Context) (database.Transaction, error) {
savepoint, err := tx.Tx.Begin(ctx) savepoint, err := tx.Tx.Begin(ctx)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
} }
return &pgxTx{savepoint}, nil return &Transaction{savepoint}, nil
} }
func transactionOptionsToPgx(opts *database.TransactionOptions) pgx.TxOptions { func transactionOptionsToPgx(opts *database.TransactionOptions) pgx.TxOptions {
@@ -11,6 +11,10 @@ type sqlConn struct {
*sql.Conn *sql.Conn
} }
func SQLConn(conn *sql.Conn) database.Client {
return &sqlConn{Conn: conn}
}
var _ database.Client = (*sqlConn)(nil) var _ database.Client = (*sqlConn)(nil)
// Release implements [database.Client]. // Release implements [database.Client].
@@ -24,7 +28,7 @@ func (c *sqlConn) Begin(ctx context.Context, opts *database.TransactionOptions)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
} }
return &sqlTx{tx}, nil return &Transaction{tx}, nil
} }
// Query implements sql.Client. // Query implements sql.Client.
@@ -54,6 +58,11 @@ func (c *sqlConn) Exec(ctx context.Context, sql string, args ...any) (int64, err
return res.RowsAffected() return res.RowsAffected()
} }
// Ping implements [database.Pool].
func (c *sqlConn) Ping(ctx context.Context) error {
return wrapError(c.PingContext(ctx))
}
// Migrate implements [database.Migrator]. // Migrate implements [database.Migrator].
func (c *sqlConn) Migrate(ctx context.Context) error { func (c *sqlConn) Migrate(ctx context.Context) error {
return ErrMigrate return ErrMigrate
@@ -61,7 +61,12 @@ func (c *sqlPool) Begin(ctx context.Context, opts *database.TransactionOptions)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
} }
return &sqlTx{tx}, nil return &Transaction{tx}, nil
}
// Ping implements [database.Pool].
func (c *sqlPool) Ping(ctx context.Context) error {
return wrapError(c.PingContext(ctx))
} }
// Close implements [database.Pool]. // Close implements [database.Pool].
+11 -11
View File
@@ -8,28 +8,28 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
) )
type sqlTx struct{ *sql.Tx } type Transaction struct{ *sql.Tx }
var _ database.Transaction = (*sqlTx)(nil) var _ database.Transaction = (*Transaction)(nil)
func SQLTx(tx *sql.Tx) *sqlTx { func SQLTx(tx *sql.Tx) *Transaction {
return &sqlTx{ return &Transaction{
Tx: tx, Tx: tx,
} }
} }
// Commit implements [database.Transaction]. // Commit implements [database.Transaction].
func (tx *sqlTx) Commit(ctx context.Context) error { func (tx *Transaction) Commit(ctx context.Context) error {
return wrapError(tx.Tx.Commit()) return wrapError(tx.Tx.Commit())
} }
// Rollback implements [database.Transaction]. // Rollback implements [database.Transaction].
func (tx *sqlTx) Rollback(ctx context.Context) error { func (tx *Transaction) Rollback(ctx context.Context) error {
return wrapError(tx.Tx.Rollback()) return wrapError(tx.Tx.Rollback())
} }
// End implements [database.Transaction]. // End implements [database.Transaction].
func (tx *sqlTx) End(ctx context.Context, err error) error { func (tx *Transaction) End(ctx context.Context, err error) error {
if err != nil { if err != nil {
rollbackErr := tx.Rollback(ctx) rollbackErr := tx.Rollback(ctx)
if rollbackErr != nil { if rollbackErr != nil {
@@ -42,7 +42,7 @@ func (tx *sqlTx) End(ctx context.Context, err error) error {
// Query implements [database.Transaction]. // Query implements [database.Transaction].
// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. // Subtle: this method shadows the method (Tx).Query of pgxTx.Tx.
func (tx *sqlTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { func (tx *Transaction) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) {
//nolint:rowserrcheck // Rows.Close is called by the caller //nolint:rowserrcheck // Rows.Close is called by the caller
rows, err := tx.QueryContext(ctx, sql, args...) rows, err := tx.QueryContext(ctx, sql, args...)
if err != nil { if err != nil {
@@ -53,13 +53,13 @@ func (tx *sqlTx) Query(ctx context.Context, sql string, args ...any) (database.R
// QueryRow implements [database.Transaction]. // QueryRow implements [database.Transaction].
// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. // Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx.
func (tx *sqlTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { func (tx *Transaction) QueryRow(ctx context.Context, sql string, args ...any) database.Row {
return &Row{tx.QueryRowContext(ctx, sql, args...)} return &Row{tx.QueryRowContext(ctx, sql, args...)}
} }
// Exec implements [database.Transaction]. // Exec implements [database.Transaction].
// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. // Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool.
func (tx *sqlTx) Exec(ctx context.Context, sql string, args ...any) (int64, error) { func (tx *Transaction) Exec(ctx context.Context, sql string, args ...any) (int64, error) {
res, err := tx.ExecContext(ctx, sql, args...) res, err := tx.ExecContext(ctx, sql, args...)
if err != nil { if err != nil {
return 0, wrapError(err) return 0, wrapError(err)
@@ -69,7 +69,7 @@ func (tx *sqlTx) Exec(ctx context.Context, sql string, args ...any) (int64, erro
// Begin implements [database.Transaction]. // Begin implements [database.Transaction].
// As postgres does not support nested transactions we use savepoints to emulate them. // As postgres does not support nested transactions we use savepoints to emulate them.
func (tx *sqlTx) Begin(ctx context.Context) (database.Transaction, error) { func (tx *Transaction) Begin(ctx context.Context) (database.Transaction, error) {
_, err := tx.ExecContext(ctx, createSavepoint) _, err := tx.ExecContext(ctx, createSavepoint)
if err != nil { if err != nil {
return nil, wrapError(err) return nil, wrapError(err)
+4
View File
@@ -95,6 +95,10 @@ func (e *IntegrityViolationError) Is(target error) bool {
return ok return ok
} }
func (e *IntegrityViolationError) Unwrap() error {
return e.original
}
// CheckError is returned when a check constraint fails. // CheckError is returned when a check constraint fails.
// It wraps the [IntegrityViolationError] to provide more context. // It wraps the [IntegrityViolationError] to provide more context.
// It is used to indicate that a check constraint was violated during an insert or update operation. // It is used to indicate that a check constraint was violated during an insert or update operation.
+7 -13
View File
@@ -4,21 +4,15 @@ import (
"context" "context"
"github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/eventstore"
) )
type Event struct { type LegacyEventstore interface {
AggregateType string `json:"aggregateType"` PushWithNewClient(ctx context.Context, client database.QueryExecutor, commands ...eventstore.Command) ([]eventstore.Event, error)
AggregateID string `json:"aggregateId"`
Type string `json:"type"`
Payload any `json:"payload,omitempty"`
} }
func Publish(ctx context.Context, events []*Event, db database.Executor) error { // Publish writes events to the eventstore using the provided pusher and database client.
for _, event := range events { func Publish(ctx context.Context, es LegacyEventstore, client database.QueryExecutor, commands ...eventstore.Command) error {
_, err := db.Exec(ctx, `INSERT INTO events (aggregate_type, aggregate_id) VALUES ($1, $2)`, event.AggregateType, event.AggregateID) _, err := es.PushWithNewClient(ctx, client, commands...)
if err != nil { return err
return err
}
}
return nil
} }
+18 -4
View File
@@ -2,6 +2,7 @@ package eventstore
import ( import (
"context" "context"
"database/sql"
"errors" "errors"
"sort" "sort"
"time" "time"
@@ -10,6 +11,8 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/zitadel/logging" "github.com/zitadel/logging"
new_db "github.com/zitadel/zitadel/backend/v3/storage/database"
new_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
@@ -91,6 +94,19 @@ func (es *Eventstore) Push(ctx context.Context, cmds ...Command) ([]Event, error
// PushWithClient pushes the events in a single transaction using the provided database client // PushWithClient pushes the events in a single transaction using the provided database client
// an event needs at least an aggregate // an event needs at least an aggregate
func (es *Eventstore) PushWithClient(ctx context.Context, client database.ContextQueryExecuter, cmds ...Command) ([]Event, error) { func (es *Eventstore) PushWithClient(ctx context.Context, client database.ContextQueryExecuter, cmds ...Command) ([]Event, error) {
var dbClient new_db.QueryExecutor
switch client := client.(type) {
case *sql.DB:
dbClient = new_sql.SQLPool(client)
case *sql.Tx:
dbClient = new_sql.SQLTx(client)
case *sql.Conn:
dbClient = new_sql.SQLConn(client)
}
return es.PushWithNewClient(ctx, dbClient, cmds...)
}
func (es *Eventstore) PushWithNewClient(ctx context.Context, client new_db.QueryExecutor, cmds ...Command) ([]Event, error) {
if es.PushTimeout > 0 { if es.PushTimeout > 0 {
var cancel func() var cancel func()
ctx, cancel = context.WithTimeout(ctx, es.PushTimeout) ctx, cancel = context.WithTimeout(ctx, es.PushTimeout)
@@ -108,7 +124,7 @@ retry:
for i := 0; i <= es.maxRetries; i++ { for i := 0; i <= es.maxRetries; i++ {
events, err = es.pusher.Push(ctx, client, cmds...) events, err = es.pusher.Push(ctx, client, cmds...)
// if there is a transaction passed the calling function needs to retry // if there is a transaction passed the calling function needs to retry
if _, ok := client.(database.Tx); ok { if _, ok := client.(new_db.Transaction); ok {
break retry break retry
} }
var pgErr *pgconn.PgError var pgErr *pgconn.PgError
@@ -284,9 +300,7 @@ type Pusher interface {
// Health checks if the connection to the storage is available // Health checks if the connection to the storage is available
Health(ctx context.Context) error Health(ctx context.Context) error
// Push stores the actions // Push stores the actions
Push(ctx context.Context, client database.ContextQueryExecuter, commands ...Command) (_ []Event, err error) Push(ctx context.Context, client new_db.QueryExecutor, commands ...Command) (_ []Event, err error)
// Client returns the underlying database connection
Client() *database.DB
} }
type FillFieldsEvent interface { type FillFieldsEvent interface {
@@ -1,162 +0,0 @@
package eventstore_test
import (
"context"
_ "embed"
"fmt"
"strconv"
"testing"
"github.com/zitadel/zitadel/internal/eventstore"
)
//go:embed bench_payload.txt
var text string
func Benchmark_Push_SameAggregate(b *testing.B) {
ctx := context.Background()
smallPayload := struct {
Username string
Firstname string
Lastname string
}{
Username: "username",
Firstname: "firstname",
Lastname: "lastname",
}
bigPayload := struct {
Username string
Firstname string
Lastname string
Text string
}{
Username: "username",
Firstname: "firstname",
Lastname: "lastname",
Text: text,
}
commands := map[string][]eventstore.Command{
"no payload one command": {
generateCommand(eventstore.AggregateType(b.Name()), "id"),
},
"small payload one command": {
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(smallPayload)),
},
"big payload one command": {
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(bigPayload)),
},
"no payload multiple commands": {
generateCommand(eventstore.AggregateType(b.Name()), "id"),
generateCommand(eventstore.AggregateType(b.Name()), "id"),
generateCommand(eventstore.AggregateType(b.Name()), "id"),
},
"mixed payload multiple command": {
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(smallPayload)),
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(bigPayload)),
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(smallPayload)),
generateCommand(eventstore.AggregateType(b.Name()), "id", withTestData(bigPayload)),
},
}
for cmdsKey, cmds := range commands {
for pusherKey, store := range pushers {
b.Run(fmt.Sprintf("Benchmark_Push_SameAggregate-%s-%s", pusherKey, cmdsKey), func(b *testing.B) {
b.StopTimer()
cleanupEventstore(clients[pusherKey])
b.StartTimer()
for n := 0; n < b.N; n++ {
_, err := store.Push(ctx, store.Client().DB, cmds...)
if err != nil {
b.Error(err)
}
}
})
}
}
}
func Benchmark_Push_MultipleAggregate_Parallel(b *testing.B) {
smallPayload := struct {
Username string
Firstname string
Lastname string
}{
Username: "username",
Firstname: "firstname",
Lastname: "lastname",
}
bigPayload := struct {
Username string
Firstname string
Lastname string
Text string
}{
Username: "username",
Firstname: "firstname",
Lastname: "lastname",
Text: text,
}
commandCreators := map[string]func(id string) []eventstore.Command{
"no payload one command": func(id string) []eventstore.Command {
return []eventstore.Command{
generateCommand(eventstore.AggregateType(b.Name()), id),
}
},
"small payload one command": func(id string) []eventstore.Command {
return []eventstore.Command{
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(smallPayload)),
}
},
"big payload one command": func(id string) []eventstore.Command {
return []eventstore.Command{
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(bigPayload)),
}
},
"no payload multiple commands": func(id string) []eventstore.Command {
return []eventstore.Command{
generateCommand(eventstore.AggregateType(b.Name()), id),
generateCommand(eventstore.AggregateType(b.Name()), id),
generateCommand(eventstore.AggregateType(b.Name()), id),
}
},
"mixed payload multiple command": func(id string) []eventstore.Command {
return []eventstore.Command{
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(smallPayload)),
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(bigPayload)),
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(smallPayload)),
generateCommand(eventstore.AggregateType(b.Name()), id, withTestData(bigPayload)),
}
},
}
for cmdsKey, commandCreator := range commandCreators {
for pusherKey, store := range pushers {
b.Run(fmt.Sprintf("Benchmark_Push_DifferentAggregate-%s-%s", cmdsKey, pusherKey), func(b *testing.B) {
b.StopTimer()
cleanupEventstore(clients[pusherKey])
ctx, cancel := context.WithCancel(context.Background())
b.StartTimer()
i := 0
b.RunParallel(func(p *testing.PB) {
for p.Next() {
i++
_, err := store.Push(ctx, store.Client().DB, commandCreator(strconv.Itoa(i))...)
if err != nil {
b.Error(err)
}
}
})
cancel()
})
}
}
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
new_db "github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/service" "github.com/zitadel/zitadel/internal/api/service"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
@@ -348,7 +349,7 @@ func (repo *testPusher) Health(ctx context.Context) error {
return nil return nil
} }
func (repo *testPusher) Push(_ context.Context, _ database.ContextQueryExecuter, commands ...Command) (events []Event, err error) { func (repo *testPusher) Push(_ context.Context, _ new_db.QueryExecutor, commands ...Command) (events []Event, err error) {
if len(repo.errs) != 0 { if len(repo.errs) != 0 {
err, repo.errs = repo.errs[0], repo.errs[1:] err, repo.errs = repo.errs[0], repo.errs[1:]
return nil, err return nil, err
@@ -14,7 +14,8 @@ import (
reflect "reflect" reflect "reflect"
decimal "github.com/shopspring/decimal" decimal "github.com/shopspring/decimal"
database "github.com/zitadel/zitadel/internal/database" database "github.com/zitadel/zitadel/backend/v3/storage/database"
database0 "github.com/zitadel/zitadel/internal/database"
eventstore "github.com/zitadel/zitadel/internal/eventstore" eventstore "github.com/zitadel/zitadel/internal/eventstore"
gomock "go.uber.org/mock/gomock" gomock "go.uber.org/mock/gomock"
) )
@@ -43,10 +44,10 @@ func (m *MockQuerier) EXPECT() *MockQuerierMockRecorder {
} }
// Client mocks base method. // Client mocks base method.
func (m *MockQuerier) Client() *database.DB { func (m *MockQuerier) Client() *database0.DB {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client") ret := m.ctrl.Call(m, "Client")
ret0, _ := ret[0].(*database.DB) ret0, _ := ret[0].(*database0.DB)
return ret0 return ret0
} }
@@ -137,20 +138,6 @@ func (m *MockPusher) EXPECT() *MockPusherMockRecorder {
return m.recorder return m.recorder
} }
// Client mocks base method.
func (m *MockPusher) Client() *database.DB {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client")
ret0, _ := ret[0].(*database.DB)
return ret0
}
// Client indicates an expected call of Client.
func (mr *MockPusherMockRecorder) Client() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockPusher)(nil).Client))
}
// Health mocks base method. // Health mocks base method.
func (m *MockPusher) Health(arg0 context.Context) error { func (m *MockPusher) Health(arg0 context.Context) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -166,7 +153,7 @@ func (mr *MockPusherMockRecorder) Health(arg0 any) *gomock.Call {
} }
// Push mocks base method. // Push mocks base method.
func (m *MockPusher) Push(arg0 context.Context, arg1 database.ContextQueryExecuter, arg2 ...eventstore.Command) ([]eventstore.Event, error) { func (m *MockPusher) Push(arg0 context.Context, arg1 database.QueryExecutor, arg2 ...eventstore.Command) ([]eventstore.Event, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []any{arg0, arg1} varargs := []any{arg0, arg1}
for _, a := range arg2 { for _, a := range arg2 {
@@ -15,6 +15,7 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
new_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
db_mock "github.com/zitadel/zitadel/internal/database/mock" db_mock "github.com/zitadel/zitadel/internal/database/mock"
"github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/database/postgres"
@@ -646,7 +647,7 @@ func Test_query_events_with_postgres(t *testing.T) {
pusher := new_es.NewEventstore(dbClient) pusher := new_es.NewEventstore(dbClient)
// setup initial data for query // setup initial data for query
if _, err := pusher.Push(context.Background(), dbClient.DB, tt.fields.existingEvents...); err != nil { if _, err := pusher.Push(context.Background(), new_sql.SQLPool(dbClient.DB), tt.fields.existingEvents...); err != nil {
t.Errorf("error in setup = %v", err) t.Errorf("error in setup = %v", err)
return return
} }
+12 -15
View File
@@ -13,6 +13,8 @@ import (
"github.com/jackc/pgx/v5/stdlib" "github.com/jackc/pgx/v5/stdlib"
"github.com/zitadel/logging" "github.com/zitadel/logging"
new_db "github.com/zitadel/zitadel/backend/v3/storage/database"
new_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/database/dialect" "github.com/zitadel/zitadel/internal/database/dialect"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
@@ -32,7 +34,7 @@ var (
) )
type Eventstore struct { type Eventstore struct {
client *database.DB client new_db.Pool
queue eventstore.ExecutionQueue queue eventstore.ExecutionQueue
} }
@@ -153,14 +155,9 @@ func RegisterEventstoreTypes(ctx context.Context, conn *pgx.Conn) error {
return nil return nil
} }
// Client implements the [eventstore.Pusher]
func (es *Eventstore) Client() *database.DB {
return es.client
}
func NewEventstore(client *database.DB, opts ...EventstoreOption) *Eventstore { func NewEventstore(client *database.DB, opts ...EventstoreOption) *Eventstore {
es := &Eventstore{ es := &Eventstore{
client: client, client: new_sql.SQLPool(client.DB),
} }
for _, opt := range opts { for _, opt := range opts {
opt(es) opt(es)
@@ -169,7 +166,7 @@ func NewEventstore(client *database.DB, opts ...EventstoreOption) *Eventstore {
} }
func (es *Eventstore) Health(ctx context.Context) error { func (es *Eventstore) Health(ctx context.Context) error {
return es.client.PingContext(ctx) return es.client.Ping(ctx)
} }
var errTypesNotFound = errors.New("types not found") var errTypesNotFound = errors.New("types not found")
@@ -188,24 +185,24 @@ func CheckExecutionPlan(ctx context.Context, conn *sql.Conn) error {
}) })
} }
func (es *Eventstore) pushTx(ctx context.Context, client database.ContextQueryExecuter) (tx database.Tx, deferrable func(err error) error, err error) { func (es *Eventstore) pushTx(ctx context.Context, client new_db.QueryExecutor) (tx new_db.Transaction, deferrable func(err error) error, err error) {
tx, ok := client.(database.Tx) tx, ok := client.(new_db.Transaction)
if ok { if ok {
return tx, nil, nil return tx, nil, nil
} }
beginner, ok := client.(database.Beginner) beginner, ok := client.(new_db.Beginner)
if !ok { if !ok {
beginner = es.client beginner = es.client
} }
tx, err = beginner.BeginTx(ctx, &sql.TxOptions{ tx, err = beginner.Begin(ctx, &new_db.TransactionOptions{
Isolation: sql.LevelReadCommitted, IsolationLevel: new_db.IsolationLevelReadCommitted,
ReadOnly: false, AccessMode: new_db.AccessModeReadWrite,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return tx, func(err error) error { return database.CloseTransaction(tx, err) }, nil return tx, func(err error) error { return tx.End(ctx, err) }, nil
} }
type EventstoreOption func(*Eventstore) type EventstoreOption func(*Eventstore)
+39 -45
View File
@@ -2,7 +2,6 @@ package eventstore
import ( import (
"context" "context"
"database/sql"
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"reflect" "reflect"
@@ -10,8 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
@@ -29,16 +28,12 @@ func (es *Eventstore) FillFields(ctx context.Context, events ...eventstore.FillF
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer span.End() defer span.End()
tx, err := es.client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) tx, err := es.client.Begin(ctx, &database.TransactionOptions{IsolationLevel: database.IsolationLevelReadCommitted})
if err != nil { if err != nil {
return err return err
} }
defer func() { defer func() {
if err != nil { err = tx.End(ctx, err)
_ = tx.Rollback()
return
}
err = tx.Commit()
}() }()
return handleFieldFillEvents(ctx, tx, events) return handleFieldFillEvents(ctx, tx, events)
@@ -52,40 +47,39 @@ func (es *Eventstore) Search(ctx context.Context, conditions ...map[eventstore.F
var builder strings.Builder var builder strings.Builder
args := buildSearchStatement(ctx, &builder, conditions...) args := buildSearchStatement(ctx, &builder, conditions...)
err = es.client.QueryContext( rows, err := es.client.Query(
ctx, ctx,
func(rows *sql.Rows) error {
for rows.Next() {
var (
res eventstore.SearchResult
value fieldValue
)
err = rows.Scan(
&res.Aggregate.InstanceID,
&res.Aggregate.ResourceOwner,
&res.Aggregate.Type,
&res.Aggregate.ID,
&res.Object.Type,
&res.Object.ID,
&res.Object.Revision,
&res.FieldName,
&value.value,
)
if err != nil {
return err
}
res.Value = &value
result = append(result, &res)
}
return nil
},
builder.String(), builder.String(),
args..., args...,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
for rows.Next() {
var (
res eventstore.SearchResult
value fieldValue
)
err = rows.Scan(
&res.Aggregate.InstanceID,
&res.Aggregate.ResourceOwner,
&res.Aggregate.Type,
&res.Aggregate.ID,
&res.Object.Type,
&res.Object.ID,
&res.Object.Revision,
&res.FieldName,
&value.value,
)
if err != nil {
return nil, err
}
res.Value = &value
result = append(result, &res)
}
return result, nil return result, nil
} }
@@ -143,7 +137,7 @@ func buildSearchCondition(builder *strings.Builder, index int, conditions map[ev
return args return args
} }
func (es *Eventstore) handleFieldCommands(ctx context.Context, tx database.Tx, commands []eventstore.Command) error { func (es *Eventstore) handleFieldCommands(ctx context.Context, tx database.Transaction, commands []eventstore.Command) error {
for _, command := range commands { for _, command := range commands {
if len(command.Fields()) > 0 { if len(command.Fields()) > 0 {
if err := handleFieldOperations(ctx, tx, command.Fields()); err != nil { if err := handleFieldOperations(ctx, tx, command.Fields()); err != nil {
@@ -154,7 +148,7 @@ func (es *Eventstore) handleFieldCommands(ctx context.Context, tx database.Tx, c
return nil return nil
} }
func handleFieldFillEvents(ctx context.Context, tx database.Tx, events []eventstore.FillFieldsEvent) error { func handleFieldFillEvents(ctx context.Context, tx database.Transaction, events []eventstore.FillFieldsEvent) error {
for _, event := range events { for _, event := range events {
if len(event.Fields()) == 0 { if len(event.Fields()) == 0 {
continue continue
@@ -166,7 +160,7 @@ func handleFieldFillEvents(ctx context.Context, tx database.Tx, events []eventst
return nil return nil
} }
func handleFieldOperations(ctx context.Context, tx database.Tx, operations []*eventstore.FieldOperation) error { func handleFieldOperations(ctx context.Context, tx database.Transaction, operations []*eventstore.FieldOperation) error {
for _, operation := range operations { for _, operation := range operations {
if operation.Set != nil { if operation.Set != nil {
if err := handleFieldSet(ctx, tx, operation.Set); err != nil { if err := handleFieldSet(ctx, tx, operation.Set); err != nil {
@@ -184,7 +178,7 @@ func handleFieldOperations(ctx context.Context, tx database.Tx, operations []*ev
return nil return nil
} }
func handleFieldSet(ctx context.Context, tx database.Tx, field *eventstore.Field) error { func handleFieldSet(ctx context.Context, tx database.Transaction, field *eventstore.Field) error {
if len(field.UpsertConflictFields) == 0 { if len(field.UpsertConflictFields) == 0 {
return handleSearchInsert(ctx, tx, field) return handleSearchInsert(ctx, tx, field)
} }
@@ -195,12 +189,12 @@ const (
insertField = `INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)` insertField = `INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`
) )
func handleSearchInsert(ctx context.Context, tx database.Tx, field *eventstore.Field) error { func handleSearchInsert(ctx context.Context, tx database.Transaction, field *eventstore.Field) error {
value, err := json.Marshal(field.Value.Value) value, err := json.Marshal(field.Value.Value)
if err != nil { if err != nil {
return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value") return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value")
} }
_, err = tx.ExecContext( _, err = tx.Exec(
ctx, ctx,
insertField, insertField,
@@ -224,13 +218,13 @@ const (
fieldsUpsertSuffix = ` RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)` fieldsUpsertSuffix = ` RETURNING * ) INSERT INTO eventstore.fields (instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, object_revision, field_name, value, value_must_be_unique, should_index) SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 WHERE NOT EXISTS (SELECT 1 FROM upsert)`
) )
func handleSearchUpsert(ctx context.Context, tx database.Tx, field *eventstore.Field) error { func handleSearchUpsert(ctx context.Context, tx database.Transaction, field *eventstore.Field) error {
value, err := json.Marshal(field.Value.Value) value, err := json.Marshal(field.Value.Value)
if err != nil { if err != nil {
return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value") return zerrors.ThrowInvalidArgument(err, "V3-fcrW1", "unable to marshal field value")
} }
_, err = tx.ExecContext( _, err = tx.Exec(
ctx, ctx,
writeUpsertField(field.UpsertConflictFields), writeUpsertField(field.UpsertConflictFields),
@@ -270,12 +264,12 @@ func writeUpsertField(fields []eventstore.FieldType) string {
const removeSearch = `DELETE FROM eventstore.fields WHERE ` const removeSearch = `DELETE FROM eventstore.fields WHERE `
func handleSearchDelete(ctx context.Context, tx database.Tx, clauses map[eventstore.FieldType]any) error { func handleSearchDelete(ctx context.Context, tx database.Transaction, clauses map[eventstore.FieldType]any) error {
if len(clauses) == 0 { if len(clauses) == 0 {
return zerrors.ThrowInvalidArgument(nil, "V3-oqlBZ", "no conditions") return zerrors.ThrowInvalidArgument(nil, "V3-oqlBZ", "no conditions")
} }
stmt, args := writeDeleteField(clauses) stmt, args := writeDeleteField(clauses)
_, err := tx.ExecContext(ctx, stmt, args...) _, err := tx.Exec(ctx, stmt, args...)
return err return err
} }
+12 -23
View File
@@ -2,22 +2,22 @@ package eventstore
import ( import (
"context" "context"
"database/sql"
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/riverqueue/river" "github.com/riverqueue/river"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/queue" "github.com/zitadel/zitadel/internal/queue"
exec_repo "github.com/zitadel/zitadel/internal/repository/execution" exec_repo "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
) )
func (es *Eventstore) Push(ctx context.Context, client database.ContextQueryExecuter, commands ...eventstore.Command) (events []eventstore.Event, err error) { func (es *Eventstore) Push(ctx context.Context, client database.QueryExecutor, commands ...eventstore.Command) (events []eventstore.Event, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -29,20 +29,9 @@ func (es *Eventstore) Push(ctx context.Context, client database.ContextQueryExec
return events, err return events, err
} }
func (es *Eventstore) writeCommands(ctx context.Context, client database.ContextQueryExecuter, commands []eventstore.Command) (_ []eventstore.Event, err error) { func (es *Eventstore) writeCommands(ctx context.Context, client database.QueryExecutor, commands []eventstore.Command) (_ []eventstore.Event, err error) {
var conn *sql.Conn if client == nil {
switch c := client.(type) { client = es.client
case database.Client:
conn, err = c.Conn(ctx)
case nil:
conn, err = es.client.Conn(ctx)
client = conn
}
if err != nil {
return nil, err
}
if conn != nil {
defer conn.Close()
} }
tx, close, err := es.pushTx(ctx, client) tx, close, err := es.pushTx(ctx, client)
@@ -56,7 +45,7 @@ func (es *Eventstore) writeCommands(ctx context.Context, client database.Context
} }
// lock the instance for reading events if await events is set for the duration of the transaction. // lock the instance for reading events if await events is set for the duration of the transaction.
_, err = tx.ExecContext(ctx, "SELECT pg_advisory_xact_lock_shared('eventstore.events2'::REGCLASS::OID::INTEGER, hashtext($1))", authz.GetInstance(ctx).InstanceID()) _, err = tx.Exec(ctx, "SELECT pg_advisory_xact_lock_shared('eventstore.events2'::REGCLASS::OID::INTEGER, hashtext($1))", authz.GetInstance(ctx).InstanceID())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -82,7 +71,7 @@ func (es *Eventstore) writeCommands(ctx context.Context, client database.Context
return events, nil return events, nil
} }
func writeEvents(ctx context.Context, tx database.Tx, commands []eventstore.Command) (_ []eventstore.Event, err error) { func writeEvents(ctx context.Context, tx database.Transaction, commands []eventstore.Command) (_ []eventstore.Event, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -91,7 +80,7 @@ func writeEvents(ctx context.Context, tx database.Tx, commands []eventstore.Comm
return nil, err return nil, err
} }
rows, err := tx.QueryContext(ctx, `select owner, created_at, "sequence", position from eventstore.push($1::eventstore.command[])`, cmds) rows, err := tx.Query(ctx, `select owner, created_at, "sequence", position from eventstore.push($1::eventstore.command[])`, cmds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -110,12 +99,12 @@ func writeEvents(ctx context.Context, tx database.Tx, commands []eventstore.Comm
return events, nil return events, nil
} }
func (es *Eventstore) queueExecutions(ctx context.Context, tx database.Tx, events []eventstore.Event) error { func (es *Eventstore) queueExecutions(ctx context.Context, tx database.Transaction, events []eventstore.Event) error {
if es.queue == nil { if es.queue == nil {
return nil return nil
} }
sqlTx, ok := tx.(*sql.Tx) sqlTx, ok := tx.(*sql.Transaction)
if !ok { if !ok {
types := make([]string, len(events)) types := make([]string, len(events))
for i, event := range events { for i, event := range events {
@@ -132,7 +121,7 @@ func (es *Eventstore) queueExecutions(ctx context.Context, tx database.Tx, event
return nil return nil
} }
return es.queue.InsertManyFastTx( return es.queue.InsertManyFastTx(
ctx, sqlTx, jobArgs, ctx, sqlTx.Tx, jobArgs,
queue.WithQueueName(exec_repo.QueueName), queue.WithQueueName(exec_repo.QueueName),
) )
} }
+23 -8
View File
@@ -2,7 +2,6 @@ package eventstore
import ( import (
"context" "context"
"database/sql"
_ "embed" _ "embed"
"testing" "testing"
@@ -11,6 +10,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
new_db "github.com/zitadel/zitadel/backend/v3/storage/database"
new_pg "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/database/postgres" "github.com/zitadel/zitadel/internal/database/postgres"
@@ -268,7 +270,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
tx database.Tx tx new_db.Transaction
events []eventstore.Event events []eventstore.Event
} }
tests := []struct { tests := []struct {
@@ -278,7 +280,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
name: "incorrect Tx type, noop", name: "no Tx type, noop",
queue: func(t *testing.T) eventstore.ExecutionQueue { queue: func(t *testing.T) eventstore.ExecutionQueue {
mQueue := mock.NewMockExecutionQueue(gomock.NewController(t)) mQueue := mock.NewMockExecutionQueue(gomock.NewController(t))
return mQueue return mQueue
@@ -290,6 +292,19 @@ func TestEventstore_queueExecutions(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "incorrect Tx type, noop",
queue: func(t *testing.T) eventstore.ExecutionQueue {
mQueue := mock.NewMockExecutionQueue(gomock.NewController(t))
return mQueue
},
args: args{
ctx: context.Background(),
tx: new_pg.PGxTx(nil),
events: events,
},
wantErr: false,
},
{ {
name: "no events", name: "no events",
queue: func(t *testing.T) eventstore.ExecutionQueue { queue: func(t *testing.T) eventstore.ExecutionQueue {
@@ -298,7 +313,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
tx: &sql.Tx{}, tx: sql.SQLTx(nil),
events: []eventstore.Event{}, events: []eventstore.Event{},
}, },
wantErr: false, wantErr: false,
@@ -311,7 +326,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
tx: &sql.Tx{}, tx: sql.SQLTx(nil),
events: events, events: events,
}, },
wantErr: false, wantErr: false,
@@ -331,7 +346,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
}, },
}), }),
), ),
tx: &sql.Tx{}, tx: sql.SQLTx(nil),
events: events, events: events,
}, },
wantErr: false, wantErr: false,
@@ -360,7 +375,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
{ExecutionID: "event"}, {ExecutionID: "event"},
}), }),
), ),
tx: &sql.Tx{}, tx: sql.SQLTx(nil),
events: events, events: events,
}, },
wantErr: false, wantErr: false,
@@ -389,7 +404,7 @@ func TestEventstore_queueExecutions(t *testing.T) {
{ExecutionID: "event/ex.removed"}, {ExecutionID: "event/ex.removed"},
}), }),
), ),
tx: &sql.Tx{}, tx: sql.SQLTx(nil),
events: events, events: events,
}, },
wantErr: false, wantErr: false,
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
@@ -35,7 +35,7 @@ var (
// pushWithoutFunc implements pushing events before setup step 39 was introduced. // pushWithoutFunc implements pushing events before setup step 39 was introduced.
// TODO: remove with v3 // TODO: remove with v3
func (es *Eventstore) pushWithoutFunc(ctx context.Context, client database.ContextQueryExecuter, commands ...eventstore.Command) (events []eventstore.Event, err error) { func (es *Eventstore) pushWithoutFunc(ctx context.Context, client database.QueryExecutor, commands ...eventstore.Command) (events []eventstore.Event, err error) {
tx, closeTx, err := es.pushTx(ctx, client) tx, closeTx, err := es.pushTx(ctx, client)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -69,13 +69,13 @@ func (es *Eventstore) pushWithoutFunc(ctx context.Context, client database.Conte
return events, nil return events, nil
} }
func (es *Eventstore) writeEventsOld(ctx context.Context, tx database.Tx, sequences []*latestSequence, commands []eventstore.Command) ([]eventstore.Event, error) { func (es *Eventstore) writeEventsOld(ctx context.Context, tx database.Transaction, sequences []*latestSequence, commands []eventstore.Command) ([]eventstore.Event, error) {
events, placeholders, args, err := mapCommands(commands, sequences) events, placeholders, args, err := mapCommands(commands, sequences)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rows, err := tx.QueryContext(ctx, fmt.Sprintf(pushStmt, strings.Join(placeholders, ", ")), args...) rows, err := tx.Query(ctx, fmt.Sprintf(pushStmt, strings.Join(placeholders, ", ")), args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+4 -5
View File
@@ -2,15 +2,14 @@ package eventstore
import ( import (
"context" "context"
"database/sql"
_ "embed" _ "embed"
"fmt" "fmt"
"strings" "strings"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
@@ -23,11 +22,11 @@ type latestSequence struct {
//go:embed sequences_query.sql //go:embed sequences_query.sql
var latestSequencesStmt string var latestSequencesStmt string
func latestSequences(ctx context.Context, tx database.Tx, commands []eventstore.Command) ([]*latestSequence, error) { func latestSequences(ctx context.Context, tx database.Transaction, commands []eventstore.Command) ([]*latestSequence, error) {
sequences := commandsToSequences(ctx, commands) sequences := commandsToSequences(ctx, commands)
conditions, args := sequencesToSql(sequences) conditions, args := sequencesToSql(sequences)
rows, err := tx.QueryContext(ctx, fmt.Sprintf(latestSequencesStmt, strings.Join(conditions, " UNION ALL ")), args...) rows, err := tx.Query(ctx, fmt.Sprintf(latestSequencesStmt, strings.Join(conditions, " UNION ALL ")), args...)
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "V3-5jU5z", "Errors.Internal") return nil, zerrors.ThrowInternal(err, "V3-5jU5z", "Errors.Internal")
} }
@@ -104,7 +103,7 @@ func sequencesToSql(sequences []*latestSequence) (conditions []string, args []an
return conditions, args return conditions, args
} }
func scanToSequence(rows *sql.Rows, sequences []*latestSequence) error { func scanToSequence(rows database.Rows, sequences []*latestSequence) error {
var aggregateType eventstore.AggregateType var aggregateType eventstore.AggregateType
var aggregateID, instanceID string var aggregateID, instanceID string
var currentSequence uint64 var currentSequence uint64
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
@@ -25,7 +25,7 @@ var (
addConstraintStmt string addConstraintStmt string
) )
func handleUniqueConstraints(ctx context.Context, tx database.Tx, commands []eventstore.Command) (err error) { func handleUniqueConstraints(ctx context.Context, tx database.Transaction, commands []eventstore.Command) (err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -62,7 +62,7 @@ func handleUniqueConstraints(ctx context.Context, tx database.Tx, commands []eve
} }
if len(deletePlaceholders) > 0 { if len(deletePlaceholders) > 0 {
_, err := tx.ExecContext(ctx, fmt.Sprintf(deleteConstraintStmt, strings.Join(deletePlaceholders, " OR ")), deleteArgs...) _, err := tx.Exec(ctx, fmt.Sprintf(deleteConstraintStmt, strings.Join(deletePlaceholders, " OR ")), deleteArgs...)
if err != nil { if err != nil {
logging.WithError(err).Warn("delete unique constraint failed") logging.WithError(err).Warn("delete unique constraint failed")
errMessage := "Errors.Internal" errMessage := "Errors.Internal"
@@ -73,7 +73,7 @@ func handleUniqueConstraints(ctx context.Context, tx database.Tx, commands []eve
} }
} }
if len(addPlaceholders) > 0 { if len(addPlaceholders) > 0 {
_, err := tx.ExecContext(ctx, fmt.Sprintf(addConstraintStmt, strings.Join(addPlaceholders, ", ")), addArgs...) _, err := tx.Exec(ctx, fmt.Sprintf(addConstraintStmt, strings.Join(addPlaceholders, ", ")), addArgs...)
if err != nil { if err != nil {
logging.WithError(err).Warn("add unique constraint failed") logging.WithError(err).Warn("add unique constraint failed")
errMessage := "Errors.Internal" errMessage := "Errors.Internal"