Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.

OSAC-1284: Update Event and Database Migration for ProjectMembership - #787

Merged
CrystalChun merged 3 commits into
osac-project:mainfrom
CrystalChun:proj-mem
Jun 29, 2026
Merged

CrystalChun merged 3 commits into
osac-project:mainfrom
CrystalChun:proj-mem

Conversation

@CrystalChun

@CrystalChun CrystalChun commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Description

Relevant changes:

  • Extends the Event message to support ProjectMembership as a payload type
  • Creates migration 67 with project_memberships and archived_project_memberships tables following the standard DAO schema.
  • Enforce unique memberships to prevent users from having multiple memberships in the same project

ProjectMembership enables assigning and unassigning users from projects.

Testing

  • buf lint passes
  • go build passes
  • unit tests pass

/cc @jhernand

Summary by CodeRabbit

  • New Features

    • Added support for project memberships, including archived records and related event payloads.
    • Membership data now enforces uniqueness per tenant, project, and user, with clearer conflict handling.
  • Bug Fixes

    • Improved database error messages for duplicate records so users see more helpful details.
    • Updated database validation to better exclude non-object tables from schema checks.

@openshift-ci-robot

openshift-ci-robot commented Jun 26, 2026 •

Copy link
Copy Markdown

@CrystalChun: This pull request references OSAC-1284 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the sub-task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Description

Relevant changes:

  • Extends the Event message to support ProjectMembership as a payload type
  • Creates migration 66 with project_memberships and archived_project_memberships tables following the standard DAO schema.

ProjectMembership enables assigning and unassigning users from projects.

Testing

  • buf lint passes
  • go build passes
  • unit tests pass

/cc @jhernand

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested a review from jhernand June 26, 2026 14:20
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds migration 67 creating project_memberships, archived_project_memberships, and project_membership_subjects tables with a PL/pgSQL trigger enforcing (tenant, project, user) uniqueness via errcode Z0004. Updates the DAO error layer to surface trigger-raised messages through ErrAlreadyExists.Reason. Adds migration integration tests and registers ProjectMembership as an Event proto payload.

Changes

Project Membership Schema, DAO Errors, and Events

Layer / File(s) Summary
Migration: tables, indexes, and uniqueness trigger
internal/database/migrations/67_create_project_memberships_tables.up.sql, internal/database/migrations.sha256, internal/database/database_tool.go
Creates project_memberships, archived_project_memberships, and project_membership_subjects tables with indexes, tenant FK, and a PL/pgSQL trigger that enforces (tenant, project, user) uniqueness by raising errcode Z0004 on collision. Excludes project_membership_subjects from the object-table tool scan and updates the migration checksum.
DAO: map trigger errors to ErrAlreadyExists
internal/database/dao/dao_errors.go, internal/database/dao/generic_dao_create.go, internal/database/dao/generic_dao_update.go
Adds Reason string to ErrAlreadyExists and updates Error() to return it when set. Changes translateError in both create and update DAOs to return ErrAlreadyExists (with Reason from pgErr.Message when available) for errNotUniqueCode instead of ErrNotUnique.
Migration 67 integration tests
internal/database/migrations/67_create_project_memberships_tables_test.go
Covers table existence, row insert/query, index presence, tenant FK violation, cross-tenant name scoping, column metadata/nullability, default values, Z0004 duplicate rejection with exact message, and multi-user same-project acceptance.
Proto: ProjectMembership event payload
proto/private/osac/private/v1/event_type.proto
Imports project_membership_type.proto and adds ProjectMembership project_membership = 33 to the Event message's oneof payload.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

lgtm

Suggested reviewers

  • jhernand
  • ygalblum

Poem

🏛️ A table is born, with tenant and key,
A trigger stands guard — "no duplicates, see!"
Z0004 it cries when two members collide,
ErrAlreadyExists now carries the Reason inside.
Proto gets its field, the checksum is new,
Project memberships land — the schema breaks through! 🎉

🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Ai-Attribution ⚠️ Warning HEAD uses Co-Authored-By: Claude Sonnet 4.5, but no Assisted-by/Generated-by trailer is present. Replace the AI attribution trailer with Red Hat’s Assisted-by or Generated-by format; do not use Co-Authored-By for AI tools.
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main changes: Event support and database migration work for ProjectMembership.
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.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, credentials, private keys, or token-like literals were added; the only long literal is a SHA-256 checksum.
No-Weak-Crypto ✅ Passed No MD5/SHA1/DES/RC4/3DES/Blowfish/ECB, custom crypto, or secret-comparison code appears in the touched files; the only hash file is a SHA-256 checksum.
No-Injection-Vectors ✅ Passed No new injection vectors found; the PR uses parameterized SQL/PLpgSQL and error-message formatting only, with no eval/shell/yaml/dangerous HTML use.
Container-Privileges ✅ Passed No container/K8s manifests were touched, and the changed files contain no privileged settings or securityContext flags.
No-Sensitive-Data-In-Logs ✅ Passed No new log statements or secret-bearing fields were added; the changes only adjust error strings and trigger exceptions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

🤖 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 `@internal/database/migrations/66_create_project_memberships_tables.up.sql`:
- Around line 24-58: `project_memberships` currently has no uniqueness guarantee
for the membership subject tuple, so duplicate `(tenant, project, user)` rows
can be inserted via the `data` JSON. Add the repo’s materialized helper-table
plus trigger pattern around `project_memberships` to materialize `(tenant,
data->'spec'->>'project', data->'spec'->>'user')` and enforce a unique
constraint there, then wire the trigger logic so inserts/updates reject
duplicates. Also add a migration test that verifies the duplicate-insert path
fails, using the existing `project_memberships` migration/test conventions.
🪄 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: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5275ef10-eb20-4dbb-a212-f3cf75b888ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3662208 and b3801d3.

⛔ Files ignored due to path filters (2)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (4)
  • internal/database/migrations.sha256
  • internal/database/migrations/66_create_project_memberships_tables.up.sql
  • internal/database/migrations/66_create_project_memberships_tables_test.go
  • proto/private/osac/private/v1/event_type.proto

@CrystalChun
CrystalChun force-pushed the proj-mem branch 2 times, most recently from 37c4d25 to b10b457 Compare June 26, 2026 20:39

@tzvatot tzvatot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary

This PR establishes the database foundation for ProjectMembership - overall follows the established patterns well (materialized helper table + trigger from migration 66). However, there is a critical error code mismatch that will cause the wrong error type at runtime.

Category Count
🔴 Critical 1
🟡 Important 1
💡 Suggestion 1

💡 AI attribution trailer

The HEAD commit uses Co-Authored-By for AI attribution. Per Red Hat convention, use Assisted-by: Claude Code <noreply@anthropic.com> instead.

What looks good

  • Migration pattern matches the established tenant_domains pattern from migration 66
  • Thorough test coverage: table creation, insert/query, indexes, FK enforcement, cross-tenant isolation, column metadata, defaults, and duplicate rejection
  • Proto change is minimal and correctly scoped
  • The backfill line (update project_memberships set data = data) is a smart way to trigger materialization for existing rows

exception when unique_violation then
raise exception using
errcode = 'Z0004',
message = format('user ''%s'' already has a membership in project ''%s'' within tenant ''%s''', v_user, v_project, new.tenant);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Error code mismatch: trigger raises Z0004 but DAO handles Z0005

This trigger raises errcode = 'Z0004' (errNotUniqueCode), but the new DAO changes in dao_errors.go define a separate errDuplicateMembershipCode = "Z0005" with handlers in generic_dao_create.go and generic_dao_update.go.

Since the trigger raises Z0004, the existing errNotUniqueCode handler catches it and returns ErrNotUnique (the raw trigger message). The new Z0005 handler is dead code and ErrAlreadyExists is never returned for duplicate memberships.

Fix: Either change the trigger to raise Z0005 to match the DAO handler, or remove the new error code and rely on the existing Z0004/ErrNotUnique path (which is what tenant_domains uses).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this! Updated it to use the existing error code

