Skip to content

Commit

Permalink
Integrate new TCK and support procedures and some exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
SaschaPeukert committed Jan 26, 2018
1 parent 61bbc68 commit e50a6c7
Show file tree
Hide file tree
Showing 8 changed files with 615 additions and 3 deletions.
Expand Up @@ -186,7 +186,8 @@ Feature: MatchAcceptance
MATCH (ts)-[:R]->(f)
RETURN k, ts, f, d
"""
Then the result should be empty
Then the result should be:
| k | ts | f | d |
And no side effects

Scenario: difficult to plan query number 3
Expand Down Expand Up @@ -366,5 +367,6 @@ Feature: MatchAcceptance
"""
MATCH (n {prop: false}) RETURN n
"""
Then the result should be empty
Then the result should be:
| n |
And no side effects
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cypher.features

import cypher.features.Neo4jExceptionToExecutionFailed._
import org.neo4j.cypher.internal.javacompat.GraphDatabaseCypherService
import org.neo4j.graphdb.{Result => Neo4jResult}
import org.neo4j.kernel.impl.factory.GraphDatabaseFacade
import org.neo4j.test.TestGraphDatabaseFactory
import org.opencypher.tools.tck.api._
import org.opencypher.tools.tck.values.CypherValue

import scala.collection.JavaConverters._
import scala.util.{Failure, Success, Try}

object Neo4jAdapter {
def apply(): Neo4jAdapter = {
val service: GraphDatabaseCypherService = new GraphDatabaseCypherService(new TestGraphDatabaseFactory().newImpermanentDatabase())
new Neo4jAdapter(service)
}
}

class Neo4jAdapter(service: GraphDatabaseCypherService) extends Graph with Neo4jProcedureAdapter {
protected val instance: GraphDatabaseFacade = service.getGraphDatabaseService

override def cypher(query: String, params: Map[String, CypherValue], meta: QueryType): Result = {
val neo4jParams = params.mapValues(v => TCKValueToNeo4jValue(v)).asJava

val tx = instance.beginTx()
val result = {
val neo4jResult = Try(instance.execute(query, neo4jParams)) match {
case Success(r) => r
case Failure(exception) =>
tx.failure()
tx.close() // TODO: better solution?
return convert(exception)
}
val convertedResult: Result = Try(convertResult(neo4jResult)) match {
case Success(r) =>
tx.success()
r
case Failure(exception) =>
tx.failure()
tx.close() // TODO: better solution?
return convert(exception)
}
tx.close()
convertedResult
}
result
}

def convertResult(result: Neo4jResult): Result = {
val header = result.columns().asScala.toList
val rows: List[Map[String, String]] = result.asScala.map { row =>
row.asScala.map { case (k, v) => (k, Neo4jValueToString(v)) }.toMap
}.toList
StringRecords(header, rows)
}

override def close(): Unit = {
instance.shutdown()
}

}

case class Neo4jExecutionException(
query: String,
params: Map[String, CypherValue],
meta: QueryType, msg: String)
extends Exception(s"Error when executing $meta query $query with params $params: $msg")

case class Neo4jValueConversionException(
query: String,
params: Map[String, CypherValue],
meta: QueryType, msg: String
)
extends Exception(s"Error when converting result values for $meta query $query with params $params: $msg")
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cypher.features

import org.opencypher.tools.tck.api.ExecutionFailed
import org.opencypher.tools.tck.constants.TCKErrorDetails._

object Neo4jExceptionToExecutionFailed {

// Error types
val typeError = "TypeError"
val syntaxError = "SyntaxError"
val unsupportedError = "UnsupportedError"

// Phases
val runtime = "runtime"
val compile = "compile time"
val unsupportedPhase = "unsupportedPhase"

def convert(neo4jException: Throwable): ExecutionFailed = {
//(errorType: String, phase: String, detail: String)
val msg = neo4jException.getMessage.toLowerCase
val errorType = if (msg.contains("type")) {
typeError
} else if (msg.contains("invalid")) {
syntaxError
} else {
unsupportedError
}
val exceptionName = neo4jException.getClass.getSimpleName
val phase = if (exceptionName.contains("Execution")) {
runtime
} else {
compile
}

val detail = {
if (msg.matches("Type mismatch: expected a map but was .+"))
PROPERTY_ACCESS_ON_NON_MAP
else if (msg.matches("Expected .+ to be a ((java.lang.String)|(org.neo4j.values.storable.TextValue)), but it was a .+"))
MAP_ELEMENT_ACCESS_BY_NON_STRING
else if (msg.matches("Expected .+ to be a ((java.lang.Number)|(org.neo4j.values.storable.NumberValue)), but it was a .+"))
LIST_ELEMENT_ACCESS_BY_NON_INTEGER
else if (msg.matches(".+ is not a collection or a map. Element access is only possible by performing a collection lookup using an integer index, or by performing a map lookup using a string key .+"))
INVALID_ELEMENT_ACCESS
else if (msg.matches(s"\nElement access is only possible by performing a collection lookup using an integer index,\nor by performing a map lookup using a string key .+"))
INVALID_ELEMENT_ACCESS
else if (msg.matches(".+ can not create a new node due to conflicts with( both)? existing( and missing)? unique nodes.*"))
"CreateBlockedByConstraint"
else if (msg.matches("Node\\(\\d+\\) already exists with label `.+` and property `.+` = .+"))
"CreateBlockedByConstraint"
else if (msg.matches("Cannot delete node\\<\\d+\\>, because it still has relationships. To delete this node, you must first delete its relationships."))
DELETE_CONNECTED_NODE
else if (msg.matches("Don't know how to compare that\\..+"))
"IncomparableValues"
else if (msg.matches("Cannot perform .+ on mixed types\\..+"))
"IncomparableValues"
else if (msg.matches("Invalid input '.+' is not a valid argument, must be a number in the range 0.0 to 1.0"))
NUMBER_OUT_OF_RANGE
else if (msg.matches("step argument to range\\(\\) cannot be zero"))
NUMBER_OUT_OF_RANGE
else if (msg.matches("Expected a (.+), got: (.*)"))
INVALID_ARGUMENT_VALUE
else if (msg.matches("The expression .+ should have been a node or a relationship, but got .+"))
REQUIRES_DIRECTED_RELATIONSHIP
else if (msg.matches("((Node)|(Relationship)) with id \\d+ has been deleted in this transaction"))
DELETED_ENTITY_ACCESS
else
"unsupportedDetail"
}
println(neo4jException.getMessage)
println(neo4jException.getClass)
ExecutionFailed(errorType, phase, detail)

}

/*
private def compileTimeError(msg: String, typ: String, phase: String, detail: String): Boolean = {
var r = true
if (msg.matches("Invalid input '-(\\d)+' is not a valid value, must be a positive integer[\\s.\\S]+"))
detail should equal(NEGATIVE_INTEGER_ARGUMENT)
else if (msg.matches("Invalid input '.+' is not a valid value, must be a positive integer[\\s.\\S]+"))
detail should equal(INVALID_ARGUMENT_TYPE)
else if (msg.matches("Can't use aggregate functions inside of aggregate functions\\."))
detail should equal(NESTED_AGGREGATION)
else if (msg.matches("Can't create node `(\\w+)` with labels or properties here. The variable is already declared in this context"))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches("Can't create node `(\\w+)` with labels or properties here. It already exists in this context"))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches("Can't create `\\w+` with properties or labels here. The variable is already declared in this context"))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches("Can't create `\\w+` with properties or labels here. It already exists in this context"))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches("Can't create `(\\w+)` with labels or properties here. It already exists in this context"))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches(semanticError("\\w+ already declared")))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches(semanticError("Only directed relationships are supported in ((CREATE)|(MERGE))")))
detail should equal(REQUIRES_DIRECTED_RELATIONSHIP)
else if (msg.matches(s"${DOTALL}Type mismatch: expected .+ but was .+"))
detail should equal(INVALID_ARGUMENT_TYPE)
else if (msg.matches(semanticError("Variable `.+` not defined")))
detail should equal(UNDEFINED_VARIABLE)
else if (msg.matches(semanticError(".+ not defined")))
detail should equal(UNDEFINED_VARIABLE)
else if (msg.matches(semanticError("Type mismatch: .+ already defined with conflicting type .+ \\(expected .+\\)")))
detail should equal(VARIABLE_TYPE_CONFLICT)
else if (msg.matches(semanticError("Cannot use the same relationship variable '.+' for multiple patterns")))
detail should equal(RELATIONSHIP_UNIQUENESS_VIOLATION)
else if (msg.matches(semanticError("Cannot use the same relationship identifier '.+' for multiple patterns")))
detail should equal(RELATIONSHIP_UNIQUENESS_VIOLATION)
else if (msg.matches(semanticError("Variable length relationships cannot be used in ((CREATE)|(MERGE))")))
detail should equal(CREATING_VAR_LENGTH)
else if (msg.matches(semanticError("Parameter maps cannot be used in ((MATCH)|(MERGE)) patterns \\(use a literal map instead, eg. \"\\{id: \\{param\\}\\.id\\}\"\\)")))
detail should equal(INVALID_PARAMETER_USE)
else if (msg.matches(semanticError("Variable `.+` already declared")))
detail should equal(VARIABLE_ALREADY_BOUND)
else if (msg.matches(semanticError("MATCH cannot follow OPTIONAL MATCH \\(perhaps use a WITH clause between them\\)")))
detail should equal(INVALID_CLAUSE_COMPOSITION)
else if (msg.matches(semanticError("Invalid combination of UNION and UNION ALL")))
detail should equal(INVALID_CLAUSE_COMPOSITION)
else if (msg.matches(semanticError("floating point number is too large")))
detail should equal(FLOATING_POINT_OVERFLOW)
else if (msg.matches(semanticError("Argument to exists\\(\\.\\.\\.\\) is not a property or pattern")))
detail should equal(INVALID_ARGUMENT_EXPRESSION)
else if (msg.startsWith("Invalid input '—':"))
detail should equal(INVALID_UNICODE_CHARACTER)
else if (msg.matches(semanticError("Can't use aggregating expressions inside of expressions executing over lists")))
detail should equal(INVALID_AGGREGATION)
else if (msg.matches(semanticError("Can't use aggregating expressions inside of expressions executing over collections")))
detail should equal(INVALID_AGGREGATION)
else if (msg.matches(semanticError("It is not allowed to refer to variables in ((SKIP)|(LIMIT))")))
detail should equal(NON_CONSTANT_EXPRESSION)
else if (msg.matches(semanticError("It is not allowed to refer to identifiers in ((SKIP)|(LIMIT))")))
detail should equal(NON_CONSTANT_EXPRESSION)
else if (msg.matches("Can't use non-deterministic \\(random\\) functions inside of aggregate functions\\."))
detail should equal(NON_CONSTANT_EXPRESSION)
else if (msg.matches(semanticError("A single relationship type must be specified for ((CREATE)|(MERGE))\\")) ||
msg.matches(semanticError("Exactly one relationship type must be specified for ((CREATE)|(MERGE))\\. " +
"Did you forget to prefix your relationship type with a \\'\\:\\'\\?")))
detail should equal(NO_SINGLE_RELATIONSHIP_TYPE)
else if (msg.matches(s"${DOTALL}Invalid input '.*': expected an identifier character, whitespace, '\\|', a length specification, a property map or '\\]' \\(line \\d+, column \\d+ \\(offset: \\d+\\)\\).*"))
detail should equal(INVALID_RELATIONSHIP_PATTERN)
else if (msg.matches(s"${DOTALL}Invalid input '.*': expected whitespace, RangeLiteral, a property map or '\\]' \\(line \\d+, column \\d+ \\(offset: \\d+\\)\\).*"))
detail should equal(INVALID_RELATIONSHIP_PATTERN)
else if (msg.matches(semanticError("invalid literal number")))
detail should equal(INVALID_NUMBER_LITERAL)
else if (msg.matches(semanticError("Unknown function '.+'")))
detail should equal(UNKNOWN_FUNCTION)
else if (msg.matches(semanticError("Invalid input '.+': expected four hexadecimal digits specifying a unicode character")))
detail should equal(INVALID_UNICODE_LITERAL)
else if (msg.matches("Cannot merge ((relationship)|(node)) using null property value for .+"))
detail should equal(MERGE_READ_OWN_WRITES)
else if (msg.matches(semanticError("Invalid use of aggregating function count\\(\\.\\.\\.\\) in this context")))
detail should equal(INVALID_AGGREGATION)
else if (msg.matches(semanticError("Cannot use aggregation in ORDER BY if there are no aggregate expressions in the preceding ((RETURN)|(WITH))")))
detail should equal(INVALID_AGGREGATION)
else if (msg.matches(semanticError("Expression in WITH must be aliased \\(use AS\\)")))
detail should equal(NO_EXPRESSION_ALIAS)
else if (msg.matches(semanticError("All sub queries in an UNION must have the same column names")))
detail should equal(DIFFERENT_COLUMNS_IN_UNION)
else if (msg.matches(semanticError("DELETE doesn't support removing labels from a node. Try REMOVE.")))
detail should equal(INVALID_DELETE)
else if (msg.matches("Property values can only be of primitive types or arrays thereof"))
detail should equal(INVALID_PROPERTY_TYPE)
else if (msg.matches(semanticError("Multiple result columns with the same name are not supported")))
detail should equal(COLUMN_NAME_CONFLICT)
else if (msg.matches(semanticError("RETURN \\* is not allowed when there are no variables in scope")))
detail should equal(NO_VARIABLES_IN_SCOPE)
else if (msg.matches(semanticError("RETURN \\* is not allowed when there are no identifiers in scope")))
detail should equal(NO_VARIABLES_IN_SCOPE)
else if (msg.matches(semanticError("Procedure call does not provide the required number of arguments.+")))
detail should equal("InvalidNumberOfArguments")
else if (msg.matches("Expected a parameter named .+"))
detail should equal("MissingParameter")
else if (msg.startsWith("Procedure call cannot take an aggregating function as argument, please add a 'WITH' to your statement."))
detail should equal("InvalidAggregation")
else if (msg.startsWith("Procedure call inside a query does not support passing arguments implicitly (pass explicitly after procedure name instead)"))
detail should equal("InvalidArgumentPassingMode")
else if (msg.matches("There is no procedure with the name `.+` registered for this database instance. Please ensure you've spelled the procedure name correctly and that the procedure is properly deployed."))
detail should equal("ProcedureNotFound")
else r = false
r
}
*/
}

0 comments on commit e50a6c7

Please sign in to comment.