Getting Started with Dolt and Prisma

INTEGRATION
13 min read

Dolt is the world's first version controlled SQL database, offering full MySQL compatibility. This means that all tools you typically use with MySQL, including Prisma, work seamlessly with Dolt. Prisma ORM is an open-source toolkit designed to enhance database interactions in Node.js and TypeScript applications. It features three key components: Prisma Client, an auto-generated, type-safe query builder; Prisma Migrate, for managing database schema changes; and Prisma Studio, a GUI for database management.

Due to a few Prisma-related issues reported by our users on GitHub, we have compiled this guide to demonstrate how to effectively use Prisma with Dolt.

Dolt + Prisma.js

Quick Start

If you prefer to skip the tutorial and directly access the code, it is available in this GitHub repository. Simply connect to any Dolt SQL server by specifying the connection details in a .env file and place it at the root of the repository. You can find directions for running the code in the repository README.

To execute the code, ensure that Git and Node.js are installed on your system. While this example connects to a Hosted Dolt database, the code is compatible with both hosted and local Dolt SQL servers. If you want to use a local Dolt SQL server, you'll need to install Dolt.

Project Initialization

Start by creating a new directory for your project and initialize it:

mkdir my-prisma-dolt-project
cd my-prisma-dolt-project
yarn init -y  # Initialize a new package.json file

Install TypeScript and ts-node:

yarn add typescript ts-node @types/node --dev

Create a tsconfig.json file for TypeScript configuration:

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}

Install Prisma CLI and Client:

yarn add @prisma/client
yarn add prisma --dev

You can now use the Prisma CLI to create a basic Prisma setup by running:

npx prisma init

Connect to Hosted Database

After running the command npx prisma init, your project will include a new /prisma directory. Inside this directory, you'll find a schema.prisma file, which serves as the main configuration file for your Prisma setup and contains definitions for your database schema. Additionally, a .env file will be created at the root of your project where you can set environment variables, such as the database connection URL and SSL information.

Create a Hosted Dolt deployment

This guide demonstrates how to connect to a Dolt database that is cloud-hosted using our Hosted Dolt product.

First, sign in or create an account and go to the Create Deployment page. Select the desired instance type. For this example, we'll use the $50 trial instance. Then, click Create Deployment.

Create deployment

Once the deployment has started, you'll find the connectivity information in the Database tab.

Prisma example connectivity information

Create a Database

In the Hosted Dolt UI, navigate to the Settings tab. Enable workbench writes by checking the Enable workbench writes option. Then click Create database.

Create database

Set Up Environment Variables

Open the .env file, add the database connectivity information and the database name.

DB_HOST="dolthub-prisma-schema-migration-test.dbs.hosted.doltdb.com"
DB_NAME="schema-migration-test"
DB_SSL_PATH="../hosted_ca.pem"
DB_USER="c2bkv70lke08xapa"
DB_PASSWORD="xxxxxxxxxxxxxxxxxxx"
DB_URL="mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}?sslmode=require&sslcert=${DB_SSL_PATH}"

You will also need to download the Certificate and add it to the root of the repository. The file name should match the DB_SSL_PATH env. Note that certificate paths are resolved relative to the ./prisma folder.

Create your database schema with Prisma

Open the /prisma/schema.prisma file and add "mysql" to the datasource field since Dolt is MySQL-compatible and load the database url from the env. In the generator block, we're specifying that we want to generate Prisma Client based on our data models.

// prisma/schema.prisma

datasource db {
  provider = "mysql"
  url      = env("DB_URL")
}

generator client {
  provider = "prisma-client-js"
}

Defining the models

Our database will have the following entities, where each entity will map to a table in the database.

  • employees, a person with an employee number, last name and first name, birth date, gender and hire date.
  • departments, a department has a department number and name.
  • dept_emp, represents the relationship between employees and departments, stores the employee number, department number, from date, and to date.

Let's create the employees, departments and dept_emp models in schema.prisma file. For more details on defining models, refer to the Prisma documentation.

model Employee {
  emp_no     Int       @id
  birth_date DateTime
  first_name String    @db.VarChar(14)
  last_name  String    @db.VarChar(16)
  hire_date  DateTime
  dept_emps  DeptEmp[]

  @@map("employees")
}

model Department {
  dept_no   String   @id @db.Char(4)
  dept_name String   @db.VarChar(40) @unique
  dept_emps DeptEmp[]

  @@map("departments")
}

