Skip to content
This repository was archived by the owner on Sep 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions api/dgraphtypes/dgraphtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"github.com/dgraph-io/dgo/v240/protos/api"
"github.com/hypermodeinc/dgraph/v24/protos/pb"
"github.com/hypermodeinc/dgraph/v24/types"
modusapi "github.com/hypermodeinc/modusdb/api"
"github.com/hypermodeinc/modusdb/api/structreflect"
"github.com/pkg/errors"
"github.com/twpayne/go-geom"
"github.com/twpayne/go-geom/encoding/wkb"
)
Expand Down Expand Up @@ -70,15 +72,16 @@ func ValueToPosting_ValType(v any) (pb.Posting_ValType, error) {
return pb.Posting_BINARY, nil
case time.Time:
return pb.Posting_DATETIME, nil
case geom.Point:
case modusapi.Point, modusapi.Polygon:
return pb.Posting_GEO, nil
case []float32, []float64:
return pb.Posting_VFLOAT, nil
default:
return pb.Posting_DEFAULT, fmt.Errorf("unsupported type %T", v)
return pb.Posting_DEFAULT, fmt.Errorf("value to posting, unsupported type %T", v)
}
}

// ValueToApiVal converts a value to an api.Value. Note the result can be nil for empty non-scalar types
func ValueToApiVal(v any) (*api.Value, error) {
switch val := v.(type) {
case string:
Expand Down Expand Up @@ -124,17 +127,44 @@ func ValueToApiVal(v any) (*api.Value, error) {
if err != nil {
return nil, err
}
return &api.Value{Val: &api.Value_DateVal{DateVal: bytes}}, nil
case geom.Point:
bytes, err := wkb.Marshal(&val, binary.LittleEndian)
return &api.Value{Val: &api.Value_DatetimeVal{DatetimeVal: bytes}}, nil
case modusapi.Point:
if len(val.Coordinates) == 0 {
return nil, nil
}
point, err := geom.NewPoint(geom.XY).SetCoords(val.Coordinates)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "converting point to api value")
}
bytes, err := wkb.Marshal(point, binary.LittleEndian)
if err != nil {
return nil, errors.Wrap(err, "marshalling point to wkb")
}
return &api.Value{Val: &api.Value_GeoVal{GeoVal: bytes}}, nil
case modusapi.Polygon:
if len(val.Coordinates) == 0 {
return nil, nil
}
coords := make([][]geom.Coord, len(val.Coordinates))
for i, polygon := range val.Coordinates {
coords[i] = make([]geom.Coord, len(polygon))
for j, point := range polygon {
coords[i][j] = geom.Coord{point[0], point[1]}
}
}
polygon, err := geom.NewPolygon(geom.XY).SetCoords(coords)
if err != nil {
return nil, errors.Wrap(err, "converting polygon to api value")
}
bytes, err := wkb.Marshal(polygon, binary.LittleEndian)
if err != nil {
return nil, errors.Wrap(err, "marshalling polygon to wkb")
}
return &api.Value{Val: &api.Value_GeoVal{GeoVal: bytes}}, nil
case uint:
return &api.Value{Val: &api.Value_DefaultVal{DefaultVal: fmt.Sprint(v)}}, nil
default:
return nil, fmt.Errorf("unsupported type %T", v)
return nil, fmt.Errorf("convert value to api value, unsupported type %T", v)
}
}

Expand Down
3 changes: 2 additions & 1 deletion api/mutations/mutations.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func CreateNQuadAndSchema(value any, gid uint64, jsonName string, t reflect.Type
return nil, nil, err
}

