# Prisma `createMany` 与 PostgreSQL `GENERATED ALWAYS AS` 列冲突

**日期**: 2026-05-15
**模块**: ai-usage（#338）
**关键词**: prisma, createMany, GENERATED column, raw SQL

## 现象

在 `platform_ai_usage.ai_usage_events` 表上定义了 `total_tokens` 列为
`GENERATED ALWAYS AS (input + output + cache_creation + cache_read) STORED`。
Prisma schema 把它声明为普通 `Int`（Prisma 不识别 generated column 语义）。

跑批量 ingestion `prisma.aiUsageEvent.createMany({ data: rows })` 时，PG 抛错：

```
cannot insert a non-DEFAULT value into column "total_tokens"
```

因为 Prisma 帮我们把 `data` 里的字段（包括 `totalTokens`）显式列在 INSERT 字段
清单里，而 PG 对 GENERATED ALWAYS 列拒绝任何显式赋值。

## 不工作的尝试

- 在 schema 里把字段标 `@ignore` → Prisma 不生成该字段，select 时拿不到值
- 在 schema 里加 `@default(dbgenerated("..."))`：Prisma 会让 generator 用 default
  但 `createMany` 仍会把字段列在 INSERT clause，PG 仍报错
- 在 createMany 里省略字段：Prisma 仍会把 schema 里有的字段全列上（值用 default 占位）

## 工作方案

**raw SQL INSERT 完全不列 generated column**：

```typescript
await this.prisma.$queryRawUnsafe(
  `INSERT INTO platform_ai_usage.ai_usage_events
     (id, raw_message_id, ..., input_tokens, output_tokens,
      cache_creation_tokens, cache_read_tokens, estimated_cost_usd, ...)
   VALUES (gen_random_uuid(), $1, ..., $11, $12, $13, $14, $15, ...)
   ON CONFLICT (raw_message_id) DO NOTHING
   RETURNING 1`,
  ...params
);
```

- 不列 `total_tokens` → PG 自动算
- `ON CONFLICT DO NOTHING` 替代 `createMany skipDuplicates`（依然按 UNIQUE 去重）
- `RETURNING 1` 配合 array length 计算实际 inserted 数（区分 inserted vs deduped）

实测在 ingestion service 单批 500 行场景下，对 dev 数据库延迟可接受
（< 200ms）。如未来需要更高吞吐：考虑批量 INSERT + multi-VALUES。

## 影响范围

任何在 prisma schema 里需要表达 PostgreSQL 计算列的场景。常见于：
- 累加列（sum / 加权和）
- 文本搜索向量（tsvector）
- 几何列（PostGIS）

约定：**generated column 一律走 raw SQL 插入**，Prisma schema 仅用于 select 类型。
在迁移文件 SQL 顶部添加 `-- generated column 需 raw INSERT` 注释提醒后续 dev。

## 参考

- 实测代码：`backend/src/modules/ai-usage/services/ingestion.service.ts` 的
  `insertEventsRaw()` 方法
- Prisma issue: https://github.com/prisma/prisma/issues/4253
