├── LICENSE ├── .gitignore ├── .gitattributes ├── gqlite-capi ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── features ├── README.md ├── supported │ ├── WhereAcceptance.feature │ ├── Create.feature │ ├── Literals.feature │ ├── Aggregation.feature │ └── ReturnAcceptance.feature └── backlog │ ├── PathEquality.feature │ ├── JoinAcceptance.feature │ ├── VarLengthAcceptance2.feature │ ├── SyntaxErrorAcceptance.feature │ ├── ColumnNameAcceptance.feature │ ├── StartingPointAcceptance.feature │ ├── ListComprehension.feature │ ├── LargeIntegerEquality.feature │ ├── OptionalMatch.feature │ ├── UnionAcceptance.feature │ ├── NullOperator.feature │ ├── ExpressionAcceptance.feature │ ├── RemoveAcceptance.feature │ ├── Comparability.feature │ ├── TernaryLogicAcceptance.feature │ ├── NullAcceptance.feature │ ├── EqualsAcceptance.feature │ ├── KeysAcceptance.feature │ ├── MergeIntoAcceptance.feature │ ├── TemporalToStringAcceptance.feature │ ├── ComparisonOperatorAcceptance.feature │ ├── MiscellaneousErrorAcceptance.feature │ ├── TemporalParseAcceptance.feature │ ├── SkipLimitAcceptance.feature │ ├── LabelsAcceptance.feature │ ├── TemporalComparisonAcceptance.feature │ ├── TemporalAccessorAcceptance.feature │ ├── UnwindAcceptance.feature │ ├── ReturnAcceptance.feature │ └── SetAcceptance.feature ├── Cargo.toml ├── src ├── main.rs ├── backend │ ├── gram.pest │ └── mod.rs ├── cypher.pest └── lib.rs ├── bin └── python-example.py ├── README.md └── .github └── workflows └── checks.yml /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 All rights reserved -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | *.iml 4 | graph.gram 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ignore the features folder for language detection in Github 2 | # See https://github.com/github/linguist#using-gitattributes 3 | features/* linguist-vendored 4 | -------------------------------------------------------------------------------- /gqlite-capi/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "gqlite-capi" 5 | version = "0.1.0" 6 | 7 | -------------------------------------------------------------------------------- /features/README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Tests 2 | 3 | The feature files are all from the OpenCypher Technology Compatibility Kit available from https://www.opencypher.org/resources (downloaded February 2020). 4 | 5 | Move feature files from the `backlog/` directory to the `supported/` directory to include them in testing. 6 | 7 | To run cucumber tests simply `cargo test` 8 | -------------------------------------------------------------------------------- /gqlite-capi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gqlite-capi" 3 | version = "0.1.0" 4 | authors = ["Jacob Davis-Hansson "] 5 | edition = "2018" 6 | description = """ 7 | A C API for gqlite 8 | """ 9 | workspace = ".." 10 | 11 | [lib] 12 | name = "gqlite" 13 | crate-type = ["staticlib", "cdylib"] 14 | 15 | [dependencies] 16 | libc = "0.2" 17 | gqlite = { version = "0", path = ".." } 18 | lazy_static = "1.4.0" 19 | uuid = "0.8" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gqlite" 3 | version = "0.1.0" 4 | authors = ["Jacob Davis-Hansson "] 5 | edition = "2018" 6 | 7 | 8 | [workspace] 9 | members = [ 10 | "gqlite-capi", 11 | ] 12 | 13 | 14 | [lib] 15 | name = "gqlite" 16 | crate-type = ["rlib"] 17 | 18 | [[bin]] 19 | name = "g" 20 | path = "src/main.rs" 21 | doc = false 22 | 23 | [dependencies] 24 | anyhow = "1.0" 25 | clap = { version = "2.33.0", optional = true } 26 | json = { version = "0.12", optional = true } 27 | pest = "2.0" 28 | pest_derive = "2.0" 29 | rand = { version = "0.7", optional = true } 30 | serde = { version = "1.0", optional = true } 31 | serde_yaml = { version = "0.8", optional = true } 32 | uuid = { version = "0.8", features = ["v1"], optional = true } 33 | 34 | [features] 35 | default = ["gram", "cli"] 36 | cli = ["clap"] 37 | gram = ["json", "rand", "serde", "serde_yaml", "uuid"] 38 | 39 | [dev-dependencies] 40 | cucumber = { package = "cucumber_rust", version = "^0.6.0" } 41 | tempfile = "3.1.0" 42 | 43 | [[test]] 44 | name = "cucumber" 45 | required-features = ["gram"] 46 | harness = false # Allows Cucumber to print output instead of libtest 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> anyhow::Result<()> { 2 | #[cfg(all(feature = "cli", feature = "gram"))] 3 | { 4 | use clap::{App, AppSettings}; 5 | use gqlite::gramdb::GramDatabase; 6 | use std::fs::OpenOptions; 7 | 8 | let matches = App::new("g") 9 | .version("0.0") 10 | .about("A graph database in a gram file!") 11 | .setting(AppSettings::ArgRequiredElseHelp) 12 | .args_from_usage( 13 | "-f, --file=[FILE] @graph.gram 'Sets the gram file to use' 14 | -h, --help 'Print help information' 15 | 'Query to execute'", 16 | ) 17 | .get_matches(); 18 | 19 | let query_str = matches.value_of("QUERY").unwrap(); 20 | let path = matches.value_of("file").unwrap_or("graph.gram"); 21 | let file = OpenOptions::new() 22 | .create(true) 23 | .write(true) 24 | .read(true) 25 | .open(path)?; 26 | 27 | let mut db = GramDatabase::open(file)?; 28 | let mut cursor = db.new_cursor(); 29 | db.run(query_str, &mut cursor)?; 30 | 31 | while let Some(row) = cursor.next()? { 32 | let mut first = true; 33 | for v in &row.slots { 34 | if first { 35 | print!("{}", v); 36 | first = false 37 | } else { 38 | print!(", {}", v) 39 | } 40 | } 41 | println!() 42 | } 43 | } 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/backend/gram.pest: -------------------------------------------------------------------------------- 1 | 2 | WHITESPACE = _{ " " | "\t" | "\r" | "\n" } 3 | 4 | expr = { string | id | num } 5 | 6 | id = { ("`" ~ id_inner ~ "`" ) | id_noticks } 7 | 8 | id_noticks = @{ ( ASCII_ALPHA | "_" | "-" ) ~ ( ASCII_ALPHANUMERIC | "_" | "-" )* } 9 | id_inner = @{ id_char* } 10 | id_char = { 11 | !("`" | "\\") ~ ANY 12 | | "\\" ~ ("`" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") 13 | } 14 | 15 | string = ${ 16 | "\"" ~ dblq_str_inner ~ "\"" | 17 | "'" ~ singleq_str_inner ~ "'" } 18 | dblq_str_inner = @{ dbldq_char* } 19 | dbldq_char = { 20 | !("\"" | "\\") ~ ANY 21 | | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") 22 | } 23 | singleq_str_inner = @{ singleq_char* } 24 | singleq_char = { 25 | !("'" | "\\") ~ ANY 26 | | "\\" ~ ("'" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") 27 | } 28 | 29 | num = { int ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } 30 | int = { ("+" | "-")? ~ ASCII_DIGIT+ } 31 | 32 | map = { 33 | "{" ~ "}" | 34 | "{" ~ map_pair ~ ("," ~ map_pair)* ~ "}" 35 | } 36 | map_pair = { id ~ ":" ~ expr } 37 | 38 | 39 | node = { "(" ~ id? ~ ( ":" ~ label )* ~ map? ~ ")" } 40 | 41 | label = ${ id } 42 | 43 | rel = { "<"? ~ "-" ~ ( "[" ~ id? ~ ( ":" ~ rel_type )? ~ map? ~ "]" )? ~ "-" ~ ">"? } 44 | rel_type = ${ id } 45 | 46 | path = { node ~ ( rel ~ node )+ } 47 | // 48 | // projection = { expr ~ ("AS" ~ id)? } 49 | // projections = { projection ~ ( "," ~ projection )* } 50 | // 51 | // create_stmt = { "CREATE" ~ pattern } 52 | // match_stmt = { "MATCH" ~ pattern } 53 | // return_stmt = { "RETURN" ~ projections } 54 | // 55 | // statement = _{ create_stmt | match_stmt } 56 | 57 | // (`Napoleon` {name: "Napoleon", group:1}) 58 | 59 | gram = { SOI ~ ( path | node ) * ~ EOI } -------------------------------------------------------------------------------- /features/supported/WhereAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: WhereAcceptance 32 | 33 | Scenario: NOT and false 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({name: 'a'}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n) 42 | WHERE NOT(n.name = 'apa' AND false) 43 | RETURN n 44 | """ 45 | Then the result should be, in any order: 46 | | n | 47 | | ({name: 'a'}) | 48 | And no side effects 49 | -------------------------------------------------------------------------------- /bin/python-example.py: -------------------------------------------------------------------------------- 1 | from ctypes import cdll, c_wchar_p, c_char_p, c_uint32, Structure, POINTER 2 | from sys import platform 3 | import os 4 | 5 | # Create a class 'wrapper' for opaque structs from rust 6 | class OpaqueStruct(Structure): 7 | pass 8 | 9 | if platform == 'darwin': 10 | prefix = 'lib' 11 | ext = 'dylib' 12 | elif platform == 'win32': 13 | prefix = '' 14 | ext = 'dll' 15 | else: 16 | prefix = 'lib' 17 | ext = 'so' 18 | 19 | # fetch the directory of this file 20 | dir_path = os.path.dirname(os.path.realpath(__file__)) 21 | # get the parent directory (project root) 22 | parent_dir = os.path.dirname(dir_path) 23 | 24 | lib = cdll.LoadLibrary(parent_dir + '/target/debug/{}gqlite.{}'.format(prefix, ext)) 25 | 26 | 27 | # Declare response type for gqlite_open 28 | lib.gqlite_open.restype = POINTER(OpaqueStruct) 29 | 30 | db = lib.gqlite_open(c_char_p((parent_dir + "/miserables.gram").encode('utf-8'))) 31 | 32 | 33 | # Declare argument types for gqlite_last_error 34 | lib.gqlite_last_error.argtypes = (POINTER(OpaqueStruct), ) 35 | 36 | if lib.gqlite_last_error(db) != 0: 37 | print("error opening db") 38 | os.exit(1) 39 | 40 | 41 | # Declare response type for gqlite_cursor_new 42 | lib.gqlite_cursor_new.restype = POINTER(OpaqueStruct) 43 | 44 | cursor = lib.gqlite_cursor_new(db) 45 | 46 | 47 | # Declare the argument types for gqlite_run 48 | lib.gqlite_run.argtypes = (POINTER(OpaqueStruct), POINTER(OpaqueStruct), c_char_p) 49 | 50 | res = lib.gqlite_run(db, cursor, c_char_p("MATCH (n:Person) RETURN n.name".encode('utf-8'))) 51 | print(f"run => {res}") 52 | 53 | 54 | # Declare the argument types for gqlite_cursor_next 55 | lib.gqlite_cursor_next.argtypes = (POINTER(OpaqueStruct), ) 56 | 57 | res = lib.gqlite_cursor_next(cursor) 58 | print(f"cursor.next => {res}") 59 | 60 | 61 | # Declare the argument types for gqlite_close 62 | lib.gqlite_close.argtypes = (POINTER(OpaqueStruct), ) 63 | 64 | res = lib.gqlite_close(db) 65 | print(f"db.close => {res}") -------------------------------------------------------------------------------- /features/backlog/PathEquality.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: PathEquality 32 | 33 | Scenario: Direction of traversed relationship is not significant for path equality, simple 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (n:A)-[:LOOP]->(n) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH p1 = (:A)-->() 42 | MATCH p2 = (:A)<--() 43 | RETURN p1 = p2 44 | """ 45 | Then the result should be, in any order: 46 | | p1 = p2 | 47 | | true | 48 | And no side effects 49 | -------------------------------------------------------------------------------- /features/backlog/JoinAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: JoinAcceptance 32 | 33 | Scenario: Find friends of others 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (:A {id: 1}), 38 | (:A {id: 2}), 39 | (:B {id: 2}), 40 | (:B {id: 3}) 41 | """ 42 | When executing query: 43 | """ 44 | MATCH (a:A), (b:B) 45 | WHERE a.id = b.id 46 | RETURN a, b 47 | """ 48 | Then the result should be, in any order: 49 | | a | b | 50 | | (:A {id: 2}) | (:B {id: 2}) | 51 | And no side effects 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Structure 3 | 4 | gqlite exposes three main API surfaces 5 | 6 | - the `gqlite` rust library, at [src/lib.rs] 7 | - the `g` program, at [src/main.rs] 8 | - the libgqlite c bindings to the rust library, at [gqlite-capi/src/lib.rs] 9 | 10 | The `g` program and the libgqlite c bindings are both wrappers around the gqlite rust library. 11 | 12 | ### Internal structure 13 | 14 | gqlite is organized into a "frontend" and a "backend". The frontend contains the parser and planner. 15 | Backends contain storage and provides executable implementations of the logical operators emitted by the frontend. 16 | 17 | # Getting Started 18 | 19 | To build everything, ensure that you have Cargo and Rust installed. 20 | 21 | ## Build 22 | 23 | ``` 24 | cargo build 25 | ``` 26 | 27 | ## Run 28 | 29 | The repo comes with a small graph in gram file format, representing the characters in Les Miserables. 30 | To run a "hello world" example, let's apply a simple cypher query that pulls out character names. 31 | 32 | ``` 33 | $ ./target/debug/g -f miserables.gram 'MATCH (n:Person) RETURN n.name' 34 | built pg: PatternGraph { e: {8: PatternNode { identifier: 8, labels: [0], props: [], solved: false }}, e_order: [8], v: [] } 35 | plan: Return { src: NodeScan { src: Argument, slot: 0, labels: Some(0) }, projections: [Projection { expr: Prop(Slot(0), [2]), alias: 9, dst: 1 }] } 36 | ---- 37 | 9 38 | ---- 39 | "Napoleon" 40 | "Myriel" 41 | "Mlle.Baptistine" 42 | "Mme.Magloire" 43 | "CountessdeLo" 44 | "Geborand" 45 | ``` 46 | 47 | ## Test 48 | 49 | ``` 50 | $ cargo test 51 | (...) 52 | test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 53 | ``` 54 | 55 | # Limitations 56 | 57 | This code is currently under development and only supports a small subset of the Cypher language. Trying certain 58 | cypher queries may result in errors about "The gram backend does not support this expression type yet" or other syntax 59 | errors. 60 | 61 | The subset of Cypher that is currently supported is best described by the grammar found in `src/backend`, and should 62 | expand over time. 63 | 64 | # License 65 | 66 | This is not (yet) available under an open source license, the source is simply available for reading. -------------------------------------------------------------------------------- /features/backlog/VarLengthAcceptance2.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: VarLengthAcceptance2 32 | 33 | Scenario: Handling relationships that are already bound in variable length paths 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (n0:Node), 38 | (n1:Node), 39 | (n2:Node), 40 | (n3:Node), 41 | (n0)-[:EDGE]->(n1), 42 | (n1)-[:EDGE]->(n2), 43 | (n2)-[:EDGE]->(n3) 44 | """ 45 | When executing query: 46 | """ 47 | MATCH ()-[r:EDGE]-() 48 | MATCH p = (n)-[*0..1]-()-[r]-()-[*0..1]-(m) 49 | RETURN count(p) AS c 50 | """ 51 | Then the result should be, in any order: 52 | | c | 53 | | 32 | 54 | And no side effects 55 | -------------------------------------------------------------------------------- /features/backlog/SyntaxErrorAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: SyntaxErrorAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: Using a non-existent function 37 | When executing query: 38 | """ 39 | MATCH (a) 40 | RETURN foo(a) 41 | """ 42 | Then a SyntaxError should be raised at compile time: UnknownFunction 43 | 44 | Scenario: Using `rand()` in aggregations 45 | When executing query: 46 | """ 47 | RETURN count(rand()) 48 | """ 49 | Then a SyntaxError should be raised at compile time: NonConstantExpression 50 | 51 | Scenario: Supplying invalid hexadecimal literal 1 52 | When executing query: 53 | """ 54 | RETURN 0x23G34 55 | """ 56 | Then a SyntaxError should be raised at compile time: InvalidNumberLiteral 57 | 58 | Scenario: Supplying invalid hexadecimal literal 2 59 | When executing query: 60 | """ 61 | RETURN 0x23j 62 | """ 63 | Then a SyntaxError should be raised at compile time: InvalidNumberLiteral 64 | -------------------------------------------------------------------------------- /features/backlog/ColumnNameAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ColumnNameAcceptance 32 | 33 | Background: 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE () 38 | """ 39 | 40 | Scenario: Keeping used expression 1 41 | When executing query: 42 | """ 43 | MATCH (n) 44 | RETURN cOuNt( * ) 45 | """ 46 | Then the result should be, in any order: 47 | | cOuNt( * ) | 48 | | 1 | 49 | And no side effects 50 | 51 | Scenario: Keeping used expression 2 52 | When executing query: 53 | """ 54 | MATCH p = (n)-->(b) 55 | RETURN nOdEs( p ) 56 | """ 57 | Then the result should be, in any order: 58 | | nOdEs( p ) | 59 | And no side effects 60 | 61 | Scenario: Keeping used expression 3 62 | When executing query: 63 | """ 64 | MATCH p = (n)-->(b) 65 | RETURN coUnt( dIstInct p ) 66 | """ 67 | Then the result should be, in any order: 68 | | coUnt( dIstInct p ) | 69 | | 0 | 70 | And no side effects 71 | 72 | Scenario: Keeping used expression 4 73 | When executing query: 74 | """ 75 | MATCH p = (n)-->(b) 76 | RETURN aVg( n.aGe ) 77 | """ 78 | Then the result should be, in any order: 79 | | aVg( n.aGe ) | 80 | | null | 81 | And no side effects 82 | -------------------------------------------------------------------------------- /features/supported/Create.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: Create 32 | 33 | Scenario: Creating a node 34 | Given any graph 35 | When executing query: 36 | """ 37 | CREATE () 38 | """ 39 | Then the result should be empty 40 | And the side effects should be: 41 | | +nodes | 1 | 42 | 43 | Scenario: Creating two nodes 44 | Given any graph 45 | When executing query: 46 | """ 47 | CREATE (), () 48 | """ 49 | Then the result should be empty 50 | And the side effects should be: 51 | | +nodes | 2 | 52 | 53 | Scenario: Creating two nodes and a relationship 54 | Given any graph 55 | When executing query: 56 | """ 57 | CREATE ()-[:TYPE]->() 58 | """ 59 | Then the result should be empty 60 | And the side effects should be: 61 | | +nodes | 2 | 62 | | +relationships | 1 | 63 | 64 | Scenario: Creating a node with a label 65 | Given an empty graph 66 | When executing query: 67 | """ 68 | CREATE (:TheLabel) 69 | """ 70 | Then the result should be empty 71 | And the side effects should be: 72 | | +nodes | 1 | 73 | | +labels | 1 | 74 | 75 | Scenario: Creating a node with a property 76 | Given any graph 77 | When executing query: 78 | """ 79 | CREATE ({ok: true}) 80 | """ 81 | Then the result should be empty 82 | And the side effects should be: 83 | | +nodes | 1 | 84 | | +properties | 1 | 85 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Commit/PR Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: stable 21 | override: true 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: check 25 | args: --all --all-features --verbose 26 | 27 | fmt: 28 | name: Rustfmt 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v1 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: stable 36 | components: rustfmt 37 | override: true 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: fmt 41 | args: --all -- --check 42 | 43 | clippy: 44 | name: Clippy 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v1 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | profile: minimal 51 | toolchain: stable 52 | components: clippy 53 | override: true 54 | - uses: actions-rs/clippy-check@v1 55 | with: 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | args: --all --all-targets --all-features 58 | 59 | test: 60 | name: Tests 61 | needs: [check, fmt, clippy] 62 | strategy: 63 | matrix: 64 | os: [ubuntu-latest, windows-latest, macOS-latest] 65 | rust: [stable, beta, nightly] 66 | features: [--no-default-features, --all-features] 67 | runs-on: ${{ matrix.os }} 68 | steps: 69 | - uses: actions/checkout@v1 70 | - uses: actions-rs/toolchain@v1 71 | with: 72 | profile: minimal 73 | toolchain: ${{ matrix.rust }} 74 | override: true 75 | - uses: actions-rs/cargo@v1 76 | with: 77 | command: test 78 | args: --all ${{ matrix.features }} --verbose 79 | 80 | binaries: 81 | name: Binaries 82 | needs: [test] 83 | strategy: 84 | matrix: 85 | os: [ubuntu-latest, windows-latest, macOS-latest] 86 | rust: [stable] 87 | runs-on: ${{ matrix.os }} 88 | steps: 89 | - uses: actions/checkout@v1 90 | - uses: actions-rs/toolchain@v1 91 | with: 92 | profile: minimal 93 | toolchain: ${{ matrix.rust }} 94 | override: true 95 | - uses: actions-rs/cargo@v1 96 | with: 97 | command: build 98 | args: --verbose --release 99 | - uses: actions/upload-artifact@v1 100 | with: 101 | name: gqlite-${{ matrix.os }} 102 | path: target/release -------------------------------------------------------------------------------- /features/backlog/StartingPointAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: StartingPointAcceptance 32 | 33 | Scenario: Find all nodes 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({name: 'a'}), 38 | ({name: 'b'}), 39 | ({name: 'c'}) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (n) 44 | RETURN n 45 | """ 46 | Then the result should be, in any order: 47 | | n | 48 | | ({name: 'a'}) | 49 | | ({name: 'b'}) | 50 | | ({name: 'c'}) | 51 | And no side effects 52 | 53 | Scenario: Find labelled nodes 54 | Given an empty graph 55 | And having executed: 56 | """ 57 | CREATE ({name: 'a'}), 58 | (:Person), 59 | (:Animal), 60 | (:Animal) 61 | """ 62 | When executing query: 63 | """ 64 | MATCH (n:Animal) 65 | RETURN n 66 | """ 67 | Then the result should be, in any order: 68 | | n | 69 | | (:Animal) | 70 | | (:Animal) | 71 | And no side effects 72 | 73 | Scenario: Find nodes by property 74 | Given an empty graph 75 | And having executed: 76 | """ 77 | CREATE ({num: 1}), 78 | ({num: 2}) 79 | """ 80 | When executing query: 81 | """ 82 | MATCH (n) 83 | WHERE n.num = 2 84 | RETURN n 85 | """ 86 | Then the result should be, in any order: 87 | | n | 88 | | ({num: 2}) | 89 | And no side effects 90 | -------------------------------------------------------------------------------- /features/backlog/ListComprehension.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ListComprehension 32 | 33 | Scenario: Returning a list comprehension 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (a:A) 38 | CREATE (a)-[:T]->(:B), 39 | (a)-[:T]->(:C) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH p = (n)-->() 44 | RETURN [x IN collect(p) | head(nodes(x))] AS p 45 | """ 46 | Then the result should be, in any order: 47 | | p | 48 | | [(:A), (:A)] | 49 | And no side effects 50 | 51 | Scenario: Using a list comprehension in a WITH 52 | Given an empty graph 53 | And having executed: 54 | """ 55 | CREATE (a:A) 56 | CREATE (a)-[:T]->(:B), 57 | (a)-[:T]->(:C) 58 | """ 59 | When executing query: 60 | """ 61 | MATCH p = (n:A)-->() 62 | WITH [x IN collect(p) | head(nodes(x))] AS p, count(n) AS c 63 | RETURN p, c 64 | """ 65 | Then the result should be, in any order: 66 | | p | c | 67 | | [(:A), (:A)] | 2 | 68 | And no side effects 69 | 70 | Scenario: Using a list comprehension in a WHERE 71 | Given an empty graph 72 | And having executed: 73 | """ 74 | CREATE (a:A {name: 'c'}) 75 | CREATE (a)-[:T]->(:B), 76 | (a)-[:T]->(:C) 77 | """ 78 | When executing query: 79 | """ 80 | MATCH (n)-->(b) 81 | WHERE n.name IN [x IN labels(b) | toLower(x)] 82 | RETURN b 83 | """ 84 | Then the result should be, in any order: 85 | | b | 86 | | (:C) | 87 | And no side effects 88 | 89 | -------------------------------------------------------------------------------- /features/backlog/LargeIntegerEquality.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: LargeIntegerEquality 32 | 33 | Background: 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (:TheLabel {id: 4611686018427387905}) 38 | """ 39 | 40 | Scenario: Does not lose precision 41 | When executing query: 42 | """ 43 | MATCH (p:TheLabel) 44 | RETURN p.id 45 | """ 46 | Then the result should be, in any order: 47 | | p.id | 48 | | 4611686018427387905 | 49 | And no side effects 50 | 51 | Scenario: Handling inlined equality of large integer 52 | When executing query: 53 | """ 54 | MATCH (p:TheLabel {id: 4611686018427387905}) 55 | RETURN p.id 56 | """ 57 | Then the result should be, in any order: 58 | | p.id | 59 | | 4611686018427387905 | 60 | And no side effects 61 | 62 | Scenario: Handling explicit equality of large integer 63 | When executing query: 64 | """ 65 | MATCH (p:TheLabel) 66 | WHERE p.id = 4611686018427387905 67 | RETURN p.id 68 | """ 69 | Then the result should be, in any order: 70 | | p.id | 71 | | 4611686018427387905 | 72 | And no side effects 73 | 74 | Scenario: Handling inlined equality of large integer, non-equal values 75 | When executing query: 76 | """ 77 | MATCH (p:TheLabel {id : 4611686018427387900}) 78 | RETURN p.id 79 | """ 80 | Then the result should be, in any order: 81 | | p.id | 82 | And no side effects 83 | 84 | Scenario: Handling explicit equality of large integer, non-equal values 85 | When executing query: 86 | """ 87 | MATCH (p:TheLabel) 88 | WHERE p.id = 4611686018427387900 89 | RETURN p.id 90 | """ 91 | Then the result should be, in any order: 92 | | p.id | 93 | And no side effects 94 | -------------------------------------------------------------------------------- /features/backlog/OptionalMatch.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: OptionalMatch 32 | 33 | Scenario: Satisfies the open world assumption, relationships between same nodes 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (a:Player), (b:Team) 38 | CREATE (a)-[:PLAYS_FOR]->(b), 39 | (a)-[:SUPPORTS]->(b) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (p:Player)-[:PLAYS_FOR]->(team:Team) 44 | OPTIONAL MATCH (p)-[s:SUPPORTS]->(team) 45 | RETURN count(*) AS matches, s IS NULL AS optMatch 46 | """ 47 | Then the result should be, in any order: 48 | | matches | optMatch | 49 | | 1 | false | 50 | And no side effects 51 | 52 | Scenario: Satisfies the open world assumption, single relationship 53 | Given an empty graph 54 | And having executed: 55 | """ 56 | CREATE (a:Player), (b:Team) 57 | CREATE (a)-[:PLAYS_FOR]->(b) 58 | """ 59 | When executing query: 60 | """ 61 | MATCH (p:Player)-[:PLAYS_FOR]->(team:Team) 62 | OPTIONAL MATCH (p)-[s:SUPPORTS]->(team) 63 | RETURN count(*) AS matches, s IS NULL AS optMatch 64 | """ 65 | Then the result should be, in any order: 66 | | matches | optMatch | 67 | | 1 | true | 68 | And no side effects 69 | 70 | Scenario: Satisfies the open world assumption, relationships between different nodes 71 | Given an empty graph 72 | And having executed: 73 | """ 74 | CREATE (a:Player), (b:Team), (c:Team) 75 | CREATE (a)-[:PLAYS_FOR]->(b), 76 | (a)-[:SUPPORTS]->(c) 77 | """ 78 | When executing query: 79 | """ 80 | MATCH (p:Player)-[:PLAYS_FOR]->(team:Team) 81 | OPTIONAL MATCH (p)-[s:SUPPORTS]->(team) 82 | RETURN count(*) AS matches, s IS NULL AS optMatch 83 | """ 84 | Then the result should be, in any order: 85 | | matches | optMatch | 86 | | 1 | true | 87 | And no side effects 88 | -------------------------------------------------------------------------------- /src/cypher.pest: -------------------------------------------------------------------------------- 1 | 2 | WHITESPACE = _{ " " | "\t" | "\r" | "\n" } 3 | 4 | expr = { and_expr ~ (^"OR " ~ and_expr)* } 5 | and_expr = { add_sub_expr ~ (^"AND " ~ add_sub_expr)* } 6 | 7 | add_sub_expr = { mult_div_expr ~ (add_sub_op ~ mult_div_expr)* } 8 | add_sub_op = ${ "-" | "+" } 9 | 10 | mult_div_expr = { term ~ (mult_div_op ~ term)* } 11 | mult_div_op = ${ "*" | "/" } 12 | 13 | term = _{ binary_op | atom } 14 | 15 | // Need something where like blah() * b.name / 12 + count(*) is handled right 16 | binary_op = { atom ~ op ~ atom } 17 | op = ${ "=" | ">" | "<>" } 18 | 19 | atom = _{ bool | science | float | int | prop_lookup | count_call | func_call | string | param | id | list | map | "(" ~ expr ~ ")" } 20 | 21 | id = ${ ( ASCII_ALPHA | "_" | "-" ) ~ ( ASCII_ALPHANUMERIC | "_" | "-" )* } 22 | 23 | param = ${ "$" ~ id } 24 | 25 | prop_lookup = { id ~ ("." ~ id)+ } 26 | 27 | func_call = { id ~ "(" ~ (expr ~ ("," ~ expr)*)? ~ ")" } 28 | count_call = { ^"COUNT" ~ "(" ~ "*" ~ ")" } 29 | 30 | string = ${ 31 | "\"" ~ dblq_str_inner ~ "\"" | 32 | "'" ~ singleq_str_inner ~ "'" } 33 | dblq_str_inner = @{ dbldq_char* } 34 | dbldq_char = { 35 | !("\"" | "\\") ~ ANY 36 | | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") 37 | } 38 | singleq_str_inner = @{ singleq_char* } 39 | singleq_char = { 40 | !("'" | "\\") ~ ANY 41 | | "\\" ~ ("'" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") 42 | } 43 | 44 | bool = _{ lit_true | lit_false } 45 | lit_true = { ^"TRUE" } 46 | lit_false = { ^"FALSE" } 47 | 48 | int = @{ 49 | "-"? ~ ( "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* ) 50 | } 51 | 52 | float = @{ 53 | "-"? ~ ( "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* ) ~ "." ~ ASCII_DIGIT* 54 | } 55 | 56 | science = @{ 57 | (float | int) ~ ^"E" ~ int 58 | } 59 | 60 | map = { 61 | "{" ~ "}" | 62 | "{" ~ map_pair ~ ("," ~ map_pair)* ~ "}" 63 | } 64 | map_pair = { id ~ ":" ~ expr } 65 | 66 | list = { 67 | "[" ~ "]" | 68 | "[" ~ expr ~ ("," ~ expr)* ~ "]" 69 | } 70 | 71 | // Multiple labels are supported for CREATE but not yet for MATCH 72 | node = { "(" ~ id? ~ ( ":" ~ label )* ~ map? ~ ")" } 73 | label = { id } 74 | 75 | rel = { left_arrow? ~ "-" ~ ( "[" ~ id? ~ ( ":" ~ rel_type )? ~ map? ~ "]" )? ~ "-" ~ right_arrow? } 76 | rel_type = { id } 77 | left_arrow = { "<" } 78 | right_arrow = { ">" } 79 | 80 | patterns = _{ pattern ~ ( "," ~ pattern )* } 81 | pattern = { node ~ ( rel ~ node )* } 82 | 83 | projection = { expr ~ (^"AS" ~ id)? } 84 | projections = { projection ~ ( "," ~ projection )* } 85 | project_all = { "*" } 86 | 87 | distinct_clause = { ^"DISTINCT" } 88 | 89 | where_clause = { ^"WHERE" ~ expr } 90 | 91 | order_clause = { ^"ORDER BY" ~ order_expr ~ ( "," ~ order_expr )* } 92 | order_expr = { expr ~ (^"DESC" | ^"ASC")? } 93 | 94 | skip_clause = { ^"SKIP" ~ expr } 95 | limit_clause = { ^"LIMIT" ~ expr } 96 | 97 | optional_clause = { ^"OPTIONAL" } 98 | 99 | create_stmt = { ^"CREATE" ~ patterns } 100 | match_stmt = { optional_clause? ~ ^"MATCH" ~ patterns ~ where_clause? } 101 | with_stmt = { ^"WITH" ~ distinct_clause? ~ projections ~ where_clause? ~ order_clause? ~ skip_clause? ~ limit_clause? } 102 | unwind_stmt = { ^"UNWIND" ~ expr ~ ^"AS" ~ id } 103 | return_stmt = { ^"RETURN" ~ distinct_clause? ~ ( projections | project_all ) ~ order_clause? ~ skip_clause? ~ limit_clause? } 104 | 105 | statement = _{ create_stmt | match_stmt | unwind_stmt | with_stmt } 106 | query = { SOI ~ ( statement )* ~ return_stmt? ~ EOI } -------------------------------------------------------------------------------- /features/backlog/UnionAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: UnionAcceptance 32 | 33 | Scenario: Should be able to create text output from union queries 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (:A), (:B) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (a:A) 42 | RETURN a AS a 43 | UNION 44 | MATCH (b:B) 45 | RETURN b AS a 46 | """ 47 | Then the result should be, in any order: 48 | | a | 49 | | (:A) | 50 | | (:B) | 51 | And no side effects 52 | 53 | Scenario: Two elements, both unique, not distinct 54 | Given an empty graph 55 | When executing query: 56 | """ 57 | RETURN 1 AS x 58 | UNION ALL 59 | RETURN 2 AS x 60 | """ 61 | Then the result should be, in any order: 62 | | x | 63 | | 1 | 64 | | 2 | 65 | And no side effects 66 | 67 | Scenario: Two elements, both unique, distinct 68 | Given an empty graph 69 | When executing query: 70 | """ 71 | RETURN 1 AS x 72 | UNION 73 | RETURN 2 AS x 74 | """ 75 | Then the result should be, in any order: 76 | | x | 77 | | 1 | 78 | | 2 | 79 | And no side effects 80 | 81 | Scenario: Three elements, two unique, distinct 82 | Given an empty graph 83 | When executing query: 84 | """ 85 | RETURN 2 AS x 86 | UNION 87 | RETURN 1 AS x 88 | UNION 89 | RETURN 2 AS x 90 | """ 91 | Then the result should be, in any order: 92 | | x | 93 | | 2 | 94 | | 1 | 95 | And no side effects 96 | 97 | Scenario: Three elements, two unique, not distinct 98 | Given an empty graph 99 | When executing query: 100 | """ 101 | RETURN 2 AS x 102 | UNION ALL 103 | RETURN 1 AS x 104 | UNION ALL 105 | RETURN 2 AS x 106 | """ 107 | Then the result should be, in any order: 108 | | x | 109 | | 2 | 110 | | 1 | 111 | | 2 | 112 | And no side effects 113 | -------------------------------------------------------------------------------- /gqlite-capi/src/lib.rs: -------------------------------------------------------------------------------- 1 | // If the documentation is available, we should warn on this (remove the next line and this comment) 2 | #![allow(clippy::missing_safety_doc)] 3 | 4 | use gqlite::gramdb::{GramCursor, GramDatabase}; 5 | use gqlite::Error; 6 | use std::ffi::CStr; 7 | use std::fs::File; 8 | use std::os::raw::{c_char, c_int}; 9 | use std::str; 10 | 11 | #[repr(C)] 12 | #[derive(Debug)] 13 | pub struct database { 14 | pub db: Option, 15 | pub last_error: Option, 16 | } 17 | 18 | #[repr(C)] 19 | #[derive(Debug)] 20 | pub struct cursor { 21 | cursor: GramCursor, 22 | } 23 | 24 | // Open a database file 25 | #[no_mangle] 26 | pub unsafe extern "C" fn gqlite_open(raw_url: *const c_char) -> *const database { 27 | let url_bytes = CStr::from_ptr(raw_url).to_bytes(); 28 | let url: &str = str::from_utf8(url_bytes).unwrap(); 29 | 30 | match File::open(url) { 31 | Ok(file) => Box::into_raw(Box::new(database { 32 | db: Some(GramDatabase::open(file).unwrap()), 33 | last_error: None, 34 | })), 35 | Err(error) => Box::into_raw(Box::new(database { 36 | db: None, 37 | last_error: Some(error.into()), 38 | })), 39 | } 40 | } 41 | 42 | #[no_mangle] 43 | pub unsafe extern "C" fn gqlite_last_error(ptr: *const database) -> i32 { 44 | // TODO: expose error information properly to caller 45 | let db = { 46 | assert!(!ptr.is_null()); 47 | &*ptr 48 | }; 49 | 50 | if db.last_error.is_some() { 51 | let e = db.last_error.as_ref().unwrap(); 52 | println!("Error: {}", e); 53 | 1 54 | } else { 55 | 0 56 | } 57 | } 58 | 59 | // Close the database, free associated file handles and cursors 60 | #[no_mangle] 61 | pub unsafe extern "C" fn gqlite_close(ptr: *mut database) -> c_int { 62 | // from https://stackoverflow.com/questions/34754036/who-is-responsible-to-free-the-memory-after-consuming-the-box/34754403 63 | // Need to test this actually works to deallocate 64 | let mut db = Box::from_raw(ptr); 65 | db.db = None; 66 | 67 | 0 68 | } 69 | 70 | // Move cursor to next row 71 | #[no_mangle] 72 | pub unsafe extern "C" fn gqlite_cursor_close(raw_cursor: *mut cursor) { 73 | let _cur = Box::from_raw(raw_cursor); 74 | } 75 | 76 | // Allocate a new cursor for the specified database 77 | #[no_mangle] 78 | pub unsafe extern "C" fn gqlite_cursor_new(raw_db: *mut database) -> *mut cursor { 79 | let db = (*raw_db).db.as_mut().unwrap(); 80 | Box::into_raw(Box::new(cursor { 81 | cursor: db.new_cursor(), 82 | })) 83 | } 84 | 85 | // Run a query, provide result in cursor 86 | #[no_mangle] 87 | pub unsafe extern "C" fn gqlite_run( 88 | raw_db: *mut database, 89 | raw_cursor: *mut cursor, 90 | raw_query: *const c_char, 91 | ) -> c_int { 92 | let query_bytes = CStr::from_ptr(raw_query).to_bytes(); 93 | let query: &str = str::from_utf8(query_bytes).unwrap(); 94 | println!("Running {}", query); 95 | 96 | let cursor = &mut *raw_cursor; 97 | let db = (*raw_db).db.as_mut().unwrap(); 98 | 99 | match db.run(&query, &mut cursor.cursor) { 100 | Ok(_) => 0, 101 | Err(error) => { 102 | println!("err: {:?}", error); 103 | (*raw_db).last_error = Some(error); 104 | 1 105 | } 106 | } 107 | } 108 | 109 | // Move cursor to next row 110 | #[no_mangle] 111 | pub unsafe extern "C" fn gqlite_cursor_next(raw_cursor: *mut cursor) -> c_int { 112 | let cursor = &mut *raw_cursor; 113 | match cursor.cursor.next() { 114 | Ok(_) => 0, 115 | Err(e) => { 116 | println!("ERR: {:?}", e); 117 | 1 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /features/backlog/NullOperator.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: NullOperator 32 | 33 | Scenario: Property null check on non-null node 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({exists: 42}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n) 42 | RETURN n.missing IS NULL, 43 | n.exists IS NULL 44 | """ 45 | Then the result should be, in any order: 46 | | n.missing IS NULL | n.exists IS NULL | 47 | | true | false | 48 | And no side effects 49 | 50 | Scenario: Property not null check on non-null node 51 | Given an empty graph 52 | And having executed: 53 | """ 54 | CREATE ({exists: 42}) 55 | """ 56 | When executing query: 57 | """ 58 | MATCH (n) 59 | RETURN n.missing IS NOT NULL, 60 | n.exists IS NOT NULL 61 | """ 62 | Then the result should be, in any order: 63 | | n.missing IS NOT NULL | n.exists IS NOT NULL | 64 | | false | true | 65 | And no side effects 66 | 67 | Scenario: Property null check on optional non-null node 68 | Given an empty graph 69 | And having executed: 70 | """ 71 | CREATE ({exists: 42}) 72 | """ 73 | When executing query: 74 | """ 75 | OPTIONAL MATCH (n) 76 | RETURN n.missing IS NULL, 77 | n.exists IS NULL 78 | """ 79 | Then the result should be, in any order: 80 | | n.missing IS NULL | n.exists IS NULL | 81 | | true | false | 82 | And no side effects 83 | 84 | Scenario: Property not null check on optional non-null node 85 | Given an empty graph 86 | And having executed: 87 | """ 88 | CREATE ({exists: 42}) 89 | """ 90 | When executing query: 91 | """ 92 | OPTIONAL MATCH (n) 93 | RETURN n.missing IS NOT NULL, 94 | n.exists IS NOT NULL 95 | """ 96 | Then the result should be, in any order: 97 | | n.missing IS NOT NULL | n.exists IS NOT NULL | 98 | | false | true | 99 | And no side effects 100 | 101 | Scenario: Property null check on null node 102 | Given an empty graph 103 | When executing query: 104 | """ 105 | OPTIONAL MATCH (n) 106 | RETURN n.missing IS NULL 107 | """ 108 | Then the result should be, in any order: 109 | | n.missing IS NULL | 110 | | true | 111 | And no side effects 112 | 113 | Scenario: Property not null check on null node 114 | Given an empty graph 115 | When executing query: 116 | """ 117 | OPTIONAL MATCH (n) 118 | RETURN n.missing IS NOT NULL 119 | """ 120 | Then the result should be, in any order: 121 | | n.missing IS NOT NULL | 122 | | false | 123 | And no side effects 124 | -------------------------------------------------------------------------------- /features/supported/Literals.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: Literals 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: Return an integer 37 | When executing query: 38 | """ 39 | RETURN 1 AS literal 40 | """ 41 | Then the result should be, in any order: 42 | | literal | 43 | | 1 | 44 | And no side effects 45 | 46 | Scenario: Return a float 47 | When executing query: 48 | """ 49 | RETURN 1.0 AS literal 50 | """ 51 | Then the result should be, in any order: 52 | | literal | 53 | | 1.0 | 54 | And no side effects 55 | 56 | Scenario: Return a float in exponent form 57 | When executing query: 58 | """ 59 | RETURN -1e-9 AS literal 60 | """ 61 | Then the result should be, in any order: 62 | | literal | 63 | | -.000000001 | 64 | And no side effects 65 | 66 | Scenario: Return a boolean 67 | When executing query: 68 | """ 69 | RETURN true AS literal 70 | """ 71 | Then the result should be, in any order: 72 | | literal | 73 | | true | 74 | And no side effects 75 | 76 | Scenario: Return a single-quoted string 77 | When executing query: 78 | """ 79 | RETURN '' AS literal 80 | """ 81 | Then the result should be, in any order: 82 | | literal | 83 | | '' | 84 | And no side effects 85 | 86 | Scenario: Return a double-quoted string 87 | When executing query: 88 | """ 89 | RETURN "" AS literal 90 | """ 91 | Then the result should be, in any order: 92 | | literal | 93 | | '' | 94 | And no side effects 95 | 96 | Scenario: Return null 97 | When executing query: 98 | """ 99 | RETURN null AS literal 100 | """ 101 | Then the result should be, in any order: 102 | | literal | 103 | | null | 104 | And no side effects 105 | 106 | Scenario: Return an empty list 107 | When executing query: 108 | """ 109 | RETURN [] AS literal 110 | """ 111 | Then the result should be, in any order: 112 | | literal | 113 | | [] | 114 | And no side effects 115 | 116 | Scenario: Return a nonempty list 117 | When executing query: 118 | """ 119 | RETURN [0, 1, 2] AS literal 120 | """ 121 | Then the result should be, in any order: 122 | | literal | 123 | | [0, 1, 2] | 124 | And no side effects 125 | 126 | Scenario: Return an empty map 127 | When executing query: 128 | """ 129 | RETURN {} AS literal 130 | """ 131 | Then the result should be, in any order: 132 | | literal | 133 | | {} | 134 | And no side effects 135 | 136 | Scenario: Return a nonempty map 137 | When executing query: 138 | """ 139 | RETURN {k1: 0, k2: 'string'} AS literal 140 | """ 141 | Then the result should be, in any order: 142 | | literal | 143 | | {k1: 0, k2: 'string'} | 144 | And no side effects 145 | -------------------------------------------------------------------------------- /features/backlog/ExpressionAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ExpressionAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: Execute n['name'] in read queries 37 | Given having executed: 38 | """ 39 | CREATE ({name: 'Apa'}) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (n {name: 'Apa'}) 44 | RETURN n['nam' + 'e'] AS value 45 | """ 46 | Then the result should be, in any order: 47 | | value | 48 | | 'Apa' | 49 | And no side effects 50 | 51 | Scenario: Execute n['name'] in update queries 52 | When executing query: 53 | """ 54 | CREATE (n {name: 'Apa'}) 55 | RETURN n['nam' + 'e'] AS value 56 | """ 57 | Then the result should be, in any order: 58 | | value | 59 | | 'Apa' | 60 | And the side effects should be: 61 | | +nodes | 1 | 62 | | +properties | 1 | 63 | 64 | Scenario: Use dynamic property lookup based on parameters when there is no type information 65 | Given parameters are: 66 | | expr | {name: 'Apa'} | 67 | | idx | 'name' | 68 | When executing query: 69 | """ 70 | WITH $expr AS expr, $idx AS idx 71 | RETURN expr[idx] AS value 72 | """ 73 | Then the result should be, in any order: 74 | | value | 75 | | 'Apa' | 76 | And no side effects 77 | 78 | Scenario: Use dynamic property lookup based on parameters when there is lhs type information 79 | Given parameters are: 80 | | idx | 'name' | 81 | When executing query: 82 | """ 83 | CREATE (n {name: 'Apa'}) 84 | RETURN n[$idx] AS value 85 | """ 86 | Then the result should be, in any order: 87 | | value | 88 | | 'Apa' | 89 | And the side effects should be: 90 | | +nodes | 1 | 91 | | +properties | 1 | 92 | 93 | Scenario: Use dynamic property lookup based on parameters when there is rhs type information 94 | Given parameters are: 95 | | expr | {name: 'Apa'} | 96 | | idx | 'name' | 97 | When executing query: 98 | """ 99 | WITH $expr AS expr, $idx AS idx 100 | RETURN expr[toString(idx)] AS value 101 | """ 102 | Then the result should be, in any order: 103 | | value | 104 | | 'Apa' | 105 | And no side effects 106 | 107 | Scenario: Fail at runtime when attempting to index with an Int into a Map 108 | Given parameters are: 109 | | expr | {name: 'Apa'} | 110 | | idx | 0 | 111 | When executing query: 112 | """ 113 | WITH $expr AS expr, $idx AS idx 114 | RETURN expr[idx] 115 | """ 116 | Then a TypeError should be raised at runtime: MapElementAccessByNonString 117 | 118 | Scenario: Fail at runtime when trying to index into a map with a non-string 119 | Given parameters are: 120 | | expr | {name: 'Apa'} | 121 | | idx | 12.3 | 122 | When executing query: 123 | """ 124 | WITH $expr AS expr, $idx AS idx 125 | RETURN expr[idx] 126 | """ 127 | Then a TypeError should be raised at runtime: MapElementAccessByNonString 128 | 129 | Scenario: Fail at runtime when trying to index something which is not a map or list 130 | Given parameters are: 131 | | expr | 100 | 132 | | idx | 0 | 133 | When executing query: 134 | """ 135 | WITH $expr AS expr, $idx AS idx 136 | RETURN expr[idx] 137 | """ 138 | Then a TypeError should be raised at runtime: InvalidElementAccess 139 | -------------------------------------------------------------------------------- /features/supported/Aggregation.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: Aggregation 32 | 33 | Scenario: `max()` over strings 34 | Given any graph 35 | When executing query: 36 | """ 37 | UNWIND ['a', 'b', 'B', null, 'abc', 'abc1'] AS i 38 | RETURN max(i) 39 | """ 40 | Then the result should be, in any order: 41 | | max(i) | 42 | | 'b' | 43 | And no side effects 44 | 45 | Scenario: `min()` over strings 46 | Given any graph 47 | When executing query: 48 | """ 49 | UNWIND ['a', 'b', 'B', null, 'abc', 'abc1'] AS i 50 | RETURN min(i) 51 | """ 52 | Then the result should be, in any order: 53 | | min(i) | 54 | | 'B' | 55 | And no side effects 56 | 57 | Scenario: `max()` over integers 58 | Given any graph 59 | When executing query: 60 | """ 61 | UNWIND [1, 2, 0, null, -1] AS x 62 | RETURN max(x) 63 | """ 64 | Then the result should be, in any order: 65 | | max(x) | 66 | | 2 | 67 | And no side effects 68 | 69 | Scenario: `min()` over integers 70 | Given any graph 71 | When executing query: 72 | """ 73 | UNWIND [1, 2, 0, null, -1] AS x 74 | RETURN min(x) 75 | """ 76 | Then the result should be, in any order: 77 | | min(x) | 78 | | -1 | 79 | And no side effects 80 | 81 | Scenario: `max()` over floats 82 | Given any graph 83 | When executing query: 84 | """ 85 | UNWIND [1.0, 2.0, 0.5, null] AS x 86 | RETURN max(x) 87 | """ 88 | Then the result should be, in any order: 89 | | max(x) | 90 | | 2.0 | 91 | And no side effects 92 | 93 | Scenario: `min()` over floats 94 | Given any graph 95 | When executing query: 96 | """ 97 | UNWIND [1.0, 2.0, 0.5, null] AS x 98 | RETURN min(x) 99 | """ 100 | Then the result should be, in any order: 101 | | min(x) | 102 | | 0.5 | 103 | And no side effects 104 | 105 | Scenario: `max()` over mixed numeric values 106 | Given any graph 107 | When executing query: 108 | """ 109 | UNWIND [1, 2.0, 5, null, 3.2, 0.1] AS x 110 | RETURN max(x) 111 | """ 112 | Then the result should be, in any order: 113 | | max(x) | 114 | | 5 | 115 | And no side effects 116 | 117 | Scenario: `min()` over mixed numeric values 118 | Given any graph 119 | When executing query: 120 | """ 121 | UNWIND [1, 2.0, 5, null, 3.2, 0.1] AS x 122 | RETURN min(x) 123 | """ 124 | Then the result should be, in any order: 125 | | min(x) | 126 | | 0.1 | 127 | And no side effects 128 | 129 | Scenario: `max()` over mixed values 130 | Given any graph 131 | When executing query: 132 | """ 133 | UNWIND [1, 'a', null, [1, 2], 0.2, 'b'] AS x 134 | RETURN max(x) 135 | """ 136 | Then the result should be, in any order: 137 | | max(x) | 138 | | 1 | 139 | And no side effects 140 | 141 | Scenario: `min()` over mixed values 142 | Given any graph 143 | When executing query: 144 | """ 145 | UNWIND [1, 'a', null, [1, 2], 0.2, 'b'] AS x 146 | RETURN min(x) 147 | """ 148 | Then the result should be, in any order: 149 | | min(x) | 150 | | [1, 2] | 151 | And no side effects 152 | 153 | Scenario: `max()` over list values 154 | Given any graph 155 | When executing query: 156 | """ 157 | UNWIND [[1], [2], [2, 1]] AS x 158 | RETURN max(x) 159 | """ 160 | Then the result should be, in any order: 161 | | max(x) | 162 | | [2, 1] | 163 | And no side effects 164 | 165 | Scenario: `min()` over list values 166 | Given any graph 167 | When executing query: 168 | """ 169 | UNWIND [[1], [2], [2, 1]] AS x 170 | RETURN min(x) 171 | """ 172 | Then the result should be, in any order: 173 | | min(x) | 174 | | [1] | 175 | And no side effects 176 | -------------------------------------------------------------------------------- /features/backlog/RemoveAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: RemoveAcceptance 32 | 33 | Scenario: Should ignore nulls 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({num: 42}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n) 42 | OPTIONAL MATCH (n)-[r]->() 43 | REMOVE r.num 44 | RETURN n 45 | """ 46 | Then the result should be, in any order: 47 | | n | 48 | | ({num: 42}) | 49 | And no side effects 50 | 51 | Scenario: Remove a single label 52 | Given an empty graph 53 | And having executed: 54 | """ 55 | CREATE (:L {num: 42}) 56 | """ 57 | When executing query: 58 | """ 59 | MATCH (n) 60 | REMOVE n:L 61 | RETURN n.num 62 | """ 63 | Then the result should be, in any order: 64 | | n.num | 65 | | 42 | 66 | And the side effects should be: 67 | | -labels | 1 | 68 | 69 | Scenario: Remove multiple labels 70 | Given an empty graph 71 | And having executed: 72 | """ 73 | CREATE (:L1:L2:L3 {num: 42}) 74 | """ 75 | When executing query: 76 | """ 77 | MATCH (n) 78 | REMOVE n:L1:L3 79 | RETURN labels(n) 80 | """ 81 | Then the result should be, in any order: 82 | | labels(n) | 83 | | ['L2'] | 84 | And the side effects should be: 85 | | -labels | 2 | 86 | 87 | Scenario: Remove a single node property 88 | Given an empty graph 89 | And having executed: 90 | """ 91 | CREATE (:L {num: 42}) 92 | """ 93 | When executing query: 94 | """ 95 | MATCH (n) 96 | REMOVE n.num 97 | RETURN exists(n.num) AS still_there 98 | """ 99 | Then the result should be, in any order: 100 | | still_there | 101 | | false | 102 | And the side effects should be: 103 | | -properties | 1 | 104 | 105 | Scenario: Remove multiple node properties 106 | Given an empty graph 107 | And having executed: 108 | """ 109 | CREATE (:L {num: 42, name: 'a', name2: 'B'}) 110 | """ 111 | When executing query: 112 | """ 113 | MATCH (n) 114 | REMOVE n.num, n.name 115 | RETURN size(keys(n)) AS props 116 | """ 117 | Then the result should be, in any order: 118 | | props | 119 | | 1 | 120 | And the side effects should be: 121 | | -properties | 2 | 122 | 123 | Scenario: Remove a single relationship property 124 | Given an empty graph 125 | And having executed: 126 | """ 127 | CREATE (a), (b), (a)-[:X {num: 42}]->(b) 128 | """ 129 | When executing query: 130 | """ 131 | MATCH ()-[r]->() 132 | REMOVE r.num 133 | RETURN exists(r.num) AS still_there 134 | """ 135 | Then the result should be, in any order: 136 | | still_there | 137 | | false | 138 | And the side effects should be: 139 | | -properties | 1 | 140 | 141 | Scenario: Remove multiple relationship properties 142 | Given an empty graph 143 | And having executed: 144 | """ 145 | CREATE (a), (b), (a)-[:X {num: 42, a: 'a', b: 'B'}]->(b) 146 | """ 147 | When executing query: 148 | """ 149 | MATCH ()-[r]->() 150 | REMOVE r.num, r.a 151 | RETURN size(keys(r)) AS props 152 | """ 153 | Then the result should be, in any order: 154 | | props | 155 | | 1 | 156 | And the side effects should be: 157 | | -properties | 2 | 158 | 159 | Scenario: Remove a missing property should be a valid operation 160 | Given an empty graph 161 | And having executed: 162 | """ 163 | CREATE (), (), () 164 | """ 165 | When executing query: 166 | """ 167 | MATCH (n) 168 | REMOVE n.num 169 | RETURN sum(size(keys(n))) AS totalNumberOfProps 170 | """ 171 | Then the result should be, in any order: 172 | | totalNumberOfProps | 173 | | 0 | 174 | And no side effects 175 | -------------------------------------------------------------------------------- /features/backlog/Comparability.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: Comparability 32 | 33 | Scenario: Comparing strings and integers using > in an AND'd predicate 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (root:Root)-[:T]->(:Child {var: 0}), 38 | (root)-[:T]->(:Child {var: 'xx'}), 39 | (root)-[:T]->(:Child) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (:Root)-->(i:Child) 44 | WHERE exists(i.var) AND i.var > 'x' 45 | RETURN i.var 46 | """ 47 | Then the result should be, in any order: 48 | | i.var | 49 | | 'xx' | 50 | And no side effects 51 | 52 | Scenario: Comparing strings and integers using > in a OR'd predicate 53 | Given an empty graph 54 | And having executed: 55 | """ 56 | CREATE (root:Root)-[:T]->(:Child {var: 0}), 57 | (root)-[:T]->(:Child {var: 'xx'}), 58 | (root)-[:T]->(:Child) 59 | """ 60 | When executing query: 61 | """ 62 | MATCH (:Root)-->(i:Child) 63 | WHERE NOT exists(i.var) OR i.var > 'x' 64 | RETURN i.var 65 | """ 66 | Then the result should be, in any order: 67 | | i.var | 68 | | 'xx' | 69 | | null | 70 | And no side effects 71 | 72 | Scenario Outline: Comparing across types yields null, except numbers 73 | Given an empty graph 74 | And having executed: 75 | """ 76 | CREATE ()-[:T]->() 77 | """ 78 | When executing query: 79 | """ 80 | MATCH p = (n)-[r]->() 81 | WITH [n, r, p, '', 1, 3.14, true, null, [], {}] AS types 82 | UNWIND range(0, size(types) - 1) AS i 83 | UNWIND range(0, size(types) - 1) AS j 84 | WITH types[i] AS lhs, types[j] AS rhs 85 | WHERE i <> j 86 | WITH lhs, rhs, lhs rhs AS result 87 | WHERE result 88 | RETURN lhs, rhs 89 | """ 90 | Then the result should be, in any order: 91 | | lhs | rhs | 92 | | | | 93 | And no side effects 94 | 95 | Examples: 96 | | operator | lhs | rhs | 97 | | < | 1 | 3.14 | 98 | | <= | 1 | 3.14 | 99 | | >= | 3.14 | 1 | 100 | | > | 3.14 | 1 | 101 | 102 | Scenario Outline: Comparing lists 103 | Given an empty graph 104 | When executing query: 105 | """ 106 | RETURN >= AS result 107 | """ 108 | Then the result should be, in any order: 109 | | result | 110 | | | 111 | And no side effects 112 | 113 | Examples: 114 | | lhs | rhs | result | 115 | | [1, 0] | [1] | true | 116 | | [1, null] | [1] | true | 117 | | [1, 2] | [1, null] | null | 118 | | [1, 'a'] | [1, null] | null | 119 | | [1, 2] | [3, null] | false | 120 | 121 | Scenario Outline: Comparing NaN 122 | Given an empty graph 123 | When executing query: 124 | """ 125 | RETURN > AS gt, >= AS gtE, < AS lt, <= AS ltE 126 | """ 127 | Then the result should be, in any order: 128 | | gt | gtE | lt | ltE | 129 | | | | | | 130 | And no side effects 131 | 132 | Examples: 133 | | lhs | rhs | result | 134 | | 0.0 / 0.0 | 1 | false | 135 | | 0.0 / 0.0 | 1.0 | false | 136 | | 0.0 / 0.0 | 0.0 / 0.0 | false | 137 | | 0.0 / 0.0 | 'a' | null | 138 | 139 | Scenario Outline: Comparability between numbers and strings 140 | Given any graph 141 | When executing query: 142 | """ 143 | RETURN < AS result 144 | """ 145 | Then the result should be, in any order: 146 | | result | 147 | | | 148 | And no side effects 149 | 150 | Examples: 151 | | lhs | rhs | result | 152 | | 1.0 | 1.0 | false | 153 | | 1 | 1.0 | false | 154 | | '1.0' | 1.0 | null | 155 | | '1' | 1 | null | 156 | -------------------------------------------------------------------------------- /features/backlog/TernaryLogicAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: TernaryLogicAcceptanceTest 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: The inverse of a null is a null 37 | When executing query: 38 | """ 39 | RETURN NOT null AS value 40 | """ 41 | Then the result should be, in any order: 42 | | value | 43 | | null | 44 | And no side effects 45 | 46 | Scenario: A literal null IS null 47 | When executing query: 48 | """ 49 | RETURN null IS NULL AS value 50 | """ 51 | Then the result should be, in any order: 52 | | value | 53 | | true | 54 | And no side effects 55 | 56 | Scenario: A literal null is not IS NOT null 57 | When executing query: 58 | """ 59 | RETURN null IS NOT NULL AS value 60 | """ 61 | Then the result should be, in any order: 62 | | value | 63 | | false | 64 | And no side effects 65 | 66 | Scenario: It is unknown - i.e. null - if a null is equal to a null 67 | When executing query: 68 | """ 69 | RETURN null = null AS value 70 | """ 71 | Then the result should be, in any order: 72 | | value | 73 | | null | 74 | And no side effects 75 | 76 | Scenario: It is unknown - i.e. null - if a null is not equal to a null 77 | When executing query: 78 | """ 79 | RETURN null <> null AS value 80 | """ 81 | Then the result should be, in any order: 82 | | value | 83 | | null | 84 | And no side effects 85 | 86 | Scenario Outline: Using null in AND 87 | Given parameters are: 88 | | lhs | | 89 | | rhs | | 90 | When executing query: 91 | """ 92 | RETURN $lhs AND $rhs AS result 93 | """ 94 | Then the result should be, in any order: 95 | | result | 96 | | | 97 | And no side effects 98 | 99 | Examples: 100 | | lhs | rhs | result | 101 | | null | null | null | 102 | | null | true | null | 103 | | true | null | null | 104 | | null | false | false | 105 | | false | null | false | 106 | 107 | Scenario Outline: Using null in OR 108 | Given parameters are: 109 | | lhs | | 110 | | rhs | | 111 | When executing query: 112 | """ 113 | RETURN $lhs OR $rhs AS result 114 | """ 115 | Then the result should be, in any order: 116 | | result | 117 | | | 118 | And no side effects 119 | 120 | Examples: 121 | | lhs | rhs | result | 122 | | null | null | null | 123 | | null | true | true | 124 | | true | null | true | 125 | | null | false | null | 126 | | false | null | null | 127 | 128 | Scenario Outline: Using null in XOR 129 | Given parameters are: 130 | | lhs | | 131 | | rhs | | 132 | When executing query: 133 | """ 134 | RETURN $lhs XOR $rhs AS result 135 | """ 136 | Then the result should be, in any order: 137 | | result | 138 | | | 139 | And no side effects 140 | 141 | Examples: 142 | | lhs | rhs | result | 143 | | null | null | null | 144 | | null | true | null | 145 | | true | null | null | 146 | | null | false | null | 147 | | false | null | null | 148 | 149 | Scenario Outline: Using null in IN 150 | Given parameters are: 151 | | elt | | 152 | | coll | | 153 | When executing query: 154 | """ 155 | RETURN $elt IN $coll AS result 156 | """ 157 | Then the result should be, in any order: 158 | | result | 159 | | | 160 | And no side effects 161 | 162 | Examples: 163 | | elt | coll | result | 164 | | null | null | null | 165 | | null | [1, 2, 3] | null | 166 | | null | [1, 2, 3, null] | null | 167 | | null | [] | false | 168 | | 1 | [1, 2, 3, null] | true | 169 | | 1 | [null, 1] | true | 170 | | 5 | [1, 2, 3, null] | null | 171 | -------------------------------------------------------------------------------- /features/backlog/NullAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: NullAcceptance 32 | 33 | Scenario: Property existence check on non-null node 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({exists: 42}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n) 42 | RETURN exists(n.missing), 43 | exists(n.exists) 44 | """ 45 | Then the result should be, in any order: 46 | | exists(n.missing) | exists(n.exists) | 47 | | false | true | 48 | And no side effects 49 | 50 | Scenario: Property existence check on optional non-null node 51 | Given an empty graph 52 | And having executed: 53 | """ 54 | CREATE ({exists: 42}) 55 | """ 56 | When executing query: 57 | """ 58 | OPTIONAL MATCH (n) 59 | RETURN exists(n.missing), 60 | exists(n.exists) 61 | """ 62 | Then the result should be, in any order: 63 | | exists(n.missing) | exists(n.exists) | 64 | | false | true | 65 | And no side effects 66 | 67 | Scenario: Property existence check on null node 68 | Given an empty graph 69 | When executing query: 70 | """ 71 | OPTIONAL MATCH (n) 72 | RETURN exists(n.missing) 73 | """ 74 | Then the result should be, in any order: 75 | | exists(n.missing) | 76 | | null | 77 | And no side effects 78 | 79 | Scenario: Ignore null when setting property 80 | Given an empty graph 81 | When executing query: 82 | """ 83 | OPTIONAL MATCH (a:DoesNotExist) 84 | SET a.num = 42 85 | RETURN a 86 | """ 87 | Then the result should be, in any order: 88 | | a | 89 | | null | 90 | And no side effects 91 | 92 | Scenario: Ignore null when removing property 93 | Given an empty graph 94 | When executing query: 95 | """ 96 | OPTIONAL MATCH (a:DoesNotExist) 97 | REMOVE a.num 98 | RETURN a 99 | """ 100 | Then the result should be, in any order: 101 | | a | 102 | | null | 103 | And no side effects 104 | 105 | Scenario: Ignore null when setting properties using an appending map 106 | Given an empty graph 107 | When executing query: 108 | """ 109 | OPTIONAL MATCH (a:DoesNotExist) 110 | SET a += {num: 42} 111 | RETURN a 112 | """ 113 | Then the result should be, in any order: 114 | | a | 115 | | null | 116 | And no side effects 117 | 118 | Scenario: Ignore null when setting properties using an overriding map 119 | Given an empty graph 120 | When executing query: 121 | """ 122 | OPTIONAL MATCH (a:DoesNotExist) 123 | SET a = {num: 42} 124 | RETURN a 125 | """ 126 | Then the result should be, in any order: 127 | | a | 128 | | null | 129 | And no side effects 130 | 131 | Scenario: Ignore null when setting label 132 | Given an empty graph 133 | When executing query: 134 | """ 135 | OPTIONAL MATCH (a:DoesNotExist) 136 | SET a:L 137 | RETURN a 138 | """ 139 | Then the result should be, in any order: 140 | | a | 141 | | null | 142 | And no side effects 143 | 144 | Scenario: Ignore null when removing label 145 | Given an empty graph 146 | When executing query: 147 | """ 148 | OPTIONAL MATCH (a:DoesNotExist) 149 | REMOVE a:L 150 | RETURN a 151 | """ 152 | Then the result should be, in any order: 153 | | a | 154 | | null | 155 | And no side effects 156 | 157 | Scenario: Ignore null when deleting node 158 | Given an empty graph 159 | When executing query: 160 | """ 161 | OPTIONAL MATCH (a:DoesNotExist) 162 | DELETE a 163 | RETURN a 164 | """ 165 | Then the result should be, in any order: 166 | | a | 167 | | null | 168 | And no side effects 169 | 170 | Scenario: Ignore null when deleting relationship 171 | Given an empty graph 172 | When executing query: 173 | """ 174 | OPTIONAL MATCH ()-[r:DoesNotExist]-() 175 | DELETE r 176 | RETURN r 177 | """ 178 | Then the result should be, in any order: 179 | | r | 180 | | null | 181 | And no side effects 182 | -------------------------------------------------------------------------------- /features/backlog/EqualsAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: EqualsAcceptance 32 | 33 | Scenario: Number-typed integer comparison 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({id: 0}) 38 | """ 39 | When executing query: 40 | """ 41 | WITH collect([0, 0.0]) AS numbers 42 | UNWIND numbers AS arr 43 | WITH arr[0] AS expected 44 | MATCH (n) WHERE toInteger(n.id) = expected 45 | RETURN n 46 | """ 47 | Then the result should be, in any order: 48 | | n | 49 | | ({id: 0}) | 50 | And no side effects 51 | 52 | Scenario: Number-typed float comparison 53 | Given an empty graph 54 | And having executed: 55 | """ 56 | CREATE ({id: 0}) 57 | """ 58 | When executing query: 59 | """ 60 | WITH collect([0.5, 0]) AS numbers 61 | UNWIND numbers AS arr 62 | WITH arr[0] AS expected 63 | MATCH (n) WHERE toInteger(n.id) = expected 64 | RETURN n 65 | """ 66 | Then the result should be, in any order: 67 | | n | 68 | And no side effects 69 | 70 | Scenario: Any-typed string comparison 71 | Given an empty graph 72 | And having executed: 73 | """ 74 | CREATE ({id: 0}) 75 | """ 76 | When executing query: 77 | """ 78 | WITH collect(['0', 0]) AS things 79 | UNWIND things AS arr 80 | WITH arr[0] AS expected 81 | MATCH (n) WHERE toInteger(n.id) = expected 82 | RETURN n 83 | """ 84 | Then the result should be, in any order: 85 | | n | 86 | And no side effects 87 | 88 | Scenario: Comparing nodes to nodes 89 | Given an empty graph 90 | And having executed: 91 | """ 92 | CREATE () 93 | """ 94 | When executing query: 95 | """ 96 | MATCH (a) 97 | WITH a 98 | MATCH (b) 99 | WHERE a = b 100 | RETURN count(b) 101 | """ 102 | Then the result should be, in any order: 103 | | count(b) | 104 | | 1 | 105 | And no side effects 106 | 107 | Scenario: Comparing relationships to relationships 108 | Given an empty graph 109 | And having executed: 110 | """ 111 | CREATE ()-[:T]->() 112 | """ 113 | When executing query: 114 | """ 115 | MATCH ()-[a]->() 116 | WITH a 117 | MATCH ()-[b]->() 118 | WHERE a = b 119 | RETURN count(b) 120 | """ 121 | Then the result should be, in any order: 122 | | count(b) | 123 | | 1 | 124 | And no side effects 125 | 126 | Scenario Outline: Comparing lists to lists 127 | Given an empty graph 128 | When executing query: 129 | """ 130 | RETURN = AS result 131 | """ 132 | Then the result should be, in any order: 133 | | result | 134 | | | 135 | And no side effects 136 | 137 | Examples: 138 | | lhs | rhs | result | 139 | | [1, 2] | [1] | false | 140 | | [null] | [1] | null | 141 | | ['a'] | [1] | false | 142 | | [[1]] | [[1], [null]] | false | 143 | | [[1], [2]] | [[1], [null]] | null | 144 | | [[1], [2, 3]] | [[1], [null]] | false | 145 | 146 | Scenario Outline: Equality and inequality of NaN 147 | Given any graph 148 | When executing query: 149 | """ 150 | RETURN = AS isEqual, <> AS isNotEqual 151 | """ 152 | Then the result should be, in any order: 153 | | isEqual | isNotEqual | 154 | | false | true | 155 | And no side effects 156 | 157 | Examples: 158 | | lhs | rhs | 159 | | 0.0 / 0.0 | 1 | 160 | | 0.0 / 0.0 | 1.0 | 161 | | 0.0 / 0.0 | 0.0 / 0.0 | 162 | | 0.0 / 0.0 | 'a' | 163 | 164 | Scenario Outline: Equality between strings and numbers 165 | Given any graph 166 | When executing query: 167 | """ 168 | RETURN = AS result 169 | """ 170 | Then the result should be, in any order: 171 | | result | 172 | | | 173 | And no side effects 174 | 175 | Examples: 176 | | lhs | rhs | result | 177 | | 1.0 | 1.0 | true | 178 | | 1 | 1.0 | true | 179 | | '1.0' | 1.0 | false | 180 | | '1' | 1 | false | 181 | -------------------------------------------------------------------------------- /features/backlog/KeysAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: KeysAcceptance 32 | 33 | Scenario: Using `keys()` on a single node, non-empty result 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({name: 'Andres', surname: 'Lopez'}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n) 42 | UNWIND keys(n) AS x 43 | RETURN DISTINCT x AS theProps 44 | """ 45 | Then the result should be, in any order: 46 | | theProps | 47 | | 'name' | 48 | | 'surname' | 49 | And no side effects 50 | 51 | Scenario: Using `keys()` on multiple nodes, non-empty result 52 | Given an empty graph 53 | And having executed: 54 | """ 55 | CREATE ({name: 'Andres', surname: 'Lopez'}), 56 | ({otherName: 'Andres', otherSurname: 'Lopez'}) 57 | """ 58 | When executing query: 59 | """ 60 | MATCH (n) 61 | UNWIND keys(n) AS x 62 | RETURN DISTINCT x AS theProps 63 | """ 64 | Then the result should be, in any order: 65 | | theProps | 66 | | 'name' | 67 | | 'surname' | 68 | | 'otherName' | 69 | | 'otherSurname' | 70 | And no side effects 71 | 72 | Scenario: Using `keys()` on a single node, empty result 73 | Given an empty graph 74 | And having executed: 75 | """ 76 | CREATE () 77 | """ 78 | When executing query: 79 | """ 80 | MATCH (n) 81 | UNWIND keys(n) AS x 82 | RETURN DISTINCT x AS theProps 83 | """ 84 | Then the result should be, in any order: 85 | | theProps | 86 | And no side effects 87 | 88 | Scenario: Using `keys()` on an optionally matched node 89 | Given an empty graph 90 | And having executed: 91 | """ 92 | CREATE () 93 | """ 94 | When executing query: 95 | """ 96 | OPTIONAL MATCH (n) 97 | UNWIND keys(n) AS x 98 | RETURN DISTINCT x AS theProps 99 | """ 100 | Then the result should be, in any order: 101 | | theProps | 102 | And no side effects 103 | 104 | Scenario: Using `keys()` on a relationship, non-empty result 105 | Given an empty graph 106 | And having executed: 107 | """ 108 | CREATE ()-[:KNOWS {status: 'bad', year: '2015'}]->() 109 | """ 110 | When executing query: 111 | """ 112 | MATCH ()-[r:KNOWS]-() 113 | UNWIND keys(r) AS x 114 | RETURN DISTINCT x AS theProps 115 | """ 116 | Then the result should be, in any order: 117 | | theProps | 118 | | 'status' | 119 | | 'year' | 120 | And no side effects 121 | 122 | Scenario: Using `keys()` on a relationship, empty result 123 | Given an empty graph 124 | And having executed: 125 | """ 126 | CREATE ()-[:KNOWS]->() 127 | """ 128 | When executing query: 129 | """ 130 | MATCH ()-[r:KNOWS]-() 131 | UNWIND keys(r) AS x 132 | RETURN DISTINCT x AS theProps 133 | """ 134 | Then the result should be, in any order: 135 | | theProps | 136 | And no side effects 137 | 138 | Scenario: Using `keys()` on an optionally matched relationship 139 | Given an empty graph 140 | And having executed: 141 | """ 142 | CREATE ()-[:KNOWS]->() 143 | """ 144 | When executing query: 145 | """ 146 | OPTIONAL MATCH ()-[r:KNOWS]-() 147 | UNWIND keys(r) AS x 148 | RETURN DISTINCT x AS theProps 149 | """ 150 | Then the result should be, in any order: 151 | | theProps | 152 | And no side effects 153 | 154 | Scenario: Using `keys()` on a literal map 155 | Given any graph 156 | When executing query: 157 | """ 158 | RETURN keys({name: 'Alice', age: 38, address: {city: 'London', residential: true}}) AS k 159 | """ 160 | Then the result should be (ignoring element order for lists): 161 | | k | 162 | | ['name', 'age', 'address'] | 163 | And no side effects 164 | 165 | Scenario: Using `keys()` on a parameter map 166 | Given any graph 167 | And parameters are: 168 | | param | {name: 'Alice', age: 38, address: {city: 'London', residential: true}} | 169 | When executing query: 170 | """ 171 | RETURN keys($param) AS k 172 | """ 173 | Then the result should be (ignoring element order for lists): 174 | | k | 175 | | ['address', 'name', 'age'] | 176 | And no side effects 177 | -------------------------------------------------------------------------------- /features/backlog/MergeIntoAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: MergeIntoAcceptance 32 | 33 | Background: 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (:A {name: 'A'}), (:B {name: 'B'}) 38 | """ 39 | 40 | Scenario: Updating one property with ON CREATE 41 | When executing query: 42 | """ 43 | MATCH (a {name: 'A'}), (b {name: 'B'}) 44 | MERGE (a)-[r:TYPE]->(b) 45 | ON CREATE SET r.name = 'foo' 46 | """ 47 | Then the result should be empty 48 | And the side effects should be: 49 | | +relationships | 1 | 50 | | +properties | 1 | 51 | When executing control query: 52 | """ 53 | MATCH ()-[r:TYPE]->() 54 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 55 | """ 56 | Then the result should be, in any order: 57 | | keyValue | 58 | | ['name->foo'] | 59 | 60 | Scenario: Null-setting one property with ON CREATE 61 | When executing query: 62 | """ 63 | MATCH (a {name: 'A'}), (b {name: 'B'}) 64 | MERGE (a)-[r:TYPE]->(b) 65 | ON CREATE SET r.name = null 66 | """ 67 | Then the result should be empty 68 | And the side effects should be: 69 | | +relationships | 1 | 70 | When executing control query: 71 | """ 72 | MATCH ()-[r:TYPE]->() 73 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 74 | """ 75 | Then the result should be, in any order: 76 | | keyValue | 77 | | [] | 78 | 79 | Scenario: Copying properties from node with ON CREATE 80 | When executing query: 81 | """ 82 | MATCH (a {name: 'A'}), (b {name: 'B'}) 83 | MERGE (a)-[r:TYPE]->(b) 84 | ON CREATE SET r = a 85 | """ 86 | Then the result should be empty 87 | And the side effects should be: 88 | | +relationships | 1 | 89 | | +properties | 1 | 90 | When executing control query: 91 | """ 92 | MATCH ()-[r:TYPE]->() 93 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 94 | """ 95 | Then the result should be, in any order: 96 | | keyValue | 97 | | ['name->A'] | 98 | 99 | Scenario: Copying properties from node with ON MATCH 100 | Given having executed: 101 | """ 102 | MATCH (a:A), (b:B) 103 | CREATE (a)-[:TYPE {name: 'bar'}]->(b) 104 | """ 105 | When executing query: 106 | """ 107 | MATCH (a {name: 'A'}), (b {name: 'B'}) 108 | MERGE (a)-[r:TYPE]->(b) 109 | ON MATCH SET r = a 110 | """ 111 | Then the result should be empty 112 | And the side effects should be: 113 | | +properties | 1 | 114 | | -properties | 1 | 115 | When executing control query: 116 | """ 117 | MATCH ()-[r:TYPE]->() 118 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 119 | """ 120 | Then the result should be, in any order: 121 | | keyValue | 122 | | ['name->A'] | 123 | 124 | Scenario: Copying properties from literal map with ON CREATE 125 | When executing query: 126 | """ 127 | MATCH (a {name: 'A'}), (b {name: 'B'}) 128 | MERGE (a)-[r:TYPE]->(b) 129 | ON CREATE SET r += {name: 'bar', name2: 'baz'} 130 | """ 131 | Then the result should be empty 132 | And the side effects should be: 133 | | +relationships | 1 | 134 | | +properties | 2 | 135 | When executing control query: 136 | """ 137 | MATCH ()-[r:TYPE]->() 138 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 139 | """ 140 | Then the result should be (ignoring element order for lists): 141 | | keyValue | 142 | | ['name->bar', 'name2->baz'] | 143 | 144 | Scenario: Copying properties from literal map with ON MATCH 145 | Given having executed: 146 | """ 147 | MATCH (a:A), (b:B) 148 | CREATE (a)-[:TYPE {name: 'bar'}]->(b) 149 | """ 150 | When executing query: 151 | """ 152 | MATCH (a {name: 'A'}), (b {name: 'B'}) 153 | MERGE (a)-[r:TYPE]->(b) 154 | ON MATCH SET r += {name: 'baz', name2: 'baz'} 155 | """ 156 | Then the result should be empty 157 | And the side effects should be: 158 | | +properties | 2 | 159 | | -properties | 1 | 160 | When executing control query: 161 | """ 162 | MATCH ()-[r:TYPE]->() 163 | RETURN [key IN keys(r) | key + '->' + r[key]] AS keyValue 164 | """ 165 | Then the result should be (ignoring element order for lists): 166 | | keyValue | 167 | | ['name->baz', 'name2->baz'] | 168 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Backends implement the actual storage of graphs, and provide implementations of the 3 | // logical operators the frontend emits that can act on that storage. 4 | // 5 | use crate::frontend::LogicalPlan; 6 | use crate::{Error, Row, Type}; 7 | use anyhow::Result; 8 | use std::cell::RefCell; 9 | use std::collections::{HashMap, HashSet}; 10 | use std::fmt::Debug; 11 | use std::rc::Rc; 12 | 13 | // I don't know if any of this makes any sense, but the thoughts here is like.. lets make it 14 | // easy to build experimental backends, that can convert a logical plan tree into something that 15 | // can be executed. I've tried really hard to avoid making this trait have generics on it, 16 | // though I'm not sure it's possible to maintain that invariant.. It does simplify a lot of stuff 17 | // in the planning side and in the API to not have to deal with different backends having different 18 | // generics. Much of that difficulty is likely my poor Rust skills tho. 19 | pub trait Backend: Debug { 20 | type Cursor: BackendCursor; 21 | 22 | fn new_cursor(&mut self) -> Self::Cursor; 23 | 24 | fn tokens(&self) -> Rc>; 25 | 26 | // Evaluate a logical plan and set the cursor up to process the result 27 | fn eval(&mut self, plan: LogicalPlan, cursor: &mut Self::Cursor) -> Result<()>; 28 | 29 | // Describe this backend for the frontends benefit 30 | fn describe(&self) -> Result; 31 | } 32 | 33 | // To allow each backend to own how values are represented, and to let them optimize 34 | // iteration to fit their own desires, backends describe this cursor interface that sits 35 | // just below a thin veil of the public API. 36 | // 37 | // Like with the public API Cursor, this is almost equivalent to an iterator, except each 38 | // iteration can be done without allocation. 39 | pub trait BackendCursor { 40 | // TODO is there a nice way to do this without copying out the strings? 41 | fn fields(&self) -> Vec; 42 | // TODO I think we'd want a try_fold implementation here, to allow an opt-in version 43 | // of interior iteration, assuming benchmarking show that makes a difference. 44 | 45 | // Move to the next record; if result is happy, you can access the record with the accessor methods 46 | fn next(&mut self) -> Result>; 47 | } 48 | 49 | // Describes, for the frontend, the layout of the backend. This is intended to include things 50 | // like schema (which the planner can take advantage of to perform optimizations), but also 51 | // type signatures of functions and perhaps listings of available special features for the planner 52 | // to use. 53 | #[derive(Debug)] 54 | pub struct BackendDesc { 55 | pub functions: Vec, 56 | // Fast lookup of functions that aggregate 57 | pub aggregates: HashSet, 58 | } 59 | 60 | impl BackendDesc { 61 | pub fn new(functions: Vec) -> BackendDesc { 62 | let mut aggregates = HashSet::new(); 63 | for f in &functions { 64 | if let FuncType::Aggregating = f.func_type { 65 | aggregates.insert(f.name); 66 | } 67 | } 68 | BackendDesc { 69 | functions, 70 | aggregates, 71 | } 72 | } 73 | } 74 | 75 | #[derive(Debug, Clone)] 76 | pub enum FuncType { 77 | // If you imagine cypher as a stream processing system, each operator consumes one or more 78 | // input streams and yields an output stream. In expressions with scalar functions, each 79 | // input row maps to one output row. 80 | // 81 | // For example, the following query involves a scalar function: 82 | // 83 | // MATCH (n) RETURN id(n) 84 | // 85 | // The MATCH (n) part yields a stream of every node in the graph, and the RETURN id(n) part 86 | // operates on one row at a time, yielding an output row with the node id for each input row. 87 | Scalar, 88 | // Aggregating functions change the operator cardinality - an operator with aggregating functions 89 | // may yield output rows than it consumed. For instance, this query: 90 | // 91 | // MATCH (n) RETURN count(n) 92 | // 93 | // Here, the "MATCH (n)" part yields a stream of every node in the graph. The "RETURN count(n)" 94 | // part consumes that stream, and yields exactly one row with the count. The "count" function 95 | // is aggregating rows. 96 | // 97 | // There are examples of aggregations yielding more than one row, when grouping is involved: 98 | // 99 | // MATCH (n) RETURN n.age, count(n) 100 | // 101 | Aggregating, 102 | } 103 | 104 | // See the "functions" section in the openCypher spec https://s3.amazonaws.com/artifacts.opencypher.org/openCypher9.pdf 105 | #[derive(Debug, Clone)] 106 | pub struct FuncSignature { 107 | // Aggregate or scalar? 108 | pub func_type: FuncType, 109 | // Name of this function 110 | pub name: Token, 111 | // Return type 112 | pub returns: Type, 113 | // Named arguments 114 | pub args: Vec<(Token, Type)>, 115 | } 116 | 117 | // gql databases are filled with short string keys. Both things stored in the graph, like property 118 | // keys, labels and relationship types. But also strings used for identifiers in queries, like 119 | // "n" in `MATCH (n)`. 120 | // These are easier for the database to work with, since they are fixed size stack allocated values. 121 | pub type Token = usize; 122 | 123 | // Simple in-memory string-to-token mapper. 124 | #[derive(Debug, Default)] 125 | pub struct Tokens { 126 | pub table: HashMap, 127 | } 128 | 129 | impl Tokens { 130 | pub fn new() -> Tokens { 131 | Tokens::default() 132 | } 133 | 134 | pub fn lookup(&self, tok: usize) -> Option<&str> { 135 | for (content, candidate) in self.table.iter() { 136 | if *candidate == tok { 137 | return Some(&content); 138 | } 139 | } 140 | None 141 | } 142 | 143 | pub fn tokenize(&mut self, content: &str) -> usize { 144 | match self.table.get(content) { 145 | Some(tok) => *tok, 146 | None => { 147 | let tok = self.table.len(); 148 | self.table.insert(content.to_string(), tok); 149 | tok 150 | } 151 | } 152 | } 153 | } 154 | 155 | #[cfg(feature = "gram")] 156 | pub mod gram; 157 | -------------------------------------------------------------------------------- /features/backlog/TemporalToStringAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: TemporalToStringAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: Should serialize date 37 | When executing query: 38 | """ 39 | WITH date({year: 1984, month: 10, day: 11}) AS d 40 | RETURN toString(d) AS ts, date(toString(d)) = d AS b 41 | """ 42 | Then the result should be, in any order: 43 | | ts | b | 44 | | '1984-10-11' | true | 45 | And no side effects 46 | 47 | Scenario: Should serialize local time 48 | When executing query: 49 | """ 50 | WITH localtime({hour: 12, minute: 31, second: 14, nanosecond: 645876123}) AS d 51 | RETURN toString(d) AS ts, localtime(toString(d)) = d AS b 52 | """ 53 | Then the result should be, in any order: 54 | | ts | b | 55 | | '12:31:14.645876123' | true | 56 | And no side effects 57 | 58 | Scenario: Should serialize time 59 | When executing query: 60 | """ 61 | WITH time({hour: 12, minute: 31, second: 14, nanosecond: 645876123, timezone: '+01:00'}) AS d 62 | RETURN toString(d) AS ts, time(toString(d)) = d AS b 63 | """ 64 | Then the result should be, in any order: 65 | | ts | b | 66 | | '12:31:14.645876123+01:00' | true | 67 | And no side effects 68 | 69 | Scenario: Should serialize local date time 70 | When executing query: 71 | """ 72 | WITH localdatetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123}) AS d 73 | RETURN toString(d) AS ts, localdatetime(toString(d)) = d AS b 74 | """ 75 | Then the result should be, in any order: 76 | | ts | b | 77 | | '1984-10-11T12:31:14.645876123' | true | 78 | And no side effects 79 | 80 | Scenario: Should serialize date time 81 | When executing query: 82 | """ 83 | WITH datetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123, timezone: '+01:00'}) AS d 84 | RETURN toString(d) AS ts, datetime(toString(d)) = d AS b 85 | """ 86 | Then the result should be, in any order: 87 | | ts | b | 88 | | '1984-10-11T12:31:14.645876123+01:00' | true | 89 | And no side effects 90 | 91 | Scenario Outline: Should serialize duration 92 | When executing query: 93 | """ 94 | WITH duration() AS d 95 | RETURN toString(d) AS ts, duration(toString(d)) = d AS b 96 | """ 97 | Then the result should be, in any order: 98 | | ts | b | 99 | | | | 100 | And no side effects 101 | 102 | Examples: 103 | | map | toString | isEqual | 104 | | {years: 12, months: 5, days: 14, hours: 16, minutes: 12, seconds: 70, nanoseconds: 1} | 'P12Y5M14DT16H13M10.000000001S' | true | 105 | | {years: 12, months: 5, days: -14, hours: 16} | 'P12Y5M-14DT16H' | true | 106 | | {minutes: 12, seconds: -60} | 'PT11M' | true | 107 | | {seconds: 2, milliseconds: -1} | 'PT1.999S' | true | 108 | | {seconds: -2, milliseconds: 1} | 'PT-1.999S' | true | 109 | | {seconds: -2, milliseconds: -1} | 'PT-2.001S' | true | 110 | | {days: 1, milliseconds: 1} | 'P1DT0.001S' | true | 111 | | {days: 1, milliseconds: -1} | 'P1DT-0.001S' | true | 112 | | {seconds: 60, milliseconds: -1} | 'PT59.999S' | true | 113 | | {seconds: -60, milliseconds: 1} | 'PT-59.999S' | true | 114 | | {seconds: -60, milliseconds: -1} | 'PT-1M-0.001S' | true | 115 | 116 | Scenario: Should serialize timezones correctly 117 | When executing query: 118 | """ 119 | WITH datetime({year: 2017, month: 8, day: 8, hour: 12, minute: 31, second: 14, nanosecond: 645876123, timezone: 'Europe/Stockholm'}) AS d 120 | RETURN toString(d) AS ts 121 | """ 122 | Then the result should be, in any order: 123 | | ts | 124 | | '2017-08-08T12:31:14.645876123+02:00[Europe/Stockholm]' | 125 | And no side effects 126 | -------------------------------------------------------------------------------- /features/backlog/ComparisonOperatorAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ComparisonOperatorAcceptance 32 | 33 | Scenario: Handling numerical ranges 1 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | UNWIND [1, 2, 3] AS i 38 | CREATE ({num: i}) 39 | """ 40 | When executing query: 41 | """ 42 | MATCH (n) 43 | WHERE 1 < n.num < 3 44 | RETURN n.num 45 | """ 46 | Then the result should be, in any order: 47 | | n.num | 48 | | 2 | 49 | And no side effects 50 | 51 | Scenario: Handling numerical ranges 2 52 | Given an empty graph 53 | And having executed: 54 | """ 55 | UNWIND [1, 2, 3] AS i 56 | CREATE ({num: i}) 57 | """ 58 | When executing query: 59 | """ 60 | MATCH (n) 61 | WHERE 1 < n.num <= 3 62 | RETURN n.num 63 | """ 64 | Then the result should be, in any order: 65 | | n.num | 66 | | 2 | 67 | | 3 | 68 | And no side effects 69 | 70 | Scenario: Handling numerical ranges 3 71 | Given an empty graph 72 | And having executed: 73 | """ 74 | UNWIND [1, 2, 3] AS i 75 | CREATE ({num: i}) 76 | """ 77 | When executing query: 78 | """ 79 | MATCH (n) 80 | WHERE 1 <= n.num < 3 81 | RETURN n.num 82 | """ 83 | Then the result should be, in any order: 84 | | n.num | 85 | | 1 | 86 | | 2 | 87 | And no side effects 88 | 89 | Scenario: Handling numerical ranges 4 90 | Given an empty graph 91 | And having executed: 92 | """ 93 | UNWIND [1, 2, 3] AS i 94 | CREATE ({num: i}) 95 | """ 96 | When executing query: 97 | """ 98 | MATCH (n) 99 | WHERE 1 <= n.num <= 3 100 | RETURN n.num 101 | """ 102 | Then the result should be, in any order: 103 | | n.num | 104 | | 1 | 105 | | 2 | 106 | | 3 | 107 | And no side effects 108 | 109 | Scenario: Handling string ranges 1 110 | Given an empty graph 111 | And having executed: 112 | """ 113 | UNWIND ['a', 'b', 'c'] AS c 114 | CREATE ({name: c}) 115 | """ 116 | When executing query: 117 | """ 118 | MATCH (n) 119 | WHERE 'a' < n.name < 'c' 120 | RETURN n.name 121 | """ 122 | Then the result should be, in any order: 123 | | n.name | 124 | | 'b' | 125 | And no side effects 126 | 127 | Scenario: Handling string ranges 2 128 | Given an empty graph 129 | And having executed: 130 | """ 131 | UNWIND ['a', 'b', 'c'] AS c 132 | CREATE ({name: c}) 133 | """ 134 | When executing query: 135 | """ 136 | MATCH (n) 137 | WHERE 'a' < n.name <= 'c' 138 | RETURN n.name 139 | """ 140 | Then the result should be, in any order: 141 | | n.name | 142 | | 'b' | 143 | | 'c' | 144 | And no side effects 145 | 146 | Scenario: Handling string ranges 3 147 | Given an empty graph 148 | And having executed: 149 | """ 150 | UNWIND ['a', 'b', 'c'] AS c 151 | CREATE ({name: c}) 152 | """ 153 | When executing query: 154 | """ 155 | MATCH (n) 156 | WHERE 'a' <= n.name < 'c' 157 | RETURN n.name 158 | """ 159 | Then the result should be, in any order: 160 | | n.name | 161 | | 'a' | 162 | | 'b' | 163 | And no side effects 164 | 165 | Scenario: Handling string ranges 4 166 | Given an empty graph 167 | And having executed: 168 | """ 169 | UNWIND ['a', 'b', 'c'] AS c 170 | CREATE ({name: c}) 171 | """ 172 | When executing query: 173 | """ 174 | MATCH (n) 175 | WHERE 'a' <= n.name <= 'c' 176 | RETURN n.name 177 | """ 178 | Then the result should be, in any order: 179 | | n.name | 180 | | 'a' | 181 | | 'b' | 182 | | 'c' | 183 | And no side effects 184 | 185 | Scenario: Handling empty range 186 | Given an empty graph 187 | And having executed: 188 | """ 189 | CREATE ({num: 3}) 190 | """ 191 | When executing query: 192 | """ 193 | MATCH (n) 194 | WHERE 10 < n.num <= 3 195 | RETURN n.num 196 | """ 197 | Then the result should be, in any order: 198 | | n.num | 199 | And no side effects 200 | 201 | Scenario: Handling long chains of operators 202 | Given an empty graph 203 | And having executed: 204 | """ 205 | CREATE (a:A {prop1: 3, prop2: 4}) 206 | CREATE (b:B {prop1: 4, prop2: 5}) 207 | CREATE (c:C {prop1: 4, prop2: 4}) 208 | CREATE (a)-[:R]->(b) 209 | CREATE (b)-[:R]->(c) 210 | CREATE (c)-[:R]->(a) 211 | """ 212 | When executing query: 213 | """ 214 | MATCH (n)-->(m) 215 | WHERE n.prop1 < m.prop1 = n.prop2 <> m.prop2 216 | RETURN labels(m) 217 | """ 218 | Then the result should be, in any order: 219 | | labels(m) | 220 | | ['B'] | 221 | And no side effects 222 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate pest; 2 | #[macro_use] 3 | extern crate pest_derive; 4 | #[macro_use] 5 | extern crate anyhow; 6 | 7 | pub mod backend; 8 | pub mod frontend; 9 | 10 | pub use anyhow::{Error, Result}; 11 | use std::fmt::{Debug, Display, Formatter}; 12 | 13 | use backend::{Backend, BackendCursor}; 14 | use core::fmt; 15 | use frontend::Frontend; 16 | 17 | #[derive(Debug)] 18 | pub struct Database { 19 | backend: T, 20 | frontend: Frontend, 21 | } 22 | 23 | impl Database { 24 | pub fn with_backend(backend: T) -> Result> { 25 | let frontend = Frontend { 26 | tokens: backend.tokens(), 27 | backend_desc: backend.describe()?, 28 | }; 29 | Ok(Database { backend, frontend }) 30 | } 31 | 32 | // TODO this is a side-effect, presumably, of me being bad at rust. 33 | // I'd like the public API to not require end-users to specify 34 | // generics everywhere, so they do not have them rewrite their 35 | // code if they swap backend.. but *at the same time* I don't 36 | // want to pay for dynamic dispatch in the hot loop of cursor 37 | // next and accessor methods. 38 | // So.. for now we lose the part where users can change backends 39 | // without rewriting their code. Benchmarking and exploration to 40 | // follow! 41 | pub fn new_cursor(&mut self) -> Cursor { 42 | let bc = self.backend.new_cursor(); 43 | Cursor { inner: bc } 44 | } 45 | 46 | pub fn run(&mut self, query_str: &str, cursor: &mut Cursor) -> Result<()> { 47 | let plan = self.frontend.plan(query_str)?; 48 | self.backend.eval(plan, &mut cursor.inner) 49 | } 50 | } 51 | 52 | // A result cursor; the cursor, when in use, points to a current record and lets you access it. 53 | // It is approximately the same thing as an iterator, except it doesn't need to allocate on each 54 | // iteration. 55 | // 56 | // We are kind of having the same issue as discussed here: 57 | // https://www.reddit.com/r/rust/comments/303a09/looking_for_more_information_on_streaming/cpoysog/ 58 | // 59 | // Eg. this is efficient, but very unergonomic. I think the solution - for now, at least? - 60 | // is to have a "sugared" version where you can get an iterator out of a cursor, so if you are 61 | // ok to pay a small performance penalty you get back the regular Rust iteration API. 62 | // 63 | // In fact, the sugared version should probably be the default thing you interact with, with an 64 | // option to drop down to a non-allocating core API if you like. 65 | // 66 | // If this could return a borrow on next(..), then you may have an API that both feels ergonomic 67 | // and is efficient.. we could at least do this with a try_fold version, which is likely faster 68 | // as well. 69 | // 70 | // TL;DR: This is all up in the air. Design goals are to make zero-allocation *possible* and the 71 | // default API *easy*, potentially by having two APIs. 72 | #[derive(Debug)] 73 | pub struct Cursor { 74 | inner: B::Cursor, 75 | } 76 | 77 | impl Cursor { 78 | pub fn fields(&self) -> Vec { 79 | self.inner.fields() 80 | } 81 | 82 | pub fn next(&mut self) -> Result> { 83 | self.inner.next() 84 | } 85 | } 86 | 87 | #[derive(Debug, Clone)] 88 | pub struct Row { 89 | pub slots: Vec, 90 | } 91 | 92 | #[derive(Debug, Clone, PartialEq)] 93 | pub struct Node { 94 | pub id: usize, 95 | // TODO not forcing massive amounts of variable-length data copying.. 96 | pub labels: Vec, 97 | pub props: Map, 98 | } 99 | 100 | #[derive(Debug, Clone, PartialEq)] 101 | pub struct Rel { 102 | pub start: usize, 103 | pub end: usize, 104 | // TODO not forcing massive amounts of variable-length data copying.. 105 | pub rel_type: String, 106 | pub props: Map, 107 | } 108 | 109 | #[derive(Debug, Clone, PartialEq)] 110 | pub enum Val { 111 | Null, 112 | Int(i64), 113 | Float(f64), 114 | String(String), 115 | Bool(bool), 116 | 117 | Map(Map), 118 | List(Vec), 119 | 120 | Node(Node), 121 | Rel(Rel), 122 | } 123 | 124 | impl Display for Val { 125 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 126 | match self { 127 | Val::Null => f.write_str("NULL"), 128 | Val::Int(v) => f.write_str(&format!("{}", v)), 129 | Val::Float(v) => f.write_str(&format!("{}", v)), 130 | Val::Bool(v) => f.write_str(&format!("{}", v)), 131 | Val::String(s) => f.write_str(&s), 132 | Val::List(vs) => f.write_str(&format!("{:?}", vs)), 133 | Val::Map(v) => f.write_str(&format!("Map{:?}", v)), 134 | Val::Node(v) => f.write_str(&format!("Node({})", v.id)), 135 | Val::Rel(v) => f.write_str(&format!("Rel({}/{})", v.start, v.rel_type)), 136 | } 137 | } 138 | } 139 | 140 | // Don't like this at all 141 | // 1) Forced to copy all those string keys each time we create one of these 142 | // 2) Unergonomic for Rust users, they'd need a HashMap or a BTreeMap or some other random-access, I guess? 143 | // 3) Meh for crossing the FFI boundary 144 | // 145 | // So just here to have something to let work on getting a walking skeleton can continue 146 | pub type Map = Vec<(String, Val)>; 147 | 148 | // Pointer to a Val in a row 149 | pub type Slot = usize; 150 | 151 | // openCypher 9 enumeration of types 152 | #[derive(Debug, Clone)] 153 | pub enum Type { 154 | // This is not a documented part of the openCypher type system, but.. well I'm not sure how 155 | // else we represent the arguments to a function like count(..). 156 | Any, 157 | 158 | // Integers and floats are both Numbers 159 | Number, 160 | // The spec does not specify a maximum bit size, it just says "exact number without decimal" 161 | Integer, 162 | // IEEE-754 64-bit float 163 | Float, 164 | 165 | // Unicode string 166 | String, 167 | Boolean, 168 | 169 | Node, 170 | Relationship, 171 | Path, 172 | 173 | List(Box), 174 | Map, 175 | } 176 | 177 | #[cfg(feature = "gram")] 178 | pub mod gramdb { 179 | use super::{Cursor, Database, Result}; 180 | use crate::backend::gram; 181 | use std::fs::File; 182 | 183 | pub type GramDatabase = Database; 184 | pub type GramCursor = Cursor; 185 | 186 | impl GramDatabase { 187 | pub fn open(file: File) -> Result { 188 | let backend = gram::GramBackend::open(file)?; 189 | Database::with_backend(backend) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /features/backlog/MiscellaneousErrorAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: MiscellaneousErrorAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: Failing on incorrect unicode literal 37 | When executing query: 38 | """ 39 | RETURN '\uH' 40 | """ 41 | Then a SyntaxError should be raised at compile time: InvalidUnicodeLiteral 42 | 43 | Scenario: Failing on merging relationship with null property 44 | When executing query: 45 | """ 46 | CREATE (a), (b) 47 | MERGE (a)-[r:X {num: null}]->(b) 48 | """ 49 | Then a SemanticError should be raised at compile time: MergeReadOwnWrites 50 | 51 | Scenario: Failing on merging node with null property 52 | When executing query: 53 | """ 54 | MERGE ({num: null}) 55 | """ 56 | Then a SemanticError should be raised at compile time: MergeReadOwnWrites 57 | 58 | Scenario: Failing on aggregation in WHERE 59 | When executing query: 60 | """ 61 | MATCH (a) 62 | WHERE count(a) > 10 63 | RETURN a 64 | """ 65 | Then a SyntaxError should be raised at compile time: InvalidAggregation 66 | 67 | Scenario: Failing on aggregation in ORDER BY after RETURN 68 | When executing query: 69 | """ 70 | MATCH (n) 71 | RETURN n.num1 72 | ORDER BY max(n.num2) 73 | """ 74 | Then a SyntaxError should be raised at compile time: InvalidAggregation 75 | 76 | Scenario: Failing on aggregation in ORDER BY after WITH 77 | When executing query: 78 | """ 79 | MATCH (n) 80 | WITH n.num1 AS foo 81 | ORDER BY max(n.num2) 82 | RETURN foo AS foo 83 | """ 84 | Then a SyntaxError should be raised at compile time: InvalidAggregation 85 | 86 | Scenario: Failing when not aliasing expressions in WITH 87 | When executing query: 88 | """ 89 | MATCH (a) 90 | WITH a, count(*) 91 | RETURN a 92 | """ 93 | Then a SyntaxError should be raised at compile time: NoExpressionAlias 94 | 95 | Scenario: Failing when using undefined variable in pattern 96 | When executing query: 97 | """ 98 | MATCH (a) 99 | CREATE (a)-[:KNOWS]->(b {name: missing}) 100 | RETURN b 101 | """ 102 | Then a SyntaxError should be raised at compile time: UndefinedVariable 103 | 104 | Scenario: Failing when using undefined variable in SET 105 | When executing query: 106 | """ 107 | MATCH (a) 108 | SET a.name = missing 109 | RETURN a 110 | """ 111 | Then a SyntaxError should be raised at compile time: UndefinedVariable 112 | 113 | Scenario: Failing when using undefined variable in DELETE 114 | When executing query: 115 | """ 116 | MATCH (a) 117 | DELETE x 118 | """ 119 | Then a SyntaxError should be raised at compile time: UndefinedVariable 120 | 121 | Scenario: Failing when using a variable that is already bound in CREATE 122 | When executing query: 123 | """ 124 | MATCH (a) 125 | CREATE (a {name: 'foo'}) 126 | RETURN a 127 | """ 128 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 129 | 130 | Scenario: Failing when using a path variable that is already bound 131 | When executing query: 132 | """ 133 | MATCH p = (a) 134 | WITH p, a 135 | MATCH p = (a)-->(b) 136 | RETURN a 137 | """ 138 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 139 | 140 | Scenario: Failing when using a list as a node 141 | When executing query: 142 | """ 143 | MATCH (n) 144 | WITH [n] AS users 145 | MATCH (users)-->(messages) 146 | RETURN messages 147 | """ 148 | Then a SyntaxError should be raised at compile time: VariableTypeConflict 149 | 150 | Scenario: Failing when using a variable length relationship as a single relationship 151 | When executing query: 152 | """ 153 | MATCH (n) 154 | MATCH (n)-[r*]->() 155 | WHERE r.name = 'apa' 156 | RETURN r 157 | """ 158 | Then a SyntaxError should be raised at compile time: InvalidArgumentType 159 | 160 | Scenario: Failing when UNION has different columns 161 | When executing query: 162 | """ 163 | RETURN 1 AS a 164 | UNION 165 | RETURN 2 AS b 166 | """ 167 | Then a SyntaxError should be raised at compile time: DifferentColumnsInUnion 168 | 169 | Scenario: Failing when mixing UNION and UNION ALL 170 | When executing query: 171 | """ 172 | RETURN 1 AS a 173 | UNION 174 | RETURN 2 AS a 175 | UNION ALL 176 | RETURN 3 AS a 177 | """ 178 | Then a SyntaxError should be raised at compile time: InvalidClauseComposition 179 | 180 | Scenario: Failing when creating without direction 181 | When executing query: 182 | """ 183 | CREATE (a)-[:FOO]-(b) 184 | """ 185 | Then a SyntaxError should be raised at compile time: RequiresDirectedRelationship 186 | 187 | Scenario: Failing when creating with two directions 188 | When executing query: 189 | """ 190 | CREATE (a)<-[:FOO]->(b) 191 | """ 192 | Then a SyntaxError should be raised at compile time: RequiresDirectedRelationship 193 | 194 | Scenario: Failing when deleting a label 195 | When executing query: 196 | """ 197 | MATCH (n) 198 | DELETE n:Person 199 | """ 200 | Then a SyntaxError should be raised at compile time: InvalidDelete 201 | 202 | Scenario: Failing when setting a list of maps as a property 203 | When executing query: 204 | """ 205 | CREATE (a) 206 | SET a.maplist = [{num: 1}] 207 | """ 208 | Then a TypeError should be raised at compile time: InvalidPropertyType 209 | 210 | Scenario: Failing when multiple columns have the same name 211 | When executing query: 212 | """ 213 | RETURN 1 AS a, 2 AS a 214 | """ 215 | Then a SyntaxError should be raised at compile time: ColumnNameConflict 216 | 217 | Scenario: Failing when using RETURN * without variables in scope 218 | When executing query: 219 | """ 220 | MATCH () 221 | RETURN * 222 | """ 223 | Then a SyntaxError should be raised at compile time: NoVariablesInScope 224 | -------------------------------------------------------------------------------- /features/backlog/TemporalParseAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: TemporalParseAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario Outline: Should parse date from string 37 | When executing query: 38 | """ 39 | RETURN date() AS result 40 | """ 41 | Then the result should be, in any order: 42 | | result | 43 | | | 44 | And no side effects 45 | 46 | Examples: 47 | | str | result | 48 | | '2015-07-21' | '2015-07-21' | 49 | | '20150721' | '2015-07-21' | 50 | | '2015-07' | '2015-07-01' | 51 | | '201507' | '2015-07-01' | 52 | | '2015-W30-2' | '2015-07-21' | 53 | | '2015W302' | '2015-07-21' | 54 | | '2015-W30' | '2015-07-20' | 55 | | '2015W30' | '2015-07-20' | 56 | | '2015-202' | '2015-07-21' | 57 | | '2015202' | '2015-07-21' | 58 | | '2015' | '2015-01-01' | 59 | 60 | Scenario Outline: Should parse local time from string 61 | When executing query: 62 | """ 63 | RETURN localtime() AS result 64 | """ 65 | Then the result should be, in any order: 66 | | result | 67 | | | 68 | And no side effects 69 | 70 | Examples: 71 | | str | result | 72 | | '21:40:32.142' | '21:40:32.142' | 73 | | '214032.142' | '21:40:32.142' | 74 | | '21:40:32' | '21:40:32' | 75 | | '214032' | '21:40:32' | 76 | | '21:40' | '21:40' | 77 | | '2140' | '21:40' | 78 | | '21' | '21:00' | 79 | 80 | Scenario Outline: Should parse time from string 81 | When executing query: 82 | """ 83 | RETURN time() AS result 84 | """ 85 | Then the result should be, in any order: 86 | | result | 87 | | | 88 | And no side effects 89 | 90 | Examples: 91 | | str | result | 92 | | '21:40:32.142+0100' | '21:40:32.142+01:00' | 93 | | '214032.142Z' | '21:40:32.142Z' | 94 | | '21:40:32+01:00' | '21:40:32+01:00' | 95 | | '214032-0100' | '21:40:32-01:00' | 96 | | '21:40-01:30' | '21:40-01:30' | 97 | | '2140-00:00' | '21:40Z' | 98 | | '2140-02' | '21:40-02:00' | 99 | | '22+18:00' | '22:00+18:00' | 100 | 101 | Scenario Outline: Should parse local date time from string 102 | When executing query: 103 | """ 104 | RETURN localdatetime() AS result 105 | """ 106 | Then the result should be, in any order: 107 | | result | 108 | | | 109 | And no side effects 110 | 111 | Examples: 112 | | str | result | 113 | | '2015-07-21T21:40:32.142' | '2015-07-21T21:40:32.142' | 114 | | '2015-W30-2T214032.142' | '2015-07-21T21:40:32.142' | 115 | | '2015-202T21:40:32' | '2015-07-21T21:40:32' | 116 | | '2015T214032' | '2015-01-01T21:40:32' | 117 | | '20150721T21:40' | '2015-07-21T21:40' | 118 | | '2015-W30T2140' | '2015-07-20T21:40' | 119 | | '2015202T21' | '2015-07-21T21:00' | 120 | 121 | Scenario Outline: Should parse date time from string 122 | When executing query: 123 | """ 124 | RETURN datetime() AS result 125 | """ 126 | Then the result should be, in any order: 127 | | result | 128 | | | 129 | And no side effects 130 | 131 | Examples: 132 | | str | result | 133 | | '2015-07-21T21:40:32.142+0100' | '2015-07-21T21:40:32.142+01:00' | 134 | | '2015-W30-2T214032.142Z' | '2015-07-21T21:40:32.142Z' | 135 | | '2015-202T21:40:32+01:00' | '2015-07-21T21:40:32+01:00' | 136 | | '2015T214032-0100' | '2015-01-01T21:40:32-01:00' | 137 | | '20150721T21:40-01:30' | '2015-07-21T21:40-01:30' | 138 | | '2015-W30T2140-00:00' | '2015-07-20T21:40Z' | 139 | | '2015-W30T2140-02' | '2015-07-20T21:40-02:00' | 140 | | '2015202T21+18:00' | '2015-07-21T21:00+18:00' | 141 | 142 | Scenario Outline: Should parse date time with named time zone from string 143 | When executing query: 144 | """ 145 | RETURN datetime() AS result 146 | """ 147 | Then the result should be, in any order: 148 | | result | 149 | | | 150 | And no side effects 151 | 152 | Examples: 153 | | str | result | 154 | | '2015-07-21T21:40:32.142+02:00[Europe/Stockholm]' | '2015-07-21T21:40:32.142+02:00[Europe/Stockholm]' | 155 | | '2015-07-21T21:40:32.142+0845[Australia/Eucla]' | '2015-07-21T21:40:32.142+08:45[Australia/Eucla]' | 156 | | '2015-07-21T21:40:32.142-04[America/New_York]' | '2015-07-21T21:40:32.142-04:00[America/New_York]' | 157 | | '2015-07-21T21:40:32.142[Europe/London]' | '2015-07-21T21:40:32.142+01:00[Europe/London]' | 158 | | '1818-07-21T21:40:32.142[Europe/Stockholm]' | '1818-07-21T21:40:32.142+01:12:12[Europe/Stockholm]' | 159 | 160 | Scenario Outline: Should parse duration from string 161 | When executing query: 162 | """ 163 | RETURN duration() AS result 164 | """ 165 | Then the result should be, in any order: 166 | | result | 167 | | | 168 | And no side effects 169 | 170 | Examples: 171 | | str | result | 172 | | 'P14DT16H12M' | 'P14DT16H12M' | 173 | | 'P5M1.5D' | 'P5M1DT12H' | 174 | | 'P0.75M' | 'P22DT19H51M49.5S' | 175 | | 'PT0.75M' | 'PT45S' | 176 | | 'P2.5W' | 'P17DT12H' | 177 | | 'P12Y5M14DT16H12M70S' | 'P12Y5M14DT16H13M10S' | 178 | | 'P2012-02-02T14:37:21.545' | 'P2012Y2M2DT14H37M21.545S' | 179 | -------------------------------------------------------------------------------- /features/backlog/SkipLimitAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: SkipLimitAcceptanceTest 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario: SKIP with an expression that depends on variables should fail 37 | When executing query: 38 | """ 39 | MATCH (n) RETURN n SKIP n.count 40 | """ 41 | Then a SyntaxError should be raised at compile time: NonConstantExpression 42 | 43 | Scenario: LIMIT with an expression that depends on variables should fail 44 | When executing query: 45 | """ 46 | MATCH (n) RETURN n LIMIT n.count 47 | """ 48 | Then a SyntaxError should be raised at compile time: NonConstantExpression 49 | 50 | Scenario: SKIP with an expression that does not depend on variables 51 | Given having executed: 52 | """ 53 | UNWIND range(1, 10) AS i 54 | CREATE ({nr: i}) 55 | """ 56 | When executing query: 57 | """ 58 | MATCH (n) 59 | WITH n SKIP toInteger(rand()*9) 60 | WITH count(*) AS count 61 | RETURN count > 0 AS nonEmpty 62 | """ 63 | Then the result should be, in any order: 64 | | nonEmpty | 65 | | true | 66 | And no side effects 67 | 68 | 69 | Scenario: LIMIT with an expression that does not depend on variables 70 | Given having executed: 71 | """ 72 | UNWIND range(1, 3) AS i 73 | CREATE ({nr: i}) 74 | """ 75 | When executing query: 76 | """ 77 | MATCH (n) 78 | WITH n LIMIT toInteger(ceil(1.7)) 79 | RETURN count(*) AS count 80 | """ 81 | Then the result should be, in any order: 82 | | count | 83 | | 2 | 84 | And no side effects 85 | 86 | Scenario: Negative parameter for LIMIT should fail 87 | Given having executed: 88 | """ 89 | CREATE (s:Person {name: 'Steven'}), 90 | (c:Person {name: 'Craig'}) 91 | """ 92 | And parameters are: 93 | | _limit | -1 | 94 | When executing query: 95 | """ 96 | MATCH (p:Person) 97 | RETURN p.name AS name 98 | LIMIT $_limit 99 | """ 100 | Then a SyntaxError should be raised at runtime: NegativeIntegerArgument 101 | 102 | Scenario: Negative parameter for LIMIT with ORDER BY should fail 103 | Given having executed: 104 | """ 105 | CREATE (s:Person {name: 'Steven'}), 106 | (c:Person {name: 'Craig'}) 107 | """ 108 | And parameters are: 109 | | _limit | -1 | 110 | When executing query: 111 | """ 112 | MATCH (p:Person) 113 | RETURN p.name AS name 114 | ORDER BY name LIMIT $_limit 115 | """ 116 | Then a SyntaxError should be raised at runtime: NegativeIntegerArgument 117 | 118 | Scenario: Negative LIMIT should fail 119 | Given having executed: 120 | """ 121 | CREATE (s:Person {name: 'Steven'}), 122 | (c:Person {name: 'Craig'}) 123 | """ 124 | When executing query: 125 | """ 126 | MATCH (p:Person) 127 | RETURN p.name AS name 128 | LIMIT -1 129 | """ 130 | Then a SyntaxError should be raised at compile time: NegativeIntegerArgument 131 | 132 | Scenario: Negative parameter for SKIP should fail 133 | Given having executed: 134 | """ 135 | CREATE (s:Person {name: 'Steven'}), 136 | (c:Person {name: 'Craig'}) 137 | """ 138 | And parameters are: 139 | | _skip | -1 | 140 | When executing query: 141 | """ 142 | MATCH (p:Person) 143 | RETURN p.name AS name 144 | SKIP $_skip 145 | """ 146 | Then a SyntaxError should be raised at runtime: NegativeIntegerArgument 147 | 148 | Scenario: Negative SKIP should fail 149 | Given having executed: 150 | """ 151 | CREATE (s:Person {name: 'Steven'}), 152 | (c:Person {name: 'Craig'}) 153 | """ 154 | When executing query: 155 | """ 156 | MATCH (p:Person) 157 | RETURN p.name AS name 158 | SKIP -1 159 | """ 160 | Then a SyntaxError should be raised at compile time: NegativeIntegerArgument 161 | 162 | Scenario: Floating point parameter for LIMIT should fail 163 | Given having executed: 164 | """ 165 | CREATE (s:Person {name: 'Steven'}), 166 | (c:Person {name: 'Craig'}) 167 | """ 168 | And parameters are: 169 | | _limit | 1.5 | 170 | When executing query: 171 | """ 172 | MATCH (p:Person) 173 | RETURN p.name AS name 174 | LIMIT $_limit 175 | """ 176 | Then a SyntaxError should be raised at runtime: InvalidArgumentType 177 | 178 | Scenario: Floating point parameter for LIMIT with ORDER BY should fail 179 | Given having executed: 180 | """ 181 | CREATE (s:Person {name: 'Steven'}), 182 | (c:Person {name: 'Craig'}) 183 | """ 184 | And parameters are: 185 | | _limit | 1.5 | 186 | When executing query: 187 | """ 188 | MATCH (p:Person) 189 | RETURN p.name AS name 190 | ORDER BY name LIMIT $_limit 191 | """ 192 | Then a SyntaxError should be raised at runtime: InvalidArgumentType 193 | 194 | Scenario: Floating point LIMIT should fail 195 | Given having executed: 196 | """ 197 | CREATE (s:Person {name: 'Steven'}), 198 | (c:Person {name: 'Craig'}) 199 | """ 200 | When executing query: 201 | """ 202 | MATCH (p:Person) 203 | RETURN p.name AS name 204 | LIMIT 1.5 205 | """ 206 | Then a SyntaxError should be raised at compile time: InvalidArgumentType 207 | 208 | Scenario: Floating point parameter for SKIP should fail 209 | Given having executed: 210 | """ 211 | CREATE (s:Person {name: 'Steven'}), 212 | (c:Person {name: 'Craig'}) 213 | """ 214 | And parameters are: 215 | | _limit | 1.5 | 216 | When executing query: 217 | """ 218 | MATCH (p:Person) 219 | RETURN p.name AS name 220 | SKIP $_limit 221 | """ 222 | Then a SyntaxError should be raised at runtime: InvalidArgumentType 223 | 224 | Scenario: Floating point SKIP should fail 225 | Given having executed: 226 | """ 227 | CREATE (s:Person {name: 'Steven'}), 228 | (c:Person {name: 'Craig'}) 229 | """ 230 | When executing query: 231 | """ 232 | MATCH (p:Person) 233 | RETURN p.name AS name 234 | SKIP 1.5 235 | """ 236 | Then a SyntaxError should be raised at compile time: InvalidArgumentType 237 | -------------------------------------------------------------------------------- /features/backlog/LabelsAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: LabelsAcceptance 32 | 33 | Background: 34 | Given an empty graph 35 | 36 | Scenario: Adding a single label 37 | Given having executed: 38 | """ 39 | CREATE () 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (n) 44 | SET n:Foo 45 | RETURN labels(n) 46 | """ 47 | Then the result should be, in any order: 48 | | labels(n) | 49 | | ['Foo'] | 50 | And the side effects should be: 51 | | +labels | 1 | 52 | 53 | Scenario: Ignore space before colon 54 | Given having executed: 55 | """ 56 | CREATE () 57 | """ 58 | When executing query: 59 | """ 60 | MATCH (n) 61 | SET n :Foo 62 | RETURN labels(n) 63 | """ 64 | Then the result should be, in any order: 65 | | labels(n) | 66 | | ['Foo'] | 67 | And the side effects should be: 68 | | +labels | 1 | 69 | 70 | Scenario: Adding multiple labels 71 | Given having executed: 72 | """ 73 | CREATE () 74 | """ 75 | When executing query: 76 | """ 77 | MATCH (n) 78 | SET n:Foo:Bar 79 | RETURN labels(n) 80 | """ 81 | Then the result should be, in any order: 82 | | labels(n) | 83 | | ['Foo', 'Bar'] | 84 | And the side effects should be: 85 | | +labels | 2 | 86 | 87 | Scenario: Ignoring intermediate whitespace 1 88 | Given having executed: 89 | """ 90 | CREATE () 91 | """ 92 | When executing query: 93 | """ 94 | MATCH (n) 95 | SET n :Foo :Bar 96 | RETURN labels(n) 97 | """ 98 | Then the result should be, in any order: 99 | | labels(n) | 100 | | ['Foo', 'Bar'] | 101 | And the side effects should be: 102 | | +labels | 2 | 103 | 104 | Scenario: Ignoring intermediate whitespace 2 105 | Given having executed: 106 | """ 107 | CREATE () 108 | """ 109 | When executing query: 110 | """ 111 | MATCH (n) 112 | SET n :Foo:Bar 113 | RETURN labels(n) 114 | """ 115 | Then the result should be, in any order: 116 | | labels(n) | 117 | | ['Foo', 'Bar'] | 118 | And the side effects should be: 119 | | +labels | 2 | 120 | 121 | Scenario: Creating node without label 122 | When executing query: 123 | """ 124 | CREATE (node) 125 | RETURN labels(node) 126 | """ 127 | Then the result should be, in any order: 128 | | labels(node) | 129 | | [] | 130 | And the side effects should be: 131 | | +nodes | 1 | 132 | 133 | Scenario: Creating node with two labels 134 | When executing query: 135 | """ 136 | CREATE (node:Foo:Bar {name: 'Mattias'}) 137 | RETURN labels(node) 138 | """ 139 | Then the result should be, in any order: 140 | | labels(node) | 141 | | ['Foo', 'Bar'] | 142 | And the side effects should be: 143 | | +nodes | 1 | 144 | | +labels | 2 | 145 | | +properties | 1 | 146 | 147 | Scenario: Ignore space when creating node with labels 148 | When executing query: 149 | """ 150 | CREATE (node :Foo:Bar) 151 | RETURN labels(node) 152 | """ 153 | Then the result should be, in any order: 154 | | labels(node) | 155 | | ['Foo', 'Bar'] | 156 | And the side effects should be: 157 | | +nodes | 1 | 158 | | +labels | 2 | 159 | 160 | Scenario: Create node with label in pattern 161 | When executing query: 162 | """ 163 | CREATE (n:Person)-[:OWNS]->(:Dog) 164 | RETURN labels(n) 165 | """ 166 | Then the result should be, in any order: 167 | | labels(n) | 168 | | ['Person'] | 169 | And the side effects should be: 170 | | +nodes | 2 | 171 | | +relationships | 1 | 172 | | +labels | 2 | 173 | 174 | Scenario: Fail when adding a new label predicate on a node that is already bound 1 175 | When executing query: 176 | """ 177 | CREATE (n:Foo)-[:T1]->(), 178 | (n:Bar)-[:T2]->() 179 | """ 180 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 181 | 182 | Scenario: Fail when adding new label predicate on a node that is already bound 2 183 | When executing query: 184 | """ 185 | CREATE ()<-[:T2]-(n:Foo), 186 | (n:Bar)<-[:T1]-() 187 | """ 188 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 189 | 190 | Scenario: Fail when adding new label predicate on a node that is already bound 3 191 | When executing query: 192 | """ 193 | CREATE (n:Foo) 194 | CREATE (n:Bar)-[:OWNS]->(:Dog) 195 | """ 196 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 197 | 198 | Scenario: Fail when adding new label predicate on a node that is already bound 4 199 | When executing query: 200 | """ 201 | CREATE (n {}) 202 | CREATE (n:Bar)-[:OWNS]->(:Dog) 203 | """ 204 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 205 | 206 | Scenario: Fail when adding new label predicate on a node that is already bound 5 207 | When executing query: 208 | """ 209 | CREATE (n:Foo) 210 | CREATE (n {})-[:OWNS]->(:Dog) 211 | """ 212 | Then a SyntaxError should be raised at compile time: VariableAlreadyBound 213 | 214 | Scenario: Using `labels()` in return clauses 215 | Given having executed: 216 | """ 217 | CREATE () 218 | """ 219 | When executing query: 220 | """ 221 | MATCH (n) 222 | RETURN labels(n) 223 | """ 224 | Then the result should be, in any order: 225 | | labels(n) | 226 | | [] | 227 | And no side effects 228 | 229 | Scenario: Removing a label 230 | Given having executed: 231 | """ 232 | CREATE (:Foo:Bar) 233 | """ 234 | When executing query: 235 | """ 236 | MATCH (n) 237 | REMOVE n:Foo 238 | RETURN labels(n) 239 | """ 240 | Then the result should be, in any order: 241 | | labels(n) | 242 | | ['Bar'] | 243 | And the side effects should be: 244 | | -labels | 1 | 245 | 246 | Scenario: Removing a non-existent label 247 | Given having executed: 248 | """ 249 | CREATE (:Foo) 250 | """ 251 | When executing query: 252 | """ 253 | MATCH (n) 254 | REMOVE n:Bar 255 | RETURN labels(n) 256 | """ 257 | Then the result should be, in any order: 258 | | labels(n) | 259 | | ['Foo'] | 260 | And no side effects 261 | -------------------------------------------------------------------------------- /features/backlog/TemporalComparisonAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: TemporalComparisonAcceptance 32 | 33 | Background: 34 | Given any graph 35 | 36 | Scenario Outline: Should compare dates 37 | When executing query: 38 | """ 39 | WITH date() AS x, date() AS d 40 | RETURN x > d, x < d, x >= d, x <= d, x = d 41 | """ 42 | Then the result should be, in any order: 43 | | x > d | x < d | x >= d | x <= d | x = d | 44 | | | | | | | 45 | And no side effects 46 | 47 | Examples: 48 | | map | map2 | gt | lt | ge | le | e | 49 | | {year: 1980, month: 12, day: 24} | {year: 1984, month: 10, day: 11} | false | true | false | true | false | 50 | | {year: 1984, month: 10, day: 11} | {year: 1984, month: 10, day: 11} | false | false | true | true | true | 51 | 52 | Scenario Outline: Should compare local times 53 | When executing query: 54 | """ 55 | WITH localtime() AS x, localtime() AS d 56 | RETURN x > d, x < d, x >= d, x <= d, x = d 57 | """ 58 | Then the result should be, in any order: 59 | | x > d | x < d | x >= d | x <= d | x = d | 60 | | | | | | | 61 | And no side effects 62 | 63 | Examples: 64 | | map | map2 | gt | lt | ge | le | e | 65 | | {hour: 10, minute: 35} | {hour: 12, minute: 31, second: 14, nanosecond: 645876123} | false | true | false | true | false | 66 | | {hour: 12, minute: 31, second: 14, nanosecond: 645876123} | {hour: 12, minute: 31, second: 14, nanosecond: 645876123} | false | false | true | true | true | 67 | 68 | Scenario Outline: Should compare times 69 | When executing query: 70 | """ 71 | WITH time() AS x, time() AS d 72 | RETURN x > d, x < d, x >= d, x <= d, x = d 73 | """ 74 | Then the result should be, in any order: 75 | | x > d | x < d | x >= d | x <= d | x = d | 76 | | | | | | | 77 | And no side effects 78 | 79 | Examples: 80 | | map | map2 | gt | lt | ge | le | e | 81 | | {hour: 10, minute: 0, timezone: '+01:00'} | {hour: 9, minute: 35, second: 14, nanosecond: 645876123, timezone: '+00:00'} | false | true | false | true | false | 82 | | {hour: 9, minute: 35, second: 14, nanosecond: 645876123, timezone: '+00:00'} | {hour: 9, minute: 35, second: 14, nanosecond: 645876123, timezone: '+00:00'} | false | false | true | true | true | 83 | 84 | Scenario Outline: Should compare local date times 85 | When executing query: 86 | """ 87 | WITH localdatetime() AS x, localdatetime() AS d 88 | RETURN x > d, x < d, x >= d, x <= d, x = d 89 | """ 90 | Then the result should be, in any order: 91 | | x > d | x < d | x >= d | x <= d | x = d | 92 | | | | | | | 93 | And no side effects 94 | 95 | Examples: 96 | | map | map2 | gt | lt | ge | le | e | 97 | | {year: 1980, month: 12, day: 11, hour: 12, minute: 31, second: 14} | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123} | false | true | false | true | false | 98 | | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123} | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123} | false | false | true | true | true | 99 | 100 | Scenario Outline: Should compare date times 101 | When executing query: 102 | """ 103 | WITH datetime() AS x, datetime() AS d 104 | RETURN x > d, x < d, x >= d, x <= d, x = d 105 | """ 106 | Then the result should be, in any order: 107 | | x > d | x < d | x >= d | x <= d | x = d | 108 | | | | | | | 109 | And no side effects 110 | 111 | Examples: 112 | | map | map2 | gt | lt | ge | le | e | 113 | | {year: 1980, month: 12, day: 11, hour: 12, minute: 31, second: 14, timezone: '+00:00'} | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+05:00'} | false | true | false | true | false | 114 | | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+05:00'} | {year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+05:00'} | false | false | true | true | true | 115 | 116 | Scenario Outline: Should compare durations for equality 117 | When executing query: 118 | """ 119 | WITH duration({years: 12, months: 5, days: 14, hours: 16, minutes: 12, seconds: 70}) AS x, AS d 120 | RETURN x = d 121 | """ 122 | Then the result should be, in any order: 123 | | x = d | 124 | | | 125 | And no side effects 126 | 127 | Examples: 128 | | other | e | 129 | | date({year: 1984, month: 10, day: 11}) | false | 130 | | localtime({hour: 12, minute: 31, second: 14, nanosecond: 645876123}) | false | 131 | | time({hour: 9, minute: 35, second: 14, nanosecond: 645876123, timezone: '+00:00'}) | false | 132 | | localdatetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123}) | false | 133 | | datetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+05:00'}) | false | 134 | | duration({years: 12, months: 5, days: 14, hours: 16, minutes: 12, seconds: 70}) | true | 135 | | duration({years: 12, months: 5, days: 14, hours: 16, minutes: 13, seconds: 10}) | true | 136 | | duration({years: 12, months: 5, days: 13, hours: 40, minutes: 13, seconds: 10}) | false | -------------------------------------------------------------------------------- /features/backlog/TemporalAccessorAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: TemporalAccessorAcceptance 32 | 33 | Background: 34 | Given an empty graph 35 | 36 | Scenario: Should provide accessors for date 37 | Given having executed: 38 | """ 39 | CREATE (:Val {date: date({year: 1984, month: 10, day: 11})}) 40 | """ 41 | When executing query: 42 | """ 43 | MATCH (v:Val) 44 | WITH v.date AS d 45 | RETURN d.year, d.quarter, d.month, d.week, d.weekYear, d.day, d.ordinalDay, d.weekDay, d.dayOfQuarter 46 | """ 47 | Then the result should be, in any order: 48 | | d.year | d.quarter | d.month | d.week | d.weekYear | d.day | d.ordinalDay | d.weekDay | d.dayOfQuarter | 49 | | 1984 | 4 | 10 | 41 | 1984 | 11 | 285 | 4 | 11 | 50 | And no side effects 51 | 52 | Scenario: Should provide accessors for date in last weekYear 53 | Given having executed: 54 | """ 55 | CREATE (:Val {date: date({year: 1984, month: 01, day: 01})}) 56 | """ 57 | When executing query: 58 | """ 59 | MATCH (v:Val) 60 | WITH v.date AS d 61 | RETURN d.year, d.weekYear, d.week, d.weekDay 62 | """ 63 | Then the result should be, in any order: 64 | | d.year | d.weekYear | d.week | d.weekDay | 65 | | 1984 | 1983 | 52 | 7 | 66 | And no side effects 67 | 68 | Scenario: Should provide accessors for local time 69 | Given having executed: 70 | """ 71 | CREATE (:Val {date: localtime({hour: 12, minute: 31, second: 14, nanosecond: 645876123})}) 72 | """ 73 | When executing query: 74 | """ 75 | MATCH (v:Val) 76 | WITH v.date AS d 77 | RETURN d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond 78 | """ 79 | Then the result should be, in any order: 80 | | d.hour | d.minute | d.second | d.millisecond | d.microsecond | d.nanosecond | 81 | | 12 | 31 | 14 | 645 | 645876 | 645876123 | 82 | And no side effects 83 | 84 | Scenario: Should provide accessors for time 85 | Given having executed: 86 | """ 87 | CREATE (:Val {date: time({hour: 12, minute: 31, second: 14, nanosecond: 645876123, timezone: '+01:00'})}) 88 | """ 89 | When executing query: 90 | """ 91 | MATCH (v:Val) 92 | WITH v.date AS d 93 | RETURN d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond, d.timezone, d.offset, d.offsetMinutes, d.offsetSeconds 94 | """ 95 | Then the result should be, in any order: 96 | | d.hour | d.minute | d.second | d.millisecond | d.microsecond | d.nanosecond | d.timezone | d.offset | d.offsetMinutes | d.offsetSeconds | 97 | | 12 | 31 | 14 | 645 | 645876 | 645876123 | '+01:00' | '+01:00' | 60 | 3600 | 98 | And no side effects 99 | 100 | Scenario: Should provide accessors for local date time 101 | Given having executed: 102 | """ 103 | CREATE (:Val {date: localdatetime({year: 1984, month: 11, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123})}) 104 | """ 105 | When executing query: 106 | """ 107 | MATCH (v:Val) 108 | WITH v.date AS d 109 | RETURN d.year, d.quarter, d.month, d.week, d.weekYear, d.day, d.ordinalDay, d.weekDay, d.dayOfQuarter, 110 | d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond 111 | """ 112 | Then the result should be, in any order: 113 | | d.year | d.quarter | d.month | d.week | d.weekYear | d.day | d.ordinalDay | d.weekDay | d.dayOfQuarter | d.hour | d.minute | d.second | d.millisecond | d.microsecond | d.nanosecond | 114 | | 1984 | 4 | 11 | 45 | 1984 | 11 | 316 | 7 | 42 | 12 | 31 | 14 | 645 | 645876 | 645876123 | 115 | And no side effects 116 | 117 | Scenario: Should provide accessors for date time 118 | Given having executed: 119 | """ 120 | CREATE (:Val {date: datetime({year: 1984, month: 11, day: 11, hour: 12, minute: 31, second: 14, nanosecond: 645876123, timezone: 'Europe/Stockholm'})}) 121 | """ 122 | When executing query: 123 | """ 124 | MATCH (v:Val) 125 | WITH v.date AS d 126 | RETURN d.year, d.quarter, d.month, d.week, d.weekYear, d.day, d.ordinalDay, d.weekDay, d.dayOfQuarter, 127 | d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond, 128 | d.timezone, d.offset, d.offsetMinutes, d.offsetSeconds, d.epochSeconds, d.epochMillis 129 | """ 130 | Then the result should be, in any order: 131 | | d.year | d.quarter | d.month | d.week | d.weekYear | d.day | d.ordinalDay | d.weekDay | d.dayOfQuarter | d.hour | d.minute | d.second | d.millisecond | d.microsecond | d.nanosecond | d.timezone | d.offset | d.offsetMinutes | d.offsetSeconds | d.epochSeconds | d.epochMillis | 132 | | 1984 | 4 | 11 | 45 | 1984 | 11 | 316 | 7 | 42 | 12 | 31 | 14 | 645 | 645876 | 645876123 | 'Europe/Stockholm' | '+01:00' | 60 | 3600 | 469020674 | 469020674645 | 133 | And no side effects 134 | 135 | Scenario: Should provide accessors for duration 136 | Given having executed: 137 | """ 138 | CREATE (:Val {date: duration({years: 1, months: 4, days: 10, hours: 1, minutes: 1, seconds: 1, nanoseconds: 111111111})}) 139 | """ 140 | When executing query: 141 | """ 142 | MATCH (v:Val) 143 | WITH v.date AS d 144 | RETURN d.years, d.quarters, d.months, d.weeks, d.days, 145 | d.hours, d.minutes, d.seconds, d.milliseconds, d.microseconds, d.nanoseconds, 146 | d.quartersOfYear, d.monthsOfQuarter, d.monthsOfYear, d.daysOfWeek, d.minutesOfHour, d.secondsOfMinute, d.millisecondsOfSecond, d.microsecondsOfSecond, d.nanosecondsOfSecond 147 | """ 148 | Then the result should be, in any order: 149 | | d.years | d.quarters | d.months | d.weeks | d.days | d.hours | d.minutes | d.seconds | d.milliseconds | d.microseconds | d.nanoseconds | d.quartersOfYear | d.monthsOfQuarter | d.monthsOfYear | d.daysOfWeek | d.minutesOfHour | d.secondsOfMinute | d.millisecondsOfSecond | d.microsecondsOfSecond | d.nanosecondsOfSecond | 150 | | 1 | 5 | 16 | 1 | 10 | 1 | 61 | 3661 | 3661111 | 3661111111 | 3661111111111 | 1 | 1 | 4 | 3 | 1 | 1 | 111 | 111111 | 111111111 | 151 | And no side effects 152 | -------------------------------------------------------------------------------- /features/backlog/UnwindAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: UnwindAcceptance 32 | 33 | Scenario: Unwinding a list 34 | Given any graph 35 | When executing query: 36 | """ 37 | UNWIND [1, 2, 3] AS x 38 | RETURN x 39 | """ 40 | Then the result should be, in any order: 41 | | x | 42 | | 1 | 43 | | 2 | 44 | | 3 | 45 | And no side effects 46 | 47 | Scenario: Unwinding a range 48 | Given any graph 49 | When executing query: 50 | """ 51 | UNWIND range(1, 3) AS x 52 | RETURN x 53 | """ 54 | Then the result should be, in any order: 55 | | x | 56 | | 1 | 57 | | 2 | 58 | | 3 | 59 | And no side effects 60 | 61 | Scenario: Unwinding a concatenation of lists 62 | Given any graph 63 | When executing query: 64 | """ 65 | WITH [1, 2, 3] AS first, [4, 5, 6] AS second 66 | UNWIND (first + second) AS x 67 | RETURN x 68 | """ 69 | Then the result should be, in any order: 70 | | x | 71 | | 1 | 72 | | 2 | 73 | | 3 | 74 | | 4 | 75 | | 5 | 76 | | 6 | 77 | And no side effects 78 | 79 | Scenario: Unwinding a collected unwound expression 80 | Given any graph 81 | When executing query: 82 | """ 83 | UNWIND RANGE(1, 2) AS row 84 | WITH collect(row) AS rows 85 | UNWIND rows AS x 86 | RETURN x 87 | """ 88 | Then the result should be, in any order: 89 | | x | 90 | | 1 | 91 | | 2 | 92 | And no side effects 93 | 94 | Scenario: Unwinding a collected expression 95 | Given an empty graph 96 | And having executed: 97 | """ 98 | CREATE ({id: 1}), ({id: 2}) 99 | """ 100 | When executing query: 101 | """ 102 | MATCH (row) 103 | WITH collect(row) AS rows 104 | UNWIND rows AS node 105 | RETURN node.id 106 | """ 107 | Then the result should be, in any order: 108 | | node.id | 109 | | 1 | 110 | | 2 | 111 | And no side effects 112 | 113 | Scenario: Creating nodes from an unwound parameter list 114 | Given an empty graph 115 | And having executed: 116 | """ 117 | CREATE (:Year {year: 2016}) 118 | """ 119 | And parameters are: 120 | | events | [{year: 2016, id: 1}, {year: 2016, id: 2}] | 121 | When executing query: 122 | """ 123 | UNWIND $events AS event 124 | MATCH (y:Year {year: event.year}) 125 | MERGE (e:Event {id: event.id}) 126 | MERGE (y)<-[:IN]-(e) 127 | RETURN e.id AS x 128 | ORDER BY x 129 | """ 130 | Then the result should be, in order: 131 | | x | 132 | | 1 | 133 | | 2 | 134 | And the side effects should be: 135 | | +nodes | 2 | 136 | | +relationships | 2 | 137 | | +labels | 1 | 138 | | +properties | 2 | 139 | 140 | Scenario: Double unwinding a list of lists 141 | Given any graph 142 | When executing query: 143 | """ 144 | WITH [[1, 2, 3], [4, 5, 6]] AS lol 145 | UNWIND lol AS x 146 | UNWIND x AS y 147 | RETURN y 148 | """ 149 | Then the result should be, in any order: 150 | | y | 151 | | 1 | 152 | | 2 | 153 | | 3 | 154 | | 4 | 155 | | 5 | 156 | | 6 | 157 | And no side effects 158 | 159 | Scenario: Unwinding the empty list 160 | Given any graph 161 | When executing query: 162 | """ 163 | UNWIND [] AS empty 164 | RETURN empty 165 | """ 166 | Then the result should be, in any order: 167 | | empty | 168 | And no side effects 169 | 170 | Scenario: Unwinding null 171 | Given any graph 172 | When executing query: 173 | """ 174 | UNWIND null AS nil 175 | RETURN nil 176 | """ 177 | Then the result should be, in any order: 178 | | nil | 179 | And no side effects 180 | 181 | Scenario: Unwinding list with duplicates 182 | Given any graph 183 | When executing query: 184 | """ 185 | UNWIND [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] AS duplicate 186 | RETURN duplicate 187 | """ 188 | Then the result should be, in any order: 189 | | duplicate | 190 | | 1 | 191 | | 1 | 192 | | 2 | 193 | | 2 | 194 | | 3 | 195 | | 3 | 196 | | 4 | 197 | | 4 | 198 | | 5 | 199 | | 5 | 200 | And no side effects 201 | 202 | Scenario: Unwind does not prune context 203 | Given any graph 204 | When executing query: 205 | """ 206 | WITH [1, 2, 3] AS list 207 | UNWIND list AS x 208 | RETURN * 209 | """ 210 | Then the result should be, in any order: 211 | | list | x | 212 | | [1, 2, 3] | 1 | 213 | | [1, 2, 3] | 2 | 214 | | [1, 2, 3] | 3 | 215 | And no side effects 216 | 217 | Scenario: Unwind does not remove variables from scope 218 | Given an empty graph 219 | And having executed: 220 | """ 221 | CREATE (s:S), 222 | (n), 223 | (e:E), 224 | (s)-[:X]->(e), 225 | (s)-[:Y]->(e), 226 | (n)-[:Y]->(e) 227 | """ 228 | When executing query: 229 | """ 230 | MATCH (a:S)-[:X]->(b1) 231 | WITH a, collect(b1) AS bees 232 | UNWIND bees AS b2 233 | MATCH (a)-[:Y]->(b2) 234 | RETURN a, b2 235 | """ 236 | Then the result should be, in any order: 237 | | a | b2 | 238 | | (:S) | (:E) | 239 | And no side effects 240 | 241 | Scenario: Multiple unwinds after each other 242 | Given any graph 243 | When executing query: 244 | """ 245 | WITH [1, 2] AS xs, [3, 4] AS ys, [5, 6] AS zs 246 | UNWIND xs AS x 247 | UNWIND ys AS y 248 | UNWIND zs AS z 249 | RETURN * 250 | """ 251 | Then the result should be, in any order: 252 | | x | xs | y | ys | z | zs | 253 | | 1 | [1, 2] | 3 | [3, 4] | 5 | [5, 6] | 254 | | 1 | [1, 2] | 3 | [3, 4] | 6 | [5, 6] | 255 | | 1 | [1, 2] | 4 | [3, 4] | 5 | [5, 6] | 256 | | 1 | [1, 2] | 4 | [3, 4] | 6 | [5, 6] | 257 | | 2 | [1, 2] | 3 | [3, 4] | 5 | [5, 6] | 258 | | 2 | [1, 2] | 3 | [3, 4] | 6 | [5, 6] | 259 | | 2 | [1, 2] | 4 | [3, 4] | 5 | [5, 6] | 260 | | 2 | [1, 2] | 4 | [3, 4] | 6 | [5, 6] | 261 | And no side effects 262 | 263 | Scenario: Unwind with merge 264 | Given an empty graph 265 | And parameters are: 266 | | props | [{login: 'login1', name: 'name1'}, {login: 'login2', name: 'name2'}] | 267 | When executing query: 268 | """ 269 | UNWIND $props AS prop 270 | MERGE (p:Person {login: prop.login}) 271 | SET p.name = prop.name 272 | RETURN p.name, p.login 273 | """ 274 | Then the result should be, in any order: 275 | | p.name | p.login | 276 | | 'name1' | 'login1' | 277 | | 'name2' | 'login2' | 278 | And the side effects should be: 279 | | +nodes | 2 | 280 | | +labels | 1 | 281 | | +properties | 4 | 282 | -------------------------------------------------------------------------------- /features/backlog/ReturnAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ReturnAcceptanceTest 32 | 33 | Scenario: Allow addition 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({id: 1337, version: 99}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (a) 42 | WHERE a.id = 1337 43 | RETURN a.version + 5 44 | """ 45 | Then the result should be, in any order: 46 | | a.version + 5 | 47 | | 104 | 48 | And no side effects 49 | 50 | Scenario: Limit to two hits 51 | Given an empty graph 52 | When executing query: 53 | """ 54 | UNWIND [1, 1, 1, 1, 1] AS i 55 | RETURN i 56 | LIMIT 2 57 | """ 58 | Then the result should be, in any order: 59 | | i | 60 | | 1 | 61 | | 1 | 62 | And no side effects 63 | 64 | Scenario: Limit to two hits with explicit order 65 | Given an empty graph 66 | And having executed: 67 | """ 68 | CREATE ({name: 'A'}), 69 | ({name: 'B'}), 70 | ({name: 'C'}), 71 | ({name: 'D'}), 72 | ({name: 'E'}) 73 | """ 74 | When executing query: 75 | """ 76 | MATCH (n) 77 | RETURN n 78 | ORDER BY n.name ASC 79 | LIMIT 2 80 | """ 81 | Then the result should be, in any order: 82 | | n | 83 | | ({name: 'A'}) | 84 | | ({name: 'B'}) | 85 | And no side effects 86 | 87 | Scenario: Start the result from the second row 88 | Given an empty graph 89 | And having executed: 90 | """ 91 | CREATE ({name: 'A'}), 92 | ({name: 'B'}), 93 | ({name: 'C'}), 94 | ({name: 'D'}), 95 | ({name: 'E'}) 96 | """ 97 | When executing query: 98 | """ 99 | MATCH (n) 100 | RETURN n 101 | ORDER BY n.name ASC 102 | SKIP 2 103 | """ 104 | Then the result should be, in order: 105 | | n | 106 | | ({name: 'C'}) | 107 | | ({name: 'D'}) | 108 | | ({name: 'E'}) | 109 | And no side effects 110 | 111 | Scenario: Start the result from the second row by param 112 | Given an empty graph 113 | And having executed: 114 | """ 115 | CREATE ({name: 'A'}), 116 | ({name: 'B'}), 117 | ({name: 'C'}), 118 | ({name: 'D'}), 119 | ({name: 'E'}) 120 | """ 121 | And parameters are: 122 | | skipAmount | 2 | 123 | When executing query: 124 | """ 125 | MATCH (n) 126 | RETURN n 127 | ORDER BY n.name ASC 128 | SKIP $skipAmount 129 | """ 130 | Then the result should be, in order: 131 | | n | 132 | | ({name: 'C'}) | 133 | | ({name: 'D'}) | 134 | | ({name: 'E'}) | 135 | And no side effects 136 | 137 | Scenario: Get rows in the middle 138 | Given an empty graph 139 | And having executed: 140 | """ 141 | CREATE ({name: 'A'}), 142 | ({name: 'B'}), 143 | ({name: 'C'}), 144 | ({name: 'D'}), 145 | ({name: 'E'}) 146 | """ 147 | When executing query: 148 | """ 149 | MATCH (n) 150 | RETURN n 151 | ORDER BY n.name ASC 152 | SKIP 2 153 | LIMIT 2 154 | """ 155 | Then the result should be, in order: 156 | | n | 157 | | ({name: 'C'}) | 158 | | ({name: 'D'}) | 159 | And no side effects 160 | 161 | Scenario: Get rows in the middle by param 162 | Given an empty graph 163 | And having executed: 164 | """ 165 | CREATE ({name: 'A'}), 166 | ({name: 'B'}), 167 | ({name: 'C'}), 168 | ({name: 'D'}), 169 | ({name: 'E'}) 170 | """ 171 | And parameters are: 172 | | s | 2 | 173 | | l | 2 | 174 | When executing query: 175 | """ 176 | MATCH (n) 177 | RETURN n 178 | ORDER BY n.name ASC 179 | SKIP $s 180 | LIMIT $l 181 | """ 182 | Then the result should be, in order: 183 | | n | 184 | | ({name: 'C'}) | 185 | | ({name: 'D'}) | 186 | And no side effects 187 | 188 | Scenario: Sort on aggregated function 189 | Given an empty graph 190 | And having executed: 191 | """ 192 | CREATE ({division: 'A', age: 22}), 193 | ({division: 'B', age: 33}), 194 | ({division: 'B', age: 44}), 195 | ({division: 'C', age: 55}) 196 | """ 197 | When executing query: 198 | """ 199 | MATCH (n) 200 | RETURN n.division, max(n.age) 201 | ORDER BY max(n.age) 202 | """ 203 | Then the result should be, in order: 204 | | n.division | max(n.age) | 205 | | 'A' | 22 | 206 | | 'B' | 44 | 207 | | 'C' | 55 | 208 | And no side effects 209 | 210 | Scenario: Support sort and distinct 211 | Given an empty graph 212 | And having executed: 213 | """ 214 | CREATE ({name: 'A'}), 215 | ({name: 'B'}), 216 | ({name: 'C'}) 217 | """ 218 | When executing query: 219 | """ 220 | MATCH (a) 221 | RETURN DISTINCT a 222 | ORDER BY a.name 223 | """ 224 | Then the result should be, in order: 225 | | a | 226 | | ({name: 'A'}) | 227 | | ({name: 'B'}) | 228 | | ({name: 'C'}) | 229 | And no side effects 230 | 231 | Scenario: Support column renaming 232 | Given an empty graph 233 | And having executed: 234 | """ 235 | CREATE (:Singleton) 236 | """ 237 | When executing query: 238 | """ 239 | MATCH (a) 240 | RETURN a AS ColumnName 241 | """ 242 | Then the result should be, in any order: 243 | | ColumnName | 244 | | (:Singleton) | 245 | And no side effects 246 | 247 | Scenario: Support ordering by a property after being distinct-ified 248 | Given an empty graph 249 | And having executed: 250 | """ 251 | CREATE (:A)-[:T]->(:B) 252 | """ 253 | When executing query: 254 | """ 255 | MATCH (a)-->(b) 256 | RETURN DISTINCT b 257 | ORDER BY b.name 258 | """ 259 | Then the result should be, in order: 260 | | b | 261 | | (:B) | 262 | And no side effects 263 | 264 | Scenario: Arithmetic precedence test 265 | Given any graph 266 | When executing query: 267 | """ 268 | RETURN 12 / 4 * 3 - 2 * 4 269 | """ 270 | Then the result should be, in any order: 271 | | 12 / 4 * 3 - 2 * 4 | 272 | | 1 | 273 | And no side effects 274 | 275 | Scenario: Arithmetic precedence with parenthesis test 276 | Given any graph 277 | When executing query: 278 | """ 279 | RETURN 12 / 4 * (3 - 2 * 4) 280 | """ 281 | Then the result should be, in any order: 282 | | 12 / 4 * (3 - 2 * 4) | 283 | | -15 | 284 | And no side effects 285 | 286 | Scenario: Count star should count everything in scope 287 | Given an empty graph 288 | And having executed: 289 | """ 290 | CREATE (:L1), (:L2), (:L3) 291 | """ 292 | When executing query: 293 | """ 294 | MATCH (a) 295 | RETURN a, count(*) 296 | ORDER BY count(*) 297 | """ 298 | Then the result should be, in any order: 299 | | a | count(*) | 300 | | (:L1) | 1 | 301 | | (:L2) | 1 | 302 | | (:L3) | 1 | 303 | And no side effects 304 | 305 | Scenario: Absolute function 306 | Given any graph 307 | When executing query: 308 | """ 309 | RETURN abs(-1) 310 | """ 311 | Then the result should be, in any order: 312 | | abs(-1) | 313 | | 1 | 314 | And no side effects 315 | -------------------------------------------------------------------------------- /features/supported/ReturnAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: ReturnAcceptance 32 | 33 | Scenario: Allow addition 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE ({id: 1337, version: 99}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (a) 42 | WHERE a.id = 1337 43 | RETURN a.version + 5 44 | """ 45 | Then the result should be, in any order: 46 | | a.version + 5 | 47 | | 104 | 48 | And no side effects 49 | 50 | Scenario: Limit to two hits 51 | Given an empty graph 52 | When executing query: 53 | """ 54 | UNWIND [1, 1, 1, 1, 1] AS i 55 | RETURN i 56 | LIMIT 2 57 | """ 58 | Then the result should be, in any order: 59 | | i | 60 | | 1 | 61 | | 1 | 62 | And no side effects 63 | 64 | Scenario: Limit to two hits with explicit order 65 | Given an empty graph 66 | And having executed: 67 | """ 68 | CREATE ({name: 'A'}), 69 | ({name: 'B'}), 70 | ({name: 'C'}), 71 | ({name: 'D'}), 72 | ({name: 'E'}) 73 | """ 74 | When executing query: 75 | """ 76 | MATCH (n) 77 | RETURN n 78 | ORDER BY n.name ASC 79 | LIMIT 2 80 | """ 81 | Then the result should be, in any order: 82 | | n | 83 | | ({name: 'A'}) | 84 | | ({name: 'B'}) | 85 | And no side effects 86 | 87 | Scenario: Start the result from the second row 88 | Given an empty graph 89 | And having executed: 90 | """ 91 | CREATE ({name: 'A'}), 92 | ({name: 'B'}), 93 | ({name: 'C'}), 94 | ({name: 'D'}), 95 | ({name: 'E'}) 96 | """ 97 | When executing query: 98 | """ 99 | MATCH (n) 100 | RETURN n 101 | ORDER BY n.name ASC 102 | SKIP 2 103 | """ 104 | Then the result should be, in order: 105 | | n | 106 | | ({name: 'C'}) | 107 | | ({name: 'D'}) | 108 | | ({name: 'E'}) | 109 | And no side effects 110 | 111 | # Scenario: Start the result from the second row by param 112 | # Given an empty graph 113 | # And having executed: 114 | # """ 115 | # CREATE ({name: 'A'}), 116 | # ({name: 'B'}), 117 | # ({name: 'C'}), 118 | # ({name: 'D'}), 119 | # ({name: 'E'}) 120 | # """ 121 | # And parameters are: 122 | # | skipAmount | 2 | 123 | # When executing query: 124 | # """ 125 | # MATCH (n) 126 | # RETURN n 127 | # ORDER BY n.name ASC 128 | # SKIP $skipAmount 129 | # """ 130 | # Then the result should be, in order: 131 | # | n | 132 | # | ({name: 'C'}) | 133 | # | ({name: 'D'}) | 134 | # | ({name: 'E'}) | 135 | # And no side effects 136 | 137 | Scenario: Get rows in the middle 138 | Given an empty graph 139 | And having executed: 140 | """ 141 | CREATE ({name: 'A'}), 142 | ({name: 'B'}), 143 | ({name: 'C'}), 144 | ({name: 'D'}), 145 | ({name: 'E'}) 146 | """ 147 | When executing query: 148 | """ 149 | MATCH (n) 150 | RETURN n 151 | ORDER BY n.name ASC 152 | SKIP 2 153 | LIMIT 2 154 | """ 155 | Then the result should be, in order: 156 | | n | 157 | | ({name: 'C'}) | 158 | | ({name: 'D'}) | 159 | And no side effects 160 | 161 | # Scenario: Get rows in the middle by param 162 | # Given an empty graph 163 | # And having executed: 164 | # """ 165 | # CREATE ({name: 'A'}), 166 | # ({name: 'B'}), 167 | # ({name: 'C'}), 168 | # ({name: 'D'}), 169 | # ({name: 'E'}) 170 | # """ 171 | # And parameters are: 172 | # | s | 2 | 173 | # | l | 2 | 174 | # When executing query: 175 | # """ 176 | # MATCH (n) 177 | # RETURN n 178 | # ORDER BY n.name ASC 179 | # SKIP $s 180 | # LIMIT $l 181 | # """ 182 | # Then the result should be, in order: 183 | # | n | 184 | # | ({name: 'C'}) | 185 | # | ({name: 'D'}) | 186 | # And no side effects 187 | 188 | Scenario: Sort on aggregated function 189 | Given an empty graph 190 | And having executed: 191 | """ 192 | CREATE ({division: 'A', age: 22}), 193 | ({division: 'B', age: 33}), 194 | ({division: 'B', age: 44}), 195 | ({division: 'C', age: 55}) 196 | """ 197 | When executing query: 198 | """ 199 | MATCH (n) 200 | RETURN n.division, max(n.age) 201 | ORDER BY max(n.age) 202 | """ 203 | Then the result should be, in order: 204 | | n.division | max(n.age) | 205 | | 'A' | 22 | 206 | | 'B' | 44 | 207 | | 'C' | 55 | 208 | And no side effects 209 | 210 | Scenario: Support sort and distinct 211 | Given an empty graph 212 | And having executed: 213 | """ 214 | CREATE ({name: 'A'}), 215 | ({name: 'B'}), 216 | ({name: 'C'}) 217 | """ 218 | When executing query: 219 | """ 220 | MATCH (a) 221 | RETURN DISTINCT a 222 | ORDER BY a.name 223 | """ 224 | Then the result should be, in order: 225 | | a | 226 | | ({name: 'A'}) | 227 | | ({name: 'B'}) | 228 | | ({name: 'C'}) | 229 | And no side effects 230 | 231 | Scenario: Support column renaming 232 | Given an empty graph 233 | And having executed: 234 | """ 235 | CREATE (:Singleton) 236 | """ 237 | When executing query: 238 | """ 239 | MATCH (a) 240 | RETURN a AS ColumnName 241 | """ 242 | Then the result should be, in any order: 243 | | ColumnName | 244 | | (:Singleton) | 245 | And no side effects 246 | 247 | Scenario: Support ordering by a property after being distinct-ified 248 | Given an empty graph 249 | And having executed: 250 | """ 251 | CREATE (:A)-[:T]->(:B) 252 | """ 253 | When executing query: 254 | """ 255 | MATCH (a)-->(b) 256 | RETURN DISTINCT b 257 | ORDER BY b.name 258 | """ 259 | Then the result should be, in order: 260 | | b | 261 | | (:B) | 262 | And no side effects 263 | 264 | Scenario: Arithmetic precedence test 265 | Given any graph 266 | When executing query: 267 | """ 268 | RETURN 12 / 4 * 3 - 2 * 4 269 | """ 270 | Then the result should be, in any order: 271 | | 12 / 4 * 3 - 2 * 4 | 272 | | 1 | 273 | And no side effects 274 | 275 | Scenario: Arithmetic precedence with parenthesis test 276 | Given any graph 277 | When executing query: 278 | """ 279 | RETURN 12 / 4 * (3 - 2 * 4) 280 | """ 281 | Then the result should be, in any order: 282 | | 12 / 4 * (3 - 2 * 4) | 283 | | -15 | 284 | And no side effects 285 | 286 | Scenario: Count star should count everything in scope 287 | Given an empty graph 288 | And having executed: 289 | """ 290 | CREATE (:L1), (:L2), (:L3) 291 | """ 292 | When executing query: 293 | """ 294 | MATCH (a) 295 | RETURN a, count(*) 296 | ORDER BY count(*) 297 | """ 298 | Then the result should be, in any order: 299 | | a | count(*) | 300 | | (:L1) | 1 | 301 | | (:L2) | 1 | 302 | | (:L3) | 1 | 303 | And no side effects 304 | 305 | Scenario: Absolute function 306 | Given any graph 307 | When executing query: 308 | """ 309 | RETURN abs(-1) 310 | """ 311 | Then the result should be, in any order: 312 | | abs(-1) | 313 | | 1 | 314 | And no side effects 315 | -------------------------------------------------------------------------------- /features/backlog/SetAcceptance.feature: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2015-2019 "Neo Technology," 3 | # Network Engine for Objects in Lund AB [http://neotechnology.com] 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Attribution Notice under the terms of the Apache License 2.0 18 | # 19 | # This work was created by the collective efforts of the openCypher community. 20 | # Without limiting the terms of Section 6, any Derivative Work that is not 21 | # approved by the public consensus process of the openCypher Implementers Group 22 | # should not be described as “Cypher” (and Cypher® is a registered trademark of 23 | # Neo4j Inc.) or as "openCypher". Extensions by implementers or prototypes or 24 | # proposals for change that have been documented or implemented should only be 25 | # described as "implementation extensions to Cypher" or as "proposed changes to 26 | # Cypher that are not yet approved by the openCypher community". 27 | # 28 | 29 | #encoding: utf-8 30 | 31 | Feature: SetAcceptance 32 | 33 | Scenario: Setting a node property to null removes the existing property 34 | Given an empty graph 35 | And having executed: 36 | """ 37 | CREATE (:A {property1: 23, property2: 46}) 38 | """ 39 | When executing query: 40 | """ 41 | MATCH (n:A) 42 | SET n.property1 = null 43 | RETURN n 44 | """ 45 | Then the result should be, in any order: 46 | | n | 47 | | (:A {property2: 46}) | 48 | And the side effects should be: 49 | | -properties | 1 | 50 | 51 | Scenario: Setting a relationship property to null removes the existing property 52 | Given an empty graph 53 | And having executed: 54 | """ 55 | CREATE ()-[:REL {property1: 12, property2: 24}]->() 56 | """ 57 | When executing query: 58 | """ 59 | MATCH ()-[r]->() 60 | SET r.property1 = null 61 | RETURN r 62 | """ 63 | Then the result should be, in any order: 64 | | r | 65 | | [:REL {property2: 24}] | 66 | And the side effects should be: 67 | | -properties | 1 | 68 | 69 | Scenario: Set a property 70 | Given any graph 71 | And having executed: 72 | """ 73 | CREATE (:A {name: 'Andres'}) 74 | """ 75 | When executing query: 76 | """ 77 | MATCH (n:A) 78 | WHERE n.name = 'Andres' 79 | SET n.name = 'Michael' 80 | RETURN n 81 | """ 82 | Then the result should be, in any order: 83 | | n | 84 | | (:A {name: 'Michael'}) | 85 | And the side effects should be: 86 | | +properties | 1 | 87 | | -properties | 1 | 88 | 89 | Scenario: Set a property to an expression 90 | Given an empty graph 91 | And having executed: 92 | """ 93 | CREATE (:A {name: 'Andres'}) 94 | """ 95 | When executing query: 96 | """ 97 | MATCH (n:A) 98 | WHERE n.name = 'Andres' 99 | SET n.name = n.name + ' was here' 100 | RETURN n 101 | """ 102 | Then the result should be, in any order: 103 | | n | 104 | | (:A {name: 'Andres was here'}) | 105 | And the side effects should be: 106 | | +properties | 1 | 107 | | -properties | 1 | 108 | 109 | Scenario: Set a property by selecting the node using a simple expression 110 | Given an empty graph 111 | And having executed: 112 | """ 113 | CREATE (:A) 114 | """ 115 | When executing query: 116 | """ 117 | MATCH (n:A) 118 | SET (n).name = 'neo4j' 119 | RETURN n 120 | """ 121 | Then the result should be, in any order: 122 | | n | 123 | | (:A {name: 'neo4j'}) | 124 | And the side effects should be: 125 | | +properties | 1 | 126 | 127 | Scenario: Set a property by selecting the relationship using a simple expression 128 | Given an empty graph 129 | And having executed: 130 | """ 131 | CREATE ()-[:REL]->() 132 | """ 133 | When executing query: 134 | """ 135 | MATCH ()-[r:REL]->() 136 | SET (r).name = 'neo4j' 137 | RETURN r 138 | """ 139 | Then the result should be, in any order: 140 | | r | 141 | | [:REL {name: 'neo4j'}] | 142 | And the side effects should be: 143 | | +properties | 1 | 144 | 145 | Scenario: Setting a property to null removes the property 146 | Given an empty graph 147 | And having executed: 148 | """ 149 | CREATE (:A {name: 'Michael', age: 35}) 150 | """ 151 | When executing query: 152 | """ 153 | MATCH (n) 154 | WHERE n.name = 'Michael' 155 | SET n.name = null 156 | RETURN n 157 | """ 158 | Then the result should be, in any order: 159 | | n | 160 | | (:A {age: 35}) | 161 | And the side effects should be: 162 | | -properties | 1 | 163 | 164 | Scenario: Add a label to a node 165 | Given an empty graph 166 | And having executed: 167 | """ 168 | CREATE (:A) 169 | """ 170 | When executing query: 171 | """ 172 | MATCH (n:A) 173 | SET n:Foo 174 | RETURN n 175 | """ 176 | Then the result should be, in any order: 177 | | n | 178 | | (:A:Foo) | 179 | And the side effects should be: 180 | | +labels | 1 | 181 | 182 | Scenario: Adding a list property 183 | Given an empty graph 184 | And having executed: 185 | """ 186 | CREATE (:A) 187 | """ 188 | When executing query: 189 | """ 190 | MATCH (n:A) 191 | SET n.numbers = [1, 2, 3] 192 | RETURN [i IN n.numbers | i / 2.0] AS x 193 | """ 194 | Then the result should be, in any order: 195 | | x | 196 | | [0.5, 1.0, 1.5] | 197 | And the side effects should be: 198 | | +properties | 1 | 199 | 200 | Scenario: Concatenate elements onto a list property 201 | Given any graph 202 | When executing query: 203 | """ 204 | CREATE (a {numbers: [1, 2, 3]}) 205 | SET a.numbers = a.numbers + [4, 5] 206 | RETURN a.numbers 207 | """ 208 | Then the result should be, in any order: 209 | | a.numbers | 210 | | [1, 2, 3, 4, 5] | 211 | And the side effects should be: 212 | | +nodes | 1 | 213 | | +properties | 1 | 214 | 215 | Scenario: Concatenate elements in reverse onto a list property 216 | Given any graph 217 | When executing query: 218 | """ 219 | CREATE (a {numbers: [3, 4, 5]}) 220 | SET a.numbers = [1, 2] + a.numbers 221 | RETURN a.numbers 222 | """ 223 | Then the result should be, in any order: 224 | | a.numbers | 225 | | [1, 2, 3, 4, 5] | 226 | And the side effects should be: 227 | | +nodes | 1 | 228 | | +properties | 1 | 229 | 230 | Scenario: Overwrite values when using += 231 | Given an empty graph 232 | And having executed: 233 | """ 234 | CREATE (:X {name: 'A', name2: 'B'}) 235 | """ 236 | When executing query: 237 | """ 238 | MATCH (n:X {name: 'A'}) 239 | SET n += {name2: 'C'} 240 | RETURN n 241 | """ 242 | Then the result should be, in any order: 243 | | n | 244 | | (:X {name: 'A', name2: 'C'}) | 245 | And the side effects should be: 246 | | +properties | 1 | 247 | | -properties | 1 | 248 | 249 | Scenario: Retain old values when using += 250 | Given an empty graph 251 | And having executed: 252 | """ 253 | CREATE (:X {name: 'A'}) 254 | """ 255 | When executing query: 256 | """ 257 | MATCH (n:X {name: 'A'}) 258 | SET n += {name2: 'B'} 259 | RETURN n 260 | """ 261 | Then the result should be, in any order: 262 | | n | 263 | | (:X {name: 'A', name2: 'B'}) | 264 | And the side effects should be: 265 | | +properties | 1 | 266 | 267 | Scenario: Explicit null values in a map remove old values 268 | Given an empty graph 269 | And having executed: 270 | """ 271 | CREATE (:X {name: 'A', name2: 'B'}) 272 | """ 273 | When executing query: 274 | """ 275 | MATCH (n:X {name: 'A'}) 276 | SET n += {name: null} 277 | RETURN n 278 | """ 279 | Then the result should be, in any order: 280 | | n | 281 | | (:X {name2: 'B'}) | 282 | And the side effects should be: 283 | | -properties | 1 | 284 | 285 | Scenario: Non-existent values in a property map are removed with SET = 286 | Given an empty graph 287 | And having executed: 288 | """ 289 | CREATE (:X {name: 'A', name2: 'B'}) 290 | """ 291 | When executing query: 292 | """ 293 | MATCH (n:X {name: 'A'}) 294 | SET n = {name: 'B', baz: 'C'} 295 | RETURN n 296 | """ 297 | Then the result should be, in any order: 298 | | n | 299 | | (:X {name: 'B', baz: 'C'}) | 300 | And the side effects should be: 301 | | +properties | 2 | 302 | | -properties | 2 | 303 | --------------------------------------------------------------------------------