Repository navigation
OSAC-1284: Update Event and Database Migration for ProjectMembership #787
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| 3edebc9c801b9aff1c40e7ce405dccb9642cc494817dd16da29695e10ff4ed28 | ||
| 763636618c139b4e2822f2d82c41adee6d00090d23d003548785af4a2fd17aa7 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,114 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Copyright (c) 2026 Red Hat Inc. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- the License. You may obtain a copy of the License at | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- specific language governing permissions and limitations under the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Create the project_memberships tables: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- This migration establishes the database schema for ProjectMembership objects following the generic schema pattern. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- ProjectMemberships define which users have access to a project and what their role is (viewer or manager). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- The data column stores: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- - spec: ProjectMembershipSpec (project, role, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- - status: ProjectMembershipStatus (state, message) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- as JSONB. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| create table project_memberships ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id text not null primary key, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| creation_timestamp timestamp with time zone not null default now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| deletion_timestamp timestamp with time zone not null default 'epoch', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| finalizers text[] not null default '{}', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| creator text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tenant text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| labels jsonb not null default '{}'::jsonb, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations jsonb not null default '{}'::jsonb, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data jsonb not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| version integer not null default 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| create table archived_project_memberships ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id text not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| creation_timestamp timestamp with time zone not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| deletion_timestamp timestamp with time zone not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| archival_timestamp timestamp with time zone not null default now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| creator text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tenant text not null default '', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| labels jsonb not null default '{}'::jsonb, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations jsonb not null default '{}'::jsonb, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data jsonb not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| version integer not null default 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Project memberships must belong to a specific organization (tenant). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| alter table project_memberships add constraint project_memberships_tenant_fk foreign key (tenant) references tenants (id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Helper table to enforce uniqueness of (tenant, project, user) tuples. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- A user can only have one membership per project within a tenant. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| create table project_membership_subjects ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tenant text not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| project text not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "user" text not null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| membership text not null references project_memberships(id) on delete cascade, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| primary key (tenant, project, "user") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| create index project_membership_subjects_by_membership on project_membership_subjects (membership); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Trigger function that materializes the (tenant, project, user) tuple from the JSONB data column. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| create function materialize_project_membership_subjects() returns trigger as $$ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| declare | ||||||||||||||||||||||||||||||||||||||||||||||||||
| v_project text; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| v_user text; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| begin | ||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Delete stale rows for this membership: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| delete from project_membership_subjects where membership = new.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Extract project and user from the spec: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| v_project := new.data->'spec'->>'project'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| v_user := new.data->'spec'->>'user'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Insert the new tuple, catching duplicates: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| begin | ||||||||||||||||||||||||||||||||||||||||||||||||||
| insert into project_membership_subjects (tenant, project, "user", membership) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| values (new.tenant, v_project, v_user, new.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception when unique_violation then | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+89
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| end; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return new; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| end; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| $$ language plpgsql; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| create trigger materialize_project_membership_subjects | ||||||||||||||||||||||||||||||||||||||||||||||||||
| after insert or update on project_memberships | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for each row | ||||||||||||||||||||||||||||||||||||||||||||||||||
| execute function materialize_project_membership_subjects(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| -- Backfill existing rows to populate the helper table: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| update project_memberships set data = data; | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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_nameindex, so two rows with the samenamecan still be inserted into the same tenant. That breaks the existing DAO contract ininternal/database/dao/generic_dao_create.goandinternal/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