diff --git a/core/src/main/java/org/polypheny/db/adapter/NeoProcedureProvider.java b/core/src/main/java/org/polypheny/db/adapter/NeoProcedureProvider.java new file mode 100644 index 0000000000..8453f967e8 --- /dev/null +++ b/core/src/main/java/org/polypheny/db/adapter/NeoProcedureProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.adapter; + +import org.polypheny.db.algebra.AlgNode; +import org.polypheny.db.plan.AlgCluster; +import org.polypheny.db.plan.AlgPlanner; +import org.polypheny.db.plan.AlgTraitSet; +import org.polypheny.db.plan.Convention; +import org.polypheny.db.plan.volcano.VolcanoPlanner; +import org.polypheny.db.type.entity.PolyValue; +import java.util.ArrayList; + +public interface NeoProcedureProvider { + public Convention getConvention( AlgPlanner planner ); + public AlgNode getCall( AlgCluster cluster, AlgTraitSet traits, ArrayList namespace, String procedureName, ArrayList arguments, boolean yieldAll, ArrayList yieldItems); + public Adapter getStore(); +} diff --git a/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java b/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java index 9b7d6403c7..065598b088 100644 --- a/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java +++ b/core/src/main/java/org/polypheny/db/algebra/constant/Kind.java @@ -514,6 +514,12 @@ public enum Kind { */ PROCEDURE_CALL, + /** + * CypherProcedureCall + * We separate Cypher procedure calls and SQL procedure calls + */ + CYPHER_PROCEDURE_CALL, + /** * NewSpecification */ @@ -1399,7 +1405,7 @@ public enum Kind { DELETE, UPDATE, MERGE, - PROCEDURE_CALL ); + PROCEDURE_CALL); /** * Category consisting of all DDL operators. diff --git a/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgAlg.java b/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgAlg.java index f3c62d9dc3..6e39a5fe01 100644 --- a/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgAlg.java +++ b/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgAlg.java @@ -36,7 +36,8 @@ enum NodeType { VALUES, AGGREGATE, MERGE, - SORT + SORT, + CALL } } diff --git a/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgCall.java b/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgCall.java new file mode 100644 index 0000000000..11e20c3d1e --- /dev/null +++ b/core/src/main/java/org/polypheny/db/algebra/core/lpg/LpgCall.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.algebra.core.lpg; + +import lombok.Getter; +import lombok.Setter; +import org.polypheny.db.adapter.Adapter; +import org.polypheny.db.algebra.AbstractAlgNode; +import org.polypheny.db.algebra.metadata.AlgMetadataQuery; +import org.polypheny.db.algebra.type.AlgDataType; +import org.polypheny.db.algebra.type.AlgDataTypeField; +import org.polypheny.db.algebra.type.AlgDataTypeFieldImpl; +import org.polypheny.db.algebra.type.AlgRecordType; +import org.polypheny.db.plan.AlgCluster; +import org.polypheny.db.plan.AlgOptCost; +import org.polypheny.db.plan.AlgPlanner; +import org.polypheny.db.plan.AlgTraitSet; +import org.polypheny.db.type.PolyType; +import org.polypheny.db.type.entity.PolyValue; +import java.util.ArrayList; + +public abstract class LpgCall extends AbstractAlgNode implements LpgAlg { + + @Getter + ArrayList namespace; + @Getter + String procedureName; + @Getter + ArrayList arguments; + @Setter + @Getter + Adapter procedureProvider; // will only be used when we reach a Neo4j store, so normally just pass a null to it + + @Getter + boolean yieldAll; + @Getter + ArrayList yieldItems; + + public LpgCall( AlgCluster cluster, AlgTraitSet traits, ArrayList namespace, String procedureName, ArrayList arguments, Adapter procedureProvider, boolean yieldAll, ArrayList yieldItems ) { + super( cluster, traits ); + this.namespace = namespace; + this.procedureName = procedureName; + this.arguments = arguments; + this.procedureProvider = procedureProvider; + this.yieldAll = yieldAll; + this.yieldItems = yieldItems; + } + + @Override + public String algCompareString() { + return getClass().getSimpleName().toString() + "$" + String.join( ".", namespace ) + + "." + procedureName + "$" + arguments.hashCode(); + } + + + @Override + public NodeType getNodeType() { + return NodeType.CALL; + } + + @Override + protected AlgDataType deriveRowType() { + if (!(procedureName.equals( "labels" ))) { + throw new UnsupportedOperationException("The called procedure is not supported"); + } + ArrayList fields = new ArrayList<>(); + fields.add( new AlgDataTypeFieldImpl( -1L, "Labels", 0, getCluster().getTypeFactory().createPolyType( PolyType.VARCHAR ) ) ); + return new AlgRecordType( fields ); + } + +} diff --git a/core/src/main/java/org/polypheny/db/algebra/logical/lpg/LogicalLpgCall.java b/core/src/main/java/org/polypheny/db/algebra/logical/lpg/LogicalLpgCall.java new file mode 100644 index 0000000000..0f34eebc73 --- /dev/null +++ b/core/src/main/java/org/polypheny/db/algebra/logical/lpg/LogicalLpgCall.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.algebra.logical.lpg; + +import org.polypheny.db.adapter.Adapter; +import org.polypheny.db.algebra.core.lpg.LpgCall; +import org.polypheny.db.plan.AlgCluster; +import org.polypheny.db.plan.AlgTraitSet; +import org.polypheny.db.type.entity.PolyValue; +import java.util.ArrayList; + +public class LogicalLpgCall extends LpgCall { + + public LogicalLpgCall( AlgCluster cluster, AlgTraitSet traits, ArrayList namespace, String procedureName, ArrayList arguments, Adapter procedureProvider, boolean yieldAll, ArrayList yieldItems ) { + super( cluster, traits, namespace, procedureName, arguments, procedureProvider, yieldAll, yieldItems ); + } + +} diff --git a/core/src/main/java/org/polypheny/db/algebra/rules/CypherCallLogicalToPhysicalRule.java b/core/src/main/java/org/polypheny/db/algebra/rules/CypherCallLogicalToPhysicalRule.java new file mode 100644 index 0000000000..d1f038cc2f --- /dev/null +++ b/core/src/main/java/org/polypheny/db/algebra/rules/CypherCallLogicalToPhysicalRule.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.algebra.rules; + +import org.polypheny.db.adapter.Adapter; +import org.polypheny.db.adapter.AdapterManager; +import org.polypheny.db.adapter.NeoProcedureProvider; +import org.polypheny.db.algebra.AlgNode; +import org.polypheny.db.algebra.core.AlgFactories; +import org.polypheny.db.algebra.core.common.Modify; +import org.polypheny.db.algebra.core.lpg.LpgCall; +import org.polypheny.db.algebra.enumerable.EnumerableConvention; +import org.polypheny.db.algebra.logical.lpg.LogicalLpgCall; +import org.polypheny.db.plan.AlgOptRule; +import org.polypheny.db.plan.AlgOptRuleCall; +import org.polypheny.db.plan.Convention; + +public class CypherCallLogicalToPhysicalRule extends AlgOptRule { + + public static final CypherCallLogicalToPhysicalRule INSTANCE = new CypherCallLogicalToPhysicalRule( LogicalLpgCall.class ); + + public CypherCallLogicalToPhysicalRule( Class callAlg ) { + super( operand( callAlg, Convention.NONE, r -> true, any() ), AlgFactories.LOGICAL_BUILDER, callAlg.getSimpleName() + "ToPhysical" ); + } + + @Override + public void onMatch( AlgOptRuleCall call ) { + LpgCall oldAlg = call.alg( 0 ); + Adapter neo = null; + for ( Adapter a : AdapterManager.getInstance().getStores().values() ) { + if ( a.adapterName.contains( "Neo" ) ) { + neo = a; + } + } + if ( neo == null ) { + throw new RuntimeException( "No neo adapter found" ); + } + AlgNode newAlg = oldAlg; + newAlg.getTraitSet().replace( ((NeoProcedureProvider)neo).getConvention(call.getPlanner()) ); + ((LpgCall)newAlg).setProcedureProvider( ((NeoProcedureProvider)neo).getStore() ); + //AlgNode newAlg = ((NeoProcedureProvider)neo).getCall( oldAlg.getCluster(), oldAlg.getTraitSet(), oldAlg.getNamespace(), oldAlg.getProcedureName(), oldAlg.getArguments() ); + if ( newAlg != null ) { + call.transformTo( newAlg ); + } + } + +} diff --git a/core/src/main/java/org/polypheny/db/monitoring/events/MonitoringType.java b/core/src/main/java/org/polypheny/db/monitoring/events/MonitoringType.java index ef07afca09..5687fdb2e6 100644 --- a/core/src/main/java/org/polypheny/db/monitoring/events/MonitoringType.java +++ b/core/src/main/java/org/polypheny/db/monitoring/events/MonitoringType.java @@ -31,7 +31,8 @@ public enum MonitoringType { UPDATE, MINUS, INTERSECT, - UNION; + UNION, + CYPHER_PROCEDURE_CALL; public static MonitoringType from( Kind kind ) { diff --git a/core/src/main/java/org/polypheny/db/plan/volcano/VolcanoPlanner.java b/core/src/main/java/org/polypheny/db/plan/volcano/VolcanoPlanner.java index e9f1919dda..70c783169c 100644 --- a/core/src/main/java/org/polypheny/db/plan/volcano/VolcanoPlanner.java +++ b/core/src/main/java/org/polypheny/db/plan/volcano/VolcanoPlanner.java @@ -73,6 +73,7 @@ import org.polypheny.db.algebra.rules.AggregateProjectMergeRule; import org.polypheny.db.algebra.rules.AggregateRemoveRule; import org.polypheny.db.algebra.rules.CalcRemoveRule; +import org.polypheny.db.algebra.rules.CypherCallLogicalToPhysicalRule; import org.polypheny.db.algebra.rules.FilterJoinRule; import org.polypheny.db.algebra.rules.JoinAssociateRule; import org.polypheny.db.algebra.rules.JoinCommuteRule; @@ -835,6 +836,7 @@ public void registerModelRules() { addRule( ModelSwitcherRule.MODEL_SWITCHER_RULE_REL_GRAPH ); addRule( ModelSwitcherRule.MODEL_SWITCHER_RULE_DOC_REL ); addRule( ModelSwitcherRule.MODEL_SWITCHER_RULE_DOC_GRAPH ); + addRule( CypherCallLogicalToPhysicalRule.INSTANCE ); } diff --git a/core/src/main/java/org/polypheny/db/tools/Programs.java b/core/src/main/java/org/polypheny/db/tools/Programs.java index 788b6891bd..2d016bd133 100644 --- a/core/src/main/java/org/polypheny/db/tools/Programs.java +++ b/core/src/main/java/org/polypheny/db/tools/Programs.java @@ -59,6 +59,7 @@ import org.polypheny.db.algebra.rules.AllocationToPhysicalModifyRule; import org.polypheny.db.algebra.rules.AllocationToPhysicalScanRule; import org.polypheny.db.algebra.rules.CalcMergeRule; +import org.polypheny.db.algebra.rules.CypherCallLogicalToPhysicalRule; import org.polypheny.db.algebra.rules.FilterAggregateTransposeRule; import org.polypheny.db.algebra.rules.FilterCalcMergeRule; import org.polypheny.db.algebra.rules.FilterJoinRule; @@ -172,6 +173,7 @@ public class Programs { AllocationToPhysicalModifyRule.REL_INSTANCE, AllocationToPhysicalModifyRule.DOC_INSTANCE, AllocationToPhysicalModifyRule.GRAPH_INSTANCE, + CypherCallLogicalToPhysicalRule.INSTANCE, RuntimeConfig.JOIN_COMMUTE.getBoolean() ? JoinAssociateRule.INSTANCE : ProjectMergeRule.INSTANCE, diff --git a/dbms/src/main/java/org/polypheny/db/processing/VolcanoQueryProcessor.java b/dbms/src/main/java/org/polypheny/db/processing/VolcanoQueryProcessor.java index b05fb9db23..39b5a42052 100644 --- a/dbms/src/main/java/org/polypheny/db/processing/VolcanoQueryProcessor.java +++ b/dbms/src/main/java/org/polypheny/db/processing/VolcanoQueryProcessor.java @@ -35,6 +35,7 @@ import org.polypheny.db.algebra.rules.AggregateValuesRule; import org.polypheny.db.algebra.rules.AllocationToPhysicalModifyRule; import org.polypheny.db.algebra.rules.AllocationToPhysicalScanRule; +import org.polypheny.db.algebra.rules.CypherCallLogicalToPhysicalRule; import org.polypheny.db.algebra.rules.FilterAggregateTransposeRule; import org.polypheny.db.algebra.rules.FilterJoinRule; import org.polypheny.db.algebra.rules.FilterProjectTransposeRule; diff --git a/dbms/src/main/java/org/polypheny/db/routing/routers/AbstractDqlRouter.java b/dbms/src/main/java/org/polypheny/db/routing/routers/AbstractDqlRouter.java index 5fa894c9cc..0d2f11374e 100644 --- a/dbms/src/main/java/org/polypheny/db/routing/routers/AbstractDqlRouter.java +++ b/dbms/src/main/java/org/polypheny/db/routing/routers/AbstractDqlRouter.java @@ -142,6 +142,9 @@ public AlgNode routeGraph( RoutedAlgBuilder builder return alg; } else if ( alg.getNodeType() == NodeType.VALUES ) { return alg; + } else if ( alg.getNodeType() == NodeType.CALL ) { + // TODO: we currently still return LogicalLpgCall + return alg; } throw new UnsupportedOperationException(); } diff --git a/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/clause/CypherCall.java b/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/clause/CypherCall.java index 98b4e991ed..d4897756a0 100644 --- a/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/clause/CypherCall.java +++ b/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/clause/CypherCall.java @@ -18,6 +18,7 @@ import java.util.List; import lombok.Getter; +import org.polypheny.db.algebra.constant.Kind; import org.polypheny.db.cypher.CypherCallResultItem; import org.polypheny.db.cypher.expression.CypherExpression; import org.polypheny.db.languages.ParserPos; @@ -64,4 +65,9 @@ public CypherKind getCypherKind() { return CypherKind.CALL; } + @Override + public Kind getKind() { + return Kind.CYPHER_PROCEDURE_CALL; + } + } diff --git a/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/cypher2alg/CypherToAlgConverter.java b/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/cypher2alg/CypherToAlgConverter.java index 9998a9f7b3..0b0f619cba 100644 --- a/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/cypher2alg/CypherToAlgConverter.java +++ b/plugins/cypher-language/src/main/java/org/polypheny/db/cypher/cypher2alg/CypherToAlgConverter.java @@ -38,6 +38,7 @@ import org.polypheny.db.algebra.constant.Kind; import org.polypheny.db.algebra.core.AggregateCall; import org.polypheny.db.algebra.core.common.Modify; +import org.polypheny.db.algebra.logical.lpg.LogicalLpgCall; import org.polypheny.db.algebra.logical.lpg.LogicalLpgFilter; import org.polypheny.db.algebra.logical.lpg.LogicalLpgMatch; import org.polypheny.db.algebra.logical.lpg.LogicalLpgModify; @@ -57,8 +58,10 @@ import org.polypheny.db.catalog.entity.logical.LogicalNamespace; import org.polypheny.db.catalog.exceptions.GenericRuntimeException; import org.polypheny.db.catalog.snapshot.Snapshot; +import org.polypheny.db.cypher.CypherCallResultItem; import org.polypheny.db.cypher.CypherNode; import org.polypheny.db.cypher.CypherNode.CypherFamily; +import org.polypheny.db.cypher.clause.CypherCall; import org.polypheny.db.cypher.clause.CypherClause; import org.polypheny.db.cypher.clause.CypherCreate; import org.polypheny.db.cypher.clause.CypherDelete; @@ -69,6 +72,7 @@ import org.polypheny.db.cypher.clause.CypherUnwind; import org.polypheny.db.cypher.clause.CypherWhere; import org.polypheny.db.cypher.clause.CypherWith; +import org.polypheny.db.cypher.expression.CypherExpression; import org.polypheny.db.cypher.pattern.CypherPattern; import org.polypheny.db.cypher.query.CypherSingleQuery; import org.polypheny.db.languages.OperatorRegistry; @@ -84,6 +88,7 @@ import org.polypheny.db.type.PolyType; import org.polypheny.db.type.entity.PolyList; import org.polypheny.db.type.entity.PolyString; +import org.polypheny.db.type.entity.PolyValue; import org.polypheny.db.type.entity.graph.PolyDictionary; import org.polypheny.db.type.entity.graph.PolyEdge; import org.polypheny.db.type.entity.graph.PolyNode; @@ -200,12 +205,39 @@ private void convertClauses( CypherClause clause, CypherContext context ) { case REMOVE: convertRemove( (CypherRemove) clause, context ); break; + case CALL: + convertCall( (CypherCall) clause, context ); + break; default: throw new UnsupportedOperationException(); } } + private void convertCall( CypherCall call, CypherContext context ) { + assert call.getNamespace() instanceof ArrayList; + context.kind = call.getKind(); + ArrayList namespace = (ArrayList) call.getNamespace(); + String procedureName = call.getName(); + ArrayList arguments = new ArrayList<>(); + for ( CypherExpression argument : call.getArguments() ) { + // TODO: are we assuming that there will not be a function/procedure call inside this procedure call? + arguments.add( argument.getComparable() ); + } + boolean yieldAll = true; + ArrayList yieldItems = new ArrayList<>(); + if ( call.getItems() != null ) { + yieldAll = false; + for ( CypherCallResultItem item : call.getItems() ) { + yieldItems.add( item.getExpression() ); + } + } else { + yieldAll = true; + } + LogicalLpgCall procedureCall = new LogicalLpgCall(cluster, cluster.traitSet(), namespace, procedureName, arguments, null, yieldAll, yieldItems ); + context.add( procedureCall ); + } + private void convertRemove( CypherRemove clause, CypherContext context ) { clause.getRemove( context ); diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/Neo4jPlugin.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/Neo4jPlugin.java index 5f1369814d..ac955368be 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/Neo4jPlugin.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/Neo4jPlugin.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -39,14 +40,18 @@ import org.neo4j.driver.Session; import org.neo4j.driver.Transaction; import org.pf4j.Extension; +import org.polypheny.db.adapter.Adapter; import org.polypheny.db.adapter.AdapterManager; import org.polypheny.db.adapter.DataStore; import org.polypheny.db.adapter.DataStore.IndexMethodModel; import org.polypheny.db.adapter.DeployMode; import org.polypheny.db.adapter.GraphModifyDelegate; +import org.polypheny.db.adapter.NeoProcedureProvider; import org.polypheny.db.adapter.annotations.AdapterProperties; +import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgCall; import org.polypheny.db.adapter.neo4j.types.NestedSingleType; import org.polypheny.db.adapter.neo4j.util.NeoUtil; +import org.polypheny.db.algebra.AlgNode; import org.polypheny.db.catalog.catalogs.GraphAdapterCatalog; import org.polypheny.db.catalog.entity.LogicalDefaultValue; import org.polypheny.db.catalog.entity.allocation.AllocationCollection; @@ -68,11 +73,17 @@ import org.polypheny.db.docker.DockerContainer.HostAndPort; import org.polypheny.db.docker.DockerInstance; import org.polypheny.db.docker.DockerManager; +import org.polypheny.db.plan.AlgCluster; +import org.polypheny.db.plan.AlgPlanner; +import org.polypheny.db.plan.AlgTraitSet; +import org.polypheny.db.plan.Convention; +import org.polypheny.db.plan.volcano.VolcanoPlanner; import org.polypheny.db.plugins.PluginContext; import org.polypheny.db.plugins.PolyPlugin; import org.polypheny.db.prepare.Context; import org.polypheny.db.transaction.PolyXid; import org.polypheny.db.type.PolyType; +import org.polypheny.db.type.entity.PolyValue; import org.polypheny.db.util.PasswordGenerator; @@ -136,7 +147,7 @@ private static String getMappingLabel( long id ) { usedModes = { DeployMode.DOCKER, DeployMode.REMOTE }, defaultMode = DeployMode.DOCKER) @Extension - public static class Neo4jStore extends DataStore { + public static class Neo4jStore extends DataStore implements NeoProcedureProvider { private final String DEFAULT_DATABASE = "public"; @Delegate(excludes = Exclude.class) @@ -155,9 +166,27 @@ public static class Neo4jStore extends DataStore { @Getter private NeoGraph currentGraph; + @Getter private final TransactionProvider transactionProvider; private String host; + private HashMap callNodes = new HashMap<>(); + private long currentNodeId = 0; + + public NeoLpgCall getCallNode( long id ) { + return callNodes.get( id ); + } + + public long addCallNode( NeoLpgCall call ) { + callNodes.put( currentNodeId, call ); + currentNodeId++; + return currentNodeId-1; + } + + public void removeCallNode( long id ) { + callNodes.remove( currentNodeId ); + } + public Neo4jStore( final long adapterId, final String uniqueName, final Map adapterSettings, DeployMode mode ) { super( adapterId, uniqueName, adapterSettings, mode, true, new GraphAdapterCatalog( adapterId ) ); @@ -573,6 +602,24 @@ protected void reloadSettings( List updatedSettings ) { } + @Override + public Convention getConvention( AlgPlanner planner ) { + NeoConvention.INSTANCE.register( planner ); + return NeoConvention.INSTANCE; + } + + + @Override + public AlgNode getCall( AlgCluster cluster, AlgTraitSet traits, ArrayList namespace, String procedureName, ArrayList arguments, boolean yieldAll, ArrayList yieldItems ) { + return new NeoLpgCall( cluster, traits, namespace, procedureName, arguments, this, yieldAll, yieldItems ); + } + + + @Override + public Adapter getStore() { + return this; + } + } diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoGraphImplementor.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoGraphImplementor.java index 7256ebf1b6..c6375f5ac8 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoGraphImplementor.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoGraphImplementor.java @@ -147,6 +147,8 @@ public void addReturnIfNecessary() { } else { statements.add( return_( statement.statements ) ); } + } else if ( statement.type == StatementType.CALL || statement.type == StatementType.YIELD ) { + return; } else { // have to add statements.add( return_( getFields( last.getTupleType() ) ) ); diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoProcedureCallHandler.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoProcedureCallHandler.java new file mode 100644 index 0000000000..96c027f83e --- /dev/null +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoProcedureCallHandler.java @@ -0,0 +1,193 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.adapter.neo4j; + +import lombok.Getter; +import lombok.Setter; +import org.apache.calcite.linq4j.AbstractEnumerable; +import org.apache.calcite.linq4j.AbstractQueryable; +import org.apache.calcite.linq4j.Enumerable; +import org.apache.calcite.linq4j.Enumerator; +import org.apache.calcite.linq4j.Linq4j; +import org.apache.calcite.linq4j.QueryProvider; +import org.apache.calcite.linq4j.Queryable; +import org.apache.calcite.linq4j.function.Function1; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.ParameterExpression; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.neo4j.driver.Record; +import org.neo4j.driver.Result; +import org.neo4j.driver.Transaction; +import org.polypheny.db.adapter.AdapterManager; +import org.polypheny.db.adapter.DataContext; +import org.polypheny.db.adapter.NeoProcedureProvider; +import org.polypheny.db.adapter.neo4j.Neo4jPlugin.Neo4jStore; +import org.polypheny.db.adapter.neo4j.NeoGraph.NeoQueryable; +import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgCall; +import org.polypheny.db.adapter.neo4j.types.NestedPolyType; +import org.polypheny.db.adapter.neo4j.util.NeoUtil; +import org.polypheny.db.algebra.type.AlgDataType; +import org.polypheny.db.algebra.type.AlgDataTypeFactory; +import org.polypheny.db.catalog.Catalog; +import org.polypheny.db.catalog.snapshot.Snapshot; +import org.polypheny.db.schema.types.QueryableEntity; +import org.polypheny.db.type.PolyType; +import org.polypheny.db.type.entity.PolyValue; +import org.polypheny.db.util.Pair; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class NeoProcedureCallHandler implements QueryableEntity { + + final long adapterId; + final long callNodeId; + final NeoLpgCall callNode; + final Neo4jStore store; + + public NeoProcedureCallHandler( long adapterId, long callNodeId ) { + this.adapterId = adapterId; + this.callNodeId = callNodeId; + store = (Neo4jStore) AdapterManager.getInstance().getStore( adapterId ).orElseThrow(); + callNode = store.getCallNode( callNodeId ); + } + + public static Expression getHandlerAsExpression( long adapterId, long callNodeId ) { + return Expressions.new_( NeoProcedureCallHandler.class, Expressions.constant( adapterId ), Expressions.constant( callNodeId ) ); + } + + public static Expression asExpression( long adapterId, long callNodeId ) { + return Expressions.call( + Expressions.convert_( + getHandlerAsExpression( adapterId, callNodeId ), + NeoProcedureCallHandler.class ), + "asQueryable", + DataContext.ROOT, + Catalog.SNAPSHOT_EXPRESSION ); + } + + + @Override + public Queryable asQueryable( DataContext dataContext, Snapshot snapshot ) { + return new NeoProcedureCallable( dataContext, snapshot, adapterId ); + } + + + @Override + public AlgDataType getTupleType( AlgDataTypeFactory typeFactory ) { + return callNode.getTupleType(); + } + + + public static class NeoProcedureCallable extends AbstractQueryable { + + private final DataContext dataContext; + //private final NeoProcedureCallHandler handler; + private final long adapterId; + private final Neo4jStore store; + private final TransactionProvider transactionProvider; + + public NeoProcedureCallable(DataContext dataContext, Snapshot snapshot, long adapterId) { + this.dataContext = dataContext; + this.adapterId = adapterId; + store = (Neo4jStore) AdapterManager.getInstance().getAdapter( adapterId ).orElseThrow(); + this.transactionProvider = store.getTransactionProvider(); + } + + @SuppressWarnings("UnusedDeclaration") + public Enumerable execute( String query, NestedPolyType types ) { + Transaction trx = getTrx(); + + dataContext.getStatement().getTransaction().registerInvolvedAdapter( store ); + + List results = new ArrayList<>(); + results.add( trx.run( query ) ); + + Function1 getter = NeoQueryable.getter( types ); + + return new AbstractEnumerable<>() { + @Override + public Enumerator enumerator() { + return new NeoEnumerator( results, getter ); + } + }; + } + + /** + * This method returns the functions, which transforms a given record into the corresponding object representation. + * + * @param types the types for which function is created + * @return the function, which transforms the {@link Record} + */ + static Function1 getter( NestedPolyType types ) { + //noinspection unchecked + return (Function1) NeoUtil.getTypesFunction( types ); + } + + + /** + * Gets an already open transaction of creates a new one if it not exists. + */ + private Transaction getTrx() { + return transactionProvider.get( dataContext.getStatement().getTransaction().getXid() ); + } + + + /** + * Commit the transaction. + */ + private void commit() { + transactionProvider.commit( dataContext.getStatement().getTransaction().getXid() ); + } + + + @Override + public Type getElementType() { + return Object[].class; + } + + + @Override + public Expression getExpression() { + return null; + } + + + @Override + public QueryProvider getProvider() { + return dataContext.getQueryProvider(); + } + + + @Override + public @NotNull Iterator iterator() { + return Linq4j.enumeratorIterator( enumerator() ); + } + + + @Override + public Enumerator enumerator() { + return null; + } + + } + +} diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverter.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverter.java index abcc566a19..7a3b9188c8 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverter.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverter.java @@ -16,14 +16,20 @@ package org.polypheny.db.adapter.neo4j; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; import org.apache.calcite.linq4j.tree.BlockBuilder; import org.apache.calcite.linq4j.tree.Expression; import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.ParameterExpression; +import org.polypheny.db.adapter.neo4j.Neo4jPlugin.Neo4jStore; +import org.polypheny.db.adapter.neo4j.NeoProcedureCallHandler.NeoProcedureCallable; +import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgCall; import org.polypheny.db.adapter.neo4j.types.NestedPolyType; import org.polypheny.db.algebra.AlgNode; import org.polypheny.db.algebra.convert.ConverterImpl; +import org.polypheny.db.algebra.core.lpg.LpgCall; import org.polypheny.db.algebra.enumerable.EnumUtils; import org.polypheny.db.algebra.enumerable.EnumerableAlg; import org.polypheny.db.algebra.enumerable.EnumerableAlgImplementor; @@ -71,6 +77,9 @@ public Result implement( EnumerableAlgImplementor implementor, Prefer pref ) { if ( this.getTraitSet().contains( ModelTrait.RELATIONAL ) ) { return getRelationalImplement( implementor, pref, blockBuilder ); } else { + if ( getInput() instanceof LpgCall ) { + return getNeoCallImplementation( implementor, pref, blockBuilder ); + } return getGraphImplement( implementor, pref, blockBuilder ); } @@ -127,6 +136,44 @@ private Result getGraphImplement( EnumerableAlgImplementor implementor, Prefer p return implementor.result( physType, blockBuilder.toBlock() ); } + /** + * We use a separate method to handle CALLs, as calls don't require a graph entity + * + * @param implementor is used build the code snippets by recursively moving through them + * @param pref preferred result format, e.g. when SCALAR -> single result gets returned as single element, if ARRAY it is wrapped in an array + * @param blockBuilder helper builder to generate expressions + * @return the code in a result representation + */ + private Result getNeoCallImplementation( EnumerableAlgImplementor implementor, Prefer pref, BlockBuilder blockBuilder ) { + final NeoGraphImplementor graphImplementor = new NeoGraphImplementor(); + + graphImplementor.visitChild( 0, getInput() ); + final AlgDataType rowType = getTupleType(); + final PhysType physType = PhysTypeImpl.of( implementor.getTypeFactory(), rowType, pref.prefer( JavaTupleFormat.ARRAY ) ); + + final Expression types = NestedPolyType.from( rowType ).asExpression(); + final String query = graphImplementor.build(); + NeoLpgCall callNode = (NeoLpgCall) getInput(); + + long callNodeId = ((Neo4jStore)callNode.getProcedureProvider()).addCallNode( callNode ); + final Expression callHandler = blockBuilder.append( "callHandler", NeoProcedureCallHandler.asExpression(callNode.getProcedureProvider().getAdapterId(), callNodeId) ); + Method method; + try { + method = NeoProcedureCallable.class.getMethod("execute", String.class, NestedPolyType.class); + } catch ( NoSuchMethodException e ) { + throw new RuntimeException( e ); + } + Expression enumerable = blockBuilder.append( + blockBuilder.newName( "enumerable" ), + Expressions.call( + Expressions.convert_( callHandler, + NeoProcedureCallable.class), + method, Expressions.constant( query ), types ) ); + blockBuilder.add( Expressions.return_( null, enumerable ) ); + + return implementor.result( physType, blockBuilder.toBlock() ); + } + /** * Generates the algebra specific code representation of the attached child nodes. diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverterRule.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverterRule.java index 7086250bec..5cea925280 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverterRule.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/NeoToEnumerableConverterRule.java @@ -19,6 +19,7 @@ import org.polypheny.db.algebra.AlgNode; import org.polypheny.db.algebra.convert.ConverterRule; import org.polypheny.db.algebra.core.AlgFactories; +import org.polypheny.db.algebra.core.lpg.LpgCall; import org.polypheny.db.algebra.enumerable.EnumerableConvention; import org.polypheny.db.plan.AlgTraitSet; import org.polypheny.db.tools.AlgBuilderFactory; @@ -35,7 +36,7 @@ public class NeoToEnumerableConverterRule extends ConverterRule { public NeoToEnumerableConverterRule( AlgBuilderFactory algBuilderFactory ) { super( AlgNode.class, - Util::containsEntity, + r -> Util.containsEntity( r ) || r instanceof LpgCall, NeoConvention.INSTANCE, EnumerableConvention.INSTANCE, algBuilderFactory, diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/NeoGraphRules.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/NeoGraphRules.java index 7af752d599..187650e3d0 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/NeoGraphRules.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/NeoGraphRules.java @@ -21,6 +21,7 @@ import org.polypheny.db.adapter.neo4j.NeoGraph; import org.polypheny.db.adapter.neo4j.NeoToEnumerableConverterRule; import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgAggregate; +import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgCall; import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgFilter; import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgMatch; import org.polypheny.db.adapter.neo4j.rules.graph.NeoLpgModify; @@ -33,6 +34,7 @@ import org.polypheny.db.algebra.convert.ConverterRule; import org.polypheny.db.algebra.core.AlgFactories; import org.polypheny.db.algebra.core.lpg.LpgAggregate; +import org.polypheny.db.algebra.core.lpg.LpgCall; import org.polypheny.db.algebra.core.lpg.LpgFilter; import org.polypheny.db.algebra.core.lpg.LpgMatch; import org.polypheny.db.algebra.core.lpg.LpgModify; @@ -55,7 +57,8 @@ public interface NeoGraphRules { NeoGraphSortRule.INSTANCE, NeoGraphUnwindRule.INSTANCE, NeoGraphAggregateRule.INSTANCE, - NeoGraphMatchRule.INSTANCE + NeoGraphMatchRule.INSTANCE, + NeoGraphCallRule.INSTANCE }; @@ -135,6 +138,31 @@ public AlgNode convert( AlgNode alg ) { } + class NeoGraphCallRule extends NeoConverterRule { + + public static NeoGraphCallRule INSTANCE = new NeoGraphCallRule( LpgCall.class, r -> true, "NeoGraphCallRule" ); + + private NeoGraphCallRule( Class clazz, Predicate supports, String description ) { + super( clazz, supports, description ); + } + + @Override + public AlgNode convert( AlgNode alg ) { + LpgCall call = (LpgCall) alg; + return new NeoLpgCall( + call.getCluster(), + call.getTraitSet().replace( NeoConvention.INSTANCE ), + call.getNamespace(), + call.getProcedureName(), + call.getArguments(), + call.getProcedureProvider(), + call.isYieldAll(), + call.getYieldItems() + ); + } + + } + class NeoGraphFilterRule extends NeoConverterRule { diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/graph/NeoLpgCall.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/graph/NeoLpgCall.java new file mode 100644 index 0000000000..d6179c8df6 --- /dev/null +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/rules/graph/NeoLpgCall.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.polypheny.db.adapter.neo4j.rules.graph; + +import org.polypheny.db.adapter.Adapter; +import org.polypheny.db.adapter.neo4j.NeoGraphImplementor; +import org.polypheny.db.adapter.neo4j.rules.NeoGraphAlg; +import org.polypheny.db.adapter.neo4j.util.NeoStatements.LiteralStatement; +import org.polypheny.db.adapter.neo4j.util.NeoStatements.NeoStatement; +import org.polypheny.db.algebra.core.lpg.LpgCall; +import org.polypheny.db.algebra.metadata.AlgMetadataQuery; +import org.polypheny.db.plan.AlgCluster; +import org.polypheny.db.plan.AlgOptCost; +import org.polypheny.db.plan.AlgPlanner; +import org.polypheny.db.plan.AlgTraitSet; +import org.polypheny.db.type.entity.PolyBinary; +import org.polypheny.db.type.entity.PolyString; +import org.polypheny.db.type.entity.PolyValue; +import java.util.ArrayList; + +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.call_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.create_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.delete_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.edge_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.list_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.literal_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.node_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.path_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.set_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.string_; +import static org.polypheny.db.adapter.neo4j.util.NeoStatements.yield_; + +public class NeoLpgCall extends LpgCall implements NeoGraphAlg { + + + public NeoLpgCall( AlgCluster cluster, AlgTraitSet traits, ArrayList namespace, String procedureName, ArrayList arguments, Adapter procedureProvider, boolean yieldAll, ArrayList yieldItems ) { + super( cluster, traits, namespace, procedureName, arguments, procedureProvider, yieldAll, yieldItems ); + } + + + @Override + public void implement( NeoGraphImplementor implementor ) { + String neoProcedureName = String.join( ".", getNamespace() ) + "." + getProcedureName(); + + ArrayList literalArgs = new ArrayList<>(); + for ( PolyValue argument : getArguments() ) { + literalArgs.add( literal_( argument ) ); + } + + boolean yieldAll = isYieldAll(); + ArrayList yieldItems = new ArrayList<>(); + + if ( getYieldItems() != null ) { + for ( String yieldItem : getYieldItems() ) { + yieldItems.add( yieldItem ); + } + } else { + yieldAll = true; + } + + implementor.add( call_( neoProcedureName, literalArgs.size() == 0, list_( literalArgs, "(", ")" ), yieldAll, yieldItems ) ); + } + + @Override + public AlgOptCost computeSelfCost( AlgPlanner planner, AlgMetadataQuery mq ) { + return planner.getCostFactory().makeTinyCost(); + } + +} diff --git a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/util/NeoStatements.java b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/util/NeoStatements.java index a7633d4a89..c39db8b3c0 100644 --- a/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/util/NeoStatements.java +++ b/plugins/neo4j-adapter/src/main/java/org/polypheny/db/adapter/neo4j/util/NeoStatements.java @@ -53,6 +53,9 @@ enum StatementType { WITH( "WITH" ), SET( "SET" ), + CALL( "CALL" ), + YIELD( "YIELD" ), + FOREACH( "FOREACH" ), DELETE( "DELETE" ), DELETE_DETACH( "DETACH DELETE" ), @@ -706,6 +709,50 @@ static MatchStatement match_( NeoStatement... statement ) { return new MatchStatement( list_( Arrays.asList( statement ) ) ); } + class CallStatement extends OperatorStatement { + + final String procedureName; + final boolean noArgs; + protected CallStatement( String procedureName, ListStatement statements, boolean noArgs, boolean yieldAll, ArrayList yieldItems ) { + super( StatementType.CALL, statements ); + this.procedureName = procedureName; + this.noArgs = noArgs; + } + + @Override + public String build() { + // statements.build may not add () if there is no arg + if ( noArgs ) { + return "CALL " + procedureName + "()"; + } + return "CALL " + procedureName + statements.build(); + } + } + static CallStatement call_( String procedureName, boolean noArgs, ListStatement statements, boolean yieldAll, ArrayList yieldItems ) { + return new CallStatement( procedureName, statements, noArgs, yieldAll, yieldItems ); + } + + class YieldStatement extends OperatorStatement { + boolean yieldAll; + + protected YieldStatement( boolean yieldAll, ListStatement statements ) { + super( StatementType.YIELD, statements ); + this.yieldAll = yieldAll; + } + + @Override + public String build() { + if ( yieldAll || statements == null ) { + return " YIELD *"; + } + return "YIELD " + statements.build(); + } + } + + static YieldStatement yield_( boolean yieldAll, ListStatement statements ) { + return new YieldStatement( yieldAll, statements ); + } + class ForeachStatement extends OperatorStatement { private final String elementId; diff --git a/plugins/neo4j-adapter/src/test/java/org/polypheny/db/adapter/neo4j/NeoGraphTest.java b/plugins/neo4j-adapter/src/test/java/org/polypheny/db/adapter/neo4j/NeoGraphTest.java index 55fcd42b5d..5346f499cf 100644 --- a/plugins/neo4j-adapter/src/test/java/org/polypheny/db/adapter/neo4j/NeoGraphTest.java +++ b/plugins/neo4j-adapter/src/test/java/org/polypheny/db/adapter/neo4j/NeoGraphTest.java @@ -54,6 +54,27 @@ public static void tearDown() { } } + @Test + public void callTest() { + CypherConnection.executeGetResponse( "CREATE (p:Person {name: \"Max Muster\"})",GRAPH_NAMESPACE ); + GraphResult res = CypherConnection.executeGetResponse( "CALL db.labels()" ); + assert res.data.length == 2; + } + + @Test + public void callWithYieldStarTest() { + CypherConnection.executeGetResponse( "CREATE (p:Person {name: \"Max Muster\"})",GRAPH_NAMESPACE ); + GraphResult res = CypherConnection.executeGetResponse( "CALL db.labels() YIELD *" ); + assert res.data.length == 2; + } + + @Test + public void callWithYieldLabelTest() { + CypherConnection.executeGetResponse( "CREATE (p:Person {name: \"Max Muster\"})",GRAPH_NAMESPACE ); + GraphResult res = CypherConnection.executeGetResponse( "CALL db.labels() YIELD label" ); + assert res.data.length == 2; + } + @Test public void insertNodeTest() { CypherConnection.executeGetResponse( "CREATE (p:Person {name: \"Max Muster\"})",GRAPH_NAMESPACE );