- 導入 Prisma 7.9.1 + SQLite(better-sqlite3 driver adapter),prisma.config.ts 管理連線設定 - 完整資料模型:User/Work/Character/CharacterAlias/EpisodicMemory/SemanticMemory/ ProceduralRule/EmotionState/Relationship/SentimentLedgerEntry - packages/db:Prisma Client 存取層(ESM,因 Prisma 7 產出的 generated client 僅支援 ESM) - apps/api 隨之改為 ESM 以相容 packages/db - packages/shared:新增角色/記憶/情緒/關係共用型別,api 與 web 皆從此匯入 - prisma/seed.ts:建立測試使用者與元氣型測試角色,含完整關係帳本與記憶種子資料 - apps/api:新增 GET /characters - scripts/smoke/B.mjs:驗證資料表齊全、API 讀出種子角色、關係與記憶可寫入讀出 - 保留 Prisma 官方隨 CLI 附的 agent skill 文件(.agents/skills 等),供後續群組查閱 npm run restart && npm run smoke -- B 皆通過(B-V),A 群組冒煙測試無回歸。
4.3 KiB
Raw Queries
Execute raw SQL when Prisma's query API isn't sufficient.
$queryRaw
Execute SELECT queries and get typed results:
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email LIKE ${'%@prisma.io'}
`
With type
type User = { id: number; email: string; name: string | null }
const users = await prisma.$queryRaw<User[]>`
SELECT id, email, name FROM "User" WHERE role = ${'ADMIN'}
`
Dynamic table/column names
Use Prisma.raw() for identifiers (not safe for user input):
import { Prisma } from '../generated/client'
const column = 'email'
const users = await prisma.$queryRaw`
SELECT ${Prisma.raw(column)} FROM "User"
`
With Prisma.sql
Build queries dynamically:
import { Prisma } from '../generated/client'
const email = 'alice@prisma.io'
const query = Prisma.sql`SELECT * FROM "User" WHERE email = ${email}`
const users = await prisma.$queryRaw(query)
Join multiple SQL fragments
import { Prisma } from '../generated/client'
const conditions = [
Prisma.sql`role = ${'ADMIN'}`,
Prisma.sql`verified = ${true}`
]
const users = await prisma.$queryRaw`
SELECT * FROM "User"
WHERE ${Prisma.join(conditions, ' AND ')}
`
$executeRaw
Execute INSERT, UPDATE, DELETE (returns affected count):
const count = await prisma.$executeRaw`
UPDATE "User" SET verified = true WHERE email LIKE ${'%@prisma.io'}
`
console.log(`Updated ${count} users`)
Delete example
const deleted = await prisma.$executeRaw`
DELETE FROM "User" WHERE "deletedAt" < ${thirtyDaysAgo}
`
Insert example
const inserted = await prisma.$executeRaw`
INSERT INTO "Log" (message, level, timestamp)
VALUES (${message}, ${level}, ${new Date()})
`
$queryRawUnsafe / $executeRawUnsafe
For fully dynamic queries (use with caution!):
// ⚠️ SQL injection risk - only use with trusted input
const table = 'User'
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "${table}" WHERE id = $1`,
userId
)
Parameterized unsafe query
const result = await prisma.$executeRawUnsafe(
'UPDATE "User" SET name = $1 WHERE id = $2',
'Alice',
1
)
SQL Injection Prevention
Safe (parameterized)
// ✅ User input is parameterized
const email = userInput
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`
Unsafe (concatenation)
// ❌ SQL injection vulnerability!
const email = userInput
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${email}'`
)
Database-Specific Features
PostgreSQL
// Array operations
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE 'admin' = ANY(roles)
`
// JSON operations
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE metadata->>'theme' = 'dark'
`
MySQL
// Full-text search
const posts = await prisma.$queryRaw`
SELECT * FROM Post WHERE MATCH(title, content) AGAINST(${searchTerm})
`
Transactions with Raw Queries
await prisma.$transaction(async (tx) => {
await tx.$executeRaw`UPDATE "Account" SET balance = balance - ${amount} WHERE id = ${senderId}`
await tx.$executeRaw`UPDATE "Account" SET balance = balance + ${amount} WHERE id = ${recipientId}`
})
Handling Results
BigInt handling
PostgreSQL returns BigInt for COUNT:
const result = await prisma.$queryRaw<[{ count: bigint }]>`
SELECT COUNT(*) as count FROM "User"
`
const count = Number(result[0].count)
Date handling
type Result = { createdAt: Date }
const users = await prisma.$queryRaw<Result[]>`
SELECT "createdAt" FROM "User"
`
// createdAt is already a Date object
Invalid JavaScript Date values passed to raw queries fail validation instead of being silently serialized as null. Validate date input at the application boundary; do not rely on new Date(badValue) reaching the database.
When a driver adapter returns an unmapped database-specific error, Prisma surfaces P2039 with the adapter's preserved original code/message. If those details are missing, fix the adapter mapping rather than parsing rendered error text.