Skip to content

Commit

Permalink
Merge e8d8f10 into 95db15d
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Nov 7, 2021
2 parents 95db15d + e8d8f10 commit 9e2623f
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 5 deletions.
30 changes: 29 additions & 1 deletion .README/QUERY_BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,34 @@ sql`

```

### `sql.literalValue`

> ⚠️ Do not use. This method interpolates values as literals and it must be used only for [building utility statements](#slonik-recipes-building-utility-statements). You are most likely looking for [value placeholders](#slonik-value-placeholders).
```js
(
value: string,
) => SqlSqlTokenType;

```

Escapes and interpolates a literal value into a query.

```js
await connection.query(sql`
CREATE USER "foo" WITH PASSWORD ${sql.literalValue('bar')}
`);

```

Produces:

```js
{
sql: 'CREATE USER "foo" WITH PASSWORD \'bar\''
}

```

### `sql.unnest`

Expand Down Expand Up @@ -347,4 +375,4 @@ Produces:
]
}

```
```
19 changes: 19 additions & 0 deletions .README/RECIPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,22 @@ masterPool.query(sql`SELECT 1`);
masterPool.query(sql`UPDATE 1`);

```

### Building Utility Statements

Parameter symbols only work in optimizable SQL commands (SELECT, INSERT, UPDATE, DELETE, and certain commands containing one of these). In other statement types (generically called utility statements, e.g. ALTER, CREATE, DROP and SET), you must insert values textually even if they are just data values.

In the context of Slonik, if you are building utility statements you must use query building methods that interpolate values directly into queries:

* [`sql.identifier`](#slonik-query-building-sql-identifier) – for identifiers.
* [`sql.literalValue`](#slonik-query-building-sql-literalvalue) – for values.

Example:

```js
await connection.query(sql`
CREATE USER ${sql.identifier(['foo'])}
WITH PASSWORD ${sql.literalValue('bar')}
`);

