drizzle-team / drizzle-team/drizzle-orm

[FEATURE]: UPDATE SET with value in CTEs

Open
#4,679 1 comment 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
TypeScript
Stars
35.8k
Forks
1.6k
Avg merge
2d 7h
Merged PRs (30d)
4

Description

### Feature hasn't been suggested before.

- [x] I have verified this feature I'm about to request hasn't been suggested before.

### Describe the enhancement you want to request

possible prev issues: #2078

currently we can't do updates use values from CTEs due to:

- `update().join()` not available
- cannot pass CTE fields to set

```typescript
// ordered_pp CTE
const orderedPp = this.drizzle.$with('ordered_pp').as(
this.drizzle
.select({
scoreId: s.id,
userId: s.userId,
mode: s.mode,
pp: s.pp,
acc: s.accuracy,
})
.from(s)
.where(
and(
eq(s.status, 2),
gt(s.pp, 0),
eq(s.mode, dbMode),
eq(s.userId, id)
)
)
.orderBy(
desc(s.pp),
desc(s.accuracy),
desc(s.id),
)
)
// bests CTE
const bests = this.drizzle.$with('bests').as(
this.drizzle
.select({
scoreId: orderedPp.scoreId,
userId: orderedPp.userId,
mode: orderedPp.mode,
pp: orderedPp.pp,
acc: orderedPp.acc,
globalRank: sql`ROW_NUMBER() OVER (ORDER BY ${orderedPp.pp} DESC, ${orderedPp.acc} DESC, ${orderedPp.scoreId} DESC)`.as('g'),
})
.from(orderedPp)
)
// user_calc CTE
const userCalc = this.drizzle.$with('user_calc').as(
this.drizzle
.select({
weightedPP: sql`SUM(POW(0.95, ${bests.globalRank} - 1) * ${bests.pp})`.as('w'),
bnsPP: sql`(1 - POW(0.9994, COUNT(*))) * 416.6667`.as('bnspp'),
acc: sql`SUM(POW(0.95, ${bests.globalRank} - 1) * ${bests.acc}) / SUM(POW(0.95, ${bests.globalRank} - 1))`.as('acc'),
pp: sql`SUM(POW(0.95, ${bests.globalRank} - 1) * ${bests.pp}) + (1 - POW(0.9994, COUNT(*))) * 416.6667`.as('pp'),
})
.from(bests)
)
// concrete_stats CTE
const concreteStats = this.drizzle.$with('concrete_stats').as(
this.drizzle
.select({
count: sql`count(*)`.as('count'),
totalScore: sql`sum(${s.score})`.as('total_score'),
rankedScore: sql`sum(if(${s.status} = 2, ${s.score}, 0))`.as('ranked_score'),
tth: sql`sum(${s.n300} + ${s.n100} + ${s.n50} + (${s.nGeki} + ${s.nKatu}))`.as('tth'),
playTime: sql`sum(${s.timeElapsed}) / 1000`.as('play_time'),
maxCombo: sql`max(${s.maxCombo})`.as('max_combo'),
countXh: sql`sum(${s.grade} = 'XH')`.as('xh'),
countX: sql`sum(${s.grade} = 'X')`.as('x'),
countSh: sql`sum(${s.grade} = 'SH')`.as('sh'),
countS: sql`sum(${s.grade} = 'S')`.as('s'),
countA: sql`sum(${s.grade} = 'A')`.as('a'),
})
.from(s)
.where(
and(
eq(s.mode, dbMode),
eq(s.userId, id)
)
)
)
// Do the update using .with().update().set()
await this.drizzle
.with(orderedPp, bests, userCalc, concreteStats)
.update(stats)
.set({
totalScore: concreteStats.totalScore,
rankedScore: concreteStats.rankedScore,
pp: userCalc.pp,
accuracy: userCalc.acc,
plays: concreteStats.count,
playTime: concreteStats.playTime,
maxCombo: concreteStats.maxCombo,
totalHits: concreteStats.tth,
xhCount: concreteStats.countXh,
xCount: concreteStats.countX,
shCount: concreteStats.countSh,
sCount: concreteStats.countS,
aCount: concreteStats.countA,
})
.where(
and(
eq(stats.id, id),
eq(stats.mode, dbMode)
)
)
```

![Image](https://github.com/user-attachments/assets/0d4b95b2-4913-4558-8a8b-668b984f70d4)

raw SQL (some variable names may be different):
```sql
WITH
ordered_pp AS (
SELECT
s1.id scoreId,
s1.userid,
s1.mode,
s1.pp,
s1.acc
FROM
scores s1
INNER JOIN maps m ON s1.map_md5 = m.md5
WHERE
s1.status = 2
AND m.status IN (2, 3)
AND s1.pp > 0
AND s1.mode = :mode
AND s1.userid = :user_id
ORDER BY
s1.pp DESC,
s1.acc DESC,
s1.id DESC
),
bests AS (
SELECT
scoreId,
pp,
acc,
ROW_NUMBER() OVER (
ORDER BY
pp DESC
) AS global_rank
FROM
ordered_pp
),
user_calc AS (
SELECT
SUM(POW (0.95, global_rank - 1) * pp) AS weightedPP,
(1 - POW (0.9994, COUNT(*))) * 416.6667 AS bnsPP,
SUM(POW (0.95, global_rank - 1) * acc) / SUM(POW (0.95, global_rank - 1)) AS acc
FROM
bests
),
calculated AS (
SELECT
*,
weightedPP + bnsPP AS pp
FROM
user_calc
),
concrete_stats AS (
SELECT
COUNT(*) AS count,
SUM(s2.score) AS total_score,
SUM(IF(m2.status IN (2, 3) AND s2.status = 2, s2.score, 0)) AS ranked_score,
SUM(s2.n300 + s2.n100 + s2.n50 + (IF(s2.mode IN (1, 3, 5), s2.ngeki + s2.nkatu, 0))) AS total_hits,
SUM(s2.time_elapsed) / 1000 AS play_time,
MAX(s2.max_combo) AS max_combo,
SUM(s2.grade = "XH") AS xh_count,
SUM(s2.grade = "X") AS x_count,
SUM(s2.grade = "SH") AS sh_count,
SUM(s2.grade = "S") AS s_count,
SUM(s2.grade = "A") AS a_count
FROM
scores s2
INNER JOIN maps m2 ON s2.map_md5 = m2.md5
WHERE s2.mode = :mode
AND s2.userid = :user_id
)
UPDATE stats s
INNER JOIN concrete_stats cs ON 1
INNER JOIN calculated c ON 1
SET
s.tscore = cs.total_score,
s.rscore = cs.ranked_score,
s.pp = c.pp,
s.acc = c.acc,
s.plays = cs.count,
s.playtime = cs.play_time,
s.max_combo = cs.max_combo,
s.total_hits = cs.total_hits,
s.xh_count = cs.xh_count,
s.x_count = cs.x_count,
s.sh_count = cs.sh_count,
s.s_count = cs.s_count,
s.s_count = cs.a_count
WHERE s.id = :user_id AND s.mode = :mode
```

Contributor guide

Open the contributing guide

Research direction

The issue names no files or tests. Start at the .with().update().set() query-builder entry point and compare its generated SQL with the supplied UPDATE ... WITH example; the feature is done when CTE fields can populate SET values and the requested joined CTE update is supported.

Written by the indexing model from the issue text.

Assessment

Tech stack
sql, typescript
Domain
backend-api-design, databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.