Skip to content
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

feat: Add Offset Implementation & Handling #300

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
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
28 changes: 28 additions & 0 deletions docs/mysql-typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,34 @@ export async function getEmailsAlphabetical() {
}
```

### offset(count)

Skip the first `count` rows. This is generally a less efficient method of pagination than using a field in the query as a "next page token". You can only use this method if you have first called `orderByAsc` or `orderByDesc` at least once.

```typescript
import db, {users} from './database';

export async function paginatedEmails(page?: number) {
const records = await users(db)
.find()
.orderByAsc(`email`)
.offset(10 * (page ?? 0))
.limit(10);
return records.map((record) => record.email)
}

export async function printAllEmails() {
let pageNumber = 0
let records = await paginatedEmails(pageNumber++);
while (records.length) {
for (const email of records) {
console.log(email);
}
records = await paginatedEmails(pageNumber++);
}
}
```

### limit(count)

Return the first `count` rows. N.B. you can only use this method if you have first called `orderByAsc` or `orderByDesc` at least once.
Expand Down
28 changes: 28 additions & 0 deletions docs/pg-typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,34 @@ export async function getOldestPostVersions() {
}
```

### offset(count)

Skip the first `count` rows. This is generally a less efficient method of pagination than using a field in the query as a "next page token". You can only use this method if you have first called `orderByAsc` or `orderByDesc` at least once.

```typescript
import db, {users} from './database';

export async function paginatedEmails(page?: number) {
const records = await users(db)
.find()
.orderByAsc(`email`)
.offset(10 * (page ?? 0))
.limit(10);
return records.map((record) => record.email)
}

export async function printAllEmails() {
let pageNumber = 0
let records = await paginatedEmails(pageNumber++);
while (records.length) {
for (const email of records) {
console.log(email);
}
records = await paginatedEmails(pageNumber++);
}
}
```

### limit(count)

Return the first `count` rows. N.B. you can only use this method if you have first called `orderByAsc` or `orderByDesc` at least once.
Expand Down
24 changes: 22 additions & 2 deletions packages/mock-db-typed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ export interface SelectQuery<TRecord> {
...fields: TKeys
): SelectQuery<Pick<TRecord, TKeys[number]>>;
}

