Skip to content

add support for result rows as arrays #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 10, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 6 additions & 12 deletions lib/native/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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);
}
Expand Down
32 changes: 6 additions & 26 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
};

Expand Down
56 changes: 52 additions & 4 deletions lib/result.js
Original file line number Diff line number Diff line change
@@ -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+)?/;
Expand Down Expand Up @@ -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;
44 changes: 31 additions & 13 deletions src/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -522,13 +522,41 @@ 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<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)
{
TRACE("PQresultStatus");
ExecStatusType status = PQresultStatus(result);
switch(status) {
case PGRES_TUPLES_OK:
{
EmitRowDescription(result);
HandleTuplesResult(result);
EmitCommandMetaData(result);
return true;
Expand Down Expand Up @@ -592,24 +620,14 @@ class Connection : public ObjectWrap {
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));

//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<Value> e = (Handle<Value>)row;
Expand Down
33 changes: 33 additions & 0 deletions test/integration/client/results-as-array-tests.js
Original file line number Diff line number Diff line change
@@ -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);
});
}));
});
37 changes: 37 additions & 0 deletions test/integration/client/row-description-on-results-tests.js
Original file line number Diff line number Diff line change
@@ -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();
}));
}));
});