Skip to content

Commit 39595b5

Browse files
feat: add a way to generate sql in pg-typed without running it (#268)
1 parent 6bb3ed9 commit 39595b5

File tree

5 files changed

+330
-150
lines changed

5 files changed

+330
-150
lines changed

docs/pg-typed.md

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export async function initialize() {
6161
favorite_color: `blue`,
6262
});
6363

64-
await db.tx(async db => {
64+
await db.tx(async (db) => {
6565
// These 2 queries are run in the same transaction
6666
await users(db).find().all();
6767
await users(db).insert({
@@ -143,6 +143,28 @@ export async function setFavoriteColor(
143143
}
144144
```
145145

146+
By default, all the columns will be updated when a conflict is encountered. You can alternatively specify exactly which columns to update. For example:
147+
148+
```typescript
149+
import db, {users} from './database';
150+
151+
export async function setFavoriteColor(
152+
emails: string[],
153+
favoriteColor: string,
154+
) {
155+
const insertedOrUpdatedUsers = await users(db).insertOrUpdate(
156+
{onConflict: [`email`], set: [`updated_at`, `favorite_color`]},
157+
...emails.map((email) => ({
158+
created_at: new Date(),
159+
updated_at: new Date(),
160+
email,
161+
favorite_color: favoriteColor,
162+
})),
163+
);
164+
console.log(insertedOrUpdatedUsers);
165+
}
166+
```
167+
146168
### insertOrIgnore(...records)
147169

148170
This is similar to `insertOrUpdate`, except that when a conflict is encountered, it simply ignores the record. Only the records that were successfully inserted are returned.
@@ -299,6 +321,27 @@ import db, {users} from './database';
299321
users(db).tableName; // 'users'
300322
```
301323

324+
### conditionToSql(whereValues, tableAlias)
325+
326+
If you want to build parts of the query yourself, you can still use pg-typed to help you construct the condition in your where clause. This can be useful for join statements. In the following example the condition is very simple, so it might have been easier to simply write that SQL by hand, but in more complex conditions the type safety can still be helpful.
327+
328+
```typescript
329+
import db, {users, blog_posts, DbUser, DbBlogPost} from './database';
330+
331+
interface ActiveUserBlogPost {
332+
username: DbUser['username'];
333+
title: DbBlogPost['title'];
334+
}
335+
async function activeUserBlogPosts(): Promise<ActiveUserBlogPost[]> {
336+
return await db.query(sql`
337+
SELECT u.username, b.title
338+
FROM users AS u
339+
INNER JOIN blog_posts AS b ON u.id=b.created_by_id
340+
WHERE ${users(db).conditionToSql({active: true}, `u`)}
341+
`);
342+
}
343+
```
344+
302345
### bulkFind(options)
303346

304347
This is like the regular `.find(condition)` API, but it lets you specify multiple distinct conditions that are efficiently or'ed together. Once you've started a query using `bulkFind` you can call `.orderByAsc`/`.orderByDesc`/`.select`/etc. just like you could if you started a query with a call to `find`. You can find more details on how this API works in [@databases/pg-bulk](pg-bulk.md).
@@ -320,11 +363,11 @@ async function getPosts() {
320363

321364
### bulkInsert(options)
322365

323-
To insert thousands of records at a time, you can use the bulk insert API. This requires you to specify any optional fields that you want to pass in. Any required (i.e. `NOT NULL` and no default value) fields are automatically expected. You can find more details on how this API works in [@databases/pg-bulk](pg-bulk.md). `bulkInsert` also returns the inserted records.
366+
To insert thousands of records at a time, you can use the bulk insert API. This requires you to specify any optional columns that you want to pass in. Any required (i.e. `NOT NULL` and no default value) columns are automatically expected. You can find more details on how this API works in [@databases/pg-bulk](pg-bulk.md). `bulkInsert` also returns the inserted records.
324367

325368
```typescript
326369
async function insertUsers() {
327-
// This example assumes that `email` is a non-nullable field
370+
// This example assumes that `email` is a non-nullable column
328371
await tables.users(db).bulkInsert({
329372
columnsToInsert: [`favorite_color`],
330373
records: [
@@ -367,7 +410,7 @@ Updating multiple records in one go, where each record needs to be updated to a
367410

368411
```typescript
369412
async function updateUsers() {
370-
// This example assumes that `email` is a non-nullable field
413+
// This example assumes that `email` is a non-nullable column
371414
await tables.users(db).bulkUpdate({
372415
whereColumnNames: [`email`],
373416
setColumnNames: [`favorite_color`],
@@ -403,7 +446,7 @@ This will delete results that match: `(org_id=1 AND user_id=10) OR (org_id=2 AND
403446

404447
## SelectQuery
405448

406-
A `SelectQuery` is a query for records within a table. The actual query is sent when you call one of the methods that returns a `Promise`, i.e. `all()`, `first()` or `limit(count)`.
449+
A `SelectQuery` is a query for records within a table. The actual query is sent when you call one of the methods that returns a `Promise`, i.e. `all()`, `one()`, `oneRequired()`, `first()` or `limit(count)`.
407450

408451
### andWhere(condition)
409452

@@ -428,9 +471,13 @@ export async function getPostsSince(since: Date) {
428471
}
429472
```
430473

431-
### select(...fields)
474+
### distinct(...columns)
475+
476+
Only return distinct results for the given columns. If you call `.distinct()` without passing any column names, the entire row is checked. You cannot specify a sort order using `.orderByAsc`/`.orderByDesc` if you are using `.distinct` and you cannot rely on the data being returned in any particular order. If you need to only return distinct rows and sort your data, you should use `.orderByAscDistinct` or `.orderByDescDistinct` instead.
432477

433-
Only return the provided fields. This can be useful if you have database records with many fields or where some fields are very large, and you typically only care about a small subset of the fields. The default is to return all fields, i.e. `*`.
478+
### select(...columns)
479+
480+
Only return the provided columns. This can be useful if you have database records with many columns or where some columns are very large, and you typically only care about a small subset of the columns. The default is to return all columns, i.e. `*`.
434481

435482
```typescript
436483
import db, {users} from './database';
@@ -441,6 +488,27 @@ export async function getEmails() {
441488
}
442489
```
443490

491+
### toSql()
492+
493+
If you want to run the query yourself, or perhaps use it as part of another more complex query, you can use the `toSql` method to return the SQLQuery
494+
495+
Example:
496+
497+
```typescript
498+
import db, {users} from './database';
499+
500+
export async function getDistinctUserFirstNamesCount(): Promise<number> {
501+
const distinctFirstNameQuery = users(db)
502+
.find()
503+
.distinct(`first_name`)
504+
.toSql();
505+
const records = await db.query(sql`
506+
SELECT COUNT(*) AS row_count FROM (${distinctFirstNameQuery}) AS u
507+
`);
508+
return parseInt(`${records[0].row_count}`, 10);
509+
}
510+
```
511+
444512
### orderByAsc(key) / orderByDesc(key)
445513

446514
Sort the records by the provided key. You can chain multiple calls to `orderByAsc` or `orderByDesc` with different keys to further sort records that have the same value for the provided key.
@@ -539,7 +607,7 @@ export async function getEmails() {
539607

540608
### one()
541609

542-
Return a single record (or null). If multiple records in the table match `whereValues`, an error is thrown. If no records match `whereValues`, `null` is returned. This is useful if you want to do `.findOne` but only need a sub-set of the fields.
610+
Return a single record (or null). If multiple records in the table match `whereValues`, an error is thrown. If no records match `whereValues`, `null` is returned. This is useful if you want to do `.findOne` but only need a sub-set of the columns.
543611

544612
```typescript
545613
import db, {users} from './database';
@@ -730,7 +798,7 @@ export async function getUsersWithValidPreference() {
730798
}
731799
```
732800

733-
### key(fieldName, whereClause)
801+
### key(columnName, whereClause)
734802

735803
There is also a helper on the table itself to allow you to do `inQueryResults`, but in a type safe way:
736804

@@ -756,7 +824,7 @@ export async function getPostsByUserEmail(email: string) {
756824

757825
## or(conditions) / and(conditions)
758826

759-
To `or`/`and` values/conditions for a single field, you can use `anyOf`/`allOf`, but the `or` utility helps if you want to have multiple distinct queries. If you anticipate many conditions in an or, you may be get better performance by using `.bulkFind`/`.bulkDelete` instead of `or`.
827+
To `or`/`and` values/conditions for a single column, you can use `anyOf`/`allOf`, but the `or` utility helps if you want to have multiple distinct queries. If you anticipate many conditions in an or, you may be get better performance by using `.bulkFind`/`.bulkDelete` instead of `or`.
760828

761829
```typescript
762830
import {or, and, greaterThan} from '@databases/pg-typed';
@@ -792,7 +860,7 @@ async function getPopularPostsByAuthor(authorId: User['id']) {
792860
}
793861
```
794862

795-
You can often avoid using the `and` helper by simply duplicating some fields in the query:
863+
You can often avoid using the `and` helper by simply duplicating some columns in the query:
796864

797865
```typescript
798866
import {or, greaterThan} from '@databases/pg-typed';

docs/sql.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ db.query(sql`SELECT * FROM users WHERE dob = ${new DayDate(2018, 1, 20)};`);
141141
// => {text: 'SELECT * FROM users WHERE dob = $1;', values: ['2018-01-20']}
142142
```
143143

144+
### `sql.isSqlQuery(query)`
145+
146+
Use this to test if an unknown value is an SQLQuery. Returns `true` if `query` is an instance of `SQLQuery`, otherwise it returns `false`.
147+
144148
### `SQLQuery.format(config: FormatConfig)`
145149

146150
This returns an object with `{text: string, values: any[]}` where the `text` field contains the SQL formatted query, and values contains the parameters.

packages/mysql-typed/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ class Table<TRecord, TInsertParameters> {
258258

259259
const columnNamesSet = new Set<keyof TRecordsToInsert[number]>();
260260
for (const row of rows) {
261-
for (const columnName of Object.keys(row)) {
261+
for (const columnName of Object.keys(row as any)) {
262262
columnNamesSet.add(columnName as keyof typeof row);
263263
}
264264
}

0 commit comments

Comments
 (0)