Giter VIP home page Giter VIP logo

pypher's Introduction

Pypher -- Cypher, but in Python

Pypher is a tiny library that focuses on building Cypher queries by constructing pure Python objects.

Binder

Setup

python setup.py install
pip install python_cypher

Running Tests

python setup.py test

Or if the package is already installed

python -m unittest pypher.test.builder

Usage

Pypher is pretty simple and has a small interface. Pypher tries to replicate building a Cypher query by utilizing all of Python's magic methods behind the scenes.

Let's say you wanted to write this Cypher query:

MATCH (mark:Person)
WHERE mark.name = "Mark"
RETURN mark;

Your Pypher would look like this:

from pypher import Pypher

q = Pypher()
q.Match.node('mark', labels='Person').WHERE.mark.property('name') == 'Mark'
q.RETURN.mark

That isn't a one-to-one match, but it is close. More importantly, easy to read, understand, and compose complex queries without string concatenation.

Creating an actual Cypher string from a Pypher query is simple

cypher = str(q) # MATCH (mark:`Person`) WHERE mark.`name` = NEO_9326c_1 RETURN mark
params = q.bound_params # {'NEO_9326c_1': 'Mark'}

Note: Pypher doesn't create the Cypher string until your Pypher instance is converted into a string via str(p) or print(p) etc., at the same time all of the bound parameters are collected through the many possible sub-instances of Pypher objects that may be in the chain.

Structure

Pypher is a very simple query builder for Cypher. It works by creating a simple linked list of objects and running __str__ against the list when it is time to render the Cypher. Along the way it stores bound params, allows for complex Cypher queries with deep Pypher nestings, and even direct string inclusion if the abstraction gets too messy.

Pypher Objects

Pypher

Pypher is the root object that all other objects sub-class and it makes everything work. Every operation taken on it (attribute access or assignments or comparisons) will result in link being added to list.

