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:
2026-10-02 22:58:00 +02:00
parent 275424c605
commit bbab5ce936
21 changed files with 516 additions and 5 deletions
+8
View File
@@ -203,6 +203,14 @@ The writer automatically generates relationship fields:
- Relationship field names use 3-letter prefixes
- Includes proper `gorm` tags with `foreignKey` and `references`
## Read-only Columns
| Column | Tags |
|---|---|
| `GENERATED ... STORED` | `<-:false;generated` |
| `GENERATED ALWAYS AS IDENTITY`, not PK | `<-:false;identity` |
| `GENERATED ALWAYS AS IDENTITY`, PK | `identity` only |
## Notes
- Model names are prefixed with "Model" (e.g., `ModelUser`)
+45
View File
@@ -0,0 +1,45 @@
package gorm
import (
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
func TestBuildGormTag_GeneratedColumn(t *testing.T) {
mapper := NewTypeMapper("")
generated := models.InitColumn("full_name", "users", "public")
generated.Type = "text"
generated.Generated = true
plain := models.InitColumn("first", "users", "public")
plain.Type = "text"
tests := []struct {
name string
column *models.Column
want bool
}{
{"generated column is write-disabled and marked", generated, true},
{"ordinary column is untouched", plain, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tag := mapper.BuildGormTag(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("<-:false") != tt.want || has("generated") != tt.want {
t.Errorf("tag %q: <-:false/generated present = %v/%v, want %v", tag, has("<-:false"), has("generated"), tt.want)
}
})
}
}
+49
View File
@@ -0,0 +1,49 @@
package gorm
import (
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
func TestBuildGormTag_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.BuildGormTag(tt.column, nil)
has := func(s string) bool {
for _, p := range strings.Split(tag, ";") {
if p == s {
return true
}
}
return false
}
if has("<-:false") != tt.wantFlag || has("identity") != tt.wantMarker {
t.Errorf("tag %q: <-:false/identity = %v/%v, want %v/%v", tag, has("<-:false"), has("identity"), tt.wantFlag, tt.wantMarker)
}
})
}
}
+15
View File
@@ -401,6 +401,21 @@ func (tm *TypeMapper) BuildGormTag(column *models.Column, table *models.Table) s
parts = append(parts, "autoIncrement")
}
// GENERATED ALWAYS ... STORED columns are computed by the database and cannot be
// written: disable write permission so GORM leaves them out of INSERT/UPDATE, and mark why.
if column.Generated {
parts = append(parts, "<-:false", "generated")
}
// GENERATED ALWAYS AS IDENTITY rejects explicit values. A primary key only gets the
// marker so it keeps its normal create/return behavior.
if column.Identity && strings.EqualFold(column.IdentityGeneration, "ALWAYS") {
if !column.IsPrimaryKey {
parts = append(parts, "<-:false")
}
parts = append(parts, "identity")
}
// Not null (skip if primary key, as it's implied)
if column.NotNull && !column.IsPrimaryKey {
parts = append(parts, "not null")