Skip to content

Instantly share code, notes, and snippets.

@bnayae
Last active February 2, 2020 13:44
Show Gist options
  • Select an option

  • Save bnayae/df7c6a597c5e15b0f83426fc1e9fab26 to your computer and use it in GitHub Desktop.

Select an option

Save bnayae/df7c6a597c5e15b0f83426fc1e9fab26 to your computer and use it in GitHub Desktop.
Cypher Builder: Simple Statement

Option 1

.Pattern<Foo, Bar>((n , r) => n("n", "label", exclude: f => f.Name)) - r["r","type", convention: x => x != nameof(Bar.Date)] > n("n1")
.Pattern<Foo, Bar>((n , r) => n("n", "label", f => f.Name, f => f.Date)) - r["r","type", nameof(Foo.Date), nameof(Foo.Id)] > n("n1")

Option 2

.Pattern<Foo, Bar>((n , r) => n(n1 => n1.label, exclude: f => f.Name)) - r[r1 => r1.type, convention: x => x != nameof(Bar.Date)] > n("n1")

Option 3

Func<Foo, dynamics> exclude = f => f.Name;
var props = P.All<Foo>(exclude);
var relProps = {nameof(Foo.Date), nameof(Foo.Id)};
var n1 = C.Pattern("n", "label", props));
var r = C.Pattern.All((_, r) =>  r["r","type", relProps]);
var n2 = C.Pattern.Convention((n, _) => n(x2 => xs != "Date"));
var pattern = n1 - r > n2;

Option 4

Func<Foo, dynamics> exclude = f => f.Name;
Func<Foo, bool> convention = n => n != nameof(f.Date);
var props = P.All<Foo>(exclude);
var relProps = {nameof(Foo.Date), nameof(Foo.Id)};
var n1 = C.Pattern.Node("n", "Label").Exac(props);
var r = C.Pattern.Relation<Bar>["r"].All(n => n != nameof(Bar.Age));
var n2 = C.Pattern.Node<Foo>("n").Convention(convention);
var pattern = n1 - r > n2;

Option 5

Func<Foo, dynamics> exclude = f => f.Name;
Func<Foo, bool> convention = n => n != nameof(f.Date);
var props = P.All<Foo>(exclude);
var relProps = {nameof(Foo.Date), nameof(Foo.Id)};
var n1 = C.Node("n", "Label").Exac(props);
var r = C.Relation<Bar>["r"].All(n => n != nameof(Bar.Age));
var n2 = C.Node<Foo>("n").Convention(convention);
var pattern = n1 - r > n2;
@AviAvni
Copy link

AviAvni commented Jan 28, 2020

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using static test.Pattern;
using static test.Schema;

namespace test
{
    class DisposeableAction : IDisposable
    {
        Action _action;

        public DisposeableAction(Action action)
        {
            _action = action;
        }

        public void Dispose()
        {
            _action();
        }
    }

    class ContextValue<T>
    {
        private Stack<T> values = new Stack<T>();

        public T Value
        {
            get => values.Peek();
        }

        public ContextValue(T defaultValue)
        {
            values.Push(defaultValue);
        }

        public IDisposable Set(T value)
        {
            values.Push(value);
            return new DisposeableAction(() => values.Pop());
        }
    }
    class CypherVisitor : ExpressionVisitor
    {
        public StringBuilder Query { get; } = new StringBuilder();
        public Dictionary<string, object> Parameters { get; } = new Dictionary<string, object>();
        private ContextValue<bool> isProperties = new ContextValue<bool>(false);
        private Dictionary<int, ContextValue<Expression>> expression = new Dictionary<int, ContextValue<Expression>>()
        {
            [0] = new ContextValue<Expression>(null),
            [1] = new ContextValue<Expression>(null),
        };

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            Visit(node.Body);
            return node;
        }

