Skip to content

feat(openframe): stamp team_id on CDC-captured tables for shared-DB Debezium - #77

Merged
oleksandrd-flamingo merged 2 commits into
mainfrom
feature/cdc-team-id-stamping
Jul 27, 2026
Merged

feat(openframe): stamp team_id on CDC-captured tables for shared-DB Debezium#77
oleksandrd-flamingo merged 2 commits into
mainfrom
feature/cdc-team-id-stamping

Conversation

@oleksandrd-flamingo

@oleksandrd-flamingo oleksandrd-flamingo commented Jul 24, 2026

Copy link
Copy Markdown

Summary

Under Fleet shared-DB multi-tenancy (one MySQL serving every tenant, gated by FLEET_OPENFRAME_MULTI_TENANCY_ENABLED), a CDC record must carry its own tenant discriminator: the Debezium connector's SMTs are stateless and cannot join host_id → hosts.team_id, and activity_past rows have no host reference at all. This PR stamps a team_id column onto the four CDC-captured tables so the shared-plane stream consumer can resolve the tenant per event (the platform-side connector + shared-stream consumption land in the openframe-saas-shared repo).

This is the CDC foundation of the Debezium-on-shared-MySQL work; it is deliberately option-agnostic (both the shared-consumption and fan-out designs build on it).

Changes

