Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions generated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package sqlite

import (
"testing"

"gorm.io/gorm/schema"
)

func TestDataTypeOfGeneratedColumn(t *testing.T) {
dialector := Dialector{}
tests := []struct {
name string
field *schema.Field
want string
}{
{
name: "computed column renders a STORED generated column",
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "price * quantity"}},
want: "real GENERATED ALWAYS AS (price * quantity) STORED",
},
{
name: "computed expression keeps commas",
field: &schema.Field{DataType: schema.String, TagSettings: map[string]string{"GENERATED": "coalesce(first_name, last_name)"}},
want: "text GENERATED ALWAYS AS (coalesce(first_name, last_name)) STORED",
},
{
// `identity` is reserved for identity columns, which SQLite renders
// through its native AUTOINCREMENT rather than a computed column.
name: "identity keyword is not treated as a computed column",
field: &schema.Field{DataType: schema.Int, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity"}},
want: "integer PRIMARY KEY AUTOINCREMENT",
},
{
name: "identity with an explicit mode is also reserved",
field: &schema.Field{DataType: schema.Int, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity always"}},
want: "integer PRIMARY KEY AUTOINCREMENT",
},
{
name: "a bare generated tag is ignored",
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "GENERATED"}},
want: "real",
},
{
name: "a lowercase generated expression is not mistaken for a bare tag",
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "generated"}},
want: "real GENERATED ALWAYS AS (generated) STORED",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dialector.DataTypeOf(tt.field); got != tt.want {
t.Errorf("DataTypeOf() = %q, want %q", got, tt.want)
}
})
}
}
49 changes: 49 additions & 0 deletions sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"strconv"
"strings"

"gorm.io/gorm/callbacks"

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

func (dialector Dialector) DataTypeOf(field *schema.Field) string {
// Computed (generated) column. SQLite 3.31+ supports STORED generated
// columns; the expression is carried by the `generated` tag, separate from
// the column type. https://www.sqlite.org/gencol.html
if expr, ok := generatedColumnExpr(field); ok {
return dialector.dataTypeOf(field) + " GENERATED ALWAYS AS (" + expr + ") STORED"
}

return dialector.dataTypeOf(field)
}

func (dialector Dialector) dataTypeOf(field *schema.Field) string {
switch field.DataType {
case schema.Bool:
return "numeric"
Expand Down Expand Up @@ -268,3 +280,40 @@ func compareVersion(version1, version2 string) int {
}
return 0
}

// generatedColumnExpr returns the expression of a computed (generated) column
// declared via the `generated` tag, if any. The `identity` keyword is reserved
// for identity columns (rendered through the dialect's native auto-increment)
// and is not a computed-column expression.
func generatedColumnExpr(field *schema.Field) (string, bool) {
value, ok := field.TagSettings["GENERATED"]
if !ok {
return "", false
}
// Ignore an empty value or a bare `generated` tag, which the tag parser
// stores as the upper-cased key, rather than treating it as an expression.
if value = strings.TrimSpace(value); value == "" || value == "GENERATED" {
return "", false
}
if isIdentityKeyword(value) {
return "", false
}
return value, true
}

// isIdentityKeyword reports whether value is the `identity` keyword, optionally
// combined with the generation mode `always` / `by default`. Any other token
// means value is a computed-column expression.
func isIdentityKeyword(value string) bool {
identity := false
for _, token := range strings.Fields(strings.ToLower(value)) {
switch token {
case "identity":
identity = true
case "always", "by", "default":
default:
return false
}
}
return identity
}
Loading