        protected override Expression VisitBinary(BinaryExpression node)
        {
            Visit(node.Left);
            switch (node.NodeType)
            {
                case ExpressionType.GreaterThan:
                    Query.Append("->");
                    break;
                case ExpressionType.LessThan:
                    Query.Append("<-");
                    break;
                case ExpressionType.Subtract:
                    Query.Append("-");
                    break;
                case ExpressionType.Equal:
                    Query.Append(" = ");
                    break;
            }
            Visit(node.Right);
            return node;
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            using var _ = isProperties.Set(isProperties.Value || node.Method.ReturnType == typeof(IProperties));

            var attributes = node.Method.GetCustomAttributes(typeof(CypherAttribute), false);
            var format = attributes.Length > 0 ? (attributes[0] as CypherAttribute)?.Format : null;
            if (format != null)
            {
                IDisposable disp = null;
                for (int i = 0; i < format.Length; i++)
                {
                    switch (format[i])
                    {
                        case '$':
                            Visit(node.Arguments[int.Parse(format[++i].ToString())]);
                            break;
                        case '!':
                            Query.Append(node.Method.GetGenericArguments()[int.Parse(format[++i].ToString())].Name);
                            break;
                        case '+':
                            disp = expression[int.Parse(format[++i].ToString())].Set(node.Arguments[int.Parse(format[++i].ToString())]);
                            break;
                        case '.':
                            disp = expression[int.Parse(format[++i].ToString())].Set(node);
                            break;
                        default:
                            Query.Append(format[i]);
                            break;
                    }
                }
                disp?.Dispose();
            }
            else if (node.Method.Name == nameof(Pattern.All))
            {
                var properties = node.Method.GetGenericArguments()[0].GetProperties();
                foreach (var item in properties)
                {
                    Visit(node.Arguments[0]);
                    Query.Append(".");
                    Query.Append(item.Name);
                    if (item != properties.Last())
                        Query.Append(", ");
                }
            }
            else if (node.Method.Name == nameof(Pattern.Convention))
            {
                var filter = (node.Arguments[0] as Expression<Func<string, bool>>).Compile();
                var properties = (expression[1].Value as MethodCallExpression).Method.GetGenericArguments()[0].GetProperties().Where(p => filter(p.Name)).ToArray();
                foreach (var item in properties)
                {
                    Query.Append(item.Name);
                    Query.Append(": $");
                    Query.Append(item.Name);
                    Parameters[item.Name] = null;
                    if (item != properties.Last())
                        Query.Append(", ");
                }
            }
            return node;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if(node.Type == typeof(Expression<Func<IProperties>>))
            {
                var expr = (node.Member as FieldInfo).GetValue((node.Expression as ConstantExpression).Value) as Expression;
                Visit(expr);
                return node;
            }
            if (node.Expression != null && !isProperties.Value)
            {
                Visit(node.Expression);
                Query.Append(".");
            }
            Query.Append(node.Member.Name);
            if (node.Member is PropertyInfo pi && pi.PropertyType == typeof(IProperty) || isProperties.Value)
            {
                Query.Append(": $");
                if (expression[0].Value != null)
                    Visit(expression[0].Value);
                Query.Append(node.Member.Name);
                Parameters[node.Member.Name] = null;
            }
            return node;
        }

        protected override Expression VisitNewArray(NewArrayExpression node)
        {
            foreach (var expr in node.Expressions)
            {
                Visit(expr);
                if (expr != node.Expressions.Last())
                    Query.Append(", ");
            }
            return node;
        }

