feat: support PostgreSQL generated columns via the generated tag#345
feat: support PostgreSQL generated columns via the generated tag#345h2zi wants to merge 1 commit into
generated tag#345Conversation
There was a problem hiding this comment.
Pull request overview
This PR adjusts the PostgreSQL dialector’s custom-type handling during AutoMigrate so that explicit SQL standard identity column types (e.g., ... GENERATED BY DEFAULT/ALWAYS AS IDENTITY) are preserved for auto-increment fields instead of being rewritten to serial/bigserial, addressing the behavior described in go-gorm/gorm#7191.
Changes:
- Update
getSchemaCustomTypeto skipserialsubstitution when the custom type already indicates self-managed generation (serialoridentity). - Extend
Test_DataTypeOfwith identity-column and regression cases to ensure the behavior is preserved and the priorbigint→bigserialconversion remains intact.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| postgres.go | Avoids rewriting explicit identity column types to serial/bigserial when AutoIncrement is true. |
| postgres_test.go | Adds table-driven test cases covering identity-column preservation and existing serial conversion behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c3f6a55 to
ecf58e0
Compare
generated tag
|
Heads-up: I've reworked this PR after the initial review. Instead of teaching |
ecf58e0 to
662311b
Compare
Add first-class support for PostgreSQL 10+ generated columns through a new `generated` struct tag, keeping the value-generation strategy separate from the column `type` (a generation strategy is not a data type): ID uint `gorm:"primaryKey;generated:identity"` // bigint GENERATED BY DEFAULT AS IDENTITY ID uint `gorm:"primaryKey;generated:identity always"` // bigint GENERATED ALWAYS AS IDENTITY Total float64 `gorm:"->;type:numeric;generated:price * quantity"` // numeric GENERATED ALWAYS AS (price * quantity) STORED - `generated:identity` renders a SQL-standard identity column, the modern replacement for serial. The mode defaults to BY DEFAULT so the application can still supply explicit values (e.g. for seeding); `identity always` renders an ALWAYS identity column. - Any other value is taken verbatim as the expression of a STORED computed column. Commas inside the expression are preserved, so functions such as coalesce(a, b) work; combine with the `->` read-only permission. This is the first-class alternative to hand-writing the clause inside `type:`, which leaks the generation clause onto referencing foreign keys and is silently overridden by the serial logic. Relates to go-gorm/gorm#7191 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
662311b to
51fcf37
Compare
What this adds
First-class support for PostgreSQL 10+ generated columns through a new
generatedstruct tag, keeping the value-generation strategy separate from the column type (a generation strategy is not a data type). Relates to go-gorm/gorm#7191.Identity columns (the modern replacement for
serial)generated:identity→<int> GENERATED BY DEFAULT AS IDENTITY. BY DEFAULT is the default mode so the application can still supply explicit values (seeding, data migration) — matching the choice made by Django, EF Core/Npgsql, etc.generated:identity always→<int> GENERATED ALWAYS AS IDENTITY(the database always generates the value). Keywords are order-independent (always identityalso works).Computed columns
STOREDcomputed column:generated:price * quantity→GENERATED ALWAYS AS (price * quantity) STORED. Commas inside the expression are preserved, sogenerated:coalesce(a, b)works. Combine with->to make the column read-only (PostgreSQL forbids writing computed columns).PostgreSQL is asymmetric here and the tag respects it: identity columns offer
{ ALWAYS | BY DEFAULT }, while computed columns are alwaysGENERATED ALWAYS AS (...) STORED(there is noBY DEFAULTform).Why a tag instead of
type:Putting
bigint generated by default as identityinsidetype:conflates the data type with the generation strategy and causes real bugs: the clause leaks onto every referencing foreign key (go-gorm/gorm#7191, go-gorm/gorm#5222), and an auto-increment field's explicit identity type is silently overridden by theseriallogic. Every mature ORM keeps generation separate from type (SQLAlchemyIdentity/Computed, EF CoreUseIdentityByDefaultColumn, DrizzlegeneratedByDefaultAsIdentity/generatedAlwaysAs, TypeORM@Generated("identity")). This tag follows that consensus.Implementation
DataTypeOfparses thegeneratedtag and renders the appropriateGENERATED ...clause; the base column type is computed exactly as before. No change to value binding — identity primary keys are alreadyAutoIncrement(omitted fromINSERT, read back viaRETURNING), and computed columns rely on the existing->read-only permission.Tests & verification
Test_DataTypeOffor both identity modes, order-independent keywords, computed columns, and comma-safe expressions.CREATE TABLEemits the expected DDL forBY DEFAULT/ALWAYSidentity andSTOREDcomputed columns;INSERTomits identity & computed columns and reads the identity value back viaRETURNING; the computed value is correct; and repeatedAutoMigrateruns are idempotent (noALTER).Related
The schema-side half of go-gorm/gorm#7191 — stopping foreign keys from inheriting a referenced key's value-generating clause — is fixed separately in go-gorm/gorm#7816.
🤖 Generated with Claude Code