Skip to content
Merged
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
32 changes: 28 additions & 4 deletions postgres/pg-codegen/__fixtures__/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ export class TableClient<App> {
): Promise<SelectResult<App, S>[]> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery().select(fields.map(field => this.column(field)));
if (args.where) query.where(this.filter(args.where));
const predicate = this.predicate(args.where);
if (predicate) query.where(predicate);
this.applyOrderBy(query, args.orderBy);
if (args.limit !== undefined) query.limit(args.limit);
if (args.offset !== undefined) query.offset(args.offset);
Expand All @@ -208,7 +209,8 @@ export class TableClient<App> {

async count(where?: Where<App>): Promise<number> {
const query = this.baseQuery().select([]).selectExpr('count', fn('count', [lit(1)]));
if (where) query.where(this.filter(where));
const predicate = this.predicate(where);
if (predicate) query.where(predicate);
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
const row = rows[0] as { count: string | number };
Expand All @@ -233,7 +235,7 @@ export class TableClient<App> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery()
.update(this.encodeData(args.data))
.where(this.filter(args.where))
.where(this.required(args.where, 'update'))
.returning(fields.map(field => this.column(field)));
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
Expand All @@ -254,7 +256,7 @@ export class TableClient<App> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery()
.delete()
.where(this.filter(args.where))
.where(this.required(args.where, 'delete'))
.returning(fields.map(field => this.column(field)));
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
Expand Down Expand Up @@ -293,6 +295,28 @@ export class TableClient<App> {
return decoded as SelectResult<App, S>;
}

/**
* The predicate to apply, or nothing to apply: a caller that spreads a
* conditional key column (a scope that records none) states an empty filter,
* and an unqualified read is what it asked for.
*/
private predicate(where: Where<App> | undefined): Filter | undefined {
if (!where) return undefined;
const filter = this.filter(where);
return Object.keys(filter).length > 0 ? filter : undefined;
}

/** A write says which rows: an empty filter would mean the whole table. */
private required(where: Where<App>, operation: string): Filter {
const predicate = this.predicate(where);
if (!predicate) {
throw new Error(
`${this.spec.table}.${operation}: refusing an empty where filter, which would match every row`
);
}
return predicate;
}

private filter(where: Where<App>): Filter {
const filter: Filter = {};
for (const [key, value] of Object.entries(where)) {
Expand Down
22 changes: 22 additions & 0 deletions postgres/pg-codegen/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ it('findFirstOrThrow raises on no match', async () => {
await expect(db.users.findFirstOrThrow({ where: { id: 999999 } })).rejects.toThrow(RowNotFoundError);
});

it('reads unqualified when a conditional filter spreads to nothing', async () => {
await db.users.create({ data: { username: 'karl' } });
const keyColumn: string | null = null;

const rows = await db.users.findMany({
where: { ...(keyColumn ? { username: keyColumn } : {}) },
select: { username: true }
});
expect(rows.map(u => u.username)).toEqual(['karl']);
expect(await db.users.count({})).toBe(1);
});

it('refuses a write whose filter would match every row', async () => {
await db.users.create({ data: { username: 'lena' } });

await expect(db.users.update({ where: {}, data: { email: 'x@example.com' } })).rejects.toThrow(
/refusing an empty where filter/
);
await expect(db.users.delete({ where: {} })).rejects.toThrow(/refusing an empty where filter/);
expect(await db.users.count()).toBe(1);
});

it('rebinds to another connection with $with', async () => {
const rebound = db.$with(pg.client);
await rebound.users.create({ data: { username: 'judy' } });
Expand Down
32 changes: 28 additions & 4 deletions postgres/pg-codegen/src/emit/templates/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ export class TableClient<App> {
): Promise<SelectResult<App, S>[]> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery().select(fields.map(field => this.column(field)));
if (args.where) query.where(this.filter(args.where));
const predicate = this.predicate(args.where);
if (predicate) query.where(predicate);
this.applyOrderBy(query, args.orderBy);
if (args.limit !== undefined) query.limit(args.limit);
if (args.offset !== undefined) query.offset(args.offset);
Expand All @@ -208,7 +209,8 @@ export class TableClient<App> {

async count(where?: Where<App>): Promise<number> {
const query = this.baseQuery().select([]).selectExpr('count', fn('count', [lit(1)]));
if (where) query.where(this.filter(where));
const predicate = this.predicate(where);
if (predicate) query.where(predicate);
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
const row = rows[0] as { count: string | number };
Expand All @@ -233,7 +235,7 @@ export class TableClient<App> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery()
.update(this.encodeData(args.data))
.where(this.filter(args.where))
.where(this.required(args.where, 'update'))
.returning(fields.map(field => this.column(field)));
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
Expand All @@ -254,7 +256,7 @@ export class TableClient<App> {
const fields = this.selectedFields(args.select);
const query = this.baseQuery()
.delete()
.where(this.filter(args.where))
.where(this.required(args.where, 'delete'))
.returning(fields.map(field => this.column(field)));
const { text, values } = query.build();
const { rows } = await this.db.query(text, values);
Expand Down Expand Up @@ -293,6 +295,28 @@ export class TableClient<App> {
return decoded as SelectResult<App, S>;
}

/**
* The predicate to apply, or nothing to apply: a caller that spreads a
* conditional key column (a scope that records none) states an empty filter,
* and an unqualified read is what it asked for.
*/
private predicate(where: Where<App> | undefined): Filter | undefined {
if (!where) return undefined;
const filter = this.filter(where);
return Object.keys(filter).length > 0 ? filter : undefined;
}

/** A write says which rows: an empty filter would mean the whole table. */
private required(where: Where<App>, operation: string): Filter {
const predicate = this.predicate(where);
if (!predicate) {
throw new Error(
`${this.spec.table}.${operation}: refusing an empty where filter, which would match every row`
);
}
return predicate;
}

private filter(where: Where<App>): Filter {
const filter: Filter = {};
for (const [key, value] of Object.entries(where)) {
Expand Down
Loading