```
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Note: Using this project does not require TypeScript. It is a regular ES6 module
* [Recipes](#slonik-recipes)
* [Inserting large number of rows](#slonik-recipes-inserting-large-number-of-rows)
* [Routing queries to different connections](#slonik-recipes-routing-queries-to-different-connections)
* [Building Utility Statements](#slonik-recipes-building-utility-statements)
* [`sql` tag](#slonik-sql-tag)
* [Value placeholders](#slonik-value-placeholders)
* [Tagged template literals](#slonik-value-placeholders-tagged-template-literals)
Expand All @@ -94,6 +95,7 @@ Note: Using this project does not require TypeScript. It is a regular ES6 module
* [`sql.identifier`](#slonik-query-building-sql-identifier)
* [`sql.json`](#slonik-query-building-sql-json)
* [`sql.join`](#slonik-query-building-sql-join)
* [`sql.literalValue`](#slonik-query-building-sql-literalvalue)
* [`sql.unnest`](#slonik-query-building-sql-unnest)
* [Query methods](#slonik-query-methods)
* [`any`](#slonik-query-methods-any)
Expand Down Expand Up @@ -1113,6 +1115,26 @@ masterPool.query(sql`UPDATE 1`);

```
<a name="slonik-recipes-building-utility-statements"></a>
### Building Utility Statements
Parameter symbols only work in optimizable SQL commands (SELECT, INSERT, UPDATE, DELETE, and certain commands containing one of these). In other statement types (generically called utility statements, e.g. ALTER, CREATE, DROP and SET), you must insert values textually even if they are just data values.
In the context of Slonik, if you are building utility statements you must use query building methods that interpolate values directly into queries:
* [`sql.identifier`](#slonik-query-building-sql-identifier) – for identifiers.
* [`sql.literalValue`](#slonik-query-building-sql-literalvalue) – for values.
Example:
```js
await connection.query(sql`
CREATE USER ${sql.identifier(['foo'])}
WITH PASSWORD ${sql.literalValue('bar')}
`);

```
<a name="slonik-sql-tag"></a>
## <code>sql</code> tag
Expand Down Expand Up @@ -1507,6 +1529,35 @@ sql`

```
<a name="slonik-query-building-sql-literalvalue"></a>
### <code>sql.literalValue</code>
> ⚠️ Do not use. This method interpolates values as literals and it must be used only for [building utility statements](#slonik-recipes-building-utility-statements). You are most likely looking for [value placeholders](#slonik-value-placeholders).
```js
(
value: string,
) => SqlSqlTokenType;

```
Escapes and interpolates a literal value into a query.
```js
await connection.query(sql`
CREATE USER "foo" WITH PASSWORD ${sql.literalValue('bar')}
`);

```
Produces:
```js
{
sql: 'CREATE USER "foo" WITH PASSWORD \'bar\''
}

```
<a name="slonik-query-building-sql-unnest"></a>
### <code>sql.unnest</code>
Expand Down Expand Up @@ -1596,6 +1647,7 @@ Produces:

```
<a name="slonik-query-methods"></a>
## Query methods
Expand Down
11 changes: 11 additions & 0 deletions src/factories/createSqlTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
ValueExpressionType,
} from '../types';
import {
escapeLiteralValue,
isPrimitiveValueExpression,
isSqlToken,
} from '../utilities';
Expand Down Expand Up @@ -153,6 +154,16 @@ sql.join = (
};
};

sql.literalValue = (
value: string,
): SqlSqlTokenType => {
return {
sql: escapeLiteralValue(value),
type: SqlToken,
values: [],
};
};

sql.unnest = (
tuples: ReadonlyArray<readonly PrimitiveValueExpressionType[]>,
columnTypes: readonly UnnestSqlColumnType[],
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export type SqlTaggedTemplateType<T extends UserQueryResultRowType = QueryResult
identifier: (names: readonly string[]) => IdentifierSqlTokenType,
json: (value: SerializableValueType) => JsonSqlTokenType,
join: (members: readonly ValueExpressionType[], glue: SqlTokenType) => ListSqlTokenType,
literalValue: (value: string) => SqlSqlTokenType,
unnest: (
// Value might be $ReadOnlyArray<$ReadOnlyArray<PrimitiveValueExpressionType>>,
// or it can be infinitely nested array, e.g.
Expand Down
4 changes: 3 additions & 1 deletion src/utilities/escapeIdentifier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const rule = /"/g;

/**
* @see https://github.com/brianc/node-postgres/blob/6c840aabb09f8a2d640800953f6b884b6841384c/lib/client.js#L306-L322
*/
export const escapeIdentifier = (identifier: string): string => {
return '"' + identifier.replace(/"/g, '""') + '"';
return '"' + identifier.replace(rule, '""') + '"';
};
26 changes: 26 additions & 0 deletions src/utilities/escapeLiteralValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @see https://github.com/brianc/node-postgres/blob/6c840aabb09f8a2d640800953f6b884b6841384c/lib/client.js#L325-L348
*/
export const escapeLiteralValue = (subject: string): string => {
let hasBackslash = false;
let escaped = '\'';

for (const character of subject) {
if (character === '\'') {
escaped += character + character;
} else if (character === '\\') {
escaped += character + character;
hasBackslash = true;
} else {
escaped += character;
}
}

escaped += '\'';

if (hasBackslash === true) {
escaped = 'E' + escaped;
}

return escaped;
};
3 changes: 3 additions & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export {
export {
escapeIdentifier,
} from './escapeIdentifier';
export {
escapeLiteralValue,
} from './escapeLiteralValue';
export {
isPrimitiveValueExpression,
} from './isPrimitiveValueExpression';
Expand Down
19 changes: 19 additions & 0 deletions test/slonik/templateTags/sql/literalValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import test from 'ava';
import {
createSqlTag,
} from '../../../../src/factories/createSqlTag';
import {
SqlToken,
} from '../../../../src/tokens';

const sql = createSqlTag();

test('creates an object describing a query with an inlined literal value', (t) => {
const query = sql`CREATE USER foo WITH PASSWORD ${sql.literalValue('bar')}`;

t.deepEqual(query, {
sql: 'CREATE USER foo WITH PASSWORD \'bar\'',
type: SqlToken,
values: [],
});
});
6 changes: 3 additions & 3 deletions test/slonik/utilities/escapeIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from '../../../src/utilities';

test('escapes SQL identifiers', (t) => {
t.assert(escapeIdentifier('foo') === '"foo"');
t.assert(escapeIdentifier('foo bar') === '"foo bar"');
t.assert(escapeIdentifier('"foo"') === '"""foo"""');
t.is(escapeIdentifier('foo'), '"foo"');
t.is(escapeIdentifier('foo bar'), '"foo bar"');
t.is(escapeIdentifier('"foo"'), '"""foo"""');
});
11 changes: 11 additions & 0 deletions test/slonik/utilities/escapeLiteralValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import test from 'ava';
import {
escapeLiteralValue,
} from '../../../src/utilities';

test('escapes SQL literal value', (t) => {
t.is(escapeLiteralValue('foo'), '\'foo\'');
t.is(escapeLiteralValue('foo bar'), '\'foo bar\'');
t.is(escapeLiteralValue('"foo"'), '\'"foo"\'');
t.is(escapeLiteralValue('foo\\bar'), 'E\'foo\\\\bar\'');
});

0 comments on commit 9e2623f

Please sign in to comment.