Skip to content

Commit 3cf8e6c

Browse files
authored
Add a voids_id column for tombstone events (#54)
* Add a voids_id column for tombstone events This is our first attempt at supporting deletion. Clients send in an event of type "dimo.tombstone" with a CloudEvent id in the data section. That id gets placed in this new column. We're adding a skipping index to event_type to make it cheaper to find these events. There should not be many. * Edit some comments and remove an unused helper * A little note about voids_id not showing up in pq
1 parent 1d6fac1 commit 3cf8e6c

6 files changed

Lines changed: 75 additions & 16 deletions

File tree

clickhouse/clickhouse.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const (
3535
IndexKeyColumn = "index_key"
3636
// DataIndexKeyColumn is the name of the data index name column in Clickhouse.
3737
DataIndexKeyColumn = "data_index_key"
38+
// VoidsIDColumn is the name of the voids_id column in Clickhouse. For
39+
// dimo.tombstone events it holds the id of the event, usually an attestation,
40+
// being tombstoned; empty for all other event types.
41+
VoidsIDColumn = "voids_id"
3842

3943
// InsertStmt is the SQL statement for inserting a row into Clickhouse.
4044
InsertStmt = "INSERT INTO " + TableName + " (" +
@@ -48,8 +52,9 @@ const (
4852
DataVersionColumn + ", " +
4953
ExtrasColumn + ", " +
5054
IndexKeyColumn + ", " +
51-
DataIndexKeyColumn +
52-
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
55+
DataIndexKeyColumn + ", " +
56+
VoidsIDColumn +
57+
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5358

5459
// hexChars contains the characters used for hex representation
5560
hexChars = "0123456789abcdef"
@@ -66,18 +71,18 @@ func CloudEventToSlice(event *cloudevent.CloudEventHeader) []any {
6671
// The order of the elements in the array match the order of the columns in the table.
6772
// This variant allows the caller to specify a value for index_key.
6873
func CloudEventToSliceWithKey(event *cloudevent.CloudEventHeader, key string) []any {
69-
return cloudEventToSlice(event, key, "")
74+
return cloudEventToSlice(event, key, "", "")
7075
}
7176

7277
// StoredEventToSlice converts a StoredEvent to an array of any for Clickhouse
73-
// insertion, populating both index_key (caller-supplied) and data_index_key
74-
// (carried on the wrapper). The order of the elements matches the column
75-
// order in the table.
78+
// insertion, populating index_key (caller-supplied), data_index_key, and
79+
// voids_id (both carried on the wrapper). The order of the elements matches
80+
// the column order in the table.
7681
func StoredEventToSlice(stored *cloudevent.StoredEvent, indexKey string) []any {
77-
return cloudEventToSlice(&stored.CloudEventHeader, indexKey, stored.DataIndexKey)
82+
return cloudEventToSlice(&stored.CloudEventHeader, indexKey, stored.DataIndexKey, stored.VoidsID)
7883
}
7984

80-
func cloudEventToSlice(event *cloudevent.CloudEventHeader, indexKey, dataIndexKey string) []any {
85+
func cloudEventToSlice(event *cloudevent.CloudEventHeader, indexKey, dataIndexKey, voidsID string) []any {
8186
// Add non-column fields to extras
8287
extras := cloudevent.AddNonColumnFieldsToExtras(event)
8388

@@ -99,6 +104,7 @@ func cloudEventToSlice(event *cloudevent.CloudEventHeader, indexKey, dataIndexKe
99104
string(jsonExtra),
100105
indexKey,
101106
dataIndexKey,
107+
voidsID,
102108
}
103109
}
104110

@@ -108,11 +114,11 @@ func UnmarshalCloudEventSlice(jsonArray []byte) ([]any, error) {
108114
if err := json.Unmarshal(jsonArray, &rawSlice); err != nil {
109115
return nil, fmt.Errorf("failed to unmarshal cloud event slice: %w", err)
110116
}
111-
if len(rawSlice) != 11 {
117+
if len(rawSlice) != 12 {
112118
return nil, fmt.Errorf("invalid cloud event slice length: %d", len(rawSlice))
113119
}
114120

115-
// Column order: subject, timestamp, eventType, id, source, producer, dataContentType, dataVersion, extras, indexKey, dataIndexKey
121+
// Column order: subject, timestamp, eventType, id, source, producer, dataContentType, dataVersion, extras, indexKey, dataIndexKey, voidsID
116122
var (
117123
subject string
118124
timestamp time.Time
@@ -125,6 +131,7 @@ func UnmarshalCloudEventSlice(jsonArray []byte) ([]any, error) {
125131
extras string
126132
indexKey string
127133
dataIndexKey string
134+
voidsID string
128135
)
129136
unmarshal := func(i int, name string, ptr any) error {
130137
if err := json.Unmarshal(rawSlice[i], ptr); err != nil {
@@ -165,7 +172,10 @@ func UnmarshalCloudEventSlice(jsonArray []byte) ([]any, error) {
165172
if err := unmarshal(10, "data index key", &dataIndexKey); err != nil {
166173
return nil, err
167174
}
168-
return []any{subject, timestamp, eventType, id, source, producer, dataContentType, dataVersion, extras, indexKey, dataIndexKey}, nil
175+
if err := unmarshal(11, "voids id", &voidsID); err != nil {
176+
return nil, err
177+
}
178+
return []any{subject, timestamp, eventType, id, source, producer, dataContentType, dataVersion, extras, indexKey, dataIndexKey, voidsID}, nil
169179
}
170180

171181
// CloudEventToObjectKey generates a unique key for storing cloud events.

clickhouse/clickhouse_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestCloudEventToSlice(t *testing.T) {
3333

3434
// Test CloudEventToSlice
3535
slice := CloudEventToSlice(event)
36-
require.Len(t, slice, 11)
36+
require.Len(t, slice, 12)
3737

3838
// Verify the order and values of the slice
3939
assert.Equal(t, event.Subject, slice[0])
@@ -60,6 +60,9 @@ func TestCloudEventToSlice(t *testing.T) {
6060

6161
// data_index_key is empty for CloudEventToSlice (payload is not external)
6262
assert.Equal(t, "", slice[10])
63+
64+
// voids_id is empty for non-tombstone events
65+
assert.Equal(t, "", slice[11])
6366
}
6467

6568
func TestCloudEventToSliceWithKey(t *testing.T) {
@@ -85,7 +88,7 @@ func TestCloudEventToSliceWithKey(t *testing.T) {
8588

8689
customKey := "custom-key"
8790
slice := CloudEventToSliceWithKey(event, customKey)
88-
require.Len(t, slice, 11)
91+
require.Len(t, slice, 12)
8992

9093
// Verify the order and values of the slice
9194
assert.Equal(t, event.Subject, slice[0])
@@ -111,6 +114,9 @@ func TestCloudEventToSliceWithKey(t *testing.T) {
111114

112115
// data_index_key is empty for CloudEventToSliceWithKey (payload is not external)
113116
assert.Equal(t, "", slice[10])
117+
118+
// voids_id is empty for non-tombstone events
119+
assert.Equal(t, "", slice[11])
114120
}
115121

116122
func TestStoredEventToSlice(t *testing.T) {
@@ -135,11 +141,17 @@ func TestStoredEventToSlice(t *testing.T) {
135141
}
136142

137143
slice := StoredEventToSlice(stored, "bundle/key#7")
138-
require.Len(t, slice, 11)
144+
require.Len(t, slice, 12)
139145

140146
assert.Equal(t, stored.Subject, slice[0])
141147
assert.Equal(t, "bundle/key#7", slice[9])
142148
assert.Equal(t, "payloads/abc123.jpg", slice[10])
149+
assert.Equal(t, "", slice[11])
150+
151+
// VoidsID on the wrapper propagates into the slice.
152+
stored.VoidsID = "voided-id-1"
153+
slice = StoredEventToSlice(stored, "bundle/key#7")
154+
assert.Equal(t, "voided-id-1", slice[11])
143155
}
144156

145157
func TestUnmarshalCloudEventSlice(t *testing.T) {
@@ -158,6 +170,7 @@ func TestUnmarshalCloudEventSlice(t *testing.T) {
158170
`{"extra1":"value1","extra2":123}`,
159171
"test-key",
160172
"test-data-key",
173+
"test-voids-id",
161174
}
162175

163176
// Marshal the slice to JSON
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
ALTER TABLE cloud_event ADD COLUMN voids_id String DEFAULT '' COMMENT 'For dimo.tombstone events, the id of the event being tombstoned. Empty for all other event types.' AFTER data_index_key;
4+
-- +goose StatementEnd
5+
-- +goose StatementBegin
6+
-- Make it cheap to find these events. This may also help with some attestation filtering.
7+
ALTER TABLE cloud_event ADD INDEX idx_event_type event_type TYPE set(0) GRANULARITY 1;
8+
-- +goose StatementEnd
9+
-- +goose StatementBegin
10+
-- Do this asynchronously.
11+
ALTER TABLE cloud_event MATERIALIZE INDEX idx_event_type;
12+
-- +goose StatementEnd
13+
14+
-- +goose Down
15+
-- +goose StatementBegin
16+
ALTER TABLE cloud_event DROP INDEX idx_event_type;
17+
-- +goose StatementEnd
18+
-- +goose StatementBegin
19+
ALTER TABLE cloud_event DROP COLUMN voids_id;
20+
-- +goose StatementEnd

clickhouse/migrations/migrations_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func TestMigration(t *testing.T) {
6060
localch.ExtrasColumn,
6161
localch.IndexKeyColumn,
6262
localch.DataIndexKeyColumn,
63+
localch.VoidsIDColumn,
6364
}
6465
assert.ElementsMatch(t, expectedCols, cols, "Columns do not match")
6566

cloudevent_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ const (
1717
// TypeAttestation is the event type for 3rd party attestations
1818
TypeAttestation = "dimo.attestation"
1919

20+
// TypeAttestationTombstone is the event type used to tombstone an
21+
// attestation. The data payload carries the id of the attestation
22+
// being tombstoned in the voidsId field; see the voids_id ClickHouse
23+
// column populated from it.
24+
TypeAttestationTombstone = "dimo.tombstone"
25+
2026
// TypeUnknown is the event type for unknown events.
2127
TypeUnknown = "dimo.unknown"
2228

stored_event.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@ package cloudevent
88
// an external object holding the payload. In that case the embedded
99
// RawEvent's Data and DataBase64 fields will typically be empty.
1010
//
11+
// VoidsID, when non-empty, is the id of the attestation that this event
12+
// tombstones. It is only meaningful for events whose Type is
13+
// TypeAttestationTombstone and is extracted server-side from the
14+
// signed Data payload — it must never be set from producer-supplied input
15+
// directly. It is used to cheaply populate the voids_id column in ClickHouse,
16+
// but is not stored as a column in Parquet bundles: you would have to parse
17+
// the data section of a Parquet row to recover the id.
18+
//
1119
// StoredEvent is deliberately separate from CloudEventHeader / RawEvent so
1220
// that the wire-format types remain pure and shared safely across services
13-
// — DataIndexKey points into trusted internal storage and must never be set
14-
// from producer-supplied input.
21+
// — DataIndexKey and VoidsID point into trusted internal storage and must
22+
// never be set from producer-supplied input.
1523
type StoredEvent struct {
1624
RawEvent
1725
DataIndexKey string
26+
VoidsID string
1827
}

0 commit comments

Comments
 (0)