Add New<Model> attributes records for type-safe record creation - #2725
Open
mpscholten wants to merge 1 commit into
Open
Add New<Model> attributes records for type-safe record creation#2725mpscholten wants to merge 1 commit into
mpscholten wants to merge 1 commit into
Conversation
`newRecord` fills every field via `Default`, so a `NOT NULL` foreign key
without a database default becomes the null UUID
`00000000-0000-0000-0000-000000000000`. That means
newRecord @comment |> createRecord
type-checks but fails the foreign key constraint at runtime — there is no
compile-time signal that `postId` was never set. This is a common footgun,
especially for AI coding agents.
This generates, per table, a `New<Model>` attributes record holding only
the *required* columns (writable, `NOT NULL`, no default, not the primary
key) plus a `new<Model>` smart constructor:
data NewComment = NewComment { postId :: Id Post, body :: Text }
newComment :: NewComment -> Comment
newComment attributes = newRecord @comment
|> set #postId attributes.postId
|> set #body attributes.body
Usage:
newComment NewComment { postId = post.id, body = "Hi" } |> createRecord
Because `New<Model>` is built with record-construction syntax, a forgotten
required field is a `-Wmissing-fields` warning — a hard compile error under
`-Werror=missing-fields`. Unlike the full model it carries no `meta`, `id`,
timestamps or defaultable columns, so there is no boilerplate and no leaking
of the internal `MetaBag`. The constructor returns an ordinary model, so it
composes unchanged with `set` (for optional columns), validation, forms and
`createRecord`. `newRecord` stays for the form workflow.
Required columns are always written to the INSERT regardless of the
touched-fields bitmask, so the provided values reach the database; using
`set` additionally marks them touched, keeping dirty-tracking consistent.
The `New<Model>` type is omitted from the schema-designer preview (like the
SetField instances) and the constructor is only emitted when the SetField
instances are.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Core Size & Compile Allocations Benchmark
HTTP Latency (GET /, 5000 reqs, 10 concurrent)
Top 10 modules (this PR)
|
mpscholten
marked this pull request as ready for review
June 17, 2026 15:41
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
newRecordfills every field via itsDefaultinstance, so aNOT NULLforeign key without a database default becomes the null UUID00000000-0000-0000-0000-000000000000. That means:…type-checks but fails the foreign key constraint at runtime — there is no compile-time signal that
postIdwas never set, and the eventual error is a cryptic Postgres FK violation rather than "you forgotpostId". This is a common footgun, and especially bad for AI coding agents, which is where this idea came from.What this generates
Per table, a
New<Model>attributes record holding only the required columns — writable (no generator),NOT NULL, no database default, and not the primary key — plus anew<Model>smart constructor:Nullable columns, columns with a
DEFAULT/SERIAL, and the primary key are intentionally excluded (they are safe to omit).Usage
Because
New<Model>is built with record-construction syntax, a forgotten required field is a-Wmissing-fieldswarning — a hard compile error under-Werror=missing-fields. Unlike the full model it carries nometa,id, timestamps or defaultable columns, so there is no boilerplate and no leaking of the internalMetaBag.Design notes
set(optional columns), validation, forms andcreateRecord.newRecordstays — it is still the right tool for the form workflow (empty record → fill from params → validate).compileSqlEntry), so the provided values reach the DB; usingsetadditionally marks them touched, keeping dirty-tracking consistent.New<Model>type lives inGenerated.ActualTypes.<Model>; the constructor lives inGenerated.<Model>(next tonewRecord/set).requiredColumnstreats composite-PK member columns as required (correct for join tables, where the user must supply both FKs).Tests
SchemaCompilerSpecexamples still pass.New<Model>type andnew<Model>constructor for aposts/commentsschema.Verification status / open questions
This is a draft / RFC to discuss the approach:
SchemaCompilerSpec(49 examples) passes; the generated string is verified to be exactly the snippet above.build/Generatedend-to-end. The generated code uses only constructs IHP already emits (newRecord @M,set #f,record.field), so it should compile, but this needs confirming — note IHP's own CI may not exercise generated code unless a fixture app does.-Werror=missing-fieldsso the guarantee actually bites?new/createactions usenew<Model> New<Model>{…}instead ofnewRecord?New<Model>type also appear in the schema-designer preview?🤖 Generated with Claude Code