HIGH integrity failuresexpresscockroachdb

Integrity Failures in Express with Cockroachdb

Integrity Failures in Express with Cockroachdb — how this specific combination creates or exposes the vulnerability

Integrity failures occur when an application fails to enforce data correctness, consistency, or referential integrity across operations. In an Express application using CockroachDB, these failures typically arise from a mismatch between optimistic application-level logic and the guarantees provided by the database, especially under concurrency, partial failures, or insufficient transaction usage.

CockroachDB is a distributed SQL database that provides strong consistency and serializable isolation by default. However, if an Express route does not explicitly use transactions or does not properly handle retryable errors, interleaved operations can lead to anomalies such as lost updates, dirty reads, or constraint violations that should have been prevented. For example, consider a financial transfer endpoint that reads a balance, computes a new value, and writes it back without a transaction:

app.put('/transfer', async (req, res) => {
  const { fromId, toId, amount } = req.body;
  const client = await pool.connect();
  const fromAcc = await client.query('SELECT balance FROM accounts WHERE id = $1', [fromId]);
  if (fromAcc.rows[0].balance < amount) {
    return res.status(400).send('Insufficient funds');
  }
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $3', [amount, toId]);
  client.release();
  res.send('OK');
});

This pattern is vulnerable to race conditions: two concurrent transfers can both read the same balance, each see sufficient funds, and then both deduct their amounts, resulting in a negative balance or lost updates. Although CockroachDB prevents dirty reads and ensures serializable execution, the application logic here does not serialize writes to the same rows, so interleaved statements can produce inconsistent state. This is an integrity failure rooted in missing transaction boundaries.

Another common integrity risk in Express with CockroachDB involves improper handling of unique constraints and foreign keys during batched or conditional inserts. For instance, an endpoint that creates a parent record and then, based on a computed value, creates child records might fail to maintain referential integrity if the parent ID is not correctly propagated within a transaction:

app.post('/order', async (req, res) => {
  const { customerId, items } = req.body;
  const client = await pool.connect();
  const insertCustomer = await client.query('INSERT INTO customers (id, name) VALUES ($1, $2) RETURNING id', [customerId, 'Example']);
  const parentId = insertCustomer.rows[0].id;
  for (const item of items) {
    await client.query('INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)', [parentId, item.productId, item.qty]);
  }
  client.release();
  res.status(201).json({ orderId: parentId });
});

If one of the item inserts fails (e.g., due to a missing product or a constraint), the customer record is already committed, creating an orphaned entity and breaking referential integrity. CockroachDB’s constraints will reject the bad item insert, but without a transaction that includes the customer insert, there is no automatic rollback of the customer record. This exposes an integrity failure where partial writes corrupt the dataset.

Finally, integrity failures can be exacerbated by incorrect isolation expectations. Developers might assume read-committed is the effective isolation level and rely on application-side checks instead of database constraints. CockroachDB defaults to serializable isolation, which prevents write skew, but if application code uses explicit locking or relies on non-repeatable reads within a transaction, subtle bugs can emerge. For example, checking existence and then inserting without holding a consistent lock can lead to duplicate entries under concurrency:

app.post('/ensure-tag', async (req, res) => {
  const { name } = req.body;
  const client = await pool.connect();
  const exists = await client.query('SELECT 1 FROM tags WHERE name = $1', [name]);
  if (exists.rows.length === 0) {
    await client.query('INSERT INTO tags (name) VALUES ($1)', [name]);
  }
  client.release();
  res.send('done');
});

Two concurrent requests can both see the tag as missing and both insert it, violating uniqueness if there is no database-side constraint. The correct approach is to rely on CockroachDB constraints and handle serialization errors, not to implement integrity checks at the application layer alone.

In summary, integrity failures in Express with CockroachDB typically stem from missing or misused transactions, inadequate constraint usage, and incorrect assumptions about isolation. The database can provide strong guarantees, but the application must structure its operations to leverage those guarantees consistently.