export interface OrderedSelectQuery<TRecord> extends SelectQuery<TRecord> {
export interface OrderedSelectQueryWithOffset<TRecord>
extends SelectQuery<TRecord> {
first(): Promise<TRecord | null>;
limit(count: number): Promise<TRecord[]>;
}
export interface OrderedSelectQuery<TRecord>
extends OrderedSelectQueryWithOffset<TRecord> {
offset(count: number): OrderedSelectQueryWithOffset<TRecord>;
}

class FieldQuery<T> {
protected readonly __query: (
Expand Down Expand Up @@ -134,6 +138,7 @@ class SelectQueryImplementation<TRecord>
{
public readonly orderByQueries: SQLQuery[] = [];
public limitCount: number | undefined;
public offsetCount: number | undefined;
private _selectFields: SQLQuery | undefined;

constructor(
Expand Down Expand Up @@ -164,6 +169,9 @@ class SelectQueryImplementation<TRecord>
if (this.limitCount) {
parts.push(sql`LIMIT ${this.limitCount}`);
}
if (this.offsetCount) {
parts.push(sql`OFFSET ${this.offsetCount}`);
}
return this._executeQuery(
parts.length === 1 ? parts[0] : sql.join(parts, sql` `),
);
Expand Down Expand Up @@ -205,6 +213,18 @@ class SelectQueryImplementation<TRecord>
this.limitCount = count;
return await this._getResults('limit');
}
public offset(offset: number) {
if (!this.orderByQueries.length) {
throw new Error(
'You cannot call "offset" until after you call "orderByAsc" or "orderByDesc".',
);
}
if (this.offsetCount !== undefined) {
throw new Error('You cannot call "offset" multiple times');
}
this.offsetCount = offset;
return this;
}
public async first() {
if (!this.orderByQueries.length) {
throw new Error(
Expand Down
12 changes: 12 additions & 0 deletions packages/mysql-typed/src/__tests__/index.test.mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ t('create users', async () => {
]
`);

const photoRecordsOffset = await photos(db)
.find({owner_user_id: 1})
.orderByAsc('cdn_url')
.offset(1)
.limit(2);
expect(photoRecords.map((p) => p.cdn_url)).toMatchInlineSnapshot(`
Array [
"http://example.com/2",
"http://example.com/3",
]
`);

const photoRecordsDesc = await photos(db)
.find({owner_user_id: 1})
.orderByDesc('cdn_url')
Expand Down
24 changes: 22 additions & 2 deletions packages/mysql-typed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ export interface SelectQuery<TRecord> {
...fields: TKeys
): SelectQuery<Pick<TRecord, TKeys[number]>>;
}

export interface OrderedSelectQuery<TRecord> extends SelectQuery<TRecord> {
export interface OrderedSelectQueryWithOffset<TRecord>
extends SelectQuery<TRecord> {
first(): Promise<TRecord | null>;
limit(count: number): Promise<TRecord[]>;
}
export interface OrderedSelectQuery<TRecord>
extends OrderedSelectQueryWithOffset<TRecord> {
offset(count: number): OrderedSelectQueryWithOffset<TRecord>;
}

class FieldQuery<T> {
protected readonly __query: (
Expand Down Expand Up @@ -134,6 +138,7 @@ class SelectQueryImplementation<TRecord>
{
public readonly orderByQueries: SQLQuery[] = [];
public limitCount: number | undefined;
public offsetCount: number | undefined;
private _selectFields: SQLQuery | undefined;

constructor(
Expand Down Expand Up @@ -164,6 +169,9 @@ class SelectQueryImplementation<TRecord>
if (this.limitCount) {
parts.push(sql`LIMIT ${this.limitCount}`);
}
if (this.offsetCount) {
parts.push(sql`OFFSET ${this.offsetCount}`);
}
return this._executeQuery(
parts.length === 1 ? parts[0] : sql.join(parts, sql` `),
);
Expand Down Expand Up @@ -205,6 +213,18 @@ class SelectQueryImplementation<TRecord>
this.limitCount = count;
return await this._getResults('limit');
}
public offset(offset: number) {
if (!this.orderByQueries.length) {
throw new Error(
'You cannot call "offset" until after you call "orderByAsc" or "orderByDesc".',
);
}
if (this.offsetCount !== undefined) {
throw new Error('You cannot call "offset" multiple times');
}
this.offsetCount = offset;
return this;
}
public async first() {
if (!this.orderByQueries.length) {
throw new Error(
Expand Down
12 changes: 12 additions & 0 deletions packages/pg-typed/src/__tests__/index.test.pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ test('create users', async () => {
]
`);

const photoRecordsOffset = await photos(db)
.find({owner_user_id: forbes.id})
.orderByAsc('cdn_url')
.offset(1)
.limit(2);
expect(photoRecordsOffset.map((p) => p.cdn_url)).toMatchInlineSnapshot(`
Array [
"http://example.com/2",
"http://example.com/3",
]
`);

const photoRecordsDesc = await photos(db)
.find({owner_user_id: forbes.id})
.orderByDesc('cdn_url')
Expand Down
36 changes: 34 additions & 2 deletions packages/pg-typed/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type UnorderedSelectQueryMethods =
export type SelectQueryMethods =
| UnorderedSelectQueryMethods
| 'first'
| 'offset'
| 'limit';
export interface SelectQuery<TRecord, TMethods extends SelectQueryMethods> {
toSql(): SQLQuery;
Expand All @@ -68,6 +69,20 @@ export interface SelectQuery<TRecord, TMethods extends SelectQueryMethods> {
all(): Promise<TRecord[]>;
first(): Promise<TRecord | null>;
limit(count: number): Promise<TRecord[]>;
offset(
count: number,
): PartialSelectQuery<
TRecord,
Exclude<
TMethods,
| 'distinct'
| 'orderByAscDistinct'
| 'orderByDescDistinct'
| 'orderByAsc'
| 'orderByDesc'
| 'offset'
>
>;

select<
TKeys extends readonly [keyof TRecord, ...(readonly (keyof TRecord)[])],
Expand All @@ -94,13 +109,13 @@ export interface SelectQuery<TRecord, TMethods extends SelectQueryMethods> {
key: keyof TRecord,
): PartialSelectQuery<
TRecord,
Exclude<TMethods, 'distinct'> | 'first' | 'limit'
Exclude<TMethods, 'distinct'> | 'first' | 'limit' | 'offset'
>;
orderByDescDistinct(
key: keyof TRecord,
): PartialSelectQuery<
TRecord,
Exclude<TMethods, 'distinct'> | 'first' | 'limit'
Exclude<TMethods, 'distinct'> | 'first' | 'limit' | 'offset'
>;
orderByAsc(
key: keyof TRecord,
Expand All @@ -112,6 +127,7 @@ export interface SelectQuery<TRecord, TMethods extends SelectQueryMethods> {
>
| 'first'
| 'limit'
| 'offset'
>;
orderByDesc(
key: keyof TRecord,
Expand All @@ -123,6 +139,7 @@ export interface SelectQuery<TRecord, TMethods extends SelectQueryMethods> {
>
| 'first'
| 'limit'
| 'offset'
>;
andWhere(condition: WhereCondition<TRecord>): this;
}
Expand Down Expand Up @@ -494,6 +511,7 @@ class SelectQueryImplementation<TRecord>
direction: 'ASC' | 'DESC';
}[] = [];
private _limitCount: number | undefined;
private _offsetCount: number | undefined;
private _selectFields: readonly string[] | undefined;
private readonly _whereAnd: WhereCondition<TRecord>[] = [];

Expand Down Expand Up @@ -525,6 +543,7 @@ class SelectQueryImplementation<TRecord>
const selectFields = this._selectFields;
const orderByQueries = this._orderByQueries;
const limitCount = this._limitCount;
const offsetCount = this._offsetCount;
const distinctColumnNames = this._distinctColumnNames;

const whereCondition =
Expand Down Expand Up @@ -578,6 +597,7 @@ class SelectQueryImplementation<TRecord>
)}`
: null,
limitCount ? sql`LIMIT ${limitCount}` : null,
offsetCount ? sql`OFFSET ${offsetCount}` : null,
].filter(<T>(v: T): v is Exclude<T, null> => v !== null),
sql` `,
);
Expand Down Expand Up @@ -661,6 +681,18 @@ class SelectQueryImplementation<TRecord>
this._limitCount = count;
return await this._getResults('limit');
}
public offset(offset: number) {
if (!this._orderByQueries.length) {
throw new Error(
'You cannot call "offset" until after you call "orderByAsc" or "orderByDesc".',
);
}
if (this.offsetCount !== undefined) {
throw new Error('You cannot call "offset" multiple times');
}
this._offsetCount = offset;
return this;
}
public async first() {
if (!this._orderByQueries.length) {
throw new Error(
Expand Down