Migration20260722000001_AddTeamIdToCdcTables (fork's separate openframe goose pipeline, not upstream lineage):

  • Nullable team_id INT UNSIGNED NULL on activity_past, activity_host_past, query_results, policy_membership.
  • No index, no FK — a team deletion must not rewrite or cascade over millions of historical CDC rows.
  • Idempotent via columnExists guards.

Write-path stamping — all wrapped in OPENFRAME(mysql-multitenancy) sentinel markers:

Table Stamp source Site
query_results request team pin (fleet.OpenframeTeamID(ctx)) OverwriteQueryResultRows
policy_membership (sync) request team pin RecordPolicyQueryExecutions
policy_membership (async collector) row's own host — (SELECT team_id FROM hosts WHERE id = ?) (cron context, never pinned) AsyncBatchInsertPolicyMembership
activity_past request pin — the only tenant signal for host-less (user/team-level) activities server/activity/internal/mysql/new_activity.go
activity_host_past row's own host via subselect same

Backward compatibility

  • Additive & inert migration: nullable, unindexed, un-FK'd column; existing rows → team_id = NULL; separate goose pipeline (migration_status_openframe) so upstream sync lineage is untouched.
  • No reader breaks: nothing does SELECT * into a fixed struct on these tables.
  • Flag-off is byte-identical: unpinned writes (flag off, or background crons writing host-less activities) keep the SQL statements unchanged and leave team_id NULL. Releasing to the current per-tenant MySQL behaves exactly like fork-main; flipping the flag later starts populating the column.

Testing

MySQL-backed (MYSQL_TEST=1):

  • TestMigrateOpenframeCdcTeamIdStamping — migration idempotency (all four columns) + stamping across pinned (query_results), unpinned-NULL, and async-from-host (policy_membership) paths.
  • TestNewActivity/OpenframeTeamStampingactivity_past stamped from the request pin, activity_host_past stamped from the host's own team via subselect.
  • Flag-off regressions green: TestQueryResults, TestPolicies/RecordPolicyQueryExecutions, TestPolicies/ResetAttemptsOnFailingToPassing{Sync,Async}, full TestNewActivity.
  • go build ./server/... ./cmd/fleet clean; gofmt clean.

Notes

  • Docs updated: openframe/docs/mysql-multitenancy-feature.md (new "CDC team stamping" section + migration row) and openframe/docs/fork-file-manifest.md.
  • The stamping gate for the subselect paths is ctx-pin || IsOpenframeMultitenancy() so the MySQL-backed tests can pin via context without t.Setenv (the parallel harness forbids it); production behavior is identical either way — the SQL reads hosts.team_id, ignoring the pin value.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added OpenFrame multitenancy “team stamping” for CDC writes.
    • Activity, query result, and policy membership records can now be stamped with the appropriate team.
    • Extended MySQL CDC schema to store team_id across relevant tables.
  • Bug Fixes
    • Improved team attribution for host-associated activity and policy records.
    • Preserved prior behavior when no team context is provided.
  • Tests
    • Added coverage for OpenFrame team stamping in activity, query results, and policy membership migrations.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f1b4912f-e62e-45fb-a9a7-606a569a10b0

📥 Commits

Reviewing files that changed from the base of the PR and between 2ba68d8 and fc98567.

📒 Files selected for processing (2)
  • server/activity/internal/mysql/new_activity_test.go
  • server/datastore/mysql/policies.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/activity/internal/mysql/new_activity_test.go
  • server/datastore/mysql/policies.go

Walkthrough

Adds an OpenFrame migration that introduces nullable team_id columns to CDC tables. Activity, query result, and policy membership writes now conditionally stamp pinned team IDs or host-derived team IDs. Tests cover migration behavior and stamping for activities, query results, and policy memberships.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: stamping team_id on CDC-captured tables for OpenFrame shared-DB Debezium.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cdc-team-id-stamping

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.

@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: 1

🧹 Nitpick comments (1)
server/datastore/mysql/policies.go (1)

958-970: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sentinel comment is missing its >>> marker.

Line 963's // OPENFRAME(mysql-multitenancy): ... comment lacks the >>> prefix used everywhere else in this file (e.g., lines 910, 2269), and there's no matching <<< close. This breaks the grep-based fork-audit convention for locating OpenFrame edits during upstream merges.

As per coding guidelines, "Fork edits in shared upstream files must be wrapped in sentinel comments with the format // >>> OPENFRAME(<slug>): <why> — openframe/docs/<doc>.md followed by fork code, then // <<< OPENFRAME(<slug>)."

🏷️ Proposed fix
 			query := fmt.Sprintf(
 				// INSERT IGNORE skips rows whose policy_id no longer exists (policy deleted
 				// after query was distributed but before results arrived).
-				// OPENFRAME(mysql-multitenancy): column list / dup-update carry team_id when the
-				// request is team-pinned (see membershipCols above).
+				// >>> OPENFRAME(mysql-multitenancy): column list / dup-update carry team_id when the
+				// request is team-pinned (see membershipCols above) — openframe/docs/mysql-multitenancy-feature.md.
 				`INSERT IGNORE INTO policy_membership (%s)
 			VALUES %s ON DUPLICATE KEY UPDATE %s`,
 				membershipCols,
 				strings.Join(bindvars, ","),
 				membershipOnDup,
+				// <<< OPENFRAME(mysql-multitenancy)
 			)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/datastore/mysql/policies.go` around lines 958 - 970, Update the
OPENFRAME comment near the policy_membership INSERT in the transaction callback
to use the required `>>> OPENFRAME(mysql-multitenancy)` opening sentinel with
its rationale and documentation reference, and add the matching `<<<
OPENFRAME(mysql-multitenancy)` closing sentinel around the fork-specific SQL
changes. Preserve the existing query behavior and keep the sentinel scope
limited to the OpenFrame modification.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/activity/internal/mysql/new_activity_test.go`:
- Around line 297-303: Update the activity_past query in the test around actID
and actTeam to filter by the specific request pin or other identifiers
established by the test, rather than only activity_type = 'ran_script'. Ensure
the lookup uniquely selects the activity created by this test and does not
depend on arbitrary row ordering or table truncation.

---

Nitpick comments:
In `@server/datastore/mysql/policies.go`:
- Around line 958-970: Update the OPENFRAME comment near the policy_membership
INSERT in the transaction callback to use the required `>>>
OPENFRAME(mysql-multitenancy)` opening sentinel with its rationale and
documentation reference, and add the matching `<<<
OPENFRAME(mysql-multitenancy)` closing sentinel around the fork-specific SQL
changes. Preserve the existing query behavior and keep the sentinel scope
limited to the OpenFrame modification.
🪄 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 Plus

Run ID: 8e33a1c2-1586-45ca-856f-1363995fa6a3

📥 Commits

Reviewing files that changed from the base of the PR and between 3c43e07 and 2ba68d8.

⛔ Files ignored due to path filters (2)
  • openframe/docs/fork-file-manifest.md is excluded by !**/*.md
  • openframe/docs/mysql-multitenancy-feature.md is excluded by !**/*.md
📒 Files selected for processing (6)
  • server/activity/internal/mysql/new_activity.go
  • server/activity/internal/mysql/new_activity_test.go
  • server/datastore/mysql/migrations/openframe/20260722000001_AddTeamIdToCdcTables.go
  • server/datastore/mysql/migrations_openframe_test.go
  • server/datastore/mysql/policies.go
  • server/datastore/mysql/query_results.go

Comment thread server/activity/internal/mysql/new_activity_test.go
oleksandrd-flamingo and others added 2 commits July 27, 2026 14:29
…ebezium

Under Fleet shared-DB multi-tenancy one MySQL serves every tenant, so a CDC
record must carry its own tenant discriminator: the Debezium connector's SMTs
are stateless and cannot join host_id -> hosts.team_id, and activity_past rows
have no host reference at all. This stamps a team_id column on the four
CDC-captured tables so the shared-plane stream consumer can resolve the tenant
per event.

- Migration 20260722000001_AddTeamIdToCdcTables (openframe goose pipeline):
  nullable team_id on activity_past, activity_host_past, query_results,
  policy_membership. No index, no FK (a team deletion must not rewrite/cascade
  over historical CDC rows). Idempotent via columnExists guards.
- Write-path stamping, all wrapped in OPENFRAME(mysql-multitenancy) markers:
  * query_results + sync policy_membership stamp from the request team pin
    (fleet.OpenframeTeamID(ctx) — the agent plane is always pinned in shared
    mode);
  * async policy_membership + activity_host_past stamp from the row's own host
    via (SELECT team_id FROM hosts WHERE id = ?) — those run from unpinned
    cron contexts;
  * activity_past stamps from the request pin — the only tenant signal for
    host-less (user/team-level) activities.

Backward compatible: unpinned writes (flag off, or background crons writing
host-less activities) keep the statements byte-identical and leave team_id
NULL. No SELECT * struct-scan readers exist on these tables, so the added
column cannot break reads. Verified with MySQL-backed tests (migration
idempotency + stamping across pinned/unpinned/async paths) and flag-off
regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- policies.go: wrap the team_id-carrying policy_membership INSERT in the
  >>> / <<< OPENFRAME(mysql-multitenancy) sentinel markers so the grep-based
  fork-audit convention finds it on upstream merges (was a bare comment).
- new_activity_test.go: scope the activity_past lookup to the test's own
  user_id + ORDER BY id DESC LIMIT 1 so it stays deterministic regardless of
  subtest isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oleksandrd-flamingo
oleksandrd-flamingo force-pushed the feature/cdc-team-id-stamping branch from fc98567 to 8a0960e Compare July 27, 2026 12:29
@oleksandrd-flamingo
oleksandrd-flamingo enabled auto-merge (squash) July 27, 2026 12:30
@oleksandrd-flamingo
oleksandrd-flamingo merged commit 7a91716 into main Jul 27, 2026
6 checks passed
@oleksandrd-flamingo
oleksandrd-flamingo deleted the feature/cdc-team-id-stamping branch July 27, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants