Skip to content

feat: support PostgreSQL generated columns via the generated tag#345

Open
h2zi wants to merge 1 commit into
go-gorm:masterfrom
h2zi:fix/7191-identity-autoincrement-type
Open

feat: support PostgreSQL generated columns via the generated tag#345
h2zi wants to merge 1 commit into
go-gorm:masterfrom
h2zi:fix/7191-identity-autoincrement-type

Conversation

@h2zi

@h2zi h2zi commented Jun 25, 2026

Copy link
Copy Markdown

What this adds

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). Relates to go-gorm/gorm#7191.

type Product struct {
    ID       uint    `gorm:"primaryKey;generated:identity"`              // bigint GENERATED BY DEFAULT AS IDENTITY
    Price    float64 `gorm:"type:numeric"`
    Quantity int
    Total    float64 `gorm:"->;type:numeric;generated:price * quantity"` // numeric GENERATED ALWAYS AS (price * quantity) STORED
}

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 identity also works).

Computed columns

  • Any other value is taken verbatim as the expression of a STORED computed column: generated:price * quantityGENERATED ALWAYS AS (price * quantity) STORED. Commas inside the expression are preserved, so generated: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 always GENERATED ALWAYS AS (...) STORED (there is no BY DEFAULT form).

Why a tag instead of type:

Putting bigint generated by default as identity inside type: 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 the serial logic. Every mature ORM keeps generation separate from type (SQLAlchemy Identity/Computed, EF Core UseIdentityByDefaultColumn, Drizzle generatedByDefaultAsIdentity/generatedAlwaysAs, TypeORM @Generated("identity")). This tag follows that consensus.

Implementation

DataTypeOf parses the generated tag and renders the appropriate GENERATED ... clause; the base column type is computed exactly as before. No change to value binding — identity primary keys are already AutoIncrement (omitted from INSERT, read back via RETURNING), and computed columns rely on the existing -> read-only permission.

Tests & verification

  • Unit tests added to Test_DataTypeOf for both identity modes, order-independent keywords, computed columns, and comma-safe expressions.
  • Verified end-to-end against a real PostgreSQL 16 instance: CREATE TABLE emits the expected DDL for BY DEFAULT/ALWAYS identity and STORED computed columns; INSERT omits identity & computed columns and reads the identity value back via RETURNING; the computed value is correct; and repeated AutoMigrate runs are idempotent (no ALTER).
CREATE TABLE "products" ("id" bigint GENERATED BY DEFAULT AS IDENTITY,"name" text,"price" numeric,
  "quantity" bigint,"total" numeric GENERATED ALWAYS AS (price * quantity) STORED,PRIMARY KEY ("id"))
CREATE TABLE "events" ("id" bigint GENERATED ALWAYS AS IDENTITY,"name" text,PRIMARY KEY ("id"))

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

Copilot AI review requested due to automatic review settings June 25, 2026 10:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 getSchemaCustomType to skip serial substitution when the custom type already indicates self-managed generation (serial or identity).
  • Extend Test_DataTypeOf with identity-column and regression cases to ensure the behavior is preserved and the prior bigintbigserial conversion 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.

Comment thread postgres_test.go
@h2zi h2zi force-pushed the fix/7191-identity-autoincrement-type branch 2 times, most recently from c3f6a55 to ecf58e0 Compare June 25, 2026 11:47
@h2zi h2zi changed the title fix: keep explicit identity column type for auto increment fields feat: support PostgreSQL generated columns via the generated tag Jun 25, 2026
@h2zi

h2zi commented Jun 25, 2026

Copy link
Copy Markdown
Author

Heads-up: I've reworked this PR after the initial review. Instead of teaching getSchemaCustomType to preserve an identity clause written inside type: (which blesses an approach that conflates the column type with its generation strategy), this now adds a first-class generated tag for both identity and computed columns, and the base type rendering is left untouched. See the updated description for the full design rationale and the real-PostgreSQL verification.

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>
@h2zi h2zi force-pushed the fix/7191-identity-autoincrement-type branch from 662311b to 51fcf37 Compare June 28, 2026 07:35
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.

2 participants