Skip to content

feat: support generated (computed) columns via the generated tag - #230

Merged
jinzhu merged 1 commit into
go-gorm:masterfrom
h2zi:feat/generated-columns
Jul 31, 2026
Merged

feat: support generated (computed) columns via the generated tag#230
jinzhu merged 1 commit into
go-gorm:masterfrom
h2zi:feat/generated-columns

Conversation

@h2zi

@h2zi h2zi commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What this adds

Support for SQLite STORED generated columns through the generated struct tag, keeping the generation expression separate from the column type. Part of rounding out generated-column support across the GORM drivers (see go-gorm/postgres#345 for the PostgreSQL side, which also covers identity columns). Relates to go-gorm/gorm#7191.

type Product struct {
    ID       uint    `gorm:"primaryKey"`
    Price    float64
    Quantity int
    Total    float64 `gorm:"->;generated:price * quantity"` // real GENERATED ALWAYS AS (price * quantity) STORED
}
  • The generated value is taken verbatim as the expression of a STORED generated column (SQLite 3.31+). Commas inside the expression are preserved, so generated:coalesce(a, b) works.
  • Combine with -> so GORM treats the column as read-only (SQLite forbids writing generated columns).
  • The identity keyword is reserved for identity columns; SQLite has no SQL-standard identity, so generated:identity is left to the existing AUTOINCREMENT handling rather than being mistaken for an expression.

Implementation

DataTypeOf appends the GENERATED ALWAYS AS (...) STORED clause when a computed expression is present; the base column type is computed exactly as before.

Tests & verification

  • Unit test TestDataTypeOfGeneratedColumn covers the computed clause, comma-safe expressions, and the reserved identity keyword.
  • Verified end-to-end against real SQLite: CREATE TABLE emits "total" real GENERATED ALWAYS AS (price * quantity) STORED, INSERT omits the generated column, the read-back value is correct, and repeated AutoMigrate runs are idempotent.
CREATE TABLE `products` (`id` integer PRIMARY KEY AUTOINCREMENT,`name` text,`price` real,
  `quantity` integer,`total` real GENERATED ALWAYS AS (price * quantity) STORED)

🤖 Generated with Claude Code

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

Adds SQLite support in the GORM SQLite dialector for STORED generated (computed) columns via a generated struct tag by appending the GENERATED ALWAYS AS (...) STORED clause to the column’s SQL type during migration.

Changes:

  • Extend Dialector.DataTypeOf to emit ... GENERATED ALWAYS AS (<expr>) STORED when a GENERATED tag setting is present.
  • Add parsing helpers to treat identity (optionally with always / by default) as a reserved keyword (i.e., not a computed expression).
  • Add unit test coverage for the generated-column datatype rendering behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
sqlite.go Adds generated-column handling in DataTypeOf and helpers to interpret the generated tag and reserve identity.
generated_test.go Adds unit tests validating generated-column datatype rendering (including comma-containing expressions and reserved identity).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread generated_test.go
Render SQLite STORED generated columns from a `generated` tag, keeping the
generation expression separate from the column type:

  Total float64 `gorm:"->;generated:price * quantity"`
  // -> "total" real GENERATED ALWAYS AS (price * quantity) STORED

The expression is taken verbatim, so commas inside it (e.g. coalesce(a, b))
are preserved; combine with the `->` read-only permission. The `identity`
keyword is reserved for identity columns and is rendered through SQLite's
native AUTOINCREMENT, so it is not treated as a computed-column expression.

Relates to go-gorm/gorm#7191

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@h2zi
h2zi force-pushed the feat/generated-columns branch from 94d4368 to 13d7cf0 Compare June 28, 2026 07:35
h2zi added a commit to libtnb/sqlite that referenced this pull request Jul 31, 2026
Port of go-gorm/sqlite#230. DataTypeOf appends `GENERATED ALWAYS AS
(expr) STORED` when a computed expression is present (SQLite 3.31+);
the `identity` keyword stays reserved for the native AUTOINCREMENT
handling, and a bare `generated` tag is ignored.

The table-rebuild side was already in place here: getColumns() excludes
generated columns from the INSERT INTO ... SELECT data copy, so
AlterColumn/DropColumn on tables with computed columns work — covered
by a dedicated regression test alongside the end-to-end and idempotency
ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jinzhu
jinzhu merged commit 69c4707 into go-gorm:master Jul 31, 2026
3 checks passed
davidpavlovschi added a commit to glebarez/sqlite that referenced this pull request Aug 2, 2026
Ports every behaviour-changing upstream commit since 7544227, verified
against the modernc backend:

- 13be5e3 (go-gorm#236): preserve indexes, triggers, table options and views
  across table rebuilds
- d450667 (go-gorm#235): HasColumn matches exactly via pragma_table_info
- 69c4707 (go-gorm#230): generated (computed) columns via the generated tag
- fc0cfd3 (go-gorm#207): parse CHECK/CONSTRAINT/FOREIGN KEY table-level lines
- 5406e41 (go-gorm#198): case/whitespace-tolerant constraint matching
- 02b8e06 (go-gorm#193): parseAllColumns state machine for composite keys
- 7230345 (go-gorm#229): disable foreign keys during DropColumn
- 6f07b51: don't overwrite caller-registered clause builders
- 75dbf08 (go-gorm#222): accept tab between column name and type
- bbca3b3 (go-gorm#185): exported Config and New(Config) constructor
- 5df1f76 (go-gorm#219): remove stray Debug() from GetIndexes

migrator.go, ddlmod.go and ddlmod_parse_all_columns.go are taken from
upstream verbatim; sqlite.go is hand-merged to keep this fork's imports,
DriverName, and its error-code-based Translate. Upstream's test files
for these paths are ported unchanged.

Refs #152
davidpavlovschi added a commit to glebarez/sqlite that referenced this pull request Aug 2, 2026
…nslation (#155)

* Port 11 behaviour fixes from go-gorm/sqlite (parity to upstream 525c431)

Ports every behaviour-changing upstream commit since 7544227, verified
against the modernc backend:

- 13be5e3 (go-gorm#236): preserve indexes, triggers, table options and views
  across table rebuilds
- d450667 (go-gorm#235): HasColumn matches exactly via pragma_table_info
- 69c4707 (go-gorm#230): generated (computed) columns via the generated tag
- fc0cfd3 (go-gorm#207): parse CHECK/CONSTRAINT/FOREIGN KEY table-level lines
- 5406e41 (go-gorm#198): case/whitespace-tolerant constraint matching
- 02b8e06 (go-gorm#193): parseAllColumns state machine for composite keys
- 7230345 (go-gorm#229): disable foreign keys during DropColumn
- 6f07b51: don't overwrite caller-registered clause builders
- 75dbf08 (go-gorm#222): accept tab between column name and type
- bbca3b3 (go-gorm#185): exported Config and New(Config) constructor
- 5df1f76 (go-gorm#219): remove stray Debug() from GetIndexes

migrator.go, ddlmod.go and ddlmod_parse_all_columns.go are taken from
upstream verbatim; sqlite.go is hand-merged to keep this fork's imports,
DriverName, and its error-code-based Translate. Upstream's test files
for these paths are ported unchanged.

Refs #152

* Bump gorm and the pure-Go sqlite backend

gorm.io/gorm 1.25.7 to 1.31.2, github.com/glebarez/go-sqlite 1.21.2 to
1.22.0, which pulls modernc.org/sqlite 1.23.1 to 1.28.0. go mod tidy
refreshes the indirect set. The go directive stays at 1.18: gorm 1.31.2
declares go 1.18 and go-sqlite 1.22.0 declares go 1.17, so nothing forces
a raise.

Test counts are unchanged, 15 top-level tests and 70 including subtests,
all passing.

Refs #152

* Document upstream parity in the README

States the last upstream commit merged (525c431), the three divergences
that are deliberate (pure-Go backend imports, DriverName "sqlite",
error-code-based Translate), and the rule that backend-agnostic fixes go
to go-gorm/sqlite first and come back as ports.

Refs #152

* Translate CHECK constraint failures to gorm.ErrCheckConstraintViolated

SQLITE_CONSTRAINT_CHECK (extended code 275) fell through Translate and
reached the caller as a raw driver error, so TranslateError users had to
string-match "CHECK constraint failed" to detect it. UNIQUE, PRIMARY KEY
and FOREIGN KEY were already mapped.

The test drives a real in-memory database: it migrates a model carrying a
CHECK constraint, asserts the constraint is in the table DDL so a wrong
failure cannot pass for the right one, then inserts a row that violates
it. Without the mapping the test fails with the raw error, code 275.

This mirrors the backport of the go-gorm#247 follow-up.

Refs #152

* Fix unquoted constraints during table rebuilds
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.

3 participants