Quoting: by default Pypher will quote labels, properties, and map_keys with backticks . This behavior can be overwritten by setting the QUOTE value in the builder module. import pyper; pyper.builder.QUOTES['propery'] = '"'` this sets the quote marksf for properties to be a double quote instead of a backtick

Useful Methods and Properties

  • bind_param(value, name=None) -- this method will add a bound param to to resulting Cypher query. If a name is not passed it, one will be generated.
  • add_link(link) -- this method is used in every interaction with the Pypher object. It lets you manually add a link to the list that you may not had been able to otherwise express with existing methods or objects.
  • func(name, *args) -- this will allow you to call a custom function. Say you want the resulting Cypher to have a Python keyword like __init__, you would call q.func('__init__', 1, 2, 3) which would resolve to __init__(1, 2, 3) (the arguments will be bound).
  • func_raw(name, *args) -- this acts just like the func method, but it will not bind the arguments passed in.
  • raw(*args) -- this will take whatever you put in it and print it out in the resulting Cypher query. This is useful if you want to do something that may not be possible in the Pypher structure.
  • rel_out(*args, **kwargs) -- this will start an outgoing relationship. See Relationship for argument details.
  • rel_in(*args, **kwargs) -- this will start an incoming relationship. See Relationship for argument details.
  • alias(alias) -- this is a way to allow for simple AS $name in the resulting Cypher.
  • property(name) -- since Pypher already co-opted the dot notation for stringing together the object, it needed a way to represent properties on a Node or Relationship. Simply type q.n.property('name') or q.n__name__ to have it create n.name in Cypher. See Property for more details. Properties will be wrapped in back ticks to allow for spaces and other special characters.
  • operator(operator, value) -- a simple way to add anything to the chain. All of the Pypher magic methods around assignments and math call this method. Note: the other needs to be a different Pypher instance or you will get a funky Cypher string.
  • _ -- the current Pypher instance. This is useful for special edge cases. See Property
  • apply_partial -- adds the result of the Partial object to the given Pypher instance.
  • append -- will allow multiple Pypher instances to be combined into a single chain.
  • clone -- will create a copy of the Pypher instance and the Params object that holds the pypher_instance.bound_params

Operators

Since Pypher is an object whose sole job is to compose a linked list via a fluid interface, adding common operators to the object is tricky. Here are some rules:

  • No matter the operator, the right side of the operation must not be the same Pypher instance as found on the left. A common way around this is to import and use the __ Anon Pypher factory.
  • Operators allow for Python dictionaires to be passed in
    • p.user += {'name': 'Mark'}
  • You can create custom Operators by calling .operator(name, other_value) on the Pypher instance -- the first operator rule must be followed if the other end is a Pypher object.
    • Operators always resolve in a space, the operator, and then the other value. Until it doesn't.
      • Certain operators (all of the Python magic methods that support it) support reflected, or right, side assignment and will print the resulting Cypher as expected. Something like 99 - p.__field__ will work as expected, but 99 > p.__field__ will result in p.field < 99
from pypher import Pypher, __

p = Pypher()
p.WHERE.n.name == __.s.__name__

str(p) # WHERE n.`name` = s.`name`

# custom operator
x = Pypher()
x.WHERE.name.operator('**', 'mark') # mark will be a bound param
str(x) # WHERE n.name ** NEO_az23p_0 
Pypher Operator Resulting Cypher Supports Referece Assignment
== = -
!= <> -
+ + yes
+= += -
- - yes
-= -= -
* * yes
*= *= -
/ / yes
/= /= -
% % yes
%= %= -
& & yes
| | yes
^ ^ yes
^= ^= -
> > -
>= >= -
< < -
<= <= -

Operator Methods

Some methods resolve to Operator instances. These are called on the Pypher instance with parenthesis.

Pypher Operator Resulting Cypher
.AND(other) AND other
.OR(other) OR other
.ALIAS(other) AS other
.AS(other) AS other
.rexp(other) =~ $other_bound_param
.BAND(right, left) apoc.bitwise.op(right, "&", left)
.BOR(right, left) apoc.bitwise.op(right, "|", left)
.BXOR(right, left) apoc.bitwise.op(right, "^", left)
.BNOT(right, left) apoc.bitwise.op(right, "~", left)
.BLSHIFT(right, left) apoc.bitwise.op(right, ">>", left)
.BRSHIFT(right, left) apoc.bitwise.op(right, "<<", left)
.BULSHIFT(right, left) apoc.bitwise.op(right, ">>>", left)

__ (double underscore)

__ The double underscore object is just an instance of Anon. It is basically a factory class that creates instances of Pypher when attributes are accessed against it.

  • Useful for creating Pypher objects that will either be passed in as arguments or used to continue a chain after a math or assignment operation on an existing chain.
from pypher import __, Pypher

p = Pypher()

p.MATCH.node('mark', labels='Person').rel(labels='knows').node('mikey', labels=['Cat', 'Animal'])
p.RETURN(__.mark, __.mikey) 

str(p) # MATCH (mark:`Person`)-[:`knows`]-(mikey:`Cat`:`Animal`) RETURN mark, mikey

# OR

p = Pypher()

p.MATCH.node('mark').SET(__.mark.property('name') == 'Mark!!')

print(str(p)) # MATCH (mark) SET mark.`name` = $NEO_2548a_0
print(dict(p.bound_params)) # {'NEO_2548a_0': 'Mark!!'}

The __ is just an instance of the Anon object. You can change what you want your factory name to be, or create an instance of Anon and assign it to another variable as you see fit.

Param

Param objects are simple containers that store a name and a value.

  • These objects are useful when you want finer control over the names of the bound params in the resulting Cypher query.
  • These can be passed in to Pyper instances and will be referenced by their name once the Cypher string is created.
  • Pypher.bind_param will return an instance of a Param object.
  • When binding params Pypher will reuse the existing reference if the same value is passed in.
    • It will also reuse the same reference if the value passed in is the name of a previously bound param.
from pypher import Param, Pypher, __


p = Pypher()
name = Param(name='namedParam', value='Mark')
p.SET(__.m.__name__ == name)

str(p) # SET m.`name` = namedParam
print(p.bound_params) # {'namedParam': 'Mark'}

# reusing the same reference per value
param = p.bind_param('some value', 'key')
param2 = p.bind_param('some_value')

param.name == param2.name # True

# reusing the same reference when the value is the key
param = p.bind_param('some value', 'some key')
param2 = p.bind_param('some key')

param.name == param2.name # True
param.value == params2.value # True

Statement

Statement objects are simple, they are things like MATCH or CREATE or RETURN.

  • Can be added to the list with any casing q.MATCH is the same as a.match both will result in MATCH being generated.
  • When an undefined attribute is accessed on a Pypher instance, it will create a Statement from it. q.iMade.ThisUp will result in IMADE THISUP
  • Will print out in ALL CAPS and end with an empty space.
  • Can take a list of arguments q.return(1, 2, 3) will print out RETURN 1, 2, 3
  • Can also just exist along the chain a.MATCH.node('m') will print out MATCH (m)
  • Random statements can be created. This is useful for when Cypher grows, but Pypher may not have manually defined the new functionality
    • p.some_statement(1, 2, 3) will return random_statement 1, 2, 3
  • Pypher provides a suite of pre-defined statements out of the box:
Pypher Object Resulting Cypher Aliases
Match MATCH
Create CREATE
Merge MERGE
Delete DELETE
Remove REMOVE
Drop DROP
Where WHERE
Distinct DISTINCT
OrderBy ORDER BY
Set SET
Skip SKIP
Limit LIMIT
Return RETURN
Unwind UNWIND
ASSERT ASSERT
Detach DETACH
DetachDelete DETACH DELETE
Foreach FOREACH
Load LOAD
CSV CSV
FROM FROM
Headers HEADERS
LoadCsvFrom LOAD CSV FROM
LoadCSVWithHeadersFrom LOAD CSV WITH HEADERS FROM
WITH WITH
UsingPeriodIcCommit USING PERIODIC COMMIT
Periodic PERIODIC
Commit COMMIT
FieldTerminator FIELDTERMINATOR
Optional OPTIONAL
OptionalMatch OPTIONAL MATCH
Desc DESC
When WHEN
ELSE ELSE
Case CASE
End END
OnCreate ON CREATE
OnCreateSet ON CREATE SET
OnMatchSet ON MATCH SET
CreateIndexOn CREATE INDEX ON
UsingIndex USING INDEX
DropIndexOn DROP INDEX ON
CreateConstraintOn CREATE CONSTRAINT ON
DropConstraintOn DROP CONSTRAINT ON
In IN
Map {}
MapProjection var {} map_projection projection
NOT NOT
IS IS
OR OR
NULL NULL
IS_NULL IS NULL
IS NOT NULL IS NOT NULL

Python keywords will be in all CAPS

  • Pypher provides a way to define a custom Statement class via a function call (this is used to create all of the statements listed above).
from pypher import create_statement, Pypher

create_statement('MyStatementName', {'name': 'MY STATEMENT IN CYPHER'})

p = Pypher()

p.MyStatementName.is.cool

str(p) # MY STATEMENT IN CYPHER IS COOL

The name definition is optional. If omitted the resulting Cypher will be the class name in call caps

Another way is to sub-class the Statement class

from pypher import Pypher, Statement


class MyStatement(Statement):
    _CAPITALIZE = True # will make the resulting name all caps. Defaults to True
    _ADD_PRECEEDING_WS = True # add whitespace before the resulting Cypher string. Defaults to True
    _CLEAR_PRECEEDING_WS = True # add whitespace after the resulting Cypher string. Defaults to False
    _ALIASES = ['myst',] # aliases for your custom statement. Will throw an exception if it is already defined
    name = 'my statement name' # the string that will be printed in the resulting Cypher. If this isn't defined, the class name will be used

Func

Func objects resolve to functions (things that have parenthesis)

  • Func objects take a list of arguments. These can be anything from Python primitives to nested Pypher objects, it must have a __str__ representation to be used.
  • Each argument will be automatically set as a bound parameter unless it is either a Param , Pypher, or Partial object. If the argument is not from the Pypher module, it will be given a randomly generated name in the resulting Cypher query and bound params.
  • Can take an unlimited number of arguments.
  • Pypher provides a suite of pre-defined functions out of the box:
Pypher Object Resulting Cypher
size size
reverse reverse
head head
tail tail
last last
extract extract
filter filter
reduce reduce
Type type
startNode startNode
endNode endNode
count count
ID id
collect collect
sum sum
percentileDisc percentileDisc
stDev stDev
coalesce coalesce
timestamp timestamp
toInteger toInteger
toFloat toFloat
toBoolean toBoolean
keys keys
properties properties
length length
nodes nodes
relationships relationships
point point
distance distance
abs abs
rand rand
ROUND round
CEIL ceil
Floor floor
sqrt sqrt
sign sign
sin sin
cos cos
tan tan
cot cot
asin asin
acos acos
atan atan
atanZ atanZ
haversin haversin
degrees degrees
radians radians
pi pi
log10 log10
log log
exp exp
E e
toString toString
replace replace
substring substring
left left
right right
trim trim
ltrim ltrim
toUpper toUpper
toLower toLower
SPLIT split
exists exists
MAX max

Python keywords will be in all CAPS

  • Pypher provides a way to define a custom Func or FuncRaw class via a function call (this is used to create all of the functions listed above)
from pypher import create_function, Pypher

create_function('myFunction', {'name': 'mfun'})

p = Pypher()

p.myFunction(1, 2, 3)

str(p) # myFunction(1, 2, 3) note that the arguments will be bound and not "1, 2, 3"

The name definition is optional. If omitted the resulting Cypher will be the exact name of the function

Another way is to sub-class the Func or FuncRaw class.

FuncRaw will not bind its arguments.

from pypher import Pypher, Func, FuncRaw


class MyCustomFunction(Func):
    _CAPATILIZE = True # will make the resulting name all caps. Defaults to False
    _ADD_PRECEEDING_WS = True # add whitespace before the resulting Cypher string. Defaults to True
    _CLEAR_PRECEEDING_WS = True # add whitespace after the resulting Cypher string. Defaults to False
    _ALIASES = ['myst',] # aliases for your custom function. Will throw an exception if it is already defined
    name = 'myCustomFunction' # the string that will be printed in the resulting Cypher. If this isn't defined, the class name will be used

Conditionals

Conditional objects allow groupings of values surrounded by parenthesis and separated by a comma or other value.

Pypher Object Resulting Cypher Aliases
Conditional (val, val2, valN)
ConditionalAND (val AND val2 AND valN) CAND, COND_AND
ConditionalOR (val OR val2 OR valN) COR, COND_OR

Entity

Entities are Node or Relationship objects. They both sub-class the Entity class and share the same attributes.

Node_ This represents an actual node in the ascii format.

  • The init can accept a variable<String>, labels<List|String>, properties<Keyword Arguments>
  • Can be added to the chain by typing .node or .n_

Relationship_ This represents an relationship node in the ascii format.

  • The init can accept a variable<String>, direction<String>['in', 'out', '>', '<'], labels<List|String>, hops<Number>, min_hops<Number>, max_hops<Number>, properties<Keyword Arguments>
  • Can be added to the chain by typing .relationship, .rel, .r_, or for directed: .rel_out or .rel_in
  • To create a variable length relationship (e.g. 1..3), use min_hops and max_hops
  • To create variable length relationship with an open bound (e.g. ..3), use min_hops or max_hops
  • To create a fixed length relationship, use hops
  • Using both hops and one of min_hops and max_hops will raise an error

Property

Property objects simply allow for adding .property to the resulting Cypher query.

  • These can be added to the chain by calling .property('name') or .__name__ (double underscore before and after)
  • Python does not allow assignment for function calls so something like this is illegal n.property('name') == 'Mark' if you wanted to use the property method in this scenario, you would have to get back to the Pypher instance like this n.property('name')._ == 'Mark' or use the double underscore method n.property.__name__ == 'Mark'.
  • Property objects work just like any other link and you can add them anywhere, even if it doesn't produce property Cypher. p.RETURN.property('name') will create RETURN.name

Label

Label objects simply add a label to the preceding link.

  • Can be init with *args of labels n.label('Person', 'Male') would produce n:Person:Male
  • This does not bind its arguments
  • Labbels will be wrapped in back ticks to allow for spaces and other special characters

Partial

Partial objects allows for encapsulation of complex Pypher chains. These objects will allow for preset definitions to be added to the current Pypher instance.

  • The sub-class must call super in the __init__
  • The sub-class must define a build method that houses all of the business rules for the Partial
  • The partial can have any interface the developer sees fit
  • Any bound params will be passed up to the parent Pypher instance
  • Partial objects maintain the same interface as Pypher objects, they simply proxy all calls up to the Pypher instance that the Partial contains (this is useful for assignments or math, etc)

Here is an example of the built in Case Partial that provides a CASE $case [WHEN $when THEN $then,...] [ELSE $else] END addition:

class Case(Partial):

    def __init__(self, case):
        super(Case, self).__init__()

        self._case = case
        self._whens = []
        self._else = None

    def WHEN(self, when, then):
        self._whens.append((when, then))

        return self

    def ELSE(self, else_case):
        self._else = else_case

        return self

    def build(self):
        self.pypher.CASE(self._case)

        for w in self._whens:
            self.pypher.WHEN(w[0]).THEN(w[1])

        if self._else:
            self.pypher.ELSE(self._else)

        self.pypher.END

#usage is simple
p = Pypher()

# build the partial according to its interface
case = Case(__.n.__eyes__)
case.WHEN('"blue"', 1)
case.WHEN('"brown"', 2)
case.ELSE(3)

# add it to the Pypher instance
p.apply_partial(case)

str(p) # CASE n.eyes WHEN "blue" THEN 1 WHEN "brown" THEN 2 ELSE 3 END

As seen in this example, if you want your resulting Cypher to have actual quotes, you must nest quotes when passing in the arguments to the Statement objects

Maps

Cypher allows for Java-style maps to be returned in some complex queries, Pypher provides two classes to assist with map creation: Map and MapProjection

  • Both objects have a signature of *args and **kwargs
    • *args will be printed out in the resoling Cypher exactly how they are defined in Python
    • **kwargs will be printed out as key:value pairs where the values are bound params
    • MapProjection has a name argument that will printed out before the map
p = Pypher()
p.RETURN.map('one', 'two', three='three')
print(str(p)) # RETURN {one, two, `three`: $three213bd_0}
print(dict(p.bound_params)) # {'three213bd_0': 'three'}

p.reset()
p.RETURN.map_projection('user', '.name', '.age')
print(str(p)) # 'RETURN user {.name, .age}'

Code Examples

This section will simply cover how to write Pypher that will convert to both common and complex Cypher queries.

A Simple Match with WHERE

MATCH (n:Person)-[:KNOWS]->(m:Person)
WHERE n.name = 'Alice'
p.MATCH.node('n', 'Person').rel_out(labels='KNOWS').node('m', 'PERSON').WHERE.n.__name__ == 'Alice'

A Simple Match with IN

MATCH (n:Person)-[:KNOWS]->(m:Person)
WHERE n.name IN ['Alice', 'Bob']
names = ['Alice', 'Bob']
p.MATCH.node('n', 'Person').rel_out(labels='KNOWS').node('m', 'PERSON').WHERE.n.__name__.In(*names)

Create A Node

CREATE (user:User {Name: 'Jim'})
p.CREATE.node('user', 'User', Name='Jim')
MERGE (user:User { Id: 456 })
ON CREATE user
SET user.Name = 'Jim'
p.MERGE.node('user', 'User', Id=456).ON.CREATE.user.SET(__.user.__Name__ == 'Jim')

Create a variable length relationship

MATCH (martin { name: 'Charlie Sheen' })-[:ACTED_IN*1..3]-(movie:Movie)
RETURN movie.title
p.Match.node('martin', name='Charlie Sheen').rel(labels='ACTED_IN', min_hops=1, max_hops=3).node('movie', 'Movie')
p.Return(__.movie.__title__)

Create a fixed length relationship

MATCH (martin { name: 'Charlie Sheen' })-[:ACTED_IN*2]-(movie:Movie)
RETURN movie.title
p.Match.node('martin', name='Charlie Sheen').rel(labels='ACTED_IN', hops=2).node('movie', 'Movie')
p.Return(__.movie.__title__)

Tester

Included is a very bare-bones CLI app that will allow you to test your Pypher scripts. After installing Pypher, you can run the script simply by calling python tester.py. Once loaded you are presented with a screen that will allow you to write Pypher code and it will generate the Cypher and bound params. This is a quick way to check if your Pypher is producing the desired Cypher for your project.

Example tester.py usage

pypher's People

Contributors

0xflotus avatar benedictboyle avatar bx-ali avatar dconatha avatar dshunfen avatar emehrkay avatar g3rd avatar j6k4m8 avatar pbabics avatar rob-mccann avatar vinli-mark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pypher's Issues

Neo.ClientError.Statement.ParameterMissing

Hi and thanks for your work.
I'm trying to build my first fastapi backend using the neo4j driver with async support (5.0.0a1) and Pypher.
Following the code that I'm using and the error code I get. Basically writing the query by hand my code is working, instead using pypher I have the message "Neo.ClientError.Statement.ParameterMissing". In the output you'll find the printed query which seems strange to me, do you agree? Any tip to understanding how to fix that? Thanks

router.py

@router.post('/posts')
async def create_new_post(post: PostIn):
    post_repo = PostRepository()
    result = await post_repo.create('a')
    return result

repository.py

class PostRepository(CrudRepositoryInterface):
    async def create(self, obj):
        p = pypher.Pypher()
        p.CREATE.node('post', 'Post', Title='titolo', Message='messaggio')
        query = str(p)
        print("DEBUG: Print query {}".format(query))
        async with db.driver.session() as session:
            result = await session.run(query)
            # result = await session.run("CREATE (user:User {Name: 'Jim'})")
            return result

    async def read(self, obj_id: UUID):
        pass

    async def update(self, obj):
        pass

    async def delete(self, obj_id: UUID):
        pass

output

restapi_1  | /usr/local/lib/python3.9/site-packages/neo4j/_async/io/_common.py:225: RuntimeWarning: coroutine 'AsyncBolt4x0.reset' was never awaited
restapi_1  |   self.connection.reset()
restapi_1  | RuntimeWarning: Enable tracemalloc to get the object allocation traceback
restapi_1  | DEBUG: Print query CREATE (post:`Post` {`Message`: $Message7f65d_0, `Title`: $Title7f65d_1})
restapi_1  | INFO:     172.19.0.1:43862 - "POST /posts HTTP/1.1" 500 Internal Server Error
restapi_1  | ERROR:    Exception in ASGI application
restapi_1  | Traceback (most recent call last):
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 373, in run_asgi
restapi_1  |     result = await app(self.scope, self.receive, self.send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
restapi_1  |     return await self.app(scope, receive, send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 208, in __call__
restapi_1  |     await super().__call__(scope, receive, send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 112, in __call__
restapi_1  |     await self.middleware_stack(scope, receive, send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 181, in __call__
restapi_1  |     raise exc from None
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 159, in __call__
restapi_1  |     await self.app(scope, receive, _send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
restapi_1  |     raise exc from None
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/exceptions.py", line 71, in __call__
restapi_1  |     await self.app(scope, receive, sender)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 580, in __call__
restapi_1  |     await route.handle(scope, receive, send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 241, in handle
restapi_1  |     await self.app(scope, receive, send)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 52, in app
restapi_1  |     response = await func(request)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 226, in app
restapi_1  |     raw_response = await run_endpoint_function(
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 159, in run_endpoint_function
restapi_1  |     return await dependant.call(**values)
restapi_1  |   File "/code/./app/routers/post.py", line 24, in create_new_post
restapi_1  |     result = await post_repo.create('a')
restapi_1  |   File "/code/./app/repositories/post.py", line 15, in create
restapi_1  |     result = await session.run(query)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/work/session.py", line 217, in run
restapi_1  |     await self._auto_result._run(
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/work/result.py", line 115, in _run
restapi_1  |     await self._attach()
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/work/result.py", line 204, in _attach
restapi_1  |     await self._connection.fetch_message()
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/io/_common.py", line 177, in inner
restapi_1  |     await coroutine_func(*args, **kwargs)
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/io/_bolt4.py", line 306, in fetch_message
restapi_1  |     await response.on_failure(summary_metadata or {})
restapi_1  |   File "/usr/local/lib/python3.9/site-packages/neo4j/_async/io/_common.py", line 232, in on_failure
restapi_1  |     raise Neo4jError.hydrate(**metadata)
restapi_1  | neo4j.exceptions.ClientError: {code: Neo.ClientError.Statement.ParameterMissing} {message: Expected parameter(s): Message7f65d_0, Title7f65d_1}

simple README fix

"lables" should be "labels" in:

p.MATCH.node('mark', labels='Person').rel(labels='knows').node('mikey', lables=['Cat', 'Animal'])

Cannot set "list" in as property value

unable to build cypher query to set "list" type value in property.
set entity.birth_date = coalesce(VALUE + entity.birth_date, entity.birth_date , [] + VALUE)

What I've tried:
from pypher import Pypher, __
p = Pypher()
node_gr = dict()
node_gr['name'] = 'XYZ'
p.Merge.node('ent', **node_gr)
p.SET(__.ent.__birth_place__ == __.COALESCE(__.ent.__birth_place__+
VALUE,__.ent.__birth_place__,[VALUE]))

Error:
python3.6/site-packages/pypher/builder.py in bind_param(self, value, name)
196 name = k
197 break
--> 198 elif bind and value in self._bound_params.keys():
199 for k, v in self._bound_params.items():
200 if k == value:

TypeError: unhashable type: 'list'

Importance of calling str(q)

Could you include in the documentation the importance of calling str(q)?
I was aware that str(q) has to be called before handing it off to the Neo driver.

However, at some point I failed to make that call, but I did a print somewhere so it still worked.
It took me a long time before I realized my mistake. Especially since Pycharm debugger did call str, which made it very confusing to know what went wrong.

Handling a list of dictionaries

I was wondering if there is a cleaner way to handle a list of dictionaries

    p = Pypher()
    batch = []
    for x in observations:
        batch.append(__.MAP(**x))

    return p.unwind(__.List(*batch))

above has been functional when the inputs looks like
[{'x':1,'y':2},{'x':2,'y':4},...]

wondering if I can avoid creating a new list of my existing list and then using the list function on it again as it seems a bit circular
thank you in advance

Howto do WHERE

Hi,

I'm trying to write pypher statement for function like

def list(db: Graph, type: str=None, okres:str = None):
    q = Pypher()
    q.Match.node('school', labels='School')
    if type is not None:
        q.WHERE.school.property('type') == type
    if okres is not None:
        q.AND.school.property('okres') == okres
    q.RETURN.school

If both conditions are NULL it works.
If type is set nad okres is NULL, it works.

Otherwise it do not works.

Is there any smart way hw to write WHERE statment, when I do not known the number of condition before? Like chaining AND, using Partials ..

regards
Peter

Config

Am I correct in saying that pypher is looking for your models in it's arguments?

Can it be configured to access a Neo4j database directly, or does it rely on something like neomodel to setup the models?

Operator precedence?

Is there way to express operator precedence in Pypher?
I need to express something like:
MATCH (n) WHERE (n.property_a = "foo" OR n.property_a = "bar") AND n.property_b = 1 RETURN n

Change `min_hops` and `max_hops` semantics

Currently the way to do "hops with no upper limit" is counterintuitive and seems like it's taking advantage of a bug... by "hops with no upper limit" I mean:

MATCH (n)-[*1..]->(m) RETURN m

Currently, the only way to do this is:

q = Pypher()
q.MATCH.node("n").rel_out(min_hops=1, max_hops="").node("m")

Whereas I would expect:

q = Pypher()
q.MATCH.node("n").rel_out(min_hops=1).node("m")

Because it's saying "I am specifying the min_hops and I am not specifying max_hops". In general the semantics of:

To create a fixed length relationship, use either min_hops or max_hops, or set both to the same number

Is not obvious or intuitive.

Proposal

A new param called hops for specifying a single hop number. min_hops and max_hops are reserved for specifying variable length relationships. If hops is specified, and one (or both) of min_hops and max_hops is specified, an error is raised. If just one of min_hops or max_hops is specified, then the range is only bounded on that side.

Examples:

q = Pypher()
q.MATCH.node("n").rel_out(hops=2).node("m")
# MATCH (n)-[*2]->(m)
q = Pypher()
q.MATCH.node("n").rel_out(min_hops=2).node("m")
# MATCH (n)-[*2..]->(m)
q = Pypher()
q.MATCH.node("n").rel_out(min_hops=2, max_hops=3).node("m")
# MATCH (n)-[*2..3]->(m)
q = Pypher()
q.MATCH.node("n").rel_out(hops=1, max_hops=3).node("m")
# raises error

If this is agreeable, I'm happy to put together a PR.

Using matched node assignments in merge

Hi,

I'm trying to create a MERGE for a relationship after a MATCH statement, as described in MERGE: neo4j documentation. However I'm having issues re-using the node-assignments.

The query I'm trying to produce is:

MATCH (a:`Article` {`article_id`: '123456'}),(p:`Product` {`product_id`: '1234567'})
MERGE a-[r:BELONGS_TO]->p

However I seem to run into two issues; the first is a multiple match, I tried defining it as:
p.MATCH.node(...).node(...)
this outputs a non comma separated MATCH sequence which turns unusable. How would I achieve a comma separated MATCH sequence (as shown in the referenced documentation)?

Secondly I can't seem to re-use the assigned node MATCH, in the above case assigned "a":

p.MERGE.a.rel_out(labels="BELONGS_TO").p

Update
What I'm trying to achieve is something similar to this:

MATCH(a:Article{product_id: '1234567'}), (p:Product{product_id: '1234567'})
MERGE (a)-[r:PART_OF]->(p)
RETURN a, r, p

Any ideas on this?

Thank you in advance

Problem with And operator

Hi, I am trying to concatenate several filters in the where clause through And conditions, but I am getting a strange NULL in the middle of the query. For example:

builder = Pypher()
builder.Where.node('procedure').rel_out(labels='type').node(labels='A')
builder.And.node('procedure').rel_out(labels='type').node(labels='B')
print(str(builder))

Is resulting on:
WHERE (procedure)-[:type]->(:A) AND NULL(procedure)-[:type]->(:B)

How can I avoid this NULL appearing after the AND keyword?

Greetings!

Rexp parameterization (Injection Attack Risk)

We have an API that uses Pypher to interact with Neo4j. We wrote a little partial string matching search endpoint recently that I discovered was vulnerable to a Cypher injection attack.

Super rough Example:

@flask.route('/search')
@use_kwargs({'query': String})
def search_nodes(query):
    q = PyhperQuery()
    
    q.MATCH.node('a').WHERE.a.property('name').rexp(
        "'(?i).*{}.*'".format(query)
    ).RETURN.a

    ... execute query

The following URL will exploit this endpoint to set an attack variable on any node matching dogs:

http://localhost:5000/search?query=dogs.*' SET a.attack = 44 WITH a as throw_away MATCH (a) WHERE a.`name` =~ '(?i).*dogs

Using .operator('=~', '(?i).*{}.*'.format(query)) closes this vector. This seems to be because Pypher's Rexp class doesn't accept bound params? Is there a reason for this?
Can we change Rexp to accept bound params by default?

Operators not allow for Nested Python dictionaires to be passed in

I ran into an issue when trying to use a python dictionary to set the values for a node, when there happened to be a list with a nested dictionary in my dataset.

Example Code

args = { 'nested': [{'item': 'one'}], 'toplevel': 1}

new_node = Pypher()
new_node.MATCH.node('updated')
new_node.SET.updated += args
new_node.RETURN('updated')
str(new_node)

Results

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-26-3a12a1ff433a> in <module>
      6 new_node.SET.updated += args
      7 new_node.RETURN('updated')
----> 8 str(new_node)

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in __str__(self)
    235 
    236     def __str__(self):
--> 237         return self.__unicode__()
    238 
    239     def __unicode__(self):

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in __unicode__(self)
    265                 suff = ' '
    266 
--> 267             part = '{}{}{}'.format(pre, str(token), suff)
    268             tokens.append(part)
    269 

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in __str__(self)
    235 
    236     def __str__(self):
--> 237         return self.__unicode__()
    238 
    239     def __unicode__(self):

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in __unicode__(self)
    881                 return new
    882 
--> 883             value = params(self.value)
    884         elif self._BIND_PARAMS:
    885             param = self.bind_param(self.value)

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in params(item)
    861                     for k, v in item.items():
    862                         if isinstance(v, (list, set, tuple, dict)):
--> 863                             v = params(v)
    864                         elif self._BIND_PARAMS:
    865                             param = self.bind_param(v)

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in params(item)
    872                     for v in item:
    873                         if self._BIND_PARAMS:
--> 874                             param = self.bind_param(v)
    875                             v = param.placeholder
    876 

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in bind_param(self, value, name)
    206 
    207     def bind_param(self, value, name=None):
--> 208         return self.params.bind_param(value=value, name=name)
    209 
    210     def __getattr__(self, attr):

~/sandbox/krawler/.venv/lib/python3.6/site-packages/pypher/builder.py in bind_param(self, value, name)
    114                     name = k
    115                     break
--> 116         elif value in self._bound_params.keys():
    117             for k, v in self._bound_params.items():
    118                 if k == value:

TypeError: unhashable type: 'dict'

function in rhs of attribute setting?

I am trying to create the Pypher equivalent of the cypher:

MERGE (p:person { lastname: coalesce(var, 'unknown last name')}) ...

where var could be some last name, including null, substituted in at run time in python.

The problem is the coalesce function on the rhs of the attribute.

without the coalesce function
lastname=Param('lastname, var)
works, but how do I add the neo4j function in there. I tried

lastname= Param('lastname', __coalesce(var, 'unknown last name'))
but this gives a Pypher object in the bound parameters which doesn't seem right.

Any ideas anyone.

Plugin Compatibility?

Love it, came here via Neo4J blog.
Is it possible to use Pypher with APOC or spatial or other jar plugins? Is there a recommended 'extension' path for adding that support?

Cheers

!= Operator should output <>

Hi,

I think the cypher output for the != operator should be <> not != as this is not valid cypher.

For example,

MATCH.node('p','person').WHERE.p.property('name') != 'me' ...
should return cypher of
MATCH (p:person) WHERE p.name <> 'me' ...
and not
MATCH (p:person) WHERE p.name != 'me' ...

This workaround will of course produce the same result.
MATCH.node('p','person').WHERE.NOT.p.property('name') != 'me'

Missing NOT

I don't see in the readme references to NOT and AND NOT

.Not is currently working, it's just not all uppercased on output like the other statements.

stmt = Pypher()
stmt.Match.node('u', 'User', username='chad')
stmt.Match.node('u2', 'User', username='sam')
stmt.Where.Not.node('u').rel_out(labels='FOLLOW').node('u2').And.Not.node('u').rel_in(labels='FOLLOW').node('u2')
print(str(stmt))

Note: For demonstration purposes not using a .rel(labels='FOLLOW')

Dynamic Variable Names

Thank you for this package.
I wanted to know if the nodes can be specified by strings

For example, I want to write a query

MATCH (n) WHERE n.root = "Something"

Now, I can do this using
p.Match.node('n').Where.n.property('root') == "Something"
However, I have to do this for many variables for a longer pattern in MATCH, and the name n is generated dynamically,
for example it could be n1, n2, n3 etc depending on which iteration of loop it is in. Is it possible to achieve that?

I tried giving Where.raw('n') and Where._('n') but both insert a STATEMENT word in the query.

(Apologies if this is a stupid question)

.`len` prefix and suffix when constructing pypher query in Python Console

I've tried to construct the example from the README.md in the Python Console and observed some strange prefix and suffix when converting the pypher object to a string. It seems that multiple .len terms are appended as prefix and suffix.

When executing the example instruction in a python script everything works as expected.

>>> from pypher import Pypher
>>> q = Pypher()
>>> q.Match.node('mark', labels='Person').WHERE.mark.property('name') == 'Mark'
<pypher.builder.Pypher object at 0x000002B3129C9AF0>
>>> q.RETURN.mark
<pypher.builder.Pypher object at 0x000002B3129C9AF0>
>>> str(q)
'.`len`.`len`.`len`.`len` MATCH MATCH (mark:`Person`) WHERE mark.`name` = $NEO_2361e_0.`len`.`len`.`len`.`len` RETURN RETURN mark.`len`.`len`.`len`.`len`'
>>> q.bound_params
OrderedDict([('NEO_2361e_0', 'Mark')])

system information:

  • Windows 10
  • Python 3.8.5
  • Pypher 0.18.1

Pypher behaves strangely in Pycharm Debug mode

ran with python 3.7.0 on both windows and mac versions, here is an output example:

str(tmp)
'MATCH (yo:`label`) shape shape.`len` shape shape.`len`'
str(tmp)
'MATCH (yo:`label`) shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len`'
str(tmp)
'MATCH (yo:`label`) shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len` shape shape.`len`'```


Each call to `__str__` adds more of the garbage strings. it doesn't happen in a regular run mode

[On Create] AttributeError: 'str' object has no attribute

I've pip installed python-cypher==0.14.3 and was trying to run through some of the examples to figure out how this module works when I ran into an issue.

p = Pypher()
p.MERGE.node('user', 'User', Id=456).ON.CREATE.user.SET(__.user.__Name__ == 'Jim')
print(p)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-a88f1b7b8be9> in <module>
      1 p = Pypher()
----> 2 p.MERGE.node('user', 'User', Id=456).ON.CREATE.user.SET(__.user.__Name__ == 'Jim')
      3 print(p)

AttributeError: 'str' object has no attribute 'user'

Question: Apoc Functions

How would you write:

MATCH (u:User {username: 'chad'}) 
WITH apoc.text.join([u.first_name, u.last_name], ' ') AS full_name
RETURN full_name

Thanks!

Unwind example

First of all, amazing work putting this library together, it is super helpful!

I'm trying to create an UNWIND statement passing it a list of items to unwind.
My goal is to have something like this:

UNWIND $batch AS row
where $batch is a list of dicts => [{...}, {...}, {...}]

Unfortunately I'm not able to set a list as a valid parameter (unhashable type list). This is my attempt:

batch = [dict(city='hamburg', members=300), dict(city='berlin', members=900), dict(city='koln', members=190)]

unwind_q = Pypher()
batch_p = Param(name='batch', value=batch)
unwind_q.unwind(batch_p).alias('row')

Initially it does not crash but it does crash when trying to see the actual cypher query with the following error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-156-1bd1789a4c0a> in <module>
      6 unwind_q.unwind(batch_p).alias('row')
      7 
----> 8 cprint(unwind_q)

<ipython-input-67-e7bd6c8eb90e> in cprint(pypher)
     27 
     28 def cprint(pypher):
---> 29     c = str(pypher)
     30     p = pypher.bound_params
     31     print('Cypher:')

/usr/local/lib/python3.7/site-packages/pypher/builder.py in __str__(self)
    323 
    324     def __str__(self):
--> 325         return self.__unicode__()
    326 
    327     def __unicode__(self):

/usr/local/lib/python3.7/site-packages/pypher/builder.py in __unicode__(self)
    353                 suff = ' '
    354 
--> 355             part = '{}{}{}'.format(pre, str(token), suff)
    356             tokens.append(part)
    357 

/usr/local/lib/python3.7/site-packages/pypher/builder.py in __str__(self)
    323 
    324     def __str__(self):
--> 325         return self.__unicode__()
    326 
    327     def __unicode__(self):

/usr/local/lib/python3.7/site-packages/pypher/builder.py in __unicode__(self)
    636                     arg.parent = self.parent
    637                 elif isinstance(arg, Param):
--> 638                     self.bind_param(arg)
    639                     arg = arg.placeholder
    640 

/usr/local/lib/python3.7/site-packages/pypher/builder.py in bind_param(self, value, name)
    294     def bind_param(self, value, name=None):
    295         self.params.pypher = self
--> 296         return self.params.bind_param(value=value, name=name)
    297 
    298     def __getattr__(self, attr):

/usr/local/lib/python3.7/site-packages/pypher/builder.py in bind_param(self, value, name)
    196                     name = k
    197                     break
--> 198         elif bind and value in self._bound_params.keys():
    199             for k, v in self._bound_params.items():
    200                 if k == value:

TypeError: unhashable type: 'list'

Any ideas on how could I pass a list to UNWIND?

Support for union and call

Hello, Im wondering if Pypher suports union and call statements. I would like to achieve something like this:

CALL{
	MATCH (result:`A`)<-[:`rel_a`]-(x) return result
	UNION
	MATCH (result:`B`)<-[:`rel_b`]-(x) return result
}

If not, I guess I can implement them through raw function. Am I right?

Thanks for your work,
greetings!

Custom class functions

Is there a way to create custom functions by subclassing Func or FuncRaw?
I noticed on the readme there's a section about it but I am not able to reproduce it.
This is may attempt

# Define de function
from pypher.builder import Func
class ApocIterate(Func):
  _CAPITALIZE = False
  _ALIASES = ['periodic_iterate', 'apoc_periodic_iterate']
  name = 'apoc.periodic.iterate'

# Attempt to use it
from pypher import Pypher
q = Pypher()
q.apoc_iterate

# Throws the following error
/usr/local/lib/python3.7/site-packages/pypher/builder.py in __getattr__(self, attr)
    302             link = Property(name=attr.strip('__'))
    303         elif attr_low in _LINKS:
--> 304             link = (getattr(_MODULE, _LINKS[attr_low]))()
    305         else:
    306             link = Statement(name=attr)

AttributeError: module 'pypher.builder' has no attribute 'ApocIterate'

I know we can define custom functions using create_function; but I'd like to define them as classes. Is this indeed supported?

name is a reserved map name

When I do:

p.With(__.collect(__.Distinct(__.map_projection('u', '.*',  name=__.apoc_text_join(__.List(__.u.__first_name__, __.u.__last_name__), "' '")))))

I get:

__init__() got multiple values for argument 'name'

SET a dict

Hello,

First off, thanks for this project. I started to write one today. Then stumbled upon this, and stopped mine.

How would I write:

CREATE (u:User) SET u = {props};

or

MATCH (u:User {username: {username}}) SET u += {props};

Thanks again!

missing aggregate functions

Hi,

I'm mssing aggregate function like max.

PLease could someone provide an example?

my query is
MATCH (i:iPerf)<-[r:HasiPerf]-(s:School) RETURN s.eduid as eduid , max(i.time) as time ORDER BY time desc

regards
hudecof

Quoting label names

I am trying to construct queries for another Cypher compatible database, specifically AgensGraph, and the first problem I ran into is the quotation of labels when creating nodes or edges.

The issue is that this particular dialect doesn't like the ` quotation mark, it would prefer double quotes or no quotes.