        protected override Expression VisitNew(NewExpression node)
        {
            using var _ = isProperties.Set(true);
            foreach (var expr in node.Arguments)
            {
                Visit(expr);
                if (expr != node.Arguments.Last())
                    Query.Append(", ");
            }
            return node;
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            var parameterName = $"p_{Parameters.Count}";
            Query.Append($"${parameterName}");
            Parameters[parameterName] = node.Value;
            return node;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            Query.Append(node.Name);
            return node;
        }
    }

    class CypherCommand
    {
        public string Query { get; }
        public Dictionary<string, object> Parameters { get; }

        public CypherCommand(string query, Dictionary<string, object> parameters)
        {
            Query = query;
            Parameters = parameters;
        }

        public void Print()
        {
            Console.WriteLine(Query);
            Console.WriteLine("---Parameters---");
            Console.WriteLine(string.Join(Environment.NewLine, Parameters));
        }
    }

    class Relation
    {
        public Relation this[IVar var, IType type] { [Cypher("[$0:$1]")]get => throw new NotImplementedException(); }
        public static Relation operator -(PD l, Relation r) => throw new NotImplementedException();
        public static Relation operator -(Relation l, PD r) => throw new NotImplementedException();
        public static Relation operator >(Relation l, Relation r) => throw new NotImplementedException();
        public static Relation operator <(Relation l, Relation r) => throw new NotImplementedException();
        public static PD operator >(Relation l, PD r) => throw new NotImplementedException();
        public static PD operator <(Relation l, PD r) => throw new NotImplementedException();
        public static PD operator >(PD l, Relation r) => throw new NotImplementedException();
        public static PD operator <(PD l, Relation r) => throw new NotImplementedException();
    }

    interface INode { }
    interface IVar { }
    interface ILabel { }
    interface IType { }
    interface IProperty { }
    interface IProperties { }

    class CypherAttribute : Attribute
    {
        public string Format { get; }

        public CypherAttribute(string format)
        {
            Format = format;
        }
    }

    static class Pattern
    {
        public delegate PD PD(IVar var);

        public static CypherCommand P(Expression<PD> expr)
        {
            var visitor = new CypherVisitor();
            visitor.Visit(expr);
            return new CypherCommand(visitor.Query.ToString(), visitor.Parameters);
        }

        [Cypher("($0:$1)")]
        public static PD N(IVar var, ILabel label) => throw new NotImplementedException();
        [Cypher("($0:$1 { $2 })")]
        public static PD N(IVar var, ILabel label, IProperties properties) => throw new NotImplementedException();
        [Cypher("($0:!0)")]
        public static PD N<T>(IVar var) => throw new NotImplementedException();
        [Cypher("($0:!0 { $1 })")]
        public static PD N<T>(IVar var, Func<T, IProperties> properties) => throw new NotImplementedException();
        [Cypher("(.1$0:!0 { $1 })")]
        public static PD N<T>(IVar var, IProperties properties) => throw new NotImplementedException();
        [Cypher("($0:!0:$1)")]
        public static PD N<T>(IVar var, ILabel label) => throw new NotImplementedException();
        [Cypher("($0:!0:$1 { $2 })")]
        public static PD N<T>(IVar var, ILabel label, Func<T, IProperties> properties) => throw new NotImplementedException();

        public static Relation R => throw new NotImplementedException();

        [Cypher("$0")]
        public static IProperties P(params IProperty[] properties) => throw new NotImplementedException();
        [Cypher("$0")]
        public static IProperties P(params object[] properties) => throw new NotImplementedException();
        [Cypher("$0")]
        public static IProperties P<T>(Func<T, IProperties> properties) => throw new NotImplementedException();
        [Cypher("$0")]
        public static IProperties P(Expression<Func<IProperties>> properties) => throw new NotImplementedException();
        [Cypher("+00$1")]
        public static IProperties Pre(IVar var, IProperties properties) => throw new NotImplementedException();
        public static IProperties Convention(Func<string, bool> filter) => throw new NotImplementedException();
        [Cypher("$0")]
        public static T As<T>(this IVar var) => throw new NotImplementedException();
        public static object All<T>(this IVar var) => throw new NotImplementedException();

        [Cypher("PROFILE")]
        public static PD Profile() => throw new NotImplementedException();
        [Cypher("MATCH $0")]
        public static PD Match(PD p) => throw new NotImplementedException();
        [Cypher("CREATE $0")]
        public static PD Create(PD p) => throw new NotImplementedException();
        [Cypher("MERGE $0")]
        public static PD Merge(PD p) => throw new NotImplementedException();
    }

    static class PatternExtensions
    {
        [Cypher("$0\r\nMATCH $1")]
        public static PD Match(this PD p, PD pp) => throw new NotImplementedException();
        [Cypher("$0\r\nCREATE $1")]
        public static PD Create(this PD p, PD pp) => throw new NotImplementedException();
        [Cypher("$0\r\nMERGE $1")]
        public static PD Merge(this PD p, PD pp) => throw new NotImplementedException();
        [Cypher("$0\r\nOPTIONAL MATCH $1")]
        public static PD OptionalMatch(this PD p, PD pp) => throw new NotImplementedException();
        [Cypher("$0\r\nWHERE $1")]
        public static PD Where(this PD p, bool condition) => throw new NotImplementedException();
        [Cypher("$0\r\nRETURN $1")]
        public static PD Return(this PD p, params object[] vars) => throw new NotImplementedException();
        [Cypher("$0\r\nWITH $1")]
        public static PD With(this PD p, params object[] vars) => throw new NotImplementedException();
        [Cypher("$0\r\nORDER BY $1")]
        public static PD OrderBy(this PD p, params object[] vars) => throw new NotImplementedException();
        [Cypher("$0\r\nORDER BY $1 DESC")]
        public static PD OrderByDesc(this PD p, params object[] vars) => throw new NotImplementedException();
        [Cypher("$0\r\nSKIP $1")]
        public static PD Skip(this PD p, int count) => throw new NotImplementedException();
        [Cypher("$0\r\nLIMIT $1")]
        public static PD Limit(this PD p, int count) => throw new NotImplementedException();
    }

    static class Schema
    {
        public static ILabel Person => throw new NotImplementedException();
        public static IType KNOWS => throw new NotImplementedException();
        public static IProperty PropA => throw new NotImplementedException();
        public static IProperty PropB => throw new NotImplementedException();
    }

    class Foo
    {
        public string Name { get; set; }
        public string PropA { get; set; }
        public string PropB { get; set; }
    }

    class Bar
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime Date { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            P(a => r1 => b => r2 => c =>
                      Match(N(a, Person) - R[r1, KNOWS] > N(b, Person) < R[r2, KNOWS] - N(c, Person))
                      .Where(a.As<Foo>().Name == "Avi")
                      .Return(a.As<Foo>().Name, r1, b.All<Bar>(), r2, c)
                      .OrderBy(a.As<Foo>().Name)
                      .Skip(1)
                      .Limit(10)).Print();

            Expression<Func<IProperties>> p = () => P(PropA, PropB);
            P(n => N(n, Person, P(p))).Print();
            P(n => N(n, Person, P(PropA, PropB))).Print();
            P(n => N<Foo>(n, n => P(n.PropA, n.PropB))).Print();
            P(n => N<Foo>(n, Person, n => P(n.PropA, n.PropB))).Print();
            P(n => N(n, Person, P<Foo>(n => P(n.PropA, n.PropB)))).Print();
            P(n1 => n2 => n2_ => N(n1, Person, P(PropA, PropB)) - R[n1, KNOWS] > N(n2, Person, Pre(n2_, P(PropA, PropB)))).Print();
            P(n => N<Foo>(n, Convention(name => name.EndsWith("B")))).Print();
        }
    }
}

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

