mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-06 13:26:28 +00:00
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
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.
1994 lines
49 KiB
Go
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)
|
|
}
|
|
}
|