feat: mark generated and identity columns across writers and DBML
- bun: scanonly+generated for GENERATED columns; scanonly+identity for ALWAYS identity (PK gets identity only) - gorm: <-:false+generated / identity, same primary key rule - dbml: write GENERATED/IDENTITY column notes and read them back into the model - pgsql: emit generation/identity clauses in migration create-table and add-column, no DEFAULT - readmes updated
This commit is contained in:
@@ -129,6 +129,9 @@ type User struct {
|
||||
- `default` - Default value
|
||||
- `rel` - Relationship definition
|
||||
- `type` - Explicit SQL type
|
||||
- `scanonly` - Excluded from INSERT/UPDATE: `GENERATED ... STORED` columns, and `GENERATED ALWAYS AS IDENTITY` columns that are not the primary key
|
||||
- `generated` - Marks a `GENERATED ... STORED` column
|
||||
- `identity` - Marks a `GENERATED ALWAYS AS IDENTITY` column (a primary key gets only this marker, since Bun drops `scanonly` fields from the PK list)
|
||||
|
||||
## Type Mapping
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
func TestBuildBunTag_GeneratedColumn(t *testing.T) {
|
||||
mapper := NewTypeMapper("", "")
|
||||
|
||||
generated := models.InitColumn("full_name", "users", "public")
|
||||
generated.Type = "text"
|
||||
generated.Generated = true
|
||||
generated.GenerationExpression = "first || ' ' || last"
|
||||
|
||||
plain := models.InitColumn("first", "users", "public")
|
||||
plain.Type = "text"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
want bool
|
||||
}{
|
||||
{"generated column is scanonly and marked", generated, true},
|
||||
{"ordinary column is untouched", plain, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tag := mapper.BuildBunTag(tt.column, nil)
|
||||
parts := strings.Split(tag, ",")
|
||||
has := func(s string) bool {
|
||||
for _, p := range parts {
|
||||
if p == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if has("scanonly") != tt.want || has("generated") != tt.want {
|
||||
t.Errorf("tag %q: scanonly/generated present = %v/%v, want %v", tag, has("scanonly"), has("generated"), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
func TestBuildBunTag_IdentityColumn(t *testing.T) {
|
||||
mapper := NewTypeMapper("", "")
|
||||
|
||||
build := func(pk bool, identity bool, generation string) *models.Column {
|
||||
col := models.InitColumn("id", "users", "public")
|
||||
col.Type = "bigint"
|
||||
col.IsPrimaryKey = pk
|
||||
col.Identity = identity
|
||||
col.IdentityGeneration = generation
|
||||
return col
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
column *models.Column
|
||||
wantFlag bool
|
||||
wantMarker bool
|
||||
}{
|
||||
{"always identity non-pk is write-blocked", build(false, true, "ALWAYS"), true, true},
|
||||
{"always identity pk is only marked", build(true, true, "ALWAYS"), false, true},
|
||||
{"by default identity is writable", build(false, true, "BY DEFAULT"), false, false},
|
||||
{"non-identity is untouched", build(false, false, ""), false, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tag := mapper.BuildBunTag(tt.column, nil)
|
||||
has := func(s string) bool {
|
||||
for _, p := range strings.Split(tag, ",") {
|
||||
if p == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if has("scanonly") != tt.wantFlag || has("identity") != tt.wantMarker {
|
||||
t.Errorf("tag %q: scanonly/identity = %v/%v, want %v/%v", tag, has("scanonly"), has("identity"), tt.wantFlag, tt.wantMarker)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -366,6 +366,22 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
|
||||
parts = append(parts, "autoincrement")
|
||||
}
|
||||
|
||||
// GENERATED ALWAYS ... STORED columns are computed by the database and cannot be
|
||||
// written: scanonly keeps them out of INSERT/UPDATE, generated marks why.
|
||||
if column.Generated {
|
||||
parts = append(parts, "scanonly", "generated")
|
||||
}
|
||||
|
||||
// GENERATED ALWAYS AS IDENTITY rejects explicit values. Bun drops scanonly fields
|
||||
// from the table's PK list, so a primary key only gets the marker; its zero value
|
||||
// is already sent as DEFAULT via autoincrement.
|
||||
if column.Identity && strings.EqualFold(column.IdentityGeneration, "ALWAYS") {
|
||||
if !column.IsPrimaryKey {
|
||||
parts = append(parts, "scanonly")
|
||||
}
|
||||
parts = append(parts, "identity")
|
||||
}
|
||||
|
||||
// Default value
|
||||
if column.Default != nil {
|
||||
// Sanitize default value to remove backticks, then quote based on column type
|
||||
|
||||
Reference in New Issue
Block a user