Manage Database Triggers with Prisma using Atlas
Triggers are powerful tools in relational databases that allow you to execute custom code when specific events occur on a table. For example, triggers can automatically log changes to a table without adding extra code to your application. This guide explains how to incorporate triggers into your Prisma project using Atlas.
Atlas support for Triggers used in this guide is available exclusively to Pro users. To use this feature, run:
atlas login
Introduction
Prisma excels at modeling your database schema using its own schema language but doesn't natively support defining database triggers. Using Atlas, you can manage advanced database features like triggers alongside your Prisma models.
Configuring Prisma with Atlas for Database Triggers
To include both your Prisma models and custom triggers, we'll configure Atlas to combine both schemas using the
composite_schema
data source.
1. Define Your Prisma Models
Start with your existing Prisma models or create new ones. For this example, we'll use User
and UserAuditLog
:
// generator {...}
// datasource {...}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
model UserAuditLog {
id Int @id @default(autoincrement())
operationType String
operationTime DateTime @default(now())
oldValue Json?
newValue Json?
}
2. Create SQL File with Trigger Definitions
Create a file named triggers.sql
containing the trigger function and triggers.
- PostgreSQL
- MySQL
-- Function to audit changes in the User table.
CREATE OR REPLACE FUNCTION audit_user_changes()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD));
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Triggers for INSERT, UPDATE, and DELETE on the User table.
CREATE TRIGGER user_insert_audit AFTER INSERT ON "User" FOR EACH ROW EXECUTE FUNCTION audit_user_changes();
CREATE TRIGGER user_update_audit AFTER UPDATE ON "User" FOR EACH ROW EXECUTE FUNCTION audit_user_changes();
CREATE TRIGGER user_delete_audit AFTER DELETE ON "User" FOR EACH ROW EXECUTE FUNCTION audit_user_changes();
CREATE TRIGGER user_insert_audit AFTER
INSERT
ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, newValue)
VALUES
('INSERT', CURRENT_TIMESTAMP, JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
CREATE TRIGGER user_update_audit AFTER
UPDATE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue, newValue)
VALUES
('UPDATE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email), JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
CREATE TRIGGER user_delete_audit AFTER DELETE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue)
VALUES
('DELETE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email));
END;
3. Use composite_schema
to Combine Prisma and Trigger Definitions
Create a file named atlas.hcl and add the following configuration:
- PostgreSQL
- MySQL
data "external_schema" "prisma" {
program = [
"npx",
"prisma",
"migrate",
"diff",
"--from-empty",
"--to-schema-datamodel",
"prisma/schema.prisma",
"--script"
]
}
data "composite_schema" "prisma-extended" {
schema {
url = data.external_schema.prisma.url
}
schema {
url = "file://triggers.sql"
}
}
env "local" {
dev = "docker://postgres/15/dev"
schema {
src = data.composite_schema.prisma-extended.url
}
migration {
dir = "file://atlas/migrations"
}
}
data "external_schema" "prisma" {
program = [
"npx",
"prisma",
"migrate",
"diff",
"--from-empty",
"--to-schema-datamodel",
"prisma/schema.prisma",
"--script"
]
}
data "composite_schema" "prisma-extended" {
schema "public" {
url = data.external_schema.prisma.url
}
schema "public" {
url = "file://triggers.sql"
}
}
env "local" {
dev = "docker://mysql/8/dev"
schema {
src = data.composite_schema.prisma-extended.url
}
migration {
dir = "file://atlas/migrations"
}
}
In this file, we first define an external_schema
data source that loads the Prisma schema from the schema definition
file and then use the composite_schema
data source to combine the Prisma schema with the triggers defined in
triggers.sql
:
Usage
Verify our setup
To verify our setup is correct, we can inspect the schema using the atlas schema inspect
command to see how Atlas
combines the Prisma schema with the triggers:
atlas schema inspect --env local --url env://schema.src --format "{{ sql . }}"
Output:
- PostgreSQL
- MySQL
-- Add new schema named "public"
CREATE SCHEMA IF NOT EXISTS "public";
-- Set comment to schema: "public"
COMMENT ON SCHEMA "public" IS 'standard public schema';
-- Create extension "pgcrypto"
CREATE EXTENSION "pgcrypto" WITH SCHEMA "public" VERSION "1.3";
-- Create "User" table
CREATE TABLE "public"."User" ("id" serial NOT NULL, "name" text NOT NULL, "email" text NOT NULL, PRIMARY KEY ("id"));
-- Create index "User_email_key" to table: "User"
CREATE UNIQUE INDEX "User_email_key" ON "public"."User" ("email");
-- Create "audit_user_changes" function
CREATE FUNCTION "public"."audit_user_changes" () RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD));
RETURN OLD;
END IF;
RETURN NULL;
END;
$$;
-- Create trigger "user_delete_audit"
CREATE TRIGGER "user_delete_audit" AFTER DELETE ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-- Create trigger "user_insert_audit"
CREATE TRIGGER "user_insert_audit" AFTER INSERT ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-- Create trigger "user_update_audit"
CREATE TRIGGER "user_update_audit" AFTER UPDATE ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-- Create "UserAuditLog" table
CREATE TABLE "public"."UserAuditLog" ("id" serial NOT NULL, "operationType" text NOT NULL, "operationTime" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "oldValue" jsonb NULL, "newValue" jsonb NULL, PRIMARY KEY ("id"));
-- Create "User" table
CREATE TABLE `User` (`id` int NOT NULL AUTO_INCREMENT, `email` varchar(191) NOT NULL, `isVerified` bool NOT NULL DEFAULT 0, `createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create trigger "user_insert_audit"
CREATE TRIGGER `user_insert_audit` AFTER INSERT ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, newValue)
VALUES
('INSERT', CURRENT_TIMESTAMP, JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
-- Create trigger "user_update_audit"
CREATE TRIGGER `user_update_audit` AFTER UPDATE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue, newValue)
VALUES
('UPDATE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email), JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
-- Create trigger "user_delete_audit"
CREATE TRIGGER `user_delete_audit` AFTER DELETE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue)
VALUES
('DELETE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email));
END;
Generate Migration Files
Next, let's see how we can generate new migration files using the atlas migrate diff
command:
atlas migrate diff --env local
Output:
.
├── atlas
│ └── migrations
│ ├── 20241204122818.sql
│ └── atlas.sum
├── atlas.hcl
├── prisma
│ ├── migrations
│ │ ├── ...
│ │ └── migration.sql
│ └── schema.prisma
└── triggers.sql
Apply Migrations to the Database
- PostgreSQL
- MySQL
atlas migrate apply --env local --url "postgresql://root:pass@localhost:5432/postgres?sslmode=disable"
atlas migrate apply --env local --url "mysql://root:admin@127.0.0.1:3306/dev"
Output:
- PostgreSQL
- MySQL
Migrating to version 20241204122818 (1 migrations in total):
-- migrating version 20241204122818
-> CREATE EXTENSION "pgcrypto" WITH SCHEMA "public" VERSION "1.3";
-> CREATE TABLE "public"."User" ("id" serial NOT NULL, "name" text NOT NULL, "email" text NOT NULL, PRIMARY KEY ("id"));
-> CREATE UNIQUE INDEX "User_email_key" ON "public"."User" ("email");
-> CREATE FUNCTION "public"."audit_user_changes" () RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue, newValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO "UserAuditLog" (operationType, operationTime, oldValue)
VALUES (TG_OP, CURRENT_TIMESTAMP, row_to_json(OLD));
RETURN OLD;
END IF;
RETURN NULL;
END;
$$;
-> CREATE TRIGGER "user_delete_audit" AFTER DELETE ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-> CREATE TRIGGER "user_insert_audit" AFTER INSERT ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-> CREATE TRIGGER "user_update_audit" AFTER UPDATE ON "public"."User" FOR EACH ROW EXECUTE FUNCTION "public"."audit_user_changes"();
-> CREATE TABLE "public"."UserAuditLog" ("id" serial NOT NULL, "operationType" text NOT NULL, "operationTime" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "oldValue" jsonb NULL, "newValue" jsonb NULL, PRIMARY KEY ("id"));
-- ok (45.681333ms)
-------------------------
-- 90.735625ms
-- 1 migration
-- 8 sql statements
Migrating to version 20241209071240 (1 migrations in total):
-- migrating version 20241209071240
-> CREATE TABLE `User` (`id` int NOT NULL AUTO_INCREMENT, `email` varchar(191) NOT NULL, `isVerified` bool NOT NULL DEFAULT 0, `createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
-> CREATE TRIGGER `user_insert_audit` AFTER INSERT ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, newValue)
VALUES
('INSERT', CURRENT_TIMESTAMP, JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
-> CREATE TRIGGER `user_update_audit` AFTER UPDATE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue, newValue)
VALUES
('UPDATE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email), JSON_OBJECT('id', NEW.id, 'email', NEW.email));
END;
-> CREATE TRIGGER `user_delete_audit` AFTER DELETE ON `User` FOR EACH ROW BEGIN
INSERT INTO
`UserAuditLog` (operationType, operationTime, oldValue)
VALUES
('DELETE', CURRENT_TIMESTAMP, JSON_OBJECT('id', OLD.id, 'email', OLD.email));
END;
-- ok (56.252458ms)
-------------------------
-- 68.137333ms
-- 1 migration
-- 4 sql statements
Conclusion
By combining Prisma ORM with Atlas, you can effectively manage both your application's data models and version-controlled database triggers. This approach lets you leverage Prisma's ORM capabilities while maintaining advanced database features like triggers through Atlas's schema management.