From 563c3483e9df0efa34eb7c097ac3cb7c8eeb2697 Mon Sep 17 00:00:00 2001 From: Conner Bradley Date: Mon, 10 Apr 2023 11:01:19 -0400 Subject: [PATCH 1/5] 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. --- docs/pages/apis/client.mdx | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index d5f335240..af87eab40 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -1,6 +1,7 @@ --- title: pg.Client --- +import { Alert } from '/components/alert.tsx' ## new Client @@ -256,6 +257,44 @@ client _note: end returning a promise is only available in pg7.0 and above_ +## client.escapeIdentifier + +Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS). There are two ways of using this method: + +First, this can be used on an existing instance of `Client`: +```js +const escpaedIdentifier = client.escapeIdentifier('FooIdentifier') +console.log(escpaedIdentifier) // '"FooIdentifier"' +``` + +Alternatively, this can be used via `Client.prototype`: +```js +const { Client } = require('pg') +const escpaedIdentifier = Client.prototype.escapeIdentifier('Bar"Identifier') +console.log(escpaedIdentifier) // '"Bar""Identifier"' +``` + +## client.escapeLiteral + +Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS). There are two ways of using this method: + + + **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](#clientquery) API for more information. + + +First, this can be used on an existing instance of `Client`: +```js +const escapedLiteral = client.escapeLiteral("hello 'world'") +console.log(escapedLiteral) // "Hello ''world''" +``` + +Alternatively, this can be used via `Client.prototype`: +```js +const { Client } = require('pg') +const escapedLiteral = Client.prototype.escapeLiteral("hello \\ ' world") +console.log(escapedLiteral) // " E'hello \\\\ '' world'" +``` + ## events ### error From 513bf1e7e6105c1032cb215a0059d07d5f979e7a Mon Sep 17 00:00:00 2001 From: Conner Bradley Date: Tue, 11 Apr 2023 11:02:14 -0400 Subject: [PATCH 2/5] Fix typos in new docs --- docs/pages/apis/client.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index af87eab40..1b1e71f39 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -263,15 +263,15 @@ Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/s First, this can be used on an existing instance of `Client`: ```js -const escpaedIdentifier = client.escapeIdentifier('FooIdentifier') -console.log(escpaedIdentifier) // '"FooIdentifier"' +const escapedIdentifier = client.escapeIdentifier('FooIdentifier') +console.log(escapedIdentifier) // '"FooIdentifier"' ``` Alternatively, this can be used via `Client.prototype`: ```js const { Client } = require('pg') -const escpaedIdentifier = Client.prototype.escapeIdentifier('Bar"Identifier') -console.log(escpaedIdentifier) // '"Bar""Identifier"' +const escapedIdentifier = Client.prototype.escapeIdentifier('Bar"Identifier') +console.log(escapedIdentifier) // '"Bar""Identifier"' ``` ## client.escapeLiteral @@ -285,7 +285,7 @@ Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql- First, this can be used on an existing instance of `Client`: ```js const escapedLiteral = client.escapeLiteral("hello 'world'") -console.log(escapedLiteral) // "Hello ''world''" +console.log(escapedLiteral) // "'hello ''world'''" ``` Alternatively, this can be used via `Client.prototype`: From c931b0a7a8417e2e5ed56f64f1b56b398630948c Mon Sep 17 00:00:00 2001 From: Conner Bradley Date: Tue, 11 Apr 2023 13:19:32 -0400 Subject: [PATCH 3/5] 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. --- docs/pages/apis/_meta.json | 3 +- docs/pages/apis/client.mdx | 39 ------------ docs/pages/apis/utilities.mdx | 30 +++++++++ packages/pg/lib/client.js | 31 ---------- packages/pg/lib/index.js | 3 + packages/pg/lib/utils.js | 34 ++++++++++ packages/pg/test/unit/client/escape-tests.js | 65 -------------------- packages/pg/test/unit/utils-tests.js | 53 ++++++++++++++++ 8 files changed, 122 insertions(+), 136 deletions(-) create mode 100644 docs/pages/apis/utilities.mdx delete mode 100644 packages/pg/test/unit/client/escape-tests.js 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/client.mdx b/docs/pages/apis/client.mdx index 1b1e71f39..d5f335240 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -1,7 +1,6 @@ --- title: pg.Client --- -import { Alert } from '/components/alert.tsx' ## new Client @@ -257,44 +256,6 @@ client _note: end returning a promise is only available in pg7.0 and above_ -## client.escapeIdentifier - -Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS). There are two ways of using this method: - -First, this can be used on an existing instance of `Client`: -```js -const escapedIdentifier = client.escapeIdentifier('FooIdentifier') -console.log(escapedIdentifier) // '"FooIdentifier"' -``` - -Alternatively, this can be used via `Client.prototype`: -```js -const { Client } = require('pg') -const escapedIdentifier = Client.prototype.escapeIdentifier('Bar"Identifier') -console.log(escapedIdentifier) // '"Bar""Identifier"' -``` - -## client.escapeLiteral - -Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS). There are two ways of using this method: - - - **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](#clientquery) API for more information. - - -First, this can be used on an existing instance of `Client`: -```js -const escapedLiteral = client.escapeLiteral("hello 'world'") -console.log(escapedLiteral) // "'hello ''world'''" -``` - -Alternatively, this can be used via `Client.prototype`: -```js -const { Client } = require('pg') -const escapedLiteral = Client.prototype.escapeLiteral("hello \\ ' world") -console.log(escapedLiteral) // " E'hello \\\\ '' world'" -``` - ## events ### error 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..33420e216 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -456,37 +456,6 @@ 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(str) { - return '"' + str.replace(/"/g, '""') + '"' - } - - // 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 - } - _pulseQueryQueue() { if (this.readyForQuery === true) { this.activeQuery = this.queryQueue.shift() 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 deleted file mode 100644 index 721b04b49..000000000 --- a/packages/pg/test/unit/client/escape-tests.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict' -var helper = require('./test-helper') - -function createClient(callback) { - var client = new Client(helper.config) - client.connect(function (err) { - return callback(client) - }) -} - -var testLit = function (testName, input, expected) { - test(testName, function () { - var client = new Client(helper.config) - var actual = client.escapeLiteral(input) - assert.equal(expected, actual) - }) -} - -var testIdent = function (testName, input, expected) { - test(testName, function () { - var client = new Client(helper.config) - var actual = client.escapeIdentifier(input) - assert.equal(expected, actual) - }) -} - -testLit('escapeLiteral: no special characters', 'hello world', "'hello world'") - -testLit('escapeLiteral: contains double quotes only', 'hello " world', "'hello \" world'") - -testLit('escapeLiteral: contains single quotes only', "hello ' world", "'hello '' world'") - -testLit('escapeLiteral: contains backslashes only', 'hello \\ world', " E'hello \\\\ world'") - -testLit('escapeLiteral: contains single quotes and double quotes', 'hello \' " world', "'hello '' \" world'") - -testLit('escapeLiteral: contains double quotes and backslashes', 'hello \\ " world', " E'hello \\\\ \" world'") - -testLit('escapeLiteral: contains single quotes and backslashes', "hello \\ ' world", " E'hello \\\\ '' world'") - -testLit( - 'escapeLiteral: contains single quotes, double quotes, and backslashes', - 'hello \\ \' " world', - " E'hello \\\\ '' \" world'" -) - -testIdent('escapeIdentifier: no special characters', 'hello world', '"hello world"') - -testIdent('escapeIdentifier: contains double quotes only', 'hello " world', '"hello "" world"') - -testIdent('escapeIdentifier: contains single quotes only', "hello ' world", '"hello \' world"') - -testIdent('escapeIdentifier: contains backslashes only', 'hello \\ world', '"hello \\ world"') - -testIdent('escapeIdentifier: contains single quotes and double quotes', 'hello \' " world', '"hello \' "" world"') - -testIdent('escapeIdentifier: contains double quotes and backslashes', 'hello \\ " world', '"hello \\ "" world"') - -testIdent('escapeIdentifier: contains single quotes and backslashes', "hello \\ ' world", '"hello \\ \' world"') - -testIdent( - 'escapeIdentifier: contains single quotes, double quotes, and backslashes', - '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"' +) From fe01e3db7f578039a9f4a1daa4cc3d5816c4cb2e Mon Sep 17 00:00:00 2001 From: Conner Bradley Date: Thu, 20 Apr 2023 10:29:23 -0400 Subject: [PATCH 4/5] 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 --- packages/pg/lib/client.js | 7 ++ packages/pg/test/unit/client/escape-tests.js | 76 ++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 packages/pg/test/unit/client/escape-tests.js diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 33420e216..ad4e290bc 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -456,6 +456,13 @@ class Client extends EventEmitter { return this._types.getTypeParser(oid, format) } + // escapeIdentifier and escapeLiteral moved to utility functions & exported + // on PG + // re-exported here for backwards compatibility + escapeIdentifier = utils.escapeIdentifier + + escapeLiteral = utils.escapeLiteral + _pulseQueryQueue() { if (this.readyForQuery === true) { this.activeQuery = this.queryQueue.shift() diff --git a/packages/pg/test/unit/client/escape-tests.js b/packages/pg/test/unit/client/escape-tests.js new file mode 100644 index 000000000..e7ee5d464 --- /dev/null +++ b/packages/pg/test/unit/client/escape-tests.js @@ -0,0 +1,76 @@ +'use strict' +var helper = require('./test-helper') +var utils = require('../../../lib/utils') + +function createClient(callback) { + var client = new Client(helper.config) + client.connect(function (err) { + return callback(client) + }) +} + +var testLit = function (testName, input, expected) { + test(testName, function () { + var client = new Client(helper.config) + var actual = client.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) { + test(testName, function () { + var client = new Client(helper.config) + var actual = client.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'") + +testLit('escapeLiteral: contains double quotes only', 'hello " world', "'hello \" world'") + +testLit('escapeLiteral: contains single quotes only', "hello ' world", "'hello '' world'") + +testLit('escapeLiteral: contains backslashes only', 'hello \\ world', " E'hello \\\\ world'") + +testLit('escapeLiteral: contains single quotes and double quotes', 'hello \' " world', "'hello '' \" world'") + +testLit('escapeLiteral: contains double quotes and backslashes', 'hello \\ " world', " E'hello \\\\ \" world'") + +testLit('escapeLiteral: contains single quotes and backslashes', "hello \\ ' world", " E'hello \\\\ '' world'") + +testLit( + 'escapeLiteral: contains single quotes, double quotes, and backslashes', + 'hello \\ \' " world', + " E'hello \\\\ '' \" world'" +) + +testIdent('escapeIdentifier: no special characters', 'hello world', '"hello world"') + +testIdent('escapeIdentifier: contains double quotes only', 'hello " world', '"hello "" world"') + +testIdent('escapeIdentifier: contains single quotes only', "hello ' world", '"hello \' world"') + +testIdent('escapeIdentifier: contains backslashes only', 'hello \\ world', '"hello \\ world"') + +testIdent('escapeIdentifier: contains single quotes and double quotes', 'hello \' " world', '"hello \' "" world"') + +testIdent('escapeIdentifier: contains double quotes and backslashes', 'hello \\ " world', '"hello \\ "" world"') + +testIdent('escapeIdentifier: contains single quotes and backslashes', "hello \\ ' world", '"hello \\ \' world"') + +testIdent( + 'escapeIdentifier: contains single quotes, double quotes, and backslashes', + 'hello \\ \' " world', + '"hello \\ \' "" world"' +) From b80b887de97a49085d025b0b07680010eb9433be Mon Sep 17 00:00:00 2001 From: Conner Bradley Date: Thu, 20 Apr 2023 14:41:52 -0400 Subject: [PATCH 5/5] 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 --- packages/pg/lib/client.js | 8 ++++++-- packages/pg/test/unit/client/escape-tests.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index ad4e290bc..f2c339d37 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -459,9 +459,13 @@ class Client extends EventEmitter { // escapeIdentifier and escapeLiteral moved to utility functions & exported // on PG // re-exported here for backwards compatibility - escapeIdentifier = utils.escapeIdentifier + escapeIdentifier(str) { + return utils.escapeIdentifier(str) + } - escapeLiteral = utils.escapeLiteral + escapeLiteral(str) { + return utils.escapeLiteral(str) + } _pulseQueryQueue() { if (this.readyForQuery === true) { diff --git a/packages/pg/test/unit/client/escape-tests.js b/packages/pg/test/unit/client/escape-tests.js index e7ee5d464..68e233fbe 100644 --- a/packages/pg/test/unit/client/escape-tests.js +++ b/packages/pg/test/unit/client/escape-tests.js @@ -15,6 +15,12 @@ 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) @@ -28,6 +34,12 @@ 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)