200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
package nigiri
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
type ConstraintType string
|
|
|
|
const (
|
|
ConstraintUnique ConstraintType = "unique"
|
|
ConstraintForeign ConstraintType = "fkey"
|
|
ConstraintRequired ConstraintType = "required"
|
|
ConstraintIndex ConstraintType = "index"
|
|
ConstraintOneToOne ConstraintType = "one_to_one"
|
|
ConstraintOneToMany ConstraintType = "one_to_many"
|
|
ConstraintManyToOne ConstraintType = "many_to_one"
|
|
ConstraintManyToMany ConstraintType = "many_to_many"
|
|
)
|
|
|
|
type RelationshipType string
|
|
|
|
const (
|
|
RelationshipOneToOne RelationshipType = "one_to_one"
|
|
RelationshipOneToMany RelationshipType = "one_to_many"
|
|
RelationshipManyToOne RelationshipType = "many_to_one"
|
|
RelationshipManyToMany RelationshipType = "many_to_many"
|
|
)
|
|
|
|
type FieldConstraint struct {
|
|
Type ConstraintType
|
|
Field string
|
|
Target string
|
|
IndexName string
|
|
Relationship RelationshipType
|
|
TargetType reflect.Type
|
|
}
|
|
|
|
type SchemaInfo struct {
|
|
Fields map[string]reflect.Type
|
|
Constraints map[string][]FieldConstraint
|
|
Indices map[string]string
|
|
Relationships map[string]FieldConstraint
|
|
}
|
|
|
|
func ParseSchema[T any]() *SchemaInfo {
|
|
var zero T
|
|
t := reflect.TypeOf(zero)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
schema := &SchemaInfo{
|
|
Fields: make(map[string]reflect.Type),
|
|
Constraints: make(map[string][]FieldConstraint),
|
|
Indices: make(map[string]string),
|
|
Relationships: make(map[string]FieldConstraint),
|
|
}
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
fieldName := field.Name
|
|
fieldType := field.Type
|
|
schema.Fields[fieldName] = fieldType
|
|
|
|
// Check for relationship patterns in field type
|
|
if relationship := detectRelationship(fieldName, fieldType); relationship != nil {
|
|
schema.Relationships[fieldName] = *relationship
|
|
schema.Constraints[fieldName] = append(schema.Constraints[fieldName], *relationship)
|
|
}
|
|
|
|
// Parse explicit db tags
|
|
dbTag := field.Tag.Get("db")
|
|
if dbTag != "" {
|
|
constraints := parseDBTag(fieldName, dbTag)
|
|
if len(constraints) > 0 {
|
|
schema.Constraints[fieldName] = append(schema.Constraints[fieldName], constraints...)
|
|
}
|
|
}
|
|
|
|
// Auto-create indices for unique and indexed fields
|
|
for _, constraint := range schema.Constraints[fieldName] {
|
|
if constraint.Type == ConstraintUnique || constraint.Type == ConstraintIndex {
|
|
indexName := constraint.IndexName
|
|
if indexName == "" {
|
|
indexName = fieldName + "_idx"
|
|
}
|
|
schema.Indices[fieldName] = indexName
|
|
}
|
|
}
|
|
}
|
|
|
|
return schema
|
|
}
|
|
|
|
func detectRelationship(fieldName string, fieldType reflect.Type) *FieldConstraint {
|
|
switch fieldType.Kind() {
|
|
case reflect.Ptr:
|
|
// *EntityType = many-to-one
|
|
elemType := fieldType.Elem()
|
|
if isEntityType(elemType) {
|
|
return &FieldConstraint{
|
|
Type: ConstraintManyToOne,
|
|
Field: fieldName,
|
|
Relationship: RelationshipManyToOne,
|
|
TargetType: elemType,
|
|
Target: getEntityName(elemType),
|
|
}
|
|
}
|
|
|
|
case reflect.Slice:
|
|
// []*EntityType = one-to-many
|
|
elemType := fieldType.Elem()
|
|
if elemType.Kind() == reflect.Ptr {
|
|
ptrTargetType := elemType.Elem()
|
|
if isEntityType(ptrTargetType) {
|
|
return &FieldConstraint{
|
|
Type: ConstraintOneToMany,
|
|
Field: fieldName,
|
|
Relationship: RelationshipOneToMany,
|
|
TargetType: ptrTargetType,
|
|
Target: getEntityName(ptrTargetType),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isEntityType(t reflect.Type) bool {
|
|
if t.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
|
|
// Check if it has an ID field
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if field.Name == "ID" && field.Type.Kind() == reflect.Int {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func getEntityName(t reflect.Type) string {
|
|
name := t.Name()
|
|
if name == "" {
|
|
name = t.String()
|
|
}
|
|
return strings.ToLower(name)
|
|
}
|
|
|
|
func parseDBTag(fieldName, tag string) []FieldConstraint {
|
|
var constraints []FieldConstraint
|
|
parts := strings.Split(tag, ",")
|
|
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case part == "unique":
|
|
constraints = append(constraints, FieldConstraint{
|
|
Type: ConstraintUnique,
|
|
Field: fieldName,
|
|
})
|
|
case part == "required":
|
|
constraints = append(constraints, FieldConstraint{
|
|
Type: ConstraintRequired,
|
|
Field: fieldName,
|
|
})
|
|
case part == "index":
|
|
constraints = append(constraints, FieldConstraint{
|
|
Type: ConstraintIndex,
|
|
Field: fieldName,
|
|
})
|
|
case strings.HasPrefix(part, "index:"):
|
|
indexName := strings.TrimPrefix(part, "index:")
|
|
constraints = append(constraints, FieldConstraint{
|
|
Type: ConstraintIndex,
|
|
Field: fieldName,
|
|
IndexName: indexName,
|
|
})
|
|
case strings.HasPrefix(part, "fkey:"):
|
|
target := strings.TrimPrefix(part, "fkey:")
|
|
constraints = append(constraints, FieldConstraint{
|
|
Type: ConstraintForeign,
|
|
Field: fieldName,
|
|
Target: target,
|
|
})
|
|
}
|
|
}
|
|
|
|
return constraints
|
|
}
|