Skip to content

Commit

Permalink
Document client.escapeIdentifier and client.escapeLiteral (#2954)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TheConner committed May 2, 2023
1 parent d63c761 commit 249182e
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 26 deletions.
3 changes: 2 additions & 1 deletion docs/pages/apis/_meta.json
Expand Up @@ -3,5 +3,6 @@
"pool": "pg.Pool",
"result": "pg.Result",
"types": "pg.Types",
"cursor": "Cursor"
"cursor": "Cursor",
"utilities": "Utilities"
}
30 changes: 30 additions & 0 deletions docs/pages/apis/utilities.mdx
@@ -0,0 +1,30 @@
---
title: Utilities
---
import { Alert } from '/components/alert.tsx'

## Utility Functions
### pg.escapeIdentifier

Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS).

```js
const { escapeIdentifier } = require('pg')
const escapedIdentifier = escapeIdentifier('FooIdentifier')
console.log(escapedIdentifier) // '"FooIdentifier"'
```


### pg.escapeLiteral

<Alert>
**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.
</Alert>

Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS).

```js
const { escapeLiteral } = require('pg')
const escapedLiteral = escapeLiteral("hello 'world'")
console.log(escapedLiteral) // "'hello ''world'''"
```
30 changes: 5 additions & 25 deletions packages/pg/lib/client.js
Expand Up @@ -456,35 +456,15 @@ class Client extends EventEmitter {
return this._types.getTypeParser(oid, format)
}

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

// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
escapeLiteral(str) {
var hasBackslash = false
var escaped = "'"

for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === "'") {
escaped += c + c
} else if (c === '\\') {
escaped += c + c
hasBackslash = true
} else {
escaped += c
}
}

escaped += "'"

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

return escaped
return utils.escapeLiteral(str)
}

_pulseQueryQueue() {
Expand Down
3 changes: 3 additions & 0 deletions packages/pg/lib/index.js
Expand Up @@ -5,6 +5,7 @@ var defaults = require('./defaults')
var Connection = require('./connection')
var Pool = require('pg-pool')
const { DatabaseError } = require('pg-protocol')
const { escapeIdentifier, escapeLiteral } = require('./utils')

const poolFactory = (Client) => {
return class BoundPool extends Pool {
Expand All @@ -23,6 +24,8 @@ var PG = function (clientConstructor) {
this.Connection = Connection
this.types = require('pg-types')
this.DatabaseError = DatabaseError
this.escapeIdentifier = escapeIdentifier
this.escapeLiteral = escapeLiteral
}

if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') {
Expand Down
34 changes: 34 additions & 0 deletions packages/pg/lib/utils.js
Expand Up @@ -175,6 +175,38 @@ const postgresMd5PasswordHash = function (user, password, salt) {
return 'md5' + outer
}

// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
const escapeIdentifier = function (str) {
return '"' + str.replace(/"/g, '""') + '"'
}

const escapeLiteral = function (str) {
var hasBackslash = false
var escaped = "'"

for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === "'") {
escaped += c + c
} else if (c === '\\') {
escaped += c + c
hasBackslash = true
} else {
escaped += c
}
}

escaped += "'"

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

return escaped
}



module.exports = {
prepareValue: function prepareValueWrapper(value) {
// this ensures that extra arguments do not get passed into prepareValue
Expand All @@ -184,4 +216,6 @@ module.exports = {
normalizeQueryConfig,
postgresMd5PasswordHash,
md5,
escapeIdentifier,
escapeLiteral
}
23 changes: 23 additions & 0 deletions packages/pg/test/unit/client/escape-tests.js
@@ -1,5 +1,6 @@
'use strict'
var helper = require('./test-helper')
var utils = require('../../../lib/utils')

function createClient(callback) {
var client = new Client(helper.config)
Expand All @@ -14,6 +15,17 @@ var testLit = function (testName, input, expected) {
var actual = client.escapeLiteral(input)
assert.equal(expected, actual)
})

test('Client.prototype.' + testName, function () {
var actual = Client.prototype.escapeLiteral(input)
assert.equal(expected, actual)
})


test('utils.' + testName, function () {
var actual = utils.escapeLiteral(input)
assert.equal(expected, actual)
})
}

var testIdent = function (testName, input, expected) {
Expand All @@ -22,6 +34,17 @@ var testIdent = function (testName, input, expected) {
var actual = client.escapeIdentifier(input)
assert.equal(expected, actual)
})

test('Client.prototype.' + testName, function () {
var actual = Client.prototype.escapeIdentifier(input)
assert.equal(expected, actual)
})


test('utils.' + testName, function () {
var actual = utils.escapeIdentifier(input)
assert.equal(expected, actual)
})
}

testLit('escapeLiteral: no special characters', 'hello world', "'hello world'")
Expand Down
53 changes: 53 additions & 0 deletions packages/pg/test/unit/utils-tests.js
Expand Up @@ -239,3 +239,56 @@ test('prepareValue: can safely be used to map an array of values including those
var out = values.map(utils.prepareValue)
assert.deepEqual(out, [1, 'test', 'zomgcustom!'])
})

var testEscapeLiteral = function (testName, input, expected) {
test(testName, function () {
var actual = utils.escapeLiteral(input)
assert.equal(expected, actual)
})
}
testEscapeLiteral('escapeLiteral: no special characters', 'hello world', "'hello world'")

testEscapeLiteral('escapeLiteral: contains double quotes only', 'hello " world', "'hello \" world'")

testEscapeLiteral('escapeLiteral: contains single quotes only', "hello ' world", "'hello '' world'")

testEscapeLiteral('escapeLiteral: contains backslashes only', 'hello \\ world', " E'hello \\\\ world'")

testEscapeLiteral('escapeLiteral: contains single quotes and double quotes', 'hello \' " world', "'hello '' \" world'")

testEscapeLiteral('escapeLiteral: contains double quotes and backslashes', 'hello \\ " world', " E'hello \\\\ \" world'")

testEscapeLiteral('escapeLiteral: contains single quotes and backslashes', "hello \\ ' world", " E'hello \\\\ '' world'")

testEscapeLiteral(
'escapeLiteral: contains single quotes, double quotes, and backslashes',
'hello \\ \' " world',
" E'hello \\\\ '' \" world'"
)

var testEscapeIdentifier = function (testName, input, expected) {
test(testName, function () {
var actual = utils.escapeIdentifier(input)
assert.equal(expected, actual)
})
}

testEscapeIdentifier('escapeIdentifier: no special characters', 'hello world', '"hello world"')

testEscapeIdentifier('escapeIdentifier: contains double quotes only', 'hello " world', '"hello "" world"')

testEscapeIdentifier('escapeIdentifier: contains single quotes only', "hello ' world", '"hello \' world"')

testEscapeIdentifier('escapeIdentifier: contains backslashes only', 'hello \\ world', '"hello \\ world"')

testEscapeIdentifier('escapeIdentifier: contains single quotes and double quotes', 'hello \' " world', '"hello \' "" world"')

testEscapeIdentifier('escapeIdentifier: contains double quotes and backslashes', 'hello \\ " world', '"hello \\ "" world"')

testEscapeIdentifier('escapeIdentifier: contains single quotes and backslashes', "hello \\ ' world", '"hello \\ \' world"')

testEscapeIdentifier(
'escapeIdentifier: contains single quotes, double quotes, and backslashes',
'hello \\ \' " world',
'"hello \\ \' "" world"'
)

0 comments on commit 249182e

Please sign in to comment.