model DeptEmp {
  emp_no     Int
  dept_no    String     @db.Char(4)
  from_date  DateTime
  to_date    DateTime
  employee   Employee   @relation(fields: [emp_no], references: [emp_no], onDelete: Cascade)
  department Department @relation(fields: [dept_no], references: [dept_no], onDelete: Cascade)

  @@id([emp_no, dept_no])
  @@map("dept_emp")
}

Migrating and pushing changes to the database

To create these tables in the database, you will use the prisma migrate dev command:

npx prisma migrate dev --name init

Then, inside the /prisma directory, you will notice a new folder called migrations. It should also contain another folder that ends with init and contains a file called migration.sql. The migration.sql file contains the generated SQL.

Now the employees, departments, and dept_emp tables have been created. One of the many benefits of using Hosted Dolt is that it comes with a SQL Workbench where you can view your data and changes across revisions. When you click on Launch SQL Workbench from the top right of your deployment page, you'll see the tables we just created, as well as an ER diagram that illustrates table relations.

Hosted ER Diagram

You'll notice an additional table _prisma_migrations is created. This _prisma_migrations table has a record for each applied migration, which includes information of migration history. This information is used by Prisma to determine which migrations have already been applied to the database, and which ones still need to be applied.

Seeding the database

Since the database is currently empty, we want to populate it with data. You'll use Prisma Client, a type-safe query builder to interact with your database and populate it with sample data.

Create a new file called seed.ts in the /prisma folder. Inside this file, import Prisma Client, instantiate it, and we will import data from CSV files:

import { PrismaClient } from "@prisma/client";
import csv from "csv-parser";
import fs from "fs";

const prisma = new PrismaClient();

async function processCSV(
  filePath: string,
  processData: (data: any[]) => Promise<void>
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    const results: any[] = [];
    fs.createReadStream(filePath)
      .pipe(csv())
      .on("data", (data) => results.push(data))
      .on("end", async () => {
        try {
          await processData(results);
          resolve();
        } catch (error) {
          reject(error);
        }
      });
  });
}

