drizzle-team / drizzle-team/drizzle-orm
[FEATURE]: UPDATE SET with value in CTEs
- 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)
)
)
```

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
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