Comment thread internal/database/database_tool.go Outdated
'tenant_domains'
)
) and
c.relname not in ('notifications', 'schema_migrations', 'project_membership_subjects')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Redundant NOT IN clause

This adds a second NOT IN that duplicates notifications and schema_migrations from the clause above. Just add 'project_membership_subjects' to the existing list:

c.relname not in (
    'notifications',
    'schema_migrations',
    'tenant_domains',
    'project_membership_subjects'
)

@CrystalChun CrystalChun Jun 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Modified as suggested. Thanks for catching this!

tenant text not null,
project text not null,
username text not null,
membership_id text not null references project_memberships(id) on delete cascade,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In other places we don't use the ..._id suffix. Can we rename this to just membership to make it more consistent?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes thank you! Removed this suffix

create table project_membership_subjects (
tenant text not null,
project text not null,
username text not null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this should be just user, as it is a reference to the user. It may be the user name, or the user identifier, but that shouldn't affect the name of this column. That way we can decide what is the best unique identifier of a user without changing the name of this column.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That makes sense! Thanks Juan, updated it to be user

Comment thread internal/database/dao/dao_errors.go Outdated
errNotUniqueCode = "Z0004"
// errDuplicateMembershipCode is the SQLSTATE error code returned by the 'materialize_project_membership_subjects'
// trigger when an insert or update attempts to create a duplicate (tenant, project, user) tuple.
errDuplicateMembershipCode = "Z0005"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can reuse errNotUniqueCode for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed this and reused the not unique code. Thank you!

exception when unique_violation then
raise exception using
errcode = 'Z0004',
message = format('user ''%s'' already has a membership in project ''%s'' within tenant ''%s''', v_user, v_project, new.tenant);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we remove the within tenant ... part? Regular users will only see a tenant, so we don't need to tell them what tenant it is.

What would be really helpful is to include in the message the name of the existing membership, something like user 'my-user' is already a member of project 'my-project' via membership 'my-membership'. Is that doable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That makes sense for the tenant, removed that part and modified the message to include project and membership.
Thank you!

case errDuplicateMembershipCode:
return &ErrAlreadyExists{
ID: id,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Aren't we loosing the nice error message that we prepared in the trigger? Can we check if if the database has populated the Message field of the error and copy it to a new Reason field of ErrAlreadyExists? We can then change the String method of ErrAlreadyExists to return that, and the gRPC server will already translate that into the appropriate gRPC error.

Also, can you add a unit test to verify this behavior, including that it generates the expected error message?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the suggestion! I've attempted to do this, but I'm not sure it's fully correct. How does it look?

@CrystalChun
CrystalChun force-pushed the proj-mem branch 2 times, most recently from 4bf4acc to 6652a58 Compare June 29, 2026 15:06
}
case errNotUniqueCode:
// Project membership uniqueness violations should return ErrAlreadyExists with the custom message
if strings.Contains(pgErr.Message, "is already a member of project") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we shouldn't check here for a substring of the message: it is brittle. As this will only happen when we explicilty set the Z.... error code I think we can assume that the message will also be something that we set explicitly. So we can just check if pgErr.Message is set, and if it is then set Reason on the error. Otherwise we set Id.

CrystalChun and others added 3 commits June 29, 2026 13:34
Add ProjectMembership as a valid payload type in the Event message so
controllers can receive create/update/delete events for project
memberships.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Create migration 67 adding project_memberships and
archived_project_memberships tables with standard DAO schema and
indexes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…memberships

Add materialized helper table pattern to prevent duplicate project membership
assignments. Uses trigger-based constraint enforcement with custom error code Z0004.

Changes:
- Add errDuplicateMembershipCode constant (Z0004)
- Translate Z0004 to ErrAlreadyExists in create and update paths
- Exclude project_membership_subjects helper table from schema validation
- Add tests verifying duplicate rejection and valid multi-user scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Jun 29, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: CrystalChun, jhernand

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@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

🤖 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 `@internal/database/migrations/67_create_project_memberships_tables.up.sql`:
- Around line 52-55: The migration currently adds only a non-unique name index,
so update the project_memberships table definition to add the tenant-scoped
unique constraint/index for (tenant, name) using a name that includes
_unique_name_ so it matches the DAO logic in generic_dao_create and
generic_dao_update. Keep the existing non-unique indexes if needed, but ensure
the new unique constraint is the one used for name collision detection. Also
extend the project_memberships test coverage to include a same-tenant
duplicate-name negative case in addition to the existing different-tenant
behavior.
- Around line 89-100: The duplicate-membership error in the membership-check
block can emit an empty identifier because project_memberships.name may be
blank. Update the logic in the section that selects existing_membership_name
from project_membership_subjects/project_memberships so it falls back to the
membership ID when name is empty, using coalesce(nullif(pm.name, ''), pm.id) or
equivalent before the raise exception message is built.
🪄 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: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 208223fb-c4ec-4365-9263-dbe2403c15b2

📥 Commits

Reviewing files that changed from the base of the PR and between b3801d3 and 8e7147b.

⛔ Files ignored due to path filters (2)
  • internal/api/osac/private/v1/event_type.pb.go is excluded by !**/*.pb.go
  • internal/api/osac/private/v1/event_type_protoopaque.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (8)
  • internal/database/dao/dao_errors.go
  • internal/database/dao/generic_dao_create.go
  • internal/database/dao/generic_dao_update.go
  • internal/database/database_tool.go
  • internal/database/migrations.sha256
  • internal/database/migrations/67_create_project_memberships_tables.up.sql
  • internal/database/migrations/67_create_project_memberships_tables_test.go
  • proto/private/osac/private/v1/event_type.proto
💤 Files with no reviewable changes (1)
  • proto/private/osac/private/v1/event_type.proto

Comment on lines +52 to +55
create index project_memberships_by_name on project_memberships (name);
create index project_memberships_by_creator on project_memberships (creator);
create index project_memberships_by_tenant on project_memberships (tenant);
create index project_memberships_by_label on project_memberships using gin (labels);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add the tenant-scoped name uniqueness constraint.

This migration only creates a non-unique project_memberships_by_name index, so two rows with the same name can still be inserted into the same tenant. That breaks the existing DAO contract in internal/database/dao/generic_dao_create.go and internal/database/dao/generic_dao_update.go, which looks for a _unique_name_ constraint to classify name collisions, and it makes the "same name in different tenants" test assert only half of the intended behavior. Please add the usual unique (tenant, name) constraint/index with a name containing _unique_name_, plus a same-tenant negative test.

🧰 Tools
🪛 SQLFluff (4.2.2)

[error] 52-52: CREATE INDEX should use CONCURRENTLY to avoid locking the table during the build.

(PG01)


[error] 53-53: CREATE INDEX should use CONCURRENTLY to avoid locking the table during the build.

(PG01)


[error] 54-54: CREATE INDEX should use CONCURRENTLY to avoid locking the table during the build.

(PG01)


[error] 55-55: CREATE INDEX should use CONCURRENTLY to avoid locking the table during the build.

(PG01)

🤖 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 `@internal/database/migrations/67_create_project_memberships_tables.up.sql`
around lines 52 - 55, The migration currently adds only a non-unique name index,
so update the project_memberships table definition to add the tenant-scoped
unique constraint/index for (tenant, name) using a name that includes
_unique_name_ so it matches the DAO logic in generic_dao_create and
generic_dao_update. Keep the existing non-unique indexes if needed, but ensure
the new unique constraint is the one used for name collision detection. Also
extend the project_memberships test coverage to include a same-tenant
duplicate-name negative case in addition to the existing different-tenant
behavior.

Comment on lines +89 to +100
declare
existing_membership_name text;
begin
select pm.name into existing_membership_name
from project_membership_subjects pms
join project_memberships pm on pm.id = pms.membership
where pms.tenant = new.tenant and pms.project = v_project and pms."user" = v_user;

raise exception using
errcode = 'Z0004',
message = format('user ''%s'' is already a member of project ''%s'' via membership ''%s''',
v_user, v_project, existing_membership_name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fall back to the membership ID when the name is empty.

project_memberships.name defaults to '', so this path can legitimately raise via membership '' for duplicates created without a name. Use coalesce(nullif(pm.name, ''), pm.id) (or equivalent) so the error always includes a usable identifier.

Suggested fix
-      select pm.name into existing_membership_name
+      select coalesce(nullif(pm.name, ''), pm.id) into existing_membership_name
         from project_membership_subjects pms
         join project_memberships pm on pm.id = pms.membership
         where pms.tenant = new.tenant and pms.project = v_project and pms."user" = v_user;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
declare
existing_membership_name text;
begin
select pm.name into existing_membership_name
from project_membership_subjects pms
join project_memberships pm on pm.id = pms.membership
where pms.tenant = new.tenant and pms.project = v_project and pms."user" = v_user;
raise exception using
errcode = 'Z0004',
message = format('user ''%s'' is already a member of project ''%s'' via membership ''%s''',
v_user, v_project, existing_membership_name);
declare
existing_membership_name text;
begin
select coalesce(nullif(pm.name, ''), pm.id) into existing_membership_name
from project_membership_subjects pms
join project_memberships pm on pm.id = pms.membership
where pms.tenant = new.tenant and pms.project = v_project and pms."user" = v_user;
raise exception using
errcode = 'Z0004',
message = format('user ''%s'' is already a member of project ''%s'' via membership ''%s''',
v_user, v_project, existing_membership_name);
🤖 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 `@internal/database/migrations/67_create_project_memberships_tables.up.sql`
around lines 89 - 100, The duplicate-membership error in the membership-check
block can emit an empty identifier because project_memberships.name may be
blank. Update the logic in the section that selects existing_membership_name
from project_membership_subjects/project_memberships so it falls back to the
membership ID when name is empty, using coalesce(nullif(pm.name, ''), pm.id) or
equivalent before the raise exception message is built.

@CrystalChun

Copy link
Copy Markdown
Contributor Author

Juan has already approved, merging.

@CrystalChun
CrystalChun merged commit e58d049 into osac-project:main Jun 29, 2026
13 of 14 checks passed
DakCrowder pushed a commit to DakCrowder/fulfillment-service that referenced this pull request Jun 30, 2026
…sac-project#787)

* OSAC-1284: Add ProjectMembership to Event payload

Add ProjectMembership as a valid payload type in the Event message so
controllers can receive create/update/delete events for project
memberships.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* OSAC-1284: Add project_memberships database tables

Create migration 67 adding project_memberships and
archived_project_memberships tables with standard DAO schema and
indexes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* OSAC-1284: Enforce unique (tenant, project, user) tuples for project memberships

Add materialized helper table pattern to prevent duplicate project membership
assignments. Uses trigger-based constraint enforcement with custom error code Z0004.

Changes:
- Add errDuplicateMembershipCode constant (Z0004)
- Translate Z0004 to ErrAlreadyExists in create and update paths
- Exclude project_membership_subjects helper table from schema validation
- Add tests verifying duplicate rejection and valid multi-user scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
akshaynadkarni pushed a commit to rgolangh/fulfillment-service that referenced this pull request Jul 13, 2026
…sac-project#787)

* OSAC-1284: Add ProjectMembership to Event payload

Add ProjectMembership as a valid payload type in the Event message so
controllers can receive create/update/delete events for project
memberships.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* OSAC-1284: Add project_memberships database tables

Create migration 67 adding project_memberships and
archived_project_memberships tables with standard DAO schema and
indexes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* OSAC-1284: Enforce unique (tenant, project, user) tuples for project memberships

Add materialized helper table pattern to prevent duplicate project membership
assignments. Uses trigger-based constraint enforcement with custom error code Z0004.

Changes:
- Add errDuplicateMembershipCode constant (Z0004)
- Translate Z0004 to ErrAlreadyExists in create and update paths
- Exclude project_membership_subjects helper table from schema validation
- Add tests verifying duplicate rejection and valid multi-user scenarios

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants