Skip to content

refactor: list entitlements for ingested events#4259

Merged
chrisgacsal merged 3 commits into
mainfrom
refactor/entitlement-query
Apr 30, 2026
Merged

refactor: list entitlements for ingested events#4259
chrisgacsal merged 3 commits into
mainfrom
refactor/entitlement-query

Conversation

@chrisgacsal
Copy link
Copy Markdown
Collaborator

@chrisgacsal chrisgacsal commented Apr 30, 2026

Overview

Improve fetching entitlements affected by ingested events. It combines 3 separate database calls into one reducing the time/resources to get affected entitlements. This helps with improving the overall performance of the balance worker which heavily relies on this call in order to figure out which entitlements need to be tracked/their balance updated.

Summary by CodeRabbit

  • Refactor

    • Consolidated entitlement lookup into a single, database-optimized query for improved performance.
    • Normalized timestamp fields to UTC and centralized error handling around query execution and row scanning.
  • New Features

    • Added structured validation for ingest-event filters to ensure required fields and meter selection.
  • Tests

    • Added unit and integration tests covering query generation, SQL argument ordering, and end-to-end behavior for entitlement lookup.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ListEntitlementsAffectedByIngestEvents was refactored to run a single dialect-aware raw SQL query produced by EntitlementsByIngestedEventsQuery (CTEs resolving customers and features) instead of multiple ORM calls; results are streamed via rows.Next()/Scan, timestamps normalized to UTC, and errors now surface from query/scan/rows.Err().

Changes

Cohort / File(s) Summary
Entitlement Query & Adapter
openmeter/entitlement/adapter/entitlement.go
Added EntitlementsByIngestedEventsQuery(dialect, ns, subject string, meters ...string) and refactored ListEntitlementsAffectedByIngestEvents to execute one raw SQL query (CTEs for eligible customers/features), stream results with rows.Next()/Scan, and normalize CreatedAt, DeletedAt, ActiveFrom, ActiveTo to UTC. Error paths now reflect query execution and row scanning.
Tests — SQL gen & integration
openmeter/entitlement/adapter/entitlement_test.go
Added unit tests validating Postgres SQL and bound-arg ordering for EntitlementsByIngestedEventsQuery with various meter sets; added integration-style tests creating meters/features/customers/entitlements and asserting ListEntitlementsAffectedByIngestEvents behavior for customer key vs subject key, non-existent meters, deleted entitlements/customers.
Input validation
openmeter/entitlement/balanceworker/repository.go
Added Validate() error method on IngestEventQueryFilter enforcing required Namespace, EventSubject, and at least one MeterSlugs; aggregates validation messages and returns a wrapped validation error.

Sequence Diagram(s)

