fix(eventstore): allow overwriting resource owner of events (#12261)

# Which Problems Are Solved

- The eventstore did not support intentionally overwriting the resource
owner when creating events for aggregates that may be reused across
owners.
- Resource owner handling was implicit and could not be controlled per
command/event type.
- We needed a safe way to distinguish between:
  - keeping the existing aggregate owner, and
  - explicitly setting a new owner for specific create-like events.

# How the Problems Are Solved

- Introduced a new eventstore command type with an explicit
enforce_owner flag.
- Updated eventstore.commands_to_events and eventstore.push so owner
assignment is now explicit:
  - if enforce_owner is true, the command owner is written
- if enforce_owner is false, the existing aggregate owner is retained
when present
- Added EnforceResourceOwnerCommand and wiring so command types can opt
in to enforced owner behavior.
- Wired the new behavior through the v3 eventstore push path, including
compatibility fallback for older command type mapping.
- Added migration/setup changes to register and use the new command type
and SQL functions.
- Added and updated tests for owner overwrite and aggregate ID reuse
scenarios.

# Additional Changes

- Added small migration/setup robustness improvements related to
eventstore setup ordering and helper reuse.
- Added focused test coverage for enforced owner behavior and
sequencing.
- Events that currently allow owner changes (implement
EnforceResourceOwner) are:
  - AddedEvent (action)
  - GroupAddedEvent
  - StartedEvent (idp intent)
  - ProjectAddedEvent
  - HumanAddedEvent
  - HumanRegisteredEvent
  - MachineAddedEvent
  - CreatedEvent (schema user)

# Additional Context

- Follow-up for eventstore owner-handling correctness in create flows
and aggregate ID reuse cases.
- No additional issue link was attached for this change.

---------

Co-authored-by: abhishek kumar gupta <[email protected]>
This commit is contained in:
Silvan
2026-06-15 11:24:37 +02:00
committed by GitHub
co-authored by abhishek kumar gupta
parent 4459a1380c
commit 6082e59d47
66 changed files with 1603 additions and 288 deletions
+112 -71
View File
@@ -23,7 +23,7 @@ CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, agg
-- represents an event to be created.
DO $$ BEGIN
CREATE TYPE eventstore.command AS (
CREATE TYPE eventstore.command2 AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
@@ -32,90 +32,131 @@ DO $$ BEGIN
, payload JSONB
, creator TEXT
, owner TEXT
, enforce_owner BOOLEAN
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type AS event_type
, cs.sequence + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
, c.revision
, NOW() AS created_at
, c.payload
, c.creator
, cs.owner
, EXTRACT(EPOCH FROM NOW()) AS position
, c.in_tx_order
FROM (
SELECT
DO $$ BEGIN
CREATE TYPE eventstore.latest_command AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, owner TEXT
, sequence BIGINT
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
RETURNS SETOF eventstore.events2
LANGUAGE 'plpgsql'
STABLE PARALLEL SAFE
ROWS 10
AS $$
DECLARE
_latest_events eventstore.latest_command[];
command eventstore.command2;
last_owner TEXT;
i INTEGER;
BEGIN
SELECT array_agg(res) INTO _latest_events
FROM (
SELECT
ROW(
c.instance_id,
c.aggregate_type,
c.aggregate_id,
COALESCE(e.owner, c.owner),
COALESCE(e.sequence, 0)
)::eventstore.latest_command AS res
FROM (
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.owner,
c.ordinality
FROM (
SELECT *
FROM UNNEST(commands) WITH ORDINALITY AS c
) AS c
ORDER BY
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.ordinality
) AS c
LEFT JOIN LATERAL (
SELECT
e.owner,
e.sequence
FROM eventstore.events2 AS e
WHERE e.instance_id = c.instance_id
AND e.aggregate_type = c.aggregate_type
AND e.aggregate_id = c.aggregate_id
ORDER BY e.sequence DESC
LIMIT 1
) AS e ON TRUE
) AS latest;
FOR command IN SELECT * FROM UNNEST(commands) LOOP
IF NOT command.enforce_owner THEN
SELECT owner INTO command.owner
FROM UNNEST(_latest_events) AS e
WHERE e.instance_id = command.instance_id
AND e.aggregate_type = command.aggregate_type
AND e.aggregate_id = command.aggregate_id;
ELSE
FOR i IN 1..array_length(_latest_events, 1) LOOP
IF _latest_events[i].instance_id = command.instance_id
AND _latest_events[i].aggregate_type = command.aggregate_type
AND _latest_events[i].aggregate_id = command.aggregate_id
THEN
_latest_events[i].owner := command.owner;
EXIT;
END IF;
END LOOP;
END IF;
END LOOP;
RETURN QUERY SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type
, c.command_type AS event_type
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
, c.revision
, NOW() AS created_at
, c.payload
, c.creator
, c.owner
, ROW_NUMBER() OVER () AS in_tx_order
FROM
UNNEST(commands) AS c
) AS c
JOIN (
SELECT
cmds.instance_id
, cmds.aggregate_type
, cmds.aggregate_id
, CASE WHEN (e.owner IS NOT NULL OR e.owner <> '') THEN e.owner ELSE command_owners.owner END AS owner
, COALESCE(MAX(e.sequence), 0) AS sequence
, CASE WHEN c.enforce_owner
THEN c.owner
ELSE COALESCE(e.owner, c.owner)
END AS owner
, EXTRACT(EPOCH FROM NOW()) AS position
, c.in_tx_order
FROM (
SELECT DISTINCT
instance_id
, aggregate_type
, aggregate_id
, owner
FROM UNNEST(commands)
) AS cmds
LEFT JOIN eventstore.events2 AS e
ON cmds.instance_id = e.instance_id
AND cmds.aggregate_type = e.aggregate_type
AND cmds.aggregate_id = e.aggregate_id
JOIN (
SELECT
DISTINCT ON (
instance_id
, aggregate_type
, aggregate_id
)
instance_id
, aggregate_type
, aggregate_id
, owner
FROM
UNNEST(commands)
) AS command_owners ON
cmds.instance_id = command_owners.instance_id
AND cmds.aggregate_type = command_owners.aggregate_type
AND cmds.aggregate_id = command_owners.aggregate_id
GROUP BY
cmds.instance_id
, cmds.aggregate_type
, cmds.aggregate_id
, 4 -- owner
) AS cs
ON c.instance_id = cs.instance_id
AND c.aggregate_type = cs.aggregate_type
AND c.aggregate_id = cs.aggregate_id
ORDER BY
in_tx_order;
$$ LANGUAGE SQL;
SELECT c.*, CAST(ROW_NUMBER() OVER () AS INTEGER) AS in_tx_order
FROM UNNEST(commands) AS c
) AS c
LEFT JOIN UNNEST(_latest_events) AS e
ON c.instance_id = e.instance_id
AND c.aggregate_type = e.aggregate_type
AND c.aggregate_id = e.aggregate_id
ORDER BY
in_tx_order;
END;
$$;
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
INSERT INTO eventstore.events2
SELECT * FROM eventstore.commands_to_events(commands)
ORDER BY in_tx_order
RETURNING *
$$ LANGUAGE SQL;
+2 -1
View File
@@ -1,6 +1,6 @@
-- represents an event to be created.
DO $$ BEGIN
CREATE TYPE eventstore.command AS (
CREATE TYPE eventstore.command2 AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
@@ -9,6 +9,7 @@ DO $$ BEGIN
, payload JSONB
, creator TEXT
, owner TEXT
, enforce_owner BOOLEAN
);
EXCEPTION
WHEN duplicate_object THEN null;
+103 -75
View File
@@ -1,92 +1,120 @@
CREATE OR REPLACE FUNCTION eventstore.latest_aggregate_state(
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, sequence OUT BIGINT
, owner OUT TEXT
)
LANGUAGE 'plpgsql'
STABLE PARALLEL SAFE
AS $$
BEGIN
SELECT
COALESCE(e.sequence, 0) AS sequence
, e.owner
INTO
sequence
, owner
FROM
eventstore.events2 e
WHERE
e.instance_id = $1
AND e.aggregate_type = $2
AND e.aggregate_id = $3
ORDER BY
e.sequence DESC
LIMIT 1;
DO $$ BEGIN
CREATE TYPE eventstore.latest_command AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, owner TEXT
, sequence BIGINT
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
RETURN;
END;
$$;
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
RETURNS SETOF eventstore.events2
LANGUAGE 'plpgsql'
STABLE PARALLEL SAFE
ROWS 10
AS $$
DECLARE
"aggregate" RECORD;
current_sequence BIGINT;
current_owner TEXT;
created_at TIMESTAMPTZ;
_latest_events eventstore.latest_command[];
command eventstore.command2;
last_owner TEXT;
i INTEGER;
BEGIN
created_at := statement_timestamp();
FOR "aggregate" IN
SELECT DISTINCT
instance_id
, aggregate_type
, aggregate_id
FROM UNNEST(commands)
LOOP
SELECT
*
INTO
current_sequence
, current_owner
FROM eventstore.latest_aggregate_state(
"aggregate".instance_id
, "aggregate".aggregate_type
, "aggregate".aggregate_id
);
RETURN QUERY
SELECT array_agg(res) INTO _latest_events
FROM (
SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type -- AS event_type
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
, c.revision
, created_at
, c.payload
, c.creator
, COALESCE(current_owner, c.owner) -- AS owner
, EXTRACT(EPOCH FROM created_at) -- AS position
, c.ordinality::{{ .InTxOrderType }} -- AS in_tx_order
FROM
UNNEST(commands) WITH ORDINALITY AS c
WHERE
c.instance_id = aggregate.instance_id
AND c.aggregate_type = aggregate.aggregate_type
AND c.aggregate_id = aggregate.aggregate_id;
ROW(
c.instance_id,
c.aggregate_type,
c.aggregate_id,
COALESCE(e.owner, c.owner),
COALESCE(e.sequence, 0)
)::eventstore.latest_command AS res
FROM (
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.owner,
c.ordinality
FROM (
SELECT *
FROM UNNEST(commands) WITH ORDINALITY AS c
) AS c
ORDER BY
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.ordinality
) AS c
LEFT JOIN LATERAL (
SELECT
e.owner,
e.sequence
FROM eventstore.events2 AS e
WHERE e.instance_id = c.instance_id
AND e.aggregate_type = c.aggregate_type
AND e.aggregate_id = c.aggregate_id
ORDER BY e.sequence DESC
LIMIT 1
) AS e ON TRUE
) AS latest;
FOR command IN SELECT * FROM UNNEST(commands) LOOP
IF NOT command.enforce_owner THEN
SELECT owner INTO command.owner
FROM UNNEST(_latest_events) AS e
WHERE e.instance_id = command.instance_id
AND e.aggregate_type = command.aggregate_type
AND e.aggregate_id = command.aggregate_id;
ELSE
FOR i IN 1..array_length(_latest_events, 1) LOOP
IF _latest_events[i].instance_id = command.instance_id
AND _latest_events[i].aggregate_type = command.aggregate_type
AND _latest_events[i].aggregate_id = command.aggregate_id
THEN
_latest_events[i].owner := command.owner;
EXIT;
END IF;
END LOOP;
END IF;
END LOOP;
RETURN;
RETURN QUERY SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type AS event_type
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.in_tx_order) AS sequence
, c.revision
, NOW() AS created_at
, c.payload
, c.creator
, CASE WHEN c.enforce_owner
THEN c.owner
ELSE COALESCE(e.owner, c.owner)
END AS owner
, EXTRACT(EPOCH FROM NOW()) AS position
, c.in_tx_order
FROM (
SELECT c.*, CAST(ROW_NUMBER() OVER () AS INTEGER) AS in_tx_order
FROM UNNEST(commands) AS c
) AS c
LEFT JOIN UNNEST(_latest_events) AS e
ON c.instance_id = e.instance_id
AND c.aggregate_type = e.aggregate_type
AND c.aggregate_id = e.aggregate_id
ORDER BY
in_tx_order;
END;
$$;
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
INSERT INTO eventstore.events2
SELECT * FROM eventstore.commands_to_events(commands)
ORDER BY in_tx_order
+3 -3
View File
@@ -20,7 +20,7 @@ type ChangePushPosition struct {
}
func (mig *ChangePushPosition) Execute(ctx context.Context, _ eventstore.Event) error {
inTxOrderType, err := mig.inTxOrderType(ctx)
inTxOrderType, err := inTxOrderType(ctx, mig.dbClient)
if err != nil {
return err
}
@@ -33,8 +33,8 @@ func (mig *ChangePushPosition) String() string {
return "64_change_push_position"
}
func (mig *ChangePushPosition) inTxOrderType(ctx context.Context) (typeName string, err error) {
err = mig.dbClient.QueryRowContext(ctx, func(row *sql.Row) error {
func inTxOrderType(ctx context.Context, client *database.DB) (typeName string, err error) {
err = client.QueryRowContext(ctx, func(row *sql.Row) error {
return row.Scan(&typeName)
}, `SELECT data_type FROM information_schema.columns WHERE table_schema = 'eventstore' AND table_name = 'events2' AND column_name = 'in_tx_order'`)
if err != nil {
+107 -43
View File
@@ -1,55 +1,119 @@
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command[])
DO $$ BEGIN
CREATE TYPE eventstore.latest_command AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, owner TEXT
, sequence BIGINT
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
RETURNS SETOF eventstore.events2
LANGUAGE 'plpgsql'
STABLE PARALLEL SAFE
ROWS 10
AS $$
DECLARE
"aggregate" RECORD;
current_sequence BIGINT;
current_owner TEXT;
created_at TIMESTAMPTZ;
_latest_events eventstore.latest_command[];
command eventstore.command2;
last_owner TEXT;
i INTEGER;
BEGIN
created_at := statement_timestamp();
FOR "aggregate" IN
SELECT DISTINCT
instance_id
, aggregate_type
, aggregate_id
FROM UNNEST(commands)
LOOP
SELECT
*
INTO
current_sequence
, current_owner
FROM eventstore.latest_aggregate_state(
"aggregate".instance_id
, "aggregate".aggregate_type
, "aggregate".aggregate_id
);
RETURN QUERY
SELECT array_agg(res) INTO _latest_events
FROM (
SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type -- AS event_type
, COALESCE(current_sequence, 0) + ROW_NUMBER() OVER () -- AS sequence
, c.revision
, created_at
, c.payload
, c.creator
, COALESCE(current_owner, c.owner) -- AS owner
, EXTRACT(EPOCH FROM created_at) -- AS position
, c.ordinality::%s -- AS in_tx_order
FROM
UNNEST(commands) WITH ORDINALITY AS c
WHERE
c.instance_id = aggregate.instance_id
AND c.aggregate_type = aggregate.aggregate_type
AND c.aggregate_id = aggregate.aggregate_id;
ROW(
c.instance_id,
c.aggregate_type,
c.aggregate_id,
COALESCE(e.owner, c.owner),
COALESCE(e.sequence, 0)
)::eventstore.latest_command AS res
FROM (
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.owner,
c.ordinality
FROM (
SELECT *
FROM UNNEST(commands) WITH ORDINALITY AS c
) AS c
ORDER BY
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.ordinality
) AS c
LEFT JOIN LATERAL (
SELECT
e.owner,
e.sequence
FROM eventstore.events2 AS e
WHERE e.instance_id = c.instance_id
AND e.aggregate_type = c.aggregate_type
AND e.aggregate_id = c.aggregate_id
ORDER BY e.sequence DESC
LIMIT 1
) AS e ON TRUE
) AS latest;
FOR command IN SELECT * FROM UNNEST(commands) LOOP
IF NOT command.enforce_owner THEN
SELECT owner INTO command.owner
FROM UNNEST(_latest_events) AS e
WHERE e.instance_id = command.instance_id
AND e.aggregate_type = command.aggregate_type
AND e.aggregate_id = command.aggregate_id;
ELSE
FOR i IN 1..array_length(_latest_events, 1) LOOP
IF _latest_events[i].instance_id = command.instance_id
AND _latest_events[i].aggregate_type = command.aggregate_type
AND _latest_events[i].aggregate_id = command.aggregate_id
THEN
_latest_events[i].owner := command.owner;
EXIT;
END IF;
END LOOP;
END IF;
END LOOP;
RETURN;
RETURN QUERY SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type AS event_type
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.ordinality) AS sequence
, c.revision
, NOW() AS created_at
, c.payload
, c.creator
, CASE WHEN c.enforce_owner
THEN c.owner
ELSE COALESCE(e.owner, c.owner)
END AS owner
, EXTRACT(EPOCH FROM NOW()) AS position
, c.ordinality::%s AS in_tx_order
FROM UNNEST(commands) WITH ORDINALITY AS c
LEFT JOIN unnest(_latest_events) AS e
ON c.instance_id = e.instance_id
AND c.aggregate_type = e.aggregate_type
AND c.aggregate_id = e.aggregate_id
ORDER BY
in_tx_order;
END;
$$;
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
INSERT INTO eventstore.events2
SELECT * FROM eventstore.commands_to_events(commands)
ORDER BY in_tx_order
RETURNING *
$$ LANGUAGE SQL;
+34
View File
@@ -0,0 +1,34 @@
package setup
import (
"context"
_ "embed"
"fmt"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 70.sql
addEventStoreCommandEnforceOwnerColumn string
)
type AddEventStoreCommandEnforceOwnerColumn struct {
dbClient *database.DB
}
func (mig *AddEventStoreCommandEnforceOwnerColumn) Execute(ctx context.Context, _ eventstore.Event) error {
inTxOrderType, err := inTxOrderType(ctx, mig.dbClient)
if err != nil {
return err
}
stmt := fmt.Sprintf(addEventStoreCommandEnforceOwnerColumn, inTxOrderType)
_, err = mig.dbClient.ExecContext(ctx, stmt)
return err
}
func (mig *AddEventStoreCommandEnforceOwnerColumn) String() string {
return "70_add_eventstore_command_type_enforce_owner_column"
}
+136
View File
@@ -0,0 +1,136 @@
-- represents an event to be created.
DO $$ BEGIN
CREATE TYPE eventstore.command2 AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, command_type TEXT
, revision INT2
, payload JSONB
, creator TEXT
, owner TEXT
, enforce_owner BOOLEAN
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
CREATE TYPE eventstore.latest_command AS (
instance_id TEXT
, aggregate_type TEXT
, aggregate_id TEXT
, owner TEXT
, sequence BIGINT
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE OR REPLACE FUNCTION eventstore.commands_to_events(commands eventstore.command2[])
RETURNS SETOF eventstore.events2
LANGUAGE 'plpgsql'
STABLE PARALLEL SAFE
ROWS 10
AS $$
DECLARE
_latest_events eventstore.latest_command[];
command eventstore.command2;
last_owner TEXT;
i INTEGER;
BEGIN
SELECT array_agg(res) INTO _latest_events
FROM (
SELECT
ROW(
c.instance_id,
c.aggregate_type,
c.aggregate_id,
COALESCE(e.owner, c.owner),
COALESCE(e.sequence, 0)
)::eventstore.latest_command AS res
FROM (
SELECT DISTINCT ON (c.instance_id, c.aggregate_type, c.aggregate_id)
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.owner,
c.ordinality
FROM (
SELECT *
FROM UNNEST(commands) WITH ORDINALITY AS c
) AS c
ORDER BY
c.instance_id,
c.aggregate_type,
c.aggregate_id,
c.ordinality
) AS c
LEFT JOIN LATERAL (
SELECT
e.owner,
e.sequence
FROM eventstore.events2 AS e
WHERE e.instance_id = c.instance_id
AND e.aggregate_type = c.aggregate_type
AND e.aggregate_id = c.aggregate_id
ORDER BY e.sequence DESC
LIMIT 1
) AS e ON TRUE
) AS latest;
FOR command IN SELECT * FROM UNNEST(commands) LOOP
IF NOT command.enforce_owner THEN
SELECT owner INTO command.owner
FROM UNNEST(_latest_events) AS e
WHERE e.instance_id = command.instance_id
AND e.aggregate_type = command.aggregate_type
AND e.aggregate_id = command.aggregate_id;
ELSE
FOR i IN 1..array_length(_latest_events, 1) LOOP
IF _latest_events[i].instance_id = command.instance_id
AND _latest_events[i].aggregate_type = command.aggregate_type
AND _latest_events[i].aggregate_id = command.aggregate_id
THEN
_latest_events[i].owner := command.owner;
EXIT;
END IF;
END LOOP;
END IF;
END LOOP;
RETURN QUERY SELECT
c.instance_id
, c.aggregate_type
, c.aggregate_id
, c.command_type AS event_type
, COALESCE(e.sequence, 0) + ROW_NUMBER() OVER (PARTITION BY c.instance_id, c.aggregate_type, c.aggregate_id ORDER BY c.ordinality) AS sequence
, c.revision
, NOW() AS created_at
, c.payload
, c.creator
, CASE WHEN c.enforce_owner
THEN c.owner
ELSE COALESCE(e.owner, c.owner)
END AS owner
, EXTRACT(EPOCH FROM NOW()) AS position
, c.ordinality::%s AS in_tx_order
FROM UNNEST(commands) WITH ORDINALITY AS c
LEFT JOIN unnest(_latest_events) AS e
ON c.instance_id = e.instance_id
AND c.aggregate_type = e.aggregate_type
AND c.aggregate_id = e.aggregate_id
ORDER BY
in_tx_order;
END;
$$;
CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$
INSERT INTO eventstore.events2
SELECT * FROM eventstore.commands_to_events(commands)
ORDER BY in_tx_order
RETURNING *
$$ LANGUAGE SQL;
+1
View File
@@ -185,6 +185,7 @@ type Steps struct {
s67SyncMemberRoleFields *SyncMemberRoleFields
s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn
s69CacheTablesLogged *CacheTablesLogged
s70AddEventStoreCommandEnforceOwner *AddEventStoreCommandEnforceOwnerColumn
RelationalTables *TransactionalTables
}
+2
View File
@@ -248,6 +248,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient}
steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient}
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
steps.s70AddEventStoreCommandEnforceOwner = &AddEventStoreCommandEnforceOwnerColumn{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
if err != nil {
@@ -257,6 +258,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
for _, step := range []migration.Migration{
steps.s14NewEventsTable,
steps.s40InitPushFunc,
steps.s70AddEventStoreCommandEnforceOwner,
steps.s1ProjectionTable,
steps.s2AssetsTable,
steps.s28AddFieldTable,