Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ Uses `pgx/v5` with a generic DAO pattern:
- CEL filter expressions translated to SQL WHERE clauses via `FilterTranslator`
- Migrations in `internal/database/migrations/` (numbered `*.up.sql` files)

### Introducing New Object Types

When a migration needs to introduce a new kind of object, use the `create_object_schema` stored
procedure instead of writing the boilerplate DDL by hand. The procedure receives the plural table
name and creates the object table with all standard DAO columns, the corresponding archive table,
the standard indexes on `name`, `creator`, `tenant`, and `labels`, and the tenant foreign key
constraint referencing the `organizations` table. For example:

```sql
call create_object_schema('widgets');
```

This single call is equivalent to manually creating the `widgets` and `archived_widgets` tables, the
four indexes (`widgets_by_name`, `widgets_by_creator`, `widgets_by_tenant`, `widgets_by_label`), and
the `widgets_tenant_fk` foreign key. Any resource-specific extras such as additional indexes,
immutability triggers, or materialized helper tables should be added in the same migration after the
procedure call.

### Enforcing Cross-Object Constraints

Because objects are stored as JSON-serialized protobuf in a single `data` column, the database
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
--
-- 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.
--

-- This migration creates a reusable stored procedure that sets up the full database schema for a new object type. It
-- creates the object table with all standard DAO columns, the corresponding archive table, the standard set of indexes
-- for name, creator, tenant, and label queries, and the tenant foreign key constraint referencing the organizations
-- table.
--
-- Future migrations that introduce a new object type can call this procedure instead of repeating the boilerplate DDL.

create procedure create_object_schema(object_name text) language plpgsql as $$
begin
-- Create the object table with all standard columns expected by the generic DAO:
execute format(
$ddl$
create table %I (
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
)
$ddl$,
object_name
);

-- Create the archive table. It mirrors the object table but replaces the primary key with a plain column (multiple
-- archived versions of the same object may coexist), drops the finalizers column (archived objects are no longer
-- subject to finalization), removes defaults from 'creation_timestamp' and 'deletion_timestamp' (they are copied from
-- the original row), and adds an 'archival_timestamp' column:
execute format(
$ddl$
create table %I (
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
)
$ddl$,
'archived_' || object_name
);

-- Create B-tree indexes for the columns used most frequently in queries:
execute format('create index %I on %I (name)', object_name || '_by_name', object_name);
execute format('create index %I on %I (creator)', object_name || '_by_creator', object_name);
execute format('create index %I on %I (tenant)', object_name || '_by_tenant', object_name);

-- Create a GIN index on the labels column for containment queries:
execute format('create index %I on %I using gin (labels)', object_name || '_by_label', object_name);

-- Add a foreign key constraint ensuring the tenant references an existing tenant:
execute format(
'alter table %I add constraint %I foreign key (tenant) references organizations (id)',
object_name, object_name || '_tenant_fk'
);
end;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
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.
*/

package migrations

import (
"context"

. "github.com/onsi/ginkgo/v2/dsl/core"
. "github.com/onsi/gomega"
)

var _ = DescribeMigration("Create object tables procedure", func() {
It("Creates the 'create_object_schema' procedure", func(ctx context.Context) {
err := tool.Migrate(ctx, 51)
Expect(err).ToNot(HaveOccurred())

var count int
row := conn.QueryRow(ctx, `
select
count(*)
from
pg_catalog.pg_proc p
join
pg_catalog.pg_namespace n on n.oid = p.pronamespace
where
n.nspname = 'public' and
p.proname = 'create_object_schema' and
p.prokind = 'p'
`)
err = row.Scan(&count)
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(1))
})
})
Loading