Skip to content

Commit c94c6b0

Browse files
lvcarobfrank
authored andcommitted
feat: supported OpenCypher constraints
Fixed issue #3459 (cherry picked from commit fcd6a39)
1 parent 7cf1435 commit c94c6b0

4 files changed

Lines changed: 529 additions & 6 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.query.opencypher.ast;
20+
21+
import java.util.List;
22+
23+
/**
24+
* AST node for Cypher DDL statements (constraints and indexes).
25+
* These are schema-modifying commands that bypass the normal query execution pipeline.
26+
*/
27+
public class CypherDDLStatement implements CypherStatement {
28+
29+
public enum Kind {
30+
CREATE_CONSTRAINT, DROP_CONSTRAINT
31+
}
32+
33+
public enum ConstraintKind {
34+
UNIQUE, NOT_NULL, KEY
35+
}
36+
37+
private final Kind kind;
38+
private final ConstraintKind constraintKind;
39+
private final String constraintName;
40+
private final String labelName;
41+
private final List<String> propertyNames;
42+
private final boolean ifNotExists;
43+
private final boolean ifExists;
44+
private final boolean forRelationship;
45+
46+
public CypherDDLStatement(final Kind kind, final ConstraintKind constraintKind, final String constraintName,
47+
final String labelName, final List<String> propertyNames, final boolean ifNotExists, final boolean ifExists,
48+
final boolean forRelationship) {
49+
this.kind = kind;
50+
this.constraintKind = constraintKind;
51+
this.constraintName = constraintName;
52+
this.labelName = labelName;
53+
this.propertyNames = propertyNames;
54+
this.ifNotExists = ifNotExists;
55+
this.ifExists = ifExists;
56+
this.forRelationship = forRelationship;
57+
}
58+
59+
public Kind getKind() {
60+
return kind;
61+
}
62+
63+
public ConstraintKind getConstraintKind() {
64+
return constraintKind;
65+
}
66+
67+
public String getConstraintName() {
68+
return constraintName;
69+
}
70+
71+
public String getLabelName() {
72+
return labelName;
73+
}
74+
75+
public List<String> getPropertyNames() {
76+
return propertyNames;
77+
}
78+
79+
public boolean isIfNotExists() {
80+
return ifNotExists;
81+
}
82+
83+
public boolean isIfExists() {
84+
return ifExists;
85+
}
86+
87+
public boolean isForRelationship() {
88+
return forRelationship;
89+
}
90+
91+
@Override
92+
public boolean isReadOnly() {
93+
return false;
94+
}
95+
96+
@Override
97+
public List<MatchClause> getMatchClauses() {
98+
return List.of();
99+
}
100+
101+
@Override
102+
public WhereClause getWhereClause() {
103+
return null;
104+
}
105+
106+
@Override
107+
public ReturnClause getReturnClause() {
108+
return null;
109+
}
110+
111+
@Override
112+
public boolean hasCreate() {
113+
return false;
114+
}
115+
116+
@Override
117+
public boolean hasMerge() {
118+
return false;
119+
}
120+
121+
@Override
122+
public boolean hasDelete() {
123+
return false;
124+
}
125+
126+
@Override
127+
public OrderByClause getOrderByClause() {
128+
return null;
129+
}
130+
131+
@Override
132+
public Expression getSkip() {
133+
return null;
134+
}
135+
136+
@Override
137+
public Expression getLimit() {
138+
return null;
139+
}
140+
141+
@Override
142+
public CreateClause getCreateClause() {
143+
return null;
144+
}
145+
146+
@Override
147+
public SetClause getSetClause() {
148+
return null;
149+
}
150+
151+
@Override
152+
public DeleteClause getDeleteClause() {
153+
return null;
154+
}
155+
156+
@Override
157+
public MergeClause getMergeClause() {
158+
return null;
159+
}
160+
161+
@Override
162+
public List<UnwindClause> getUnwindClauses() {
163+
return List.of();
164+
}
165+
166+
@Override
167+
public List<WithClause> getWithClauses() {
168+
return List.of();
169+
}
170+
}

engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherASTBuilder.java

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,107 @@ public class CypherASTBuilder extends Cypher25ParserBaseVisitor<Object> {
5757

5858
@Override
5959
public CypherStatement visitStatement(final Cypher25Parser.StatementContext ctx) {
60-
// For now, focus on queryWithLocalDefinitions (the most common case)
61-
if (ctx.queryWithLocalDefinitions() != null) {
60+
if (ctx.queryWithLocalDefinitions() != null)
6261
return (CypherStatement) visit(ctx.queryWithLocalDefinitions());
62+
if (ctx.command() != null)
63+
return handleCommand(ctx.command());
64+
throw new CommandParsingException("Unsupported statement type");
65+
}
66+
67+
private CypherDDLStatement handleCommand(final Cypher25Parser.CommandContext ctx) {
68+
if (ctx.createCommand() != null)
69+
return handleCreateCommand(ctx.createCommand());
70+
if (ctx.dropCommand() != null)
71+
return handleDropCommand(ctx.dropCommand());
72+
throw new CommandParsingException("Only constraint commands are currently supported");
73+
}
74+
75+
private CypherDDLStatement handleCreateCommand(final Cypher25Parser.CreateCommandContext ctx) {
76+
if (ctx.createConstraint() != null)
77+
return handleCreateConstraint(ctx.createConstraint());
78+
throw new CommandParsingException("Only CREATE CONSTRAINT is currently supported");
79+
}
80+
81+
private CypherDDLStatement handleDropCommand(final Cypher25Parser.DropCommandContext ctx) {
82+
if (ctx.dropConstraint() != null)
83+
return handleDropConstraint(ctx.dropConstraint());
84+
throw new CommandParsingException("Only DROP CONSTRAINT is currently supported");
85+
}
86+
87+
private CypherDDLStatement handleCreateConstraint(final Cypher25Parser.CreateConstraintContext ctx) {
88+
// Extract optional constraint name
89+
final String constraintName = ctx.symbolicNameOrStringParameter() != null
90+
? stripBackticks(ctx.symbolicNameOrStringParameter().getText()) : null;
91+
92+
// IF NOT EXISTS
93+
final boolean ifNotExists = ctx.IF() != null && ctx.NOT() != null && ctx.EXISTS() != null;
94+
95+
// Extract label name and determine if it's a node or relationship constraint
96+
final boolean forRelationship;
97+
final String labelName;
98+
if (ctx.commandNodePattern() != null) {
99+
forRelationship = false;
100+
labelName = stripBackticks(ctx.commandNodePattern().labelType().symbolicNameString().getText());
101+
} else if (ctx.commandRelPattern() != null) {
102+
forRelationship = true;
103+
labelName = stripBackticks(ctx.commandRelPattern().relType().symbolicNameString().getText());
104+
} else {
105+
throw new CommandParsingException("CREATE CONSTRAINT requires a node or relationship pattern");
106+
}
107+
108+
// Extract property names from constraintType
109+
final Cypher25Parser.ConstraintTypeContext constraintType = ctx.constraintType();
110+
final List<String> propertyNames = extractPropertyNames(constraintType);
111+
112+
// Determine constraint kind
113+
final CypherDDLStatement.ConstraintKind constraintKind;
114+
if (constraintType instanceof Cypher25Parser.ConstraintIsUniqueContext)
115+
constraintKind = CypherDDLStatement.ConstraintKind.UNIQUE;
116+
else if (constraintType instanceof Cypher25Parser.ConstraintIsNotNullContext)
117+
constraintKind = CypherDDLStatement.ConstraintKind.NOT_NULL;
118+
else if (constraintType instanceof Cypher25Parser.ConstraintKeyContext)
119+
constraintKind = CypherDDLStatement.ConstraintKind.KEY;
120+
else
121+
throw new CommandParsingException("Unsupported constraint type: " + constraintType.getText());
122+
123+
return new CypherDDLStatement(CypherDDLStatement.Kind.CREATE_CONSTRAINT, constraintKind,
124+
constraintName, labelName, propertyNames, ifNotExists, false, forRelationship);
125+
}
126+
127+
private CypherDDLStatement handleDropConstraint(final Cypher25Parser.DropConstraintContext ctx) {
128+
final String constraintName = stripBackticks(ctx.symbolicNameOrStringParameter().getText());
129+
final boolean ifExists = ctx.IF() != null && ctx.EXISTS() != null;
130+
return new CypherDDLStatement(CypherDDLStatement.Kind.DROP_CONSTRAINT, null,
131+
constraintName, null, null, false, ifExists, false);
132+
}
133+
134+
/**
135+
* Extracts property names from a constraintType context.
136+
* Handles both single property (p.id) and property list ((p.first, p.last)).
137+
*/
138+
private List<String> extractPropertyNames(final Cypher25Parser.ConstraintTypeContext ctx) {
139+
// propertyList() is defined on each specific subclass, not on the base ConstraintTypeContext
140+
final Cypher25Parser.PropertyListContext propList;
141+
if (ctx instanceof Cypher25Parser.ConstraintIsUniqueContext)
142+
propList = ((Cypher25Parser.ConstraintIsUniqueContext) ctx).propertyList();
143+
else if (ctx instanceof Cypher25Parser.ConstraintIsNotNullContext)
144+
propList = ((Cypher25Parser.ConstraintIsNotNullContext) ctx).propertyList();
145+
else if (ctx instanceof Cypher25Parser.ConstraintKeyContext)
146+
propList = ((Cypher25Parser.ConstraintKeyContext) ctx).propertyList();
147+
else
148+
throw new CommandParsingException("Unsupported constraint type for property extraction");
149+
150+
final List<String> names = new ArrayList<>();
151+
if (propList.enclosedPropertyList() != null) {
152+
// Parenthesized list: (p.first, p.last)
153+
final Cypher25Parser.EnclosedPropertyListContext enclosed = propList.enclosedPropertyList();
154+
for (final Cypher25Parser.PropertyContext prop : enclosed.property())
155+
names.add(stripBackticks(prop.propertyKeyName().getText()));
156+
} else {
157+
// Single property: p.id
158+
names.add(stripBackticks(propList.property().propertyKeyName().getText()));
63159
}
64-
throw new CommandParsingException("Command statements not yet supported");
160+
return names;
65161
}
66162

67163
@Override

engine/src/main/java/com/arcadedb/query/opencypher/query/OpenCypherQueryEngine.java

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@
2222
import com.arcadedb.database.DatabaseInternal;
2323
import com.arcadedb.exception.CommandExecutionException;
2424
import com.arcadedb.exception.CommandParsingException;
25+
import com.arcadedb.exception.SchemaException;
26+
import com.arcadedb.query.opencypher.ast.CypherDDLStatement;
2527
import com.arcadedb.query.opencypher.optimizer.plan.PhysicalPlan;
26-
import com.arcadedb.query.opencypher.parser.Cypher25AntlrParser;
2728
import com.arcadedb.query.opencypher.ast.CypherStatement;
2829
import com.arcadedb.query.opencypher.planner.CypherExecutionPlanner;
2930
import com.arcadedb.query.opencypher.executor.CypherExecutionPlan;
3031
import com.arcadedb.query.opencypher.executor.CypherFunctionFactory;
3132
import com.arcadedb.query.opencypher.executor.ExpressionEvaluator;
3233
import com.arcadedb.query.QueryEngine;
34+
import com.arcadedb.query.sql.executor.InternalResultSet;
3335
import com.arcadedb.query.sql.executor.ResultSet;
36+
import com.arcadedb.schema.Property;
37+
import com.arcadedb.schema.Schema;
3438
import com.arcadedb.function.sql.DefaultSQLFunctionFactory;
3539

3640
import java.util.HashMap;
@@ -73,8 +77,7 @@ public boolean isIdempotent() {
7377

7478
@Override
7579
public boolean isDDL() {
76-
// Cypher doesn't have DDL statements
77-
return false;
80+
return statement instanceof CypherDDLStatement;
7881
}
7982
};
8083
} catch (final Exception e) {
@@ -137,6 +140,11 @@ public ResultSet command(final String query, final ContextConfiguration configur
137140

138141
// Use statement cache to avoid re-parsing
139142
final CypherStatement statement = database.getCypherStatementCache().get(actualQuery);
143+
144+
// DDL statements (constraints) are executed directly without the planner pipeline
145+
if (statement instanceof CypherDDLStatement)
146+
return executeDDL((CypherDDLStatement) statement);
147+
140148
return execute(actualQuery, statement, configuration, parameters, explain, profile);
141149
} catch (final CommandExecutionException | CommandParsingException e) {
142150
throw e;
@@ -193,6 +201,71 @@ private ResultSet execute(final String queryString, final CypherStatement statem
193201
return plan.execute();
194202
}
195203

204+
/**
205+
* Executes a DDL statement (constraint creation/deletion) directly against the schema.
206+
*/
207+
private ResultSet executeDDL(final CypherDDLStatement ddl) {
208+
final Schema schema = database.getSchema();
209+
210+
switch (ddl.getKind()) {
211+
case CREATE_CONSTRAINT:
212+
executeCreateConstraint(ddl, schema);
213+
break;
214+
case DROP_CONSTRAINT:
215+
executeDropConstraint(ddl, schema);
216+
break;
217+
}
218+
219+
return new InternalResultSet();
220+
}
221+
222+
private void executeCreateConstraint(final CypherDDLStatement ddl, final Schema schema) {
223+
final String typeName = ddl.getLabelName();
224+
final String[] propertyNames = ddl.getPropertyNames().toArray(new String[0]);
225+
226+
// Ensure all properties exist before creating indexes (ArcadeDB requires this)
227+
for (final String propName : propertyNames) {
228+
if (schema.getType(typeName).getPropertyIfExists(propName) == null)
229+
schema.getType(typeName).createProperty(propName, com.arcadedb.schema.Type.STRING);
230+
}
231+
232+
switch (ddl.getConstraintKind()) {
233+
case UNIQUE:
234+
schema.buildTypeIndex(typeName, propertyNames)
235+
.withType(Schema.INDEX_TYPE.LSM_TREE)
236+
.withUnique(true)
237+
.withIgnoreIfExists(ddl.isIfNotExists())
238+
.create();
239+
break;
240+
241+
case NOT_NULL:
242+
for (final String propName : propertyNames)
243+
schema.getType(typeName).getPropertyIfExists(propName).setMandatory(true);
244+
break;
245+
246+
case KEY:
247+
// NODE KEY = unique index + mandatory properties
248+
schema.buildTypeIndex(typeName, propertyNames)
249+
.withType(Schema.INDEX_TYPE.LSM_TREE)
250+
.withUnique(true)
251+
.withIgnoreIfExists(ddl.isIfNotExists())
252+
.create();
253+
for (final String propName : propertyNames)
254+
schema.getType(typeName).getPropertyIfExists(propName).setMandatory(true);
255+
break;
256+
}
257+
}
258+
259+
private void executeDropConstraint(final CypherDDLStatement ddl, final Schema schema) {
260+
final String constraintName = ddl.getConstraintName();
261+
if (ddl.isIfExists()) {
262+
if (schema.existsIndex(constraintName))
263+
schema.dropIndex(constraintName);
264+
} else {
265+
schema.dropIndex(constraintName);
266+
}
267+
}
268+
196269
/**
197270
* Get the shared ExpressionEvaluator instance.
198271
* This is used by other components that need access to the evaluator.

0 commit comments

Comments
 (0)