Skip to content

Commit

Permalink
feat: adds support for SqlSqlToken as a memberType in sql.unnest (fixes
Browse files Browse the repository at this point in the history
#326) (#332)

* corrects invalid references to SqlToken when intention is SqlSqlToken
* adds support for SqlSqlToken as a memberType in sql.unnest
  • Loading branch information
gajus committed Apr 1, 2022
1 parent 476a856 commit ec950da
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 71 deletions.
58 changes: 48 additions & 10 deletions .README/QUERY_BUILDING.md
Expand Up @@ -214,8 +214,8 @@ Produces:

```js
(
members: SqlToken[],
glue: SqlToken
members: SqlSqlToken[],
glue: SqlSqlToken
) => ListSqlToken;

```
Expand Down Expand Up @@ -317,8 +317,8 @@ Produces:

```js
(
tuples: PrimitiveValueExpression[][],
columnTypes: (string | string[])[]
tuples: ReadonlyArray<readonly any[]>,
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>
): UnnestSqlToken;

```
Expand Down Expand Up @@ -346,7 +346,7 @@ Produces:

```js
{
sql: 'SELECT bar, baz FROM unnest($1::int4[], $2::text[]) AS foo(bar, baz)',
sql: 'SELECT bar, baz FROM unnest($1::"int4"[], $2::"text"[]) AS foo(bar, baz)',
values: [
[
1,
Expand All @@ -361,8 +361,47 @@ Produces:

```

if `columnType` type is `string[][]`, it will act similar as [`sql.identifier`](#sqlidentifier), e.g.
If `columnType` array member type is `string`, it will treat it as a type name identifier (and quote with double quotes; illustrated in the example above).

If `columnType` array member type is `SqlToken`, it will inline type name without quotes, e.g.

```js
await connection.query(sql`
SELECT bar, baz
FROM ${sql.unnest(
[
[1, 'foo'],
[2, 'bar']
],
[
sql`integer`,
sql`text`
]
)} AS foo(bar, baz)
`);

```

Produces:

```js
{
sql: 'SELECT bar, baz FROM unnest($1::integer[], $2::text[]) AS foo(bar, baz)',
values: [
[
1,
2
],
[
'foo',
'bar'
]
]
}

```

If `columnType` array member type is `[...string[], TypeNameIdentifier]`, it will act as [`sql.identifier`](#sqlidentifier), e.g.

```js
await connection.query(sql`
Expand All @@ -373,8 +412,8 @@ await connection.query(sql`
[2, 4]
],
[
['foo', 'level'],
['foo', 'score']
['foo', 'int4'],
['foo', 'int4']
]
)} AS foo(bar, baz)
`);
Expand All @@ -385,7 +424,7 @@ Produces:

```js
{
sql: 'SELECT bar, baz FROM unnest($1::"foo"."level"[], $2::"foo"."score"[]) AS foo(bar, baz)',
sql: 'SELECT bar, baz FROM unnest($1::"foo"."int4"[], $2::"foo"."int4"[]) AS foo(bar, baz)',
values: [
[
1,
Expand All @@ -397,5 +436,4 @@ Produces:
]
]
}

```
58 changes: 48 additions & 10 deletions README.md
Expand Up @@ -1565,8 +1565,8 @@ Produces:
```js
(
members: SqlToken[],
glue: SqlToken
members: SqlSqlToken[],
glue: SqlSqlToken
) => ListSqlToken;

```
Expand Down Expand Up @@ -1672,8 +1672,8 @@ Produces:
```js
(
tuples: PrimitiveValueExpression[][],
columnTypes: (string | string[])[]
tuples: ReadonlyArray<readonly any[]>,
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>
): UnnestSqlToken;

```
Expand Down Expand Up @@ -1701,7 +1701,7 @@ Produces:
```js
{
sql: 'SELECT bar, baz FROM unnest($1::int4[], $2::text[]) AS foo(bar, baz)',
sql: 'SELECT bar, baz FROM unnest($1::"int4"[], $2::"text"[]) AS foo(bar, baz)',
values: [
[
1,
Expand All @@ -1716,8 +1716,47 @@ Produces:

```
if `columnType` type is `string[][]`, it will act similar as [`sql.identifier`](#user-content-sqlidentifier), e.g.
If `columnType` array member type is `string`, it will treat it as a type name identifier (and quote with double quotes; illustrated in the example above).
If `columnType` array member type is `SqlToken`, it will inline type name without quotes, e.g.
```js
await connection.query(sql`
SELECT bar, baz
FROM ${sql.unnest(
[
[1, 'foo'],
[2, 'bar']
],
[
sql`integer`,
sql`text`
]
)} AS foo(bar, baz)
`);

```
Produces:
```js
{
sql: 'SELECT bar, baz FROM unnest($1::integer[], $2::text[]) AS foo(bar, baz)',
values: [
[
1,
2
],
[
'foo',
'bar'
]
]
}

```
If `columnType` array member type is `[...string[], TypeNameIdentifier]`, it will act as [`sql.identifier`](#user-content-sqlidentifier), e.g.
```js
await connection.query(sql`
Expand All @@ -1728,8 +1767,8 @@ await connection.query(sql`
[2, 4]
],
[
['foo', 'level'],
['foo', 'score']
['foo', 'int4'],
['foo', 'int4']
]
)} AS foo(bar, baz)
`);
Expand All @@ -1740,7 +1779,7 @@ Produces:
```js
{
sql: 'SELECT bar, baz FROM unnest($1::"foo"."level"[], $2::"foo"."score"[]) AS foo(bar, baz)',
sql: 'SELECT bar, baz FROM unnest($1::"foo"."int4"[], $2::"foo"."int4"[]) AS foo(bar, baz)',
values: [
[
1,
Expand All @@ -1752,7 +1791,6 @@ Produces:
]
]
}

```
Expand Down
5 changes: 2 additions & 3 deletions src/factories/createSqlTag.ts
Expand Up @@ -29,7 +29,6 @@ import type {
SqlSqlToken,
SqlTaggedTemplate,
TypeNameIdentifier,
UnnestSqlColumn,
UnnestSqlToken,
ValueExpression,
} from '../types';
Expand Down Expand Up @@ -110,7 +109,7 @@ const sql: SqlTaggedTemplate = (

sql.array = (
values: readonly PrimitiveValueExpression[],
memberType: SqlTokenType | TypeNameIdentifier | string,
memberType: SqlTokenType | TypeNameIdentifier,
): ArraySqlToken => {
return {
memberType,
Expand Down Expand Up @@ -178,7 +177,7 @@ sql.literalValue = (

sql.unnest = (
tuples: ReadonlyArray<readonly PrimitiveValueExpression[]>,
columnTypes: readonly UnnestSqlColumn[],
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>,
): UnnestSqlToken => {
return {
columnTypes,
Expand Down
18 changes: 13 additions & 5 deletions src/sqlFragmentFactories/createUnnestSqlFragment.ts
Expand Up @@ -28,19 +28,27 @@ export const createUnnestSqlFragment = (token: UnnestSqlToken, greatestParameter
let placeholderIndex = greatestParameterPosition;

while (columnIndex < columnTypes.length) {
const typeMember = columnTypes[columnIndex];

let columnType = columnTypes[columnIndex];
let columnTypeIsIdentifier = typeof columnType !== 'string';
let columnTypeIsIdentifier;

if (typeof columnType !== 'string') {
columnTypeIsIdentifier = true;
columnType = columnType.map((identifierName) => {
if (typeof typeMember === 'string') {
columnType = typeMember;
columnTypeIsIdentifier = false;
} else if (Array.isArray(typeMember)) {
columnType = typeMember.map((identifierName) => {
if (typeof identifierName !== 'string') {
throw new InvalidInputError('sql.unnest column identifier name array member type must be a string.');
throw new InvalidInputError('sql.unnest column identifier name array member type must be a string (type name identifier) or a SQL token.');
}

return escapeIdentifier(identifierName);
})
.join('.');
columnTypeIsIdentifier = true;
} else {
columnType = typeMember.sql;
columnTypeIsIdentifier = true;
}

unnestSqlTokens.push(
Expand Down
13 changes: 8 additions & 5 deletions src/types.ts
Expand Up @@ -34,7 +34,12 @@ export type ConnectionOptions = {
username?: string,
};

/**
* "string" type covers all type name identifiers – the literal values are added only to assist developer
* experience with auto suggestions for commonly used type name identifiers.
*/
export type TypeNameIdentifier =
| string
| 'bool'
| 'bytea'
| 'float4'
Expand Down Expand Up @@ -267,7 +272,7 @@ export type QueryContext = {
};

export type ArraySqlToken = {
readonly memberType: SqlToken | TypeNameIdentifier | string,
readonly memberType: SqlToken | TypeNameIdentifier,
readonly type: typeof tokens.ArrayToken,
readonly values: readonly PrimitiveValueExpression[],
};
Expand Down Expand Up @@ -304,10 +309,8 @@ export type SqlSqlToken = {
readonly values: readonly PrimitiveValueExpression[],
};

export type UnnestSqlColumn = string | readonly string[];

export type UnnestSqlToken = {
readonly columnTypes: readonly UnnestSqlColumn[],
readonly columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>,
readonly tuples: ReadonlyArray<readonly ValueExpression[]>,
readonly type: typeof tokens.UnnestToken,
};
Expand Down Expand Up @@ -358,7 +361,7 @@ export type SqlTaggedTemplate<T extends UserQueryResultRow = QueryResultRow> = {
// https://github.com/gajus/slonik/issues/44
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tuples: ReadonlyArray<readonly any[]>,
columnTypes: readonly UnnestSqlColumn[],
columnTypes: Array<[...string[], TypeNameIdentifier]> | Array<SqlSqlToken | TypeNameIdentifier>
) => UnnestSqlToken,
};

Expand Down

0 comments on commit ec950da

Please sign in to comment.