sequenceDiagram
  participant Worker as Worker (balance worker)
  participant Adapter as Entitlement Adapter
  participant DB as Database

  Worker->>Adapter: ListEntitlementsAffectedByIngestEvents(ns, subject, meters...)
  Adapter->>DB: Exec EntitlementsByIngestedEventsQuery(dialect, ns, subject, meters)
  DB-->>Adapter: rows stream (entitlement rows)
  Adapter->>Adapter: rows.Next()/rows.Scan() -> map to response (normalize UTC)
  Adapter-->>Worker: ListAffectedEntitlementsResponse (entitlement IDs + metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

area/entitlements

Suggested reviewers

  • tothandras
  • turip
  • gergely-kurucz-konghq
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: refactoring the list entitlements function to use a single query instead of multiple database calls for ingested events.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/entitlement-query

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chrisgacsal chrisgacsal force-pushed the refactor/entitlement-query branch from ccbb032 to c30f887 Compare April 30, 2026 10:20
@chrisgacsal chrisgacsal force-pushed the refactor/entitlement-query branch from c30f887 to e285e99 Compare April 30, 2026 12:33
@chrisgacsal chrisgacsal added the release-note/misc Miscellaneous changes label Apr 30, 2026
@chrisgacsal chrisgacsal marked this pull request as ready for review April 30, 2026 12:36
@chrisgacsal chrisgacsal requested a review from a team as a code owner April 30, 2026 12:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 324-331: Within the entutils.TransactingRepo closure the raw query
uses the base client (a.db) so the read can escape an existing transaction;
switch both the dialect lookup and the QueryContext call to the
transaction-bound client (use repo.db.GetConfig().Driver.Dialect() and
repo.db.QueryContext(ctx, query, args...)) where
EntitlementsByIngestedEventsQuery and the rows retrieval are performed to ensure
the query runs within the transaction.
- Around line 285-290: The WHERE clause uses sql.In(mt.C(db_meter.FieldKey),
lo.ToAnySlice(meters)...) which generates SQL "IN ($1)" with an array param and
fails on Postgres; replace that predicate with an ANY-style comparison e.g.
sql.EQ(mt.C(db_meter.FieldKey), sql.Any(lo.ToAnySlice(meters))) (update the
Where(sql.And(...)) clause) so the generated SQL becomes "col = ANY($1)" and
accepts the array parameter correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6dd04ba9-9e60-46b1-b642-bc39e644190b

📥 Commits

Reviewing files that changed from the base of the PR and between a63f63c and e285e99.

📒 Files selected for processing (2)
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/entitlement/adapter/entitlement_test.go

Comment thread openmeter/entitlement/adapter/entitlement.go
Comment thread openmeter/entitlement/adapter/entitlement.go Outdated
Signed-off-by: Krisztian Gacsal <chrisgacsal@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
openmeter/entitlement/adapter/entitlement.go (1)

324-331: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the transaction-bound client here.

This closure is already inside entutils.TransactingRepo, but the raw SQL still goes through a.db. That can escape the caller's transaction and read a different snapshot than the rest of the balance-worker flow.

Small fix
-			query, args := EntitlementsByIngestedEventsQuery(
-				a.db.GetConfig().Driver.Dialect(),
+			query, args := EntitlementsByIngestedEventsQuery(
+				repo.db.GetConfig().Driver.Dialect(),
 				eventFilter.Namespace,
 				eventFilter.EventSubject,
 				eventFilter.MeterSlugs...,
 			)
 
-			rows, err := a.db.QueryContext(ctx, query, args...)
+			rows, err := repo.db.QueryContext(ctx, query, args...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/entitlement/adapter/entitlement.go` around lines 324 - 331, The SQL
query is executed against a.db which can bypass the caller's transaction from
entutils.TransactingRepo; change the call to use the transaction-bound client
provided by the transacting repo (e.g., tx or repo provided inside the
entutils.TransactingRepo closure) so EntitlementsByIngestedEventsQuery is
executed with tx.QueryContext(...) (replace a.db.QueryContext with the
transaction client) to ensure the read uses the same transactional snapshot as
the balance-worker flow.
🧹 Nitpick comments (1)
openmeter/entitlement/adapter/entitlement_test.go (1)

47-161: ⚡ Quick win

Thread the test context through setup/helpers.

setup and the helper inserts still start from context.Background(). Using t.Context() here keeps cancellations and timeouts attached to the test harness instead of letting setup work keep running after a test is already done.

Small cleanup
-	if err := dbClient.Schema.Create(context.Background()); err != nil {
+	if err := dbClient.Schema.Create(t.Context()); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/entitlement/adapter/entitlement_test.go` around lines 47 - 161, The
tests use context.Background() and context.WithCancel() instead of the
test-scoped context, which can let work continue after a test finishes; update
setup, createCustomerWithSubject, and createCustomerWithSubjectAndKey to use
t.Context() (propagate the test context) for DB migrations and all
repository/service calls (replace context.Background() and the
context.WithCancel pattern with ctx := t.Context(), and remove the manual cancel
where used) so cancellations/timeouts are tied to the test runner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 324-331: The SQL query is executed against a.db which can bypass
the caller's transaction from entutils.TransactingRepo; change the call to use
the transaction-bound client provided by the transacting repo (e.g., tx or repo
provided inside the entutils.TransactingRepo closure) so
EntitlementsByIngestedEventsQuery is executed with tx.QueryContext(...) (replace
a.db.QueryContext with the transaction client) to ensure the read uses the same
transactional snapshot as the balance-worker flow.

---

Nitpick comments:
In `@openmeter/entitlement/adapter/entitlement_test.go`:
- Around line 47-161: The tests use context.Background() and
context.WithCancel() instead of the test-scoped context, which can let work
continue after a test finishes; update setup, createCustomerWithSubject, and
createCustomerWithSubjectAndKey to use t.Context() (propagate the test context)
for DB migrations and all repository/service calls (replace context.Background()
and the context.WithCancel pattern with ctx := t.Context(), and remove the
manual cancel where used) so cancellations/timeouts are tied to the test runner.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ccffff2-a8c6-4296-819c-afb205c310cd

📥 Commits

Reviewing files that changed from the base of the PR and between e285e99 and 0402e10.

📒 Files selected for processing (2)
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/entitlement/adapter/entitlement_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
openmeter/entitlement/adapter/entitlement.go (1)

319-329: ⚡ Quick win

Short-circuit when no meter slugs are provided.

If eventFilter.MeterSlugs is empty, returning early avoids building/executing a query that cannot produce useful matches.

♻️ Suggested change
 func (a *entitlementDBAdapter) ListEntitlementsAffectedByIngestEvents(ctx context.Context, eventFilter balanceworker.IngestEventQueryFilter) ([]balanceworker.ListAffectedEntitlementsResponse, error) {
 	return entutils.TransactingRepo[[]balanceworker.ListAffectedEntitlementsResponse, *entitlementDBAdapter](
 		ctx,
 		a,
 		func(ctx context.Context, repo *entitlementDBAdapter) ([]balanceworker.ListAffectedEntitlementsResponse, error) {
+			if len(eventFilter.MeterSlugs) == 0 {
+				return []balanceworker.ListAffectedEntitlementsResponse{}, nil
+			}
+
 			query, args := EntitlementsByIngestedEventsQuery(
 				repo.db.GetConfig().Driver.Dialect(),
 				eventFilter.Namespace,
 				eventFilter.EventSubject,
 				eventFilter.MeterSlugs...,
 			)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/entitlement/adapter/entitlement.go` around lines 319 - 329, In
ListEntitlementsAffectedByIngestEvents, add a short-circuit that checks if
eventFilter.MeterSlugs is empty and immediately returns an empty
[]balanceworker.ListAffectedEntitlementsResponse and nil error to avoid
building/executing EntitlementsByIngestedEventsQuery with no meter slugs; place
the check at the start of the transacting function (or before calling
EntitlementsByIngestedEventsQuery) so the function exits early when
eventFilter.MeterSlugs has length 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 349-360: The CreatedAt timestamp on the scanned entitlement row is
not being normalized to UTC like the other timestamps; after scanning into r
(the entitlement row variable) call r.CreatedAt = convert.SafeToUTC(r.CreatedAt)
alongside the existing r.DeletedAt/ActiveFrom/ActiveTo conversions so all
timestamps use UTC. Update the same block where the scan occurs (the code that
scans into &r.CreatedAt, &r.DeletedAt, &r.ActiveFrom, &r.ActiveTo) to include
this conversion using convert.SafeToUTC.

---

Nitpick comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 319-329: In ListEntitlementsAffectedByIngestEvents, add a
short-circuit that checks if eventFilter.MeterSlugs is empty and immediately
returns an empty []balanceworker.ListAffectedEntitlementsResponse and nil error
to avoid building/executing EntitlementsByIngestedEventsQuery with no meter
slugs; place the check at the start of the transacting function (or before
calling EntitlementsByIngestedEventsQuery) so the function exits early when
eventFilter.MeterSlugs has length 0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6dd4c9b7-5061-47b9-ac6c-fc42c82d275d

📥 Commits

Reviewing files that changed from the base of the PR and between 0402e10 and a8cbfbb.

📒 Files selected for processing (2)
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/entitlement/adapter/entitlement_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/entitlement/adapter/entitlement_test.go

Comment thread openmeter/entitlement/adapter/entitlement.go
turip
turip previously approved these changes Apr 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
openmeter/entitlement/adapter/entitlement.go (1)

324-329: ⚡ Quick win

Consider a small fast-path for empty/duplicate meter slugs.

Quick win here: dedupe eventFilter.MeterSlugs and return early when the list is empty before building/executing SQL. It keeps query args smaller and avoids unnecessary DB work in high-throughput ingestion batches.

⚙️ Suggested tweak
+			meters := lo.Uniq(eventFilter.MeterSlugs)
+			if len(meters) == 0 {
+				return []balanceworker.ListAffectedEntitlementsResponse{}, nil
+			}
+
 			query, args := EntitlementsByIngestedEventsQuery(
 				repo.db.GetConfig().Driver.Dialect(),
 				eventFilter.Namespace,
 				eventFilter.EventSubject,
-				eventFilter.MeterSlugs...,
+				meters...,
 			)

As per coding guidelines: "Performance should be a priority in critical code paths. Anything related to event ingestion, message processing, database operations (regardless of database) should be vetted for potential performance bottlenecks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/entitlement/adapter/entitlement.go` around lines 324 - 329, Dedupe
eventFilter.MeterSlugs and short-circuit when empty before calling
EntitlementsByIngestedEventsQuery: in the function containing the shown snippet,
create a small deduping step for eventFilter.MeterSlugs (e.g., build a temporary
map/set and produce a compact slice of unique slugs), replace uses of
eventFilter.MeterSlugs with that deduped slice, and if the deduped slice is
empty return immediately (the same zero/empty result the function would
otherwise produce) to avoid building/executing the SQL; ensure you update
references to EntitlementsByIngestedEventsQuery to pass the deduped slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 324-329: Dedupe eventFilter.MeterSlugs and short-circuit when
empty before calling EntitlementsByIngestedEventsQuery: in the function
containing the shown snippet, create a small deduping step for
eventFilter.MeterSlugs (e.g., build a temporary map/set and produce a compact
slice of unique slugs), replace uses of eventFilter.MeterSlugs with that deduped
slice, and if the deduped slice is empty return immediately (the same zero/empty
result the function would otherwise produce) to avoid building/executing the
SQL; ensure you update references to EntitlementsByIngestedEventsQuery to pass
the deduped slice.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 97e83879-23d3-4f08-8aef-cfcf0991cf55

📥 Commits

Reviewing files that changed from the base of the PR and between a8cbfbb and 4f87342.

📒 Files selected for processing (1)
  • openmeter/entitlement/adapter/entitlement.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/entitlement/adapter/entitlement.go`:
- Around line 299-310: The query in the entitlement adapter is incorrectly
filtering out deleted rows by using
sql.IsNull(et.C(db_entitlement.FieldDeletedAt)), which prevents DeletedAt from
being scanned into ListAffectedEntitlementsResponse and breaks
GetEntitlementActivityPeriod; remove that hard filter so deleted entitlements
are included (i.e., stop adding sql.IsNull on db_entitlement.FieldDeletedAt) in
the Select/Where building (the block that
Select(...).From(et).Join(cbs)...Where(sql.And(...)) in this file), and make the
same change for the other identical query occurrence around the 349-363 region
so deleted entitlements are returned and their DeletedAt values can be used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: edda6542-0772-45eb-90a1-eaa84c709c52

📥 Commits

Reviewing files that changed from the base of the PR and between 4f87342 and 317e924.

📒 Files selected for processing (3)
  • openmeter/entitlement/adapter/entitlement.go
  • openmeter/entitlement/adapter/entitlement_test.go
  • openmeter/entitlement/balanceworker/repository.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/entitlement/adapter/entitlement_test.go

Comment thread openmeter/entitlement/adapter/entitlement.go
@chrisgacsal chrisgacsal merged commit ff379af into main Apr 30, 2026
29 of 30 checks passed
@chrisgacsal chrisgacsal deleted the refactor/entitlement-query branch April 30, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants