Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add create api #26

Closed
wants to merge 3 commits into from
Closed
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
210 changes: 210 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
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/schema"
"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

Check failure on line 23 in api.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

field `value` is unused (unused)

Check failure on line 23 in api.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

field `value` is unused (unused)
}

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=<type>,field=<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 addNamespace(ns uint64, pred string) string {
return x.NamespaceAttr(ns, pred)
}

func valueToPosting_ValType(v any) pb.Posting_ValType {
switch v.(type) {
case string:
return pb.Posting_STRING
case int:
return pb.Posting_INT
case int64:
return pb.Posting_INT
case uint64:
return pb.Posting_INT
case bool:
return pb.Posting_BOOL
case float64:
return pb.Posting_FLOAT
default:
return pb.Posting_DEFAULT
}
}

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)}}

Check failure on line 98 in api.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

G115: integer overflow conversion uint64 -> int64 (gosec)

Check failure on line 98 in api.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

G115: integer overflow conversion uint64 -> int64 (gosec)
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)
sch := &schema.ParsedSchema{}

nquads := make([]*api.NQuad, 0)
for jsonName, value := range values {
if jsonName == "uid" {
continue
}
sch.Preds = append(sch.Preds, &pb.SchemaUpdate{
Predicate: addNamespace(n.id, getPredicateName(t.Name(), jsonName)),
ValueType: valueToPosting_ValType(value),
})
nquad := &api.NQuad{
Namespace: n.ID(),
Subject: fmt.Sprint(uids.StartId),
Predicate: getPredicateName(t.Name(), jsonName),
ObjectValue: valueToValType(value),
}
nquads = append(nquads, nquad)
}
sch.Types = append(sch.Types, &pb.TypeUpdate{
TypeName: addNamespace(n.id,t.Name()),
Fields: sch.Preds,
})

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()

err = n.alterSchemaWithParsed(ctx, sch)
if err != nil {
return 0, object, err
}

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)
}

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
}
77 changes: 77 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package modusdb_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/hypermodeinc/modusdb"
)

type User struct{
Uid uint64 `json:"uid,omitempty"`
Name string `json:"name,omitempty"`
Age int `json:"age,omitempty"`
}

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()))

// TODO schema{} should work
resp, err = db1.Query(context.Background(), `schema(pred: [User.name, User.age]) {type}`)
require.NoError(t, err)

require.JSONEq(t,
`{"schema":[{"predicate":"User.age","type":"int"},{"predicate":"User.name","type":"string"}]}`,
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)
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
23 changes: 23 additions & 0 deletions namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@
if err != nil {
return fmt.Errorf("error parsing schema: %w", err)
}
return n.alterSchemaWithParsed(ctx, sc)
for _, pred := range sc.Preds {

Check failure on line 65 in namespace.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

unreachable: unreachable code (govet)

Check failure on line 65 in namespace.go

View workflow job for this annotation

GitHub Actions / ci-go-tests

unreachable: unreachable code (govet)
worker.InitTablet(pred.Predicate)
}

startTs, err := n.db.z.nextTs()
if err != nil {
return err
}

p := &pb.Proposal{Mutations: &pb.Mutations{
GroupId: 1,
StartTs: startTs,
Schema: sc.Preds,
Types: sc.Types,
}}
if err := worker.ApplyMutations(ctx, p); err != nil {
return fmt.Errorf("error applying mutation: %w", err)
}
return nil
}

func (n *Namespace) alterSchemaWithParsed(ctx context.Context, sc *schema.ParsedSchema) error {
for _, pred := range sc.Preds {
worker.InitTablet(pred.Predicate)
}
Expand Down
Loading