Parameter usage sample

P("name", "id"),
P<Foo>(with: f => f.Name, contextPrefix: PCtxPrefix.Unwind).Add(f => f.Id),
P<Foo>(convertion: n => n != "x", useVariableAsPrefix: true),
P<Foo>(exclude: f => f.Date, fixPrefix: "x_", noParameterSign: true),

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

public static class PropFactory
{ 
    public static FluentCypher P<T>(
        Expression<Func<T, dynamic>>? with = null,
        Func<string, bool>? convention = null,
        Expression<Func<T, dynamic>>? exclude = null)
    {
        throw new NotImplementedException();
    }

    // Some naming problem because params array & optional parameter don't  mixed well
    public static FluentCypher P_<T>(params Expression<Func<T, dynamic>>[] exclude)
    {
        throw new NotImplementedException();
    }
    public string void Try()
    {
        P<IArgumentProvider>(with: f => f.ArgumentCount);
        P<IArgumentProvider>(convention: n => !n.StartsWith("X"));
        P<IArgumentProvider>(exclude: f => f.ArgumentCount);
        P_<TimeoutException>(f => f.Data, f => f.HelpLink);
    }
}

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

other option
public static class PropFactory
{
public static ICypherPropertiesBuilder P(
Expression<Func<T, dynamic>>? with = null,
Func<string, bool>? convention = null,
Expression<Func<T, dynamic>>? exclude = null)
{
throw new NotImplementedException();
}
public string void Try()
{
P(with: f => f.AmbientLabels).With(f => f.Concurrency.eTagName);
P(convention: n => !n.StartsWith("X")).With(f => f.Pluralization);
P(exclude: f => f.AmbientLabels.Values);
}
}
public interface ICypherPropertiesBuilder
{
ICypherPropertiesBuilder With(
Expression<Func<T, dynamic>> with);
ICypherPropertiesBuilder ByConvention(
Func<string, bool> convention);
FluentCypher All(
Expression<Func<T, dynamic>> exclude);
}

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

public static class P
{
    public static ICypherPropertiesBuilder<T> As<T>()
    {
        throw new NotImplementedException();
    }
}
class Tmp
{
    public void Temp()
    {
        P.As<ICypherConfig>().With(f => f.Concurrency.eTagName)
                             .With(f => f.AmbientLabels)
                             .ByConvention(n => n.StartsWith("eT"));
        P.As<ICypherConfig>().ByConvention(n => !n.StartsWith("X"))
                             .With(f => f.Pluralization);
        P.As<ICypherConfig>().All(exclude => exclude.AmbientLabels.Values);
    }
}

public interface ICypherPropertiesBuilder<T>
{
    ICypherPropertiesBuilder<T> With(
        Expression<Func<T, dynamic>> with);
    ICypherPropertiesBuilder<T> ByConvention(
        Func<string, bool> convention);
    FluentCypher All(
        Expression<Func<T, dynamic>> exclude);
}

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

