This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 6ab2e1acd5 Fixes #8836 : Graph index and Graph constraint: resolve
variables in … (#8849)
6ab2e1acd5 is described below
commit 6ab2e1acd54e9df7f8371c5f05f866178b9bbad4
Author: Bart Maertens <[email protected]>
AuthorDate: Mon Oct 12 11:25:00 2026 +0200
Fixes #8836 : Graph index and Graph constraint: resolve variables in …
(#8849)
* Fixes #8836 : Graph index and Graph constraint: resolve variables in
names, labels and properties, and support Apache AGE indexes
* Issue #8836 : Apache AGE index: ignore the error of a label created
concurrently by another session
---
.../apache/hop/core/graph/IGraphConnection.java | 19 ++
.../modules/ROOT/pages/technology/age/index.adoc | 6 +-
.../pages/workflow/actions/graph-constraint.adoc | 3 +
.../ROOT/pages/workflow/actions/graph-index.adoc | 22 +-
.../main-graph-index-variables.hwf} | 227 ++++++++++-----------
.../graph-vector-search-nodes-validation.hpl | 2 +-
.../main-graph-vector-search-nodes.hwf | 39 ++--
.../graph-vector-search-nodes-validation.hpl | 2 +-
.../main-graph-vector-search-nodes.hwf | 39 ++--
.../datasets/graph-vector-search-nodes-golden.csv | 6 +
.../dataset/graph-vector-search-nodes-golden.json | 40 ++++
.../neo4j-graph-model/VectorSearchDocs.json | 38 ++++
.../graph-vector-search-nodes-validation UNIT.json | 40 ++++
.../graph-vector-search-nodes-build.hpl | 146 +++++++++++++
.../graph-vector-search-nodes-validation.hpl | 2 +-
.../main-graph-vector-search-nodes.hwf | 39 ++--
.../org/apache/hop/age/AgeGraphConnection.java | 18 ++
.../java/org/apache/hop/age/AgeGraphDialect.java | 90 +++++++-
.../age/AgeGraphDialectCharacterizationTest.java | 15 +-
.../src/test/java/org/apache/hop/age/AgeIT.java | 42 ++++
.../neo4j/actions/constraint/Neo4jConstraint.java | 27 ++-
.../actions/constraint/Neo4jConstraintDialog.java | 11 +-
.../apache/hop/neo4j/actions/index/Neo4jIndex.java | 27 ++-
.../hop/neo4j/actions/index/Neo4jIndexDialog.java | 9 +-
.../hop/neo4j/shared/NeoConnectionUtils.java | 16 +-
.../constraint/Neo4jConstraintVariablesTest.java | 67 ++++++
.../actions/index/Neo4jIndexVariablesTest.java | 94 +++++++++
.../neo4j/shared/ThirdPartyGraphDialectTest.java | 36 ++++
28 files changed, 927 insertions(+), 195 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/graph/IGraphConnection.java
b/core/src/main/java/org/apache/hop/core/graph/IGraphConnection.java
index fff97d0b8c..6a9ae4930a 100644
--- a/core/src/main/java/org/apache/hop/core/graph/IGraphConnection.java
+++ b/core/src/main/java/org/apache/hop/core/graph/IGraphConnection.java
@@ -57,6 +57,25 @@ public interface IGraphConnection extends AutoCloseable {
return executeWrite(work);
}
+ /**
+ * Run an index or constraint statement of the dialect. By default the
statement is run like any
+ * other, in a write transaction or on its own.
+ *
+ * @param statement The statement from the dialect
+ * @param inTransaction True to run it in a write transaction, false to run
it on its own
+ */
+ default void executeSchemaStatement(String statement, boolean inTransaction)
throws HopException {
+ if (inTransaction) {
+ executeWrite(
+ transaction -> {
+ transaction.execute(statement, Map.of());
+ return true;
+ });
+ } else {
+ execute(statement, Map.of());
+ }
+ }
+
/**
* @return The dialect of the database: what it supports and the syntax of
its index and
* constraint statements. By default Cypher with the syntax of Neo4j 5.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/technology/age/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/technology/age/index.adoc
index ae2f5c2a8c..c6e5a2a5ca 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/technology/age/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/technology/age/index.adoc
@@ -41,13 +41,14 @@ Hop loads the AGE extension in every connection, so there
is no need to set `sha
* xref:pipeline/transforms/cypher-builder.adoc[Cypher builder]
* xref:pipeline/transforms/split-graph.adoc[Split graph] and
xref:pipeline/transforms/generate-graph-csvs.adoc[Generate graph CSVs]
* xref:workflow/actions/graph-script.adoc[Graph script]
+* The xref:workflow/actions/graph-index.adoc[Graph index] action, for named
indexes on node properties: it creates PostgreSQL indexes, see below.
* xref:workflow/actions/check-graph-database-connections.adoc[Check graph
database connections]
* Execution logging (`HOP_GRAPH_LOGGING_CONNECTION`), the
xref:pipeline/transforms/get-graph-logging-info.adoc[Get graph logging info]
transform and the xref:hop-gui/perspective-graph-logging.adoc[Graph logging
perspective]
* The xref:metadata-types/graphs/graph-database-location.adoc[graph database
location] for execution information
== What doesn't work
-* The xref:workflow/actions/graph-index.adoc[Graph index] and
xref:workflow/actions/graph-constraint.adoc[Graph constraint] actions: AGE
indexes and constraints are PostgreSQL indexes and constraints, created with
SQL, see below.
+* The xref:workflow/actions/graph-constraint.adoc[Graph constraint] action,
and the Graph index action for vector indexes and indexes on relationship
properties: AGE indexes and constraints are PostgreSQL indexes and constraints.
* *Create indexes* in Graph output and Cypher output, for the same reason.
* `RETURN *` and statements which return unnamed values.
* Nodes with more than one label, and labels with spaces.
@@ -59,7 +60,8 @@ Hop loads the AGE extension in every connection, so there is
no need to set `sha
* A node has one label, and label names can't contain spaces. Graph names have
at least 3 characters.
* AGE has no temporal types: dates and times are stored as ISO strings.
* Indexes are PostgreSQL indexes on the tables AGE keeps per label, created
with SQL.
-The xref:workflow/actions/graph-index.adoc[Graph index] and
xref:workflow/actions/graph-constraint.adoc[Graph constraint] actions don't
support AGE, but the *Show indexes DDL* button of the graph database location
gives the SQL for its indexes, for example:
+The xref:workflow/actions/graph-index.adoc[Graph index] action creates them on
node properties, and creates the label first when it has no nodes yet.
+The *Show indexes DDL* button of the graph database location gives the SQL for
its indexes, for example:
+
[source,sql]
----
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-constraint.adoc
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-constraint.adoc
index 0e804cd2bd..629a1630a1 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-constraint.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-constraint.adoc
@@ -39,6 +39,9 @@ For similar operations on indexes, check the
xref:workflow/actions/graph-index.a
** Object name: the label of the node or relationship you want to update a
constraint for
** Property: the property to put the constraint on (for NODE_KEY, use
comma-separated properties like "id,name")
+The constraint name, object name and properties can be variables, for example
`${PERSON_KEY}`.
+The action resolves them before it builds the statement, for create and drop,
and the Cypher preview shows the resolved values.
+
[NOTE]
====
NODE_KEY constraints are only available in Neo4j Enterprise Edition. NODE_KEY
constraints require multiple properties (comma-separated) and can only be
applied to nodes, not relationships.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-index.adoc
index d7bae87e23..ae37987fe6 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/graph-index.adoc
@@ -24,7 +24,7 @@ under the License.
== Description
-The `Graph index` action (previously Neo4j index) creates or drops indexes on
node or relationship properties in a graph database, in the syntax of the
database: Neo4j, Memgraph or FalkorDB.
+The `Graph index` action (previously Neo4j index) creates or drops indexes on
node or relationship properties in a graph database, in the syntax of the
database: Neo4j, Memgraph, FalkorDB or Apache AGE.
For similar operations on constraints, check the
xref:workflow/actions/graph-constraint.adoc[Graph constraint] action.
@@ -42,7 +42,25 @@ For similar operations on constraints, check the
xref:workflow/actions/graph-con
** Vector similarity: VECTOR only, COSINE (the default) or EUCLIDEAN
** Vector capacity: VECTOR on Memgraph only, the number of vectors to reserve
room for, 1000 when empty
-Vector dimensions and capacity can be variables.
+The index name, object name, properties, vector dimensions and vector capacity
can be variables, for example `${CHUNK_INDEX}` to keep a test and a production
index apart.
+The action resolves them before it builds the statement, for create and drop,
and the Cypher preview shows the resolved values.
+Use the same variable for the index name in
xref:pipeline/transforms/graph-vector-search.adoc[Graph vector search].
+
+== Databases
+
+[options="header"]
+|===
+|Database |Range index |Vector index
+
+|Neo4j |Nodes and relationships |Nodes and relationships
+|Memgraph |Nodes and relationships |Nodes and relationships
+|FalkorDB |Nodes and relationships |Nodes and relationships
+|Apache AGE |Nodes, with a name |No
+|Amazon Neptune, Gremlin |No |No
+|===
+
+On Apache AGE an index is a PostgreSQL index on the table of the label,
created and dropped with SQL.
+The action creates the label first when there are no nodes with it yet, so the
index can be created before the data is loaded.
== Vector indexes
diff --git
a/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
b/integration-tests/age/tests/graph-index-variables/main-graph-index-variables.hwf
similarity index 52%
copy from
integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
copy to
integration-tests/age/tests/graph-index-variables/main-graph-index-variables.hwf
index 3b95ea27e9..125771bec3 100644
---
a/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
+++
b/integration-tests/age/tests/graph-index-variables/main-graph-index-variables.hwf
@@ -18,21 +18,36 @@ limitations under the License.
-->
<workflow>
- <name>main-graph-vector-search-nodes</name>
+ <name>main-graph-index-variables</name>
<name_sync_with_filename>Y</name_sync_with_filename>
- <description>Graph vector search on the vector index of nodes</description>
+ <description>Graph index on Apache AGE with the index name, label and
property in variables</description>
<extended_description/>
<workflow_version/>
<created_user>-</created_user>
- <created_date>2026/10/07 12:00:00.000</created_date>
+ <created_date>2026/10/10 12:00:00.000</created_date>
<modified_user>-</modified_user>
- <modified_date>2026/10/07 12:00:00.000</modified_date>
+ <modified_date>2026/10/10 12:00:00.000</modified_date>
<parameters>
<parameter>
<name>TEST_FOLDER</name>
- <default_value>graph-vector-search-nodes</default_value>
+ <default_value>graph-index-variables</default_value>
<description/>
</parameter>
+ <parameter>
+ <name>GRAPH_INDEX</name>
+ <default_value>graph_index_variables_id</default_value>
+ <description>The name of the index: the action resolves it</description>
+ </parameter>
+ <parameter>
+ <name>GRAPH_LABEL</name>
+ <default_value>IndexVariablesChunk</default_value>
+ <description>The label of the index: it needs no nodes yet</description>
+ </parameter>
+ <parameter>
+ <name>GRAPH_PROPERTY</name>
+ <default_value>chunk_id</default_value>
+ <description>The indexed property</description>
+ </parameter>
</parameters>
<actions>
<action>
@@ -54,21 +69,19 @@ limitations under the License.
<attributes_hac/>
</action>
<action>
- <name>drop indexes before</name>
+ <name>drop index before</name>
<description/>
<type>NEO4J_INDEX</type>
<attributes/>
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${GRAPH_INDEX}</index_name>
+ <object_name>${GRAPH_LABEL}</object_name>
+ <object_properties>${GRAPH_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
- <index_type>VECTOR</index_type>
- <vector_dimensions>3</vector_dimensions>
- <vector_similarity>COSINE</vector_similarity>
+ <index_type>RANGE</index_type>
</update>
</updates>
<parallel>N</parallel>
@@ -77,140 +90,105 @@ limitations under the License.
<attributes_hac/>
</action>
<action>
- <name>cleanup-database.hwf</name>
- <description/>
- <type>WORKFLOW</type>
- <attributes/>
- <run_configuration>local</run_configuration>
- <filename>${PROJECT_HOME}/tests/shared/cleanup-database.hwf</filename>
- <params_from_previous>N</params_from_previous>
- <exec_per_row>N</exec_per_row>
- <set_logfile>N</set_logfile>
- <logfile/>
- <logext/>
- <add_date>N</add_date>
- <add_time>N</add_time>
- <loglevel>Nothing</loglevel>
- <wait_until_finished>Y</wait_until_finished>
- <create_parent_folder>N</create_parent_folder>
- <parameters>
- <pass_all_parameters>Y</pass_all_parameters>
- </parameters>
- <set_append_logfile>N</set_append_logfile>
- <parallel>N</parallel>
- <xloc>384</xloc>
- <yloc>96</yloc>
- <attributes_hac/>
- </action>
- <action>
- <name>create indexes</name>
+ <name>create index</name>
<description/>
<type>NEO4J_INDEX</type>
<attributes/>
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${GRAPH_INDEX}</index_name>
+ <object_name>${GRAPH_LABEL}</object_name>
+ <object_properties>${GRAPH_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>CREATE</update_type>
- <index_type>VECTOR</index_type>
- <vector_dimensions>3</vector_dimensions>
- <vector_similarity>COSINE</vector_similarity>
+ <index_type>RANGE</index_type>
</update>
</updates>
<parallel>N</parallel>
- <xloc>544</xloc>
- <yloc>96</yloc>
- <attributes_hac/>
- </action>
- <action>
- <name>graph-vector-search-nodes-build.hpl</name>
- <description/>
- <type>PIPELINE</type>
- <attributes/>
-
<filename>${PROJECT_HOME}/tests/graph-vector-search-nodes/graph-vector-search-nodes-build.hpl</filename>
- <params_from_previous>N</params_from_previous>
- <exec_per_row>N</exec_per_row>
- <clear_rows>N</clear_rows>
- <clear_files>N</clear_files>
- <set_logfile>N</set_logfile>
- <logfile/>
- <logext/>
- <add_date>N</add_date>
- <add_time>N</add_time>
- <loglevel>Basic</loglevel>
- <set_append_logfile>N</set_append_logfile>
- <wait_until_finished>Y</wait_until_finished>
- <create_parent_folder>N</create_parent_folder>
- <run_configuration>local</run_configuration>
- <parameters>
- <pass_all_parameters>Y</pass_all_parameters>
- </parameters>
- <parallel>N</parallel>
- <xloc>704</xloc>
+ <xloc>384</xloc>
<yloc>96</yloc>
<attributes_hac/>
</action>
<action>
- <name>Run graph-vector-search-nodes tests</name>
- <description/>
- <type>RunPipelineTests</type>
+ <name>index created</name>
+ <description>The index exists with the resolved name, on the resolved
label and property</description>
+ <type>EVAL_TABLE_CONTENT</type>
<attributes/>
- <test_names>
- <test_name>
- <name>graph-vector-search-nodes-validation UNIT</name>
- </test_name>
- </test_names>
+ <connection>age-sql</connection>
+ <schemaname/>
+ <tablename/>
+ <success_condition>rows_count_equal</success_condition>
+ <limit>1</limit>
+ <is_custom_sql>Y</is_custom_sql>
+ <is_usevars>N</is_usevars>
+ <custom_sql>SELECT indexname FROM pg_indexes WHERE schemaname = 'hop_it'
AND tablename = 'IndexVariablesChunk' AND indexname =
'graph_index_variables_id' AND indexdef LIKE
'%"chunk_id"%'</custom_sql>
+ <add_rows_result>N</add_rows_result>
+ <clear_result_rows>Y</clear_result_rows>
<parallel>N</parallel>
- <xloc>864</xloc>
+ <xloc>544</xloc>
<yloc>96</yloc>
<attributes_hac/>
</action>
<action>
- <name>drop indexes after test</name>
+ <name>drop index</name>
<description/>
<type>NEO4J_INDEX</type>
<attributes/>
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${GRAPH_INDEX}</index_name>
+ <object_name>${GRAPH_LABEL}</object_name>
+ <object_properties>${GRAPH_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
- <index_type>VECTOR</index_type>
- <vector_dimensions>3</vector_dimensions>
- <vector_similarity>COSINE</vector_similarity>
+ <index_type>RANGE</index_type>
</update>
</updates>
<parallel>N</parallel>
- <xloc>1024</xloc>
+ <xloc>704</xloc>
+ <yloc>96</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>index dropped</name>
+ <description>The index with the resolved name is gone</description>
+ <type>EVAL_TABLE_CONTENT</type>
+ <attributes/>
+ <connection>age-sql</connection>
+ <schemaname/>
+ <tablename/>
+ <success_condition>rows_count_equal</success_condition>
+ <limit>0</limit>
+ <is_custom_sql>Y</is_custom_sql>
+ <is_usevars>N</is_usevars>
+ <custom_sql>SELECT indexname FROM pg_indexes WHERE schemaname = 'hop_it'
AND tablename = 'IndexVariablesChunk' AND indexname =
'graph_index_variables_id' AND indexdef LIKE
'%"chunk_id"%'</custom_sql>
+ <add_rows_result>N</add_rows_result>
+ <clear_result_rows>Y</clear_result_rows>
+ <parallel>N</parallel>
+ <xloc>864</xloc>
<yloc>96</yloc>
<attributes_hac/>
</action>
<action>
- <name>drop indexes after failure</name>
+ <name>drop index after failure</name>
<description/>
<type>NEO4J_INDEX</type>
<attributes/>
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${GRAPH_INDEX}</index_name>
+ <object_name>${GRAPH_LABEL}</object_name>
+ <object_properties>${GRAPH_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
- <index_type>VECTOR</index_type>
- <vector_dimensions>3</vector_dimensions>
- <vector_similarity>COSINE</vector_similarity>
+ <index_type>RANGE</index_type>
</update>
</updates>
<parallel>N</parallel>
- <xloc>1024</xloc>
+ <xloc>704</xloc>
<yloc>224</yloc>
<attributes_hac/>
</action>
@@ -220,7 +198,7 @@ limitations under the License.
<type>SUCCESS</type>
<attributes/>
<parallel>N</parallel>
- <xloc>1184</xloc>
+ <xloc>1024</xloc>
<yloc>96</yloc>
<attributes_hac/>
</action>
@@ -229,9 +207,9 @@ limitations under the License.
<description/>
<type>ABORT</type>
<attributes/>
- <message>graph-vector-search-nodes failed</message>
+ <message>graph-index-variables failed</message>
<parallel>N</parallel>
- <xloc>1184</xloc>
+ <xloc>1024</xloc>
<yloc>224</yloc>
<attributes_hac/>
</action>
@@ -239,67 +217,74 @@ limitations under the License.
<hops>
<hop>
<from>Start</from>
- <to>drop indexes before</to>
+ <to>drop index before</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>Y</unconditional>
</hop>
<hop>
- <from>drop indexes before</from>
- <to>cleanup-database.hwf</to>
+ <from>drop index before</from>
+ <to>create index</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
- <unconditional>Y</unconditional>
+ <unconditional>N</unconditional>
</hop>
<hop>
- <from>cleanup-database.hwf</from>
- <to>create indexes</to>
+ <from>create index</from>
+ <to>index created</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
</hop>
<hop>
- <from>create indexes</from>
- <to>graph-vector-search-nodes-build.hpl</to>
+ <from>index created</from>
+ <to>drop index</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
</hop>
<hop>
- <from>graph-vector-search-nodes-build.hpl</from>
- <to>Run graph-vector-search-nodes tests</to>
+ <from>drop index</from>
+ <to>index dropped</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
</hop>
<hop>
- <from>Run graph-vector-search-nodes tests</from>
- <to>drop indexes after test</to>
+ <from>index dropped</from>
+ <to>Success</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>N</unconditional>
</hop>
<hop>
- <from>drop indexes after test</from>
- <to>Success</to>
+ <from>create index</from>
+ <to>drop index after failure</to>
<enabled>Y</enabled>
- <evaluation>Y</evaluation>
- <unconditional>Y</unconditional>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
</hop>
<hop>
- <from>Run graph-vector-search-nodes tests</from>
- <to>drop indexes after failure</to>
+ <from>index created</from>
+ <to>drop index after failure</to>
<enabled>Y</enabled>
<evaluation>N</evaluation>
<unconditional>N</unconditional>
</hop>
<hop>
- <from>drop indexes after failure</from>
+ <from>drop index after failure</from>
<to>Error out</to>
<enabled>Y</enabled>
<evaluation>Y</evaluation>
<unconditional>Y</unconditional>
</hop>
+ <hop>
+ <from>index dropped</from>
+ <to>Error out</to>
+ <enabled>Y</enabled>
+ <evaluation>N</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
</hops>
<notepads>
</notepads>
diff --git
a/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
b/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
index 9b7c53cad9..472bffadf2 100644
---
a/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
+++
b/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
@@ -120,7 +120,7 @@ limitations under the License.
</partitioning>
<connection>Test</connection>
<element_type>NODE</element_type>
- <index_name>graph_vector_search_nodes</index_name>
+ <index_name>${VECTOR_INDEX}</index_name>
<label>VectorSearchDoc</label>
<vector_property>embedding</vector_property>
<similarity>COSINE</similarity>
diff --git
a/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
b/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
index 3b95ea27e9..2db21b2f30 100644
---
a/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
+++
b/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
@@ -33,6 +33,21 @@ limitations under the License.
<default_value>graph-vector-search-nodes</default_value>
<description/>
</parameter>
+ <parameter>
+ <name>VECTOR_INDEX</name>
+ <default_value>graph_vector_search_nodes</default_value>
+ <description>The vector index: the actions and Graph vector search
resolve its name</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_LABEL</name>
+ <default_value>VectorSearchDoc</default_value>
+ <description>The label of the vector index</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_PROPERTY</name>
+ <default_value>embedding</default_value>
+ <description>The property of the vector index</description>
+ </parameter>
</parameters>
<actions>
<action>
@@ -61,9 +76,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -110,9 +125,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>CREATE</update_type>
<index_type>VECTOR</index_type>
@@ -176,9 +191,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -199,9 +214,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
diff --git
a/integration-tests/memgraph/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
b/integration-tests/memgraph/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
index 9b7c53cad9..472bffadf2 100644
---
a/integration-tests/memgraph/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
+++
b/integration-tests/memgraph/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
@@ -120,7 +120,7 @@ limitations under the License.
</partitioning>
<connection>Test</connection>
<element_type>NODE</element_type>
- <index_name>graph_vector_search_nodes</index_name>
+ <index_name>${VECTOR_INDEX}</index_name>
<label>VectorSearchDoc</label>
<vector_property>embedding</vector_property>
<similarity>COSINE</similarity>
diff --git
a/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
b/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
index 3b95ea27e9..2db21b2f30 100644
---
a/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
+++
b/integration-tests/memgraph/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
@@ -33,6 +33,21 @@ limitations under the License.
<default_value>graph-vector-search-nodes</default_value>
<description/>
</parameter>
+ <parameter>
+ <name>VECTOR_INDEX</name>
+ <default_value>graph_vector_search_nodes</default_value>
+ <description>The vector index: the actions and Graph vector search
resolve its name</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_LABEL</name>
+ <default_value>VectorSearchDoc</default_value>
+ <description>The label of the vector index</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_PROPERTY</name>
+ <default_value>embedding</default_value>
+ <description>The property of the vector index</description>
+ </parameter>
</parameters>
<actions>
<action>
@@ -61,9 +76,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -110,9 +125,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>CREATE</update_type>
<index_type>VECTOR</index_type>
@@ -176,9 +191,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -199,9 +214,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
diff --git
a/integration-tests/neo4j/datasets/graph-vector-search-nodes-golden.csv
b/integration-tests/neo4j/datasets/graph-vector-search-nodes-golden.csv
new file mode 100644
index 0000000000..dafa2fe2e9
--- /dev/null
+++ b/integration-tests/neo4j/datasets/graph-vector-search-nodes-golden.csv
@@ -0,0 +1,6 @@
+query_id,seq,found_id,score
+q1,1,D1,1.00
+q1,2,D2,0.80
+q1,3,D3,0.60
+q2,4,D4,1.00
+q3,5,,
diff --git
a/integration-tests/neo4j/metadata/dataset/graph-vector-search-nodes-golden.json
b/integration-tests/neo4j/metadata/dataset/graph-vector-search-nodes-golden.json
new file mode 100644
index 0000000000..73c04553d1
--- /dev/null
+++
b/integration-tests/neo4j/metadata/dataset/graph-vector-search-nodes-golden.json
@@ -0,0 +1,40 @@
+{
+ "base_filename": "graph-vector-search-nodes-golden.csv",
+ "name": "graph-vector-search-nodes-golden",
+ "description": "The rank, id and rounded score of the matches",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "query_id"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "seq"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "found_id"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 2,
+ "field_precision": -1,
+ "field_format": "",
+ "field_name": "score"
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/neo4j/metadata/neo4j-graph-model/VectorSearchDocs.json
b/integration-tests/neo4j/metadata/neo4j-graph-model/VectorSearchDocs.json
new file mode 100644
index 0000000000..7d546b45d7
--- /dev/null
+++ b/integration-tests/neo4j/metadata/neo4j-graph-model/VectorSearchDocs.json
@@ -0,0 +1,38 @@
+{
+ "relationships": [],
+ "nodes": [
+ {
+ "presentation": {
+ "x": 0,
+ "y": 0
+ },
+ "name": "Doc",
+ "description": "",
+ "properties": [
+ {
+ "indexed": false,
+ "unique": false,
+ "name": "id",
+ "description": "",
+ "type": "String",
+ "mandatory": false,
+ "primary": true
+ },
+ {
+ "indexed": false,
+ "unique": false,
+ "name": "embedding",
+ "description": "",
+ "type": "Vector",
+ "mandatory": false,
+ "primary": false
+ }
+ ],
+ "labels": [
+ "VectorSearchDoc"
+ ]
+ }
+ ],
+ "name": "VectorSearchDocs",
+ "description": "Nodes with an embedding, searched with Graph vector search"
+}
diff --git
a/integration-tests/neo4j/metadata/unit-test/graph-vector-search-nodes-validation
UNIT.json
b/integration-tests/neo4j/metadata/unit-test/graph-vector-search-nodes-validation
UNIT.json
new file mode 100644
index 0000000000..42ad8b54aa
--- /dev/null
+++
b/integration-tests/neo4j/metadata/unit-test/graph-vector-search-nodes-validation
UNIT.json
@@ -0,0 +1,40 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": true,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "query_id",
+ "data_set_field": "query_id"
+ },
+ {
+ "transform_field": "seq",
+ "data_set_field": "seq"
+ },
+ {
+ "transform_field": "found_id",
+ "data_set_field": "found_id"
+ },
+ {
+ "transform_field": "score",
+ "data_set_field": "score"
+ }
+ ],
+ "field_order": [
+ "seq"
+ ],
+ "transform_name": "Validation",
+ "data_set_name": "graph-vector-search-nodes-golden"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "graph-vector-search-nodes-validation UNIT",
+ "description": "",
+ "trans_test_tweaks": [],
+ "persist_filename": "",
+ "pipeline_filename":
"./tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl",
+ "test_type": "UNIT_TEST"
+}
diff --git
a/integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-build.hpl
b/integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-build.hpl
new file mode 100644
index 0000000000..e99df5110b
--- /dev/null
+++
b/integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-build.hpl
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
+
+-->
+<pipeline>
+ <info>
+ <name>graph-vector-search-nodes-build</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Four VectorSearchDoc nodes with an embedding</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <pipeline_status>0</pipeline_status>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/10/07 12:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/10/07 12:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>vectors</from>
+ <to>VectorSearchDocs</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>vectors</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <set_empty_string>N</set_empty_string>
+ <length>-1</length>
+ <name>id</name>
+ <precision>-1</precision>
+ <type>String</type>
+ </field>
+ <field>
+ <set_empty_string>N</set_empty_string>
+ <length>-1</length>
+ <name>embedding</name>
+ <precision>-1</precision>
+ <type>Vector</type>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>D1</item>
+ <item>[1,0,0]</item>
+ </line>
+ <line>
+ <item>D2</item>
+ <item>[0.8,0.6,0]</item>
+ </line>
+ <line>
+ <item>D3</item>
+ <item>[0.6,0.8,0]</item>
+ </line>
+ <line>
+ <item>D4</item>
+ <item>[0,0,1]</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>128</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>VectorSearchDocs</name>
+ <type>Neo4jGraphOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <batch_size>100</batch_size>
+ <connection>Test</connection>
+ <create_indexes>N</create_indexes>
+ <mappings>
+ <mapping>
+ <source_field>id</source_field>
+ <target_hint>None</target_hint>
+ <target_name>Doc</target_name>
+ <target_property>id</target_property>
+ <target_type>Node</target_type>
+ </mapping>
+ <mapping>
+ <source_field>embedding</source_field>
+ <target_hint>None</target_hint>
+ <target_name>Doc</target_name>
+ <target_property>embedding</target_property>
+ <target_type>Node</target_type>
+ </mapping>
+ </mappings>
+ <model>VectorSearchDocs</model>
+ <node_mappings/>
+ <out_of_order_allowed>Y</out_of_order_allowed>
+ <relationship_mappings>
+ </relationship_mappings>
+ <return_graph_field>graph</return_graph_field>
+ <returning_graph>N</returning_graph>
+ <validate_against_model>N</validate_against_model>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
b/integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
similarity index 99%
copy from
integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
copy to
integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
index 9b7c53cad9..472bffadf2 100644
---
a/integration-tests/falkordb/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
+++
b/integration-tests/neo4j/tests/graph-vector-search-nodes/graph-vector-search-nodes-validation.hpl
@@ -120,7 +120,7 @@ limitations under the License.
</partitioning>
<connection>Test</connection>
<element_type>NODE</element_type>
- <index_name>graph_vector_search_nodes</index_name>
+ <index_name>${VECTOR_INDEX}</index_name>
<label>VectorSearchDoc</label>
<vector_property>embedding</vector_property>
<similarity>COSINE</similarity>
diff --git
a/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
b/integration-tests/neo4j/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
similarity index 87%
copy from
integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
copy to
integration-tests/neo4j/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
index 3b95ea27e9..2db21b2f30 100644
---
a/integration-tests/falkordb/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
+++
b/integration-tests/neo4j/tests/graph-vector-search-nodes/main-graph-vector-search-nodes.hwf
@@ -33,6 +33,21 @@ limitations under the License.
<default_value>graph-vector-search-nodes</default_value>
<description/>
</parameter>
+ <parameter>
+ <name>VECTOR_INDEX</name>
+ <default_value>graph_vector_search_nodes</default_value>
+ <description>The vector index: the actions and Graph vector search
resolve its name</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_LABEL</name>
+ <default_value>VectorSearchDoc</default_value>
+ <description>The label of the vector index</description>
+ </parameter>
+ <parameter>
+ <name>VECTOR_PROPERTY</name>
+ <default_value>embedding</default_value>
+ <description>The property of the vector index</description>
+ </parameter>
</parameters>
<actions>
<action>
@@ -61,9 +76,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -110,9 +125,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>CREATE</update_type>
<index_type>VECTOR</index_type>
@@ -176,9 +191,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
@@ -199,9 +214,9 @@ limitations under the License.
<connection>Test</connection>
<updates>
<update>
- <index_name>graph_vector_search_nodes</index_name>
- <object_name>VectorSearchDoc</object_name>
- <object_properties>embedding</object_properties>
+ <index_name>${VECTOR_INDEX}</index_name>
+ <object_name>${VECTOR_LABEL}</object_name>
+ <object_properties>${VECTOR_PROPERTY}</object_properties>
<object_type>NODE</object_type>
<update_type>DROP</update_type>
<index_type>VECTOR</index_type>
diff --git
a/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphConnection.java
b/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphConnection.java
index d5da0cc64f..7dd5bed06e 100644
--- a/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphConnection.java
+++ b/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphConnection.java
@@ -21,6 +21,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -84,6 +85,23 @@ public class AgeGraphConnection implements IGraphConnection {
}
}
+ /**
+ * The index statements of {@link AgeGraphDialect} are SQL: they run as they
are, not in {@code
+ * cypher()}. PostgreSQL runs the statements of one call in one transaction.
+ */
+ @Override
+ public void executeSchemaStatement(String statement, boolean inTransaction)
throws HopException {
+ if (log != null && log.isDebug()) {
+ log.logDebug("Apache AGE schema statement: " + statement);
+ }
+ try (Statement sqlStatement = connection.createStatement()) {
+ sqlStatement.execute(statement);
+ } catch (SQLException e) {
+ throw new HopException(
+ "Error executing statement on Apache AGE graph " + graphName + ": "
+ statement, e);
+ }
+ }
+
@Override
public IGraphTransaction beginTransaction() throws HopException {
try {
diff --git
a/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphDialect.java
b/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphDialect.java
index cd84d01157..bcfa426c8d 100644
--- a/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphDialect.java
+++ b/plugins/tech/age/src/main/java/org/apache/hop/age/AgeGraphDialect.java
@@ -18,11 +18,16 @@
package org.apache.hop.age;
import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.graph.GraphIndexDefinition;
import org.apache.hop.core.graph.IGraphDialect;
/**
* Apache AGE speaks Cypher, but its indexes and constraints are PostgreSQL
ones: the Cypher index
- * and constraint statements are not supported. The indexes on node properties
are created in SQL.
+ * and constraint statements are not supported. The indexes on node properties
are created in SQL,
+ * run by {@link AgeGraphConnection#executeSchemaStatement}. There are no
vector indexes, no indexes
+ * on relationship properties and no constraints.
*/
public class AgeGraphDialect implements IGraphDialect {
@@ -45,6 +50,89 @@ public class AgeGraphDialect implements IGraphDialect {
return getId();
}
+ @Override
+ public boolean isSupportingNodeIndexes() {
+ return true;
+ }
+
+ /**
+ * Create the label if it doesn't exist yet, then the index on its table:
the index can be created
+ * before the nodes are loaded.
+ */
+ @Override
+ public String getCreateIndexStatement(GraphIndexDefinition index) throws
HopException {
+ validateIndex(index);
+ if (index.properties().isEmpty()) {
+ throw new HopException("Please specify the properties to index on " +
index.objectName());
+ }
+ return getCreateLabelStatement(index.objectName())
+ + ";\n"
+ + getCreateNodeIndexStatement(index.name(), index.objectName(),
index.properties());
+ }
+
+ @Override
+ public String getDropIndexStatement(GraphIndexDefinition index) throws
HopException {
+ validateIndex(index);
+ return "DROP INDEX IF EXISTS "
+ + quoteIdentifier(graphName)
+ + '.'
+ + quoteIdentifier(index.name());
+ }
+
+ /** Only named indexes on nodes: PostgreSQL needs the name to create the
index if not exists. */
+ private void validateIndex(GraphIndexDefinition index) throws HopException {
+ if (index.isRelationship()) {
+ throw IGraphDialect.indexesNotSupported(this, index.objectType(),
index.objectName());
+ }
+ if (StringUtils.isEmpty(index.objectName())) {
+ throw new HopException("Please specify the label of the index " +
index.name());
+ }
+ if (StringUtils.isEmpty(index.name())) {
+ throw new HopException(
+ "Apache AGE indexes need a name. Label: "
+ + index.objectName()
+ + ", properties: "
+ + String.join(",", index.properties()));
+ }
+ }
+
+ /**
+ * A block creating the node label in the graph unless it exists: AGE
creates the table of a label
+ * with the first node. Another session can create the same label between
the check and the
+ * creation: the error is ignored if the label exists when checked again.
+ */
+ String getCreateLabelStatement(String label) {
+ String graphLiteral = quoteLiteral(graphName);
+ String labelLiteral = quoteLiteral(label);
+ String labelExists =
+ "EXISTS (SELECT 1 FROM ag_catalog.ag_label l"
+ + " JOIN ag_catalog.ag_graph g ON g.graphid = l.graph WHERE g.name
= "
+ + graphLiteral
+ + " AND l.kind = 'v' AND l.name = "
+ + labelLiteral
+ + ")";
+ String body =
+ "BEGIN IF NOT "
+ + labelExists
+ + " THEN BEGIN PERFORM ag_catalog.create_vlabel("
+ + graphLiteral
+ + ", "
+ + labelLiteral
+ + "); EXCEPTION WHEN OTHERS THEN IF NOT "
+ + labelExists
+ + " THEN RAISE; END IF; END; END IF; END";
+ String tag = "$hop$";
+ for (int i = 1; body.contains(tag); i++) {
+ tag = "$hop" + i + "$";
+ }
+ return "DO " + tag + " " + body + " " + tag;
+ }
+
+ /** A PostgreSQL string literal: between single quotes, with the single
quotes in it doubled. */
+ static String quoteLiteral(String value) {
+ return '\'' + value.replace("'", "''") + '\'';
+ }
+
/**
* A PostgreSQL index on the label's table: AGE stores the nodes of each
label in a table of the
* graph's schema, their properties in an agtype column. The label needs to
exist: it does once a
diff --git
a/plugins/tech/age/src/test/java/org/apache/hop/age/AgeGraphDialectCharacterizationTest.java
b/plugins/tech/age/src/test/java/org/apache/hop/age/AgeGraphDialectCharacterizationTest.java
index db47727856..1749fc1abb 100644
---
a/plugins/tech/age/src/test/java/org/apache/hop/age/AgeGraphDialectCharacterizationTest.java
+++
b/plugins/tech/age/src/test/java/org/apache/hop/age/AgeGraphDialectCharacterizationTest.java
@@ -217,12 +217,19 @@ class AgeGraphDialectCharacterizationTest {
/**
* Changed on purpose since the dialect SPI: AGE|executionIndex: the SQL
index statement, as for
- * an AGE graph database connection before.
+ * an AGE graph database connection before. Since #8836: index.create|A,
index.drop|A and the
+ * nodeIndexes flag, AGE creates and drops named node indexes.
*/
static Map<String, String> expected() {
Map<String, String> e = new LinkedHashMap<>();
- e.put("AGE|index.create|A", "!ERROR");
- e.put("AGE|index.drop|A", "!ERROR");
+ e.put(
+ "AGE|index.create|A",
+ "DO $hop$ BEGIN IF NOT EXISTS (SELECT 1 FROM ag_catalog.ag_label l
JOIN ag_catalog.ag_graph g ON g.graphid = l.graph WHERE g.name = 'hop_graph'
AND l.kind = 'v' AND l.name = 'Person') THEN BEGIN PERFORM
ag_catalog.create_vlabel('hop_graph', 'Person');"
+ + " EXCEPTION WHEN OTHERS THEN IF NOT EXISTS (SELECT 1 FROM
ag_catalog.ag_label l JOIN ag_catalog.ag_graph g ON g.graphid = l.graph WHERE
g.name = 'hop_graph' AND l.kind = 'v' AND l.name = 'Person') THEN RAISE; END
IF; END; END IF; END $hop$;\n"
+ + "CREATE INDEX IF NOT EXISTS \"idx\" ON \"hop_graph\".\"Person\"
("
+ + "ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties,
'\"name\"'::ag_catalog.agtype]), "
+ + "ag_catalog.agtype_access_operator(VARIADIC ARRAY[properties,
'\"age\"'::ag_catalog.agtype]))");
+ e.put("AGE|index.drop|A", "DROP INDEX IF EXISTS \"hop_graph\".\"idx\"");
e.put("AGE|index.create|B", "!ERROR");
e.put("AGE|index.drop|B", "!ERROR");
e.put("AGE|index.create|C", "!ERROR");
@@ -280,7 +287,7 @@ class AgeGraphDialectCharacterizationTest {
e.put("AGE|vectorValue|$p", "$p");
e.put("AGE|vectorValue|pr.p", "pr.p");
e.put("AGE|flag|cypher", "true");
- e.put("AGE|flag|nodeIndexes", "false");
+ e.put("AGE|flag|nodeIndexes", "true");
e.put("AGE|flag|relationshipIndexes", "false");
e.put("AGE|flag|schemaChangesInTransactions", "true");
e.put("AGE|flag|vectorIndexes", "false");
diff --git a/plugins/tech/age/src/test/java/org/apache/hop/age/AgeIT.java
b/plugins/tech/age/src/test/java/org/apache/hop/age/AgeIT.java
index 4bfa685088..aeb2652afa 100644
--- a/plugins/tech/age/src/test/java/org/apache/hop/age/AgeIT.java
+++ b/plugins/tech/age/src/test/java/org/apache/hop/age/AgeIT.java
@@ -44,7 +44,9 @@ import java.util.concurrent.TimeUnit;
import org.apache.hop.core.HopClientEnvironment;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.graph.GraphIndex;
+import org.apache.hop.core.graph.GraphIndexDefinition;
import org.apache.hop.core.graph.GraphNodeValue;
+import org.apache.hop.core.graph.GraphObjectType;
import org.apache.hop.core.graph.GraphPathValue;
import org.apache.hop.core.graph.GraphRelationshipValue;
import org.apache.hop.core.graph.GraphSchema;
@@ -226,6 +228,46 @@ class AgeIT {
}
}
+ /**
+ * The index statements of the dialect run through executeSchemaStatement:
the label is created
+ * when it doesn't exist yet, creating and dropping twice does nothing the
second time.
+ */
+ @Test
+ void testCreateAndDropIndexStatements() throws Exception {
+ try (IGraphConnection connection =
graphDatabase.connect(LogChannel.GENERAL, variables, "it")) {
+ IGraphDialect dialect = connection.getGraphDialect();
+ GraphIndexDefinition index =
+ new GraphIndexDefinition(
+ "it_chunk_id", GraphObjectType.NODE, "ItChunk", List.of("id",
"the name"));
+ String create = dialect.getCreateIndexStatement(index);
+ connection.executeSchemaStatement(create, true);
+ connection.executeSchemaStatement(create, true);
+ GraphIndex created =
+ connection.getIndexes().stream()
+ .filter(i -> i.name().equals("it_chunk_id"))
+ .findFirst()
+ .orElseThrow();
+ assertEquals(List.of("ItChunk"), created.labelsOrTypes());
+ assertEquals(List.of("id", "the name"), created.properties());
+
+ // The label created with the index takes nodes
+ connection.execute("CREATE (:ItChunk {id: 1})", Map.of());
+ assertEquals(
+ 1L,
+ ((Number)
+ connection
+ .execute("MATCH (c:ItChunk) RETURN count(c) AS n",
Map.of())
+ .get(0)
+ .get("n"))
+ .longValue());
+
+ String drop = dialect.getDropIndexStatement(index);
+ connection.executeSchemaStatement(drop, true);
+ connection.executeSchemaStatement(drop, true);
+ assertTrue(connection.getIndexes().stream().noneMatch(i ->
i.name().equals("it_chunk_id")));
+ }
+ }
+
/** A Vector property arrives as a list of numbers; AGE stores it as an
agtype array. */
@Test
void testVectorWriteAndRead() throws Exception {
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
index 00ff927a40..fde47a64d6 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraint.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.graph.GraphConstraintDefinition;
import org.apache.hop.core.graph.GraphObjectType;
import org.apache.hop.core.graph.IGraphDialect;
+import org.apache.hop.core.variables.IVariables;
import org.apache.hop.metadata.api.HopMetadataProperty;
import org.apache.hop.metadata.api.HopMetadataPropertyType;
import org.apache.hop.neo4j.actions.index.Neo4jIndex;
@@ -142,8 +143,29 @@ public class Neo4jConstraint extends ActionBase implements
IAction {
Neo4jIndex.splitProperties(constraintUpdate.getObjectProperties()));
}
+ /**
+ * A copy of the update with the variables resolved in its constraint name,
object name and
+ * properties.
+ */
+ ConstraintUpdate resolved(ConstraintUpdate constraintUpdate) {
+ return resolved(constraintUpdate, this);
+ }
+
+ /**
+ * A copy of the update with the variables resolved in its constraint name,
object name and
+ * properties.
+ */
+ public static ConstraintUpdate resolved(ConstraintUpdate constraintUpdate,
IVariables variables) {
+ ConstraintUpdate copy = new ConstraintUpdate(constraintUpdate);
+
copy.setConstraintName(variables.resolve(constraintUpdate.getConstraintName()));
+ copy.setObjectName(variables.resolve(constraintUpdate.getObjectName()));
+
copy.setObjectProperties(variables.resolve(constraintUpdate.getObjectProperties()));
+ return copy;
+ }
+
private void dropConstraint(final ConstraintUpdate constraintUpdate) throws
HopException {
- String cypher = generateDropConstraintCypher(constraintUpdate,
connection.getDialect());
+ String cypher =
+ generateDropConstraintCypher(resolved(constraintUpdate),
connection.getDialect(this));
// Run this cypher statement...
//
@@ -152,7 +174,8 @@ public class Neo4jConstraint extends ActionBase implements
IAction {
}
private void createConstraint(ConstraintUpdate constraintUpdate) throws
HopException {
- String cypher = generateCreateConstraintCypher(constraintUpdate,
connection.getDialect());
+ String cypher =
+ generateCreateConstraintCypher(resolved(constraintUpdate),
connection.getDialect(this));
// Run this cypher statement...
//
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintDialog.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintDialog.java
index cdc80222db..d3208cc74e 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintDialog.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintDialog.java
@@ -140,6 +140,9 @@ public class Neo4jConstraintDialog extends ActionDialog
implements IActionDialog
PKG,
"Neo4jConstraintDialog.ConstraintUpdates.Column.ObjectProperties"),
ColumnInfo.COLUMN_TYPE_TEXT),
};
+ columns[3].setUsingVariables(true);
+ columns[4].setUsingVariables(true);
+ columns[5].setUsingVariables(true);
// Only offer what the database of the selected connection supports
//
@@ -231,8 +234,10 @@ public class Neo4jConstraintDialog extends ActionDialog
implements IActionDialog
String objectProperties = item.getText(6);
ConstraintUpdate constraintUpdate =
- new ConstraintUpdate(
- type, objectType, constraintType, constraintName, objectName,
objectProperties);
+ Neo4jConstraint.resolved(
+ new ConstraintUpdate(
+ type, objectType, constraintType, constraintName,
objectName, objectProperties),
+ variables);
String cypher;
if (type == UpdateType.CREATE) {
@@ -344,7 +349,7 @@ public class Neo4jConstraintDialog extends ActionDialog
implements IActionDialog
NeoConnectionUtils.findGraphConnection(
getMetadataProvider(), variables.resolve(wConnection.getText()));
if (connection != null) {
- return connection.getDialect();
+ return connection.getDialect(variables);
}
} catch (Exception e) {
// Fall back to Neo4j
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
index f0d1f28009..7d0c77ad91 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndex.java
@@ -28,6 +28,7 @@ import org.apache.hop.core.graph.GraphIndexDefinition;
import org.apache.hop.core.graph.GraphObjectType;
import org.apache.hop.core.graph.GraphVectorIndexDefinition;
import org.apache.hop.core.graph.IGraphDialect;
+import org.apache.hop.core.variables.IVariables;
import org.apache.hop.metadata.api.HopMetadataProperty;
import org.apache.hop.metadata.api.HopMetadataPropertyType;
import org.apache.hop.neo4j.shared.NamedGraphConnection;
@@ -139,16 +140,30 @@ public class Neo4jIndex extends ActionBase implements
IAction {
return dialect.getCreateIndexStatement(toIndexDefinition(indexUpdate));
}
- /** A copy of the update with the variables in its vector settings resolved.
*/
- private IndexUpdate resolved(IndexUpdate indexUpdate) {
+ /**
+ * A copy of the update with the variables resolved in its index name,
object name, properties and
+ * vector settings.
+ */
+ IndexUpdate resolved(IndexUpdate indexUpdate) {
+ return resolved(indexUpdate, this);
+ }
+
+ /**
+ * A copy of the update with the variables resolved in its index name,
object name, properties and
+ * vector settings.
+ */
+ public static IndexUpdate resolved(IndexUpdate indexUpdate, IVariables
variables) {
IndexUpdate copy = new IndexUpdate(indexUpdate);
- copy.setVectorDimensions(resolve(indexUpdate.getVectorDimensions()));
- copy.setVectorCapacity(resolve(indexUpdate.getVectorCapacity()));
+ copy.setIndexName(variables.resolve(indexUpdate.getIndexName()));
+ copy.setObjectName(variables.resolve(indexUpdate.getObjectName()));
+
copy.setObjectProperties(variables.resolve(indexUpdate.getObjectProperties()));
+
copy.setVectorDimensions(variables.resolve(indexUpdate.getVectorDimensions()));
+ copy.setVectorCapacity(variables.resolve(indexUpdate.getVectorCapacity()));
return copy;
}
private void dropIndex(final IndexUpdate indexUpdate) throws HopException {
- String cypher = generateDropIndexCypher(resolved(indexUpdate),
connection.getDialect());
+ String cypher = generateDropIndexCypher(resolved(indexUpdate),
connection.getDialect(this));
// Run this cypher statement...
//
@@ -157,7 +172,7 @@ public class Neo4jIndex extends ActionBase implements
IAction {
}
private void createIndex(IndexUpdate indexUpdate) throws HopException {
- String cypher = generateCreateIndexCypher(resolved(indexUpdate),
connection.getDialect());
+ String cypher = generateCreateIndexCypher(resolved(indexUpdate),
connection.getDialect(this));
// Run this cypher statement...
//
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndexDialog.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndexDialog.java
index be96aa4833..ddc7e4fc6e 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndexDialog.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/actions/index/Neo4jIndexDialog.java
@@ -146,6 +146,9 @@ public class Neo4jIndexDialog extends ActionDialog
implements IActionDialog {
};
columns[5].setToolTip(
BaseMessages.getString(PKG,
"Neo4jIndexDialog.IndexUpdates.Column.IndexType.Tooltip"));
+ columns[2].setUsingVariables(true);
+ columns[3].setUsingVariables(true);
+ columns[4].setUsingVariables(true);
columns[6].setUsingVariables(true);
columns[8].setUsingVariables(true);
columns[8].setToolTip(
@@ -213,10 +216,8 @@ public class Neo4jIndexDialog extends ActionDialog
implements IActionDialog {
for (int i = 0; i < items.size(); i++) {
TableItem item = items.get(i);
try {
- IndexUpdate indexUpdate = toIndexUpdate(item);
+ IndexUpdate indexUpdate = Neo4jIndex.resolved(toIndexUpdate(item),
variables);
UpdateType type = indexUpdate.getType();
-
indexUpdate.setVectorDimensions(variables.resolve(indexUpdate.getVectorDimensions()));
-
indexUpdate.setVectorCapacity(variables.resolve(indexUpdate.getVectorCapacity()));
String cypher;
if (type == UpdateType.CREATE) {
@@ -341,7 +342,7 @@ public class Neo4jIndexDialog extends ActionDialog
implements IActionDialog {
NeoConnectionUtils.findGraphConnection(
getMetadataProvider(), variables.resolve(wConnection.getText()));
if (connection != null) {
- return connection.getDialect();
+ return connection.getDialect(variables);
}
} catch (Exception e) {
// Fall back to Neo4j
diff --git
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnectionUtils.java
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnectionUtils.java
index bb916d3288..654a66db4e 100644
---
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnectionUtils.java
+++
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/shared/NeoConnectionUtils.java
@@ -356,7 +356,7 @@ public class NeoConnectionUtils {
String cypher,
String description)
throws HopException {
- IGraphDialect dialect = graphConnection.getDialect();
+ IGraphDialect dialect = graphConnection.getDialect(variables);
// Connecting is outside the try which tolerates existing or missing
indexes: a connection
// error always fails, whatever its message says.
//
@@ -370,16 +370,10 @@ public class NeoConnectionUtils {
try (connection) {
log.logDetailed(description + " with cypher: " + cypher);
try {
- if (!dialect.isSupportingSchemaChangesInTransactions()
- || !connection.isSupportingTransactions()) {
- connection.execute(cypher, Map.of());
- } else {
- connection.executeWrite(
- transaction -> {
- transaction.execute(cypher, Map.of());
- return true;
- });
- }
+ connection.executeSchemaStatement(
+ cypher,
+ dialect.isSupportingSchemaChangesInTransactions()
+ && connection.isSupportingTransactions());
} catch (HopException e) {
if (dialect.isExistingOrMissingIndexError(e)) {
log.logDetailed(description + ": nothing to do, " +
Const.getSimpleStackTrace(e));
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintVariablesTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintVariablesTest.java
new file mode 100644
index 0000000000..a03981135d
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/constraint/Neo4jConstraintVariablesTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.hop.neo4j.actions.constraint;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.graph.GraphConstraintType;
+import org.apache.hop.neo4j.bolt.Neo4jGraphDialect;
+import org.junit.jupiter.api.Test;
+
+/** The variables in the constraint name, object name and properties are
resolved. */
+class Neo4jConstraintVariablesTest {
+
+ private static Neo4jConstraint action() {
+ Neo4jConstraint action = new Neo4jConstraint("constraint");
+ action.setVariable("CONSTRAINT", "person_id");
+ action.setVariable("LABEL", "Person");
+ action.setVariable("PROPERTY", "id");
+ return action;
+ }
+
+ private static ConstraintUpdate update(UpdateType type) {
+ return new ConstraintUpdate(
+ type,
+ ObjectType.NODE,
+ GraphConstraintType.UNIQUE,
+ "${CONSTRAINT}",
+ "${LABEL}",
+ "${PROPERTY}");
+ }
+
+ @Test
+ void create() throws HopException {
+ ConstraintUpdate update = update(UpdateType.CREATE);
+ ConstraintUpdate resolved = action().resolved(update);
+ assertEquals(
+ "CREATE CONSTRAINT `person_id` IF NOT EXISTS FOR (n:`Person`) REQUIRE
n.`id` IS UNIQUE ",
+ Neo4jConstraint.generateCreateConstraintCypher(resolved,
Neo4jGraphDialect.INSTANCE));
+ // The action itself keeps the variables
+ assertEquals("${CONSTRAINT}", update.getConstraintName());
+ assertEquals(GraphConstraintType.UNIQUE, resolved.getConstraintType());
+ }
+
+ @Test
+ void drop() throws HopException {
+ assertEquals(
+ "DROP CONSTRAINT `person_id` IF EXISTS ",
+ Neo4jConstraint.generateDropConstraintCypher(
+ action().resolved(update(UpdateType.DROP)),
Neo4jGraphDialect.INSTANCE));
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexVariablesTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexVariablesTest.java
new file mode 100644
index 0000000000..ab17ca76c9
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/actions/index/Neo4jIndexVariablesTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.hop.neo4j.actions.index;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.graph.GraphVectorSimilarity;
+import org.apache.hop.neo4j.bolt.MemgraphGraphDialect;
+import org.apache.hop.neo4j.bolt.Neo4jGraphDialect;
+import org.junit.jupiter.api.Test;
+
+/** The variables in the index name, object name and properties are resolved,
for every dialect. */
+class Neo4jIndexVariablesTest {
+
+ private static Neo4jIndex action() {
+ Neo4jIndex action = new Neo4jIndex("index");
+ action.setVariable("INDEX", "chunk_index");
+ action.setVariable("LABEL", "Chunk");
+ action.setVariable("PROPERTY", "embedding");
+ action.setVariable("DIMENSIONS", "768");
+ return action;
+ }
+
+ @Test
+ void createRangeIndex() throws HopException {
+ Neo4jIndex action = action();
+ IndexUpdate update =
+ action.resolved(
+ new IndexUpdate(
+ UpdateType.CREATE, ObjectType.NODE, "${INDEX}", "${LABEL}",
"id, ${PROPERTY}"));
+ assertEquals(
+ "CREATE INDEX `chunk_index` IF NOT EXISTS FOR (n:`Chunk`) ON (n.`id`,
n.`embedding`)",
+ Neo4jIndex.generateCreateIndexCypher(update,
Neo4jGraphDialect.INSTANCE));
+ }
+
+ @Test
+ void dropRangeIndex() throws HopException {
+ IndexUpdate update =
+ action()
+ .resolved(
+ new IndexUpdate(UpdateType.DROP, ObjectType.NODE, "${INDEX}",
"${LABEL}", "id"));
+ assertEquals(
+ "DROP INDEX `chunk_index` IF EXISTS",
+ Neo4jIndex.generateDropIndexCypher(update,
Neo4jGraphDialect.INSTANCE));
+ }
+
+ @Test
+ void createAndDropVectorIndex() throws HopException {
+ Neo4jIndex action = action();
+ IndexUpdate create = vector(UpdateType.CREATE);
+ IndexUpdate resolved = action.resolved(create);
+
+ assertEquals("chunk_index", resolved.getIndexName());
+ assertEquals("Chunk", resolved.getObjectName());
+ assertEquals("embedding", resolved.getObjectProperties());
+ assertEquals("768", resolved.getVectorDimensions());
+ // The action itself keeps the variables
+ assertEquals("${INDEX}", create.getIndexName());
+
+ String memgraph = Neo4jIndex.generateCreateIndexCypher(resolved,
MemgraphGraphDialect.INSTANCE);
+ assertEquals(
+ "CREATE VECTOR INDEX `chunk_index` ON :`Chunk`(`embedding`)",
+ memgraph.substring(0, memgraph.indexOf(" WITH CONFIG")));
+ assertEquals(
+ "DROP VECTOR INDEX `chunk_index`",
+ Neo4jIndex.generateDropIndexCypher(
+ action.resolved(vector(UpdateType.DROP)),
MemgraphGraphDialect.INSTANCE));
+ }
+
+ private static IndexUpdate vector(UpdateType type) {
+ IndexUpdate update =
+ new IndexUpdate(type, ObjectType.NODE, "${INDEX}", "${LABEL}",
"${PROPERTY}");
+ update.setIndexType(IndexType.VECTOR);
+ update.setVectorDimensions("${DIMENSIONS}");
+ update.setVectorSimilarity(GraphVectorSimilarity.COSINE);
+ return update;
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/shared/ThirdPartyGraphDialectTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/shared/ThirdPartyGraphDialectTest.java
index 8673822662..fbf5da9354 100644
---
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/shared/ThirdPartyGraphDialectTest.java
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/shared/ThirdPartyGraphDialectTest.java
@@ -31,6 +31,7 @@ import org.apache.hop.core.graph.GraphDatabaseMeta;
import org.apache.hop.core.graph.GraphIndexDefinition;
import org.apache.hop.core.graph.IGraphDialect;
import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.IVariables;
import org.apache.hop.core.variables.Variables;
import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
import org.apache.hop.neo4j.actions.index.IndexUpdate;
@@ -106,6 +107,41 @@ class ThirdPartyGraphDialectTest {
assertEquals("ACME KEY `Person`", database.executed.get(1));
}
+ /**
+ * The actions use the dialect of the connection with its settings resolved:
the statements of
+ * Apache AGE, for example, name the graph, which can be a variable.
+ */
+ @Test
+ void testActionsUseTheDialectWithResolvedSettings() throws Exception {
+ FakeGraphDatabase database =
+ new FakeGraphDatabase(new AcmeGraphDialect(), null) {
+ @Override
+ public IGraphDialect getGraphDialect(IVariables variables) {
+ String graph = variables.resolve("${ACME_GRAPH}");
+ return new AcmeGraphDialect() {
+ @Override
+ public String getCreateIndexStatement(GraphIndexDefinition
index) {
+ return "ACME CREATE INDEX ON " + graph + "." +
quote(index.objectName());
+ }
+ };
+ }
+ };
+ MemoryMetadataProvider metadataProvider = new MemoryMetadataProvider();
+ metadataProvider
+ .getSerializer(GraphDatabaseMeta.class)
+ .save(new GraphDatabaseMeta("acme", database));
+
+ Neo4jIndex action = new Neo4jIndex("index");
+ action.setVariable("ACME_GRAPH", "test_graph");
+ action.setConnectionName("acme");
+ action
+ .getIndexUpdates()
+ .add(new IndexUpdate(UpdateType.CREATE, ObjectType.NODE, "i",
"Person", "id"));
+ action.setMetadataProvider(metadataProvider);
+ assertTrue(action.execute(new Result(), 0).getResult());
+ assertEquals(List.of("ACME CREATE INDEX ON test_graph.`Person`"),
database.executed);
+ }
+
/** The dialect decides which errors mean the index exists already. */
@Test
void testThirdPartyExistingIndexIsNotAnError() throws Exception {