Skip to content

Commit bc6cc4e

Browse files
Sql triggers executing SQL, Javascript and Java code (#3222)
* feat: added triggers via SQL executing SQL, JS and Java code Fixed issue #1395 * feat: added trigger benchmark * Update docs/SQL-Triggers.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update docs/SQL-Triggers.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * perf: using shared polyglot engine in triggers * Update docs/SQL-Triggers.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update docs/SQL-Triggers.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 4bed2ba commit bc6cc4e

31 files changed

+3439
-25
lines changed

docs/SQL-Triggers.md

Lines changed: 770 additions & 0 deletions
Large diffs are not rendered by default.

engine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ FUNCTION: F U N C T I O N;
202202
GLOBAL: G L O B A L;
203203
PARAMETERS: P A R A M E T E R S;
204204
LANGUAGE: L A N G U A G E;
205+
TRIGGER: T R I G G E R;
205206
FAIL: F A I L;
206207
FIX: F I X;
207208
SLEEP: S L E E P;

engine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLParser.g4

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ statement
9393
| CREATE BUCKET createBucketBody # createBucketStmt
9494
| CREATE VERTEX createVertexBody # createVertexStmt
9595
| CREATE EDGE createEdgeBody # createEdgeStmt
96+
| CREATE TRIGGER createTriggerBody # createTriggerStmt
9697

9798
// DDL Statements - ALTER variants
9899
| ALTER TYPE alterTypeBody # alterTypeStmt
@@ -105,6 +106,7 @@ statement
105106
| DROP PROPERTY dropPropertyBody # dropPropertyStmt
106107
| DROP INDEX dropIndexBody # dropIndexStmt
107108
| DROP BUCKET dropBucketBody # dropBucketStmt
109+
| DROP TRIGGER dropTriggerBody # dropTriggerStmt
108110

109111
// DDL Statements - TRUNCATE variants
110112
| TRUNCATE TYPE truncateTypeBody # truncateTypeStmt
@@ -586,6 +588,46 @@ dropBucketBody
586588
: identifier (IF EXISTS)?
587589
;
588590

591+
// ============================================================================
592+
// TRIGGER MANAGEMENT
593+
// ============================================================================
594+
595+
/**
596+
* CREATE TRIGGER statement
597+
* Syntax: CREATE TRIGGER [IF NOT EXISTS] name (BEFORE|AFTER) (CREATE|READ|UPDATE|DELETE)
598+
* ON [TYPE] typeName (EXECUTE SQL 'statement' | EXECUTE JAVASCRIPT 'code' | EXECUTE JAVA 'className')
599+
*/
600+
createTriggerBody
601+
: (IF NOT EXISTS)? identifier
602+
triggerTiming triggerEvent
603+
ON TYPE? identifier
604+
triggerAction
605+
;
606+
607+
triggerTiming
608+
: BEFORE
609+
| AFTER
610+
;
611+
612+
triggerEvent
613+
: CREATE
614+
| READ
615+
| UPDATE
616+
| DELETE
617+
;
618+
619+
triggerAction
620+
: EXECUTE identifier STRING_LITERAL
621+
;
622+
623+
/**
624+
* DROP TRIGGER statement
625+
* Syntax: DROP TRIGGER [IF EXISTS] name
626+
*/
627+
dropTriggerBody
628+
: (IF EXISTS)? identifier
629+
;
630+
589631
// ============================================================================
590632
// DDL STATEMENTS - TRUNCATE
591633
// ============================================================================

engine/src/main/java/com/arcadedb/function/polyglot/PolyglotFunctionLibraryDefinition.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import com.arcadedb.database.Database;
2121
import com.arcadedb.function.FunctionLibraryDefinition;
2222
import com.arcadedb.query.polyglot.GraalPolyglotEngine;
23-
import org.graalvm.polyglot.Engine;
23+
import com.arcadedb.query.polyglot.PolyglotEngineManager;
2424

2525
import java.util.*;
2626
import java.util.concurrent.*;
@@ -42,7 +42,8 @@ protected PolyglotFunctionLibraryDefinition(final Database database, final Strin
4242
this.libraryName = libraryName;
4343
this.language = language;
4444
this.allowedPackages = allowedPackages;
45-
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, Engine.create()).setLanguage(language).setAllowedPackages(allowedPackages).build();
45+
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, PolyglotEngineManager.getInstance().getSharedEngine())
46+
.setLanguage(language).setAllowedPackages(allowedPackages).build();
4647
}
4748

4849
public PolyglotFunctionLibraryDefinition registerFunction(final T function) {
@@ -51,7 +52,8 @@ public PolyglotFunctionLibraryDefinition registerFunction(final T function) {
5152

5253
// REGISTER ALL THE FUNCTIONS UNDER THE NEW ENGINE INSTANCE
5354
this.polyglotEngine.close();
54-
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, Engine.create()).setLanguage(language).setAllowedPackages(allowedPackages).build();
55+
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, PolyglotEngineManager.getInstance().getSharedEngine())
56+
.setLanguage(language).setAllowedPackages(allowedPackages).build();
5557
for (final T f : functions.values())
5658
f.init(this);
5759

engine/src/main/java/com/arcadedb/query/polyglot/GraalPolyglotEngine.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public class GraalPolyglotEngine implements AutoCloseable {
4949

5050
static {
5151
try {
52-
supportedLanguages = Engine.create().getLanguages().keySet();
52+
// Use the shared engine to discover supported languages
53+
supportedLanguages = PolyglotEngineManager.getInstance().getSharedEngine().getLanguages().keySet();
5354
} catch (Throwable e) {
5455
LogManager.instance().log(GraalPolyglotEngine.class, Level.SEVERE, "GraalVM Polyglot Engine: no languages found");
5556
supportedLanguages = Collections.emptySet();
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd ([email protected])
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 ([email protected])
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.query.polyglot;
20+
21+
import com.arcadedb.log.LogManager;
22+
import org.graalvm.polyglot.Engine;
23+
24+
import java.util.logging.Level;
25+
26+
/**
27+
* Singleton manager for GraalVM Polyglot Engine instances.
28+
* The Engine is a heavyweight object that should be shared across multiple Context instances
29+
* for optimal performance. This manager provides a single shared Engine instance for the entire
30+
* ArcadeDB process.
31+
*
32+
* @author Luca Garulli ([email protected])
33+
*/
34+
public class PolyglotEngineManager {
35+
private static final PolyglotEngineManager INSTANCE = new PolyglotEngineManager();
36+
private volatile Engine sharedEngine;
37+
38+
private PolyglotEngineManager() {
39+
// Private constructor for singleton
40+
}
41+
42+
public static PolyglotEngineManager getInstance() {
43+
return INSTANCE;
44+
}
45+
46+
/**
47+
* Returns the shared GraalVM Engine instance. Creates it on first access (lazy initialization).
48+
* The Engine is thread-safe and can be used concurrently by multiple contexts.
49+
*
50+
* @return the shared Engine instance
51+
*/
52+
public Engine getSharedEngine() {
53+
if (sharedEngine == null) {
54+
synchronized (this) {
55+
if (sharedEngine == null) {
56+
LogManager.instance()
57+
.log(this, Level.FINE, "Creating shared GraalVM Polyglot Engine for ArcadeDB process");
58+
sharedEngine = Engine.create();
59+
}
60+
}
61+
}
62+
return sharedEngine;
63+
}
64+
65+
/**
66+
* Closes the shared Engine. This should only be called during application shutdown.
67+
* After calling this method, subsequent calls to getSharedEngine() will create a new Engine.
68+
*/
69+
public synchronized void closeSharedEngine() {
70+
if (sharedEngine != null) {
71+
try {
72+
LogManager.instance().log(this, Level.FINE, "Closing shared GraalVM Polyglot Engine");
73+
sharedEngine.close();
74+
} catch (final Exception e) {
75+
LogManager.instance().log(this, Level.WARNING, "Error closing shared GraalVM Polyglot Engine", e);
76+
} finally {
77+
sharedEngine = null;
78+
}
79+
}
80+
}
81+
}

engine/src/main/java/com/arcadedb/query/polyglot/PolyglotQueryEngine.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import com.arcadedb.query.sql.executor.InternalResultSet;
3030
import com.arcadedb.query.sql.executor.ResultInternal;
3131
import com.arcadedb.query.sql.executor.ResultSet;
32-
import org.graalvm.polyglot.Engine;
3332
import org.graalvm.polyglot.Value;
3433

3534
import java.util.*;
@@ -88,8 +87,8 @@ protected PolyglotQueryEngine(final DatabaseInternal database, final String lang
8887
this.language = language;
8988
this.database = database;
9089
this.allowedPackages = allowedPackages;
91-
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, Engine.create()).setLanguage(language)
92-
.setAllowedPackages(allowedPackages).build();
90+
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, PolyglotEngineManager.getInstance().getSharedEngine())
91+
.setLanguage(language).setAllowedPackages(allowedPackages).build();
9392
this.userCodeExecutorQueue = new ArrayBlockingQueue<>(10000);
9493
this.userCodeExecutor = new ThreadPoolExecutor(8, 8, 30, TimeUnit.SECONDS, userCodeExecutorQueue,
9594
new ThreadPoolExecutor.CallerRunsPolicy());
@@ -174,8 +173,8 @@ public QueryEngine registerFunctions(final String function) {
174173

175174
@Override
176175
public QueryEngine unregisterFunctions() {
177-
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, Engine.create()).setLanguage(language)
178-
.setAllowedPackages(allowedPackages).build();
176+
this.polyglotEngine = GraalPolyglotEngine.newBuilder(database, PolyglotEngineManager.getInstance().getSharedEngine())
177+
.setLanguage(language).setAllowedPackages(allowedPackages).build();
179178
return this;
180179
}
181180

engine/src/main/java/com/arcadedb/query/sql/antlr/SQLASTBuilder.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5595,6 +5595,90 @@ public DropBucketStatement visitDropBucketStmt(final SQLParser.DropBucketStmtCon
55955595
return stmt;
55965596
}
55975597

5598+
// TRIGGER MANAGEMENT
5599+
5600+
/**
5601+
* Visit CREATE TRIGGER statement.
5602+
*/
5603+
@Override
5604+
public CreateTriggerStatement visitCreateTriggerStmt(final SQLParser.CreateTriggerStmtContext ctx) {
5605+
final CreateTriggerStatement stmt = new CreateTriggerStatement(-1);
5606+
final SQLParser.CreateTriggerBodyContext bodyCtx = ctx.createTriggerBody();
5607+
5608+
// IF NOT EXISTS flag
5609+
stmt.ifNotExists = bodyCtx.IF() != null && bodyCtx.NOT() != null && bodyCtx.EXISTS() != null;
5610+
5611+
// Trigger name (first identifier)
5612+
stmt.name = (Identifier) visit(bodyCtx.identifier(0));
5613+
5614+
// Trigger timing (BEFORE or AFTER)
5615+
stmt.timing = (Identifier) visit(bodyCtx.triggerTiming());
5616+
5617+
// Trigger event (CREATE, READ, UPDATE, DELETE)
5618+
stmt.event = (Identifier) visit(bodyCtx.triggerEvent());
5619+
5620+
// Type name (second identifier - the one after ON)
5621+
stmt.typeName = (Identifier) visit(bodyCtx.identifier(1));
5622+
5623+
// Action type and code (SQL or JAVASCRIPT)
5624+
final SQLParser.TriggerActionContext actionCtx = bodyCtx.triggerAction();
5625+
final Identifier actionTypeId = (Identifier) visit(actionCtx.identifier());
5626+
stmt.actionType = actionTypeId;
5627+
5628+
// Extract string literal and remove quotes
5629+
final String rawText = actionCtx.STRING_LITERAL().getText();
5630+
stmt.actionCode = rawText.substring(1, rawText.length() - 1);
5631+
5632+
return stmt;
5633+
}
5634+
5635+
/**
5636+
* Visit DROP TRIGGER statement.
5637+
*/
5638+
@Override
5639+
public DropTriggerStatement visitDropTriggerStmt(final SQLParser.DropTriggerStmtContext ctx) {
5640+
final DropTriggerStatement stmt = new DropTriggerStatement(-1);
5641+
final SQLParser.DropTriggerBodyContext bodyCtx = ctx.dropTriggerBody();
5642+
5643+
// Trigger name
5644+
stmt.name = (Identifier) visit(bodyCtx.identifier());
5645+
5646+
// IF EXISTS
5647+
stmt.ifExists = bodyCtx.IF() != null && bodyCtx.EXISTS() != null;
5648+
5649+
return stmt;
5650+
}
5651+
5652+
/**
5653+
* Visit trigger timing (BEFORE or AFTER).
5654+
*/
5655+
@Override
5656+
public Identifier visitTriggerTiming(final SQLParser.TriggerTimingContext ctx) {
5657+
if (ctx.BEFORE() != null) {
5658+
return new Identifier("BEFORE");
5659+
} else if (ctx.AFTER() != null) {
5660+
return new Identifier("AFTER");
5661+
}
5662+
return null;
5663+
}
5664+
5665+
/**
5666+
* Visit trigger event (CREATE, READ, UPDATE, DELETE).
5667+
*/
5668+
@Override
5669+
public Identifier visitTriggerEvent(final SQLParser.TriggerEventContext ctx) {
5670+
if (ctx.CREATE() != null) {
5671+
return new Identifier("CREATE");
5672+
} else if (ctx.READ() != null) {
5673+
return new Identifier("READ");
5674+
} else if (ctx.UPDATE() != null) {
5675+
return new Identifier("UPDATE");
5676+
} else if (ctx.DELETE() != null) {
5677+
return new Identifier("DELETE");
5678+
}
5679+
return null;
5680+
}
5681+
55985682
// DDL STATEMENTS - TRUNCATE
55995683

56005684
/**

0 commit comments

Comments
 (0)