Skip to content

feat(messaging): add key, owner, source and hash columns to templates - #118769

Closed
Silthus wants to merge 1 commit into
PostHog:masterfrom
Silthus:spec/code-managed-templates-migration
Closed

Silthus wants to merge 1 commit into
PostHog:masterfrom
Silthus:spec/code-managed-templates-migration

Conversation

@Silthus

@Silthus Silthus commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Developers who keep email templates in their repository have no way to point PostHog at "the welcome template" of a project.
A message template has only a UUID, no owner, and no record of which file or content it came from.
The code-managed templates spec (Silthus/posthog#485) needs those columns before any upload or send-by-key route can exist.

Changes

Nothing changes for users yet. This is the migration-only base of the code-managed templates stack.

  • 0007 adds seven nullable columns to posthog_messagetemplate: key, managed_by (gui or code), source_repository, source_path, source_ref, content_hash and superseded_content_hashes.
  • 0008 adds a partial unique index on (team_id, key) for templates that have a key and are not deleted. A deleted template releases its key.
  • 0008 builds the index with CREATE UNIQUE INDEX CONCURRENTLY through CreateIndexConcurrently, in its own atomic = False migration.
  • No default and no backfill. No serializer, view or frontend reads or writes the columns. The serializers in this product list their fields by hand, so nothing leaks into the API.
  • The model carries help_text now, so the serializer slice can expose the fields without another AlterField migration.

Important

#118664 also adds messaging migration 0007. Whichever PR lands second renumbers its migrations.

Note

🔵 Merge risk: low · two-way door (merge-risk rubric)

  • Schema change: seven ADD COLUMN ... NULL statements take a brief lock on posthog_messagetemplate, which is not a hot table.
  • The unique index builds concurrently with timeouts off, and covers no existing row, because every key is null after 0007.
  • Rolling back 0008 and 0007 removes the index and the columns. No code writes them, so no data is lost.

How did you test this code?

  • Test seam: the database constraint, through MessageTemplate.objects.create in products/messaging/backend/tests/test_message_template_key.py.
  • Red, before the change: all three cases fail with MessageTemplate() got unexpected keyword arguments: 'key'.
  • Green, after it: the same three cases pass. The messaging backend suite passes locally too.
  • Mutation check: a test index without the NOT "deleted" clause fails test_the_key_is_free_1_after_the_holder_is_deleted.
  • analyze_migration_risk --fail-on-blocked: both migrations are Safe, none blocked.
  • Local stack: migrate messaging 0008 and back to 0005 both run cleanly. The index and the columns appear, then disappear.

Test rationale: the test catches a constraint that blocks the same key in another team, or that a deleted template keeps holding. No existing test covers message template keys. The test needs Postgres, because the rule lives in a partial index.

sqlmigrate output
-- sqlmigrate messaging 0007
BEGIN;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "content_hash" varchar(80) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "key" varchar(400) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "managed_by" varchar(400) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "source_path" varchar(400) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "source_ref" varchar(400) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "source_repository" varchar(400) NULL;
ALTER TABLE "posthog_messagetemplate" ADD COLUMN "superseded_content_hashes" varchar(80)[] NULL;
COMMIT;

-- sqlmigrate messaging 0008
SET lock_timeout = 0;
SET statement_timeout = 0;
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "unique_message_template_key_per_team" ON "posthog_messagetemplate" ("team_id", "key") WHERE (NOT "deleted" AND "key" IS NOT NULL);
Local migrate forward and back
$ python manage.py migrate messaging 0008
  Applying messaging.0006_messagetemplate_transactional_hog_flow_id... OK
  Applying messaging.0007_messagetemplate_code_managed_fields... OK
  Applying messaging.0008_messagetemplate_unique_key_per_team... OK
$ psql: index
CREATE UNIQUE INDEX unique_message_template_key_per_team ON public.posthog_messagetemplate USING btree (team_id, key) WHERE ((NOT deleted) AND (key IS NOT NULL))
$ python manage.py migrate messaging 0005
  Unapplying messaging.0008_messagetemplate_unique_key_per_team... OK
  Unapplying messaging.0007_messagetemplate_code_managed_fields... OK
  Unapplying messaging.0006_messagetemplate_transactional_hog_flow_id... OK
$ psql: index after rollback
0

Proofs owed per the proof rubric:

Proof Status
Live run The local migrate forward and back above. No runtime path reads the columns yet.
Prod shape Not owed. The index build reads only rows with a key, and 0007 leaves every key null.
Cross-version Not owed. Old pods insert without the new columns, which are nullable. No Node or Rust service reads this table.

Release status

  • No feature flag controls this change

Docs update

None. Nothing user-facing changes.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5

  • Built from ticket Silthus/posthog#486 of the code-managed templates map, test-first.
  • Skills: /tdd, /django-migrations, /writing-tests, /pr-description, /writing-pr-descriptions.
  • Ported by hand from the earlier workflow key and ownership migrations, not cherry-picked.
  • Duplicate search: no open PR adds these columns.
  • Public artifact: the diff and this body carry no session material. The test data is invented.
Decision Who Where
A soft-deleted template releases its key 👤 Human #484 handoff
The index name is unique_message_template_key_per_team, not the workflow one, because Postgres index names share one namespace 🤖 Agent #486 resolution
help_text sits on the model now, to avoid a later no-SQL migration when the serializer exposes the fields 🤖 Agent #486 resolution
managed_by uses max_length=400 and the same choices as the workflow column 🤖 Agent #486 resolution

Involvement: 1 spec grilling round answered, 1 cross-model review (GPT-6.1 Sol, high: no findings at a5dbb10), proof captured.

🤖 Generated with Claude Code

Seven nullable columns on the message template, and a partial unique
index on (team, key) for templates that are not deleted. The index is
built concurrently in its own non-atomic migration. Nothing reads or
writes the columns yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Oct 16, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@Silthus

Silthus commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 writing on behalf of Michael

Closing: #118924 now carries these migrations (0009 and 0010).

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.

1 participant