fix(spec): unwrap SQL wrapper metadata types

This commit is contained in:
2026-08-28 22:42:52 +02:00
parent dab4940ace
commit a68cf83be6
6 changed files with 218 additions and 19 deletions
+36 -4
View File
@@ -2073,16 +2073,23 @@ func (h *Handler) generateMetadata(schema, entity string, model interface{}) *co
jsonName = field.Name
}
if field.Type.Kind() == reflect.Slice ||
(field.Type.Kind() == reflect.Struct && field.Type.Name() != "Time") {
columnField := field
isSQLType := false
if unwrappedType, ok := unwrapSQLType(field.Type); ok {
columnField.Type = unwrappedType
isSQLType = true
}
if !isSQLType && (columnField.Type.Kind() == reflect.Slice ||
(columnField.Type.Kind() == reflect.Struct && columnField.Type.Name() != "Time")) {
metadata.Relations = append(metadata.Relations, jsonName)
continue
}
column := common.Column{
Name: jsonName,
Type: getColumnType(field),
IsNullable: isNullable(field),
Type: getColumnType(columnField),
IsNullable: isSQLType || isNullable(field),
IsPrimary: strings.Contains(gormTag, "primaryKey"),
IsUnique: strings.Contains(gormTag, "unique") || strings.Contains(gormTag, "uniqueIndex"),
HasIndex: strings.Contains(gormTag, "index") || strings.Contains(gormTag, "uniqueIndex"),
@@ -2173,6 +2180,31 @@ func getColumnType(field reflect.StructField) string {
}
}
// unwrapSQLType returns the value type wrapped by a spectypes SQL value. These
// types represent columns, even when their Go representation is a struct or a
// slice (for example, SqlNull[string] and SqlJSONB).
func unwrapSQLType(t reflect.Type) (reflect.Type, bool) {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.PkgPath() != "github.com/bitechdev/ResolveSpec/pkg/spectypes" {
return nil, false
}
// SqlNull aliases and the date/time wrappers expose their actual value via
// Val. FieldByName also resolves Val through the embedded SqlNull field.
if t.Kind() == reflect.Struct {
if value, ok := t.FieldByName("Val"); ok {
return value.Type, true
}
}
// SqlJSONB has no wrapper field, but remains a scalar SQL value rather than
// a relation.
return t, true
}
func isNullable(field reflect.StructField) bool {
// Check if it's a pointer type
if field.Type.Kind() == reflect.Pointer {
+43
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
)
func TestNewHandler(t *testing.T) {
@@ -202,6 +203,48 @@ func TestGetColumnType(t *testing.T) {
}
}
func TestGenerateMetadata_UnwrapsSQLTypes(t *testing.T) {
type related struct {
Name string `json:"name"`
}
type model struct {
Name spectypes.SqlString `json:"name"`
Count spectypes.SqlInt64 `json:"count"`
CreatedAt spectypes.SqlTimeStamp `json:"created_at"`
Metadata spectypes.SqlJSONB `json:"metadata" gorm:"type:jsonb"`
Related related `json:"related"`
}
metadata := NewHandler(nil, nil).generateMetadata("public", "models", model{})
columns := make(map[string]common.Column, len(metadata.Columns))
for _, column := range metadata.Columns {
columns[column.Name] = column
}
for name, wantType := range map[string]string{
"name": "string",
"count": "bigint",
"created_at": "timestamp",
"metadata": "jsonb",
} {
column, ok := columns[name]
if !ok {
t.Errorf("expected %q to be a metadata column", name)
continue
}
if column.Type != wantType {
t.Errorf("%q: expected type %q, got %q", name, wantType, column.Type)
}
if !column.IsNullable {
t.Errorf("%q: expected SQL wrapper to be nullable", name)
}
}
if len(metadata.Relations) != 1 || metadata.Relations[0] != "related" {
t.Errorf("expected only related to be a relation, got %v", metadata.Relations)
}
}
func TestIsNullable(t *testing.T) {
tests := []struct {
name string