.Scope(P.As().With(f => f.Concurrency.eTagName)
.With(f => f.AmbientLabels),
p => (n,Label, p)

// cypher
(n:Label {n.eTagName = $eTagName, n.AmbientLabels = $AmbientLabels}

@bnayae
Copy link
Author

bnayae commented Jan 29, 2020

(n,Label, P{eTagName, AmbientLabels} )

// cypher
(n:Label {n.eTagName = $eTagName, n.AmbientLabels = $AmbientLabels}

@bnayae
Copy link
Author

bnayae commented Feb 2, 2020

Properties requirements

Patterns

The properties pattern is also relevant for WHERE & SET (the pattern are same while the Cypher out come is different).
Some of the patterns may use generics.
The samples ahead will assume the existence of the following declarations:

        public static ILabel Person => throw new NotImplementedException();
        public static IType KNOWS => throw new NotImplementedException();
        public static IProperty PropA => throw new NotImplementedException();
        public static IProperty PropB => throw new NotImplementedException();

Specify array of names

// suggested API
P(n => N(n, Person, new {PropA, PropB} )  // CYPHER: (n:Person {PropA: $PropA, PropB: $PropB}
// with generic factory
P<Foo>(n => N(n, new {n.PropA, n.PropB} ) // the label is typeof(Foo).Name (this is the default when label doesn't declared )
P<Foo>(n => N(n, n.GetType, Person, new {n.PropA, n.PropB} ) // the label is n.GetType = typeof(Foo).Name + Person i.e. Foo:Person
P<Foo>(n => N(n, Person, new {n.PropA, n.PropB} ) // the label is  Person (ignore the generics auto label, the generic is used by the parameters)

Property materialization

The default form of property materialization (rendering) is:

P(n => N(n, Person, new {PropA, PropB} )  
CYPHER: (n:Person {PropA: $PropA, PropB: $PropB}

But there are other cases where the $ sign shouldn't be render like

UNWIND $items AS item
 (n:Person {PropA: item.PropA, PropB: item.PropB}

Prefix is another requirements, where the same property name appear multiple time but should map to different parameter:
Option 1:

P(n1 => n2 => n2_ => N(n, Person, new {PropA, PropB} )  - R[] > N(n2, Person, Pre(n2_,  new {PropA, PropB} )))
CYPHER: (n1:Person {PropA: $PropA, PropB: $PropB} - [] -> (n2:Person {PropA: $n2_PropA, PropB: $n2_PropB)

Option 2 (use value tuple):

P(n1 => n2 => n2_ => N(n, Person, new {PropA, PropB} )  - R[] > N(n2, Person,  new {(n2_, PropA), (n2_, PropB)} ))
CYPHER: (n1:Person {PropA: $PropA, PropB: $PropB} - [] -> (n2:Person {PropA: $n2_PropA, PropB: $n2_PropB)

Nested expression

// Reuse with scope (can be thought as nesting patterns)
P<Foo, Bar>(p => new {p1.PropA, p1.PropB},  // from Foo
                     p2 => new { p2.ProbC },                 // from Bar
                     n1 => n2 => N(n1, Person, p) - R[_, KNOWS] > N(n2, Person, p)
) // CYPHER: (n1:Person {PropA: $PropA, PropB: $PropB}) - [:KNOW] -> (n2:Person {PropA: $PropA, PropB: $PropB})

SET note

When the patterns apply to CREATE, MERGE, SET there are more options like

CREATE (n:Person $map) // entity parameter
MATCH (n:Person {Id: $map.Id})
SET n += $map

Combined with UNWIND

UNWIND $items AS item
MATCH (n:Person {Id: item.Id}) // NO $ SIGN
SET n += item

All / Exclude

Generics patterns can be used for mapping all Type's properties or most of them excluding a few

// Reuse with scope (can be thought as nesting patterns)
P<Foo>(n  => N(n, all: n, exclude: f => f.Id) 
// Or
P<Foo>(n  => N(n, ALL(exclude: f => f.Id)) 
) // CYPHER: (n:Foo {PropA: $PropA, PropB: $PropB}) // anything except Id

By Convention

Generics patterns can be used for mapping Type's properties by convention using delegate

// Reuse with scope (can be thought as nesting patterns)
P<Foo>(n  => N(n, convention: name => name.EndsWith("B") 
) // CYPHER: (n:Foo {PropB: $PropB}) // anything except Id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment