Skip to content

Fix FlatTableProjection partial-mapping events violating NOT NULL (#4255)#4257

Merged
jeremydmiller merged 1 commit intomasterfrom
fix/4255-flat-table-null-constraint
Apr 16, 2026
Merged

Fix FlatTableProjection partial-mapping events violating NOT NULL (#4255)#4257
jeremydmiller merged 1 commit intomasterfrom
fix/4255-flat-table-null-constraint

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #4255

Problem

When a FlatTableProjection maps multiple events to the same table, each event's generated mt_upsert_* function used INSERT … ON CONFLICT DO UPDATE listing only the columns that event populates. If the table had a NOT NULL column not populated by every event, partial-mapping events raised:

23502: null value in column "other_id" violates not-null constraint

PostgreSQL validates the proposed INSERT row against constraints before ON CONFLICT resolves, so the error fires whether the row exists or not.

Fix

FlatTableUpsertFunction now detects events that map a strict subset of the table's non-PK columns and emits an UPDATE-only function for them:

-- Partial event (maps only `field`):
CREATE FUNCTION mt_upsert_proj_eventb(p_id uuid, p_field text) RETURNS void AS $$
BEGIN
  UPDATE proj SET field = p_field WHERE id = p_id;
END;
$$ LANGUAGE plpgsql;

Full-mapping events keep the original INSERT … ON CONFLICT DO UPDATE form, so they can still create rows.

Semantics

  • Partial events update existing rows
  • Partial event on a stream with no row yet → UPDATE matches zero rows, safe no-op
  • Streams should start with a full-mapping event that creates the row (existing expected usage)

Backward-compatible for the common case where every event maps every column (existing test WriteTableWithGuidIdentifierProjection — all 32 existing flat-table tests still pass).

Test plan

  • partial_event_on_existing_row_updates_without_violating_not_null — reproduces the bug, confirms fix
  • partial_event_on_new_stream_is_a_safe_noop — confirms partial event on empty stream does not create a row
  • full_mapping_event_still_uses_insert_on_conflict — confirms backward compatibility
  • All 29 existing FlatTableProjection tests pass (32 total with the new 3)

Files changed

  • src/Marten/Events/Projections/Flattened/FlatTableUpsertFunction.cs — detect partial mapping, emit UPDATE-only SQL
  • src/EventSourcingTests/Projections/Flattened/Bug_4255_flat_table_not_null_constraint.cs — new regression tests
  • docs/events/projections/flat.md — documents the new behavior

🤖 Generated with Claude Code

)

Previously, every event mapped into a FlatTableProjection generated an
INSERT … ON CONFLICT DO UPDATE function using only the columns that
event populates. When the target table had a NOT NULL column not
populated by every event, the partial-mapping event's INSERT would
violate the constraint (PostgreSQL validates the proposed INSERT row
against constraints before ON CONFLICT resolves).

Fix: detect events that map a strict subset of the table's non-PK
columns and generate an UPDATE-only function for them. Full-mapping
events keep the original INSERT … ON CONFLICT DO UPDATE form, so they
can still create rows.

Semantics: partial events update existing rows. If a stream starts
with a partial event before any full-mapping event, the UPDATE matches
zero rows and is a safe no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit f22d022 into master Apr 16, 2026
6 checks passed
@jeremydmiller jeremydmiller deleted the fix/4255-flat-table-null-constraint branch April 16, 2026 17:56
@neistow
Copy link
Copy Markdown

neistow commented Apr 16, 2026

Hello,

First of all, thanks for the fix!

I would like to share some thoughts on flat table design:

A couple of days ago in my team we didn't know if this issue was a bug or by-design solution, so to unblock us we came up with customized class that inherited from FlatTableProjection and overrided a few methods in order to generate correct sql.

We decided to call them ProjectCreate and ProjectUpdate, so our developers, who define FlatTableProjection would know that those methods do different things and would call respective method for 'initiating' and 'updating' events.

I understand that renaming Project method might be a breaking change for the clients, but on the other hand explicitness will prevent the pitfalls of defining no-op mappings.

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.

FlatTableProjection generates invalid upsert statement when target table has not null constraint

2 participants