Cockroachdb-Specific Remediation in Express — concrete code fixes

To remediate integrity failures, Express applications using CockroachDB should embrace explicit transactions, proper constraint design, and error handling aligned with CockroachDB’s serializable semantics. Below are concrete, production-grade patterns.

1. Use explicit transactions for multi-statement operations

Wrap all related reads and writes in a client transaction to ensure atomicity and consistency:

app.put('/transfer-safe', async (req, res) => {
  const { fromId, toId, amount } = req.body;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const fromAcc = await client.query('SELECT balance FROM accounts WHERE id = $1 FOR UPDATE', [fromId]);
    if (fromAcc.rows[0].balance < amount) {
      await client.query('ROLLBACK');
      return res.status(400).send('Insufficient funds');
    }
    await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
    await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $3', [amount, toId]);
    await client.query('COMMIT');
    res.send('OK');
  } catch (err) {
    await client.query('ROLLBACK');
    if (err.code === '40001') {
      // Serializable retry error
      return res.status(409).send('Conflict, please retry');
    }
    res.status(500).send('Internal error');
  } finally {
    client.release();
  }
});

The FOR UPDATE clause locks the rows for the duration of the transaction, preventing concurrent modifications and ensuring serializable execution. Handling CockroachDB’s serialization retry error (SQLSTATE 40001) is essential for robust operation.

2. Enforce referential integrity with transactions and constraints

Always perform inserts that depend on other entities within the same transaction, and rely on foreign key constraints:

app.post('/order-safe', async (req, res) => {
  const { customerId, items } = req.body;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query('INSERT INTO customers (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING', [customerId, 'Example']);
    for (const item of items) {
      await client.query('INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)', [customerId, item.productId, item.qty]);
    }
    await client.query('COMMIT');
    res.status(201).json({ orderId: customerId });
  } catch (err) {
    await client.query('ROLLBACK');
    if (err.code === '23503') {
      // Foreign key violation
      return res.status(400).send('Referential integrity violation');
    }
    res.status(500).send('Internal error');
  } finally {
    client.release();
  }
});

Using ON CONFLICT DO NOTHING avoids duplicate customer creation, and foreign key constraints ensure that invalid order_id values are rejected by the database.

3. Avoid application-side checks for uniqueness; use constraints and handle retries

Replace existence checks with constraints and handle serialization errors gracefully:

app.post('/tag-safe', async (req, res) => {
  const { name } = req.body;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query('INSERT INTO tags (name) VALUES ($1)', [name]);
    await client.query('COMMIT');
    res.status(201).send('Created');
  } catch (err) {
    await client.query('ROLLBACK');
    if (err.code === '23505') {
      // Unique violation, tag already exists — safe to ignore
      return res.status(200).send('Already exists');
    }
    if (err.code === '40001') {
      return res.status(409).send('Conflict, please retry');
    }
    res.status(500).send('Internal error');
  } finally {
    client.release();
  }
});

This pattern leverages CockroachDB’s unique constraint to enforce integrity, avoiding race conditions. The 23505 error code indicates a unique violation; returning a 200 status is acceptable if the business logic allows idempotent creation.

By combining explicit transactions, robust constraint usage, and proper error handling, Express applications can fully leverage CockroachDB’s consistency guarantees and avoid integrity failures.

Frequently Asked Questions

Why does my Express app with CockroachDB still have integrity issues despite using constraints?
Constraints alone are not sufficient if multi-statement operations are not wrapped in explicit transactions. Without BEGIN/COMMIT, concurrent requests can interleave reads and writes, leading to lost updates or partial writes even when unique and foreign key constraints exist. Always use transactions for related operations and handle serialization retries.
How should I handle CockroachDB serialization errors in Express routes?
When a transaction receives a 40001 (serialization failure) error, the transaction must be retried from the beginning with the same operations. Implement a retry wrapper that re-runs the transaction logic up to a small limit, ensuring idempotency of the operations within the transaction.