zitadel/internal/eventstore/v2/repository/sql/crdb.go

305 lines
7.9 KiB
Go
Raw Normal View History

2020-10-05 17:09:26 +00:00
package sql
import (
"context"
"database/sql"
"errors"
2020-10-05 17:09:26 +00:00
"regexp"
"strconv"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
2020-10-05 17:09:26 +00:00
"github.com/caos/zitadel/internal/eventstore/v2/repository"
"github.com/cockroachdb/cockroach-go/v2/crdb"
//sql import for cockroach
_ "github.com/lib/pq"
)
const (
crdbInsert = "WITH input_event ( " +
" event_type, " +
" aggregate_type, " +
" aggregate_id, " +
" aggregate_version, " +
" creation_date, " +
" event_data, " +
" editor_user, " +
" editor_service, " +
" resource_owner, " +
" previous_sequence, " +
" check_previous, " +
// variables below are calculated
2020-10-06 18:20:23 +00:00
" max_event_seq " +
") AS ( " +
" ( " +
//the following select will return no row if no previous event defined
" SELECT " +
" $1::VARCHAR, " +
" $2::VARCHAR, " +
" $3::VARCHAR, " +
" $4::VARCHAR, " +
" COALESCE($5::TIMESTAMPTZ, NOW()), " +
" $6::JSONB, " +
" $7::VARCHAR, " +
" $8::VARCHAR, " +
" resource_owner, " +
" $10::BIGINT, " +
" $11::BOOLEAN," +
" MAX(event_sequence) AS max_event_seq " +
" FROM eventstore.events " +
" WHERE " +
" aggregate_type = $2::VARCHAR " +
" AND aggregate_id = $3::VARCHAR " +
" GROUP BY resource_owner " +
2020-11-05 12:24:37 +00:00
" ) UNION (" +
// if no previous event we use the given data
" VALUES (" +
" $1::VARCHAR, " +
" $2::VARCHAR, " +
" $3::VARCHAR, " +
" $4::VARCHAR, " +
" COALESCE($5::TIMESTAMPTZ, NOW()), " +
" $6::JSONB, " +
" $7::VARCHAR, " +
" $8::VARCHAR, " +
" $9::VARCHAR, " +
" $10::BIGINT, " +
" $11::BOOLEAN, " +
" NULL::BIGINT " +
" ) " +
" ) " +
// ensure only 1 row in input_event
" LIMIT 1 " +
") " +
"INSERT INTO eventstore.events " +
" ( " +
" event_type, " +
" aggregate_type," +
" aggregate_id, " +
" aggregate_version, " +
" creation_date, " +
" event_data, " +
" editor_user, " +
" editor_service, " +
" resource_owner, " +
" previous_sequence " +
" ) " +
" ( " +
" SELECT " +
" event_type, " +
" aggregate_type," +
" aggregate_id, " +
" aggregate_version, " +
" COALESCE(creation_date, NOW()), " +
" event_data, " +
" editor_user, " +
" editor_service, " +
" resource_owner, " +
" ( " +
" SELECT " +
" CASE " +
2020-10-06 18:20:23 +00:00
" WHEN NOT check_previous " +
" THEN NULL " +
" ELSE previous_sequence " +
" END" +
" ) " +
" FROM input_event " +
2020-10-06 18:20:23 +00:00
" WHERE 1 = " +
" CASE " +
2020-10-06 18:20:23 +00:00
" WHEN NOT check_previous " +
" THEN 1 " +
" ELSE ( " +
" SELECT 1 FROM input_event " +
2020-10-06 18:20:23 +00:00
" WHERE (max_event_seq IS NULL AND previous_sequence IS NULL) OR (max_event_seq IS NOT NULL AND max_event_seq = previous_sequence) " +
" ) " +
" END " +
" ) " +
"RETURNING id, event_sequence, previous_sequence, creation_date, resource_owner "
)
type CRDB struct {
2020-10-05 17:09:26 +00:00
client *sql.DB
}
2020-10-23 14:16:46 +00:00
func NewCRDB(client *sql.DB) *CRDB {
return &CRDB{client}
}
2020-10-05 17:09:26 +00:00
func (db *CRDB) Health(ctx context.Context) error { return db.client.Ping() }
// Push adds all events to the eventstreams of the aggregates.
// This call is transaction save. The transaction will be rolled back if one event fails
2020-10-05 17:09:26 +00:00
func (db *CRDB) Push(ctx context.Context, events ...*repository.Event) error {
err := crdb.ExecuteTx(ctx, db.client, nil, func(tx *sql.Tx) error {
stmt, err := tx.PrepareContext(ctx, crdbInsert)
if err != nil {
logging.Log("SQL-3to5p").WithError(err).Warn("prepare failed")
return caos_errs.ThrowInternal(err, "SQL-OdXRE", "prepare failed")
}
2020-10-06 18:20:23 +00:00
2020-10-19 07:53:32 +00:00
for _, event := range events {
2020-10-19 11:58:59 +00:00
previousSequence := Sequence(event.PreviousSequence)
if event.PreviousEvent != nil {
if event.PreviousEvent.AggregateType != event.AggregateType || event.PreviousEvent.AggregateID != event.AggregateID {
return caos_errs.ThrowPreconditionFailed(nil, "SQL-J55uR", "aggregate of linked events unequal")
2020-10-19 07:53:32 +00:00
}
2020-10-19 11:58:59 +00:00
previousSequence = Sequence(event.PreviousEvent.Sequence)
}
err = stmt.QueryRowContext(ctx,
event.Type,
event.AggregateType,
event.AggregateID,
event.Version,
&sql.NullTime{
Time: event.CreationDate,
Valid: !event.CreationDate.IsZero(),
},
Data(event.Data),
event.EditorUser,
event.EditorService,
event.ResourceOwner,
previousSequence,
event.CheckPreviousSequence,
).Scan(&event.ID, &event.Sequence, &previousSequence, &event.CreationDate, &event.ResourceOwner)
2020-10-19 11:58:59 +00:00
event.PreviousSequence = uint64(previousSequence)
2020-10-19 11:58:59 +00:00
if err != nil {
logging.LogWithFields("SQL-IP3js",
"aggregate", event.AggregateType,
"aggregateId", event.AggregateID,
"aggregateType", event.AggregateType,
2020-11-27 10:30:56 +00:00
"eventType", event.Type).WithError(err).Info("query failed",
"seq", event.PreviousSequence)
2020-10-19 11:58:59 +00:00
return caos_errs.ThrowInternal(err, "SQL-SBP37", "unable to create event")
}
2020-10-19 07:53:32 +00:00
}
return nil
})
if err != nil && !errors.Is(err, &caos_errs.CaosError{}) {
err = caos_errs.ThrowInternal(err, "SQL-DjgtG", "unable to store events")
}
return err
}
// Filter returns all events matching the given search query
2020-10-05 17:09:26 +00:00
func (db *CRDB) Filter(ctx context.Context, searchQuery *repository.SearchQuery) (events []*repository.Event, err error) {
2020-10-05 18:39:36 +00:00
events = []*repository.Event{}
2020-10-21 17:00:41 +00:00
err = query(ctx, db, searchQuery, &events)
2020-10-05 17:09:26 +00:00
if err != nil {
return nil, err
}
return events, nil
}
2020-10-05 17:09:26 +00:00
//LatestSequence returns the latests sequence found by the the search query
func (db *CRDB) LatestSequence(ctx context.Context, searchQuery *repository.SearchQuery) (uint64, error) {
var seq Sequence
2020-10-21 17:00:41 +00:00
err := query(ctx, db, searchQuery, &seq)
2020-10-05 17:09:26 +00:00
if err != nil {
return 0, err
}
return uint64(seq), nil
}
2020-10-21 17:00:41 +00:00
func (db *CRDB) db() *sql.DB {
return db.client
}
2020-10-05 18:39:36 +00:00
2020-10-21 17:00:41 +00:00
func (db *CRDB) orderByEventSequence(desc bool) string {
if desc {
return " ORDER BY event_sequence DESC"
2020-10-05 18:39:36 +00:00
}
2020-10-21 17:00:41 +00:00
return " ORDER BY event_sequence"
2020-10-05 17:09:26 +00:00
}
func (db *CRDB) eventQuery() string {
return "SELECT" +
" creation_date" +
", event_type" +
", event_sequence" +
", previous_sequence" +
", event_data" +
", editor_service" +
", editor_user" +
", resource_owner" +
", aggregate_type" +
", aggregate_id" +
", aggregate_version" +
" FROM eventstore.events"
}
func (db *CRDB) maxSequenceQuery() string {
return "SELECT MAX(event_sequence) FROM eventstore.events"
}
func (db *CRDB) columnName(col repository.Field) string {
switch col {
2020-10-06 19:28:09 +00:00
case repository.FieldAggregateID:
2020-10-05 17:09:26 +00:00
return "aggregate_id"
2020-10-06 19:28:09 +00:00
case repository.FieldAggregateType:
2020-10-05 17:09:26 +00:00
return "aggregate_type"
2020-10-06 19:28:09 +00:00
case repository.FieldSequence:
2020-10-05 17:09:26 +00:00
return "event_sequence"
2020-10-06 19:28:09 +00:00
case repository.FieldResourceOwner:
2020-10-05 17:09:26 +00:00
return "resource_owner"
2020-10-06 19:28:09 +00:00
case repository.FieldEditorService:
2020-10-05 17:09:26 +00:00
return "editor_service"
2020-10-06 19:28:09 +00:00
case repository.FieldEditorUser:
2020-10-05 17:09:26 +00:00
return "editor_user"
2020-10-06 19:28:09 +00:00
case repository.FieldEventType:
2020-10-05 17:09:26 +00:00
return "event_type"
2020-11-23 18:31:12 +00:00
case repository.FieldEventData:
return "event_data"
default:
2020-10-05 17:09:26 +00:00
return ""
}
}
func (db *CRDB) conditionFormat(operation repository.Operation) string {
2020-10-06 19:28:09 +00:00
if operation == repository.OperationIn {
2020-10-05 17:09:26 +00:00
return "%s %s ANY(?)"
}
2020-10-05 17:09:26 +00:00
return "%s %s ?"
}
2020-10-05 17:09:26 +00:00
func (db *CRDB) operation(operation repository.Operation) string {
switch operation {
2020-10-06 19:28:09 +00:00
case repository.OperationEquals, repository.OperationIn:
2020-10-05 17:09:26 +00:00
return "="
2020-10-06 19:28:09 +00:00
case repository.OperationGreater:
2020-10-05 17:09:26 +00:00
return ">"
2020-10-06 19:28:09 +00:00
case repository.OperationLess:
2020-10-05 17:09:26 +00:00
return "<"
2020-11-23 18:31:12 +00:00
case repository.OperationJSONContains:
return "@>"
2020-10-05 17:09:26 +00:00
}
return ""
}
var (
placeholder = regexp.MustCompile(`\?`)
)
//placeholder replaces all "?" with postgres placeholders ($<NUMBER>)
func (db *CRDB) placeholder(query string) string {
occurances := placeholder.FindAllStringIndex(query, -1)
if len(occurances) == 0 {
return query
}
replaced := query[:occurances[0][0]]
for i, l := range occurances {
nextIDX := len(query)
if i < len(occurances)-1 {
nextIDX = occurances[i+1][0]
}
replaced = replaced + "$" + strconv.Itoa(i+1) + query[l[1]:nextIDX]
}
return replaced
}