Skip to content

Commit 249182e

Browse files
authored
Document client.escapeIdentifier and client.escapeLiteral (#2954)
* Document client.escapeIdentifier and client.escapeLiteral Per #1978 it seems that these client APIs are undocumented. Added documentation for these functions along with some examples and relevant links. * Fix typos in new docs * Migrate escapeIdentifier and escapeLiteral from Client to PG These are standalone utility functions, they do not need a client instance to function. Changes made: - Refactored escapeIdentifer and escapeLiteral from client class to functions in utils - Update PG to export escapeIdentifier and escapeLiteral - Migrated tests for Client.escapeIdentifier and Client.escapeLiteral to tests for utils - Updated documentation, added a "utilities" page where these helpers are discussed **note** this is a breaking change. Users who used these functions (previously undocumented) on instances of Client, or via Client.prototype. * Export escapeIdentifier and escapeLiteral from PG These are standalone utility functions, they should not depend on a client instance. Changes made: - Refactored escapeIdentifer and escapeLiteral from client class to functions in utils - Re-exported functions on client for backwards compatibility - Update PG to export escapeIdentifier and escapeLiteral - Updated tests to validate the newly exported functions from both entry points - Updated documentation, added a "utilities" page where these helpers are discussed * Ensure escape functions work via Client.prototype Updated changes such that escapeIdentifier and escapeLiteral are usable via the client prototype Updated tests to check for both entry points in client
1 parent d63c761 commit 249182e

File tree

7 files changed

+150
-26
lines changed

7 files changed

+150
-26
lines changed

Diff for: docs/pages/apis/_meta.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"pool": "pg.Pool",
44
"result": "pg.Result",
55
"types": "pg.Types",
6-
"cursor": "Cursor"
6+
"cursor": "Cursor",
7+
"utilities": "Utilities"
78
}

Diff for: docs/pages/apis/utilities.mdx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: Utilities
3+
---
4+
import { Alert } from '/components/alert.tsx'
5+
6+
## Utility Functions
7+
### pg.escapeIdentifier
8+
9+
Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS).
10+
11+
```js
12+
const { escapeIdentifier } = require('pg')
13+
const escapedIdentifier = escapeIdentifier('FooIdentifier')
14+
console.log(escapedIdentifier) // '"FooIdentifier"'
15+
```
16+
17+
18+
### pg.escapeLiteral
19+
20+
<Alert>
21+
**Note**: Instead of manually escaping SQL literals, it is recommended to use parameterized queries. Refer to [parameterized queries](/features/queries#parameterized-query) and the [client.query](/apis/client#clientquery) API for more information.
22+
</Alert>
23+
24+
Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS).
25+
26+
```js
27+
const { escapeLiteral } = require('pg')
28+
const escapedLiteral = escapeLiteral("hello 'world'")
29+
console.log(escapedLiteral) // "'hello ''world'''"
30+
```

Diff for: packages/pg/lib/client.js

+5-25
Original file line numberDiff line numberDiff line change
@@ -456,35 +456,15 @@ class Client extends EventEmitter {
456456
return this._types.getTypeParser(oid, format)
457457
}
458458