WHERE START WITH do not quote the string

Hi,

I'm trying to use WHERE and STARTS WITH, but the string is not quoted. Maybe my code is wrong, but ...

...
    if name is not None:
        where_device.append(__.device.__hostname__.STARTS.WITH(str(name)))

    q.Match.node('school', labels='School', **params_school)
    q.rel_out(labels='HasDevice')
    q.node('device', labels="Device", **params_device)
    if where_device:
        q.WHERE.CAND(*where_device)
...

Generated code is

"MATCH (school:`School`)-[:`HasDevice`]->(device:`Device` {`type`: $type60639_0}) WHERE (device.`hostname` STARTS WITH sw01) OPTIONAL MATCH (device)-[:`HasSN`]->(sn:`Sn`) OPTIONAL MATCH (sn)-[:`IsModel`]->(model:`Model`) RETURN school, device, sn, model"

Exception

py2neo.database.ClientError: SyntaxError: Variable `sw01` not defined (line 1, column 119 (offset: 118))

None in Python and NULL in Pypher inconsistency?

I think I've found an inconsistency in handling NULL attributes in Pypher:

p = Pypher()
p.MERGE.node('foo', 'baz').OnCreate.SET(__.bar == None)
cprint(p)

Produces:

Cypher:
MERGE (foo:`baz`) ON CREATE SET bar = $NEO_65fc3_0
Bound Params:
{'NEO_65fc3_0': 'null'}

Note that bar is a string containing the word null.

But the equivalent code in CYPHER for the same intent would be

MERGE (foo:`baz`) ON CREATE SET foo.bar == NULL

which creates node with no attribute bar,

rather than this

MERGE (foo:`baz`) ON CREATE SET foo.bar == 'null'

which creates node with attribute bar = 'null'
which is what the Pypher version does. I'm trying to create equivalent Pypher so that no attribute in neo4j is created when the Python type is None. Am I just confused?

Question: use with neo4j-driver

I'm using the Neo4j Python driver, example code below. The driver requires the params in Cypher to be surrounded by parentheses.

Is there a way to configure?

Thanks!
Chad.

p = Pypher()
p.CREATE.node('user', 'User', Name='Jim')

 with driver.session() as  session:
       session.run(str(p), **dict(p.bound_params))

Raises:

neo4j.exceptions.CypherSyntaxError: Parentheses are required to identify nodes in patterns

Multiple types for relationships

I think your code will not work with multiple types for relationships.

Since both Node and Relationship are children of Entity, and Entity uses : to delimit labels, your rels will be written like nodes, i.e. [name:TYPE1:TYPE2] which I think is not supported. Instead, it should be [name:TYPE1|TYPE2]. It's because a rel in Neo4j can have only one type, so you can only match them with OR, not AND.

