Skip to content

Add caller metadata to projection create and update#47

Merged
w1am merged 2 commits into
mainfrom
UI-1644/projection-metadata
Jun 25, 2026
Merged

Add caller metadata to projection create and update#47
w1am merged 2 commits into
mainfrom
UI-1644/projection-metadata

Conversation

@George-Payne

@George-Payne George-Payne commented Jun 24, 2026

Copy link
Copy Markdown
Member

Adds support for attaching caller-supplied metadata to projection create and update, kurrent-io/KurrentDB#5638.

  • projections.proto - add the properties map to CreateReq.Options (field 6) and UpdateReq.Options (field 5), and regenerate.
  • projection_options.go / projection_client.go - expose Metadata map[string]interface{} on both option structs, converted to a struct-value map before the RPC and failing fast with a wrapped error on non-JSON values.
  • Tests - round-trip create/update, update-without-metadata-keeps-existing, and invalid-value rejection; the round-trip tests are gated to server 26.2.0+.

Mirrors KurrentDB server PR #5638. The server stamps a caller-supplied
properties map onto the projection's $ProjectionUpdated definition event,
giving deployment tooling (gaffer) a per-deploy annotation bound to the
exact projection version. It does not affect the projection engine, and
older servers ignore the field.

- Add the properties map to CreateReq.Options (field 6) and
  UpdateReq.Options (field 5) in projections.proto and regenerate
- Expose it as Metadata map[string]interface{} on CreateProjectionOptions
  and UpdateProjectionOptions, converted to a struct-value map before the
  RPC and failing fast on non-JSON values
- Add round-trip, update-keeps-existing, and invalid-value tests; the
  round-trip tests are gated to server 26.2.0+
@George-Payne George-Payne added enhancement New feature or request go Pull requests that update go code labels Jun 24, 2026
@George-Payne George-Payne self-assigned this Jun 24, 2026
@linear-code

linear-code Bot commented Jun 24, 2026

Copy link
Copy Markdown

UI-1644

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add caller metadata support to projection Create/Update requests
✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Description

• Extend projection Create/Update RPC options with a caller-supplied properties map.
• Expose/validate Metadata on Go projection option structs and encode as protobuf values.
• Add sample usage and integration tests for round-trip, retention, and invalid metadata.
Diagram

graph TD
  A["Caller app / deploy tool"] --> B["ProjectionClient (Go)"] --> C["projectionMetadata() validation"] --> D["Projections gRPC Create/Update"] --> E["KurrentDB server"] --> F["$ProjectionUpdated event metadata"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Accept json.RawMessage values instead of interface{}
  • ➕ Eliminates runtime type ambiguity; callers must provide valid JSON bytes
  • ➕ Avoids protobuf structpb conversion edge-cases and Go type coercions (e.g., int -> float64 in JSON tooling)
  • ➖ More cumbersome for typical callers; requires manual marshaling everywhere
  • ➖ Harder to express nested objects ergonomically compared to map/[] types
2. Validate via encoding/json marshal/unmarshal then build structpb.Struct
  • ➕ Validation semantics match JSON exactly (including numeric handling)
  • ➕ Could produce more uniform behavior across Go value types
  • ➖ Extra allocations and CPU (double conversion) vs structpb.NewValue per entry
  • ➖ Still must end up with map to satisfy the proto contract

Recommendation: Keep the current approach: exposing Metadata map[string]interface{} is ergonomic for callers, structpb.NewValue enforces protobuf/JSON-compatible values early with a clear error, and the wire format matches the server’s map contract while remaining backward-compatible with older servers that ignore the unknown field.

Files changed (7) +341 / -96

Enhancement (3) +51 / -2
projection_client.goEncode/validate projection metadata and send as RPC properties +33/-2

Encode/validate projection metadata and send as RPC properties

• Adds 'projectionMetadata' helper to convert 'map[string]interface{}' into 'map[string]*structpb.Value' with wrapped errors on invalid values. Wires the resulting 'Properties' into both Create and Update projection RPC options.

kurrentdb/projection_client.go

projection_options.goExpose 'Metadata' on create/update projection options +16/-0

Expose 'Metadata' on create/update projection options

• Introduces 'Metadata map[string]interface{}' on 'CreateProjectionOptions' and 'UpdateProjectionOptions' with documentation explaining intent (deployment annotations), JSON-compatibility requirements, and backward-compat behavior on unsupported servers.

kurrentdb/projection_options.go

projections.protoAdd 'properties' map to Create/Update projection options +2/-0

Add 'properties' map to Create/Update projection options

• Extends 'CreateReq.Options' (field 6) and 'UpdateReq.Options' (field 5) with 'map<string, google.protobuf.Value> properties', enabling caller-supplied metadata to flow to the server.

protos/kurrentdb/protocols/v1/projections.proto

Tests (2) +145 / -0
projection_fixture_test.goAdd helper to read stamped projection definition metadata +45/-0

Add helper to read stamped projection definition metadata

• Adds 'ProjectionDefinitionMetadata' that reads the '$projections-<name>' stream backwards, finds the latest '$ProjectionUpdated' event, and unmarshals its user metadata as a protobuf 'Struct' into a Go map for assertions.

test/projection_fixture_test.go

projection_test.goAdd create/update metadata behavior tests with server gating +100/-0

Add create/update metadata behavior tests with server gating

• Adds integration tests for create/update round-trip metadata stamping, verifies that update-without-metadata preserves existing metadata, and asserts invalid metadata values are rejected client-side. Round-trip tests are gated to KurrentDB server >= 26.2.0.

test/projection_test.go

Documentation (1) +17 / -0
projectionManagement.goAdd sample showing projection creation with metadata +17/-0

Add sample showing projection creation with metadata

• Adds 'CreateWithMetadata' example demonstrating how to pass a deployment/tool annotation via 'CreateProjectionOptions.Metadata'. Keeps the sample scoped to creation (mirrors intended deployment-tool usage).

samples/projectionManagement.go

Other (1) +128 / -94
projections.pb.goRegenerate projections protobuf Go bindings +128/-94

Regenerate projections protobuf Go bindings

• Regenerates Go protobuf output to include the new 'Properties' fields/accessors and associated map entry message types, updating message indexes and dependency mappings accordingly.

protos/kurrentdb/protocols/v1/projections/projections.pb.go

@qodo-code-review

qodo-code-review Bot commented Jun 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. []byte metadata unsupported ✓ Resolved 🐞 Bug ≡ Correctness
Description
The public option docs claim Metadata values may be []byte, but the implementation converts values
through structpb.NewValue and the tests decode stored metadata as a protobuf Struct (AsMap), which
is JSON-like and not raw-bytes. Callers following the docs and providing []byte will either get
client-side validation errors or won’t be able to round-trip byte values as bytes.
Code

kurrentdb/projection_options.go[R36-42]

+	// Metadata is caller-supplied metadata stamped onto the projection
+	// definition event. It does not affect the projection engine and is
+	// intended for deployment tooling to annotate projections (for example
+	// with a deployment identifier or the name of the managing tool). Values
+	// must be JSON-compatible: string, bool, number, nil, []byte, or nested
+	// []interface{} and map[string]interface{} of those. Ignored by servers
+	// that don't support it.
Evidence
The docs explicitly list []byte as allowed. The implementation converts values through
structpb.NewValue, and the tests assert metadata is stored/decoded as a protobuf Struct and returned
as map[string]interface{}, which cannot represent raw bytes as bytes.

kurrentdb/projection_options.go[36-43]
kurrentdb/projection_client.go[21-27]
test/projection_fixture_test.go[160-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`CreateProjectionOptions.Metadata` / `UpdateProjectionOptions.Metadata` documentation says values may be `[]byte`, but the client encodes metadata using `google.protobuf.Value` via `structpb.NewValue`, and tests decode the stored representation as a `structpb.Struct` into `map[string]interface{}`. This contract implies JSON-like types, not raw byte arrays.

### Issue Context
This is a public API contract mismatch: callers may supply `[]byte` expecting it to work because the docs explicitly allow it.

### Fix Focus Areas
- kurrentdb/projection_options.go[36-43]
- kurrentdb/projection_options.go[74-81]
- kurrentdb/projection_client.go[16-27]
- test/projection_fixture_test.go[160-166]

### Suggested fix
Choose one:
1) **Documentation fix (simplest):** remove `[]byte` from the allowed types and clarify the supported JSON-like set.
2) **Behavior fix:** explicitly support `[]byte` by converting it to an agreed encoding (e.g., base64 string) before calling `structpb.NewValue`, and update docs/tests to reflect that the on-wire type is a string.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Empty metadata ignored 🐞 Bug ≡ Correctness
Description
projectionMetadata collapses an explicitly empty Metadata map to nil, so Create/Update omit the
Properties field and callers can’t express an intentional overwrite to “empty metadata” (it behaves
the same as omitting metadata). This is especially relevant because update-without-metadata is
explicitly relied on to preserve existing metadata.
Code

kurrentdb/projection_client.go[R16-19]

+func projectionMetadata(md map[string]interface{}) (map[string]*structpb.Value, error) {
+	if len(md) == 0 {
+		return nil, nil
+	}
Evidence
The helper returns nil when the map is empty, and Create/Update always use this helper output for
the RPC field. The new tests explicitly assert that an update with no metadata must keep existing
metadata, highlighting why empty-vs-omitted semantics matter.

kurrentdb/projection_client.go[16-19]
kurrentdb/projection_client.go[75-78]
kurrentdb/projection_client.go[117-120]
test/projection_test.go[168-195]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`projectionMetadata` treats `len(md)==0` as no metadata and returns `nil`, which means an explicitly empty map is indistinguishable from omitting metadata. Callers who pass `Metadata: map[string]interface{}{}` cannot express a semantic difference from not setting `Metadata` at all.

### Issue Context
Tests in this PR rely on “no metadata” updates preserving previously stamped metadata, but the API currently doesn’t clarify what happens when a caller passes an explicit empty map.

### Fix Focus Areas
- kurrentdb/projection_client.go[16-19]
- kurrentdb/projection_options.go[36-43]
- kurrentdb/projection_options.go[74-81]

### Suggested fix
Update the `Metadata` field docs on both option structs to explicitly state that `nil` or an empty map results in *no* `properties` being sent (and therefore metadata will be preserved on update), and that clearing/overwriting-to-empty is not representable via this API/protocol as implemented.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

structpb.NewValue base64-encodes []byte into a string Value, so it does
not round-trip as bytes. Advertising it was misleading.
@George-Payne George-Payne requested a review from w1am June 24, 2026 14:22
@w1am w1am merged commit 649feec into main Jun 25, 2026
21 checks passed
@w1am w1am deleted the UI-1644/projection-metadata branch June 25, 2026 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants