Triggers

Available from version 26.2.1

ArcadeDB supports database triggers that automatically execute SQL statements, JavaScript code, or Java classes in response to record events. Triggers enable you to implement business logic, data validation, audit trails, and automated workflows directly in your database.

Overview

A trigger is a named database object that automatically executes when specific events occur on records of a particular type. Triggers can execute SQL statements, JavaScript code, or Java classes, giving you flexibility in how you implement your logic.

Key Features

  • Event-driven: Triggers fire automatically on CREATE, READ, UPDATE, or DELETE operations

  • Timing control: Execute BEFORE or AFTER the event

  • Multi-language: Choose between SQL statements, JavaScript code, or Java classes

  • High performance: Java class triggers offer maximum performance with compiled code

  • Persistent: Triggers are stored in the schema and survive database restarts

  • Type-specific: Each trigger applies to a specific document/vertex/edge type

Trigger Events

Triggers can respond to eight different combinations of timing and events:

Event Description

BEFORE CREATE

Before a new record is created

AFTER CREATE

After a new record is created

BEFORE READ

Before a record is read from database

AFTER READ

After a record is read from database

BEFORE UPDATE

Before a record is modified

AFTER UPDATE

After a record is modified

BEFORE DELETE

Before a record is deleted

AFTER DELETE

After a record is deleted

SQL Triggers

SQL triggers execute SQL statements. They have access to the current record through context variables.

Context Variables

  • $record or record - The current record being operated on

Example: Audit Trail

Create an audit log that tracks all user creations:

-- Create the audit log type
CREATE DOCUMENT TYPE AuditLog;

-- Create trigger to log user creations
CREATE TRIGGER user_audit AFTER CREATE ON TYPE User
EXECUTE SQL 'INSERT INTO AuditLog SET action = "user_created",
             userName = $record.name,
             timestamp = sysdate()';

Now every time a User is created, an entry is automatically added to the AuditLog:

-- Create a user
INSERT INTO User SET name = 'Alice', email = '[email protected]';

-- Check the audit log
SELECT * FROM AuditLog;
-- Returns: {action: "user_created", userName: "Alice", timestamp: ...}

Example: Auto-increment Counter

Automatically set a sequence number on new documents:

-- Create a counter type
CREATE DOCUMENT TYPE Counter;
INSERT INTO Counter SET name = 'order_sequence', value = 1000;

-- Create trigger to auto-increment order numbers
CREATE TRIGGER order_number BEFORE CREATE ON TYPE Order
EXECUTE SQL 'UPDATE Counter SET value = value + 1
             WHERE name = "order_sequence";
             UPDATE $record SET orderNumber =
             (SELECT value FROM Counter WHERE name = "order_sequence")';

Example: Cascade Updates

Update related records when the parent changes:

-- Update all orders when customer email changes
CREATE TRIGGER customer_email_update AFTER UPDATE ON TYPE Customer
EXECUTE SQL 'UPDATE Order SET customerEmail = $record.email
             WHERE customerId = $record.@rid';

Example: Data Validation

Enforce business rules using BEFORE triggers:

-- Ensure product prices are positive
CREATE TRIGGER validate_price BEFORE CREATE ON TYPE Product
EXECUTE SQL 'SELECT FROM Product WHERE @this = $record AND price > 0';
-- If this SELECT statement returns no results, the trigger fails and the operation is aborted.

JavaScript Triggers

JavaScript triggers offer more flexibility and can implement complex logic with conditional statements, loops, and calculations.

Context Variables

  • record or $record - The current record being operated on

  • oldRecord or $oldRecord - The original record (for UPDATE operations), null otherwise

  • database - The database instance

Return Value

JavaScript triggers can return false to abort the operation (for BEFORE triggers only).

Example: Data Validation

Validate email format before creating users:

CREATE TRIGGER validate_email BEFORE CREATE ON TYPE User
EXECUTE JAVASCRIPT 'if (!record.email || !record.email.includes("@")) {
  throw new Error("Invalid email address");
}';

Example: Auto-populate Fields

Automatically set timestamps and computed fields:

CREATE TRIGGER user_defaults BEFORE CREATE ON TYPE User
EXECUTE JAVASCRIPT '
  // Set creation timestamp
  record.createdAt = new Date();

  // Generate username from email if not provided
  if (!record.username && record.email) {
    record.username = record.email.split("@")[0];
  }

  // Set default role
  if (!record.role) {
    record.role = "user";
  }
';

Example: Complex Business Logic

Implement discount rules based on order total:

CREATE TRIGGER calculate_discount BEFORE CREATE ON TYPE Order
EXECUTE JAVASCRIPT '
  var total = record.total || 0;
  var discount = 0;

  // Apply discount based on order total
  if (total > 1000) {
    discount = 0.15;  // 15% discount
  } else if (total > 500) {
    discount = 0.10;  // 10% discount
  } else if (total > 100) {
    discount = 0.05;  // 5% discount
  }

  record.discountPercent = discount * 100;
  record.discountAmount = total * discount;
  record.finalTotal = total - (total * discount);
';

Example: Conditional Abort

Prevent operations based on business rules:

CREATE TRIGGER prevent_weekend_orders BEFORE CREATE ON TYPE Order
EXECUTE JAVASCRIPT '
  var day = new Date().getDay();
  if (day === 0 || day === 6) {
    throw new Error("Orders cannot be placed on weekends");
  }
';

Example: Audit with Details

Create detailed audit logs with JavaScript:

CREATE TRIGGER audit_update AFTER UPDATE ON TYPE Product
EXECUTE JAVASCRIPT '
  database.command("sql",
    "INSERT INTO AuditLog SET action = ?, productId = ?, productName = ?, timestamp = sysdate()",
    "product_updated",
    record["@rid"],
    record.name
  );
';

Java Triggers

Java triggers offer maximum performance by executing compiled Java code. They require implementing the JavaTrigger interface and must be available in the classpath.

Creating a Java Trigger Class

First, create a Java class that implements the com.arcadedb.schema.trigger.JavaTrigger interface:

package com.example.triggers;

import com.arcadedb.database.Database;
import com.arcadedb.database.Document;
import com.arcadedb.database.Record;
import com.arcadedb.schema.trigger.JavaTrigger;

public class EmailValidationTrigger implements JavaTrigger {

  @Override
  public boolean execute(Database database, Record record, Record oldRecord) throws Exception {
    if (record instanceof Document) {
      Document doc = (Document) record;
      String email = doc.getString("email");

      if (email == null || !email.contains("@")) {
        throw new IllegalArgumentException("Invalid email address");
      }
    }
    return true;  // Continue with the operation
  }
}

JavaTrigger Interface

public interface JavaTrigger {
  /**
   * Execute the trigger logic.
   *
   * @param database  The database instance
   * @param record    The current record being operated on
   * @param oldRecord The original record (for UPDATE operations), null otherwise
   * @return true to continue the operation, false to abort (BEFORE triggers only)
   * @throws Exception to abort the operation with an error message
   */
  boolean execute(Database database, Record record, Record oldRecord) throws Exception;
}

Registering Java Triggers

Once your class is compiled and in the classpath, register it using SQL:

CREATE TRIGGER validate_email BEFORE CREATE ON TYPE User
EXECUTE JAVA 'com.example.triggers.EmailValidationTrigger';

Example: Data Validation

Validate complex business rules with full Java capabilities:

package com.example.triggers;

import com.arcadedb.database.Database;
import com.arcadedb.database.Document;
import com.arcadedb.database.Record;
import com.arcadedb.schema.trigger.JavaTrigger;
import java.math.BigDecimal;

public class PriceValidationTrigger implements JavaTrigger {

  private static final BigDecimal MIN_PRICE = new BigDecimal("0.01");
  private static final BigDecimal MAX_PRICE = new BigDecimal("999999.99");

  @Override
  public boolean execute(Database database, Record record, Record oldRecord) throws Exception {
    if (record instanceof Document) {
      Document doc = (Document) record;
      BigDecimal price = doc.get("price");

      if (price == null) {
        throw new IllegalArgumentException("Price is required");
      }

      if (price.compareTo(MIN_PRICE) < 0) {
        throw new IllegalArgumentException("Price must be at least " + MIN_PRICE);
      }

      if (price.compareTo(MAX_PRICE) > 0) {
        throw new IllegalArgumentException("Price cannot exceed " + MAX_PRICE);
      }
    }
    return true;
  }
}

Example: Database Queries

Execute queries within the trigger:

package com.example.triggers;

import com.arcadedb.database.Database;
import com.arcadedb.database.Document;
import com.arcadedb.database.Record;
import com.arcadedb.query.sql.executor.ResultSet;
import com.arcadedb.schema.trigger.JavaTrigger;

public class StockCheckTrigger implements JavaTrigger {