(Inversadly, nodes only support AND and not OR, so you cannot do (name:LABEL1|LABEL2), only (name:LABEL1:LABEL2)).

If you separate the related code from Entity into Node and Relationship, maybe you could also rename Relationship's labels to types, to better match the Neo4j concepts 😄

IN statement doesn't produce correct output when combined with partial statement returning a list

I want to write something like:

p1 = Pypher()
p2 = Pypher()
p2.split(<some input>, ",")
p1.IN(p2)

But the way the class IN(Statement) is defined means that this will come out unnecessarily wrapped in square braces and the cypher engine will fall over when it gets a list of lists.

I think this could be fixed by adding some logic to the class definition of IN and am happy to do the work over the weekend to raise a PR but wondered if you had a preference for how to fix given you know + understand the codebase far better than me? I would do something like checking if the partial statement contains a split() or collect() but I feel like there's a lot of scope for missing cases there and seems not very elegant.

Also possible I've misunderstood how to make a partial properly and there's a way to do what I want without changing the code.

Substitute params to construct complete neo4j query

Does Pypher provide functionality of substituting params into cypher query?

Current output of Pypher is this:

Cypher:
MATCH (user:`User`) WHERE user.`id` = $NEO_f4755_0 SET user.`Age` = $NEO_f4755_1

Bound Params:
{'$NEO_f4755_0': 123, '$NEO_f4755_1': 25}

What I want to have cypher query like this:

MATCH (user:`User`) WHERE user.`id` = 123 SET user.`Age` = 25

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.