Files
ResolveSpec/pkg/reflection/model_utils_test.go
T
Hein aec87a81e7
Tests / Integration Tests (push) Skipped
Tests / Unit Tests (push) Successful in 1m37s
Tests / Race Detector (push) Successful in 3m52s
Build , Vet Test, and Lint / Build (push) Successful in 1m33s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m16s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m19s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m19s
fix(bun): ignore scanonly columns in ExcludeColumn
Bun's ExcludeColumn errors with "can't find column" for scanonly fields
because they are not in the table's writable fields. Filter the exclude
list to writable bun fields so models with scanonly buffers can insert
and update again. Add tests for the adapter and reflection.
2026-10-05 14:10:52 +02:00

1994 lines
49 KiB
Go

package reflection
import (
"reflect"
"testing"
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
)
// Test models for GORM
type GormModelWithGetIDName struct {
ID int `gorm:"column:rid_test;primaryKey" json:"id"`
Name string `json:"name"`
}
func (m GormModelWithGetIDName) GetIDName() string {
return "rid_test"
}
type GormModelWithColumnTag struct {
ID int `gorm:"column:custom_id;primaryKey" json:"id"`
Name string `json:"name"`
}
type GormModelWithJSONFallback struct {
ID int `gorm:"primaryKey" json:"user_id"`
Name string `json:"name"`
}
// Test models for Bun
type BunModelWithGetIDName struct {
ID int `bun:"rid_test,pk" json:"id"`
Name string `json:"name"`
}
func (m BunModelWithGetIDName) GetIDName() string {
return "rid_test"
}
type BunModelWithColumnTag struct {
ID int `bun:"custom_id,pk" json:"id"`
Name string `json:"name"`
}
type BunModelWithJSONFallback struct {
ID int `bun:",pk" json:"user_id"`
Name string `json:"name"`
}
func TestGetPrimaryKeyName(t *testing.T) {
tests := []struct {
name string
model any
expected string
}{
{
name: "GORM model with GetIDName method",
model: GormModelWithGetIDName{},
expected: "rid_test",
},
{
name: "GORM model with column tag",
model: GormModelWithColumnTag{},
expected: "custom_id",
},
{
name: "GORM model with JSON fallback",
model: GormModelWithJSONFallback{},
expected: "user_id",
},
{
name: "GORM model pointer with GetIDName",
model: &GormModelWithGetIDName{},
expected: "rid_test",
},
{
name: "GORM model pointer with column tag",
model: &GormModelWithColumnTag{},
expected: "custom_id",
},
{
name: "Bun model with GetIDName method",
model: BunModelWithGetIDName{},
expected: "rid_test",
},
{
name: "Bun model with column tag",
model: BunModelWithColumnTag{},
expected: "custom_id",
},
{
name: "Bun model with JSON fallback",
model: BunModelWithJSONFallback{},
expected: "user_id",
},
{
name: "Bun model pointer with GetIDName",
model: &BunModelWithGetIDName{},
expected: "rid_test",
},
{
name: "Bun model pointer with column tag",
model: &BunModelWithColumnTag{},
expected: "custom_id",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPrimaryKeyName(tt.model)
if result != tt.expected {
t.Errorf("GetPrimaryKeyName() = %v, want %v", result, tt.expected)
}
})
}
}
func TestExtractColumnFromGormTag(t *testing.T) {
tests := []struct {
name string
tag string
expected string
}{
{
name: "column tag with primaryKey",
tag: "column:rid_test;primaryKey",
expected: "rid_test",
},
{
name: "column tag with spaces",
tag: "column:user_id ; primaryKey ; autoIncrement",
expected: "user_id",
},
{
name: "no column tag",
tag: "primaryKey;autoIncrement",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractColumnFromGormTag(tt.tag)
if result != tt.expected {
t.Errorf("ExtractColumnFromGormTag() = %v, want %v", result, tt.expected)
}
})
}
}
func TestExtractColumnFromBunTag(t *testing.T) {
tests := []struct {
name string
tag string
expected string
}{
{
name: "column name with pk flag",
tag: "rid_test,pk",
expected: "rid_test",
},
{
name: "only pk flag",
tag: ",pk",
expected: "",
},
{
name: "column with multiple flags",
tag: "user_id,pk,autoincrement",
expected: "user_id",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractColumnFromBunTag(tt.tag)
if result != tt.expected {
t.Errorf("ExtractColumnFromBunTag() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetModelColumns(t *testing.T) {
tests := []struct {
name string
model any
expected []string
}{
{
name: "Bun model with multiple columns",
model: BunModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
{
name: "GORM model with multiple columns",
model: GormModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
{
name: "Bun model pointer",
model: &BunModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
{
name: "GORM model pointer",
model: &GormModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
{
name: "Bun model with JSON fallback",
model: BunModelWithJSONFallback{},
expected: []string{"user_id", "name"},
},
{
name: "GORM model with JSON fallback",
model: GormModelWithJSONFallback{},
expected: []string{"user_id", "name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetModelColumns(tt.model)
if len(result) != len(tt.expected) {
t.Errorf("GetModelColumns() returned %d columns, want %d", len(result), len(tt.expected))
return
}
for i, col := range result {
if col != tt.expected[i] {
t.Errorf("GetModelColumns()[%d] = %v, want %v", i, col, tt.expected[i])
}
}
})
}
}
// Test models with embedded structs
type BaseModel struct {
ID int `bun:"rid_base,pk" json:"id"`
CreatedAt string `bun:"created_at" json:"created_at"`
}
type AdhocBuffer struct {
CQL1 string `json:"cql1,omitempty" gorm:"->" bun:",scanonly"`
CQL2 string `json:"cql2,omitempty" gorm:"->" bun:",scanonly"`
RowNumber int64 `json:"_rownumber,omitempty" gorm:"-" bun:",scanonly"`
}
type ModelWithEmbedded struct {
BaseModel
Name string `bun:"name" json:"name"`
Description string `bun:"description" json:"description"`
AdhocBuffer
}
type GormBaseModel struct {
ID int `gorm:"column:rid_base;primaryKey" json:"id"`
CreatedAt string `gorm:"column:created_at" json:"created_at"`
}
type GormAdhocBuffer struct {
CQL1 string `json:"cql1,omitempty" gorm:"column:cql1;->" bun:",scanonly"`
CQL2 string `json:"cql2,omitempty" gorm:"column:cql2;->" bun:",scanonly"`
RowNumber int64 `json:"_rownumber,omitempty" gorm:"-" bun:",scanonly"`
}
type GormModelWithEmbedded struct {
GormBaseModel
Name string `gorm:"column:name" json:"name"`
Description string `gorm:"column:description" json:"description"`
GormAdhocBuffer
}
func TestGetPrimaryKeyNameWithEmbedded(t *testing.T) {
tests := []struct {
name string
model any
expected string
}{
{
name: "Bun model with embedded base",
model: ModelWithEmbedded{},
expected: "rid_base",
},
{
name: "Bun model with embedded base (pointer)",
model: &ModelWithEmbedded{},
expected: "rid_base",
},
{
name: "GORM model with embedded base",
model: GormModelWithEmbedded{},
expected: "rid_base",
},
{
name: "GORM model with embedded base (pointer)",
model: &GormModelWithEmbedded{},
expected: "rid_base",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPrimaryKeyName(tt.model)
if result != tt.expected {
t.Errorf("GetPrimaryKeyName() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetPrimaryKeyValueWithEmbedded(t *testing.T) {
bunModel := ModelWithEmbedded{
BaseModel: BaseModel{
ID: 123,
CreatedAt: "2024-01-01",
},
Name: "Test",
Description: "Test Description",
}
gormModel := GormModelWithEmbedded{
GormBaseModel: GormBaseModel{
ID: 456,
CreatedAt: "2024-01-02",
},
Name: "GORM Test",
Description: "GORM Test Description",
}
tests := []struct {
name string
model any
expected any
}{
{
name: "Bun model with embedded base",
model: bunModel,
expected: 123,
},
{
name: "Bun model with embedded base (pointer)",
model: &bunModel,
expected: 123,
},
{
name: "GORM model with embedded base",
model: gormModel,
expected: 456,
},
{
name: "GORM model with embedded base (pointer)",
model: &gormModel,
expected: 456,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPrimaryKeyValue(tt.model)
if result != tt.expected {
t.Errorf("GetPrimaryKeyValue() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetModelColumnsWithEmbedded(t *testing.T) {
tests := []struct {
name string
model any
expected []string
}{
{
name: "Bun model with embedded structs",
model: ModelWithEmbedded{},
expected: []string{"rid_base", "created_at", "name", "description", "cql1", "cql2", "_rownumber"},
},
{
name: "Bun model with embedded structs (pointer)",
model: &ModelWithEmbedded{},
expected: []string{"rid_base", "created_at", "name", "description", "cql1", "cql2", "_rownumber"},
},
{
name: "GORM model with embedded structs",
model: GormModelWithEmbedded{},
expected: []string{"rid_base", "created_at", "name", "description", "cql1", "cql2", "_rownumber"},
},
{
name: "GORM model with embedded structs (pointer)",
model: &GormModelWithEmbedded{},
expected: []string{"rid_base", "created_at", "name", "description", "cql1", "cql2", "_rownumber"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetModelColumns(tt.model)
if len(result) != len(tt.expected) {
t.Errorf("GetModelColumns() returned %d columns, want %d. Got: %v", len(result), len(tt.expected), result)
return
}
for i, col := range result {
if col != tt.expected[i] {
t.Errorf("GetModelColumns()[%d] = %v, want %v", i, col, tt.expected[i])
}
}
})
}
}
func TestHasColumn(t *testing.T) {
m := ModelWithEmbedded{}
for _, col := range []string{"name", "description", "rid_base", "created_at", "cql1", "cql2"} {
if !HasColumn(m, col) {
t.Errorf("HasColumn(%q) = false, want true", col)
}
}
if HasColumn(m, "nonexistent_column") {
t.Error("HasColumn(nonexistent_column) = true, want false")
}
if HasColumn(m, "") {
t.Error("HasColumn(\"\") = true, want false")
}
}
func TestIsColumnWritableWithEmbedded(t *testing.T) {
tests := []struct {
name string
model any
columnName string
expected bool
}{
{
name: "Bun model - writable column in main struct",
model: ModelWithEmbedded{},
columnName: "name",
expected: true,
},
{
name: "Bun model - writable column in embedded base",
model: ModelWithEmbedded{},
columnName: "rid_base",
expected: true,
},
{
name: "Bun model - scanonly column in embedded adhoc buffer",
model: ModelWithEmbedded{},
columnName: "cql1",
expected: false,
},
{
name: "Bun model - scanonly column _rownumber",
model: ModelWithEmbedded{},
columnName: "_rownumber",
expected: false,
},
{
name: "GORM model - writable column in main struct",
model: GormModelWithEmbedded{},
columnName: "name",
expected: true,
},
{
name: "GORM model - writable column in embedded base",
model: GormModelWithEmbedded{},
columnName: "rid_base",
expected: true,
},
{
name: "GORM model - readonly column in embedded adhoc buffer",
model: GormModelWithEmbedded{},
columnName: "cql1",
expected: false,
},
{
name: "GORM model - readonly column _rownumber",
model: GormModelWithEmbedded{},
columnName: "_rownumber",
expected: false, // bun:",scanonly" marks it as read-only, takes precedence over gorm:"-"
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsColumnWritable(tt.model, tt.columnName)
if result != tt.expected {
t.Errorf("IsColumnWritable(%s) = %v, want %v", tt.columnName, result, tt.expected)
}
})
}
}
// Test models with relations for GetSQLModelColumns
type User struct {
ID int `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
Email string `bun:"email" json:"email"`
ProfileData string `json:"profile_data"` // No bun/gorm tag
Posts []Post `bun:"rel:has-many,join:id=user_id" json:"posts"`
Profile *Profile `bun:"rel:has-one,join:id=user_id" json:"profile"`
RowNumber int64 `bun:",scanonly" json:"_rownumber"`
}
type Post struct {
ID int `gorm:"column:id;primaryKey" json:"id"`
Title string `gorm:"column:title" json:"title"`
UserID int `gorm:"column:user_id;foreignKey" json:"user_id"`
User *User `gorm:"foreignKey:UserID;references:ID" json:"user"`
Tags []Tag `gorm:"many2many:post_tags" json:"tags"`
Content string `json:"content"` // No bun/gorm tag
}
type Profile struct {
ID int `bun:"id,pk" json:"id"`
Bio string `bun:"bio" json:"bio"`
UserID int `bun:"user_id" json:"user_id"`
}
type Tag struct {
ID int `gorm:"column:id;primaryKey" json:"id"`
Name string `gorm:"column:name" json:"name"`
}
// Model with scan-only embedded struct
type EntityWithScanOnlyEmbedded struct {
ID int `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
AdhocBuffer `bun:",scanonly"` // Entire embedded struct is scan-only
}
func TestGetSQLModelColumns(t *testing.T) {
tests := []struct {
name string
model any
expected []string
}{
{
name: "Bun model with relations - excludes relations and non-SQL fields",
model: User{},
// Should include: id, name, email (has bun tags)
// Should exclude: profile_data (no bun tag), Posts/Profile (relations), RowNumber (scan-only in embedded would be excluded)
expected: []string{"id", "name", "email"},
},
{
name: "GORM model with relations - excludes relations and non-SQL fields",
model: Post{},
// Should include: id, title, user_id (has gorm tags)
// Should exclude: content (no gorm tag), User/Tags (relations)
expected: []string{"id", "title", "user_id"},
},
{
name: "Model with embedded base and scan-only embedded",
model: EntityWithScanOnlyEmbedded{},
// Should include: id, name from main struct
// Should exclude: all fields from AdhocBuffer (scan-only embedded struct)
expected: []string{"id", "name"},
},
{
name: "Model with embedded - includes SQL fields, excludes scan-only",
model: ModelWithEmbedded{},
// Should include: rid_base, created_at (from BaseModel), name, description (from main)
// Should exclude: cql1, cql2, _rownumber (from AdhocBuffer - scan-only fields)
expected: []string{"rid_base", "created_at", "name", "description"},
},
{
name: "GORM model with embedded - includes SQL fields, excludes scan-only",
model: GormModelWithEmbedded{},
// Should include: rid_base, created_at (from GormBaseModel), name, description (from main)
// Should exclude: cql1, cql2 (scan-only), _rownumber (no gorm column tag, marked as -)
expected: []string{"rid_base", "created_at", "name", "description"},
},
{
name: "Simple Profile model",
model: Profile{},
// Should include all fields with bun tags
expected: []string{"id", "bio", "user_id"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSQLModelColumns(tt.model)
if len(result) != len(tt.expected) {
t.Errorf("GetSQLModelColumns() returned %d columns, want %d.\nGot: %v\nWant: %v",
len(result), len(tt.expected), result, tt.expected)
return
}
for i, col := range result {
if col != tt.expected[i] {
t.Errorf("GetSQLModelColumns()[%d] = %v, want %v.\nFull result: %v",
i, col, tt.expected[i], result)
}
}
})
}
}
func TestGetSQLModelColumnsVsGetModelColumns(t *testing.T) {
// Demonstrate the difference between GetModelColumns and GetSQLModelColumns
user := User{}
allColumns := GetModelColumns(user)
sqlColumns := GetSQLModelColumns(user)
t.Logf("GetModelColumns(User): %v", allColumns)
t.Logf("GetSQLModelColumns(User): %v", sqlColumns)
// GetModelColumns should return more columns (includes fields with only json tags)
if len(allColumns) <= len(sqlColumns) {
t.Errorf("Expected GetModelColumns to return more columns than GetSQLModelColumns")
}
// GetSQLModelColumns should not include 'profile_data' (no bun tag)
for _, col := range sqlColumns {
if col == "profile_data" {
t.Errorf("GetSQLModelColumns should not include 'profile_data' (no bun/gorm tag)")
}
}
// GetModelColumns should include 'profile_data' (has json tag)
hasProfileData := false
for _, col := range allColumns {
if col == "profile_data" {
hasProfileData = true
break
}
}
if !hasProfileData {
t.Errorf("GetModelColumns should include 'profile_data' (has json tag)")
}
}
// ============= Tests for helpers.go =============
func TestLen(t *testing.T) {
tests := []struct {
name string
input any
expected int
}{
{
name: "slice of ints",
input: []int{1, 2, 3, 4, 5},
expected: 5,
},
{
name: "empty slice",
input: []string{},
expected: 0,
},
{
name: "array",
input: [3]int{1, 2, 3},
expected: 3,
},
{
name: "string",
input: "hello",
expected: 5,
},
{
name: "empty string",
input: "",
expected: 0,
},
{
name: "map",
input: map[string]int{"a": 1, "b": 2, "c": 3},
expected: 3,
},
{
name: "empty map",
input: map[string]int{},
expected: 0,
},
{
name: "pointer to slice",
input: &[]int{1, 2, 3},
expected: 3,
},
{
name: "non-lennable type (int)",
input: 42,
expected: 0,
},
{
name: "non-lennable type (struct)",
input: struct{}{},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Len(tt.input)
if result != tt.expected {
t.Errorf("Len() = %v, want %v", result, tt.expected)
}
})
}
}
func TestExtractTableNameOnly(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple table name",
input: "users",
expected: "users",
},
{
name: "schema.table",
input: "public.users",
expected: "users",
},
{
name: "table with comma",
input: "users,",
expected: "users",
},
{
name: "table with space",
input: "users WHERE",
expected: "users",
},
{
name: "schema.table with space",
input: "public.users WHERE id = 1",
expected: "users",
},
{
name: "schema.table with comma",
input: "myschema.mytable, other_table",
expected: "mytable",
},
{
name: "table with tab",
input: "users\tJOIN",
expected: "users",
},
{
name: "table with newline",
input: "users\nWHERE",
expected: "users",
},
{
name: "multiple dots",
input: "db.schema.table WHERE",
expected: "table",
},
{
name: "no delimiters",
input: "tablename",
expected: "tablename",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractTableNameOnly(tt.input)
if result != tt.expected {
t.Errorf("ExtractTableNameOnly(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
// ============= Tests for utility functions =============
func TestExtractSourceColumn(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "column with ->> operator",
input: "columna->>'val'",
expected: "columna",
},
{
name: "column with -> operator",
input: "columna->'key'",
expected: "columna",
},
{
name: "simple column",
input: "columna",
expected: "columna",
},
{
name: "table.column with ->> operator",
input: "table.columna->>'val'",
expected: "table.columna",
},
{
name: "table.column with -> operator",
input: "table.columna->'key'",
expected: "table.columna",
},
{
name: "column with spaces before operator",
input: "columna ->>'value'",
expected: "columna",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractSourceColumn(tt.input)
if result != tt.expected {
t.Errorf("ExtractSourceColumn(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestToSnakeCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "CamelCase",
input: "CamelCase",
expected: "camel_case",
},
{
name: "camelCase",
input: "camelCase",
expected: "camel_case",
},
{
name: "UserID",
input: "UserID",
expected: "user_id",
},
{
name: "HTTPServer",
input: "HTTPServer",
expected: "http_server",
},
{
name: "lowercase",
input: "lowercase",
expected: "lowercase",
},
{
name: "UPPERCASE",
input: "UPPERCASE",
expected: "uppercase",
},
{
name: "Single",
input: "A",
expected: "a",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToSnakeCase(tt.input)
if result != tt.expected {
t.Errorf("ToSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestIsNumericType(t *testing.T) {
tests := []struct {
name string
kind reflect.Kind
expected bool
}{
{"int", reflect.Int, true},
{"int8", reflect.Int8, true},
{"int16", reflect.Int16, true},
{"int32", reflect.Int32, true},
{"int64", reflect.Int64, true},
{"uint", reflect.Uint, true},
{"uint8", reflect.Uint8, true},
{"uint16", reflect.Uint16, true},
{"uint32", reflect.Uint32, true},
{"uint64", reflect.Uint64, true},
{"float32", reflect.Float32, true},
{"float64", reflect.Float64, true},
{"string", reflect.String, false},
{"bool", reflect.Bool, false},
{"struct", reflect.Struct, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsNumericType(tt.kind)
if result != tt.expected {
t.Errorf("IsNumericType(%v) = %v, want %v", tt.kind, result, tt.expected)
}
})
}
}
func TestIsStringType(t *testing.T) {
tests := []struct {
name string
kind reflect.Kind
expected bool
}{
{"string", reflect.String, true},
{"int", reflect.Int, false},
{"bool", reflect.Bool, false},
{"struct", reflect.Struct, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsStringType(tt.kind)
if result != tt.expected {
t.Errorf("IsStringType(%v) = %v, want %v", tt.kind, result, tt.expected)
}
})
}
}
func TestIsNumericValue(t *testing.T) {
tests := []struct {
name string
value string
expected bool
}{
{"integer", "123", true},
{"negative integer", "-456", true},
{"float", "123.45", true},
{"negative float", "-123.45", true},
{"scientific notation", "1.23e10", true},
{"with spaces", " 789 ", true},
{"non-numeric", "abc", false},
{"mixed", "123abc", false},
{"empty string", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsNumericValue(tt.value)
if result != tt.expected {
t.Errorf("IsNumericValue(%q) = %v, want %v", tt.value, result, tt.expected)
}
})
}
}
func TestConvertToNumericType(t *testing.T) {
tests := []struct {
name string
value string
kind reflect.Kind
expected interface{}
expectError bool
}{
// Integer types
{"int", "123", reflect.Int, int(123), false},
{"int8", "100", reflect.Int8, int8(100), false},
{"int16", "1000", reflect.Int16, int16(1000), false},
{"int32", "100000", reflect.Int32, int32(100000), false},
{"int64", "9223372036854775807", reflect.Int64, int64(9223372036854775807), false},
{"negative int", "-456", reflect.Int, int(-456), false},
{"invalid int", "abc", reflect.Int, nil, true},
// Unsigned integer types
{"uint", "123", reflect.Uint, uint(123), false},
{"uint8", "255", reflect.Uint8, uint8(255), false},
{"uint16", "65535", reflect.Uint16, uint16(65535), false},
{"uint32", "4294967295", reflect.Uint32, uint32(4294967295), false},
{"uint64", "18446744073709551615", reflect.Uint64, uint64(18446744073709551615), false},
{"invalid uint", "abc", reflect.Uint, nil, true},
{"negative uint", "-1", reflect.Uint, nil, true},
// Float types
{"float32", "123.45", reflect.Float32, float32(123.45), false},
{"float64", "123.456789", reflect.Float64, float64(123.456789), false},
{"negative float", "-123.45", reflect.Float64, float64(-123.45), false},
{"scientific notation", "1.23e10", reflect.Float64, float64(1.23e10), false},
{"invalid float", "abc", reflect.Float32, nil, true},
// Edge cases
{"with spaces", " 789 ", reflect.Int, int(789), false},
// Unsupported types
{"unsupported type", "123", reflect.String, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ConvertToNumericType(tt.value, tt.kind)
if tt.expectError {
if err == nil {
t.Errorf("ConvertToNumericType(%q, %v) expected error, got nil", tt.value, tt.kind)
}
return
}
if err != nil {
t.Errorf("ConvertToNumericType(%q, %v) unexpected error: %v", tt.value, tt.kind, err)
return
}
if result != tt.expected {
t.Errorf("ConvertToNumericType(%q, %v) = %v, want %v", tt.value, tt.kind, result, tt.expected)
}
})
}
}
// Test model for GetColumnTypeFromModel
type TypeTestModel struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Balance float64 `json:"balance"`
Active bool `json:"active"`
Metadata string `json:"metadata"`
}
func TestGetColumnTypeFromModel(t *testing.T) {
model := TypeTestModel{
ID: 1,
Name: "Test",
Age: 30,
Balance: 100.50,
Active: true,
Metadata: `{"key": "value"}`,
}
tests := []struct {
name string
model interface{}
colName string
expected reflect.Kind
}{
{"int field", model, "id", reflect.Int},
{"string field", model, "name", reflect.String},
{"int field by name", model, "age", reflect.Int},
{"float64 field", model, "balance", reflect.Float64},
{"bool field", model, "active", reflect.Bool},
{"string with JSON", model, "metadata", reflect.String},
{"non-existent field", model, "nonexistent", reflect.Invalid},
{"nil model", nil, "id", reflect.Invalid},
{"pointer to model", &model, "name", reflect.String},
{"column with JSON operator", model, "metadata->>'key'", reflect.String},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetColumnTypeFromModel(tt.model, tt.colName)
if result != tt.expected {
t.Errorf("GetColumnTypeFromModel(%v, %q) = %v, want %v", tt.model, tt.colName, result, tt.expected)
}
})
}
}
// SqlNull-wrapped columns (e.g. nullable bigint foreign keys) must report the
// wrapped value's Kind, not reflect.Struct, so numeric eq/gt/lt filters don't
// get an unnecessary CAST(... AS TEXT) that defeats the column's index.
type SqlNullFKModel struct {
RidParent spectypes.SqlInt64 `bun:"rid_parent" json:"rid_parent"`
}
func TestGetColumnTypeFromModel_SqlNullWrapper(t *testing.T) {
model := SqlNullFKModel{RidParent: spectypes.NewSqlInt64(90446096)}
result := GetColumnTypeFromModel(model, "rid_parent")
if result != reflect.Int64 {
t.Errorf("GetColumnTypeFromModel(rid_parent) = %v, want %v (SqlInt64 must unwrap to its numeric Kind)", result, reflect.Int64)
}
}
// ============= Tests for relation functions =============
// Models for relation testing
type Author struct {
ID int `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
Books []Book `bun:"rel:has-many,join:id=author_id" json:"books"`
}
type Book struct {
ID int `bun:"id,pk" json:"id"`
Title string `bun:"title" json:"title"`
AuthorID int `bun:"author_id" json:"author_id"`
Author *Author `bun:"rel:belongs-to,join:author_id=id" json:"author"`
Publisher *Publisher `bun:"rel:has-one,join:id=book_id" json:"publisher"`
}
type Publisher struct {
ID int `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
BookID int `bun:"book_id" json:"book_id"`
}
type Student struct {
ID int `gorm:"column:id;primaryKey" json:"id"`
Name string `gorm:"column:name" json:"name"`
Courses []Course `gorm:"many2many:student_courses" json:"courses"`
}
type Course struct {
ID int `gorm:"column:id;primaryKey" json:"id"`
Title string `gorm:"column:title" json:"title"`
Students []Student `gorm:"many2many:student_courses" json:"students"`
}
// Recursive relation model
type Category struct {
ID int `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
ParentID *int `bun:"parent_id" json:"parent_id"`
Parent *Category `bun:"rel:belongs-to,join:parent_id=id" json:"parent"`
Children []Category `bun:"rel:has-many,join:id=parent_id" json:"children"`
}
func TestGetRelationType(t *testing.T) {
tests := []struct {
name string
model interface{}
fieldName string
expected RelationType
}{
// Bun relations
{"has-many relation", Author{}, "Books", RelationHasMany},
{"belongs-to relation", Book{}, "Author", RelationBelongsTo},
{"has-one relation", Book{}, "Publisher", RelationHasOne},
// GORM relations
{"many-to-many relation (GORM)", Student{}, "Courses", RelationManyToMany},
{"many-to-many reverse (GORM)", Course{}, "Students", RelationManyToMany},
// Recursive relations
{"recursive belongs-to", Category{}, "Parent", RelationBelongsTo},
{"recursive has-many", Category{}, "Children", RelationHasMany},
// Edge cases
{"non-existent field", Author{}, "NonExistent", RelationUnknown},
{"nil model", nil, "Books", RelationUnknown},
{"empty field name", Author{}, "", RelationUnknown},
{"pointer model", &Author{}, "Books", RelationHasMany},
// Case-insensitive field names
{"case-insensitive has-many", Author{}, "books", RelationHasMany},
{"case-insensitive belongs-to", Book{}, "author", RelationBelongsTo},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetRelationType(tt.model, tt.fieldName)
if result != tt.expected {
t.Errorf("GetRelationType(%T, %q) = %v, want %v", tt.model, tt.fieldName, result, tt.expected)
}
})
}
}
func TestShouldUseJoin(t *testing.T) {
tests := []struct {
name string
relType RelationType
expected bool
}{
{"belongs-to should use JOIN", RelationBelongsTo, true},
{"has-one should use JOIN", RelationHasOne, true},
{"has-many should NOT use JOIN", RelationHasMany, false},
{"many-to-many should NOT use JOIN", RelationManyToMany, false},
{"unknown should NOT use JOIN", RelationUnknown, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.relType.ShouldUseJoin()
if result != tt.expected {
t.Errorf("RelationType(%v).ShouldUseJoin() = %v, want %v", tt.relType, result, tt.expected)
}
})
}
}
func TestGetRelationModel(t *testing.T) {
tests := []struct {
name string
model interface{}
fieldName string
isNil bool
}{
{"has-many relation", Author{}, "Books", false},
{"belongs-to relation", Book{}, "Author", false},
{"has-one relation", Book{}, "Publisher", false},
{"many-to-many relation", Student{}, "Courses", false},
// Recursive relations
{"recursive belongs-to", Category{}, "Parent", false},
{"recursive has-many", Category{}, "Children", false},
// Nested/recursive field paths
{"nested recursive", Category{}, "Parent.Parent", false},
{"nested recursive children", Category{}, "Children", false},
// Edge cases
{"non-existent field", Author{}, "NonExistent", true},
{"nil model", nil, "Books", true},
{"empty field name", Author{}, "", true},
{"pointer model", &Author{}, "Books", false},
// Case-insensitive field names
{"case-insensitive has-many", Author{}, "books", false},
{"case-insensitive belongs-to", Book{}, "author", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetRelationModel(tt.model, tt.fieldName)
if tt.isNil {
if result != nil {
t.Errorf("GetRelationModel(%T, %q) = %v, want nil", tt.model, tt.fieldName, result)
}
} else {
if result == nil {
t.Errorf("GetRelationModel(%T, %q) = nil, want non-nil", tt.model, tt.fieldName)
}
}
})
}
}
// ============= Additional edge case tests for better coverage =============
func TestGetPrimaryKeyName_EdgeCases(t *testing.T) {
tests := []struct {
name string
model any
expected string
}{
{
name: "nil model",
model: nil,
expected: "",
},
{
name: "string model name (not implemented yet)",
model: "SomeModel",
expected: "",
},
{
name: "slice of models",
model: []BunModelWithColumnTag{},
expected: "",
},
{
name: "array of models",
model: [3]BunModelWithColumnTag{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPrimaryKeyName(tt.model)
if result != tt.expected {
t.Errorf("GetPrimaryKeyName() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetPrimaryKeyValue_EdgeCases(t *testing.T) {
tests := []struct {
name string
model any
expected any
}{
{
name: "nil model",
model: nil,
expected: nil,
},
{
name: "non-struct type",
model: 123,
expected: nil,
},
{
name: "slice",
model: []int{1, 2, 3},
expected: nil,
},
{
name: "model without primary key tags - fallback to ID field",
model: struct {
ID int
Name string
}{ID: 99, Name: "Test"},
expected: 99,
},
{
name: "model without ID field",
model: struct {
Name string
}{Name: "Test"},
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetPrimaryKeyValue(tt.model)
if result != tt.expected {
t.Errorf("GetPrimaryKeyValue() = %v, want %v", result, tt.expected)
}
})
}
}
func TestGetModelColumns_EdgeCases(t *testing.T) {
tests := []struct {
name string
model any
expected []string
}{
{
name: "nil type",
model: nil,
expected: []string{},
},
{
name: "non-struct type",
model: 123,
expected: []string{},
},
{
name: "slice type",
model: []BunModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
{
name: "array type",
model: [3]BunModelWithColumnTag{},
expected: []string{"custom_id", "name"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetModelColumns(tt.model)
if len(result) != len(tt.expected) {
t.Errorf("GetModelColumns() returned %d columns, want %d", len(result), len(tt.expected))
return
}
for i, col := range result {
if col != tt.expected[i] {
t.Errorf("GetModelColumns()[%d] = %v, want %v", i, col, tt.expected[i])
}
}
})
}
}
func TestIsColumnWritable_EdgeCases(t *testing.T) {
tests := []struct {
name string
model any
columnName string
expected bool
}{
{
name: "nil model",
model: nil,
columnName: "name",
expected: false,
},
{
name: "non-struct type",
model: 123,
columnName: "name",
expected: false,
},
{
name: "column not found in model (dynamic column)",
model: BunModelWithColumnTag{},
columnName: "dynamic_column",
expected: true, // Not found, allow it (might be dynamic)
},
{
name: "pointer to model",
model: &ModelWithEmbedded{},
columnName: "name",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsColumnWritable(tt.model, tt.columnName)
if result != tt.expected {
t.Errorf("IsColumnWritable(%s) = %v, want %v", tt.columnName, result, tt.expected)
}
})
}
}
func TestIsGormFieldReadOnly_EdgeCases(t *testing.T) {
tests := []struct {
name string
tag string
expected bool
}{
{
name: "read-only marker",
tag: "column:name;->",
expected: true,
},
{
name: "write restriction <-:false",
tag: "column:name;<-:false",
expected: true,
},
{
name: "write allowed <-:create",
tag: "<-:create",
expected: false,
},
{
name: "write allowed <-:update",
tag: "<-:update",
expected: false,
},
{
name: "no restrictions",
tag: "column:name;type:varchar(255)",
expected: false,
},
{
name: "empty tag",
tag: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isGormFieldReadOnly(tt.tag)
if result != tt.expected {
t.Errorf("isGormFieldReadOnly(%q) = %v, want %v", tt.tag, result, tt.expected)
}
})
}
}
func TestGetSQLModelColumns_EdgeCases(t *testing.T) {
tests := []struct {
name string
model any
expected []string
}{
{
name: "nil model",
model: nil,
expected: []string{},
},
{
name: "non-struct type",
model: 123,
expected: []string{},
},
{
name: "slice type",
model: []Profile{},
expected: []string{"id", "bio", "user_id"},
},
{
name: "array type",
model: [2]Profile{},
expected: []string{"id", "bio", "user_id"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSQLModelColumns(tt.model)
if len(result) != len(tt.expected) {
t.Errorf("GetSQLModelColumns() returned %d columns, want %d.\nGot: %v\nWant: %v",
len(result), len(tt.expected), result, tt.expected)
return
}
for i, col := range result {
if col != tt.expected[i] {
t.Errorf("GetSQLModelColumns()[%d] = %v, want %v.\nFull result: %v",
i, col, tt.expected[i], result)
}
}
})
}
}
// Test models with table:, rel:, join: tags for ExtractColumnFromBunTag
type BunSpecialTagsModel struct {
Table string `bun:"table:users"`
Relation []Post `bun:"rel:has-many"`
Join string `bun:"join:id=user_id"`
NormalCol string `bun:"normal_col"`
}
func TestExtractColumnFromBunTag_SpecialTags(t *testing.T) {
tests := []struct {
name string
tag string
expected string
}{
{
name: "table tag",
tag: "table:users",
expected: "",
},
{
name: "rel tag",
tag: "rel:has-many",
expected: "",
},
{
name: "join tag",
tag: "join:id=user_id",
expected: "",
},
{
name: "normal column",
tag: "normal_col,pk",
expected: "normal_col",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractColumnFromBunTag(tt.tag)
if result != tt.expected {
t.Errorf("ExtractColumnFromBunTag(%q) = %q, want %q", tt.tag, result, tt.expected)
}
})
}
}
// Test GORM fallback scenarios
type GormFallbackModel struct {
UserID int `gorm:"foreignKey:UserId"`
}
func TestGetRelationType_GORMFallback(t *testing.T) {
tests := []struct {
name string
model interface{}
fieldName string
expected RelationType
}{
{
name: "GORM slice without many2many",
model: Post{},
fieldName: "Tags",
expected: RelationManyToMany, // Has many2many tag
},
{
name: "GORM pointer with foreignKey",
model: Post{},
fieldName: "User",
expected: RelationBelongsTo,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetRelationType(tt.model, tt.fieldName)
if result != tt.expected {
t.Errorf("GetRelationType(%T, %q) = %v, want %v", tt.model, tt.fieldName, result, tt.expected)
}
})
}
}
// Additional tests for better coverage of GetRelationType
func TestGetRelationType_AdditionalCases(t *testing.T) {
// Test model with GORM has-one (pointer without foreignKey or with references)
type Address struct {
ID int `gorm:"column:id;primaryKey"`
UserID int `gorm:"column:user_id"`
}
type UserWithAddress struct {
ID int `gorm:"column:id;primaryKey"`
Address *Address `gorm:"references:UserID"` // has-one relation
}
// Test model with field type inference
type Company struct {
ID int
Name string
}
type Employee struct {
ID int
Company Company // Single struct (not pointer, not slice) - belongs-to
Coworkers []Employee // Slice without bun/gorm tags - has-many
}
tests := []struct {
name string
model interface{}
fieldName string
expected RelationType
}{
{
name: "GORM has-one (pointer with references)",
model: UserWithAddress{},
fieldName: "Address",
expected: RelationHasOne,
},
{
name: "Field type inference - single struct",
model: Employee{},
fieldName: "Company",
expected: RelationBelongsTo,
},
{
name: "Field type inference - slice",
model: Employee{},
fieldName: "Coworkers",
expected: RelationHasMany,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetRelationType(tt.model, tt.fieldName)
if result != tt.expected {
t.Errorf("GetRelationType(%T, %q) = %v, want %v", tt.model, tt.fieldName, result, tt.expected)
}
})
}
}
// Test for GetColumnTypeFromModel with more edge cases
func TestGetColumnTypeFromModel_AdditionalCases(t *testing.T) {
type ModelWithSnakeCase struct {
UserID int `json:"user_id"`
UserName string // No tag, will match by snake_case conversion
}
model := ModelWithSnakeCase{
UserID: 123,
UserName: "John",
}
tests := []struct {
name string
model interface{}
colName string
expected reflect.Kind
}{
{"field by snake_case name", model, "user_name", reflect.String},
{"non-struct model", 123, "field", reflect.Invalid},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetColumnTypeFromModel(tt.model, tt.colName)
if result != tt.expected {
t.Errorf("GetColumnTypeFromModel(%v, %q) = %v, want %v", tt.model, tt.colName, result, tt.expected)
}
})
}
}
// Test for getRelationModelSingleLevel edge cases
func TestGetRelationModel_WithTags(t *testing.T) {
// Test matching by gorm column tag
type Department struct {
ID int `gorm:"column:dept_id;primaryKey"`
Name string `gorm:"column:dept_name"`
}
type Manager struct {
ID int `gorm:"column:id;primaryKey"`
DeptID int `gorm:"column:department_id"`
Department *Department `gorm:"column:dept;foreignKey:DeptID"`
}
tests := []struct {
name string
model interface{}
fieldName string
isNil bool
}{
// Test matching by gorm column name
{"match by gorm column", Manager{}, "dept", false},
// Test matching by json tag
{"match by json tag", Book{}, "author", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetRelationModel(tt.model, tt.fieldName)
if tt.isNil {
if result != nil {
t.Errorf("GetRelationModel(%T, %q) = %v, want nil", tt.model, tt.fieldName, result)
}
} else {
if result == nil {
t.Errorf("GetRelationModel(%T, %q) = nil, want non-nil", tt.model, tt.fieldName)
}
}
})
}
}
func TestMapToStruct(t *testing.T) {
// Test model with various field types
type TestModel struct {
ID int64 `bun:"id,pk" json:"id"`
Name string `bun:"name" json:"name"`
Age int `bun:"age" json:"age"`
Active bool `bun:"active" json:"active"`
Score float64 `bun:"score" json:"score"`
Data []byte `bun:"data" json:"data"`
MetaJSON []byte `bun:"meta_json" json:"meta_json"`
}
tests := []struct {
name string
dataMap map[string]interface{}
expected TestModel
wantErr bool
}{
{
name: "Basic types conversion",
dataMap: map[string]interface{}{
"id": int64(123),
"name": "Test User",
"age": 30,
"active": true,
"score": 95.5,
},
expected: TestModel{
ID: 123,
Name: "Test User",
Age: 30,
Active: true,
Score: 95.5,
},
wantErr: false,
},
{
name: "Byte slice (SqlJSONB-like) from []byte",
dataMap: map[string]interface{}{
"id": int64(456),
"name": "JSON Test",
"data": []byte(`{"key":"value"}`),
},
expected: TestModel{
ID: 456,
Name: "JSON Test",
Data: []byte(`{"key":"value"}`),
},
wantErr: false,
},
{
name: "Byte slice from string",
dataMap: map[string]interface{}{
"id": int64(789),
"data": "string data",
},
expected: TestModel{
ID: 789,
Data: []byte("string data"),
},
wantErr: false,
},
{
name: "Byte slice from map (JSON marshal)",
dataMap: map[string]interface{}{
"id": int64(999),
"meta_json": map[string]interface{}{
"field1": "value1",
"field2": 42,
},
},
expected: TestModel{
ID: 999,
MetaJSON: []byte(`{"field1":"value1","field2":42}`),
},
wantErr: false,
},
{
name: "Byte slice from slice (JSON marshal)",
dataMap: map[string]interface{}{
"id": int64(111),
"meta_json": []interface{}{"item1", "item2", 3},
},
expected: TestModel{
ID: 111,
MetaJSON: []byte(`["item1","item2",3]`),
},
wantErr: false,
},
{
name: "Field matching by bun tag",
dataMap: map[string]interface{}{
"id": int64(222),
"name": "Tagged Field",
},
expected: TestModel{
ID: 222,
Name: "Tagged Field",
},
wantErr: false,
},
{
name: "Nil values",
dataMap: map[string]interface{}{
"id": int64(333),
"data": nil,
},
expected: TestModel{
ID: 333,
Data: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result TestModel
err := MapToStruct(tt.dataMap, &result)
if (err != nil) != tt.wantErr {
t.Errorf("MapToStruct() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Compare fields individually for better error messages
if result.ID != tt.expected.ID {
t.Errorf("ID = %v, want %v", result.ID, tt.expected.ID)
}
if result.Name != tt.expected.Name {
t.Errorf("Name = %v, want %v", result.Name, tt.expected.Name)
}
if result.Age != tt.expected.Age {
t.Errorf("Age = %v, want %v", result.Age, tt.expected.Age)
}
if result.Active != tt.expected.Active {
t.Errorf("Active = %v, want %v", result.Active, tt.expected.Active)
}
if result.Score != tt.expected.Score {
t.Errorf("Score = %v, want %v", result.Score, tt.expected.Score)
}
// For byte slices, compare as strings for JSON data
if tt.expected.Data != nil {
if string(result.Data) != string(tt.expected.Data) {
t.Errorf("Data = %s, want %s", string(result.Data), string(tt.expected.Data))
}
}
if tt.expected.MetaJSON != nil {
if string(result.MetaJSON) != string(tt.expected.MetaJSON) {
t.Errorf("MetaJSON = %s, want %s", string(result.MetaJSON), string(tt.expected.MetaJSON))
}
}
})
}
}
func TestMapToStruct_Errors(t *testing.T) {
type TestModel struct {
ID int `bun:"id" json:"id"`
}
tests := []struct {
name string
dataMap map[string]interface{}
target interface{}
wantErr bool
}{
{
name: "Nil dataMap",
dataMap: nil,
target: &TestModel{},
wantErr: true,
},
{
name: "Nil target",
dataMap: map[string]interface{}{"id": 1},
target: nil,
wantErr: true,
},
{
name: "Non-pointer target",
dataMap: map[string]interface{}{"id": 1},
target: TestModel{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := MapToStruct(tt.dataMap, tt.target)
if (err != nil) != tt.wantErr {
t.Errorf("MapToStruct() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestRemoveNonWritableColumns_Generated(t *testing.T) {
type m struct {
ID int `bun:"id,pk"`
Email string `bun:"email"`
Norm string `bun:"email_normalized,generated"`
Scan string `bun:"scan_col,scanonly"`
}
vals := map[string]interface{}{"id": 1, "email": "A", "email_normalized": "a", "scan_col": "x", "dynamic": 1}
RemoveNonWritableColumns(&m{}, vals)
if _, ok := vals["email_normalized"]; ok {
t.Error("generated column not removed")
}
if _, ok := vals["scan_col"]; ok {
t.Error("scanonly column not removed")
}
if len(vals) != 3 {
t.Errorf("unexpected keys: %v", vals)
}
}
func TestNonWritableColumns(t *testing.T) {
type base struct {
Created string `bun:"created_at,scanonly"`
}
type m struct {
base
ID int `bun:"id,pk"`
Email string `bun:"email"`
Norm string `bun:"email_normalized,generated"`
Ro string `gorm:"column:ro;->"`
}
got := NonWritableColumns(&m{})
want := map[string]bool{"created_at": true, "email_normalized": true, "ro": true}
if len(got) != len(want) {
t.Fatalf("got %v", got)
}
for _, c := range got {
if !want[c] {
t.Errorf("unexpected %s", c)
}
}
}
func TestNonWritableColumns_EmbeddedScanOnlyBuffer(t *testing.T) {
type buffer struct {
CQL1 string `json:"cql1,omitempty" gorm:"->" bun:",scanonly"`
RowNumber int64 `json:"_rownumber,omitempty" gorm:"-" bun:",scanonly"`
}
type m struct {
ID int `json:"id" bun:"id,pk"`
Note string `json:"note" bun:"note,type:citext,"`
buffer `json:",omitempty" bun:",scanonly"`
}
got := NonWritableColumns(&m{})
has := map[string]bool{}
for _, c := range got {
has[c] = true
}
if !has["cql1"] {
t.Errorf("cql1 should be non-writable, got %v", got)
}
if has["id"] || has["note"] {
t.Errorf("writable columns reported as non-writable: %v", got)
}
vals := map[string]interface{}{"id": 1, "note": "x", "cql1": "y"}
RemoveNonWritableColumns(&m{}, vals)
if _, ok := vals["cql1"]; ok || len(vals) != 2 {
t.Errorf("unexpected values: %v", vals)
}
}