Skip to content

Commit 94d4368

Browse files
h2ziclaude
andcommitted
feat: support generated (computed) columns via the generated tag
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>
1 parent 7230345 commit 94d4368

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

generated_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package sqlite
2+
3+
import (
4+
"testing"
5+
6+
"gorm.io/gorm/schema"
7+
)
8+
9+
func TestDataTypeOfGeneratedColumn(t *testing.T) {
10+
dialector := Dialector{}
11+
tests := []struct {
12+
name string
13+
field *schema.Field
14+
want string
15+
}{
16+
{
17+
name: "computed column renders a STORED generated column",
18+
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "price * quantity"}},
19+
want: "real GENERATED ALWAYS AS (price * quantity) STORED",
20+
},
21+
{
22+
name: "computed expression keeps commas",
23+
field: &schema.Field{DataType: schema.String, TagSettings: map[string]string{"GENERATED": "coalesce(first_name, last_name)"}},
24+
want: "text GENERATED ALWAYS AS (coalesce(first_name, last_name)) STORED",
25+
},
26+
{
27+
// `identity` is reserved for identity columns, which SQLite renders
28+
// through its native AUTOINCREMENT rather than a computed column.
29+
name: "identity keyword is not treated as a computed column",
30+
field: &schema.Field{DataType: schema.Int, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity"}},
31+
want: "integer PRIMARY KEY AUTOINCREMENT",
32+
},
33+
{
34+
name: "identity with an explicit mode is also reserved",
35+
field: &schema.Field{DataType: schema.Int, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity always"}},
36+
want: "integer PRIMARY KEY AUTOINCREMENT",
37+
},
38+
{
39+
name: "a bare generated tag is ignored",
40+
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "GENERATED"}},
41+
want: "real",
42+
},
43+
{
44+
name: "a lowercase generated expression is not mistaken for a bare tag",
45+
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "generated"}},
46+
want: "real GENERATED ALWAYS AS (generated) STORED",
47+
},
48+
}
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
if got := dialector.DataTypeOf(tt.field); got != tt.want {
52+
t.Errorf("DataTypeOf() = %q, want %q", got, tt.want)
53+
}
54+
})
55+
}
56+
}

sqlite.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"strconv"
7+
"strings"
78

89
"gorm.io/gorm/callbacks"
910

@@ -206,6 +207,17 @@ func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
206207
}
207208

208209
func (dialector Dialector) DataTypeOf(field *schema.Field) string {
210+
// Computed (generated) column. SQLite 3.31+ supports STORED generated
211+
// columns; the expression is carried by the `generated` tag, separate from
212+
// the column type. https://www.sqlite.org/gencol.html
213+
if expr, ok := generatedColumnExpr(field); ok {
214+
return dialector.dataTypeOf(field) + " GENERATED ALWAYS AS (" + expr + ") STORED"
215+
}
216+
217+
return dialector.dataTypeOf(field)
218+
}
219+
220+
func (dialector Dialector) dataTypeOf(field *schema.Field) string {
209221
switch field.DataType {
210222
case schema.Bool:
211223
return "numeric"
@@ -235,6 +247,43 @@ func (dialector Dialector) DataTypeOf(field *schema.Field) string {
235247
return string(field.DataType)
236248
}
237249

250+
// generatedColumnExpr returns the expression of a computed (generated) column
251+
// declared via the `generated` tag, if any. The `identity` keyword is reserved
252+
// for identity columns (rendered through the dialect's native auto-increment)
253+
// and is not a computed-column expression.
254+
func generatedColumnExpr(field *schema.Field) (string, bool) {
255+
value, ok := field.TagSettings["GENERATED"]
256+
if !ok {
257+
return "", false
258+
}
259+
// Ignore an empty value or a bare `generated` tag, which the tag parser
260+
// stores as the upper-cased key, rather than treating it as an expression.
261+
if value = strings.TrimSpace(value); value == "" || value == "GENERATED" {
262+
return "", false
263+
}
264+
if isIdentityKeyword(value) {
265+
return "", false
266+
}
267+
return value, true
268+
}
269+
270+
// isIdentityKeyword reports whether value is the `identity` keyword, optionally
271+
// combined with the generation mode `always` / `by default`. Any other token
272+
// means value is a computed-column expression.
273+
func isIdentityKeyword(value string) bool {
274+
identity := false
275+
for _, token := range strings.Fields(strings.ToLower(value)) {
276+
switch token {
277+
case "identity":
278+
identity = true
279+
case "always", "by", "default":
280+
default:
281+
return false
282+
}
283+
}
284+
return identity
285+
}
286+
238287
func (dialectopr Dialector) SavePoint(tx *gorm.DB, name string) error {
239288
tx.Exec("SAVEPOINT " + name)
240289
return nil

0 commit comments

Comments
 (0)