Files
teatea-pension/.trellis/spec/big-question/postgres-json-jsonb.md

5.2 KiB

PostgreSQL JSON vs JSONB Type Issues with Drizzle ORM

Problem

Database queries using PostgreSQL's jsonb_* functions fail with type errors, even though the column appears to store JSON data correctly.

Error message:

function jsonb_array_elements(json) does not exist
HINT: No function matches the given name and argument types.
You might need to add explicit type casts.

Additional symptoms:

  • Raw SQL queries fail to find columns with camelCase names
  • JSON aggregation functions return unexpected results

Root Cause

Issue 1: Drizzle's json() Maps to PostgreSQL json, Not jsonb

When defining a JSON column in Drizzle ORM:

// Drizzle schema definition
export const orders = pgTable("orders", {
  id: text("id").primaryKey(),
  metadata: json("metadata"), // Creates PostgreSQL 'json' type, NOT 'jsonb'
});

PostgreSQL has two JSON types with different characteristics:

Feature json jsonb
Storage Text (preserves whitespace, key order) Binary (normalized)
Functions json_* functions only jsonb_* functions only
Indexing Limited GIN indexes supported
Performance Slower for operations Faster for operations

The jsonb_* functions (like jsonb_array_elements, jsonb_extract_path) only work with jsonb type.

Issue 2: Column Name Case Sensitivity

PostgreSQL treats unquoted identifiers as lowercase. If your column uses camelCase:

-- This fails (looks for column named 'metadata' in lowercase)
SELECT metadata->>'userId' FROM orders;

-- Column is actually named "metaData" with exact case
SELECT "metaData"->>'userId' FROM orders;

Solution

Solution 1: Use Type Cast for jsonb Functions

Add ::jsonb type cast before using jsonb functions:

// Before (fails)
const result = await db.execute(sql`
  SELECT jsonb_array_elements(items) as item
  FROM orders
  WHERE id = ${orderId}
`);

// After (works)
const result = await db.execute(sql`
  SELECT jsonb_array_elements(items::jsonb) as item
  FROM orders
  WHERE id = ${orderId}
`);

Solution 2: Define Column as jsonb in Schema

If you need jsonb functionality frequently, define the column as jsonb:

import { pgTable, text, jsonb } from "drizzle-orm/pg-core";

export const orders = pgTable("orders", {
  id: text("id").primaryKey(),
  metadata: jsonb("metadata"), // Now uses PostgreSQL 'jsonb' type
});

Note: This requires a migration if the column already exists.

Solution 3: Quote camelCase Column Names in Raw SQL

Always use double quotes for camelCase column names:

// Before (fails - column not found)
const result = await db.execute(sql`
  SELECT "userId", createdAt
  FROM orders
`);

// After (works)
const result = await db.execute(sql`
  SELECT "userId", "createdAt"
  FROM orders
`);

Complete Example: Querying JSON Array Data

import { sql } from "drizzle-orm";

// Table with json column storing an array of items
// items: [{ "productId": "123", "quantity": 2 }, ...]

// Query to find orders containing a specific product
async function findOrdersWithProduct(productId: string) {
  const result = await db.execute(sql`
    SELECT
      o.id,
      o."createdAt",
      item->>'productId' as "productId",
      (item->>'quantity')::int as quantity
    FROM orders o,
    jsonb_array_elements(o.items::jsonb) as item
    WHERE item->>'productId' = ${productId}
  `);

  return result.rows;
}

// Query to aggregate JSON array data
async function getOrderItemStats(orderId: string) {
  const result = await db.execute(sql`
    SELECT
      COUNT(*) as "itemCount",
      SUM((item->>'quantity')::int) as "totalQuantity"
    FROM orders o,
    jsonb_array_elements(o.items::jsonb) as item
    WHERE o.id = ${orderId}
  `);

  return result.rows[0];
}

Best Practice: Create a Helper for JSON Queries

// utils/db-helpers.ts
import { sql, SQL } from "drizzle-orm";

/**
 * Wraps a column reference with ::jsonb cast for use with jsonb functions
 */
export function asJsonb(column: SQL | string): SQL {
  if (typeof column === "string") {
    return sql.raw(`"${column}"::jsonb`);
  }
  return sql`${column}::jsonb`;
}

// Usage
const result = await db.execute(sql`
  SELECT jsonb_array_elements(${asJsonb("items")}) as item
  FROM orders
`);

Key Takeaways

  1. Know the difference between json and jsonb

    • json: Text storage, use json_* functions
    • jsonb: Binary storage, use jsonb_* functions, better performance
  2. Drizzle's json() creates PostgreSQL json type - use jsonb() if you need jsonb functionality

  3. Always add ::jsonb cast when using jsonb functions with json columns

  4. Quote camelCase identifiers in raw SQL queries with double quotes

  5. Prefer Drizzle's query builder over raw SQL when possible to avoid these issues

  6. Test raw SQL queries directly in a PostgreSQL client before using in code