diff --git a/packages/client/readme.md b/packages/client/readme.md index 27860819..cbd0a6da 100644 --- a/packages/client/readme.md +++ b/packages/client/readme.md @@ -44,6 +44,8 @@ Note that @pgkit/migra and @pgkit/schemainspect are pure ports of their Python e - [sql.unnest](#sqlunnest) - [sql.join](#sqljoin) - [sql.fragment](#sqlfragment) + - [nested `sql` tag](#nested-sql-tag) + - [sql.fragment](#sqlfragment-1) - [sql.interval](#sqlinterval) - [sql.binary](#sqlbinary) - [sql.json](#sqljson) @@ -269,6 +271,38 @@ expect(result).toEqual({id: 100, name: 'one hundred'}) ### sql.fragment +Use `sql.fragment` to build reusable pieces which can be plugged into full queries. + +```typescript +const idGreaterThan = (id: number) => sql.fragment`id >= ${id}` +const result = await client.any(sql` + select * from usage_test where ${idGreaterThan(2)} +`) + +expect(result).toEqual([ + {id: 2, name: 'two'}, + {id: 3, name: 'three'}, +]) +``` + +### nested `sql` tag + +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. + +```typescript +const idGreaterThan = (id: number) => sql`id >= ${id}` +const result = await client.any(sql` + select * from usage_test where ${idGreaterThan(2)} +`) + +expect(result).toEqual([ + {id: 2, name: 'two'}, + {id: 3, name: 'three'}, +]) +``` + +### sql.fragment + Lets you create reusable SQL fragments, for example a where clause. Note that right now, fragments do not allow parameters. ```typescript @@ -895,6 +929,7 @@ Generally, usage of a _client_ (or pool, to use the slonik term), should be iden - no `stream` support yet - See [future](#👽-future) for more details/which parity features are planned + ### Added features/improvements #### `sql` diff --git a/packages/client/src/sql.ts b/packages/client/src/sql.ts index 024b4605..915d70be 100644 --- a/packages/client/src/sql.ts +++ b/packages/client/src/sql.ts @@ -10,6 +10,7 @@ const sqlMethodHelpers: SQLMethodHelpers = { name: nameQuery([query]), token: 'sql', values: [], + templateArgs: () => [[query]], }), type: type => @@ -31,6 +32,7 @@ const sqlMethodHelpers: SQLMethodHelpers = { sql: strings.join(''), token: 'sql', values: parameters, + templateArgs: () => [strings, ...parameters], } }, } @@ -99,14 +101,14 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => { } case 'sql': { - if (param.values?.length) { - throw new QueryError(`Can't handle nested SQL with parameters`, { - cause: {query: {name: nameQuery(strings), sql, values: inputParameters}}, - }) + const [parts, ...fragmentValues] = param.templateArgs() + for (let i = 0; i < parts.length; i++) { + sql += parts[i] + if (i < fragmentValues.length) { + values.push(fragmentValues[i]) + sql += '$' + String(i + 1) + } } - - sql += param.sql - // values.push(...param.values); break } @@ -132,7 +134,14 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => { } case 'fragment': { - sql += param.args[0][0] + const [parts, ...fragmentValues] = param.args + for (let i = 0; i < parts.length; i++) { + sql += parts[i] + if (i < fragmentValues.length) { + values.push(fragmentValues[i]) + sql += '$' + String(i + 1) + } + } break } @@ -155,6 +164,7 @@ const sqlFn: SQLTagFunction = (strings, ...inputParameters) => { sql, token: 'sql', values, + templateArgs: () => [strings, ...inputParameters], } } diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index db3df96a..a48abf22 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -6,6 +6,8 @@ export interface SQLQuery, Values extends unkno sql: string values: Values parse: (input: unknown) => Result + /** @internal */ + templateArgs: () => [strings: readonly string[], ...inputParameters: readonly any[]] } export type TimeUnit = 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' @@ -62,6 +64,8 @@ export type SqlFragment = { token: 'sql' sql: string values: unknown[] + /** @internal */ + templateArgs: () => [strings: readonly string[], ...inputParameters: readonly any[]] } /** * "string" type covers all type name identifiers – the literal values are added only to assist developer @@ -94,7 +98,7 @@ export type SQLTagHelperParameters = { array: [values: readonly PrimitiveValueExpression[], memberType: MemberType] binary: [data: Buffer] date: [date: Date] - fragment: [parts: TemplateStringsArray] + fragment: [parts: TemplateStringsArray, ...values: readonly ValueExpression[]] identifier: [names: readonly string[]] interval: [interval: IntervalInput] join: [members: readonly ValueExpression[], glue: SqlFragment] diff --git a/packages/client/test/api-usage.test.ts b/packages/client/test/api-usage.test.ts index d211f74e..9b8d3805 100644 --- a/packages/client/test/api-usage.test.ts +++ b/packages/client/test/api-usage.test.ts @@ -84,6 +84,37 @@ test('sql.join', async () => { expect(result).toEqual({id: 100, name: 'one hundred'}) }) +/** + * Use `sql.fragment` to build reusable pieces which can be plugged into full queries. + */ +test('sql.fragment', async () => { + const idGreaterThan = (id: number) => sql.fragment`id >= ${id}` + const result = await client.any(sql` + select * from usage_test where ${idGreaterThan(2)} + `) + + expect(result).toEqual([ + {id: 2, name: 'two'}, + {id: 3, name: 'three'}, + ]) +}) + +/** + * 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. + */ +test('nested `sql` tag', async () => { + const idGreaterThan = (id: number) => sql`id >= ${id}` + const result = await client.any(sql` + select * from usage_test where ${idGreaterThan(2)} + `) + + expect(result).toEqual([ + {id: 2, name: 'two'}, + {id: 3, name: 'three'}, + ]) +}) + /** * Lets you create reusable SQL fragments, for example a where clause. Note that right now, fragments do not allow parameters. */