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

193 lines
5.2 KiB
Markdown

# 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:
```typescript
// 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:
```sql
-- 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:
```typescript
// 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:
```typescript
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:
```typescript
// 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
```typescript
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
```typescript
// 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
## Related Resources
- [PostgreSQL JSON Types Documentation](https://www.postgresql.org/docs/current/datatype-json.html)
- [PostgreSQL JSON Functions](https://www.postgresql.org/docs/current/functions-json.html)
- [Drizzle ORM PostgreSQL Column Types](https://orm.drizzle.team/docs/column-types/pg)