Skip to content

Commit 47fe83b

Browse files
committed
Add more sql features, helpers and advanced features
1 parent b3b73e7 commit 47fe83b

File tree

16 files changed

+2328
-52
lines changed

16 files changed

+2328
-52
lines changed

docs/advanced-queries.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,327 @@ const rawSingleUser = await qb.raw<User>({
717717

718718
console.log('Raw single user:', rawSingleUser.results);
719719
```
720+
721+
## DISTINCT Selection
722+
723+
Use the `distinct()` method to remove duplicate rows from your query results.
724+
725+
### Simple DISTINCT
726+
727+
```typescript
728+
import { D1QB } from 'workers-qb';
729+
730+
const qb = new D1QB(env.DB);
731+
732+
// SELECT DISTINCT * FROM users
733+
const uniqueUsers = await qb.select('users')
734+
.distinct()
735+
.execute();
736+
737+
// SELECT DISTINCT name, email FROM users
738+
const uniqueUserInfo = await qb.select('users')
739+
.distinct()
740+
.fields(['name', 'email'])
741+
.execute();
742+
```
743+
744+
### DISTINCT ON (PostgreSQL)
745+
746+
PostgreSQL supports `DISTINCT ON` to select distinct rows based on specific columns:
747+
748+
```typescript
749+
import { PGQB } from 'workers-qb';
750+
751+
const qb = new PGQB(client);
752+
753+
// SELECT DISTINCT ON (department) department, name FROM users ORDER BY department
754+
const firstUserPerDept = await qb.select('users')
755+
.distinct(['department'])
756+
.fields(['department', 'name'])
757+
.orderBy('department')
758+
.execute();
759+
```
760+
761+
## Additional JOIN Types
762+
763+
In addition to INNER, LEFT, and CROSS joins, `workers-qb` supports RIGHT, FULL, and NATURAL joins.
764+
765+
### RIGHT JOIN
766+
767+
A RIGHT JOIN returns all rows from the right table and matching rows from the left table:
768+
769+
```typescript
770+
const result = await qb.select('orders')
771+
.rightJoin({ table: 'users', on: 'orders.user_id = users.id' })
772+
.execute();
773+
```
774+
775+
### FULL OUTER JOIN
776+
777+
A FULL JOIN returns all rows when there's a match in either table:
778+
779+
```typescript
780+
const result = await qb.select('users')
781+
.fullJoin({ table: 'profiles', on: 'users.id = profiles.user_id' })
782+
.execute();
783+
```
784+
785+
### NATURAL JOIN
786+
787+
A NATURAL JOIN automatically joins tables based on columns with the same name:
788+
789+
```typescript
790+
const result = await qb.select('users')
791+
.naturalJoin('profiles')
792+
.execute();
793+
```
794+
795+
## Set Operations (UNION, INTERSECT, EXCEPT)
796+
797+
Combine results from multiple queries using set operations.
798+
799+
### UNION
800+
801+
Combine results from two queries, removing duplicates:
802+
803+
```typescript
804+
const result = await qb.select('active_users')
805+
.fields(['id', 'name'])
806+
.union(qb.select('archived_users').fields(['id', 'name']))
807+
.execute();
808+
```
809+
810+
### UNION ALL
811+
812+
Combine results while keeping duplicates:
813+
814+
```typescript
815+
const result = await qb.select('table1')
816+
.fields(['id'])
817+
.unionAll(qb.select('table2').fields(['id']))
818+
.execute();
819+
```
820+
821+
### INTERSECT
822+
823+
Return only rows that appear in both queries:
824+
825+
```typescript
826+
const result = await qb.select('users')
827+
.fields(['id'])
828+
.intersect(qb.select('admins').fields(['user_id']))
829+
.execute();
830+
```
831+
832+
### EXCEPT
833+
834+
Return rows from the first query that don't appear in the second:
835+
836+
```typescript
837+
const result = await qb.select('all_users')
838+
.fields(['id'])
839+
.except(qb.select('blocked_users').fields(['user_id']))
840+
.execute();
841+
```
842+
843+
### Chaining Set Operations
844+
845+
You can chain multiple set operations and add ORDER BY to the final result:
846+
847+
```typescript
848+
const result = await qb.select('table1')
849+
.fields(['id', 'name'])
850+
.union(qb.select('table2').fields(['id', 'name']))
851+
.union(qb.select('table3').fields(['id', 'name']))
852+
.orderBy({ name: 'ASC' })
853+
.execute();
854+
```
855+
856+
## Common Table Expressions (CTEs)
857+
858+
CTEs allow you to define named temporary result sets using the `WITH` clause.
859+
860+
### Simple CTE
861+
862+
```typescript
863+
const result = await qb.select('orders')
864+
.with('active_users', qb.select('users').where('status = ?', 'active'))
865+
.join({ table: 'active_users', on: 'orders.user_id = active_users.id' })
866+
.execute();
867+
868+
// SQL: WITH active_users AS (SELECT * FROM users WHERE status = 'active')
869+
// SELECT * FROM orders JOIN active_users ON orders.user_id = active_users.id
870+
```
871+
872+
### Multiple CTEs
873+
874+
```typescript
875+
const result = await qb.select('combined')
876+
.with('recent_orders', qb.select('orders').where('created_at > ?', lastWeek))
877+
.with('active_users', qb.select('users').where('status = ?', 'active'))
878+
.execute();
879+
```
880+
881+
### CTE with Column Names
882+
883+
```typescript
884+
const result = await qb.select('results')
885+
.with(
886+
'user_stats',
887+
qb.select('users').fields(['id', 'COUNT(*) as cnt']).groupBy('id'),
888+
['user_id', 'count'] // Column names for the CTE
889+
)
890+
.execute();
891+
892+
// SQL: WITH user_stats(user_id, count) AS (SELECT id, COUNT(*) as cnt FROM users GROUP BY id)
893+
```
894+
895+
## Query Inspection (toSQL / toDebugSQL)
896+
897+
Inspect generated SQL without executing the query.
898+
899+
### toSQL()
900+
901+
Get the SQL query and parameters without executing:
902+
903+
```typescript
904+
const { sql, params } = qb.select('users')
905+
.where('status = ?', 'active')
906+
.where('age > ?', 18)
907+
.toSQL();
908+
909+
console.log(sql); // "SELECT * FROM users WHERE (status = ?) AND (age > ?)"
910+
console.log(params); // ['active', 18]
911+
```
912+
913+
### toDebugSQL()
914+
915+
Get the SQL with parameters interpolated (for debugging only):
916+
917+
```typescript
918+
const debugSql = qb.select('users')
919+
.where('id = ?', 1)
920+
.where('name = ?', "O'Brien")
921+
.toDebugSQL();
922+
923+
console.log(debugSql);
924+
// "SELECT * FROM users WHERE (id = 1) AND (name = 'O''Brien')"
925+
```
926+
927+
**Warning:** Never use `toDebugSQL()` output to execute queries as it bypasses parameterization.
928+
929+
## Pagination
930+
931+
The `paginate()` method provides convenient pagination with metadata.
932+
933+
```typescript
934+
const result = await qb.select('users')
935+
.where('active = ?', true)
936+
.orderBy({ created_at: 'DESC' })
937+
.paginate({ page: 2, perPage: 20 });
938+
939+
console.log(result.results); // Array of users for page 2
940+
console.log(result.pagination); // Pagination metadata
941+
942+
// result.pagination = {
943+
// page: 2,
944+
// perPage: 20,
945+
// total: 150,
946+
// totalPages: 8,
947+
// hasNext: true,
948+
// hasPrev: true
949+
// }
950+
```
951+
952+
## Query Plan Analysis (EXPLAIN)
953+
954+
Use `explain()` to get the query execution plan for debugging performance:
955+
956+
```typescript
957+
const plan = await qb.select('users')
958+
.where('email = ?', 'test@example.com')
959+
.explain();
960+
961+
console.log(plan.results);
962+
// Shows SQLite's EXPLAIN QUERY PLAN output
963+
```
964+
965+
## Transactions
966+
967+
Execute multiple queries atomically.
968+
969+
### D1 Transactions
970+
971+
D1 uses batching for atomic operations:
972+
973+
```typescript
974+
import { D1QB } from 'workers-qb';
975+
976+
const qb = new D1QB(env.DB);
977+
978+
const results = await qb.transaction(async (tx) => {
979+
return [
980+
tx.insert({ tableName: 'orders', data: { user_id: 1, total: 100 } }),
981+
tx.update({
982+
tableName: 'inventory',
983+
data: { stock: 9 },
984+
where: { conditions: 'product_id = ?', params: [1] }
985+
}),
986+
];
987+
});
988+
```
989+
990+
### Durable Objects Transactions
991+
992+
DOQB uses SQLite transactions synchronously:
993+
994+
```typescript
995+
import { DOQB } from 'workers-qb';
996+
997+
// Inside a Durable Object, use within blockConcurrencyWhile
998+
this.ctx.blockConcurrencyWhile(() => {
999+
qb.transaction((tx) => {
1000+
tx.insert({ tableName: 'orders', data: { user_id: 1, total: 100 } }).execute();
1001+
tx.update({
1002+
tableName: 'inventory',
1003+
data: { stock: 9 },
1004+
where: { conditions: 'product_id = ?', params: [1] }
1005+
}).execute();
1006+
});
1007+
});
1008+
```
1009+
1010+
## Query Hooks
1011+
1012+
Register hooks to intercept queries before or after execution.
1013+
1014+
### beforeQuery Hook
1015+
1016+
Modify queries before execution (e.g., add tenant filters):
1017+
1018+
```typescript
1019+
const tenantId = 'tenant-123';
1020+
1021+
qb.beforeQuery((query, type) => {
1022+
if (type === 'SELECT' || type === 'UPDATE' || type === 'DELETE') {
1023+
// Add tenant filter to all queries
1024+
query.query = query.query.replace(
1025+
'WHERE',
1026+
`WHERE tenant_id = '${tenantId}' AND`
1027+
);
1028+
}
1029+
return query;
1030+
});
1031+
```
1032+
1033+
### afterQuery Hook
1034+
1035+
Process results or record metrics after execution:
1036+
1037+
```typescript
1038+
qb.afterQuery((result, query, duration) => {
1039+
console.log(`Query took ${duration}ms:`, query.query);
1040+
metrics.record('query_duration', duration);
1041+
return result;
1042+
});
1043+
```

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "workers-qb",
3-
"version": "1.12.0",
3+
"version": "1.13.0",
44
"description": "Zero dependencies Query Builder for Cloudflare Workers",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

0 commit comments

Comments
 (0)