From 61eec2298bbb61cdb5601f2afbdbdcb2062c03d3 Mon Sep 17 00:00:00 2001 From: Daniel Gavrilov Date: Tue, 31 Oct 2017 13:58:30 +0000 Subject: [PATCH 1/3] Add tests for binary parameters --- test/async-socket.js | 60 +++++++++++++++++++++++++++++++++++++++++ test/helper.js | 8 ++++++ test/sync-parameters.js | 20 ++++++++++++++ test/sync-prepare.js | 22 +++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/test/async-socket.js b/test/async-socket.js index e193560..44572fe 100644 --- a/test/async-socket.js +++ b/test/async-socket.js @@ -54,6 +54,24 @@ describe('async simple query', function() { }) }); + it('dispatches query with binary parameter', function(done) { + var pq = this.pq; + var string = 'fo\\o'; + var buffer = Buffer.from(string, 'utf8'); + var success = pq.sendQueryParams('SELECT $1::bytea as value', [buffer]); + assert(success, pq.errorMessage()); + assert.strictEqual(pq.flush(), 0, 'Should have flushed query text & parameters'); + consume(pq, function() { + assert.ifError(pq.errorMessage()); + assert(pq.getResult()); + assert.strictEqual(pq.getResult(), false); + assert.strictEqual(pq.ntuples(), 1); + var value = helper.parseHexOutput(pq.getvalue(0, 0)); + assert.equal(value, string); + done(); + }) + }); + it('dispatches named query', function(done) { var pq = this.pq; var statementName = 'async-get-name'; @@ -92,4 +110,46 @@ describe('async simple query', function() { }); }); }); + + it('dispatches named query with binary parameter', function(done) { + var pq = this.pq; + var statementName = 'async-get-binary-param'; + var string = 'fo\\o'; + var buffer = Buffer.from(string, 'utf8'); + var success = pq.sendPrepare(statementName, 'SELECT $1::bytea as value', 1); + assert(success, pq.errorMessage()); + assert.strictEqual(pq.flush(), 0, 'Should have flushed query text'); + consume(pq, function() { + assert.ifError(pq.errorMessage()); + + //first time there should be a result + assert(pq.getResult()); + + //call 'getResult' until it returns false indicating + //there is no more input to consume + assert.strictEqual(pq.getResult(), false); + + //since we only prepared a statement there should be + //0 tuples in the result + assert.equal(pq.ntuples(), 0); + + //now execute the previously prepared statement + var success = pq.sendQueryPrepared(statementName, [buffer]); + assert(success, pq.errorMessage()); + assert.strictEqual(pq.flush(), 0, 'Should have flushed parameters'); + consume(pq, function() { + assert.ifError(pq.errorMessage()); + + //consume the result of the query execution + assert(pq.getResult()); + assert.equal(pq.ntuples(), 1); + var value = helper.parseHexOutput(pq.getvalue(0, 0)); + assert.equal(value, string); + + //call 'getResult' again to ensure we're finished + assert.strictEqual(pq.getResult(), false); + done(); + }); + }); + }); }); diff --git a/test/helper.js b/test/helper.js index f396363..66de1fe 100644 --- a/test/helper.js +++ b/test/helper.js @@ -17,5 +17,13 @@ module.exports = { after(function() { this.pq.finish(); }); + }, + parseHexOutput: function(hexx) { + var hex = hexx.toString().substring(2); // remove leading \x + var str = ''; + for (var i = 0; i < hex.length; i += 2) { + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + } + return str; } }; diff --git a/test/sync-parameters.js b/test/sync-parameters.js index 464206a..f84b44e 100644 --- a/test/sync-parameters.js +++ b/test/sync-parameters.js @@ -23,4 +23,24 @@ describe('sync query with parameters', function() { this.pq.execParams(queryText, ['Barkley', 4]); assert.equal(this.pq.resultErrorMessage(), ''); }); + + it('works with non-escaped binary parameter', function() { + var queryText = 'SELECT $1::bytea as value'; + var string = 'foo'; + var buffer = Buffer.from(string, 'utf8'); + this.pq.execParams(queryText, [buffer]); + assert.strictEqual(this.pq.ntuples(), 1); + var value = helper.parseHexOutput(this.pq.getvalue(0, 0)); + assert.strictEqual(value, string); + }); + + it('works with escaped binary parameter', function() { + var queryText = 'SELECT $1::bytea as value'; + var string = 'fo\\o'; + var buffer = Buffer.from(string, 'utf8'); + this.pq.execParams(queryText, [buffer]); + assert.strictEqual(this.pq.ntuples(), 1); + var value = helper.parseHexOutput(this.pq.getvalue(0, 0)); + assert.strictEqual(value, string); + }); }); diff --git a/test/sync-prepare.js b/test/sync-prepare.js index af009d1..79e1e5d 100644 --- a/test/sync-prepare.js +++ b/test/sync-prepare.js @@ -25,3 +25,25 @@ describe('prepare and execPrepared', function() { }); }); }); + +describe('prepare and execPrepared with binary parameter', function() { + + helper.setupIntegration(); + + var statementName = 'get-binary-param'; + + it('works properly', function() { + this.pq.prepare(statementName, 'SELECT $1::bytea as value', 1); + assert.ifError(this.pq.resultErrorMessage()); + assert.equal(this.pq.resultStatus(), 'PGRES_COMMAND_OK'); + + var string = 'fo\\o'; + var buffer = Buffer.from(string, 'utf8'); + this.pq.execPrepared(statementName, [buffer]); + assert.ifError(this.pq.resultErrorMessage()); + assert.strictEqual(this.pq.ntuples(), 1) + assert.strictEqual(this.pq.nfields(), 1); + var value = helper.parseHexOutput(this.pq.getvalue(0, 0)); + assert.strictEqual(value, string); + }); +}); From 174345e2bf338b24a3ae0eab9aee9d9370f66e28 Mon Sep 17 00:00:00 2001 From: Daniel Gavrilov Date: Tue, 31 Oct 2017 14:01:22 +0000 Subject: [PATCH 2/3] Pass paramFormats and paramLengths onto libpq --- index.js | 32 +++++++++++++++++++--- src/connection.cc | 67 ++++++++++++++++++++++++++++++++++++++++------- src/connection.h | 1 + 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index d12518b..429da71 100644 --- a/index.js +++ b/index.js @@ -91,7 +91,13 @@ PQ.prototype.execParams = function(commandText, parameters) { if(!parameters) { parameters = []; } - this.$execParams(commandText, parameters); + var paramFormats = parameters.map(function(value) { + return value instanceof Buffer ? 1 : 0; + }); + var paramLengths = parameters.map(function(value) { + return value instanceof Buffer ? value.length : 0; + }); + this.$execParams(commandText, parameters, paramFormats, paramLengths); }; //SYNC prepares a named query and stores the result @@ -123,7 +129,13 @@ PQ.prototype.execPrepared = function(statementName, parameters) { if(!parameters) { parameters = []; } - this.$execPrepared(statementName, parameters); + var paramFormats = parameters.map(function(value) { + return value instanceof Buffer ? 1 : 0; + }); + var paramLengths = parameters.map(function(value) { + return value instanceof Buffer ? value.length : 0; + }); + this.$execPrepared(statementName, parameters, paramFormats, paramLengths); }; //send a command to begin executing a query in async mode @@ -144,7 +156,13 @@ PQ.prototype.sendQueryParams = function(commandText, parameters) { if(!parameters) { parameters = []; } - return this.$sendQueryParams(commandText, parameters); + var paramFormats = parameters.map(function(value) { + return value instanceof Buffer ? 1 : 0; + }); + var paramLengths = parameters.map(function(value) { + return value instanceof Buffer ? value.length : 0; + }); + return this.$sendQueryParams(commandText, parameters, paramFormats, paramLengths); }; //send a command to prepare a named query in async mode @@ -170,7 +188,13 @@ PQ.prototype.sendQueryPrepared = function(statementName, parameters) { if(!parameters) { parameters = []; } - return this.$sendQueryPrepared(statementName, parameters); + var paramFormats = parameters.map(function(value) { + return value instanceof Buffer ? 1 : 0; + }); + var paramLengths = parameters.map(function(value) { + return value instanceof Buffer ? value.length : 0; + }); + return this.$sendQueryPrepared(statementName, parameters, paramFormats, paramLengths); }; //'pops' a result out of the buffered diff --git a/src/connection.cc b/src/connection.cc index fc8ae16..36f01b9 100644 --- a/src/connection.cc +++ b/src/connection.cc @@ -102,9 +102,14 @@ NAN_METHOD(Connection::ExecParams) { TRACEF("Connection::Exec: %s\n", *commandText); v8::Local jsParams = v8::Local::Cast(info[1]); + v8::Local jsParamFormats = v8::Local::Cast(info[2]); + v8::Local jsParamLengths = v8::Local::Cast(info[3]); int numberOfParams = jsParams->Length(); + char **parameters = NewCStringArray(jsParams); + int *paramFormats = NewCIntArray(jsParamFormats); + int *paramLengths = NewCIntArray(jsParamLengths); PGresult* result = PQexecParams( self->pq, @@ -112,12 +117,14 @@ NAN_METHOD(Connection::ExecParams) { numberOfParams, NULL, //const Oid* paramTypes[], parameters, //const char* const* paramValues[] - NULL, //const int* paramLengths[] - NULL, //const int* paramFormats[], + paramLengths, //const int* paramLengths[] + paramFormats, //const int* paramFormats[], 0 //result format of text ); DeleteCStringArray(parameters, numberOfParams); + delete [] paramFormats; + delete [] paramLengths; self->SetLastResult(result); } @@ -150,21 +157,28 @@ NAN_METHOD(Connection::ExecPrepared) { TRACEF("Connection::ExecPrepared: %s\n", *statementName); v8::Local jsParams = v8::Local::Cast(info[1]); + v8::Local jsParamFormats = v8::Local::Cast(info[2]); + v8::Local jsParamLengths = v8::Local::Cast(info[3]); int numberOfParams = jsParams->Length(); - char** parameters = NewCStringArray(jsParams); + + char **parameters = NewCStringArray(jsParams); + int *paramFormats = NewCIntArray(jsParamFormats); + int *paramLengths = NewCIntArray(jsParamLengths); PGresult* result = PQexecPrepared( self->pq, *statementName, numberOfParams, parameters, //const char* const* paramValues[] - NULL, //const int* paramLengths[] - NULL, //const int* paramFormats[], + paramLengths, //const int* paramLengths[] + paramFormats, //const int* paramFormats[], 0 //result format of text ); DeleteCStringArray(parameters, numberOfParams); + delete [] paramFormats; + delete [] paramLengths; self->SetLastResult(result); } @@ -354,9 +368,14 @@ NAN_METHOD(Connection::SendQueryParams) { TRACEF("Connection::SendQueryParams: %s\n", *commandText); v8::Local jsParams = v8::Local::Cast(info[1]); + v8::Local jsParamFormats = v8::Local::Cast(info[2]); + v8::Local jsParamLengths = v8::Local::Cast(info[3]); int numberOfParams = jsParams->Length(); + char** parameters = NewCStringArray(jsParams); + int *paramFormats = NewCIntArray(jsParamFormats); + int *paramLengths = NewCIntArray(jsParamLengths); int success = PQsendQueryParams( self->pq, @@ -364,12 +383,14 @@ NAN_METHOD(Connection::SendQueryParams) { numberOfParams, NULL, //const Oid* paramTypes[], parameters, //const char* const* paramValues[] - NULL, //const int* paramLengths[] - NULL, //const int* paramFormats[], + paramLengths, //const int* paramLengths[] + paramFormats, //const int* paramFormats[], 0 //result format of text ); DeleteCStringArray(parameters, numberOfParams); + delete [] paramFormats; + delete [] paramLengths; info.GetReturnValue().Set(success == 1); } @@ -404,21 +425,28 @@ NAN_METHOD(Connection::SendQueryPrepared) { TRACEF("Connection::SendQueryPrepared: %s\n", *statementName); v8::Local jsParams = v8::Local::Cast(info[1]); + v8::Local jsParamFormats = v8::Local::Cast(info[2]); + v8::Local jsParamLengths = v8::Local::Cast(info[3]); int numberOfParams = jsParams->Length(); - char** parameters = NewCStringArray(jsParams); + + char **parameters = NewCStringArray(jsParams); + int *paramFormats = NewCIntArray(jsParamFormats); + int *paramLengths = NewCIntArray(jsParamLengths); int success = PQsendQueryPrepared( self->pq, *statementName, numberOfParams, parameters, //const char* const* paramValues[] - NULL, //const int* paramLengths[] - NULL, //const int* paramFormats[], + paramLengths, //const int* paramLengths[] + paramFormats, //const int* paramFormats[], 0 //result format of text ); DeleteCStringArray(parameters, numberOfParams); + delete [] paramFormats; + delete [] paramLengths; info.GetReturnValue().Set(success == 1); } @@ -786,6 +814,25 @@ void Connection::DeleteCStringArray(char** array, int length) { delete [] array; } +int* Connection::NewCIntArray(v8::Local jsParams) { + Nan::HandleScope scope; + + int len = jsParams->Length(); + + int* array = new int[len]; + + for(int i = 0; i < len; i++) { + v8::Local val = Nan::Get(jsParams, i).ToLocalChecked(); + if(val->IsNull()) { + array[i] = 0; + continue; + } + array[i] = Nan::To(val).FromJust(); + } + + return array; +} + void Connection::Emit(const char* message) { Nan::HandleScope scope; diff --git a/src/connection.h b/src/connection.h index d73b5e7..41f66f3 100644 --- a/src/connection.h +++ b/src/connection.h @@ -76,6 +76,7 @@ class Connection : public Nan::ObjectWrap { static char* NewCString(v8::Local val); static char** NewCStringArray(v8::Local jsParams); static void DeleteCStringArray(char** array, int length); + static int* NewCIntArray(v8::Local jsParams); void Emit(const char* message); }; From 421462f0a687967f3f5982162cc036242b53db1f Mon Sep 17 00:00:00 2001 From: Daniel Gavrilov Date: Tue, 31 Oct 2017 16:42:54 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Make=20buffers=20node=20=E2=89=A44=20compat?= =?UTF-8?q?ible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/async-socket.js | 4 ++-- test/helper.js | 7 +++++++ test/sync-parameters.js | 4 ++-- test/sync-prepare.js | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test/async-socket.js b/test/async-socket.js index 44572fe..d638686 100644 --- a/test/async-socket.js +++ b/test/async-socket.js @@ -57,7 +57,7 @@ describe('async simple query', function() { it('dispatches query with binary parameter', function(done) { var pq = this.pq; var string = 'fo\\o'; - var buffer = Buffer.from(string, 'utf8'); + var buffer = helper.createBuffer(string, 'utf8'); var success = pq.sendQueryParams('SELECT $1::bytea as value', [buffer]); assert(success, pq.errorMessage()); assert.strictEqual(pq.flush(), 0, 'Should have flushed query text & parameters'); @@ -115,7 +115,7 @@ describe('async simple query', function() { var pq = this.pq; var statementName = 'async-get-binary-param'; var string = 'fo\\o'; - var buffer = Buffer.from(string, 'utf8'); + var buffer = helper.createBuffer(string, 'utf8'); var success = pq.sendPrepare(statementName, 'SELECT $1::bytea as value', 1); assert(success, pq.errorMessage()); assert.strictEqual(pq.flush(), 0, 'Should have flushed query text'); diff --git a/test/helper.js b/test/helper.js index 66de1fe..140b476 100644 --- a/test/helper.js +++ b/test/helper.js @@ -25,5 +25,12 @@ module.exports = { str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return str; + }, + createBuffer: function(string, encoding) { + if (Number(process.version.match(/^v(\d+)/)[1]) <= 4) { + return new Buffer(string, encoding); + } else { + return Buffer.from(string, encoding); + } } }; diff --git a/test/sync-parameters.js b/test/sync-parameters.js index f84b44e..d25e735 100644 --- a/test/sync-parameters.js +++ b/test/sync-parameters.js @@ -27,7 +27,7 @@ describe('sync query with parameters', function() { it('works with non-escaped binary parameter', function() { var queryText = 'SELECT $1::bytea as value'; var string = 'foo'; - var buffer = Buffer.from(string, 'utf8'); + var buffer = helper.createBuffer(string, 'utf8'); this.pq.execParams(queryText, [buffer]); assert.strictEqual(this.pq.ntuples(), 1); var value = helper.parseHexOutput(this.pq.getvalue(0, 0)); @@ -37,7 +37,7 @@ describe('sync query with parameters', function() { it('works with escaped binary parameter', function() { var queryText = 'SELECT $1::bytea as value'; var string = 'fo\\o'; - var buffer = Buffer.from(string, 'utf8'); + var buffer = helper.createBuffer(string, 'utf8'); this.pq.execParams(queryText, [buffer]); assert.strictEqual(this.pq.ntuples(), 1); var value = helper.parseHexOutput(this.pq.getvalue(0, 0)); diff --git a/test/sync-prepare.js b/test/sync-prepare.js index 79e1e5d..14fc136 100644 --- a/test/sync-prepare.js +++ b/test/sync-prepare.js @@ -38,7 +38,7 @@ describe('prepare and execPrepared with binary parameter', function() { assert.equal(this.pq.resultStatus(), 'PGRES_COMMAND_OK'); var string = 'fo\\o'; - var buffer = Buffer.from(string, 'utf8'); + var buffer = helper.createBuffer(string, 'utf8'); this.pq.execPrepared(statementName, [buffer]); assert.ifError(this.pq.resultErrorMessage()); assert.strictEqual(this.pq.ntuples(), 1)