Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 257
feat(db): add caseWhen query operator#1536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f28393a
docs: add caseWhen operator plan
samwillis 2f3bb41
docs: decouple caseWhen plan from multi-source from
samwillis 2fc10bd
feat(db): add caseWhen query operator
samwillis 54f33d1
feat(db): support guarded includes in caseWhen
samwillis b6f3666
test(db): cover caseWhen plan gaps
samwillis be5f3c7
fix(db): complete caseWhen plan coverage
samwillis 1aa57b2
test(db): assert caseWhen literal inference
samwillis eaa6431
test(db): update caseWhen type assertions
samwillis b346153
test(db): cover caseWhen collection includes
samwillis e273d02
test(db): cover caseWhen expression gaps
samwillis f05b9d1
docs(db): move caseWhen plan out of branch
samwillis 17564a4
ci: apply automated fixes
autofix-ci[bot] 5812d9d
fix(db): address caseWhen review feedback
samwillis 901eb6e
ci: apply automated fixes
autofix-ci[bot] a5b4038
refactor(db): share caseWhen condition truthiness
samwillis 7ce5c2c
refactor(db): limit caseWhen overloads
samwillis af13048
fix(db): harden conditional select compilation
samwillis d773b66
fix(db): resolve grouped caseWhen refs
samwillis 42ea9e7
refactor(db): add broad caseWhen overload fallback
samwillis c3ff562
ci: apply automated fixes
autofix-ci[bot] a746f73
fix(db): tighten caseWhen expression detection
samwillis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/db': patch | ||
| --- | ||
| Added the `caseWhen` query operator for scalar conditional expressions and conditional select projections with guarded includes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { Aggregate, Func } from '../ir' | ||
| import { toExpression } from './ref-proxy.js' | ||
| import { isRefProxy, toExpression } from './ref-proxy.js' | ||
| import type { BasicExpression } from '../ir' | ||
| import type { RefProxy } from './ref-proxy.js' | ||
| import type { | ||
| @@ -58,6 +58,28 @@ type ExpressionLike = | ||
| | undefined | ||
| | Array<unknown> | ||
| type CaseWhenValue = | ||
| | ExpressionLike | ||
| | QueryBuilder<any> | ||
| | ToArrayWrapper<any> | ||
| | ConcatToArrayWrapper<any> | ||
| | Record<string, any> | ||
| type ExtractCaseWhenValue<T> = | ||
| T extends CaseWhenWrapper<infer TResult> ? TResult : T | ||
| type CaseWhenResult< | ||
| TValues extends Array<CaseWhenValue>, | ||
| THasDefault extends boolean, | ||
| > = TValues[number] extends ExpressionLike | ||
| ? BasicExpression< | ||
| ExtractType<TValues[number]> | (THasDefault extends true ? never : null) | ||
| > | ||
| : CaseWhenWrapper< | ||
| | ExtractCaseWhenValue<TValues[number]> | ||
| | (THasDefault extends true ? never : undefined) | ||
| > | ||
| // Helper type to extract the underlying type from various expression types | ||
| type ExtractType<T> = | ||
| T extends RefProxy<infer U> | ||
| @@ -351,6 +373,249 @@ export function coalesce<T extends [ExpressionLike, ...Array<ExpressionLike>]>( | ||
| ) as CoalesceReturnType<T> | ||
| } | ||
| /** | ||
| * Returns the value for the first matching condition, similar to SQL | ||
| * `CASE WHEN`. | ||
| * | ||
| * Arguments are evaluated as condition/value pairs followed by an optional | ||
| * default value. Scalar branch values return a query expression and can be used | ||
| * in expression contexts like `select`, `where`, `orderBy`, `groupBy`, | ||
| * `having`, and equality join operands. If no scalar branch matches and no | ||
| * default is provided, the result is `null`. | ||
| * | ||
| * When a branch value is a projection object, `caseWhen` becomes a select-only | ||
| * projection value. Projection branches can include nested fields, ref spreads, | ||
| * and includes. If no projection branch matches and no default is provided, the | ||
| * result is `undefined`. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * caseWhen(gt(user.age, 18), `adult`, `minor`) | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * caseWhen( | ||
| * gt(user.age, 65), | ||
| * `senior`, | ||
| * gt(user.age, 18), | ||
| * `adult`, | ||
| * `minor`, | ||
| * ) | ||
| * ``` | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * caseWhen(gt(user.age, 18), { | ||
| * ...user, | ||
| * posts: q | ||
| * .from({ post: postsCollection }) | ||
| * .where(({ post }) => eq(post.userId, user.id)), | ||
| * }) | ||
| * ``` | ||
| */ | ||
| export function caseWhen<C1 extends ExpressionLike, V1 extends CaseWhenValue>( | ||
| condition1: C1, | ||
| value1: V1, | ||
| ): CaseWhenResult<[V1], false> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| D extends CaseWhenValue, | ||
| >(condition1: C1, value1: V1, defaultValue: D): CaseWhenResult<[V1, D], true> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| ): CaseWhenResult<[V1, V2], false> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| D extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| defaultValue: D, | ||
| ): CaseWhenResult<[V1, V2, D], true> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| ): CaseWhenResult<[V1, V2, V3], false> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| D extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| defaultValue: D, | ||
| ): CaseWhenResult<[V1, V2, V3, D], true> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| C4 extends ExpressionLike, | ||
| V4 extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| condition4: C4, | ||
| value4: V4, | ||
| ): CaseWhenResult<[V1, V2, V3, V4], false> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| C4 extends ExpressionLike, | ||
| V4 extends CaseWhenValue, | ||
| D extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| condition4: C4, | ||
| value4: V4, | ||
| defaultValue: D, | ||
| ): CaseWhenResult<[V1, V2, V3, V4, D], true> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| C4 extends ExpressionLike, | ||
| V4 extends CaseWhenValue, | ||
| C5 extends ExpressionLike, | ||
| V5 extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| condition4: C4, | ||
| value4: V4, | ||
| condition5: C5, | ||
| value5: V5, | ||
| ): CaseWhenResult<[V1, V2, V3, V4, V5], false> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| C4 extends ExpressionLike, | ||
| V4 extends CaseWhenValue, | ||
| C5 extends ExpressionLike, | ||
| V5 extends CaseWhenValue, | ||
| D extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| condition4: C4, | ||
| value4: V4, | ||
| condition5: C5, | ||
| value5: V5, | ||
| defaultValue: D, | ||
| ): CaseWhenResult<[V1, V2, V3, V4, V5, D], true> | ||
| export function caseWhen< | ||
| C1 extends ExpressionLike, | ||
| V1 extends CaseWhenValue, | ||
| C2 extends ExpressionLike, | ||
| V2 extends CaseWhenValue, | ||
| C3 extends ExpressionLike, | ||
| V3 extends CaseWhenValue, | ||
| C4 extends ExpressionLike, | ||
| V4 extends CaseWhenValue, | ||
| C5 extends ExpressionLike, | ||
| V5 extends CaseWhenValue, | ||
| >( | ||
| condition1: C1, | ||
| value1: V1, | ||
| condition2: C2, | ||
| value2: V2, | ||
| condition3: C3, | ||
| value3: V3, | ||
| condition4: C4, | ||
| value4: V4, | ||
| condition5: C5, | ||
| value5: V5, | ||
| condition6: ExpressionLike, | ||
| value6: CaseWhenValue, | ||
| ...rest: Array<CaseWhenValue> | ||
| ): any | ||
| export function caseWhen(...args: Array<CaseWhenValue>): any { | ||
| if (args.length < 2) { | ||
| throw new Error(`caseWhen() requires at least two arguments`) | ||
| } | ||
| const pairCount = Math.floor(args.length / 2) | ||
| for (let i = 0; i < pairCount; i++) { | ||
| const condition = args[i * 2] | ||
| if (!isConditionValue(condition)) { | ||
| throw new Error(`caseWhen() conditions must be expression-like values`) | ||
| } | ||
| } | ||
| if (caseWhenHasOnlyExpressionValues(args)) { | ||
| return new Func( | ||
| `caseWhen`, | ||
| args.map((arg) => toExpression(arg)), | ||
| ) | ||
| } | ||
| return new CaseWhenWrapper(args) | ||
| } | ||
| export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>( | ||
| left: T1, | ||
| right: T2, | ||
| @@ -426,6 +691,7 @@ export const operators = [ | ||
| `add`, | ||
| // Utility functions | ||
| `coalesce`, | ||
| `caseWhen`, | ||
| // Aggregate functions | ||
| `count`, | ||
| `avg`, | ||
| @@ -450,8 +716,75 @@ export class ConcatToArrayWrapper<_T = unknown> { | ||
| constructor(public readonly query: QueryBuilder<any>) {} | ||
| } | ||
| export class CaseWhenWrapper<_T = any> { | ||
| readonly __brand = `CaseWhenWrapper` as const | ||
| declare readonly _type: `caseWhen` | ||
| readonly _result?: _T | ||
| constructor(public readonly args: Array<CaseWhenValue>) {} | ||
| } | ||
| export function toArray<TContext extends Context>( | ||
| query: QueryBuilder<TContext>, | ||
| ): ToArrayWrapper<GetRawResult<TContext>> { | ||
| return new ToArrayWrapper(query) | ||
| } | ||
| function caseWhenHasOnlyExpressionValues(args: Array<CaseWhenValue>): boolean { | ||
| const valueIndexes = getCaseWhenValueIndexes(args.length) | ||
| return valueIndexes.every((index) => isExpressionValue(args[index])) | ||
| } | ||
| function getCaseWhenValueIndexes(argCount: number): Array<number> { | ||
| const valueIndexes: Array<number> = [] | ||
| const hasDefaultValue = argCount % 2 === 1 | ||
| const pairCount = Math.floor(argCount / 2) | ||
| for (let i = 0; i < pairCount; i++) { | ||
| valueIndexes.push(i * 2 + 1) | ||
| } | ||
| if (hasDefaultValue) { | ||
| valueIndexes.push(argCount - 1) | ||
| } | ||
| return valueIndexes | ||
| } | ||
| function isExpressionValue(value: CaseWhenValue | undefined): boolean { | ||
| if (isRefProxy(value)) return true | ||
| if (value instanceof Aggregate || value instanceof Func) return true | ||
| if (value == null) return true | ||
| if ( | ||
| typeof value === `string` || | ||
| typeof value === `number` || | ||
| typeof value === `boolean` || | ||
| typeof value === `bigint` | ||
| ) { | ||
| return true | ||
| } | ||
| if (value instanceof Date || Array.isArray(value)) return true | ||
| if (typeof value === `object`) { | ||
| const candidate = value as { | ||
| type?: unknown | ||
| args?: unknown | ||
| name?: unknown | ||
| path?: unknown | ||
| value?: unknown | ||
| } | ||
| if ( | ||
| (candidate.type === `agg` || candidate.type === `func`) && | ||
| typeof candidate.name === `string` && | ||
| Array.isArray(candidate.args) | ||
| ) { | ||
| return true | ||
| } | ||
| if (candidate.type === `ref` && Array.isArray(candidate.path)) return true | ||
| if (candidate.type === `val` && `value` in candidate) return true | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return false | ||
| } | ||
| function isConditionValue(value: CaseWhenValue | undefined): boolean { | ||
| return isExpressionValue(value) && !Array.isArray(value) | ||
| } | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.