Skip to content

feat(dedupe): per-table id_field/require_id overrides#377

Open
taitelee wants to merge 5 commits into
mainfrom
dedupe-per-table
Open

feat(dedupe): per-table id_field/require_id overrides#377
taitelee wants to merge 5 commits into
mainfrom
dedupe-per-table

Conversation

@taitelee

@taitelee taitelee commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Dedupe's id_field and require_id were single, deployment-wide settings. This PR makes them per-table and fixes two dedupe keying bugs found along the way:

Related Issues

Closes #222
Closes #370

@taitelee taitelee requested review from a team and EricAndrechek July 6, 2026 19:11
@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code area/observability Metrics, logs, traces, health, profiling area/api HTTP handlers, routing, middleware area/dedupe Deduplication (Pebble, ScyllaDB) area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release labels Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@taitelee, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a95d431d-285d-4210-a32a-b9a814a37436

📥 Commits

Reviewing files that changed from the base of the PR and between 70a9b91 and b94b175.

📒 Files selected for processing (5)
  • AGENTS.md
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • internal/api/ingest_test.go
  • tests/e2e/fixtures/config.yaml
📝 Walkthrough

Walkthrough

Adds per-table dedupe overrides (dedupe.tables YAML config), table-namespaced dedupe keys to prevent cross-table id collisions, and fixes explicit JSON null id_field values being treated as missing. Updates Deduplicator interface, config, ingest handler, mocks, tests, and documentation accordingly.

Changes

Per-table dedupe overrides and table namespacing

Layer / File(s) Summary
Config schema for per-table overrides
internal/config/config.go, config.yaml
Dedupe.Tables map[string]DedupeTable added with optional pointer IDField/RequireID fields; commented YAML template added.
Deduplicator interface namespaced by table
internal/dedupe/dedupe.go, internal/dedupe/embedded.go, internal/dedupe/embedded_test.go, internal/testutil/mocks.go, internal/observability/metrics_test.go
CheckAndMark now requires a table argument; keys namespaced as table + "\x00" + eventID across embedded, mock, and stub implementations; tests verify cross-table non-collision.
Ingest handler per-table resolution and null-id handling
internal/api/ingest.go, cmd/wavehouse/main.go
DedupeOverride type and DedupeTables field added; dedupeFieldsFor(table) resolves effective idField/requireID; explicit JSON null id treated as missing; wiring in main.go builds overrides from config.
Ingest tests for new dedupe behavior
internal/api/ingest_test.go
New tests cover null-id rejection/observe mode and per-table RequireID, IDField, and opt-out overrides.
Docs and changelog
CHANGELOG.md, docs/src/content/docs/api.md, docs/src/content/docs/architecture.md, docs/src/content/docs/configuration.mdx
Documents new dedupe.tables config, null-id handling, and breaking dedupe key namespacing change.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant IngestHandler
  participant Deduplicator

  Client->>IngestHandler: POST /v1/ingest?table={table}
  IngestHandler->>IngestHandler: dedupeFieldsFor(table)
  alt idField empty for table
    IngestHandler-->>Client: publish without dedupe
  else id missing or explicit null
    alt requireID true
      IngestHandler-->>Client: 400 reject (missing dedupe id)
    else
      IngestHandler-->>Client: publish un-deduped
    end
  else id present
    IngestHandler->>Deduplicator: CheckAndMark(table, eventID)
    Deduplicator-->>IngestHandler: isDuplicate
    IngestHandler-->>Client: publish or mark duplicate
  end
Loading

Possibly related issues

Suggested labels: area/ingest

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: per-table overrides for dedupe id_field and require_id settings.
Description check ✅ Passed The description is directly related and accurately describes the dedupe override, keying, and null-handling changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dedupe-per-table
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch dedupe-per-table

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.

@taitelee taitelee moved this from Backlog to In review in WaveHouse Task Board Jul 6, 2026
@github-code-quality

github-code-quality Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Go

Go

The overall coverage in the branch remains at 90%, unchanged from the branch.

Show a code coverage summary of the most impacted files.
File adee449 b94b175 +/-
cmd/wavehouse/main.go 70% 70% 0%
internal/api/ingest.go 97% 97% 0%
internal/dedupe/embedded.go 84% 85% +1%

Updated July 06, 2026 19:53 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📚 Docs preview is livehttps://304a76cb-wavehouse-docs.wave-rf.workers.dev

  • Commitb94b175: doc fixes
  • Author@taitelee
  • Committed — 2026-07-06 15:50 (UTC-04:00)
  • Deployed — 2026-07-06 15:53 EDT

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5f8723d4-7881-4381-981c-e130bfb559fb

📥 Commits