async function main() {
  try {
    await processCSV("./employees.csv", async (data) => {
      for (const emp of data) {
        await prisma.employee.create({
          data: {
            emp_no: parseInt(emp.emp_no),
            last_name: emp.last_name,
            first_name: emp.first_name,
            birth_date: new Date(emp.birth_date),
            hire_date: new Date(emp.hire_date),
          },
        });
      }
      console.log("Employees have been inserted");
    });

    await processCSV("./departments.csv", async (data) => {
      for (const dept of data) {
        await prisma.department.create({
          data: {
            dept_no: dept.dept_no,
            dept_name: dept.dept_name,
          },
        });
      }
      console.log("Departments have been inserted");
    });

    await processCSV("./dept_emp.csv", async (data) => {
      for (const deptEmp of data) {
        await prisma.deptEmp.create({
          data: {
            emp_no: parseInt(deptEmp.emp_no),
            dept_no: deptEmp.dept_no,
            from_date: new Date(deptEmp.from_date),
            to_date: new Date(deptEmp.to_date),
          },
        });
      }
      console.log("Department_Employee have been inserted");
    });
  } catch (e) {
    console.error("Error during database operation", e);
  } finally {
    await prisma.$disconnect();
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Update your package.json with a "seed" script for seeding the database, which includes an added prisma key:

...
"prisma": {
    "seed": "ts-node --transpile-only ./prisma/seed.ts"
  }
...

You can now seed your database by running the following command:

npx prisma db seed

We can check the imported data in the Hosted Workbench:

Working set

We can also use the dolt_status system table to see what tables changed. In our examples, we use Prisma's queryRaw and executeRaw methods for executing Dolt-specific queries to system tables, such as dolt_status. We're passing a PrismaTransaction object to our functions, which we'll discuss in more detail in a later section.

// Define an interface for dolt status
interface DoltStatus {
  table_name: string;
  staged: number;
  status: string;
}

export async function printStatus(prisma: PrismaTransaction) {
  const res = await prisma.$queryRaw<DoltStatus[]>`SELECT * FROM dolt_status`;
  console.log("Status:");
  if (res.length === 0) {
    console.log("No tables modified");
  } else {
    res.forEach((row) => {
      console.log(`  ${row.table_name}: ${row.status}`);
    });
  }
}
const prisma = new PrismaClient();
printStatus(prisma);

The printed result will look like this:

Status:
  departments: modified
  dept_emp: modified
  employees: modified

Create a Dolt Commit

Next, we'll commit our new tables using the DOLT_COMMIT() procedure. If you're familiar with Git, you'll find Dolt's commands and procedures straightforward to use.

export async function doltCommit(
  prisma: PrismaTransaction,
  author: string,
  msg: string
) {
  const result =
    await prisma.$executeRaw`CALL DOLT_COMMIT('--author', ${author}, '-Am', ${msg}) `;
  console.log("Created commit:", result);
}
const prisma = new PrismaClient();
doltCommit(prisma, "LiuLiu <liu@dolthub.com>", "Create tables");

Let’s take a closer look at the Dolt commit log. You can view the commit log using the dolt_log system table, which is designed to function similarly to the git log and dolt log commands used on the command line.

// Define an interface for the commit log
interface CommitLog {
  commit_hash: string;
  committer: string;
  message: string;
}

export async function printCommitLog(prisma: PrismaTransaction) {
  const res = await prisma.$queryRaw<
    CommitLog[]
  >`SELECT commit_hash, committer, message FROM dolt_log ORDER BY date DESC`;
  console.log("Commit log:", res);
}

const prisma = new PrismaClient();
printCommitLog(prisma);

And it outputs:

Commit log: [
  {
    commit_hash: 'akgo0pgfau2kstciubbr1mhn36ne5evd',
    committer: 'LiuLiu',
    message: 'Create tables'
  },
  {
    commit_hash: 'qpfrtco9ivuur5ltlcm1gdj46kv4tl0b',
    committer: 'Dolt System Account',
    message: 'Initialize data repository'
  }
]

Change your schema

In this section, you will change your Prisma schema and then generate and apply the migration to your hosted database.

We want to add a new field for the manager in the Department model. This field will create a foreign key reference to the Employee model. Open your schema.prisma file and modify the Department model as follows:

model Department {
  dept_no    String    @id @db.Char(4)
  dept_name  String    @unique @db.VarChar(40)
  manager_id Int?      
  manager    Employee? @relation(fields: [manager_id], references: [emp_no])
  dept_emps  DeptEmp[]
  @@map("departments")
}

Once you have updated your schema, use Prisma's migration tool to apply this change to your database. Run the following command:

npx prisma migrate dev --name add_department_manager

You will see the schema change in your hosted working set:

Schema change

Update data on a branch

In this section, we will create a branch named add-manager and switch to it. Then, we'll update the departments table by populating it with manager data on this active branch. Note that before doing this, we need to commit all the previous changes in the main branch so the schema migration is in.

Creating a branch is considered a write operation, which we perform using the procedure dolt_branch(). We then switch to this branch using dolt_checkout(). Alternatively, you can both create and switch to a new branch in one step with dolt_checkout('-b', 'newbranch').

When using dolt_checkout() to switch branches, it use a new connection because Dolt does not allow transactions across branches. For an in-depth discussion of how branch switching is handled in Dolt, refer to this blog post. To ensure that all operations are executed on the same database connection and within the same branch, we use Prisma's transaction API. This API enables a batch of operations to be executed within a single transaction, thus ensuring data consistency during the creation, viewing, and updating of tables.

Let's define a type called PrismaTransaction and use it to manage our transactions across various functions:

export type PrismaTransaction = Omit<
  PrismaClient<Prisma.PrismaClientOptions, never, DefaultArgs>,
  "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
>;
 
export async function createBranch(prisma: PrismaTransaction, branch: string) {
  const res = await getBranch(prisma, branch);
  if (res && res.length > 0) {
    console.log("Branch exists:", branch);
  } else {
    await prisma.$executeRaw`CALL DOLT_BRANCH(${branch})`;
    console.log("Created branch:", branch);
  }
}

export async function checkoutBranch(
  prisma: PrismaTransaction,
  branch: string
) {
  const res = await prisma.$executeRaw`CALL DOLT_CHECKOUT(${branch})`;
  console.log("Using branch:", branch);
}

Now that we're on a new branch, we can make modifications safely, knowing that the main branch remains unaffected. We will then update the departments table for the manager column we added in the last section:

async function updateDepartmentManagers(prisma: PrismaTransaction) {
  // Update Marketing
  await prisma.department.update({
    where: { dept_no: "d001" },
    data: { manager_id: 10001 },
  });

  // Update Finance
  await prisma.department.update({
    where: { dept_no: "d002" },
    data: { manager_id: 10002 },
  });

  // Update Human Resources
  await prisma.department.update({
    where: { dept_no: "d003" },
    data: { manager_id: 10003 },
  });

  // Update Production
  await prisma.department.update({
    where: { dept_no: "d004" },
    data: { manager_id: 10004 },
  });

  // Update Development
  await prisma.department.update({
    where: { dept_no: "d005" },
    data: { manager_id: 10005 },
  });

  console.log("Department managers updated successfully.");
}

The entire process, from creating and checking out branches to updating records, is now executed within a transaction:

async function updateManager() {
  const prisma = new PrismaClient();
  try {
    await prisma.$transaction(async (tx) => {
      await createBranch(tx, "add-manager");
      await checkoutBranch(tx, "add-manager");
      await updateDepartmentManagers(tx);
    });
    console.log("All operations completed successfully.");
  } catch (error) {
    console.error("An error occurred:", error);
  } finally {
    await prisma.$disconnect();
  }
}

updateManager();

Let's inspect the specific row changes within our departments table to make sure our change looks right. In Dolt, you can view diffs in a couple of ways: through a dolt_diff_<table> system table designated for each user-defined table, or by using the dolt_diff() table function. Here we filter the diff output to only show WORKING changes, which allows us to see modifications that have not yet been staged or committed.

export async function printDiff(prisma: PrismaTransaction, table: string) {
  const query = Prisma.sql`SELECT * FROM ${Prisma.raw(
    "dolt_diff_" + table
  )} where to_commit = 'WORKING'`;
  const res = await prisma.$queryRaw(query);
  console.log(`Diff for ${table}:`);
  console.log(res);
}

const prisma = new PrismaClient()
printDiff(prisma, "departments")

Printed result:

Diff for departments:
[
  {
    to_dept_no: 'd001',
    to_dept_name: 'Marketing',
    to_manager_id: 10001,
    to_commit: 'WORKING',
    to_commit_date: null,
    from_dept_no: 'd001',
    from_dept_name: 'Marketing',
    from_manager_id: null,
    from_commit: 'm9h541a33e7kdr368urvdqudbqjrp1vf',
    from_commit_date: 2024-06-24T20:51:12.489Z,
    diff_type: 'modified'
  },
  {
    to_dept_no: 'd002',
    to_dept_name: 'Finance',
    to_manager_id: 10002,
    to_commit: 'WORKING',
    to_commit_date: null,
    from_dept_no: 'd002',
    from_dept_name: 'Finance',
    from_manager_id: null,
    from_commit: 'm9h541a33e7kdr368urvdqudbqjrp1vf',
    from_commit_date: 2024-06-24T20:51:12.489Z,
    diff_type: 'modified'
  },
  {
    to_dept_no: 'd003',
    to_dept_name: 'Human Resources',
    to_manager_id: 10003,
    to_commit: 'WORKING',
    to_commit_date: null,
    from_dept_no: 'd003',
    from_dept_name: 'Human Resources',
    from_manager_id: null,
    from_commit: 'm9h541a33e7kdr368urvdqudbqjrp1vf',
    from_commit_date: 2024-06-24T20:51:12.489Z,
    diff_type: 'modified'
  },
  {
    to_dept_no: 'd004',
    to_dept_name: 'Production',
    to_manager_id: 10004,
    to_commit: 'WORKING',
    to_commit_date: null,
    from_dept_no: 'd004',
    from_dept_name: 'Production',
    from_manager_id: null,
    from_commit: 'm9h541a33e7kdr368urvdqudbqjrp1vf',
    from_commit_date: 2024-06-24T20:51:12.489Z,
    diff_type: 'modified'
  },
  {
    to_dept_no: 'd005',
    to_dept_name: 'Development',
    to_manager_id: 10005,
    to_commit: 'WORKING',
    to_commit_date: null,
    from_dept_no: 'd005',
    from_dept_name: 'Development',
    from_manager_id: null,
    from_commit: 'm9h541a33e7kdr368urvdqudbqjrp1vf',
    from_commit_date: 2024-06-24T20:51:12.489Z,
    diff_type: 'modified'
  }
]

This is what this diff looks like on the workbench:

Diff on the workbench

Finally, let's commit these changes so we can make different changes on another branch.

await doltCommit(prisma, "LiuLiu <liu@dolthub.com>", "Add managers");
await printCommitLog(prisma);

And you'll see our new commit in the log:

Commit log: [
  {
    commit_hash: 'inpudaq106s74m94g63gpmmr7h3ton6i',
    committer: 'LiuLiu',
    message: 'Add managers'
  },
  {
    commit_hash: 'akgo0pgfau2kstciubbr1mhn36ne5evd',
    committer: 'LiuLiu',
    message: 'Create tables'
  },
  {
    commit_hash: 'qpfrtco9ivuur5ltlcm1gdj46kv4tl0b',
    committer: 'Dolt System Account',
    message: 'Initialize data repository'
  }
]

Merge the changes into main

Now we will merge the add-manager branch into main branch using the procedure dolt_merge().

export async function doltMerge(prisma: PrismaTransaction, branch: string) {
  await prisma.$executeRaw`CALL DOLT_MERGE(${branch})`;
  console.log("Merge complete for ", branch);
}

async function mergeBranch() {
  const prisma = new PrismaClient();
  try {
    await prisma.$transaction(async (tx) => {
      await checkoutBranch(tx, "main");
      await printActiveBranch(tx);
      await doltMerge(tx, "add-manager");
      await printCommitLog(tx);
    });
    console.log("All operations completed successfully.");
  } catch (error) {
    console.error("An error occurred:", error);
  } finally {
    await prisma.$disconnect();
  }
}

mergeBranch();

This outputs the following.

Using branch: main
Active branch: [ { 'ACTIVE_BRANCH()': 'main' } ]
Merge complete for  add-manager
Commit log: [
  {
    commit_hash: 'inpudaq106s74m94g63gpmmr7h3ton6i',
    committer: 'LiuLiu',
    message: 'Add managers'
  },
  {
    commit_hash: 'akgo0pgfau2kstciubbr1mhn36ne5evd',
    committer: 'LiuLiu',
    message: 'Create tables'
  },
  {
    commit_hash: 'qpfrtco9ivuur5ltlcm1gdj46kv4tl0b',
    committer: 'Dolt System Account',
    message: 'Initialize data repository'
  }
]
All operations completed successfully.

You'll also see the final results in the Hosted Workbench:

Merge into main

Rolling back a mistake

Dolt offers robust rollback features that can be a lifesaver in scenarios where you might accidentally delete a table. Consider a situation where I inadvertently drop the dept_emp table:

  await prisma.$executeRaw`DROP TABLE dept_emp`;

After running this, checking the database status and tables shows that dept_emp is gone:

    await printTables(prisma);
    await printStatus(prisma);
Tables in database: [
  { 'Tables_in_schema-migration-test': '_prisma_migrations' },
  { 'Tables_in_schema-migration-test': 'departments' },
  { 'Tables_in_schema-migration-test': 'employees' }
]
Status:
  dept_emp: deleted

In traditional databases, such a mistake could be irreversible. However, with Dolt, we can easily restore the table by resetting to a previous state using call dolt_reset('hard'). This function takes an optional commit, if not provided, it defaults to the latest commit (HEAD):

export async function doltResetHard(
  prisma: PrismaTransaction,
  commit?: string
) {
  if (commit) {
    await prisma.$executeRaw`CALL DOLT_RESET('--hard', ${commit})`;
    console.log("Resetting to commit:", commit);
  } else {
    await prisma.$executeRaw`CALL DOLT_RESET('--hard')`;
    console.log("Resetting to HEAD");
  }
}

const prisma = new PrismaClient();
doltResetHard(prisma)
Resetting to HEAD
Rolled back to the previous commit.

This capability ensures that operating databases with Dolt is less prone to irreversible errors. You can confidently revert changes or restore your database to a previous, stable state whenever needed.

Conclusion

Integrating Dolt with Prisma adds a safety layer to managing databases by allowing you to view detailed diffs and easily roll back changes. This ensures more control during tasks like schema migrations and query executions. If you're interested in how Dolt handles schema migrations, check out our blog on schema migrations in Dolt. If you have any questions or suggestions, feel free to reach out on GitHub or join our Discord to chat. We're here to help make your experience smooth.

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.