diff --git a/lib/native/index.js b/lib/native/index.js index d6dd4b2..f89740d 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 e3c36f3..905d1f7 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -16,12 +16,13 @@ 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; - this._result = new Result(); + this._result = new Result(config.rowMode); + this._addedFields = false; //normalize values if(this.values) { for(var i = 0, len = this.values.length; i < len; i++) { @@ -33,19 +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.type, 'text')(item.value); - } - return result; +NativeQuery.prototype.handleRowDescription = function(rowDescription) { + this._result.addFields(rowDescription); }; NativeQuery.prototype.handleRow = function(rowData) { - var row = mapRowData(rowData); + var row = this._result.parseRow(rowData); if(this.callback) { this._result.addRow(row); } diff --git a/lib/query.js b/lib/query.js index 71cb3b8..44ecee9 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); @@ -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 7a1e3c0..2e4feec 100644 --- a/lib/result.js +++ b/lib/result.js @@ -1,12 +1,18 @@ +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+)?/; @@ -34,13 +40,55 @@ 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) { + 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 = this._parsers[i](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 = []; + this._parsers = []; + } + for(var i = 0; i < fieldDescriptions.length; i++) { + var desc = fieldDescriptions[i]; + this.fields.push(desc); + this._parsers.push(types.getTypeParser(desc.dataTypeID, desc.format || 'text')); + } }; module.exports = Result; diff --git a/src/binding.cc b/src/binding.cc index a1c5685..9e26b26 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -61,7 +61,7 @@ public: 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 @@ protected: } 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 @@ protected: switch(status) { case PGRES_TUPLES_OK: { + EmitRowDescription(result); HandleTuplesResult(result); EmitCommandMetaData(result); return true; @@ -592,24 +620,14 @@ protected: 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/results-as-array-tests.js b/test/integration/client/results-as-array-tests.js new file mode 100644 index 0000000..ef11a89 --- /dev/null +++ b/test/integration/client/results-as-array-tests.js @@ -0,0 +1,33 @@ +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'); + 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, null', + 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); + }); + })); +}); 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 0000000..22c9296 --- /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(); + })); + })); +});