// val can be null here for "empty" no-scalar types
val, err := dgraphtypes.ValueToApiVal(value)
if err != nil {
return nil, nil, err
Expand All @@ -68,7 +69,7 @@ func CreateNQuadAndSchema(value any, gid uint64, jsonName string, t reflect.Type
if valType == pb.Posting_UID {
nquad.ObjectId = fmt.Sprint(value)
u.Directive = pb.SchemaUpdate_REVERSE
} else {
} else if val != nil {
nquad.ObjectValue = val
}

Expand Down
47 changes: 43 additions & 4 deletions api/structreflect/structreflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"fmt"
"reflect"
"strconv"
"time"

"github.com/hypermodeinc/modusdb/api"
"github.com/hypermodeinc/modusdb/api/apiutils"
)

Expand Down Expand Up @@ -43,12 +45,48 @@ func GetFieldTags(t reflect.Type) (*TagMaps, error) {
return tags, nil
}

var skipProcessStructTypes = []reflect.Type{
reflect.TypeOf(api.Point{}),
reflect.TypeOf(api.Polygon{}),
reflect.TypeOf(api.MultiPolygon{}),
reflect.TypeOf(time.Time{}),
}

func IsDgraphType(value any) bool {
valueType := reflect.TypeOf(value)
if valueType.Kind() == reflect.Ptr {
valueType = valueType.Elem()
}
for _, t := range skipProcessStructTypes {
if valueType == t {
return true
}
}
return false
}

func IsStructAndNotDgraphType(field reflect.StructField) bool {
fieldType := field.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
if fieldType.Kind() != reflect.Struct {
return false
}
for _, t := range skipProcessStructTypes {
if fieldType == t {
return false
}
}
return true
}

func CreateDynamicStruct(t reflect.Type, fieldToJson map[string]string, depth int) reflect.Type {
fields := make([]reflect.StructField, 0, len(fieldToJson))
for fieldName, jsonName := range fieldToJson {
field, _ := t.FieldByName(fieldName)
if fieldName != "Gid" {
if field.Type.Kind() == reflect.Struct {
if IsStructAndNotDgraphType(field) {
if depth <= 1 {
tagMaps, _ := GetFieldTags(field.Type)
nestedType := CreateDynamicStruct(field.Type, tagMaps.FieldToJson, depth+1)
Expand All @@ -59,7 +97,7 @@ func CreateDynamicStruct(t reflect.Type, fieldToJson map[string]string, depth in
})
}
} else if field.Type.Kind() == reflect.Ptr &&
field.Type.Elem().Kind() == reflect.Struct {
IsStructAndNotDgraphType(field) {
tagMaps, _ := GetFieldTags(field.Type.Elem())
nestedType := CreateDynamicStruct(field.Type.Elem(), tagMaps.FieldToJson, depth+1)
fields = append(fields, reflect.StructField{
Expand Down Expand Up @@ -132,13 +170,14 @@ func MapDynamicToFinal(dynamic any, final any, isNested bool) (uint64, error) {
} else {
finalField = vFinal.FieldByName(dynamicField.Name)
}
if dynamicFieldType.Kind() == reflect.Struct {
//if dynamicFieldType.Kind() == reflect.Struct {
if IsStructAndNotDgraphType(dynamicField) {
_, err := MapDynamicToFinal(dynamicValue.Addr().Interface(), finalField.Addr().Interface(), true)
if err != nil {
return 0, err
}
} else if dynamicFieldType.Kind() == reflect.Ptr &&
dynamicFieldType.Elem().Kind() == reflect.Struct {
IsStructAndNotDgraphType(dynamicField) {
// if field is a pointer, find if the underlying is a struct
_, err := MapDynamicToFinal(dynamicValue.Interface(), finalField.Interface(), true)
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package api

type Point struct {
Type string `json:"type,omitempty"`
Coordinates []float64 `json:"coordinates,omitempty"`
}

type Polygon struct {
Type string `json:"type,omitempty"`
Coordinates [][][]float64 `json:"coordinates,omitempty"`
}

type MultiPolygon = Polygon

func NewPolygon(coordinates [][]float64) *Polygon {
polygon := &Polygon{
Type: "Polygon",
Coordinates: [][][]float64{coordinates},
}
return polygon
}

func NewMultiPolygon(coordinates [][][]float64) *MultiPolygon {
multiPolygon := &MultiPolygon{
Type: "MultiPolygon",
Coordinates: coordinates,
}
return multiPolygon
}
46 changes: 46 additions & 0 deletions api/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package api

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNewPolygon(t *testing.T) {
coordinates := [][]float64{
{-122.083506, 37.4259518}, // Northwest
{-122.081506, 37.4259518}, // Northeast
{-122.081506, 37.4239518}, // Southeast
{-122.083506, 37.4239518}, // Southwest
{-122.083506, 37.4259518}, // Close the polygon
}

polygon := NewPolygon(coordinates)
require.NotNil(t, polygon)
require.Len(t, polygon.Coordinates, 1)
require.Equal(t, coordinates, polygon.Coordinates[0])
}

func TestNewMultiPolygon(t *testing.T) {
coordinates := [][][]float64{
{
{-122.083506, 37.4259518},
{-122.081506, 37.4259518},
{-122.081506, 37.4239518},
{-122.083506, 37.4239518},
{-122.083506, 37.4259518},
},
{
{-122.073506, 37.4359518},
{-122.071506, 37.4359518},
{-122.071506, 37.4339518},
{-122.073506, 37.4339518},
{-122.073506, 37.4359518},
},
}

multiPolygon := NewMultiPolygon(coordinates)
require.NotNil(t, multiPolygon)
require.Equal(t, "MultiPolygon", multiPolygon.Type)
require.Equal(t, coordinates, multiPolygon.Coordinates)
}
6 changes: 6 additions & 0 deletions api_mutation_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ func generateSetDqlMutationsAndSchema[T any](ctx context.Context, n *Namespace,
}

sch.Preds = append(sch.Preds, u)
// Handle nil object values - only skip geo types with nil values
if nquad.ObjectValue == nil && (strings.Contains(nquad.Predicate, ".multiArea") ||
strings.Contains(nquad.Predicate, ".area") ||
strings.Contains(nquad.Predicate, ".loc")) {
continue
}
nquads = append(nquads, nquad)
}
if !uniqueConstraintFound {
Expand Down
2 changes: 1 addition & 1 deletion api_mutation_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
)

func processStructValue(ctx context.Context, value any, ns *Namespace) (any, error) {
if reflect.TypeOf(value).Kind() == reflect.Struct {
if !structreflect.IsDgraphType(value) && reflect.TypeOf(value).Kind() == reflect.Struct {
value = reflect.ValueOf(value).Interface()
newGid, err := getUidOrMutate(ctx, ns.engine, ns, value)
if err != nil {
Expand Down
Loading