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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
run-tests:
strategy:
matrix:
go: [ '1.21', '1.20', '1.19' ]
go: [ 'oldstable', 'stable' ]
platform: [ ubuntu-latest ]
runs-on: ubuntu-latest

Expand Down
58 changes: 58 additions & 0 deletions generated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sqlserver

import (
"testing"

"gorm.io/gorm/schema"
)

func TestDataTypeOfGeneratedColumn(t *testing.T) {
dialector := Dialector{Config: &Config{}}
tests := []struct {
name string
field *schema.Field
want string
}{
{
// SQL Server infers a computed column's type from the expression,
// so the column type is omitted and the value is PERSISTED (stored).
name: "computed column renders a PERSISTED computed column",
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "price * quantity"}},
want: "AS (price * quantity) PERSISTED",
},
{
name: "computed expression keeps commas",
field: &schema.Field{DataType: schema.String, TagSettings: map[string]string{"GENERATED": "concat(first_name, last_name)"}},
want: "AS (concat(first_name, last_name)) PERSISTED",
},
{
// `identity` is reserved for identity columns, which SQL Server
// renders through its native IDENTITY rather than a computed column.
name: "identity keyword is not treated as a computed column",
field: &schema.Field{DataType: schema.Int, Size: 64, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity"}},
want: "bigint IDENTITY(1,1)",
},
{
name: "identity with an explicit mode is also reserved",
field: &schema.Field{DataType: schema.Int, Size: 64, AutoIncrement: true, TagSettings: map[string]string{"GENERATED": "identity always"}},
want: "bigint IDENTITY(1,1)",
},
{
name: "a bare generated tag is ignored",
field: &schema.Field{DataType: schema.Float, TagSettings: map[string]string{"GENERATED": "GENERATED"}},
want: "float",
},
{
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: "AS (generated) PERSISTED",
},
}
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 sqlserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
}

func (dialector Dialector) DataTypeOf(field *schema.Field) string {
// Computed (generated) column. SQL Server uses `AS (expr) PERSISTED` (the
// column type is inferred from the expression, so it is omitted). The
// expression is carried by the `generated` tag, separate from the type.
// https://learn.microsoft.com/en-us/sql/relational-databases/tables/specify-computed-columns-in-a-table
if expr, ok := generatedColumnExpr(field); ok {
return "AS (" + expr + ") PERSISTED"
}

return dialector.dataTypeOf(field)
}

func (dialector Dialector) dataTypeOf(field *schema.Field) string {
switch field.DataType {
case schema.Bool:
return "bit"
Expand Down Expand Up @@ -243,3 +255,40 @@ func (dialectopr Dialector) RollbackTo(tx *gorm.DB, name string) error {
tx.Exec("ROLLBACK TRANSACTION " + name)
return nil
}

// 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 IDENTITY) 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