Skip to content

Commit 5af8ca4

Browse files
committed
support sql and sql.fragment with arguments
1 parent 300b0ee commit 5af8ca4

File tree

4 files changed

+89
-9
lines changed

4 files changed

+89
-9
lines changed

packages/client/readme.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Note that @pgkit/migra and @pgkit/schemainspect are pure ports of their Python e
4444
- [sql.unnest](#sqlunnest)
4545
- [sql.join](#sqljoin)
4646
- [sql.fragment](#sqlfragment)
47+
- [nested `sql` tag](#nested-sql-tag)
48+
- [sql.fragment](#sqlfragment-1)
4749
- [sql.interval](#sqlinterval)
4850
- [sql.binary](#sqlbinary)
4951
- [sql.json](#sqljson)
@@ -269,6 +271,38 @@ expect(result).toEqual({id: 100, name: 'one hundred'})
269271

270272
### sql.fragment
271273

274+
Use `sql.fragment` to build reusable pieces which can be plugged into full queries.
275+
276+
```typescript
277+
const idGreaterThan = (id: number) => sql.fragment`id >= ${id}`
278+
const result = await client.any(sql`
279+
select * from usage_test where ${idGreaterThan(2)}
280+
`)
281+
282+
expect(result).toEqual([
283+
{id: 2, name: 'two'},
284+
{id: 3, name: 'three'},
285+
])
286+
```
287+
288+
### nested `sql` tag
289+
290+
You can also use `` sql`...` `` to create a fragment of SQL, but it's recommended to use `sql.fragment` instead for explicitness. Support for [type-generation](https://npmjs.com/package/@pgkit/typegen) is better using `sql.fragment` too.
291+
292+
```typescript
293+
const idGreaterThan = (id: number) => sql`id >= ${id}`
294+
const result = await client.any(sql`
295+
select * from usage_test where ${idGreaterThan(2)}
296+
`)
297+
298+
expect(result).toEqual([
299+
{id: 2, name: 'two'},
300+
{id: 3, name: 'three'},
301+
])
302+
```
303+
304+
### sql.fragment
305+
272306
Lets you create reusable SQL fragments, for example a where clause. Note that right now, fragments do not allow parameters.
273307

274308
```typescript
@@ -895,6 +929,7 @@ Generally, usage of a _client_ (or pool, to use the slonik term), should be iden
895929
- no `stream` support yet
896930
- See [future](#👽-future) for more details/which parity features are planned
897931

932+
898933
### Added features/improvements
899934

900935
#### `sql`

packages/client/src/sql.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const sqlMethodHelpers: SQLMethodHelpers = {
1010
name: nameQuery([query]),
1111
token: 'sql',
1212
values: [],
13+
templateArgs: () => [[query]],
1314
}),
1415
type:
1516
type =>
@@ -31,6 +32,7 @@ const sqlMethodHelpers: SQLMethodHelpers = {
3132
sql: strings.join(''),
3233
token: 'sql',
3334
values: parameters,
35+
templateArgs: () => [strings, ...parameters],
3436
}
3537
},
3638
}
@@ -99,14 +101,14 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => {
99101
}
100102

101103
case 'sql': {
102-
if (param.values?.length) {
103-
throw new QueryError(`Can't handle nested SQL with parameters`, {
104-
cause: {query: {name: nameQuery(strings), sql, values: inputParameters}},
105-
})
104+
const [parts, ...fragmentValues] = param.templateArgs()
105+
for (let i = 0; i < parts.length; i++) {
106+
sql += parts[i]
107+
if (i < fragmentValues.length) {
108+
values.push(fragmentValues[i])
109+
sql += '$' + String(i + 1)
110+
}
106111
}
107-
108-
sql += param.sql
109-
// values.push(...param.values);
110112
break
111113
}
112114

@@ -132,7 +134,14 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => {
132134
}
133135

134136
case 'fragment': {
135-
sql += param.args[0][0]
137+
const [parts, ...fragmentValues] = param.args
138+
for (let i = 0; i < parts.length; i++) {
139+
sql += parts[i]
140+
if (i < fragmentValues.length) {
141+
values.push(fragmentValues[i])
142+
sql += '$' + String(i + 1)
143+
}
144+
}
136145
break
137146
}
138147

@@ -155,6 +164,7 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => {
155164
sql,
156165
token: 'sql',
157166
values,
167+
templateArgs: () => [strings, ...inputParameters],
158168
}
159169
}
160170

packages/client/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface SQLQuery<Result = Record<string, unknown>, Values extends unkno
66
sql: string
77
values: Values
88
parse: (input: unknown) => Result
9+
/** @internal */
10+
templateArgs: () => [strings: readonly string[], ...inputParameters: readonly any[]]
911
}
1012

1113
export type TimeUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds'
@@ -62,6 +64,8 @@ export type SqlFragment = {
6264
token: 'sql'
6365
sql: string
6466
values: unknown[]
67+
/** @internal */
68+
templateArgs: () => [strings: readonly string[], ...inputParameters: readonly any[]]
6569
}
6670
/**
6771
* "string" type covers all type name identifiers – the literal values are added only to assist developer
@@ -94,7 +98,7 @@ export type SQLTagHelperParameters = {
9498
array: [values: readonly PrimitiveValueExpression[], memberType: MemberType]
9599
binary: [data: Buffer]
96100
date: [date: Date]
97-
fragment: [parts: TemplateStringsArray]
101+
fragment: [parts: TemplateStringsArray, ...values: readonly ValueExpression[]]
98102
identifier: [names: readonly string[]]
99103
interval: [interval: IntervalInput]
100104
join: [members: readonly ValueExpression[], glue: SqlFragment]

packages/client/test/api-usage.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ test('sql.join', async () => {
8484
expect(result).toEqual({id: 100, name: 'one hundred'})
8585
})
8686

87+
/**
88+
* Use `sql.fragment` to build reusable pieces which can be plugged into full queries.
89+
*/
90+
test('sql.fragment', async () => {
91+
const idGreaterThan = (id: number) => sql.fragment`id >= ${id}`
92+
const result = await client.any(sql`
93+
select * from usage_test where ${idGreaterThan(2)}
94+
`)
95+
96+
expect(result).toEqual([
97+
{id: 2, name: 'two'},
98+
{id: 3, name: 'three'},
99+
])
100+
})
101+
102+
/**
103+
* You can also use `` sql`...` `` to create a fragment of SQL, but it's recommended to use `sql.fragment` instead for explicitness.
104+
* Support for [type-generation](https://npmjs.com/package/@pgkit/typegen) is better using `sql.fragment` too.
105+
*/
106+
test('nested `sql` tag', async () => {
107+
const idGreaterThan = (id: number) => sql`id >= ${id}`
108+
const result = await client.any(sql`
109+
select * from usage_test where ${idGreaterThan(2)}
110+
`)
111+
112+
expect(result).toEqual([
113+
{id: 2, name: 'two'},
114+
{id: 3, name: 'three'},
115+
])
116+
})
117+
87118
/**
88119
* Lets you create reusable SQL fragments, for example a where clause. Note that right now, fragments do not allow parameters.
89120
*/

0 commit comments

Comments
 (0)