Skip to content

Commit

Permalink
feat: add literalValue (fixes #309, #150)
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Nov 7, 2021
1 parent 95db15d commit e79245f
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
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
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
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
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
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
@@ -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
@@ -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
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
@@ -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
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
@@ -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 e79245f

Please sign in to comment.