npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mosaic-code/prisma-select-for-update

v0.1.1

Published

Prisma v7 extension for SELECT ... FOR UPDATE row locking

Downloads

220

Readme

@mosaic-code/prisma-select-for-update

A Prisma v7 extension that adds SELECT ... FOR UPDATE row locking functionality for PostgreSQL databases. This extension provides type-safe methods to lock rows during transactions, preventing concurrent modifications.

Features

  • Type-safe row locking - Three methods: findUniqueForUpdate, findFirstForUpdate, findManyForUpdate
  • Multiple lock modes - Support for FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, and FOR KEY SHARE
  • NOWAIT and SKIP LOCKED - Options for non-blocking lock acquisition
  • Prisma-compatible API - Works seamlessly with existing Prisma queries
  • PostgreSQL-only - Optimized for PostgreSQL's row-level locking

Installation

npm install @mosaic-code/prisma-select-for-update

Peer Dependencies:

  • @prisma/client@^7.0.0
  • @prisma/adapter-pg@^7.0.0 (for PostgreSQL adapter)

Quick Start

import { PrismaClient } from '@prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'
import { withForUpdate } from '@mosaic-code/prisma-select-for-update'

const { Pool } = pg
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaPg(pool)

const basePrisma = new PrismaClient({ adapter })
const prisma = basePrisma.$extends(withForUpdate())

// Use within a transaction
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueForUpdate({
    where: { id: 1 },
  })
  
  // User row is now locked - safe to modify
  if (user && user.balance >= 100) {
    await tx.user.update({
      where: { id: user.id },
      data: { balance: { decrement: 100 } },
    })
  }
})

API Reference

Methods

All methods must be called within a $transaction callback. Calling them outside a transaction will throw an error.

findUniqueForUpdate

Locks and returns a single row by unique field(s).

const user = await prisma.$transaction(async (tx) => {
  return tx.user.findUniqueForUpdate({
    where: { id: 1 },
    select: { id: true, email: true }, // optional
    lock: { mode: 'ForUpdate' }, // optional, defaults to ForNoKeyUpdate
  })
})

findFirstForUpdate

Locks and returns the first matching row.

const task = await prisma.$transaction(async (tx) => {
  return tx.task.findFirstForUpdate({
    where: { status: 'pending' },
    orderBy: { priority: 'desc' },
    lock: { skipLocked: true },
  })
})

findManyForUpdate

Locks and returns multiple matching rows.

const users = await prisma.$transaction(async (tx) => {
  return tx.user.findManyForUpdate({
    where: { balance: { gte: 100 } },
    orderBy: { balance: 'asc' },
    take: 10,
    skip: 0,
    lock: { mode: 'ForShare' },
  })
})

Lock Options

interface LockOptions {
  /** Lock mode - defaults to 'ForNoKeyUpdate' */
  mode?: 'ForUpdate' | 'ForNoKeyUpdate' | 'ForShare' | 'ForKeyShare'
  /** Fail immediately if row is locked (NOWAIT) */
  noWait?: boolean
  /** Skip locked rows instead of waiting (SKIP LOCKED) */
  skipLocked?: boolean
}

Lock Modes

  • ForNoKeyUpdate (default) - Exclusive lock for non-key columns, allows other transactions to acquire FOR KEY SHARE locks. Less restrictive than ForUpdate and common for updates that don't modify key columns.
  • ForUpdate - Exclusive lock, prevents other transactions from modifying or locking the row
  • ForShare - Shared lock, allows other transactions to read but not modify
  • ForKeyShare - Weakest lock, allows other transactions to read and acquire FOR KEY SHARE locks

NOWAIT and SKIP LOCKED

// Fail immediately if row is locked
const user = await prisma.$transaction(async (tx) => {
  return tx.user.findUniqueForUpdate({
    where: { id: 1 },
    lock: { noWait: true }, // Throws error if row is locked
  })
})

