mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:47:32 +00:00
feat(eventstore): exclude aggregate IDs when event_type occurred (#8940)
# Which Problems Are Solved For truly event-based notification handler, we need to be able to filter out events of aggregates which are already handled. For example when an event like `notify.success` or `notify.failed` was created on an aggregate, we no longer require events from that aggregate ID. # How the Problems Are Solved Extend the query builder to use a `NOT IN` clause which excludes aggregate IDs when they have certain events for a certain aggregate type. For optimization and proper index usages, certain filters are inherited from the parent query, such as: - Instance ID - Instance IDs - Position offset This is a prettified query as used by the unit tests: ```sql SELECT created_at, event_type, "sequence", "position", payload, creator, "owner", instance_id, aggregate_type, aggregate_id, revision FROM eventstore.events2 WHERE instance_id = $1 AND aggregate_type = $2 AND event_type = $3 AND "position" > $4 AND aggregate_id NOT IN ( SELECT aggregate_id FROM eventstore.events2 WHERE aggregate_type = $5 AND event_type = ANY($6) AND instance_id = $7 AND "position" > $8 ) ORDER BY "position" DESC, in_tx_order DESC LIMIT $9 ``` I used this query to run it against the `oidc_session` aggregate looking for added events, excluding aggregates where a token was revoked, against a recent position. It fully used index scans: <details> ```json [ { "Plan": { "Node Type": "Index Scan", "Parallel Aware": false, "Async Capable": false, "Scan Direction": "Forward", "Index Name": "es_projection", "Relation Name": "events2", "Alias": "events2", "Actual Rows": 2, "Actual Loops": 1, "Index Cond": "((instance_id = '286399006995644420'::text) AND (aggregate_type = 'oidc_session'::text) AND (event_type = 'oidc_session.added'::text) AND (\"position\" > 1731582100.784168))", "Rows Removed by Index Recheck": 0, "Filter": "(NOT (hashed SubPlan 1))", "Rows Removed by Filter": 1, "Plans": [ { "Node Type": "Index Scan", "Parent Relationship": "SubPlan", "Subplan Name": "SubPlan 1", "Parallel Aware": false, "Async Capable": false, "Scan Direction": "Forward", "Index Name": "es_projection", "Relation Name": "events2", "Alias": "events2_1", "Actual Rows": 1, "Actual Loops": 1, "Index Cond": "((instance_id = '286399006995644420'::text) AND (aggregate_type = 'oidc_session'::text) AND (event_type = 'oidc_session.access_token.revoked'::text) AND (\"position\" > 1731582100.784168))", "Rows Removed by Index Recheck": 0 } ] }, "Triggers": [ ] } ] ``` </details> # Additional Changes - None # Additional Context - Related to https://github.com/zitadel/zitadel/issues/8931 --------- Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
This commit is contained in:
@@ -21,6 +21,7 @@ type SearchQueryBuilder struct {
|
||||
instanceIDs []string
|
||||
editorUser string
|
||||
queries []*SearchQuery
|
||||
excludeAggregateIDs *ExclusionQuery
|
||||
tx *sql.Tx
|
||||
lockRows bool
|
||||
lockOption LockOption
|
||||
@@ -68,6 +69,10 @@ func (b *SearchQueryBuilder) GetQueries() []*SearchQuery {
|
||||
return b.queries
|
||||
}
|
||||
|
||||
func (b *SearchQueryBuilder) GetExcludeAggregateIDs() *ExclusionQuery {
|
||||
return b.excludeAggregateIDs
|
||||
}
|
||||
|
||||
func (b *SearchQueryBuilder) GetTx() *sql.Tx {
|
||||
return b.tx
|
||||
}
|
||||
@@ -136,6 +141,20 @@ func (q SearchQuery) GetPositionAfter() float64 {
|
||||
return q.positionAfter
|
||||
}
|
||||
|
||||
type ExclusionQuery struct {
|
||||
builder *SearchQueryBuilder
|
||||
aggregateTypes []AggregateType
|
||||
eventTypes []EventType
|
||||
}
|
||||
|
||||
func (q ExclusionQuery) GetAggregateTypes() []AggregateType {
|
||||
return q.aggregateTypes
|
||||
}
|
||||
|
||||
func (q ExclusionQuery) GetEventTypes() []EventType {
|
||||
return q.eventTypes
|
||||
}
|
||||
|
||||
// Columns defines which fields of the event are needed for the query
|
||||
type Columns int8
|
||||
|
||||
@@ -346,6 +365,16 @@ func (builder *SearchQueryBuilder) AddQuery() *SearchQuery {
|
||||
return query
|
||||
}
|
||||
|
||||
// ExcludeAggregateIDs excludes events from the aggregate IDs returned by the [ExclusionQuery].
|
||||
// There can be only 1 exclusion query. Subsequent calls overwrite previous definitions.
|
||||
func (builder *SearchQueryBuilder) ExcludeAggregateIDs() *ExclusionQuery {
|
||||
query := &ExclusionQuery{
|
||||
builder: builder,
|
||||
}
|
||||
builder.excludeAggregateIDs = query
|
||||
return query
|
||||
}
|
||||
|
||||
// Or creates a new sub query on the search query builder
|
||||
func (query SearchQuery) Or() *SearchQuery {
|
||||
return query.builder.AddQuery()
|
||||
@@ -398,3 +427,20 @@ func (query *SearchQuery) matches(command Command) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AggregateTypes filters for events with the given aggregate types
|
||||
func (query *ExclusionQuery) AggregateTypes(types ...AggregateType) *ExclusionQuery {
|
||||
query.aggregateTypes = types
|
||||
return query
|
||||
}
|
||||
|
||||
// EventTypes filters for events with the given event types
|
||||
func (query *ExclusionQuery) EventTypes(types ...EventType) *ExclusionQuery {
|
||||
query.eventTypes = types
|
||||
return query
|
||||
}
|
||||
|
||||
// Builder returns the SearchQueryBuilder of the sub query
|
||||
func (query *ExclusionQuery) Builder() *SearchQueryBuilder {
|
||||
return query.builder
|
||||
}
|
||||
|
Reference in New Issue
Block a user