Skip to content

Commit

Permalink
feat: Allow props to be injected in MATCH and MERGE clauses
Browse files Browse the repository at this point in the history
  • Loading branch information
rlch committed Sep 15, 2023
1 parent 71d3883 commit 37bd5ab
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 34 deletions.
8 changes: 4 additions & 4 deletions internal/cypher.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ func (cy *cypher) writeNode(m *member) {
cy.WriteString(string(m.variable.PropsExpr))
}
}
if m.props != "" {
if m.propsParam != "" {
resolvedProps++
if padProps {
cy.WriteRune(' ')
}
cy.WriteString(m.props)
cy.WriteString(m.propsParam)
}
if resolvedProps > 1 {
panic(errUnresolvedProps)
Expand Down Expand Up @@ -172,9 +172,9 @@ func (cy *cypher) writeRelationship(m *member, rs *relationshipPattern) {
inner = inner + " " + string(m.variable.PropsExpr)
}
}
if m.props != "" {
if m.propsParam != "" {
resolvedProps++
inner = inner + " " + m.props
inner = inner + " " + m.propsParam
}
if resolvedProps > 1 {
panic(errUnresolvedProps)
Expand Down
64 changes: 52 additions & 12 deletions internal/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ type (
expr string
// alias is the qualified name of the variable
alias string
// The name of the property in the cypher query
props string
// The name of the properties as a parameter in the cypher query
propsParam string

variable *Variable

Expand Down Expand Up @@ -82,7 +82,7 @@ func (m *member) Print() {
variable: %+v,
where: %+v,
projection: %+v,
}`+"\n", m.identifier, m.isNew, m.expr, m.alias, m.props, m.variable, m.where, m.projectionBody)
}`+"\n", m.identifier, m.isNew, m.expr, m.alias, m.propsParam, m.variable, m.where, m.projectionBody)
}

func (s *Scope) clone() *Scope {
Expand Down Expand Up @@ -275,15 +275,14 @@ func (s *Scope) bindFields(strct reflect.Value, memberName string) {
vf := strct.Field(i)
vfT := vsT.Field(i)

jsTag, ok := vsT.Field(i).Tag.Lookup("json")
accessor, ok := extractJsonFieldName(vsT.Field(i))
if !ok {
// Recurse into composite fields
if vfT.Anonymous {
s.bindFields(vf, memberName)
}
continue
}
accessor := strings.Split(jsTag, ",")[0]
ptr := uintptr(vf.Addr().UnsafePointer())
f := field{
name: accessor,
Expand Down Expand Up @@ -440,8 +439,8 @@ func (s *Scope) register(value any, lookup bool, isNode *bool) *member {
if m.alias != "" {
panic(fmt.Errorf("%w: alias %s already bound to expression %s", ErrAliasAlreadyBound, m.alias, m.expr))
}
switch inner.Kind() {
case reflect.Struct, reflect.Slice:

injectParams := func() {
effProp := v
effName := m.alias
if effName == "" {
Expand All @@ -454,15 +453,56 @@ func (s *Scope) register(value any, lookup bool, isNode *bool) *member {
}
param := s.addParameter(effProp, effName)
if canHaveProps {
m.props = param
m.propsParam = param
} else {
m.alias = m.expr
m.expr = param
}
case reflect.Map:
param := s.addParameter(v, m.expr)
m.alias = m.expr
m.expr = param
}
switch inner.Kind() {
case reflect.Struct:
if inner.CanInterface() {
// Special case, we don't inject the fields of a Param.
if _, ok := inner.Interface().(Param); ok {
injectParams()
break
}
}

// Instead of injecting struct as parameter, inject its fields as
// qualified parameters. This allows props to be used in MATCH and MERGE
// clause for instance, where a property expression is not allowed.
props := make(Props)
innerT := inner.Type()
for i := 0; i < innerT.NumField(); i++ {
f := inner.Field(i)
if !f.IsValid() || !f.CanInterface() || f.IsZero() {
continue
}
fT := innerT.Field(i)
name, ok := extractJsonFieldName(fT)
if !ok {
continue
}
propName := name
if m.expr != "" {
propName = m.expr + "_" + name
}

prop := f.Interface()
props[name] = Param{
Name: propName,
Value: &prop,
}
}
if len(props) > 0 {
if m.variable == nil {
m.variable = &Variable{}
}
m.variable.Props = props
}
case reflect.Slice, reflect.Map:
injectParams()
}
}
return m
Expand Down
8 changes: 8 additions & 0 deletions internal/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,11 @@ func extractNeo4JName(instance any, fields ...string) ([]string, error) {
}
return tags, nil
}

func extractJsonFieldName(field reflect.StructField) (string, bool) {
jsTag, ok := field.Tag.Lookup("json")
if !ok {
return "", false
}
return strings.Split(jsTag, ",")[0], true
}
32 changes: 16 additions & 16 deletions internal/tests/call_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,9 @@ func TestCallSubquery(t *testing.T) {

t.Run("Variable collisions are avoided", func(t *testing.T) {
c := internal.NewCypherClient()
v1 := db.Var([]int{1, 2, 3})
v2 := db.Var([]int{4, 5, 6})
v3 := db.Var([]int{7, 8, 9})
v1 := []int{1, 2, 3}
v2 := []int{4, 5, 6}
v3 := []int{7, 8, 9}
cy, err := c.
With(&v1).
Subquery(func(c *internal.CypherClient) *internal.CypherRunner {
Expand All @@ -420,27 +420,27 @@ func TestCallSubquery(t *testing.T) {

Check(t, cy, err, internal.CompiledCypher{
Cypher: `
WITH $ptr AS ptr
WITH $slice AS slice
CALL {
WITH ptr
RETURN $ptr1 AS ptr1
WITH slice
RETURN $slice1 AS slice1
}
WITH ptr, ptr1
WITH slice, slice1
CALL {
WITH ptr, ptr1
RETURN $ptr2 AS ptr2
WITH slice, slice1
RETURN $slice2 AS slice2
}
RETURN ptr, ptr1, ptr2
RETURN slice, slice1, slice2
`,
Bindings: map[string]reflect.Value{
"ptr": reflect.ValueOf(&v1),
"ptr1": reflect.ValueOf(&v2),
"ptr2": reflect.ValueOf(&v3),
"slice": reflect.ValueOf(&v1),
"slice1": reflect.ValueOf(&v2),
"slice2": reflect.ValueOf(&v3),
},
Parameters: map[string]any{
"ptr": &v1,
"ptr1": &v2,
"ptr2": &v3,
"slice": &v1,
"slice1": &v2,
"slice2": &v3,
},
})
})
Expand Down
5 changes: 3 additions & 2 deletions internal/tests/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,12 @@ func TestCreate(t *testing.T) {

Check(t, cy, err, internal.CompiledCypher{
Cypher: `
CREATE (n:Person $n)
CREATE (n:Person {name: $n_name, position: $n_position})
RETURN n
`,
Parameters: map[string]any{
"n": &n,
"n_name": n.Name,
"n_position": n.Position,
},
Bindings: map[string]reflect.Value{
"n": reflect.ValueOf(&n),
Expand Down

0 comments on commit 37bd5ab

Please sign in to comment.