diff --git a/api.go b/api.go new file mode 100644 index 0000000..ba4751a --- /dev/null +++ b/api.go @@ -0,0 +1,178 @@ +package modusdb + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/dgraph-io/dgo/v240/protos/api" + "github.com/dgraph-io/dgraph/v24/dql" + "github.com/dgraph-io/dgraph/v24/protos/pb" + "github.com/dgraph-io/dgraph/v24/query" + "github.com/dgraph-io/dgraph/v24/worker" + "github.com/dgraph-io/dgraph/v24/x" +) + +type UniqueField interface{ + uint64 | ConstrainedField +} +type ConstrainedField struct { + key string + value any +} + +func getFieldTags(t reflect.Type) (jsonTags map[string]string, reverseEdgeTags map[string]string, err error) { + jsonTags = make(map[string]string) + reverseEdgeTags = make(map[string]string) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + return nil, nil, fmt.Errorf("field %s has no json tag", field.Name) + } + jsonName := strings.Split(jsonTag, ",")[0] + jsonTags[field.Name] = jsonName + reverseEdgeTag := field.Tag.Get("readFrom") + if reverseEdgeTag != "" { + typeAndField := strings.Split(reverseEdgeTag, ",") + if len(typeAndField) != 2 { + return nil, nil, fmt.Errorf("field %s has invalid readFrom tag, expected format is type=,field=", field.Name) + } + t := strings.Split(typeAndField[0], "=")[1] + f := strings.Split(typeAndField[1], "=")[1] + reverseEdgeTags[field.Name] = getPredicateName(t, f) + } + } + return jsonTags, reverseEdgeTags, nil +} + +func getFieldValues(object any, jsonFields map[string]string) map[string]any { + values := make(map[string]any) + v := reflect.ValueOf(object).Elem() + for fieldName, jsonName := range jsonFields { + fieldValue := v.FieldByName(fieldName) + values[jsonName] = fieldValue.Interface() + + } + return values +} + +func getPredicateName(typeName, fieldName string) string { + return fmt.Sprint(typeName, ".", fieldName) +} + +func valueToValType(v any) *api.Value { + switch val := v.(type) { + case string: + return &api.Value{Val: &api.Value_StrVal{StrVal: val}} + case int: + return &api.Value{Val: &api.Value_IntVal{IntVal: int64(val)}} + case int64: + return &api.Value{Val: &api.Value_IntVal{IntVal: val}} + case uint64: + return &api.Value{Val: &api.Value_IntVal{IntVal: int64(val)}} + case bool: + return &api.Value{Val: &api.Value_BoolVal{BoolVal: val}} + case float64: + return &api.Value{Val: &api.Value_DoubleVal{DoubleVal: val}} + default: + return &api.Value{Val: &api.Value_DefaultVal{DefaultVal: fmt.Sprint(v)}} + } +} + +func Create[T any](ctx context.Context, n *Namespace, object *T) (uint64, *T, error){ + uids, err := n.db.z.nextUIDs(&pb.Num{Val: uint64(1), Type: pb.Num_UID}) + if err != nil { + return 0, object, err + } + + t := reflect.TypeOf(*object) + if t.Kind() != reflect.Struct { + return 0, object, fmt.Errorf("expected struct, got %s", t.Kind()) + } + + jsonFields, _, err := getFieldTags(t) + if err != nil { + return 0, object, err + } + values := getFieldValues(object, jsonFields) + + + + nquads := make([]*api.NQuad, 0) + for jsonName, value := range values { + if jsonName == "uid" { + continue + } + nquad := &api.NQuad{ + Namespace: n.ID(), + Subject: fmt.Sprint(uids.StartId), + Predicate: getPredicateName(t.Name(), jsonName), + ObjectValue: valueToValType(value), + } + nquads = append(nquads, nquad) + } + + dms := make([]*dql.Mutation, 0) + dms = append(dms, &dql.Mutation{ + Set: nquads, + }) + edges, err := query.ToDirectedEdges(dms, nil) + if err != nil { + return 0, object, err + } + ctx = x.AttachNamespace(ctx, n.ID()) + + n.db.mutex.Lock() + defer n.db.mutex.Unlock() + + if !n.db.isOpen { + return 0, object, ErrClosedDB + } + + startTs, err := n.db.z.nextTs() + if err != nil { + return 0, object, err + } + commitTs, err := n.db.z.nextTs() + if err != nil { + return 0, object, err + } + + m := &pb.Mutations{ + GroupId: 1, + StartTs: startTs, + Edges: edges, + } + m.Edges, err = query.ExpandEdges(ctx, m) + if err != nil { + return 0, object, fmt.Errorf("error expanding edges: %w", err) + } + + for _, edge := range m.Edges { + worker.InitTablet(edge.Attr) + } + + p := &pb.Proposal{Mutations: m, StartTs: startTs} + if err := worker.ApplyMutations(ctx, p); err != nil { + return 0, object, err + } + + err = worker.ApplyCommited(ctx, &pb.OracleDelta{ + Txns: []*pb.TxnStatus{{StartTs: startTs, CommitTs: commitTs}}, + }) + if err != nil { + return 0, object, err + } + + v := reflect.ValueOf(object).Elem() + + uidField := v.FieldByName("Uid") + + if uidField.IsValid() && uidField.CanSet() && uidField.Kind() == reflect.Uint64 { + uidField.SetUint(uids.StartId) + } + + return uids.StartId, object, nil +} diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..001480f --- /dev/null +++ b/api_test.go @@ -0,0 +1,69 @@ +package modusdb_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/hypermodeinc/modusdb" +) + +type User struct{ + Uid uint64 `json:"uid"` + Name string `json:"name"` + Age int `json:"age"` +} + +func TestCreateApi(t *testing.T) { + db, err := modusdb.New(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer db.Close() + + db1, err := db.CreateNamespace() + require.NoError(t, err) + + require.NoError(t, db1.DropData(context.Background())) + + user := &User{ + Name: "B", + Age: 20, + } + + uid, _, err := modusdb.Create(context.Background(), db1, user) + require.NoError(t, err) + + require.Equal(t, "B", user.Name) + require.Equal(t, uint64(2), uid) + require.Equal(t, uint64(2), user.Uid) + + query := `{ + me(func: has(User.name)) { + uid + User.name + User.age + } + }` + resp, err := db1.Query(context.Background(), query) + require.NoError(t, err) + require.JSONEq(t, `{"me":[{"uid":"0x2","User.name":"B","User.age":20}]}`, string(resp.GetJson())) +} + +func TestCreateApiWithNonStruct(t *testing.T) { + db, err := modusdb.New(modusdb.NewDefaultConfig(t.TempDir())) + require.NoError(t, err) + defer db.Close() + + db1, err := db.CreateNamespace() + require.NoError(t, err) + + require.NoError(t, db1.DropData(context.Background())) + + user := &User{ + Name: "B", + Age: 20, + } + + _, _, err = modusdb.Create[*User](context.Background(), db1, &user) + require.Error(t, err) +} \ No newline at end of file diff --git a/go.mod b/go.mod index 7253865..291d7c4 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/dgraph-io/ristretto/v2 v2.0.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.10.0 - golang.org/x/sync v0.9.0 + golang.org/x/sync v0.10.0 ) require ( @@ -124,9 +124,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.6.0 // indirect diff --git a/go.sum b/go.sum index 46296ee..152dd35 100644 --- a/go.sum +++ b/go.sum @@ -697,8 +697,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -787,8 +787,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -840,8 +840,8 @@ golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=