// Skip locked rows (useful for queue processing)
const tasks = await prisma.$transaction(async (tx) => {
  return tx.task.findManyForUpdate({
    where: { status: 'pending' },
    lock: { skipLocked: true }, // Returns only unlocked rows
  })
})

Examples

Bank Account Transfer

await prisma.$transaction(async (tx) => {
  // Lock both accounts
  const fromAccount = await tx.account.findUniqueForUpdate({
    where: { id: fromAccountId },
  })
  const toAccount = await tx.account.findUniqueForUpdate({
    where: { id: toAccountId },
  })

  if (!fromAccount || fromAccount.balance < amount) {
    throw new Error('Insufficient funds')
  }

  // Update balances
  await tx.account.update({
    where: { id: fromAccountId },
    data: { balance: { decrement: amount } },
  })
  await tx.account.update({
    where: { id: toAccountId },
    data: { balance: { increment: amount } },
  })
})

Queue Processing with SKIP LOCKED

// Worker 1
const jobs1 = await prisma.$transaction(async (tx) => {
  return tx.job.findManyForUpdate({
    where: { status: 'pending' },
    orderBy: { createdAt: 'asc' },
    take: 10,
    lock: { skipLocked: true }, // Skip rows locked by other workers
  })
})

// Worker 2 (runs concurrently)
const jobs2 = await prisma.$transaction(async (tx) => {
  return tx.job.findManyForUpdate({
    where: { status: 'pending' },
    orderBy: { createdAt: 'asc' },
    take: 10,
    lock: { skipLocked: true }, // Gets different rows
  })
})

Complex WHERE Clauses

All standard Prisma where operators are supported:

const users = await prisma.$transaction(async (tx) => {
  return tx.user.findManyForUpdate({
    where: {
      OR: [
        { balance: { gte: 1000 } },
        { email: { contains: '@company.com' } },
      ],
      AND: [
        { status: 'active' },
        { createdAt: { gte: new Date('2024-01-01') } },
      ],
      NOT: {
        role: 'admin',
      },
    },
    orderBy: [{ balance: 'desc' }, { createdAt: 'asc' }],
  })
})

Supported WHERE Operators

  • Equality: equals, not
  • Comparison: lt, lte, gt, gte
  • Arrays: in, notIn
  • Strings: contains, startsWith, endsWith
  • Logical: AND, OR, NOT
  • Null: null, not: null

Note: String operators (contains, startsWith, endsWith) escape LIKE wildcards (%, _) to match Prisma's behavior. For custom LIKE patterns, use $queryRaw within your transaction.

Known Limitations

  1. PostgreSQL-only - This extension is designed specifically for PostgreSQL's row-level locking. MySQL and SQLite are not supported.

  2. Case-insensitive matching - The mode: 'insensitive' option is not supported. Use $queryRaw with ILIKE for case-insensitive matching:

await prisma.$transaction(async (tx) => {
  const users = await tx.$queryRaw`
    SELECT * FROM "User" 
    WHERE email ILIKE ${'%test%'} 
    FOR UPDATE
  `
})
  1. Transaction required - All forUpdate methods must be called within a transaction. This is enforced at runtime.

  2. Relation filters - Relation filters (e.g., posts: { some: {...} }) are not supported. Use joins or separate queries instead.

Type Safety

The extension maintains full TypeScript type safety:

const user = await prisma.$transaction(async (tx) => {
  return tx.user.findUniqueForUpdate({
    where: { id: 1 },
    select: { id: true, email: true },
  })
})

// Type: { id: number; email: string } | null

Error Handling

try {
  await prisma.$transaction(async (tx) => {
    return tx.user.findUniqueForUpdate({
      where: { id: 1 },
      lock: { noWait: true },
    })
  })
} catch (error) {
  if (error.message.includes('could not obtain lock')) {
    // Row is locked by another transaction
  }
}

Testing

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

Publishing

To publish this package to npm:

# Build the package (runs automatically before publish)
npm run build

# Publish to npm
npm publish

The prepublishOnly script ensures a clean build before publishing. Only the dist folder, README.md, and LICENSE are included in the published package.

License

Do No Harm

Related