Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/2-sql/4-lanes/relational-core/src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,15 @@ export class OrderByItem extends AstNode {
rewrite(rewriter: ExpressionRewriter): OrderByItem {
return new OrderByItem(this.expr.rewrite(rewriter), this.dir);
}

/**
* A new frozen item with the sort direction flipped and `expr` unchanged.
* Integrations that own pagination (e.g. backward cursor pagination) use
* this to reverse a user's sort order without reaching into the AST.
*/
reverse(): OrderByItem {
return new OrderByItem(this.expr, this.dir === 'asc' ? 'desc' : 'asc');
}
}

export class JsonArrayAggExpr extends Expression {
Expand Down
20 changes: 20 additions & 0 deletions packages/2-sql/4-lanes/relational-core/test/ast/order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,24 @@ describe('ast/order', () => {
expect(rewritten.expr).toEqual(col('article', 'title'));
expect(rewritten.dir).toBe('asc');
});

it('reverses direction into a new frozen instance, preserving expr identity', () => {
const expr = col('user', 'id');
const asc = OrderByItem.asc(expr);
const reversed = asc.reverse();

expect(reversed.dir).toBe('desc');
expect(reversed.expr).toBe(expr);
expect(reversed).not.toBe(asc);
expect(asc.dir).toBe('asc');
expect(Object.isFrozen(reversed)).toBe(true);
});

it('round-trips a double reverse back to the original direction', () => {
const desc = OrderByItem.desc(col('post', 'title'));
const roundTrip = desc.reverse().reverse();

expect(roundTrip.dir).toBe('desc');
expect(roundTrip.expr).toBe(desc.expr);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expectTypeOf, test } from 'vitest';
import type { AnyExpression, Direction, OrderByItem } from '../../src/ast/types';

test('OrderByItem.reverse() returns an OrderByItem', () => {
expectTypeOf<OrderByItem['reverse']>().toEqualTypeOf<() => OrderByItem>();
});

test('OrderByItem exposes readable dir and expr', () => {
expectTypeOf<OrderByItem['dir']>().toEqualTypeOf<Direction>();
expectTypeOf<OrderByItem['expr']>().toEqualTypeOf<AnyExpression>();
});
24 changes: 24 additions & 0 deletions packages/3-extensions/sql-orm-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ const posts = await db.Post
.all();
```

## Pagination

`.orderBy(...)` accepts model-accessor callbacks that return `OrderByItem`s via the column's `.asc()` / `.desc()` helpers:

```ts
// Forward page (newest first).
const firstPage = await db.Post
.orderBy((post) => post.createdAt.desc())
.take(10)
.all();
```

Integrations that own pagination — Relay-style backward cursor pagination, REST list endpoints — need to flip the user's sort order. Each `OrderByItem` exposes `.reverse()`, which returns a new frozen item with the direction flipped and the expression unchanged. Call it on the item the selector returns to build the backward page (then re-reverse the returned rows in application code):

```ts
// Backward page: same sort, flipped.
const lastPage = await db.Post
.orderBy((post) => post.createdAt.desc().reverse())
.take(10)
.all();
```

An integration that wraps a user-supplied order selector flips it the same way — `(post) => userSelector(post).reverse()` — without having to inspect the opaque `OrderByItem`.

## Codec Roundtrip

The runtime always awaits codec query-time methods, but rows yielded to user code carry **plain field values** — no `Promise`-typed fields ever reach `.first()` / `.all()` / streaming consumers, regardless of whether a column's codec is sync or async. This is true for both one-shot and streaming usage:
Expand Down
Loading