From c7f95b3d32bc1ec50ab73b12dcf58901ab8f5274 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Wed, 26 Feb 2025 17:51:20 -0500 Subject: [PATCH 01/15] Fix incorrect type Add more information in errors --- api/dgraphtypes/dgraphtypes.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/dgraphtypes/dgraphtypes.go b/api/dgraphtypes/dgraphtypes.go index b418f68..0e2f487 100644 --- a/api/dgraphtypes/dgraphtypes.go +++ b/api/dgraphtypes/dgraphtypes.go @@ -75,7 +75,7 @@ func ValueToPosting_ValType(v any) (pb.Posting_ValType, error) { 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) } } @@ -124,7 +124,7 @@ func ValueToApiVal(v any) (*api.Value, error) { if err != nil { return nil, err } - return &api.Value{Val: &api.Value_DateVal{DateVal: bytes}}, nil + return &api.Value{Val: &api.Value_DatetimeVal{DatetimeVal: bytes}}, nil case geom.Point: bytes, err := wkb.Marshal(&val, binary.LittleEndian) if err != nil { @@ -134,7 +134,7 @@ func ValueToApiVal(v any) (*api.Value, error) { 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) } } From 9147f05c887b724d0a372e511a1bae2d91defe2f Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Wed, 26 Feb 2025 17:52:52 -0500 Subject: [PATCH 02/15] Support processing of structs that correspond to Dgraph scalars (time.Time, etc) --- api/structreflect/structreflect.go | 43 +++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/api/structreflect/structreflect.go b/api/structreflect/structreflect.go index 0dc1540..c395c49 100644 --- a/api/structreflect/structreflect.go +++ b/api/structreflect/structreflect.go @@ -9,6 +9,7 @@ import ( "fmt" "reflect" "strconv" + "time" "github.com/hypermodeinc/modusdb/api/apiutils" ) @@ -43,12 +44,45 @@ func GetFieldTags(t reflect.Type) (*TagMaps, error) { return tags, nil } +var skipProcessStructTypes = []reflect.Type{ + reflect.TypeOf(time.Time{}), +} + +func IsDgraphType(value any) bool { + kind := reflect.TypeOf(value).Kind() + if kind == reflect.Ptr { + kind = reflect.TypeOf(value).Elem().Kind() + } + for _, t := range skipProcessStructTypes { + if kind == t.Kind() { + return true + } + } + return false +} + +func IsStructAndNotDgraphType(field reflect.StructField) bool { + kind := field.Type.Kind() + if kind == reflect.Ptr { + kind = field.Type.Elem().Kind() + } + if kind != reflect.Struct { + return false + } + for _, t := range skipProcessStructTypes { + if kind == t.Kind() { + 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) @@ -59,7 +93,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{ @@ -132,13 +166,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 { From 93efd4d012d0053f4921cbf95cd12ed24269e4f8 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Wed, 26 Feb 2025 17:53:33 -0500 Subject: [PATCH 03/15] Do not reflect into known structs (time.Time, etc) --- api_mutation_helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_mutation_helpers.go b/api_mutation_helpers.go index a12d239..da63c56 100644 --- a/api_mutation_helpers.go +++ b/api_mutation_helpers.go @@ -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 { From 887cb3164e8e85efa86972769951e0cd9387b5f4 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Wed, 26 Feb 2025 17:54:11 -0500 Subject: [PATCH 04/15] Add tests for time.Time storage --- unit_test/api_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/unit_test/api_test.go b/unit_test/api_test.go index a404547..053dc88 100644 --- a/unit_test/api_test.go +++ b/unit_test/api_test.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/stretchr/testify/require" @@ -876,3 +877,70 @@ func TestVectorIndexSearchWithQuery(t *testing.T) { require.Equal(t, "fox", docs[3].Text) require.Equal(t, "gorilla", docs[4].Text) } + +type Alltypes struct { + Gid uint64 `json:"gid,omitempty"` + Name string `json:"name,omitempty"` + Age int `json:"age,omitempty"` + Count int64 `json:"count,omitempty"` + Married bool `json:"married,omitempty"` + FloatVal float32 `json:"floatVal,omitempty"` + Float64Val float64 `json:"float64Val,omitempty"` + //Loc geom.Point `json:"loc,omitempty"` + DoB time.Time `json:"dob,omitempty"` +} + +func TestAllSchemaTypes(t *testing.T) { + ctx := context.Background() + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer engine.Close() + + require.NoError(t, engine.DropAll(ctx)) + + //loc := geom.NewPoint(geom.XY).MustSetCoords(geom.Coord{-122.082506, 37.4249518}) + dob := time.Date(1965, 6, 24, 0, 0, 0, 0, time.UTC) + _, omnibus, err := modusdb.Create(context.Background(), engine, Alltypes{ + Name: "John Doe", + Age: 30, + Count: 100, + Married: true, + FloatVal: 3.14159, + Float64Val: 222333444.555666777, + //Loc: *loc, + DoB: dob, + }) + + require.NoError(t, err) + require.NotZero(t, omnibus.Gid) + require.Equal(t, "John Doe", omnibus.Name) + require.Equal(t, 30, omnibus.Age) + require.Equal(t, true, omnibus.Married) + //require.Equal(t, loc, omnibus.Loc) + require.Equal(t, dob, omnibus.DoB) +} + +type JustTime struct { + Name string `json:"name,omitempty" db:"constraint=unique"` + Time time.Time `json:"time,omitempty"` + TimePtr *time.Time `json:"timePtr,omitempty"` +} + +func TestTime(t *testing.T) { + ctx := context.Background() + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer engine.Close() + + d := time.Date(1965, 6, 24, 12, 0, 0, 0, time.UTC) + _, justTime, err := modusdb.Create(ctx, engine, JustTime{ + Name: "John Doe", + Time: d, + TimePtr: &d, + }) + require.NoError(t, err) + + require.Equal(t, "John Doe", justTime.Name) + require.Equal(t, d, justTime.Time) + require.Equal(t, d, *justTime.TimePtr) +} From bf877281be0a2efc6c624a6f8f53e0d0f572f4c4 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:35:09 -0400 Subject: [PATCH 05/15] Add geo types --- api/types.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 api/types.go diff --git a/api/types.go b/api/types.go new file mode 100644 index 0000000..91eab9d --- /dev/null +++ b/api/types.go @@ -0,0 +1,16 @@ +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 struct { + Type string `json:"type,omitempty"` + Coordinates [][][]float64 `json:"coordinates,omitempty"` +} From d49550cbf71b33d96ea994daf66bd0e154c0813e Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:41:30 -0400 Subject: [PATCH 06/15] Only append the nquad if the value is not null --- api_mutation_gen.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api_mutation_gen.go b/api_mutation_gen.go index e7095da..2bd0103 100644 --- a/api_mutation_gen.go +++ b/api_mutation_gen.go @@ -100,7 +100,9 @@ func generateSetDqlMutationsAndSchema[T any](ctx context.Context, n *Namespace, } sch.Preds = append(sch.Preds, u) - nquads = append(nquads, nquad) + if nquad.ObjectValue != nil { + nquads = append(nquads, nquad) + } } if !uniqueConstraintFound { return fmt.Errorf(apiutils.NoUniqueConstr, t.Name()) From ff3384e001e52b7147e5b5a3b0d28fbc3dbf3dfe Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:42:24 -0400 Subject: [PATCH 07/15] Add support for geo encoding over grpc --- api/dgraphtypes/dgraphtypes.go | 37 ++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/api/dgraphtypes/dgraphtypes.go b/api/dgraphtypes/dgraphtypes.go index 0e2f487..a8d7f02 100644 --- a/api/dgraphtypes/dgraphtypes.go +++ b/api/dgraphtypes/dgraphtypes.go @@ -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" ) @@ -70,7 +72,7 @@ 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, modusapi.MultiPolygon: return pb.Posting_GEO, nil case []float32, []float64: return pb.Posting_VFLOAT, nil @@ -79,6 +81,7 @@ func ValueToPosting_ValType(v any) (pb.Posting_ValType, error) { } } +// 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: @@ -125,10 +128,36 @@ func ValueToApiVal(v any) (*api.Value, error) { return nil, err } return &api.Value{Val: &api.Value_DatetimeVal{DatetimeVal: bytes}}, nil - case geom.Point: - bytes, err := wkb.Marshal(&val, binary.LittleEndian) + 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: + // TODO: handle multi-polygon + // TODO: test for empty (nullable polygon) + 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: From 168b77ad70de7b59f77f11db0eec2d9afee24f92 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:43:05 -0400 Subject: [PATCH 08/15] Set the nquad val only if the value is non-null --- api/mutations/mutations.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/mutations/mutations.go b/api/mutations/mutations.go index 92b6d70..2beab62 100644 --- a/api/mutations/mutations.go +++ b/api/mutations/mutations.go @@ -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 @@ -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 } From 5f0aac2b04de4acb90d763bac0bea08675a8f511 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:43:46 -0400 Subject: [PATCH 09/15] Add geom types for Dgraph types --- api/structreflect/structreflect.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/structreflect/structreflect.go b/api/structreflect/structreflect.go index c395c49..d92e926 100644 --- a/api/structreflect/structreflect.go +++ b/api/structreflect/structreflect.go @@ -11,6 +11,7 @@ import ( "strconv" "time" + "github.com/hypermodeinc/modusdb/api" "github.com/hypermodeinc/modusdb/api/apiutils" ) @@ -45,6 +46,9 @@ func GetFieldTags(t reflect.Type) (*TagMaps, error) { } var skipProcessStructTypes = []reflect.Type{ + reflect.TypeOf(api.Point{}), + reflect.TypeOf(api.Polygon{}), + reflect.TypeOf(api.MultiPolygon{}), reflect.TypeOf(time.Time{}), } From 46fcb676bec2b4c49f34795603a1f77b43bc3498 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Fri, 14 Mar 2025 17:44:07 -0400 Subject: [PATCH 10/15] Add geo tests --- unit_test/api_test.go | 95 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/unit_test/api_test.go b/unit_test/api_test.go index 053dc88..b8859c7 100644 --- a/unit_test/api_test.go +++ b/unit_test/api_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/hypermodeinc/modusdb" + "github.com/hypermodeinc/modusdb/api" "github.com/hypermodeinc/modusdb/api/apiutils" ) @@ -906,7 +907,7 @@ func TestAllSchemaTypes(t *testing.T) { Count: 100, Married: true, FloatVal: 3.14159, - Float64Val: 222333444.555666777, + Float64Val: 123.456789, //Loc: *loc, DoB: dob, }) @@ -916,11 +917,14 @@ func TestAllSchemaTypes(t *testing.T) { require.Equal(t, "John Doe", omnibus.Name) require.Equal(t, 30, omnibus.Age) require.Equal(t, true, omnibus.Married) + require.Equal(t, int64(100), omnibus.Count) + require.Equal(t, float32(3.14159), omnibus.FloatVal) + require.InDelta(t, 123.456789, omnibus.Float64Val, 0.000001) //require.Equal(t, loc, omnibus.Loc) require.Equal(t, dob, omnibus.DoB) } -type JustTime struct { +type TimeStruct struct { Name string `json:"name,omitempty" db:"constraint=unique"` Time time.Time `json:"time,omitempty"` TimePtr *time.Time `json:"timePtr,omitempty"` @@ -933,7 +937,7 @@ func TestTime(t *testing.T) { defer engine.Close() d := time.Date(1965, 6, 24, 12, 0, 0, 0, time.UTC) - _, justTime, err := modusdb.Create(ctx, engine, JustTime{ + gid, justTime, err := modusdb.Create(ctx, engine, TimeStruct{ Name: "John Doe", Time: d, TimePtr: &d, @@ -943,4 +947,89 @@ func TestTime(t *testing.T) { require.Equal(t, "John Doe", justTime.Name) require.Equal(t, d, justTime.Time) require.Equal(t, d, *justTime.TimePtr) + + _, justTime, err = modusdb.Get[TimeStruct](ctx, engine, gid) + require.NoError(t, err) + require.Equal(t, "John Doe", justTime.Name) + require.Equal(t, d, justTime.Time) + require.Equal(t, d, *justTime.TimePtr) + + // Add another time entry + d2 := time.Date(1965, 6, 24, 11, 59, 59, 0, time.UTC) + _, _, err = modusdb.Create(ctx, engine, TimeStruct{ + Name: "Jane Doe", + Time: d2, + TimePtr: &d2, + }) + require.NoError(t, err) + + _, entries, err := modusdb.Query[TimeStruct](ctx, engine, modusdb.QueryParams{ + Filter: &modusdb.Filter{ + Field: "time", + String: modusdb.StringPredicate{ + // TODO: Not too crazy about this. Thinking we should add XXXPredicate definitions for all scalars -MM + GreaterOrEqual: fmt.Sprintf("\"%s\"", d.Format(time.RFC3339)), + }, + }, + }) + require.NoError(t, err) + require.Len(t, entries, 1) + require.Equal(t, "John Doe", entries[0].Name) + require.Equal(t, d, entries[0].Time) + require.Equal(t, d, *entries[0].TimePtr) +} + +type GeomStruct struct { + Name string `json:"name,omitempty" db:"constraint=unique"` + Point api.Point `json:"loc,omitempty"` + Area api.Polygon `json:"area,omitempty"` +} + +func TestGeom(t *testing.T) { + ctx := context.Background() + //engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig("./foo")) + require.NoError(t, err) + defer engine.Close() + + loc := api.Point{ + Coordinates: []float64{-122.082506, 37.4249518}, + } + gid, geomStruct, err := modusdb.Create(ctx, engine, GeomStruct{ + Name: "John Doe", + Point: loc, + }) + require.NoError(t, err) + require.Equal(t, "John Doe", geomStruct.Name) + require.Equal(t, loc.Coordinates, geomStruct.Point.Coordinates) + + _, geomStruct, err = modusdb.Get[GeomStruct](ctx, engine, gid) + require.NoError(t, err) + require.Equal(t, "John Doe", geomStruct.Name) + require.Equal(t, loc.Coordinates, geomStruct.Point.Coordinates) + + area := api.Polygon{ + 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 by repeating first point + }, + }, + } + gid, geomStruct, err = modusdb.Create(ctx, engine, GeomStruct{ + Name: "Jane Doe", + Point: loc, + Area: area, + }) + require.NoError(t, err) + require.Equal(t, "Jane Doe", geomStruct.Name) + require.Equal(t, area.Coordinates, geomStruct.Area.Coordinates) + + _, geomStruct, err = modusdb.Get[GeomStruct](ctx, engine, gid) + require.NoError(t, err) + require.Equal(t, "Jane Doe", geomStruct.Name) + require.Equal(t, area.Coordinates, geomStruct.Area.Coordinates) } From 0f899decc6282845b1d8ef769805bd64d09251e8 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Mon, 17 Mar 2025 16:15:44 -0400 Subject: [PATCH 11/15] Alias identical structs; add helpers Add tests --- api/types.go | 19 ++++++++++++++++--- api/types_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 api/types_test.go diff --git a/api/types.go b/api/types.go index 91eab9d..e524381 100644 --- a/api/types.go +++ b/api/types.go @@ -10,7 +10,20 @@ type Polygon struct { Coordinates [][][]float64 `json:"coordinates,omitempty"` } -type MultiPolygon 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 } diff --git a/api/types_test.go b/api/types_test.go new file mode 100644 index 0000000..dd229c7 --- /dev/null +++ b/api/types_test.go @@ -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) +} From 5a9b9c21829cfe2a8654e80271aefb1a7db4384e Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Mon, 17 Mar 2025 16:16:16 -0400 Subject: [PATCH 12/15] Handle empty polygons --- api/dgraphtypes/dgraphtypes.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/dgraphtypes/dgraphtypes.go b/api/dgraphtypes/dgraphtypes.go index a8d7f02..b17b75b 100644 --- a/api/dgraphtypes/dgraphtypes.go +++ b/api/dgraphtypes/dgraphtypes.go @@ -72,7 +72,7 @@ func ValueToPosting_ValType(v any) (pb.Posting_ValType, error) { return pb.Posting_BINARY, nil case time.Time: return pb.Posting_DATETIME, nil - case modusapi.Point, modusapi.Polygon, modusapi.MultiPolygon: + case modusapi.Point, modusapi.Polygon: return pb.Posting_GEO, nil case []float32, []float64: return pb.Posting_VFLOAT, nil @@ -142,8 +142,9 @@ func ValueToApiVal(v any) (*api.Value, error) { } return &api.Value{Val: &api.Value_GeoVal{GeoVal: bytes}}, nil case modusapi.Polygon: - // TODO: handle multi-polygon - // TODO: test for empty (nullable 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)) From 972a16521e18aee132f284d9da8f81daecec6026 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Mon, 17 Mar 2025 16:17:22 -0400 Subject: [PATCH 13/15] Add additional tests --- unit_test/api_test.go | 110 +++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/unit_test/api_test.go b/unit_test/api_test.go index b8859c7..e032a0e 100644 --- a/unit_test/api_test.go +++ b/unit_test/api_test.go @@ -980,15 +980,17 @@ func TestTime(t *testing.T) { } type GeomStruct struct { - Name string `json:"name,omitempty" db:"constraint=unique"` - Point api.Point `json:"loc,omitempty"` - Area api.Polygon `json:"area,omitempty"` + Gid uint64 `json:"gid,omitempty"` + Name string `json:"name,omitempty" db:"constraint=unique"` + Point api.Point `json:"loc,omitempty"` + Area api.Polygon `json:"area,omitempty"` + MultiArea api.MultiPolygon `json:"multiArea,omitempty"` } -func TestGeom(t *testing.T) { +func TestPoint(t *testing.T) { ctx := context.Background() - //engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) - engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig("./foo")) + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + //engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig("./foo")) require.NoError(t, err) defer engine.Close() @@ -1008,28 +1010,88 @@ func TestGeom(t *testing.T) { require.Equal(t, "John Doe", geomStruct.Name) require.Equal(t, loc.Coordinates, geomStruct.Point.Coordinates) - area := api.Polygon{ - 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 by repeating first point - }, - }, - } - gid, geomStruct, err = modusdb.Create(ctx, engine, GeomStruct{ - Name: "Jane Doe", - Point: loc, - Area: area, + query := ` + { + geomStruct(func: type(GeomStruct)) { + GeomStruct.name + } + }` + resp, err := engine.GetDefaultNamespace().Query(ctx, query) + require.NoError(t, err) + require.JSONEq(t, `{ + "geomStruct":[ + {"GeomStruct.name":"John Doe"} + ] + }`, string(resp.GetJson())) +} + +func TestPolygon(t *testing.T) { + ctx := context.Background() + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer engine.Close() + + polygon := api.NewPolygon([][]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 by repeating first point + }) + _, geomStruct, err := modusdb.Create(ctx, engine, GeomStruct{ + Name: "Jane Doe", + Area: *polygon, }) require.NoError(t, err) require.Equal(t, "Jane Doe", geomStruct.Name) - require.Equal(t, area.Coordinates, geomStruct.Area.Coordinates) + require.Equal(t, polygon.Coordinates, geomStruct.Area.Coordinates) +} - _, geomStruct, err = modusdb.Get[GeomStruct](ctx, engine, gid) +func TestMultiPolygon(t *testing.T) { + ctx := context.Background() + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer engine.Close() + + multiPolygon := api.NewMultiPolygon([][][]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 by repeating first point + }, + { + {-122.073506, 37.4359518}, // Northwest + {-122.071506, 37.4359518}, // Northeast + {-122.071506, 37.4339518}, // Southeast + {-122.073506, 37.4339518}, // Southwest + {-122.073506, 37.4359518}, // Close the polygon by repeating first point + }, + }) + _, geomStruct, err := modusdb.Create(ctx, engine, GeomStruct{ + Name: "Jane Doe", + MultiArea: *multiPolygon, + }) require.NoError(t, err) require.Equal(t, "Jane Doe", geomStruct.Name) - require.Equal(t, area.Coordinates, geomStruct.Area.Coordinates) + require.Equal(t, multiPolygon.Coordinates, geomStruct.MultiArea.Coordinates) +} + +func TestUserStore(t *testing.T) { + ctx := context.Background() + //engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig(t.TempDir())) + engine, err := modusdb.NewEngine(modusdb.NewDefaultConfig("./foo")) + require.NoError(t, err) + defer engine.Close() + + user := User{ + Name: "John Doe", + Age: 30, + } + gid, user, err := modusdb.Create(ctx, engine, user) + require.NoError(t, err) + require.NotZero(t, gid) + require.Equal(t, "John Doe", user.Name) + require.Equal(t, 30, user.Age) } From 8a8672a4e14735eaceddc78bd81bbcd34c1baa93 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Sun, 27 Apr 2025 15:42:19 -0400 Subject: [PATCH 14/15] Fix examination of types --- api/structreflect/structreflect.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/api/structreflect/structreflect.go b/api/structreflect/structreflect.go index d92e926..1134d3f 100644 --- a/api/structreflect/structreflect.go +++ b/api/structreflect/structreflect.go @@ -53,12 +53,12 @@ var skipProcessStructTypes = []reflect.Type{ } func IsDgraphType(value any) bool { - kind := reflect.TypeOf(value).Kind() - if kind == reflect.Ptr { - kind = reflect.TypeOf(value).Elem().Kind() + valueType := reflect.TypeOf(value) + if valueType.Kind() == reflect.Ptr { + valueType = valueType.Elem() } for _, t := range skipProcessStructTypes { - if kind == t.Kind() { + if valueType == t { return true } } @@ -66,15 +66,15 @@ func IsDgraphType(value any) bool { } func IsStructAndNotDgraphType(field reflect.StructField) bool { - kind := field.Type.Kind() - if kind == reflect.Ptr { - kind = field.Type.Elem().Kind() + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() } - if kind != reflect.Struct { + if fieldType.Kind() != reflect.Struct { return false } for _, t := range skipProcessStructTypes { - if kind == t.Kind() { + if fieldType == t { return false } } From 77f435bfc4118516364dedc72b0b642845a327f8 Mon Sep 17 00:00:00 2001 From: Matthew McNeely Date: Sun, 27 Apr 2025 15:45:51 -0400 Subject: [PATCH 15/15] Add test for geo types --- api_mutation_gen.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api_mutation_gen.go b/api_mutation_gen.go index 2bd0103..7f2e63c 100644 --- a/api_mutation_gen.go +++ b/api_mutation_gen.go @@ -100,9 +100,13 @@ func generateSetDqlMutationsAndSchema[T any](ctx context.Context, n *Namespace, } sch.Preds = append(sch.Preds, u) - if nquad.ObjectValue != nil { - nquads = append(nquads, nquad) + // 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 { return fmt.Errorf(apiutils.NoUniqueConstr, t.Name())