From 05e9026aead008552ff0d1572214bba3131b0928 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 08:16:10 -0500 Subject: [PATCH 1/6] Remove tab character --- lib/native/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/native/query.js b/lib/native/query.js index e3c36f371..e358caf58 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -16,7 +16,7 @@ var NativeQuery = function(config, values, callback) { var c = utils.normalizeQueryConfig(config, values, callback); - this.name = c.name; + this.name = c.name; this.text = c.text; this.values = c.values; this.callback = c.callback; From 3f96bbbc5cd38dc25535a67d5b67411ca73454a7 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 09:19:30 -0500 Subject: [PATCH 2/6] Add field metadata to query result object Refactored the way rows are built in the native bindings which should result in a small performance improvement --- lib/native/index.js | 4 ++ lib/native/query.js | 27 +++++++++++- src/binding.cc | 44 +++++++++++++------ .../row-description-on-results-tests.js | 37 ++++++++++++++++ 4 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 test/integration/client/row-description-on-results-tests.js diff --git a/lib/native/index.js b/lib/native/index.js index d6dd4b22c..f89740dbd 100644 --- a/lib/native/index.js +++ b/lib/native/index.js @@ -171,6 +171,10 @@ var clientBuilder = function(config) { connection._pulseQueryQueue(true); }); + connection.on('_rowDescription', function(rowDescription) { + connection._activeQuery.handleRowDescription(rowDescription); + }); + //proxy some events to active query connection.on('_row', function(row) { connection._activeQuery.handleRow(row); diff --git a/lib/native/query.js b/lib/native/query.js index e358caf58..081c5503a 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -22,6 +22,7 @@ var NativeQuery = function(config, values, callback) { this.callback = c.callback; this._result = new Result(); + this._addedFields = false; //normalize values if(this.values) { for(var i = 0, len = this.values.length; i < len; i++) { @@ -39,13 +40,35 @@ var mapRowData = function(row) { for(var i = 0, len = row.length; i < len; i++) { var item = row[i]; result[item.name] = item.value === null ? null : - types.getTypeParser(item.type, 'text')(item.value); + types.getTypeParser(item.dataTypeID, 'text')(item.value); } return result; }; +NativeQuery.prototype.handleRowDescription = function(rowDescription) { + //multiple query statements in 1 action can result in multiple sets + //of rowDescriptions...eg: 'select NOW(); select 1::int;' + if(this._result.fields.length) { + this._result.fields = []; + } + for(var i = 0, len = rowDescription.length; i < len; i++) { + this._result.addField(rowDescription[i]); + } +}; + NativeQuery.prototype.handleRow = function(rowData) { - var row = mapRowData(rowData); + var row = {}; + for(var i = 0, len = rowData.length; i < len; i++) { + var rawValue = rowData[i]; + var field = this._result.fields[i]; + var fieldType = field.dataTypeID; + var parsedValue = null; + if(rawValue !== null) { + parsedValue = types.getTypeParser(fieldType, 'text')(rawValue); + } + var fieldName = field.name; + row[fieldName] = parsedValue; + } if(this.callback) { this._result.addRow(row); } diff --git a/src/binding.cc b/src/binding.cc index a1c568531..9e26b266d 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -61,7 +61,7 @@ class Connection : public ObjectWrap { routine_symbol = NODE_PSYMBOL("routine"); name_symbol = NODE_PSYMBOL("name"); value_symbol = NODE_PSYMBOL("value"); - type_symbol = NODE_PSYMBOL("type"); + type_symbol = NODE_PSYMBOL("dataTypeID"); channel_symbol = NODE_PSYMBOL("channel"); payload_symbol = NODE_PSYMBOL("payload"); command_symbol = NODE_PSYMBOL("command"); @@ -522,6 +522,33 @@ class Connection : public ObjectWrap { } return false; } + + //maps the postgres tuple results to v8 objects + //and emits row events + //TODO look at emitting fewer events because the back & forth between + //javascript & c++ might introduce overhead (requires benchmarking) + void EmitRowDescription(const PGresult* result) + { + HandleScope scope; + Local row = Array::New(); + int fieldCount = PQnfields(result); + for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) { + Local field = Object::New(); + //name of field + char* fieldName = PQfname(result, fieldNumber); + field->Set(name_symbol, String::New(fieldName)); + + //oid of type of field + int fieldType = PQftype(result, fieldNumber); + field->Set(type_symbol, Integer::New(fieldType)); + + row->Set(Integer::New(fieldNumber), field); + } + + Handle e = (Handle)row; + Emit("_rowDescription", &e); + } + bool HandleResult(PGresult* result) { TRACE("PQresultStatus"); @@ -529,6 +556,7 @@ class Connection : public ObjectWrap { switch(status) { case PGRES_TUPLES_OK: { + EmitRowDescription(result); HandleTuplesResult(result); EmitCommandMetaData(result); return true; @@ -592,24 +620,14 @@ class Connection : public ObjectWrap { Local row = Array::New(); int fieldCount = PQnfields(result); for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) { - Local field = Object::New(); - //name of field - char* fieldName = PQfname(result, fieldNumber); - field->Set(name_symbol, String::New(fieldName)); - - //oid of type of field - int fieldType = PQftype(result, fieldNumber); - field->Set(type_symbol, Integer::New(fieldType)); //value of field if(PQgetisnull(result, rowNumber, fieldNumber)) { - field->Set(value_symbol, Null()); + row->Set(Integer::New(fieldNumber), Null()); } else { char* fieldValue = PQgetvalue(result, rowNumber, fieldNumber); - field->Set(value_symbol, String::New(fieldValue)); + row->Set(Integer::New(fieldNumber), String::New(fieldValue)); } - - row->Set(Integer::New(fieldNumber), field); } Handle e = (Handle)row; diff --git a/test/integration/client/row-description-on-results-tests.js b/test/integration/client/row-description-on-results-tests.js new file mode 100644 index 000000000..22c929653 --- /dev/null +++ b/test/integration/client/row-description-on-results-tests.js @@ -0,0 +1,37 @@ +var helper = require('./test-helper'); + +var Client = helper.Client; + +var conInfo = helper.config; + +var checkResult = function(result) { + assert(result.fields); + assert.equal(result.fields.length, 3); + var fields = result.fields; + assert.equal(fields[0].name, 'now'); + assert.equal(fields[1].name, 'num'); + assert.equal(fields[2].name, 'texty'); + assert.equal(fields[0].dataTypeID, 1184); + assert.equal(fields[1].dataTypeID, 23); + assert.equal(fields[2].dataTypeID, 25); +}; + +test('row descriptions on result object', function() { + var client = new Client(conInfo); + client.connect(assert.success(function() { + client.query('SELECT NOW() as now, 1::int as num, $1::text as texty', ["hello"], assert.success(function(result) { + checkResult(result); + client.end(); + })); + })); +}); + +test('row description on no rows', function() { + var client = new Client(conInfo); + client.connect(assert.success(function() { + client.query('SELECT NOW() as now, 1::int as num, $1::text as texty LIMIT 0', ["hello"], assert.success(function(result) { + checkResult(result); + client.end(); + })); + })); +}); From 413eff72e550612d119c5c2c1401bd1c7aa224a1 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 09:30:10 -0500 Subject: [PATCH 3/6] Move row parsing into result object --- lib/native/query.js | 33 ++------------------------------- lib/query.js | 30 +++++------------------------- lib/result.js | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 59 deletions(-) diff --git a/lib/native/query.js b/lib/native/query.js index 081c5503a..38fb2fda2 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -34,41 +34,12 @@ var NativeQuery = function(config, values, callback) { util.inherits(NativeQuery, EventEmitter); -//maps from native rowdata into api compatible row object -var mapRowData = function(row) { - var result = {}; - for(var i = 0, len = row.length; i < len; i++) { - var item = row[i]; - result[item.name] = item.value === null ? null : - types.getTypeParser(item.dataTypeID, 'text')(item.value); - } - return result; -}; - NativeQuery.prototype.handleRowDescription = function(rowDescription) { - //multiple query statements in 1 action can result in multiple sets - //of rowDescriptions...eg: 'select NOW(); select 1::int;' - if(this._result.fields.length) { - this._result.fields = []; - } - for(var i = 0, len = rowDescription.length; i < len; i++) { - this._result.addField(rowDescription[i]); - } + this._result.addFields(rowDescription); }; NativeQuery.prototype.handleRow = function(rowData) { - var row = {}; - for(var i = 0, len = rowData.length; i < len; i++) { - var rawValue = rowData[i]; - var field = this._result.fields[i]; - var fieldType = field.dataTypeID; - var parsedValue = null; - if(rawValue !== null) { - parsedValue = types.getTypeParser(fieldType, 'text')(rawValue); - } - var fieldName = field.name; - row[fieldName] = parsedValue; - } + var row = this._result.parseRow(rowData); if(this.callback) { this._result.addRow(row); } diff --git a/lib/query.js b/lib/query.js index 71cb3b8fd..cce393014 100644 --- a/lib/query.js +++ b/lib/query.js @@ -55,36 +55,16 @@ var noParse = function(val) { //message with this query object //metadata used when parsing row results Query.prototype.handleRowDescription = function(msg) { - this._fieldNames = []; - this._fieldConverters = []; - var len = msg.fields.length; - for(var i = 0; i < len; i++) { - var field = msg.fields[i]; - var format = field.format; - this._fieldNames[i] = field.name; - this._fieldConverters[i] = Types.getTypeParser(field.dataTypeID, format); - this._result.addField(field); - } + this._result.addFields(msg.fields); }; Query.prototype.handleDataRow = function(msg) { - var self = this; - var row = {}; - for(var i = 0; i < msg.fields.length; i++) { - var rawValue = msg.fields[i]; - if(rawValue === null) { - //leave null values alone - row[self._fieldNames[i]] = null; - } else { - //convert value to javascript - row[self._fieldNames[i]] = self._fieldConverters[i](rawValue); - } - } - self.emit('row', row, self._result); + var row = this._result.parseRow(msg.fields); + this.emit('row', row, this._result); //if there is a callback collect rows - if(self.callback) { - self._result.addRow(row); + if(this.callback) { + this._result.addRow(row); } }; diff --git a/lib/result.js b/lib/result.js index 7a1e3c04e..7a953b2a5 100644 --- a/lib/result.js +++ b/lib/result.js @@ -1,3 +1,5 @@ +var types = require(__dirname + '/types/'); + //result object returned from query //in the 'end' event and also //passed as second argument to provided callback @@ -34,13 +36,39 @@ Result.prototype.addCommandComplete = function(msg) { } }; +//rowData is an array of text or binary values +//this turns the row into a JavaScript object +Result.prototype.parseRow = function(rowData) { + var row = {}; + for(var i = 0, len = rowData.length; i < len; i++) { + var rawValue = rowData[i]; + var field = this.fields[i]; + var fieldType = field.dataTypeID; + var parsedValue = null; + if(rawValue !== null) { + parsedValue = types.getTypeParser(fieldType, field.format || 'text')(rawValue); + } + var fieldName = field.name; + row[fieldName] = parsedValue; + } + return row; +}; + Result.prototype.addRow = function(row) { this.rows.push(row); }; -//Add a field definition to the result -Result.prototype.addField = function(field) { - this.fields.push(field); +Result.prototype.addFields = function(fieldDescriptions) { + //clears field definitions + //multiple query statements in 1 action can result in multiple sets + //of rowDescriptions...eg: 'select NOW(); select 1::int;' + //you need to reset the fields + if(this.fields.length) { + this.fields = []; + } + for(var i = 0; i < fieldDescriptions.length; i++) { + this.fields.push(fieldDescriptions[i]); + } }; module.exports = Result; From 5462561e514ce56168c5527ccd8a881231855f33 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 09:32:53 -0500 Subject: [PATCH 4/6] Cache result parser lookups --- lib/result.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/result.js b/lib/result.js index 7a953b2a5..d6383058c 100644 --- a/lib/result.js +++ b/lib/result.js @@ -9,6 +9,7 @@ var Result = function() { this.oid = null; this.rows = []; this.fields = []; + this._parsers = []; }; var matchRegexp = /([A-Za-z]+) ?(\d+ )?(\d+)?/; @@ -46,7 +47,7 @@ Result.prototype.parseRow = function(rowData) { var fieldType = field.dataTypeID; var parsedValue = null; if(rawValue !== null) { - parsedValue = types.getTypeParser(fieldType, field.format || 'text')(rawValue); + parsedValue = this._parsers[i](rawValue); } var fieldName = field.name; row[fieldName] = parsedValue; @@ -65,9 +66,12 @@ Result.prototype.addFields = function(fieldDescriptions) { //you need to reset the fields if(this.fields.length) { this.fields = []; + this._parsers = []; } for(var i = 0; i < fieldDescriptions.length; i++) { - this.fields.push(fieldDescriptions[i]); + var desc = fieldDescriptions[i]; + this.fields.push(desc); + this._parsers.push(types.getTypeParser(desc.dataTypeID, desc.format || 'text')); } }; From 325a6d91539f1e66eaef52f32e487403581fe35a Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 09:40:32 -0500 Subject: [PATCH 5/6] Add failing test for result rows as arrays --- .../client/results-as-array-tests.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 test/integration/client/results-as-array-tests.js diff --git a/test/integration/client/results-as-array-tests.js b/test/integration/client/results-as-array-tests.js new file mode 100644 index 000000000..e8eb58d6a --- /dev/null +++ b/test/integration/client/results-as-array-tests.js @@ -0,0 +1,28 @@ +var util = require('util'); +var helper = require('./test-helper'); + +var Client = helper.Client; + +var conInfo = helper.config; + +test('returns results as array', function() { + var client = new Client(conInfo); + var checkRow = function(row) { + assert(util.isArray(row), 'row should be an array'); + } + client.connect(assert.success(function() { + var config = { + text: 'SELECT NOW(), 1::int, $1::text', + values: ['hai'], + rowMode: 'array' + }; + var query = client.query(config, assert.success(function(result) { + assert.equal(result.rows.length, 1); + checkRow(result.rows[0]); + client.end(); + })); + assert.emits(query, 'row', function(row) { + checkRow(row); + }); + })); +}); From 145666c1b3afc5a25e70fdb272cdd0c91fcb8c32 Mon Sep 17 00:00:00 2001 From: Brian Carlson Date: Mon, 8 Jul 2013 17:45:06 -0500 Subject: [PATCH 6/6] Support result rows as arrays --- lib/native/query.js | 2 +- lib/query.js | 2 +- lib/result.js | 18 +++++++++++++++++- .../client/results-as-array-tests.js | 7 ++++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/native/query.js b/lib/native/query.js index 38fb2fda2..905d1f789 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -21,7 +21,7 @@ var NativeQuery = function(config, values, callback) { this.values = c.values; this.callback = c.callback; - this._result = new Result(); + this._result = new Result(config.rowMode); this._addedFields = false; //normalize values if(this.values) { diff --git a/lib/query.js b/lib/query.js index cce393014..44ecee9f2 100644 --- a/lib/query.js +++ b/lib/query.js @@ -23,7 +23,7 @@ var Query = function(config, values, callback) { this.callback = config.callback; this._fieldNames = []; this._fieldConverters = []; - this._result = new Result(); + this._result = new Result(config.rowMode); this.isPreparedStatement = false; this._canceledDueToError = false; EventEmitter.call(this); diff --git a/lib/result.js b/lib/result.js index d6383058c..2e4feece7 100644 --- a/lib/result.js +++ b/lib/result.js @@ -3,13 +3,16 @@ var types = require(__dirname + '/types/'); //result object returned from query //in the 'end' event and also //passed as second argument to provided callback -var Result = function() { +var Result = function(rowMode) { this.command = null; this.rowCount = null; this.oid = null; this.rows = []; this.fields = []; this._parsers = []; + if(rowMode == "array") { + this.parseRow = this._parseRowAsArray; + } }; var matchRegexp = /([A-Za-z]+) ?(\d+ )?(\d+)?/; @@ -37,6 +40,19 @@ Result.prototype.addCommandComplete = function(msg) { } }; +Result.prototype._parseRowAsArray = function(rowData) { + var row = []; + for(var i = 0, len = rowData.length; i < len; i++) { + var rawValue = rowData[i]; + if(rawValue !== null) { + row.push(this._parsers[i](rawValue)); + } else { + row.push(null); + } + } + return row; +}; + //rowData is an array of text or binary values //this turns the row into a JavaScript object Result.prototype.parseRow = function(rowData) { diff --git a/test/integration/client/results-as-array-tests.js b/test/integration/client/results-as-array-tests.js index e8eb58d6a..ef11a891c 100644 --- a/test/integration/client/results-as-array-tests.js +++ b/test/integration/client/results-as-array-tests.js @@ -9,10 +9,15 @@ test('returns results as array', function() { var client = new Client(conInfo); var checkRow = function(row) { assert(util.isArray(row), 'row should be an array'); + assert.equal(row.length, 4); + assert.equal(row[0].getFullYear(), new Date().getFullYear()); + assert.strictEqual(row[1], 1); + assert.strictEqual(row[2], 'hai'); + assert.strictEqual(row[3], null); } client.connect(assert.success(function() { var config = { - text: 'SELECT NOW(), 1::int, $1::text', + text: 'SELECT NOW(), 1::int, $1::text, null', values: ['hai'], rowMode: 'array' };