  @Override
  public boolean execute(Database database, Record record, Record oldRecord) throws Exception {
    if (record instanceof Document) {
      Document doc = (Document) record;
      String productId = doc.getString("productId");
      Integer quantity = doc.getInteger("quantity");

      ResultSet result = database.query("sql",
          "SELECT stock FROM Product WHERE @rid = ?", productId);

      if (result.hasNext()) {
        Document product = result.next().toElement().asDocument();
        Integer stock = product.getInteger("stock");

        if (stock < quantity) {
          throw new IllegalStateException(
              "Insufficient stock. Available: " + stock + ", Requested: " + quantity);
        }
      }
    }
    return true;
  }
}

Java Trigger Advantages

  1. Performance: Compiled code executes faster than interpreted JavaScript

  2. Type Safety: Compile-time type checking prevents runtime errors

  3. Full Java Ecosystem: Access to all Java libraries and frameworks

  4. IDE Support: Code completion, refactoring, and debugging

  5. Testability: Unit test your triggers like any Java class

  6. Reusability: Share trigger code across projects

Java Trigger Considerations

  1. Classpath: Trigger classes must be in the ArcadeDB classpath at runtime

  2. Deployment: Requires redeployment when trigger logic changes

  3. Error Handling: Exceptions abort the operation and rollback the transaction

  4. Thread Safety: Trigger instances may be reused across threads; ensure thread-safety

  5. No State: Avoid instance variables; triggers should be stateless

Use Cases

1. Audit Trails

Track who changed what and when:

CREATE TRIGGER audit_all AFTER UPDATE ON TYPE ImportantData
EXECUTE SQL 'INSERT INTO AuditLog SET
             tableName = "ImportantData",
             recordId = $record.@rid,
             modifiedAt = sysdate()';

2. Data Integrity

Ensure referential integrity and business rules:

CREATE TRIGGER check_inventory BEFORE CREATE ON TYPE OrderItem
EXECUTE JAVASCRIPT '
  var result = database.query("sql",
    "SELECT stock FROM Product WHERE @rid = ?",
    record.productId
  );

  if (result.hasNext()) {
    var product = result.next();
    if (product.stock < record.quantity) {
      throw new Error("Insufficient stock");
    }
  }
';

3. Denormalization

Maintain computed or cached values:

CREATE TRIGGER update_order_total AFTER CREATE ON TYPE OrderItem
EXECUTE SQL 'UPDATE Order SET
             totalAmount = (SELECT sum(price * quantity) FROM OrderItem
                           WHERE orderId = $record.orderId)
             WHERE @rid = $record.orderId';

4. Notifications

Send alerts or trigger external processes:

CREATE TRIGGER low_stock_alert AFTER UPDATE ON TYPE Product
EXECUTE JAVASCRIPT '
  if (record.stock < 10) {
    database.command("sql",
      "INSERT INTO Notification SET type = ?, productId = ?, message = ?",
      "low_stock",
      record["@rid"],
      "Product stock low: " + record.stock
    );
  }
';

5. Workflow Automation

Automatically progress through workflow states:

CREATE TRIGGER order_workflow AFTER UPDATE ON TYPE Order
EXECUTE JAVASCRIPT '
  // Auto-approve small orders
  if (record.status === "pending" && record.total < 100) {
    record.status = "approved";
    record.approvedAt = new Date();
  }
';

Performance Considerations

Benchmark Results

Performance tests measuring trigger execution overhead on document creation with identical operations (100,000 iterations, Java 21, macOS). All triggers perform the same operation: INSERT INTO AuditLog SET triggered = true.

Trigger Type Avg Time (µs) Overhead (µs) Overhead (%)

No Trigger (Baseline)

95

Java Trigger

147

+52

+54.7%

SQL Trigger

150

+55

+57.9%

JavaScript Trigger

187

+92

+96.8%

Key Findings:

  1. Java and SQL triggers have nearly identical performance: Only 2% difference (147 vs 150 µs), both execute compiled code paths efficiently.

  2. JavaScript triggers are ~27% slower: GraalVM JavaScript execution adds noticeable overhead compared to native execution.

  3. All triggers add overhead: Expect ~50-95% overhead depending on trigger type, which is acceptable for most use cases.

  4. Trigger overhead is predictable: The performance impact is consistent and can be factored into capacity planning.

