diff --git a/docs/pages/apis/_meta.json b/docs/pages/apis/_meta.json index 0b6a193c7..67da94d93 100644 --- a/docs/pages/apis/_meta.json +++ b/docs/pages/apis/_meta.json @@ -3,5 +3,6 @@ "pool": "pg.Pool", "result": "pg.Result", "types": "pg.Types", - "cursor": "Cursor" + "cursor": "Cursor", + "utilities": "Utilities" } diff --git a/docs/pages/apis/utilities.mdx b/docs/pages/apis/utilities.mdx new file mode 100644 index 000000000..d33718081 --- /dev/null +++ b/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 + + + **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. + + +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'''" +``` diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 99c06d661..f2c339d37 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -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() { diff --git a/packages/pg/lib/index.js b/packages/pg/lib/index.js index 7f02abab5..1742d168a 100644 --- a/packages/pg/lib/index.js +++ b/packages/pg/lib/index.js @@ -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 { @@ -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') { diff --git a/packages/pg/lib/utils.js b/packages/pg/lib/utils.js index d63fe68f1..1b8fdaf46 100644 --- a/packages/pg/lib/utils.js +++ b/packages/pg/lib/utils.js @@ -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 @@ -184,4 +216,6 @@ module.exports = { normalizeQueryConfig, postgresMd5PasswordHash, md5, + escapeIdentifier, + escapeLiteral } diff --git a/packages/pg/test/unit/client/escape-tests.js b/packages/pg/test/unit/client/escape-tests.js index 721b04b49..68e233fbe 100644 --- a/packages/pg/test/unit/client/escape-tests.js +++ b/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) @@ -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) { @@ -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'") diff --git a/packages/pg/test/unit/utils-tests.js b/packages/pg/test/unit/utils-tests.js index 3d087ad0d..b8ce47ec0 100644 --- a/packages/pg/test/unit/utils-tests.js +++ b/packages/pg/test/unit/utils-tests.js @@ -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"' +)