Reviewing files that changed from the base of the PR and between adee449 and 70a9b91.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • cmd/wavehouse/main.go
  • config.yaml
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/configuration.mdx
  • internal/api/ingest.go
  • internal/api/ingest_test.go
  • internal/config/config.go
  • internal/dedupe/dedupe.go
  • internal/dedupe/embedded.go
  • internal/dedupe/embedded_test.go
  • internal/observability/metrics_test.go
  • internal/testutil/mocks.go
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Integration tests
  • GitHub Check: E2E tests
  • GitHub Check: Docs build
  • GitHub Check: Coverage
  • GitHub Check: Lint
⚠️ CI failures not shown inline (2)

GitHub Actions: PR housekeeping / PR housekeeping: feat(dedupe): per-table id_field/require_id overrides

Conclusion: failure

View job details

##[group]Run # Single source of truth for the rule: scripts/lint-pr-title.sh — the
 �[36;1m# Single source of truth for the rule: scripts/lint-pr-title.sh — the�[0m
 �[36;1m# SAME validator the local agent gate runs (.claude/hooks/agent-bash-gate.sh),�[0m
 �[36;1m# so CI and local can't drift. The checkout above is ref: main, so this is�[0m
 �[36;1m# always the default-branch script. Dependabot's grouped-update titles�[0m
 �[36;1m# routinely exceed the 72-char subject cap and the format isn't�[0m
 �[36;1m# configurable, so Dependabot PRs are exempt from the length check�[0m
 �[36;1m# (the format check still applies).�[0m
 �[36;1mif [[ "$PR_AUTHOR" == "dependabot[bot]" || "$PR_AUTHOR" == "app/dependabot" ]]; then�[0m
 �[36;1m  export PR_TITLE_SKIP_LENGTH=1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif reason=$(bash scripts/lint-pr-title.sh "$PR_TITLE" 2>&1); then�[0m
 �[36;1m  echo "passed=true" >> "$GITHUB_OUTPUT"�[0m
 �[36;1m  echo "PR title OK: $PR_TITLE"�[0m
 �[36;1melse�[0m
 �[36;1m  echo "passed=false" >> "$GITHUB_OUTPUT"�[0m
 �[36;1m  printf '%s\n' "$reason"�[0m
 �[36;1m  echo "::error::$(printf '%s' "$reason" | head -1)"�[0m

GitHub Actions: PR housekeeping / 0_PR housekeeping.txt: feat(dedupe): per-table id_field/require_id overrides

Conclusion: failure

View job details

##[group]Run # Single source of truth for the rule: scripts/lint-pr-title.sh — the
 �[36;1m# Single source of truth for the rule: scripts/lint-pr-title.sh — the�[0m
 �[36;1m# SAME validator the local agent gate runs (.claude/hooks/agent-bash-gate.sh),�[0m
 �[36;1m# so CI and local can't drift. The checkout above is ref: main, so this is�[0m
 �[36;1m# always the default-branch script. Dependabot's grouped-update titles�[0m
 �[36;1m# routinely exceed the 72-char subject cap and the format isn't�[0m
 �[36;1m# configurable, so Dependabot PRs are exempt from the length check�[0m
 �[36;1m# (the format check still applies).�[0m
 �[36;1mif [[ "$PR_AUTHOR" == "dependabot[bot]" || "$PR_AUTHOR" == "app/dependabot" ]]; then�[0m
 �[36;1m  export PR_TITLE_SKIP_LENGTH=1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif reason=$(bash scripts/lint-pr-title.sh "$PR_TITLE" 2>&1); then�[0m
 �[36;1m  echo "passed=true" >> "$GITHUB_OUTPUT"�[0m
 �[36;1m  echo "PR title OK: $PR_TITLE"�[0m
 �[36;1melse�[0m
 �[36;1m  echo "passed=false" >> "$GITHUB_OUTPUT"�[0m
 �[36;1m  printf '%s\n' "$reason"�[0m
 �[36;1m  echo "::error::$(printf '%s' "$reason" | head -1)"�[0m
🧰 Additional context used
📓 Path-based instructions (5)
internal/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Core packages should be interface-first: behaviors such as cache, dedupe, publisher, and subscriber should be defined behind interfaces to keep implementations swappable.

Files:

  • internal/dedupe/embedded.go
  • internal/observability/metrics_test.go
  • internal/dedupe/embedded_test.go
  • internal/dedupe/dedupe.go
  • internal/testutil/mocks.go
  • internal/api/ingest_test.go
  • internal/config/config.go
  • internal/api/ingest.go
cmd/wavehouse/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Wire new dependencies in cmd/wavehouse/main.go rather than relying on global state or hidden initialization.

Files:

  • cmd/wavehouse/main.go
docs/src/content/docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Update the relevant docs pages when APIs, configuration, architecture, ingest format, deployment, or development workflows change; keep examples and prose accurate.

Files:

  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/api.md
docs/src/content/docs/**/*.mdx

📄 CodeRabbit inference engine (AGENTS.md)

Author Mermaid diagrams vertically by default, keep labels short, and avoid placing two large diagrams side by side.

Files:

  • docs/src/content/docs/configuration.mdx
internal/config/config.go

📄 CodeRabbit inference engine (AGENTS.md)

Add configuration fields with matching yaml, env, and env-default struct tags, and keep the config struct in sync with documented settings.

Files:

  • internal/config/config.go
🧠 Learnings (5)
📚 Learning: 2026-05-25T11:25:08.794Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 180
File: internal/observability/instruments.go:40-117
Timestamp: 2026-05-25T11:25:08.794Z
Learning: In WaveHouse (Go), it is idiomatic to declare OpenTelemetry (OTel) metric instruments as package-level `var` values created via the OTel Go SDK global proxy pattern (e.g., `var h metric.Float64Histogram = Meter().Float64Histogram(...)`, `var c metric.Int64Counter = Meter().Int64Counter(...)`). When reviewing, do NOT flag these as “global state” violations under the AGENTS.md constructor-injection rule; that rule is intended for swappable application-level dependencies (Cache/Publisher/Subscriber/Deduplicator), not OTel proxy instrument declarations. Do not recommend refactoring these instruments behind an `Instruments` struct for dependency injection.

Applied to files:

  • internal/observability/metrics_test.go
📚 Learning: 2026-06-26T12:23:22.696Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 346
File: internal/stream/subscriber_test.go:9-28
Timestamp: 2026-06-26T12:23:22.696Z
Learning: In this Go repository, prefer table-driven tests (e.g., `[]struct{...}` with `t.Run(...)`) only for tests that cover multiple scenarios/inputs and can be cleanly enumerated. Do not artificially rewrite a clear single-scenario sequential behavioral-flow test into a table-driven form just to fit the pattern; if there’s only one meaningful scenario, keep the test as a straightforward linear flow (as in `TestSubscriber_SendDeliversThenDropsWhenFull`).

Applied to files:

  • internal/observability/metrics_test.go
  • internal/dedupe/embedded_test.go
  • internal/api/ingest_test.go
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.

Applied to files:

  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/api.md
  • CHANGELOG.md
📚 Learning: 2026-05-20T01:02:00.784Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 164
File: internal/api/router_test.go:289-350
Timestamp: 2026-05-20T01:02:00.784Z
Learning: In WaveHouse’s internal API tests (files matching internal/api/**/*_test.go), follow the existing separation-of-concerns convention for testing the RequireRole middleware: inject `ContextKeyRole` directly into the request `context.Context` instead of using `testutil.MakeJWT`/JWT-driven flows. Do not refactor role-gate tests to use JWT tokens—JWT parsing and token handling are covered separately in `middleware_test.go` (the dedicated JWT parsing tests), and mixing those concerns would expand the failure surface and reduce isolation.

Applied to files:

  • internal/api/ingest_test.go
📚 Learning: 2026-05-23T01:23:59.268Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 174
File: internal/api/ingest_test.go:111-111
Timestamp: 2026-05-23T01:23:59.268Z
Learning: In WaveHouse Go tests in internal/api/**/*_test.go, use internal/testutil.AssertJSONErrorResponse(t, w) for HTTP error-path JSON assertions. Do not use (or reintroduce) package-local assertJSONErrorResponse helpers. AssertJSONErrorResponse verifies the response Content-Type is application/json, includes the X-Content-Type-Options: nosniff header, and that the JSON body contains an "error" field.

Applied to files:

  • internal/api/ingest_test.go
🔇 Additional comments (15)
CHANGELOG.md (1)

14-15: LGTM!

Also applies to: 33-37

docs/src/content/docs/architecture.md (1)

77-77: LGTM!

Also applies to: 171-172

docs/src/content/docs/configuration.mdx (1)

132-149: LGTM!

internal/config/config.go (1)

135-148: LGTM!

config.yaml (1)

76-85: LGTM!

internal/dedupe/dedupe.go (1)

6-16: LGTM!

internal/dedupe/embedded.go (1)

28-51: LGTM!

internal/dedupe/embedded_test.go (1)

21-57: LGTM! Sequential test structure here correctly follows the prior preference for not forcing single-scenario tests into a table-driven form.

internal/testutil/mocks.go (1)

100-112: LGTM!

internal/observability/metrics_test.go (1)

23-27: LGTM!

internal/api/ingest.go (1)

39-46: LGTM!

Also applies to: 59-79, 427-463

cmd/wavehouse/main.go (1)

334-340: LGTM!

internal/api/ingest_test.go (3)

533-551: LGTM!


553-576: LGTM!


599-650: LGTM!

Comment thread docs/src/content/docs/api.md Outdated
Comment thread internal/api/ingest_test.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api HTTP handlers, routing, middleware area/dedupe Deduplication (Pebble, ScyllaDB) area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/observability Metrics, logs, traces, health, profiling documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Status: In review

1 participant