Performance Recommendations

  • Minimize Work: Keep trigger code as lightweight as possible

  • Choose the Right Type:

    • Use Java triggers when you need type safety, IDE support, and debugging capabilities

    • Use SQL triggers for database operations - performance is nearly identical to Java

    • Use JavaScript triggers for dynamic logic where ~30% slower performance is acceptable

  • Avoid Complex Queries: Heavy queries in triggers can slow down operations

  • Consider Batch Operations: Triggers fire for each record, which can be expensive in bulk operations

  • Monitor Impact: Test performance with realistic data volumes

  • Profile Your Workload: Measure actual impact in your specific use case

Internal Optimizations

ArcadeDB optimizes JavaScript trigger performance through engine pooling:

  • Shared GraalVM Engine: All JavaScript triggers share a single GraalVM Polyglot Engine instance across the entire database process, reducing memory overhead and initialization time

  • Lightweight Contexts: Each trigger creates a lightweight execution context that reuses the shared engine

  • Lazy Initialization: Engine and context creation is deferred until the trigger first executes

  • Automatic Resource Management: Contexts are properly closed when triggers are removed, while the shared engine persists for the lifetime of the database process

This architecture ensures that creating multiple JavaScript triggers does not linearly increase memory consumption or initialization overhead.

When to Use Each Type

Java Triggers - Best for:

  • Complex validation requiring type safety and compile-time checks

  • Integration with existing Java libraries

  • Code that benefits from IDE support and refactoring

  • Unit testing requirements

  • Team prefers strongly-typed languages

SQL Triggers - Best for:

  • Simple database operations (audit logs, denormalization)

  • Prototyping and development (no compilation step)

  • Deployment simplicity (embedded in schema)

  • Operations that are primarily SQL-based

  • Performance-critical paths (nearly identical performance to Java)

JavaScript Triggers - Best for:

  • Moderate complexity business logic

  • Rapid development and iteration

  • Dynamic validation rules that change frequently

  • Scenarios where scripting flexibility outweighs performance

  • Teams comfortable with JavaScript

Best Practices

1. Keep Triggers Simple

Triggers execute synchronously and can impact performance. Keep logic simple and fast:

-- Good: Simple, fast operation
CREATE TRIGGER set_timestamp BEFORE CREATE ON TYPE Document
EXECUTE JAVASCRIPT 'record.createdAt = new Date();';

-- Avoid: Complex calculations that could be done elsewhere

2. Use Meaningful Names

Name triggers clearly to indicate their purpose:

-- Good naming
CREATE TRIGGER audit_user_creation AFTER CREATE ON TYPE User ...
CREATE TRIGGER validate_email BEFORE CREATE ON TYPE User ...

-- Poor naming
CREATE TRIGGER trigger1 AFTER CREATE ON TYPE User ...

3. Handle Errors Gracefully

In JavaScript triggers, throw descriptive errors:

CREATE TRIGGER validate_age BEFORE CREATE ON TYPE User
EXECUTE JAVASCRIPT '
  if (!record.age || record.age < 18) {
    throw new Error("User must be at least 18 years old");
  }
';

4. Avoid Infinite Loops

Be careful not to create circular trigger dependencies:

-- DANGER: This could create an infinite loop if not careful
-- Trigger A updates Type B, which triggers B updates Type A, etc.

5. Test Thoroughly

Test triggers with various scenarios:

  • Normal operations

  • Edge cases (null values, empty strings, etc.)

  • Error conditions

  • Performance with large datasets

Managing Triggers

List All Triggers

Use the Java API to list all triggers:

Trigger[] triggers = database.getSchema().getTriggers();
for (Trigger trigger : triggers) {
  System.out.println(trigger.getName() + " on " + trigger.getTypeName());
}

Check if Trigger Exists

boolean exists = database.getSchema().existsTrigger("trigger_name");

Get Triggers for a Type

Trigger[] triggers = database.getSchema().getTriggersForType("User");

Remove a Trigger

DROP TRIGGER user_audit;

Limitations

  1. Synchronous Execution: Triggers execute synchronously within the transaction. Long-running triggers can impact performance.

  2. Type Matching: Triggers match the exact type name. Polymorphic matching (inheriting triggers from parent types) is not currently supported.

  3. Order of Execution: When multiple triggers exist for the same event on the same type, they execute in alphabetical order by trigger name.

  4. BEFORE READ Limitation: BEFORE READ triggers receive only the RID and must load the record, which can cause a double-read.

  5. Transaction Context: Triggers execute within the same transaction as the triggering operation. If a trigger fails, the entire transaction rolls back.

  6. JavaScript Sandboxing: JavaScript triggers run in a sandboxed environment with limited access to Java packages for security.