Merge pull request #393 from brianc/issues/324

add support for result rows as arrays
This commit is contained in:
Brian C 2013-07-09 21:05:18 -07:00
commit f263fe1b0a
7 changed files with 169 additions and 55 deletions

View File

@ -171,6 +171,10 @@ var clientBuilder = function(config) {
connection._pulseQueryQueue(true); connection._pulseQueryQueue(true);
}); });
connection.on('_rowDescription', function(rowDescription) {
connection._activeQuery.handleRowDescription(rowDescription);
});
//proxy some events to active query //proxy some events to active query
connection.on('_row', function(row) { connection.on('_row', function(row) {
connection._activeQuery.handleRow(row); connection._activeQuery.handleRow(row);

View File

@ -21,7 +21,8 @@ var NativeQuery = function(config, values, callback) {
this.values = c.values; this.values = c.values;
this.callback = c.callback; this.callback = c.callback;
this._result = new Result(); this._result = new Result(config.rowMode);
this._addedFields = false;
//normalize values //normalize values
if(this.values) { if(this.values) {
for(var i = 0, len = this.values.length; i < len; i++) { 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); util.inherits(NativeQuery, EventEmitter);
//maps from native rowdata into api compatible row object NativeQuery.prototype.handleRowDescription = function(rowDescription) {
var mapRowData = function(row) { this._result.addFields(rowDescription);
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.handleRow = function(rowData) { NativeQuery.prototype.handleRow = function(rowData) {
var row = mapRowData(rowData); var row = this._result.parseRow(rowData);
if(this.callback) { if(this.callback) {
this._result.addRow(row); this._result.addRow(row);
} }

View File

@ -23,7 +23,7 @@ var Query = function(config, values, callback) {
this.callback = config.callback; this.callback = config.callback;
this._fieldNames = []; this._fieldNames = [];
this._fieldConverters = []; this._fieldConverters = [];
this._result = new Result(); this._result = new Result(config.rowMode);
this.isPreparedStatement = false; this.isPreparedStatement = false;
this._canceledDueToError = false; this._canceledDueToError = false;
EventEmitter.call(this); EventEmitter.call(this);
@ -55,36 +55,16 @@ var noParse = function(val) {
//message with this query object //message with this query object
//metadata used when parsing row results //metadata used when parsing row results
Query.prototype.handleRowDescription = function(msg) { Query.prototype.handleRowDescription = function(msg) {
this._fieldNames = []; this._result.addFields(msg.fields);
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);
}
}; };
Query.prototype.handleDataRow = function(msg) { Query.prototype.handleDataRow = function(msg) {
var self = this; var row = this._result.parseRow(msg.fields);
var row = {}; this.emit('row', row, this._result);
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);
//if there is a callback collect rows //if there is a callback collect rows
if(self.callback) { if(this.callback) {
self._result.addRow(row); this._result.addRow(row);
} }
}; };

View File

@ -1,12 +1,18 @@
var types = require(__dirname + '/types/');
//result object returned from query //result object returned from query
//in the 'end' event and also //in the 'end' event and also
//passed as second argument to provided callback //passed as second argument to provided callback
var Result = function() { var Result = function(rowMode) {
this.command = null; this.command = null;
this.rowCount = null; this.rowCount = null;
this.oid = null; this.oid = null;
this.rows = []; this.rows = [];
this.fields = []; this.fields = [];
this._parsers = [];
if(rowMode == "array") {
this.parseRow = this._parseRowAsArray;
}
}; };
var matchRegexp = /([A-Za-z]+) ?(\d+ )?(\d+)?/; 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) { Result.prototype.addRow = function(row) {
this.rows.push(row); this.rows.push(row);
}; };
//Add a field definition to the result Result.prototype.addFields = function(fieldDescriptions) {
Result.prototype.addField = function(field) { //clears field definitions
this.fields.push(field); //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; module.exports = Result;

View File

@ -61,7 +61,7 @@ public:
routine_symbol = NODE_PSYMBOL("routine"); routine_symbol = NODE_PSYMBOL("routine");
name_symbol = NODE_PSYMBOL("name"); name_symbol = NODE_PSYMBOL("name");
value_symbol = NODE_PSYMBOL("value"); value_symbol = NODE_PSYMBOL("value");
type_symbol = NODE_PSYMBOL("type"); type_symbol = NODE_PSYMBOL("dataTypeID");
channel_symbol = NODE_PSYMBOL("channel"); channel_symbol = NODE_PSYMBOL("channel");
payload_symbol = NODE_PSYMBOL("payload"); payload_symbol = NODE_PSYMBOL("payload");
command_symbol = NODE_PSYMBOL("command"); command_symbol = NODE_PSYMBOL("command");
@ -522,6 +522,33 @@ protected:
} }
return false; 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<Array> row = Array::New();
int fieldCount = PQnfields(result);
for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) {
Local<Object> 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<Value> e = (Handle<Value>)row;
Emit("_rowDescription", &e);
}
bool HandleResult(PGresult* result) bool HandleResult(PGresult* result)
{ {
TRACE("PQresultStatus"); TRACE("PQresultStatus");
@ -529,6 +556,7 @@ protected:
switch(status) { switch(status) {
case PGRES_TUPLES_OK: case PGRES_TUPLES_OK:
{ {
EmitRowDescription(result);
HandleTuplesResult(result); HandleTuplesResult(result);
EmitCommandMetaData(result); EmitCommandMetaData(result);
return true; return true;
@ -592,24 +620,14 @@ protected:
Local<Array> row = Array::New(); Local<Array> row = Array::New();
int fieldCount = PQnfields(result); int fieldCount = PQnfields(result);
for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) { for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) {
Local<Object> 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 //value of field
if(PQgetisnull(result, rowNumber, fieldNumber)) { if(PQgetisnull(result, rowNumber, fieldNumber)) {
field->Set(value_symbol, Null()); row->Set(Integer::New(fieldNumber), Null());
} else { } else {
char* fieldValue = PQgetvalue(result, rowNumber, fieldNumber); 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<Value> e = (Handle<Value>)row; Handle<Value> e = (Handle<Value>)row;

View File

@ -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);
});
}));
});

View File

@ -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();
}));
}));
});