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.Pointer: // *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.Pointer { 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 }