459-
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
459+
// escapeIdentifier and escapeLiteral moved to utility functions & exported
460+
// on PG
461+
// re-exported here for backwards compatibility
460462
escapeIdentifier(str) {
461-
return '"' + str.replace(/"/g, '""') + '"'
463+
return utils.escapeIdentifier(str)
462464
}
463465

464-
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
465466
escapeLiteral(str) {
466-
var hasBackslash = false
467-
var escaped = "'"
468-
469-
for (var i = 0; i < str.length; i++) {
470-
var c = str[i]
471-
if (c === "'") {
472-
escaped += c + c
473-
} else if (c === '\\') {
474-
escaped += c + c
475-
hasBackslash = true
476-
} else {
477-
escaped += c
478-
}
479-
}
480-
481-
escaped += "'"
482-
483-
if (hasBackslash === true) {
484-
escaped = ' E' + escaped
485-
}
486-
487-
return escaped
467+
return utils.escapeLiteral(str)
488468
}
489469

490470
_pulseQueryQueue() {

Diff for: packages/pg/lib/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var defaults = require('./defaults')
55
var Connection = require('./connection')
66
var Pool = require('pg-pool')
77
const { DatabaseError } = require('pg-protocol')
8+
const { escapeIdentifier, escapeLiteral } = require('./utils')
89

910
const poolFactory = (Client) => {
1011
return class BoundPool extends Pool {
@@ -23,6 +24,8 @@ var PG = function (clientConstructor) {
2324
this.Connection = Connection
2425
this.types = require('pg-types')
2526
this.DatabaseError = DatabaseError
27+
this.escapeIdentifier = escapeIdentifier
28+
this.escapeLiteral = escapeLiteral
2629
}
2730

2831
if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') {

Diff for: packages/pg/lib/utils.js

+34
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,38 @@ const postgresMd5PasswordHash = function (user, password, salt) {
175175
return 'md5' + outer
176176
}
177177

178+
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
179+
const escapeIdentifier = function (str) {
180+
return '"' + str.replace(/"/g, '""') + '"'
181+
}
182+
183+
const escapeLiteral = function (str) {
184+
var hasBackslash = false
185+
var escaped = "'"
186+
187+
for (var i = 0; i < str.length; i++) {
188+
var c = str[i]
189+
if (c === "'") {
190+
escaped += c + c
191+
} else if (c === '\\') {
192+
escaped += c + c
193+
hasBackslash = true
194+
} else {
195+
escaped += c
196+
}
197+
}
198+
199+
escaped += "'"
200+
201+
if (hasBackslash === true) {
202+
escaped = ' E' + escaped
203+
}
204+
205+
return escaped
206+
}
207+
208+
209+
178210
module.exports = {
179211
prepareValue: function prepareValueWrapper(value) {
180212
// this ensures that extra arguments do not get passed into prepareValue
@@ -184,4 +216,6 @@ module.exports = {
184216
normalizeQueryConfig,
185217
postgresMd5PasswordHash,
186218
md5,
219+
escapeIdentifier,
220+
escapeLiteral
187221
}

Diff for: packages/pg/test/unit/client/escape-tests.js

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22
var helper = require('./test-helper')
3+
var utils = require('../../../lib/utils')
34

45
function createClient(callback) {
56
var client = new Client(helper.config)
@@ -14,6 +15,17 @@ var testLit = function (testName, input, expected) {
1415
var actual = client.escapeLiteral(input)
1516
assert.equal(expected, actual)
1617
})
18+
19+
test('Client.prototype.' + testName, function () {
20+
var actual = Client.prototype.escapeLiteral(input)
21+
assert.equal(expected, actual)
22+
})
23+
24+
25+
test('utils.' + testName, function () {
26+
var actual = utils.escapeLiteral(input)
27+
assert.equal(expected, actual)
28+
})
1729
}
1830

1931
var testIdent = function (testName, input, expected) {
@@ -22,6 +34,17 @@ var testIdent = function (testName, input, expected) {
2234
var actual = client.escapeIdentifier(input)
2335
assert.equal(expected, actual)
2436
})
37+
38+
test('Client.prototype.' + testName, function () {
39+
var actual = Client.prototype.escapeIdentifier(input)
40+
assert.equal(expected, actual)
41+
})
42+
43+
44+
test('utils.' + testName, function () {
45+
var actual = utils.escapeIdentifier(input)
46+
assert.equal(expected, actual)
47+
})
2548
}
2649

2750
testLit('escapeLiteral: no special characters', 'hello world', "'hello world'")

Diff for: packages/pg/test/unit/utils-tests.js

+53
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,56 @@ test('prepareValue: can safely be used to map an array of values including those
239239
var out = values.map(utils.prepareValue)
240240
assert.deepEqual(out, [1, 'test', 'zomgcustom!'])
241241
})
242+
243+
var testEscapeLiteral = function (testName, input, expected) {
244+
test(testName, function () {
245+
var actual = utils.escapeLiteral(input)
246+
assert.equal(expected, actual)
247+
})
248+
}
249+
testEscapeLiteral('escapeLiteral: no special characters', 'hello world', "'hello world'")
250+
251+
testEscapeLiteral('escapeLiteral: contains double quotes only', 'hello " world', "'hello \" world'")
252+
253+
testEscapeLiteral('escapeLiteral: contains single quotes only', "hello ' world", "'hello '' world'")
254+
255+
testEscapeLiteral('escapeLiteral: contains backslashes only', 'hello \\ world', " E'hello \\\\ world'")
256+
257+
testEscapeLiteral('escapeLiteral: contains single quotes and double quotes', 'hello \' " world', "'hello '' \" world'")
258+
259+
testEscapeLiteral('escapeLiteral: contains double quotes and backslashes', 'hello \\ " world', " E'hello \\\\ \" world'")
260+
261+
testEscapeLiteral('escapeLiteral: contains single quotes and backslashes', "hello \\ ' world", " E'hello \\\\ '' world'")
262+
263+
testEscapeLiteral(
264+
'escapeLiteral: contains single quotes, double quotes, and backslashes',
265+
'hello \\ \' " world',
266+
" E'hello \\\\ '' \" world'"
267+
)
268+
269+
var testEscapeIdentifier = function (testName, input, expected) {
270+
test(testName, function () {
271+
var actual = utils.escapeIdentifier(input)
272+
assert.equal(expected, actual)
273+
})
274+
}
275+
276+
testEscapeIdentifier('escapeIdentifier: no special characters', 'hello world', '"hello world"')
277+
278+
testEscapeIdentifier('escapeIdentifier: contains double quotes only', 'hello " world', '"hello "" world"')
279+
280+
testEscapeIdentifier('escapeIdentifier: contains single quotes only', "hello ' world", '"hello \' world"')
281+
282+
testEscapeIdentifier('escapeIdentifier: contains backslashes only', 'hello \\ world', '"hello \\ world"')
283+
284+
testEscapeIdentifier('escapeIdentifier: contains single quotes and double quotes', 'hello \' " world', '"hello \' "" world"')
285+
286+
testEscapeIdentifier('escapeIdentifier: contains double quotes and backslashes', 'hello \\ " world', '"hello \\ "" world"')
287+
288+
testEscapeIdentifier('escapeIdentifier: contains single quotes and backslashes', "hello \\ ' world", '"hello \\ \' world"')
289+
290+
testEscapeIdentifier(
291+
'escapeIdentifier: contains single quotes, double quotes, and backslashes',
292+
'hello \\ \' " world',
293+
'"hello \\ \' "" world"'
294+
)

0 commit comments

Comments
 (0)