├── .gitignore ├── .npmignore ├── .travis.yml ├── test ├── config.js ├── testHelper.js ├── parserTests.js ├── basicTests.js ├── clientTests.js └── connectionTests.js ├── README.md ├── LICENSE.txt ├── package.json ├── lib ├── utils.js ├── writers.js ├── readers.js ├── types.js ├── connection.js ├── streams.js └── encoder.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | .idea 14 | 15 | npm-debug.log 16 | node_modules 17 | testing.js 18 | test/localConfig.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | *.tgz 11 | 12 | pids 13 | logs/ 14 | results 15 | docs/ 16 | examples/ 17 | support/ 18 | test/ 19 | testing.js 20 | .DS_Store 21 | .idea 22 | 23 | npm-debug.log 24 | node_modules 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_install: 6 | - sudo sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Djava.net.preferIPv4Stack=false\"' >> /usr/local/cassandra/conf/cassandra-env.sh" 7 | - sudo service cassandra start 8 | 9 | branches: 10 | only: 11 | - master 12 | - stable 13 | 14 | before_script: 15 | - sleep 5 -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var utils = require('../lib/utils.js'); 4 | 5 | var config = { 6 | "host": "localhost", 7 | "host2": "localhost", 8 | "port": 9042, 9 | "username": "cassandra", 10 | "password": "cassandra" 11 | }; 12 | 13 | if (fs.existsSync(path.resolve(__dirname, './localConfig.json'))) { 14 | var localConfig = require('./localConfig.json'); 15 | utils.extend(config, localConfig); 16 | } 17 | 18 | module.exports = config; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Node.js CQL Driver for Apache Cassandra ## 2 | 3 | This is a fork of [node-cassandra-cql](https://github.com/jorgebay/node-cassandra-cql) that includes 4 | [Kit Cambridge's normalize-typed-value-v1 branch](https://github.com/kitcambridge/node-cassandra-cql/tree/normalize-typed-value-v1) 5 | that is required to properly support `map` types under the binary v1 protocol. 6 | 7 | **This repository was specifically created to support the [priam](https://github.com/godaddy/node-priam) library without requiring a 8 | GitHub dependency under NPM.** 9 | -------------------------------------------------------------------------------- /test/testHelper.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var types = require('../lib/types.js'); 3 | 4 | var helper = { 5 | /** 6 | * Execute the query per each parameter array into paramsArray 7 | * @param {Connection|Client} con 8 | * @param {String} query 9 | * @param {Array} paramsArray Array of arrays of params 10 | * @param {Function} callback 11 | */ 12 | batchInsert: function (con, query, paramsArray, callback) { 13 | async.mapSeries(paramsArray, function (params, next) { 14 | con.execute(query, params, types.consistencies.one, next); 15 | }, callback); 16 | }, 17 | throwop: function (err) { 18 | if (err) throw err; 19 | } 20 | }; 21 | 22 | module.exports = helper; -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Jorge Bay Gondra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "priam-cassandra-cql", 3 | "version": "0.4.6", 4 | "description": "Node.js driver for Apache Cassandra - fork from node-cassandra-cql that includes Kit Cambridge's normalize-typed-value-v1 branch for the binary v1 protocol. Specifically used for the Go Daddy Priam driver.", 5 | "author": "Stephen Commisso ", 6 | "contributors" :[ 7 | "Jorge Bay", 8 | "Andrew Kish", 9 | "Tommy Messbauer", 10 | "Adam Faulkner", 11 | "Adrian Pike", 12 | "Suguru Namura", 13 | "Jan Schmidle", 14 | "Sam Grönblom", 15 | "Daniel Smedegaard Buus", 16 | "Bryce Baril", 17 | "Kit Cambridge" 18 | ], 19 | "keywords": [ 20 | "priam" 21 | ], 22 | "licenses": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://raw.githubusercontent.com/godaddy/node-cassandra-cql/normalize-typed-value-v1/LICENSE.txt" 26 | } 27 | ], 28 | "dependencies": { 29 | "async": ">= 0.2.5", 30 | "long": ">= 1.1.2", 31 | "node-uuid": "1.4.0" 32 | }, 33 | "devDependencies": { 34 | "mocha": ">= 1.14.0" 35 | }, 36 | "repository": { 37 | "type" : "git", 38 | "url" : "https://github.com/godaddy/node-cassandra-cql.git" 39 | }, 40 | "bugs": { 41 | "url" : "https://github.com/https://github.com/godaddy/node-cassandra-cql.git/node-cassandra-cql/issues" 42 | }, 43 | "engines": {"node" : ">=0.10.0"}, 44 | "scripts": { 45 | "test": "mocha test -R spec -t 5000" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | /** 3 | * Creates a copy of a buffer 4 | */ 5 | function copyBuffer(buf) { 6 | var targetBuffer = new Buffer(buf.length); 7 | buf.copy(targetBuffer); 8 | return targetBuffer; 9 | } 10 | 11 | /** 12 | * Appends the original stacktrace to the error after a tick of the event loop 13 | */ 14 | function fixStack(stackTrace, error) { 15 | error.stack += '\n (event loop)\n' + stackTrace.substr(stackTrace.indexOf("\n") + 1); 16 | return error; 17 | } 18 | 19 | /** 20 | * Gets the sum of the length of the items of an array 21 | */ 22 | function totalLength (arr) { 23 | if (!arr) { 24 | return 0; 25 | } 26 | var total = 0; 27 | arr.forEach(function (item) { 28 | var length = item.length; 29 | length = length ? length : 0; 30 | total += length; 31 | }); 32 | return total; 33 | } 34 | 35 | /** 36 | * Merge the contents of two or more objects together into the first object. Similar to jQuery.extend 37 | */ 38 | function extend(target) { 39 | var sources = [].slice.call(arguments, 1); 40 | sources.forEach(function (source) { 41 | for (var prop in source) { 42 | if (source.hasOwnProperty(prop)) { 43 | target[prop] = source[prop]; 44 | } 45 | } 46 | }); 47 | return target; 48 | } 49 | 50 | /** 51 | * Sync events: executes the callback when the event (with the same parameters) have been called in all emitters. 52 | */ 53 | function syncEvent(emitters, eventName, context, callback) { 54 | var thisKey = ''; 55 | var eventListener = getListener(eventName, context); 56 | emitters.forEach(function (item) { 57 | thisKey += '_' + item.constructor.name; 58 | item.on(eventName, eventListener); 59 | }); 60 | context[thisKey] = {emittersLength: emitters.length}; 61 | 62 | function getListener(eventName, context) { 63 | return function listener () { 64 | var argsKey = '_' + eventName + Array.prototype.slice.call(arguments).join('_'); 65 | var elements = context[thisKey]; 66 | if (typeof elements[argsKey] === 'undefined') { 67 | elements[argsKey] = 0; 68 | return; 69 | } 70 | else if (elements[argsKey] < elements.emittersLength-2){ 71 | elements[argsKey] = elements[argsKey] + 1; 72 | return; 73 | } 74 | delete elements[argsKey]; 75 | callback.apply(context, Array.prototype.slice.call(arguments)); 76 | }; 77 | } 78 | } 79 | 80 | /** 81 | * Parses the arguments used by exec methods. 82 | * Returns an array of parameters, containing also arguments as properties (query, params, consistency, options) 83 | */ 84 | function parseCommonArgs (query, params, consistency, options, callback) { 85 | var args = Array.prototype.slice.call(arguments); 86 | 87 | if (args.length < 2 || typeof args[args.length-1] !== 'function') { 88 | throw new Error('It should contain at least 2 arguments, with the callback as the last argument.'); 89 | } 90 | 91 | if(args.length < 5) { 92 | options = null; 93 | callback = args[args.length-1]; 94 | if (args.length < 4) { 95 | consistency = null; 96 | if (typeof params === 'number') { 97 | consistency = params; 98 | params = null; 99 | } 100 | } 101 | if (args.length < 3) { 102 | params = null; 103 | } 104 | } 105 | args.query = query; 106 | args.options = options; 107 | args.params = params; 108 | args.consistency = consistency; 109 | args.callback = callback; 110 | return args; 111 | } 112 | 113 | 114 | exports.copyBuffer = copyBuffer; 115 | exports.extend = extend; 116 | exports.totalLength = totalLength; 117 | exports.syncEvent = syncEvent; 118 | exports.parseCommonArgs = parseCommonArgs; 119 | exports.fixStack = fixStack; 120 | -------------------------------------------------------------------------------- /test/parserTests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var util = require('util'); 3 | var async = require('async'); 4 | 5 | var streams = require('../lib/streams.js'); 6 | var types = require('../lib/types.js'); 7 | 8 | /** 9 | * Tests for the transform streams that are involved in the reading of a response 10 | */ 11 | describe('Parser', function () { 12 | describe('#_transform()', function () { 13 | it('should read a READY opcode', function (done) { 14 | var parser = new streams.Parser({objectMode:true}); 15 | parser.on('readable', function () { 16 | var item = parser.read(); 17 | assert.strictEqual(item.header.bodyLength, 0); 18 | assert.strictEqual(item.header.opcode, types.opcodes.ready); 19 | done(); 20 | }); 21 | parser._transform({header: getFrameHeader(0, types.opcodes.ready), chunk: new Buffer([])}, null, doneIfError(done)); 22 | }); 23 | 24 | it('should read a AUTHENTICATE opcode', function (done) { 25 | var parser = new streams.Parser({objectMode:true}); 26 | parser.on('readable', function () { 27 | var item = parser.read(); 28 | assert.strictEqual(item.header.opcode, types.opcodes.authenticate); 29 | assert.ok(item.mustAuthenticate, 'it should return a mustAuthenticate return flag'); 30 | done(); 31 | }); 32 | parser._transform({header: getFrameHeader(0, types.opcodes.authenticate), chunk: new Buffer([])}, null, doneIfError(done)); 33 | }); 34 | 35 | it('should read a VOID result', function (done) { 36 | var parser = new streams.Parser({objectMode:true}); 37 | parser.on('readable', function () { 38 | var item = parser.read(); 39 | assert.strictEqual(item.header.bodyLength, 4); 40 | assert.strictEqual(item.header.opcode, types.opcodes.result); 41 | done(); 42 | }); 43 | parser._transform({ 44 | header: getFrameHeader(4, types.opcodes.result), 45 | chunk: new Buffer([0, 0, 0, types.resultKind.voidResult]) 46 | }, null, doneIfError(done)); 47 | }); 48 | 49 | it('should read a buffer until there is enough data', function (done) { 50 | var parser = new streams.Parser({objectMode:true}); 51 | parser.on('readable', function () { 52 | var item = parser.read(); 53 | assert.strictEqual(item.header.bodyLength, 4); 54 | assert.strictEqual(item.header.opcode, types.opcodes.result); 55 | done(); 56 | }); 57 | parser._transform({ 58 | header: getFrameHeader(4, types.opcodes.result), 59 | chunk: new Buffer([0]) 60 | }, null, doneIfError(done)); 61 | parser._transform({ 62 | header: getFrameHeader(4, types.opcodes.result), 63 | chunk: new Buffer([0, 0, types.resultKind.voidResult]) 64 | }, null, doneIfError(done)); 65 | }); 66 | 67 | it('should emit empty result one column no rows', function (done) { 68 | var parser = new streams.Parser({objectMode:true}); 69 | parser.on('readable', function () { 70 | var item = parser.read(); 71 | assert.strictEqual(item.header.opcode, types.opcodes.result); 72 | assert.ok(item.result && item.result.rows && item.result.rows.length === 0); 73 | done(); 74 | }); 75 | //kind 76 | parser._transform(getBodyChunks(1, 0, 0, 4), null, doneIfError(done)); 77 | //metadata 78 | parser._transform(getBodyChunks(1, 0, 4, 12), null, doneIfError(done)); 79 | //column names and rows 80 | parser._transform(getBodyChunks(1, 0, 12, null), null, doneIfError(done)); 81 | }); 82 | 83 | it('should emit empty result two columns no rows', function (done) { 84 | var parser = new streams.Parser({objectMode:true}); 85 | parser.on('readable', function () { 86 | var item = parser.read(); 87 | assert.strictEqual(item.header.opcode, types.opcodes.result); 88 | assert.ok(item.result && item.result.rows && item.result.rows.length === 0); 89 | done(); 90 | }); 91 | //2 columns, no rows, in one chunk 92 | parser._transform(getBodyChunks(2, 0, 0, null), null, doneIfError(done)); 93 | }); 94 | 95 | it('should emit row when rows present', function (done) { 96 | var parser = new streams.Parser({objectMode:true}); 97 | var rowLength = 2; 98 | var rowCounter = 0; 99 | parser.on('readable', function () { 100 | var item = parser.read(); 101 | assert.strictEqual(item.header.opcode, types.opcodes.result); 102 | assert.ok(item.row); 103 | if ((++rowCounter) === rowLength) { 104 | done(); 105 | } 106 | }); 107 | //2 columns, 1 rows 108 | parser._transform(getBodyChunks(3, rowLength, 0, 10), null, doneIfError(done)); 109 | parser._transform(getBodyChunks(3, rowLength, 10, 32), null, doneIfError(done)); 110 | parser._transform(getBodyChunks(3, rowLength, 32, 37), null, doneIfError(done)); 111 | parser._transform(getBodyChunks(3, rowLength, 37, null), null, doneIfError(done)); 112 | }); 113 | }); 114 | }); 115 | 116 | /** 117 | * Test Helper method to get a frame header 118 | * @returns {FrameHeader} 119 | */ 120 | function getFrameHeader(bodyLength, opcode) { 121 | var header = new types.FrameHeader(); 122 | header.bufferLength = bodyLength + 8; 123 | header.isResponse = true; 124 | header.version = 1; 125 | header.flags = 0; 126 | header.streamId = 12; 127 | header.opcode = opcode; 128 | header.bodyLength = bodyLength; 129 | return header; 130 | } 131 | 132 | function getBodyChunks(columnLength, rowLength, fromIndex, toIndex) { 133 | var i; 134 | var fullChunk = [ 135 | //kind 136 | 0, 0, 0, types.resultKind.rows, 137 | //flags and column count 138 | 0, 0, 0, 1, 0, 0, 0, columnLength, 139 | //column names 140 | 0, 1, 97, //string 'a' as ksname 141 | 0, 1, 98 //string 'b' as tablename 142 | ]; 143 | for (i = 0; i < columnLength; i++) { 144 | fullChunk = fullChunk.concat([ 145 | 0, 1, 99 + i, //string name, starting by 'c' as column name 146 | 0, types.dataTypes.text //short datatype 147 | ]); 148 | } 149 | //rows length 150 | fullChunk = fullChunk.concat([0, 0, 0, rowLength || 0]); 151 | for (i = 0; i < rowLength; i++) { 152 | var rowChunk = []; 153 | for (var j = 0; j < columnLength; j++) { 154 | //4 bytes length + bytes of each column value 155 | rowChunk.push(0); 156 | rowChunk.push(0); 157 | rowChunk.push(0); 158 | rowChunk.push(1); 159 | //value 160 | rowChunk.push(j); 161 | } 162 | fullChunk = fullChunk.concat(rowChunk); 163 | } 164 | 165 | return { 166 | header: getFrameHeader(fullChunk.length, types.opcodes.result), 167 | chunk: new Buffer(fullChunk.slice(fromIndex, toIndex || undefined)) 168 | }; 169 | } 170 | 171 | /** 172 | * Calls done in case there is an error 173 | */ 174 | function doneIfError(done) { 175 | return function (err) { 176 | if (err) done(err); 177 | }; 178 | } -------------------------------------------------------------------------------- /lib/writers.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var events = require('events'); 3 | var util = require('util'); 4 | 5 | var encoder = require('./encoder.js'); 6 | var types = require('./types.js'); 7 | var utils = require('./utils.js'); 8 | var FrameHeader = types.FrameHeader; 9 | /** 10 | * FrameWriter: Contains the logic to write all the different types to the frame. 11 | * Based on https://github.com/isaacbwagner/node-cql3/blob/master/lib/frameBuilder.js 12 | * under the MIT License https://github.com/isaacbwagner/node-cql3/blob/master/LICENSE 13 | */ 14 | function FrameWriter(opcodename, streamId) { 15 | if (!opcodename) { 16 | throw new Error('Opcode not provided'); 17 | } 18 | this.streamId = streamId; 19 | this.buffers = []; 20 | this.opcode = types.opcodes[opcodename.toString().toLowerCase()]; 21 | } 22 | 23 | FrameWriter.prototype.writeShort = function(num) { 24 | var buf = new Buffer(2); 25 | buf.writeUInt16BE(num, 0); 26 | this.buffers.push(buf); 27 | }; 28 | 29 | FrameWriter.prototype.writeInt = function(num) { 30 | var buf = new Buffer(4); 31 | buf.writeInt32BE(num, 0); 32 | this.buffers.push(buf); 33 | }; 34 | 35 | FrameWriter.prototype.writeBytes = function(bytes) { 36 | if(bytes === null) { 37 | this.writeInt(-1); 38 | } 39 | else { 40 | this.writeInt(bytes.length); 41 | this.buffers.push(bytes); 42 | } 43 | }; 44 | 45 | FrameWriter.prototype.writeShortBytes = function(bytes) { 46 | if(bytes === null) { 47 | this.writeShort(-1); 48 | } 49 | else { 50 | this.writeShort(bytes.length); 51 | 52 | this.buffers.push(bytes); 53 | } 54 | }; 55 | 56 | FrameWriter.prototype.writeString = function(str) { 57 | if (typeof str === "undefined") { 58 | throw new Error("can not write undefined"); 59 | } 60 | var len = Buffer.byteLength(str, 'utf8'); 61 | var buf = new Buffer(2 + len); 62 | buf.writeUInt16BE(len, 0); 63 | buf.write(str, 2, buf.length-2, 'utf8'); 64 | this.buffers.push(buf); 65 | }; 66 | 67 | FrameWriter.prototype.writeLString = function(str) { 68 | var len = Buffer.byteLength(str, 'utf8'); 69 | var buf = new Buffer(4 + len); 70 | buf.writeInt32BE(len, 0); 71 | buf.write(str, 4, buf.length-4, 'utf8'); 72 | this.buffers.push(buf); 73 | }; 74 | 75 | FrameWriter.prototype.writeStringList = function(strings) { 76 | this.writeShort (strings.length); 77 | var self = this; 78 | strings.forEach(function(str) { 79 | self.writeString(str); 80 | }); 81 | }; 82 | 83 | FrameWriter.prototype.writeStringMap = function (map) { 84 | var keys = []; 85 | for (var k in map) { 86 | if (map.hasOwnProperty(k)) { 87 | keys.push(k); 88 | } 89 | } 90 | 91 | this.writeShort(keys.length); 92 | 93 | for(var i = 0; i < keys.length; i++) { 94 | var key = keys[i]; 95 | this.writeString(key); 96 | this.writeString(map[key]); 97 | } 98 | }; 99 | 100 | FrameWriter.prototype.write = function() { 101 | var body = Buffer.concat(this.buffers); 102 | this.streamId = parseInt(this.streamId, 10); 103 | if (!(this.streamId >= 0 && this.streamId < 128)) { 104 | throw new types.DriverError('streamId must be a number from 0 to 127'); 105 | } 106 | var header = new FrameHeader({streamId: this.streamId, opcode: this.opcode, bodyLength: body.length}); 107 | 108 | return Buffer.concat([header.toBuffer(), body], body.length + FrameHeader.size); 109 | }; 110 | 111 | function QueryWriter(query, params, consistency) { 112 | this.query = query; 113 | this.params = params; 114 | this.consistency = consistency; 115 | this.streamId = null; 116 | if (consistency === null || typeof consistency === 'undefined') { 117 | this.consistency = types.consistencies.getDefault(); 118 | } 119 | } 120 | 121 | QueryWriter.prototype.write = function () { 122 | var frameWriter = new FrameWriter('QUERY', this.streamId); 123 | var query = types.queryParser.parse(this.query, this.params, encoder.stringifyValue); 124 | frameWriter.writeLString(query); 125 | frameWriter.writeShort(this.consistency); 126 | return frameWriter.write(); 127 | }; 128 | 129 | function PrepareQueryWriter(query) { 130 | this.streamId = null; 131 | this.query = query; 132 | } 133 | 134 | PrepareQueryWriter.prototype.write = function () { 135 | var frameWriter = new FrameWriter('PREPARE', this.streamId); 136 | frameWriter.writeLString(this.query); 137 | return frameWriter.write(); 138 | }; 139 | 140 | function StartupWriter(cqlVersion) { 141 | this.cqlVersion = cqlVersion; 142 | this.streamId = null; 143 | } 144 | 145 | StartupWriter.prototype.write = function() { 146 | var frameWriter = new FrameWriter('STARTUP', this.streamId); 147 | frameWriter.writeStringMap({ 148 | CQL_VERSION: this.cqlVersion 149 | }); 150 | return frameWriter.write(); 151 | }; 152 | 153 | function RegisterWriter(events) { 154 | this.events = events; 155 | this.streamId = null; 156 | } 157 | 158 | RegisterWriter.prototype.write = function() { 159 | var frameWriter = new FrameWriter('REGISTER', this.streamId); 160 | frameWriter.writeStringList(this.events); 161 | return frameWriter.write(); 162 | }; 163 | 164 | function CredentialsWriter(username, password) { 165 | this.username = username; 166 | this.password = password; 167 | this.streamId = null; 168 | } 169 | 170 | CredentialsWriter.prototype.write = function() { 171 | var frameWriter = new FrameWriter('CREDENTIALS', this.streamId); 172 | frameWriter.writeStringMap({username:this.username,password:this.password}); 173 | return frameWriter.write(); 174 | }; 175 | /** 176 | * Writes a execute query (given a prepared queryId) 177 | */ 178 | function ExecuteWriter(queryId, params, consistency) { 179 | this.queryId = queryId; 180 | this.params = params ? params : []; 181 | this.consistency = consistency; 182 | this.streamId = null; 183 | if (consistency === null || typeof consistency === 'undefined') { 184 | this.consistency = types.consistencies.getDefault(); 185 | } 186 | } 187 | 188 | ExecuteWriter.prototype.write = function () { 189 | var frameWriter = new FrameWriter('EXECUTE', this.streamId); 190 | frameWriter.writeShortBytes(this.queryId); 191 | frameWriter.writeShort(this.params.length); 192 | for (var i=0; i 0; 228 | }, 229 | function (next) { 230 | self.isRunning = true; 231 | var writeItem = self.queue.shift(); 232 | var data = null; 233 | var startTime = process.hrtime(); 234 | try { 235 | data = writeItem.writer.write(); 236 | self.emit('perf', 'serialize', process.hrtime(startTime)); 237 | } 238 | catch (err) { 239 | writeCallback(err); 240 | return; 241 | } 242 | self.netClient.write(data, writeCallback); 243 | 244 | function writeCallback(err) { 245 | writeItem.callback(err); 246 | //it is better to queue it up on the event loop 247 | //to allow IO between writes 248 | setImmediate(next); 249 | } 250 | }, 251 | function () { 252 | //the queue is empty 253 | self.isRunning = false; 254 | } 255 | ); 256 | }; 257 | 258 | exports.CredentialsWriter = CredentialsWriter; 259 | exports.PrepareQueryWriter = PrepareQueryWriter; 260 | exports.QueryWriter = QueryWriter; 261 | exports.RegisterWriter = RegisterWriter; 262 | exports.StartupWriter = StartupWriter; 263 | exports.ExecuteWriter = ExecuteWriter; 264 | exports.WriteQueue = WriteQueue; 265 | -------------------------------------------------------------------------------- /lib/readers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/isaacbwagner/node-cql3/blob/master/lib/frameParser.js 3 | * under the MIT License https://github.com/isaacbwagner/node-cql3/blob/master/LICENSE 4 | */ 5 | var util = require('util'); 6 | var utils = require('./utils.js'); 7 | var types = require('./types.js'); 8 | 9 | /** 10 | * Buffer forward reader of CQL binary frames 11 | */ 12 | function FrameReader(header, body) { 13 | this.header = header; 14 | this.opcode = header.opcode; 15 | this.offset = 0; 16 | this.buf = body; 17 | } 18 | 19 | FrameReader.prototype.remainingLength = function () { 20 | return this.buf.length - this.offset; 21 | }; 22 | 23 | FrameReader.prototype.getBuffer = function () { 24 | return this.buf; 25 | }; 26 | 27 | /** 28 | * Slices the underlining buffer 29 | */ 30 | FrameReader.prototype.slice = function (begin, end) { 31 | if (typeof end === 'undefined') { 32 | end = this.buf.length; 33 | } 34 | return this.buf.slice(begin, end); 35 | }; 36 | 37 | /** 38 | * Modifies the underlying buffer, it concatenates the given buffer with the original (internalBuffer = concat(bytes, internalBuffer) 39 | */ 40 | FrameReader.prototype.unshift = function (bytes) { 41 | if (this.offset > 0) { 42 | throw new Error('Can not modify the underlying buffer if already read'); 43 | } 44 | this.buf = Buffer.concat([bytes, this.buf], bytes.length + this.buf.length); 45 | }; 46 | 47 | /** 48 | * Reads any number of bytes and moves the offset. 49 | * if length not provided or it's larger than the remaining bytes, reads to end. 50 | */ 51 | FrameReader.prototype.read = function (length) { 52 | var end = this.buf.length; 53 | if (typeof length !== 'undefined' && this.offset + length < this.buf.length) { 54 | end = this.offset + length; 55 | } 56 | var bytes = this.slice(this.offset, end); 57 | this.offset = end; 58 | return bytes; 59 | }; 60 | 61 | /** 62 | * Moves the reader cursor to the end 63 | */ 64 | FrameReader.prototype.toEnd = function () { 65 | this.offset = this.buf.length; 66 | }; 67 | 68 | FrameReader.prototype.readInt = function() { 69 | var result = this.buf.readInt32BE(this.offset); 70 | this.offset += 4; 71 | return result; 72 | }; 73 | 74 | FrameReader.prototype.readShort = function () { 75 | var result = this.buf.readUInt16BE(this.offset); 76 | this.offset += 2; 77 | return result; 78 | }; 79 | 80 | FrameReader.prototype.readByte = function () { 81 | var result = this.buf.readUInt8(this.offset); 82 | this.offset += 1; 83 | return result; 84 | }; 85 | 86 | FrameReader.prototype.readString = function () { 87 | var length = this.readShort(); 88 | this.checkOffset(length); 89 | var result = this.buf.toString('utf8', this.offset, this.offset+length); 90 | this.offset += length; 91 | return result; 92 | }; 93 | 94 | /** 95 | * Checks that the new length to read is within the range of the buffer length. Throws a RangeError if not. 96 | */ 97 | FrameReader.prototype.checkOffset = function (newLength) { 98 | if (this.offset + newLength > this.buf.length) { 99 | throw new RangeError('Trying to access beyond buffer length'); 100 | } 101 | }; 102 | 103 | FrameReader.prototype.readUUID = function () { 104 | var octets = []; 105 | for (var i = 0; i < 16; i++) { 106 | octets.push(this.readByte()); 107 | } 108 | 109 | var str = ""; 110 | 111 | octets.forEach(function(octet) { 112 | str += octet.toString(16); 113 | }); 114 | 115 | return str.slice(0, 8) + '-' + str.slice(8, 12) + '-' + str.slice(12, 16) + '-' + str.slice(16, 20) + '-' + str.slice(20); 116 | }; 117 | 118 | FrameReader.prototype.readStringList = function () { 119 | var num = this.readShort(); 120 | 121 | var list = []; 122 | 123 | for (var i = 0; i < num; i++) { 124 | list.push(this.readString()); 125 | } 126 | 127 | return list; 128 | }; 129 | /** 130 | * Reads the amount of bytes that the field has and returns them (slicing them). 131 | */ 132 | FrameReader.prototype.readBytes = function () { 133 | var length = this.readInt(); 134 | if (length < 0) { 135 | return null; 136 | } 137 | this.checkOffset(length); 138 | 139 | return this.read(length); 140 | }; 141 | 142 | FrameReader.prototype.readShortBytes = function () { 143 | var length = this.readShort(); 144 | if (length < 0) { 145 | return null; 146 | } 147 | this.checkOffset(length); 148 | return this.read(length); 149 | }; 150 | 151 | /* returns an array with two elements */ 152 | FrameReader.prototype.readOption = function () { 153 | var id = this.readShort(); 154 | 155 | switch(id) { 156 | case 0x0000: 157 | return [id, this.readString()]; 158 | case 0x0001: 159 | case 0x0002: 160 | case 0x0003: 161 | case 0x0004: 162 | case 0x0005: 163 | case 0x0006: 164 | case 0x0007: 165 | case 0x0008: 166 | case 0x0009: 167 | case 0x000A: 168 | case 0x000B: 169 | case 0x000C: 170 | case 0x000D: 171 | case 0x000E: 172 | case 0x000F: 173 | case 0x0010: 174 | return [id, null]; 175 | case 0x0020: 176 | return [id, this.readOption()]; 177 | case 0x0021: 178 | return [id, [this.readOption(), this.readOption()]]; 179 | case 0x0022: 180 | return [id, this.readOption()]; 181 | } 182 | 183 | return [id, null]; 184 | }; 185 | 186 | /* returns an array of arrays */ 187 | FrameReader.prototype.readOptionList = function () { 188 | var num = this.readShort(); 189 | var options = []; 190 | for(var i = 0; i < num; i++) { 191 | options.push(this.readOption()); 192 | } 193 | return options; 194 | }; 195 | 196 | FrameReader.prototype.readInet = function () { 197 | //TODO 198 | }; 199 | 200 | FrameReader.prototype.readStringMap = function () { 201 | var num = this.readShort(); 202 | var map = {}; 203 | for(var i = 0; i < num; i++) { 204 | var key = this.readString(); 205 | var value = this.readString(); 206 | map[key] = value; 207 | } 208 | return map; 209 | }; 210 | 211 | FrameReader.prototype.readStringMultimap = function () { 212 | var num = this.readShort(); 213 | var map = {}; 214 | for(var i = 0; i < num; i++) { 215 | var key = this.readString(); 216 | map[key] = this.readStringList(); 217 | } 218 | return map; 219 | }; 220 | 221 | /** 222 | * Reads the metadata from a row or a prepared result response 223 | * @returns {Object} 224 | */ 225 | FrameReader.prototype.readMetadata = function() { 226 | var meta = {}; 227 | //as used in Rows and Prepared responses 228 | var flags = this.readInt(); 229 | 230 | var columnCount = this.readInt(); 231 | 232 | if(flags & 0x0001) { 233 | //only one table spec is provided 234 | meta.global_tables_spec = true; 235 | meta.keyspace = this.readString(); 236 | meta.table = this.readString(); 237 | } 238 | 239 | meta.columns = []; 240 | 241 | for(var i = 0; i < columnCount; i++) { 242 | var spec = {}; 243 | if(!meta.global_tables_spec) { 244 | spec.ksname = this.readString(); 245 | spec.tablename = this.readString(); 246 | } 247 | 248 | spec.name = this.readString(); 249 | spec.type = this.readOption(); 250 | meta.columns.push(spec); 251 | //Store the column index by name, to be able to find the column by name 252 | meta.columns['_col_' + spec.name] = i; 253 | } 254 | 255 | return meta; 256 | }; 257 | 258 | /** 259 | * Try to read a value, in case there is a RangeError (out of bounds) it returns null 260 | * @param method 261 | */ 262 | FrameReader.prototype.tryRead = function (method) { 263 | var value; 264 | try{ 265 | value = method(); 266 | } 267 | catch (e) { 268 | if (e instanceof RangeError) { 269 | return null; 270 | } 271 | throw e; 272 | } 273 | 274 | }; 275 | 276 | /** 277 | * Reads the error from the frame 278 | * @throws {RangeError} 279 | * @returns {ResponseError} 280 | */ 281 | FrameReader.prototype.readError = function () { 282 | var code = this.readInt(); 283 | var message = this.readString(); 284 | //determine if the server is unhealthy 285 | //if true, the client should not retry for a while 286 | var isServerUnhealthy = false; 287 | switch (code) { 288 | case types.responseErrorCodes.serverError: 289 | case types.responseErrorCodes.overloaded: 290 | case types.responseErrorCodes.isBootstrapping: 291 | isServerUnhealthy = true; 292 | break; 293 | } 294 | return new ResponseError(code, message, isServerUnhealthy); 295 | }; 296 | 297 | function readEvent(data, emitter) { 298 | var reader = new FrameReader(data); 299 | var event = reader.readString(); 300 | if(event === 'TOPOLOGY_CHANGE') { 301 | emitter.emit(event, reader.readString(), reader.readInet()); 302 | } 303 | else if (event === 'STATUS_CHANGE') { 304 | emitter.emit(event, reader.readString(), reader.readInet()); 305 | } 306 | else if (event === 'SCHEMA_CHANGE') { 307 | emitter.emit(event, reader.readString(), reader.readString(), reader.readString()); 308 | } 309 | else { 310 | throw new Error('Unknown EVENT type: ' + event); 311 | } 312 | } 313 | 314 | function ResponseError(code, message, isServerUnhealthy) { 315 | ResponseError.super_.call(this, message, this.constructor); 316 | this.code = code; 317 | this.isServerUnhealthy = isServerUnhealthy; 318 | this.info = 'Represents a error message from the server'; 319 | } 320 | 321 | util.inherits(ResponseError, types.DriverError); 322 | 323 | exports.readEvent = readEvent; 324 | exports.FrameReader = FrameReader; -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var stream = require('stream'); 3 | var async = require('async'); 4 | var utils = require('./utils.js'); 5 | var uuidGenerator = require('node-uuid'); 6 | 7 | //instances 8 | 9 | /** 10 | * Consistency levels 11 | */ 12 | var consistencies = { 13 | any: 0x00, 14 | one: 0x01, 15 | two: 0x02, 16 | three: 0x03, 17 | quorum: 0x04, 18 | all: 0x05, 19 | localQuorum: 0x06, 20 | eachQuorum: 0x07, 21 | localOne: 0x10, 22 | getDefault: function () { 23 | return this.quorum; 24 | } 25 | }; 26 | 27 | var reList = /^(list|set)\s*<\s*(\w+)\s*>$/i; 28 | var reMap = /^map\s*<\s*(\w+)\s*,\s*(\w+)/i; 29 | 30 | /** 31 | * CQL data types 32 | */ 33 | var dataTypes = { 34 | custom: 0x0000, 35 | ascii: 0x0001, 36 | bigint: 0x0002, 37 | blob: 0x0003, 38 | boolean: 0x0004, 39 | counter: 0x0005, 40 | decimal: 0x0006, 41 | double: 0x0007, 42 | float: 0x0008, 43 | int: 0x0009, 44 | text: 0x000a, 45 | timestamp: 0x000b, 46 | uuid: 0x000c, 47 | varchar: 0x000d, 48 | varint: 0x000e, 49 | timeuuid: 0x000f, 50 | inet: 0x0010, 51 | list: 0x0020, 52 | map: 0x0021, 53 | set: 0x0022, 54 | getByName: function(name) { 55 | var typeInfo = { name: name.toLowerCase() }; 56 | var listMatches = reList.exec(typeInfo.name); 57 | if (listMatches) { 58 | typeInfo.name = listMatches[1].toLowerCase(); 59 | typeInfo.subtype = listMatches[2]; 60 | } 61 | else { 62 | var mapMatches = reMap.exec(typeInfo.name); 63 | if (mapMatches) { 64 | typeInfo.name = 'map'; 65 | typeInfo.keyType = mapMatches[1]; 66 | typeInfo.valueType = mapMatches[2]; 67 | } 68 | } 69 | typeInfo.type = this[typeInfo.name]; 70 | if (typeof typeInfo.type !== 'number') { 71 | throw new TypeError('Datatype with name ' + name + ' not valid', null); 72 | } 73 | return typeInfo; 74 | } 75 | }; 76 | 77 | /** 78 | * An integer byte that distinguish the actual message from and to Cassandra 79 | */ 80 | var opcodes = { 81 | error: 0x00, 82 | startup: 0x01, 83 | ready: 0x02, 84 | authenticate: 0x03, 85 | credentials: 0x04, 86 | options: 0x05, 87 | supported: 0x06, 88 | query: 0x07, 89 | result: 0x08, 90 | prepare: 0x09, 91 | execute: 0x0a, 92 | register: 0x0b, 93 | event: 0x0c, 94 | /** 95 | * Determines if the code is a valid opcode 96 | */ 97 | isInRange: function (code) { 98 | return code > this.error && code > this.event; 99 | } 100 | }; 101 | 102 | /** 103 | * Parses a string query and stringifies the parameters 104 | */ 105 | var queryParser = { 106 | /** 107 | * Replaced the query place holders with the stringified value 108 | * @param {String} query 109 | * @param {Array} params 110 | * @param {Function} stringifier 111 | */ 112 | parse: function (query, params, stringifier) { 113 | if (!query || !query.length || !params) { 114 | return query; 115 | } 116 | if (!stringifier) { 117 | stringifier = function (a) {return a.toString()}; 118 | } 119 | var parts = []; 120 | var isLiteral = false; 121 | var lastIndex = 0; 122 | var paramsCounter = 0; 123 | for (var i = 0; i < query.length; i++) { 124 | var char = query.charAt(i); 125 | if (char === "'" && query.charAt(i-1) !== '\\') { 126 | //opening or closing quotes in a literal value of the query 127 | isLiteral = !isLiteral; 128 | } 129 | if (!isLiteral && char === '?') { 130 | //is a placeholder 131 | parts.push(query.substring(lastIndex, i)); 132 | parts.push(stringifier(params[paramsCounter++])); 133 | lastIndex = i+1; 134 | } 135 | } 136 | parts.push(query.substring(lastIndex)); 137 | return parts.join(''); 138 | } 139 | }; 140 | 141 | /** 142 | * Server error codes returned by Cassandra 143 | */ 144 | var responseErrorCodes = { 145 | serverError: 0x0000, 146 | protocolError: 0x000A, 147 | badCredentials: 0x0100, 148 | unavailableException: 0x1000, 149 | overloaded: 0x1001, 150 | isBootstrapping: 0x1002, 151 | truncateError: 0x1003, 152 | writeTimeout: 0x1100, 153 | readTimeout: 0x1200, 154 | syntaxError: 0x2000, 155 | unauthorized: 0x2100, 156 | invalid: 0x2200, 157 | configError: 0x2300, 158 | alreadyExists: 0x2400, 159 | unprepared: 0x2500 160 | }; 161 | 162 | /** 163 | * Type of result included in a response 164 | */ 165 | var resultKind = { 166 | voidResult: 0x0001, 167 | rows: 0x0002, 168 | setKeyspace: 0x0003, 169 | prepared: 0x0004, 170 | schemaChange: 0x0005 171 | }; 172 | 173 | /** 174 | * Generates and returns a RFC4122 v1 (timestamp based) UUID. 175 | * Uses node-uuid module as generator. 176 | */ 177 | function timeuuid(options, buffer, offset) { 178 | return uuidGenerator.v1(options, buffer, offset); 179 | } 180 | 181 | /** 182 | * Generate and return a RFC4122 v4 UUID. 183 | * Uses node-uuid module as generator. 184 | */ 185 | function uuid(options, buffer, offset) { 186 | return uuidGenerator.v4(options, buffer, offset); 187 | } 188 | 189 | //classes 190 | 191 | /** 192 | * Represents a frame header that could be used to read from a Buffer or to write to a Buffer 193 | */ 194 | function FrameHeader(values) { 195 | if (values) { 196 | if (values instanceof Buffer) { 197 | this.fromBuffer(values); 198 | } 199 | else { 200 | for (var prop in values) { 201 | this[prop] = values[prop]; 202 | } 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * The length of the header of the protocol 209 | */ 210 | FrameHeader.size = 8; 211 | FrameHeader.prototype.version = 1; 212 | FrameHeader.prototype.flags = 0x0; 213 | FrameHeader.prototype.streamId = null; 214 | FrameHeader.prototype.opcode = null; 215 | FrameHeader.prototype.bodyLength = 0; 216 | 217 | FrameHeader.prototype.fromBuffer = function (buf) { 218 | if (buf.length < FrameHeader.size) { 219 | //there is not enough data to read the header 220 | return; 221 | } 222 | this.bufferLength = buf.length; 223 | this.isResponse = buf[0] & 0x80; 224 | this.version = buf[0] & 0x7F; 225 | this.flags = buf.readUInt8(1); 226 | this.streamId = buf.readInt8(2); 227 | this.opcode = buf.readUInt8(3); 228 | this.bodyLength = buf.readUInt32BE(4); 229 | }; 230 | 231 | FrameHeader.prototype.toBuffer = function () { 232 | var buf = new Buffer(FrameHeader.size); 233 | buf.writeUInt8(0 + this.version, 0); 234 | buf.writeUInt8(this.flags, 1); 235 | buf.writeUInt8(this.streamId, 2); 236 | buf.writeUInt8(this.opcode, 3); 237 | buf.writeUInt32BE(this.bodyLength, 4); 238 | return buf; 239 | }; 240 | 241 | /** 242 | * Long constructor, wrapper of the internal library used. 243 | */ 244 | var Long = require('long'); 245 | /** 246 | * Returns a long representation. 247 | * Used internally for deserialization 248 | */ 249 | Long.fromBuffer = function (value) { 250 | if (!(value instanceof Buffer)) { 251 | throw new TypeError('Expected Buffer', value, Buffer); 252 | } 253 | return new Long(value.readInt32BE(4), value.readInt32BE(0, 4)); 254 | }; 255 | 256 | /** 257 | * Returns a big-endian buffer representation of the Long instance 258 | * @param {Long} value 259 | */ 260 | Long.toBuffer = function (value) { 261 | if (!(value instanceof Long)) { 262 | throw new TypeError('Expected Long', value, Long); 263 | } 264 | var buffer = new Buffer(8); 265 | buffer.writeUInt32BE(value.getHighBitsUnsigned(), 0); 266 | buffer.writeUInt32BE(value.getLowBitsUnsigned(), 4); 267 | return buffer; 268 | }; 269 | 270 | /** 271 | * Wraps a value to be included as literal in a query 272 | */ 273 | function QueryLiteral (value) { 274 | this.value = value; 275 | } 276 | 277 | QueryLiteral.prototype.toString = function () { 278 | return this.value.toString(); 279 | }; 280 | 281 | /** 282 | * Queues callbacks while the condition tests true. Similar behaviour as async.whilst. 283 | */ 284 | function QueueWhile(test, delayRetry) { 285 | this.queue = async.queue(function (task, queueCallback) { 286 | async.whilst( 287 | test, 288 | function(cb) { 289 | //Retry in a while 290 | if (delayRetry) { 291 | setTimeout(cb, delayRetry); 292 | } 293 | else { 294 | setImmediate(cb); 295 | } 296 | }, 297 | function() { 298 | queueCallback(null, null); 299 | } 300 | ); 301 | }, 1); 302 | } 303 | 304 | QueueWhile.prototype.push = function (callback) { 305 | this.queue.push({}, callback); 306 | }; 307 | 308 | /** 309 | * Readable stream using to yield data from a result or a field 310 | */ 311 | function ResultStream(opt) { 312 | stream.Readable.call(this, opt); 313 | this.buffer = []; 314 | this.paused = true; 315 | } 316 | 317 | util.inherits(ResultStream, stream.Readable); 318 | 319 | ResultStream.prototype._read = function() { 320 | this.paused = false; 321 | if (this.buffer.length === 0) { 322 | this._readableState.reading = false; 323 | } 324 | while (!this.paused && this.buffer.length > 0) { 325 | this.paused = this.push(this.buffer.shift()); 326 | } 327 | }; 328 | 329 | ResultStream.prototype.add = function (chunk) { 330 | this.buffer.push(chunk); 331 | this.read(0); 332 | }; 333 | 334 | /** 335 | * Represents a result row 336 | */ 337 | function Row(columns) { 338 | this.columns = columns; 339 | } 340 | 341 | /** 342 | * Returns the cell value. 343 | * Created for backward compatibility: use row[columnName] instead. 344 | * @param {String|Number} columnName Name or index of the column 345 | */ 346 | Row.prototype.get = function (columnName) { 347 | if (typeof columnName === 'number') { 348 | if (this.columns && this.columns[columnName]) { 349 | columnName = this.columns[columnName].name; 350 | } 351 | else { 352 | throw new Error('Column not found'); 353 | } 354 | } 355 | return this[columnName]; 356 | }; 357 | 358 | //error classes 359 | 360 | /** 361 | * Base Error 362 | */ 363 | function DriverError (message, constructor) { 364 | if (constructor) { 365 | Error.captureStackTrace(this, constructor); 366 | this.name = constructor.name; 367 | } 368 | this.message = message || 'Error'; 369 | this.info = 'Cassandra Driver Error'; 370 | } 371 | util.inherits(DriverError, Error); 372 | 373 | 374 | function QueryParserError(e) { 375 | QueryParserError.super_.call(this, e.message, this.constructor); 376 | this.internalError = e; 377 | } 378 | util.inherits(QueryParserError, DriverError); 379 | 380 | function TimeoutError (message) { 381 | TimeoutError.super_.call(this, message, this.constructor); 382 | this.info = 'Represents an error that happens when the maximum amount of time for an operation passed.'; 383 | } 384 | util.inherits(TimeoutError, DriverError); 385 | 386 | function TypeError (message, value, expectedType, actualType, reference) { 387 | if (!message) { 388 | message = 'Type not supported for operation'; 389 | } 390 | TimeoutError.super_.call(this, message, this.constructor); 391 | this.value = value; 392 | this.info = 'Represents an error that happens when trying to convert from one type to another.'; 393 | if (expectedType) { 394 | this.expectedType = expectedType; 395 | } 396 | if (this.actualType) { 397 | this.actualType = actualType; 398 | } 399 | if (this.reference) { 400 | this.reference = reference; 401 | } 402 | } 403 | util.inherits(TypeError, DriverError); 404 | 405 | exports.opcodes = opcodes; 406 | exports.consistencies = consistencies; 407 | exports.dataTypes = dataTypes; 408 | exports.queryParser = queryParser; 409 | exports.responseErrorCodes = responseErrorCodes; 410 | exports.resultKind = resultKind; 411 | exports.timeuuid = timeuuid; 412 | exports.uuid = uuid; 413 | exports.FrameHeader = FrameHeader; 414 | exports.Long = Long; 415 | exports.QueryLiteral = QueryLiteral; 416 | exports.QueueWhile = QueueWhile; 417 | exports.ResultStream = ResultStream; 418 | exports.Row = Row; 419 | exports.DriverError = DriverError; 420 | exports.TimeoutError = TimeoutError; -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | var util = require('util'); 4 | var writers = require('./writers.js'); 5 | var streams = require('./streams.js'); 6 | var utils = require('./utils.js'); 7 | var types = require('./types.js'); 8 | 9 | var optionsDefault = { 10 | port: 9042, 11 | version: '3.0.0', 12 | //max simultaneous requests (before waiting for a response) (max=128) 13 | maxRequests: 32, 14 | //When the simultaneous requests has been reached, it determines the amount of milliseconds before retrying to get an available streamId 15 | maxRequestsRetry: 100, 16 | //Connect timeout: time to wait when trying to connect to a host, 17 | connectTimeout: 2000 18 | }; 19 | function Connection(options) { 20 | events.EventEmitter.call(this); 21 | 22 | this.streamHandlers = {}; 23 | this.options = utils.extend({}, optionsDefault, options); 24 | } 25 | 26 | util.inherits(Connection, events.EventEmitter); 27 | 28 | Connection.prototype.createSocket = function() { 29 | var self = this; 30 | if (this.netClient) { 31 | this.netClient.destroy(); 32 | } 33 | this.netClient = new net.Socket(); 34 | this.writeQueue = new writers.WriteQueue(this.netClient); 35 | var protocol = new streams.Protocol({objectMode: true}); 36 | this.parser = new streams.Parser({objectMode: true}); 37 | var resultEmitter = new streams.ResultEmitter({objectMode: true}); 38 | this.netClient 39 | .pipe(protocol) 40 | .pipe(this.parser) 41 | .pipe(resultEmitter); 42 | 43 | resultEmitter.on('result', this.handleResult.bind(this)); 44 | resultEmitter.on('row', this.handleRow.bind(this)); 45 | resultEmitter.on('frameEnded', this.freeStreamId.bind(this)); 46 | 47 | this.netClient.on('close', function() { 48 | self.emit('log', 'info', 'Socket disconnected'); 49 | self.connected = false; 50 | self.connecting = false; 51 | self.invokePendingCallbacks(); 52 | }); 53 | }; 54 | 55 | /** 56 | * Connects a socket and sends the startup protocol messages, including authentication and the keyspace used. 57 | */ 58 | Connection.prototype.open = function (callback) { 59 | var self = this; 60 | self.emit('log', 'info', 'Connecting to ' + this.options.host + ':' + this.options.port); 61 | self.createSocket(); 62 | self.connecting = true; 63 | function errorConnecting (err, destroy) { 64 | self.connecting = false; 65 | if (destroy) { 66 | //there is a TCP connection that should be killed. 67 | self.netClient.destroy(); 68 | } 69 | callback(err); 70 | } 71 | this.netClient.once('error', errorConnecting); 72 | this.netClient.once('timeout', function connectTimeout() { 73 | var err = new types.DriverError('Connection timeout'); 74 | errorConnecting(err, true); 75 | }); 76 | this.netClient.setTimeout(this.options.connectTimeout); 77 | 78 | this.netClient.connect(this.options.port, this.options.host, function connectCallback() { 79 | self.emit('log', 'info', 'Socket connected to ' + self.options.host + ':' + self.options.port); 80 | self.netClient.removeListener('error', errorConnecting); 81 | self.netClient.removeAllListeners('connect'); 82 | self.netClient.removeAllListeners('timeout'); 83 | 84 | self.sendStream(new writers.StartupWriter(self.options.version), null, function (err, response) { 85 | if (response && response.mustAuthenticate) { 86 | return self.authenticate(startupCallback); 87 | } 88 | startupCallback(err); 89 | }); 90 | }); 91 | 92 | function startupCallback(err) { 93 | if (err) { 94 | return errorConnecting(err, true); 95 | } 96 | //The socket is connected and the connection is authenticated 97 | if (!self.options.keyspace) { 98 | return self.connectionReady(callback); 99 | } 100 | //Use the keyspace 101 | self.execute('USE ' + self.options.keyspace + ';', null, function (err) { 102 | if (err) { 103 | return errorConnecting(err, true); 104 | } 105 | self.connectionReady(callback); 106 | }); 107 | } 108 | }; 109 | 110 | /** 111 | * Sets the connection to ready/connected status 112 | */ 113 | Connection.prototype.connectionReady = function (callback) { 114 | this.emit('connected'); 115 | this.connected = true; 116 | this.connecting = false; 117 | this.netClient.on('error', this.handleSocketError.bind(this)); 118 | callback(); 119 | }; 120 | 121 | /** 122 | * Handle socket errors, if the socket is not readable invoke all pending callbacks 123 | */ 124 | Connection.prototype.handleSocketError = function (err) { 125 | this.emit('log', 'error', 'Socket error ' + err, 'r/w:', this.netClient.readable, this.netClient.writable); 126 | this.invokePendingCallbacks(err); 127 | }; 128 | 129 | /** 130 | * Invokes all pending callback of sent streams 131 | */ 132 | Connection.prototype.invokePendingCallbacks = function (innerError) { 133 | var err = new types.DriverError('Socket was closed'); 134 | err.isServerUnhealthy = true; 135 | if (innerError) { 136 | err.innerError = innerError; 137 | } 138 | //invoke all pending callbacks 139 | var handlers = []; 140 | for (var streamId in this.streamHandlers) { 141 | if (this.streamHandlers.hasOwnProperty(streamId)) { 142 | handlers.push(this.streamHandlers[streamId]); 143 | } 144 | } 145 | this.streamHandlers = {}; 146 | if (handlers.length > 0) { 147 | this.emit('log', 'info', 'Invoking ' + handlers.length + ' pending callbacks'); 148 | } 149 | handlers.forEach(function (item) { 150 | if (!item.callback) return; 151 | item.callback(err); 152 | }); 153 | }; 154 | 155 | Connection.prototype.authenticate = function(callback) { 156 | if (!this.options.username) { 157 | return callback(new Error("Server needs authentication which was not provided")); 158 | } 159 | this.sendStream(new writers.CredentialsWriter(this.options.username, this.options.password), null, callback); 160 | }; 161 | /** 162 | * Executes a query sending a QUERY stream to the host 163 | */ 164 | Connection.prototype.execute = function () { 165 | var args = utils.parseCommonArgs.apply(null, arguments); 166 | this.emit('log', 'info', 'executing query: ' + args.query); 167 | this.sendStream(new writers.QueryWriter(args.query, args.params, args.consistency), null, args.callback); 168 | }; 169 | 170 | /** 171 | * Executes a (previously) prepared statement and yields the rows into a ReadableStream 172 | * @returns {ResultStream} 173 | */ 174 | Connection.prototype.stream = function () { 175 | var args = utils.parseCommonArgs.apply(null, arguments); 176 | this.emit('log', 'info', 'Executing for streaming prepared query: 0x' + args.query.toString('hex')); 177 | 178 | var resultStream = new types.ResultStream({objectMode:true}); 179 | this.sendStream( 180 | new writers.ExecuteWriter(args.query, args.params, args.consistency), 181 | utils.extend({}, args.options, {resultStream: resultStream}), 182 | args.callback); 183 | return resultStream; 184 | }; 185 | 186 | /** 187 | * Executes a (previously) prepared statement with a given id 188 | * @param {Buffer} queryId 189 | * @param {Array} [params] 190 | * @param {Number} [consistency] 191 | * @param [options] 192 | * @param {function} callback 193 | */ 194 | Connection.prototype.executePrepared = function () { 195 | var args = utils.parseCommonArgs.apply(null, arguments); 196 | this.emit('log', 'info', 'Executing prepared query: 0x' + args.query.toString('hex')); 197 | //When using each row, the final (end) callback is optional 198 | if (args.options && args.options.byRow && !args.options.rowCallback) { 199 | args.options.rowCallback = args.callback; 200 | args.callback = null; 201 | } 202 | this.sendStream( 203 | new writers.ExecuteWriter(args.query, args.params, args.consistency), 204 | args.options, 205 | args.callback); 206 | }; 207 | 208 | /** 209 | * Prepares a query on a host 210 | * @param {String} query 211 | * @param {function} callback 212 | */ 213 | Connection.prototype.prepare = function (query, callback) { 214 | this.emit('log', 'info', 'Preparing query: ' + query); 215 | this.sendStream(new writers.PrepareQueryWriter(query), null, callback); 216 | }; 217 | 218 | Connection.prototype.register = function register (events, callback) { 219 | this.sendStream(new writers.RegisterWriter(events), null, callback); 220 | }; 221 | 222 | /** 223 | * Uses the frame writer to write into the wire 224 | * @param frameWriter 225 | * @param [options] 226 | * @param {function} [callback] 227 | */ 228 | Connection.prototype.sendStream = function (frameWriter, options, callback) { 229 | var self = this; 230 | this.getStreamId(function (streamId) { 231 | this.emit('log', 'info', 'Sending stream #' + streamId); 232 | frameWriter.streamId = streamId; 233 | this.writeQueue.push(frameWriter, writeCallback); 234 | }); 235 | if (!callback) { 236 | callback = function noop () {}; 237 | } 238 | 239 | function writeCallback (err) { 240 | if (err) { 241 | if (!(err instanceof TypeError)) { 242 | //TypeError is raised when there is a serialization issue 243 | //If it is not a serialization issue is a socket issue 244 | err.isServerUnhealthy = true; 245 | } 246 | return callback(err); 247 | } 248 | if (frameWriter instanceof writers.ExecuteWriter) { 249 | if (options && options.byRow) { 250 | self.parser.setOptions(frameWriter.streamId, {byRow: true, streamField: options.streamField}); 251 | } 252 | else if (options && options.resultStream) { 253 | self.parser.setOptions(frameWriter.streamId, {resultStream: options.resultStream}); 254 | } 255 | } 256 | self.emit('log', 'info', 'Sent stream #' + frameWriter.streamId); 257 | self.streamHandlers[frameWriter.streamId] = { 258 | callback: callback, 259 | options: options}; 260 | } 261 | }; 262 | 263 | Connection.prototype.getStreamId = function(callback) { 264 | var self = this; 265 | if (!this.availableStreamIds) { 266 | this.availableStreamIds = []; 267 | if (this.options.maxRequests > 128) { 268 | throw new Error('Max requests can not be greater than 128'); 269 | } 270 | for(var i = 0; i < this.options.maxRequests; i++) { 271 | this.availableStreamIds.push(i); 272 | } 273 | this.getStreamQueue = new types.QueueWhile(function () { 274 | return self.availableStreamIds.length === 0; 275 | }, self.options.maxRequestsRetry); 276 | } 277 | this.getStreamQueue.push(function () { 278 | var streamId = self.availableStreamIds.shift(); 279 | callback.call(self, streamId); 280 | }); 281 | }; 282 | 283 | Connection.prototype.freeStreamId = function(header) { 284 | var streamId = header.streamId; 285 | var handler = this.streamHandlers[streamId]; 286 | delete this.streamHandlers[streamId]; 287 | this.availableStreamIds.push(streamId); 288 | if(handler && handler.callback) { 289 | handler.callback(null, handler.rowLength); 290 | } 291 | this.emit('log', 'info', 'Done receiving frame #' + streamId); 292 | }; 293 | 294 | /** 295 | * Handles a result and error response 296 | */ 297 | Connection.prototype.handleResult = function (header, err, result) { 298 | var streamId = header.streamId; 299 | if(streamId < 0) { 300 | return this.emit('log', 'info', 'event received', header); 301 | } 302 | var handler = this.streamHandlers[streamId]; 303 | if (!handler) { 304 | return this.emit('log', 'error', 'The server replied with a wrong streamId #' + streamId); 305 | } 306 | this.emit('log', 'info', 'Received frame #' + streamId); 307 | var callback = handler.callback; 308 | callback(err, result); 309 | //set the callback to null to avoid it being called when freed 310 | handler.callback = null; 311 | }; 312 | 313 | /** 314 | * Handles a row response 315 | */ 316 | Connection.prototype.handleRow = function (header, row, fieldStream, rowLength) { 317 | var streamId = header.streamId; 318 | if(streamId < 0) { 319 | return this.emit('log', 'info', 'Event received', header); 320 | } 321 | var handler = this.streamHandlers[streamId]; 322 | if (!handler) { 323 | return this.emit('log', 'error', 'The server replied with a wrong streamId #' + streamId); 324 | } 325 | this.emit('log', 'info', 'Received streaming frame #' + streamId); 326 | handler.rowLength = rowLength; 327 | handler.rowIndex = handler.rowIndex || 0; 328 | var rowCallback = handler.options && handler.options.rowCallback; 329 | if (rowCallback) { 330 | rowCallback(handler.rowIndex++, row, fieldStream, rowLength); 331 | } 332 | }; 333 | 334 | Connection.prototype.close = function disconnect (callback) { 335 | this.emit('log', 'info', 'disconnecting'); 336 | if(callback) { 337 | if (!this.netClient) { 338 | callback(); 339 | return; 340 | } 341 | if (!this.connected) { 342 | this.netClient.destroy(); 343 | callback(); 344 | return; 345 | } 346 | this.netClient.on('close', function (hadError) { 347 | var err = hadError ? new types.DriverError('The socket was closed due to a transmission error') : null; 348 | callback(err); 349 | }); 350 | } 351 | 352 | this.netClient.end(); 353 | 354 | this.streamHandlers = {}; 355 | }; 356 | 357 | exports.Connection = Connection; 358 | -------------------------------------------------------------------------------- /lib/streams.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var stream = require('stream'); 3 | var Transform = stream.Transform; 4 | var Writable = stream.Writable; 5 | 6 | var encoder = require('./encoder.js'); 7 | var types = require('./types.js'); 8 | var utils = require('./utils.js'); 9 | var FrameHeader = types.FrameHeader; 10 | var FrameReader = require('./readers.js').FrameReader; 11 | 12 | /** 13 | * Transforms chunks, emits data objects {header, chunk} 14 | */ 15 | function Protocol (options) { 16 | Transform.call(this, options); 17 | this.header = null; 18 | this.headerChunks = []; 19 | this.bodyLength = 0; 20 | } 21 | 22 | util.inherits(Protocol, Transform); 23 | 24 | Protocol.prototype._transform = function (chunk, encoding, callback) { 25 | var error = null; 26 | try { 27 | this.transformChunk(chunk); 28 | } 29 | catch (err) { 30 | error = err; 31 | } 32 | callback(error); 33 | }; 34 | 35 | Protocol.prototype.transformChunk = function (chunk) { 36 | var bodyChunk = chunk; 37 | 38 | if (this.header === null) { 39 | this.headerChunks.push(chunk); 40 | var length = utils.totalLength(this.headerChunks); 41 | if (length < FrameHeader.size) { 42 | return; 43 | } 44 | var chunksGrouped = Buffer.concat(this.headerChunks, length); 45 | this.header = new FrameHeader(chunksGrouped); 46 | if (length >= FrameHeader.size) { 47 | bodyChunk = chunksGrouped.slice(FrameHeader.size); 48 | } 49 | } 50 | 51 | this.bodyLength += bodyChunk.length; 52 | var frameEnded = this.bodyLength >= this.header.bodyLength; 53 | var header = this.header; 54 | 55 | var nextChunk = null; 56 | 57 | if (this.bodyLength > this.header.bodyLength) { 58 | //We received more than a complete frame 59 | var previousBodyLength = (this.bodyLength - bodyChunk.length); 60 | 61 | var nextStart = this.header.bodyLength - previousBodyLength; 62 | if (nextStart > bodyChunk.length) { 63 | throw new Error('Tried to slice a received chunk outside boundaries'); 64 | } 65 | nextChunk = bodyChunk.slice(nextStart); 66 | bodyChunk = bodyChunk.slice(0, nextStart); 67 | this.clear(); 68 | 69 | //close loop: parse next chunk before emitting 70 | this.transformChunk(nextChunk); 71 | } 72 | else if (this.bodyLength === this.header.bodyLength) { 73 | this.clear(); 74 | } 75 | 76 | this.push({header: header, chunk: bodyChunk, frameEnded: frameEnded}); 77 | }; 78 | 79 | Protocol.prototype.clear = function () { 80 | this.header = null; 81 | this.bodyLength = 0; 82 | this.headerChunks = []; 83 | }; 84 | 85 | /** 86 | * A stream that gets reads header + body chunks and transforms them into header + (row | error) 87 | */ 88 | function Parser (options) { 89 | Transform.call(this, options); 90 | //frames that are streaming, indexed by id 91 | this.frames = {}; 92 | } 93 | 94 | util.inherits(Parser, Transform); 95 | 96 | Parser.prototype._transform = function (item, encoding, callback) { 97 | var frameInfo = this.frameState(item); 98 | 99 | var error = null; 100 | try { 101 | this.parseBody(frameInfo, item); 102 | } 103 | catch (err) { 104 | error = err; 105 | } 106 | callback(error); 107 | 108 | if (item.frameEnded) { 109 | //all the parsing finished and it was streamed down 110 | //emit an item that signals it 111 | this.emitItem(frameInfo, {frameEnded: true}); 112 | } 113 | }; 114 | 115 | /** 116 | * Pushes the item with the header and the provided props to the consumer 117 | */ 118 | Parser.prototype.emitItem = function (frameInfo, props) { 119 | //flag that determines if it needs push down the header and props to the consumer 120 | var pushDown = true; 121 | if (frameInfo.resultStream) { 122 | //emit rows into the specified stream 123 | if (props.row) { 124 | frameInfo.resultStream.add(props.row); 125 | pushDown = false; 126 | } 127 | else if (props.frameEnded) { 128 | frameInfo.resultStream.add(null); 129 | } 130 | else if (props.error) { 131 | //Cassandra sent a response error 132 | frameInfo.resultStream.emit('error', props.error) 133 | } 134 | } 135 | if (pushDown) { 136 | //push the header and props to be read by consumers 137 | this.push(utils.extend({header: frameInfo.header}, props)); 138 | } 139 | 140 | }; 141 | 142 | Parser.prototype.parseBody = function (frameInfo, item) { 143 | var reader = new FrameReader(item.header, item.chunk); 144 | if (frameInfo.buffer) { 145 | reader.unshift(frameInfo.buffer); 146 | frameInfo.buffer = null; 147 | } 148 | switch (item.header.opcode) { 149 | case types.opcodes.ready: 150 | return this.emitItem(frameInfo, {ready: true}); 151 | case types.opcodes.authenticate: 152 | return this.emitItem(frameInfo, {mustAuthenticate: true}); 153 | case types.opcodes.error: 154 | return this.parseError(frameInfo, reader); 155 | case types.opcodes.result: 156 | return this.parseResult(frameInfo, reader); 157 | default: 158 | return this.emitItem(frameInfo, {error: new Error('Received invalid opcode: ' + item.header.opcode)}); 159 | } 160 | }; 161 | 162 | /** 163 | * Tries to read the error code and message. 164 | * If there is enough data to read, it pushes the header and error. If there isn't, it buffers it. 165 | * @param frameInfo information of the frame being parsed 166 | * @param {FrameReader} reader 167 | */ 168 | Parser.prototype.parseError = function (frameInfo, reader) { 169 | try { 170 | this.emitItem(frameInfo, {error: reader.readError()}); 171 | } 172 | catch (e) { 173 | if (e instanceof RangeError) { 174 | frameInfo.buffer = reader.getBuffer(); 175 | return; 176 | } 177 | throw e; 178 | } 179 | }; 180 | 181 | /** 182 | * Tries to read the result in the body of a message 183 | * @param frameInfo Frame information, header / metadata 184 | * @param {FrameReader} reader 185 | */ 186 | Parser.prototype.parseResult = function (frameInfo, reader) { 187 | var originalOffset = reader.offset; 188 | try { 189 | if (!frameInfo.meta) { 190 | frameInfo.kind = reader.readInt(); 191 | 192 | if (frameInfo.kind === types.resultKind.prepared) { 193 | frameInfo.preparedId = utils.copyBuffer(reader.readShortBytes()); 194 | } 195 | if (frameInfo.kind === types.resultKind.rows || 196 | frameInfo.kind === types.resultKind.prepared) { 197 | frameInfo.meta = reader.readMetadata(); 198 | } 199 | } 200 | } 201 | catch (e) { 202 | if (e instanceof RangeError) { 203 | //A controlled error, the kind / metadata is not available to be read yet 204 | return this.bufferForLater(frameInfo, reader, originalOffset); 205 | } 206 | throw e; 207 | } 208 | if (frameInfo.kind !== types.resultKind.rows) { 209 | return this.emitItem(frameInfo, {id: frameInfo.preparedId, meta: frameInfo.meta}); 210 | } 211 | if (frameInfo.streamField) { 212 | frameInfo.streamingColumn = frameInfo.meta.columns[frameInfo.meta.columns.length-1].name; 213 | } 214 | //it contains rows 215 | if (reader.remainingLength() > 0) { 216 | this.parseRows(frameInfo, reader); 217 | } 218 | }; 219 | 220 | Parser.prototype.parseRows = function (frameInfo, reader) { 221 | if (typeof frameInfo.rowLength === 'undefined') { 222 | try { 223 | frameInfo.rowLength = reader.readInt(); 224 | } 225 | catch (e) { 226 | if (e instanceof RangeError) { 227 | //there is not enough data to read this row 228 | this.bufferForLater(frameInfo, reader); 229 | return; 230 | } 231 | throw e; 232 | } 233 | } 234 | if (frameInfo.rowLength === 0) { 235 | return this.emitItem(frameInfo, {result: {rows: []}}); 236 | } 237 | var meta = frameInfo.meta; 238 | frameInfo.rowIndex = frameInfo.rowIndex || 0; 239 | var stopReading = false; 240 | for (var i = frameInfo.rowIndex; i < frameInfo.rowLength && !stopReading; i++) { 241 | this.emit('log', 'info', 'Reading row ' + i); 242 | if (frameInfo.fieldStream) { 243 | this.streamField(frameInfo, reader, null, i); 244 | stopReading = reader.remainingLength() === 0; 245 | continue; 246 | } 247 | var row = new types.Row(meta.columns); 248 | var rowOffset = reader.offset; 249 | for(var j = 0; j < meta.columns.length; j++ ) { 250 | var col = meta.columns[j]; 251 | this.emit('log', 'info', 'Reading cell value for ' + col.name); 252 | if (col.name !== frameInfo.streamingColumn) { 253 | var bytes = null; 254 | try { 255 | bytes = reader.readBytes(); 256 | } 257 | catch (e) { 258 | if (e instanceof RangeError) { 259 | //there is not enough data to read this row 260 | this.bufferForLater(frameInfo, reader, rowOffset, i); 261 | stopReading = true; 262 | break; 263 | } 264 | throw e; 265 | } 266 | try 267 | { 268 | row[col.name] = encoder.decode(bytes, col.type); 269 | bytes = null; 270 | } 271 | catch (e) { 272 | throw new ParserError(e, i, j); 273 | } 274 | if (j === meta.columns.length -1) { 275 | //the is no field to stream, emit that the row has been parsed 276 | this.emitItem(frameInfo, { 277 | row: row, 278 | meta: frameInfo.meta, 279 | byRow: frameInfo.byRow, 280 | length: frameInfo.rowLength 281 | }); 282 | } 283 | } 284 | else { 285 | var couldRead = this.streamField(frameInfo, reader, row, i); 286 | if (couldRead && reader.remainingLength() > 0) { 287 | //could be next field/row 288 | continue; 289 | } 290 | if (!couldRead) { 291 | this.bufferForLater(frameInfo, reader, rowOffset, frameInfo.rowIndex); 292 | } 293 | stopReading = true; 294 | } 295 | } 296 | } 297 | }; 298 | 299 | /** 300 | * Streams the content of a field 301 | * @returns {Boolean} true if read from the reader 302 | */ 303 | Parser.prototype.streamField = function (frameInfo, reader, row, rowIndex) { 304 | this.emit('log', 'info', 'Streaming field'); 305 | var fieldStream = frameInfo.fieldStream; 306 | if (!fieldStream) { 307 | try { 308 | frameInfo.fieldLength = reader.readInt(); 309 | } 310 | catch (e) { 311 | if (e instanceof RangeError) { 312 | return false; 313 | } 314 | throw e; 315 | } 316 | if (frameInfo.fieldLength < 0) { 317 | //null value 318 | this.emitItem(frameInfo, { 319 | row: row, 320 | meta: frameInfo.meta, 321 | byRow: true, 322 | length: frameInfo.rowLength 323 | }); 324 | return true; 325 | } 326 | fieldStream = new types.ResultStream(); 327 | frameInfo.streamedSoFar = 0; 328 | frameInfo.rowIndex = rowIndex; 329 | frameInfo.fieldStream = fieldStream; 330 | this.emitItem(frameInfo, { 331 | row: row, 332 | meta: frameInfo.meta, 333 | fieldStream: fieldStream, 334 | byRow: true, 335 | length: frameInfo.rowLength 336 | }); 337 | } 338 | var availableChunk = reader.read(frameInfo.fieldLength - frameInfo.streamedSoFar); 339 | 340 | //push into the stream 341 | fieldStream.add(availableChunk); 342 | frameInfo.streamedSoFar += availableChunk.length; 343 | //check if finishing 344 | if (frameInfo.streamedSoFar === frameInfo.fieldLength) { 345 | //EOF - Finished streaming this 346 | fieldStream.push(null); 347 | frameInfo.fieldStream = null; 348 | } 349 | return true; 350 | }; 351 | 352 | /** 353 | * Sets parser options (ie: how to yield the results as they are parsed) 354 | * @param {Number} id Id of the stream 355 | * @param options 356 | */ 357 | Parser.prototype.setOptions = function (id, options) { 358 | if (this.frames[id.toString()]) { 359 | throw new types.DriverError('There was already state for this frame'); 360 | } 361 | this.frames[id.toString()] = options; 362 | }; 363 | 364 | /** 365 | * Gets the frame info from the internal state. 366 | * In case it is not there, it creates it. 367 | * In case the frame ended 368 | */ 369 | Parser.prototype.frameState = function (item) { 370 | var frameInfo = this.frames[item.header.streamId]; 371 | if (!frameInfo) { 372 | frameInfo = this.frames[item.header.streamId] = {}; 373 | } 374 | if (item.frameEnded) { 375 | delete this.frames[item.header.streamId]; 376 | } 377 | frameInfo.header = item.header; 378 | return frameInfo; 379 | }; 380 | 381 | /** 382 | * Buffers for later use as there isn't enough data to read 383 | * @param frameInfo 384 | * @param {FrameReader} reader 385 | * @param {Number} [originalOffset] 386 | * @param {Number} [rowIndex] 387 | */ 388 | Parser.prototype.bufferForLater = function (frameInfo, reader, originalOffset, rowIndex) { 389 | if (!originalOffset && originalOffset !== 0) { 390 | originalOffset = reader.offset; 391 | } 392 | frameInfo.rowIndex = rowIndex; 393 | frameInfo.buffer = reader.slice(originalOffset); 394 | reader.toEnd(); 395 | }; 396 | 397 | /** 398 | * Represents a writable streams that emits results 399 | */ 400 | function ResultEmitter(options) { 401 | Writable.call(this, options); 402 | /** 403 | * Stores the rows for frames that needs to be yielded as one result with many rows 404 | */ 405 | this.rowBuffer = {}; 406 | } 407 | 408 | util.inherits(ResultEmitter, Writable); 409 | 410 | ResultEmitter.prototype._write = function (item, encoding, callback) { 411 | var error = null; 412 | try { 413 | this.each(item); 414 | } 415 | catch (err) { 416 | error = err; 417 | } 418 | callback(error); 419 | }; 420 | 421 | 422 | /** 423 | * Analyzes the item and emit the corresponding event 424 | */ 425 | ResultEmitter.prototype.each = function (item) { 426 | if (item.error || item.result) { 427 | //no transformation needs to be made 428 | return this.emit('result', item.header, item.error, item.result); 429 | } 430 | if (item.frameEnded) { 431 | return this.emit('frameEnded', item.header); 432 | } 433 | if (item.byRow) { 434 | //it should be yielded by row 435 | return this.emit('row', item.header, item.row, item.fieldStream, item.length); 436 | } 437 | if (item.row) { 438 | //it should be yielded as a result 439 | //it needs to be buffered to an array of rows 440 | return this.bufferAndEmit(item); 441 | } 442 | //its a raw result (object with flags) 443 | return this.emit('result', item.header, null, item); 444 | }; 445 | 446 | /** 447 | * Buffers the rows until the result set is completed and emits the result event. 448 | */ 449 | ResultEmitter.prototype.bufferAndEmit = function (item) { 450 | var rows = this.rowBuffer[item.header.streamId]; 451 | if (!rows) { 452 | rows = this.rowBuffer[item.header.streamId] = []; 453 | } 454 | rows.push(item.row); 455 | if (rows.length === item.length) { 456 | this.emit('result', item.header, null, {rows: rows, meta: item.meta}); 457 | delete this.rowBuffer[item.header.streamId]; 458 | } 459 | }; 460 | 461 | function ParserError(err, rowIndex, colIndex) { 462 | types.DriverError.call(this, err.message, this.constructor); 463 | this.rowIndex = rowIndex; 464 | this.colIndex = colIndex; 465 | this.innerError = err; 466 | this.info = 'Represents an Error while parsing the result'; 467 | } 468 | 469 | util.inherits(ParserError, types.DriverError); 470 | 471 | exports.Protocol = Protocol; 472 | exports.Parser = Parser; 473 | exports.ResultEmitter = ResultEmitter; -------------------------------------------------------------------------------- /test/basicTests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var util = require('util'); 3 | var events = require('events'); 4 | var uuid = require('node-uuid'); 5 | var async = require('async'); 6 | var utils = require('../lib/utils.js'); 7 | var types = require('../lib/types.js'); 8 | var encoder = require('../lib/encoder.js'); 9 | var config = require('./config.js'); 10 | var dataTypes = types.dataTypes; 11 | var Connection = require('../index.js').Connection; 12 | 13 | before(function (done) { 14 | this.timeout(5000); 15 | var con = new Connection(utils.extend({}, config)); 16 | async.series([ 17 | con.open.bind(con), 18 | function (next) { 19 | con.execute('select cql_version, native_protocol_version, release_version from system.local', function (err, result) { 20 | if (!err && result && result.rows) { 21 | console.log('Cassandra version', result.rows[0].get('release_version')); 22 | console.log('Cassandra higher protocol version', result.rows[0].get('native_protocol_version'), '\n'); 23 | } 24 | next(); 25 | }); 26 | }, 27 | con.close.bind(con)], done); 28 | }); 29 | 30 | describe('encoder', function () { 31 | describe('#stringifyValue()', function () { 32 | it('should be valid for query', function () { 33 | function testStringify(value, expected, dataType) { 34 | var stringValue = encoder.stringifyValue({value: value, hint: dataType}); 35 | if (typeof stringValue === 'string') { 36 | stringValue = stringValue.toLowerCase(); 37 | } 38 | assert.strictEqual(stringValue, expected); 39 | } 40 | testStringify(1, '1', dataTypes.int); 41 | testStringify(1.1, '1.1', dataTypes.double); 42 | testStringify("text", "'text'", dataTypes.text); 43 | testStringify("It's a quote", "'it''s a quote'", 'text'); 44 | testStringify("some 'quoted text'", "'some ''quoted text'''", dataTypes.text); 45 | testStringify(null, 'null', dataTypes.text); 46 | testStringify([1,2,3], '[1,2,3]', dataTypes.list); 47 | testStringify([], '[]', dataTypes.list); 48 | testStringify(['one', 'two'], '[\'one\',\'two\']', dataTypes.list); 49 | testStringify(['one', 'two'], '{\'one\',\'two\'}', 'set'); 50 | testStringify({key1:'value1', key2:'value2'}, '{\'key1\':\'value1\',\'key2\':\'value2\'}', 'map'); 51 | testStringify( 52 | types.Long.fromBuffer(new Buffer([0x5, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23])), 53 | 'blobasbigint(0x056789abcdef0123)', 54 | dataTypes.bigint); 55 | var date = new Date('Tue, 13 Aug 2013 09:10:32 GMT'); 56 | testStringify(date, date.getTime().toString(), dataTypes.timestamp); 57 | var uuidValue = uuid.v4(); 58 | testStringify(uuidValue, uuidValue.toString(), dataTypes.uuid); 59 | }); 60 | }); 61 | 62 | describe('#guessDataType()', function () { 63 | it('should guess the native types', function () { 64 | var guessDataType = encoder.guessDataType; 65 | assert.strictEqual(guessDataType(1), dataTypes.int, 'Guess type for an integer number failed'); 66 | assert.strictEqual(guessDataType(1.01), dataTypes.double, 'Guess type for a double number failed'); 67 | assert.strictEqual(guessDataType(true), dataTypes.boolean, 'Guess type for a boolean value failed'); 68 | assert.strictEqual(guessDataType([1,2,3]), dataTypes.list, 'Guess type for an Array value failed'); 69 | assert.strictEqual(guessDataType('a string'), dataTypes.text, 'Guess type for an string value failed'); 70 | assert.strictEqual(guessDataType(new Buffer('bip bop')), dataTypes.blob, 'Guess type for a buffer value failed'); 71 | assert.strictEqual(guessDataType(new Date()), dataTypes.timestamp, 'Guess type for a Date value failed'); 72 | assert.strictEqual(guessDataType(new types.Long(10)), dataTypes.bigint, 'Guess type for a Int 64 value failed'); 73 | assert.strictEqual(guessDataType(uuid.v4()), dataTypes.uuid, 'Guess type for a UUID value failed'); 74 | assert.strictEqual(guessDataType(types.uuid()), dataTypes.uuid, 'Guess type for a UUID value failed'); 75 | assert.strictEqual(guessDataType(types.timeuuid()), dataTypes.uuid, 'Guess type for a Timeuuid value failed'); 76 | }); 77 | }); 78 | 79 | describe('#encode() and #decode', function () { 80 | var typeEncoder = encoder; 81 | it('should encode and decode maps', function () { 82 | var value = {value1: 'Surprise', value2: 'Madafaka'}; 83 | var encoded = typeEncoder.encode({hint: dataTypes.map, value: value}); 84 | var decoded = typeEncoder.decode(encoded, [dataTypes.map, [[dataTypes.text], [dataTypes.text]]]); 85 | assert.strictEqual(util.inspect(decoded), util.inspect(value)); 86 | }); 87 | 88 | it('should respect type hints for maps', function () { 89 | var value = {value1: 5, value2: 10}; 90 | var encodedAsDouble = typeEncoder.encode({hint: 'map', value: value}); 91 | var decodedAsDouble = typeEncoder.decode(encodedAsDouble, [dataTypes.map, [[dataTypes.text], [dataTypes.double]]]); 92 | assert.strictEqual(util.inspect(decodedAsDouble), util.inspect(value)); 93 | var encodedAsInt = typeEncoder.encode({hint: dataTypes.map, value: value}); 94 | var decodedAsInt = typeEncoder.decode(encodedAsInt, [dataTypes.map, [[dataTypes.text], [dataTypes.int]]]); 95 | assert.strictEqual(util.inspect(decodedAsInt), util.inspect(value)); 96 | }); 97 | 98 | it('should encode and decode list', function () { 99 | var value = [1, 2, 3, 4]; 100 | var encoded = typeEncoder.encode({hint: 'list', value: value}); 101 | var decoded = typeEncoder.decode(encoded, [dataTypes.list, [dataTypes.int]]); 102 | assert.strictEqual(util.inspect(decoded), util.inspect(value)); 103 | }); 104 | 105 | it('should encode and decode set', function () { 106 | var value = ['1', '2', '3', '4']; 107 | var encoded = typeEncoder.encode({hint: 'set', value: value}); 108 | var decoded = typeEncoder.decode(encoded, [dataTypes.set, [dataTypes.text]]); 109 | assert.strictEqual(util.inspect(decoded), util.inspect(value)); 110 | }); 111 | 112 | it('should ignore case and whitespace in type parameters', function () { 113 | var listValue = [1, 2, 3, 4]; 114 | var encodedList = typeEncoder.encode({hint: 'LIST', value: listValue}); 115 | var decodedList = typeEncoder.decode(encodedList, [dataTypes.list, [dataTypes.int]]); 116 | assert.strictEqual(util.inspect(decodedList), util.inspect(listValue)); 117 | var mapValue = {value1: 'Surprise', value2: 'Madafaka'}; 118 | var encodedMap = typeEncoder.encode({hint: 'Map<\tTEXT\t,\tTEXT\t>', value: mapValue}); 119 | var decodedMap = typeEncoder.decode(encodedMap, [dataTypes.map, [[dataTypes.text], [dataTypes.text]]]); 120 | assert.strictEqual(util.inspect(decodedMap), util.inspect(mapValue)); 121 | }); 122 | }) 123 | }); 124 | 125 | describe('types', function () { 126 | describe('queryParser', function () { 127 | it('should replace placeholders', function () { 128 | var parse = types.queryParser.parse; 129 | assert.strictEqual(parse("SELECT ?", ['123']), "SELECT 123"); 130 | assert.strictEqual(parse("A = 'SCIENCE?' AND KEY = ?", ['2']), "A = 'SCIENCE?' AND KEY = 2"); 131 | assert.strictEqual(parse("key0=? key1 = 'SCIENCE?' AND KEY=?", ['1', '2']), "key0=1 key1 = 'SCIENCE?' AND KEY=2"); 132 | assert.strictEqual(parse("keyA=? AND keyB=? AND keyC=?", ['1', '2', '3']), "keyA=1 AND keyB=2 AND keyC=3"); 133 | //replace in the middle 134 | assert.strictEqual(parse("key=? AND key2='value'", null), "key=? AND key2='value'"); 135 | //Nothing to replace here 136 | assert.strictEqual(parse("SELECT", []), "SELECT"); 137 | assert.strictEqual(parse("SELECT", null), "SELECT"); 138 | }); 139 | }); 140 | 141 | describe('Long', function () { 142 | var Long = types.Long; 143 | it('should convert from and to Buffer', function () { 144 | [ 145 | //int64 decimal value //hex value 146 | ['-123456789012345678', 'fe4964b459cf0cb2'], 147 | ['-800000000000000000', 'f4e5d43d13b00000'], 148 | ['-888888888888888888', 'f3aa0843dcfc71c8'], 149 | ['-555555555555555555', 'f84a452a6a1dc71d'], 150 | ['-789456', 'fffffffffff3f430'], 151 | ['-911111111111111144', 'f35b15458f4f8e18'], 152 | ['-9007199254740993', 'ffdfffffffffffff'], 153 | ['-1125899906842624', 'fffc000000000000'], 154 | ['555555555555555555', '07b5bad595e238e3'], 155 | ['789456' , '00000000000c0bd0'], 156 | ['888888888888888888', '0c55f7bc23038e38'] 157 | ].forEach(function (item) { 158 | var buffer = new Buffer(item[1], 'hex'); 159 | var value = Long.fromBuffer(buffer); 160 | assert.strictEqual(value.toString(), item[0]); 161 | assert.strictEqual(Long.toBuffer(value).toString('hex'), buffer.toString('hex'), 162 | 'Hexadecimal values should match for ' + item[1]); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('ResultStream', function () { 168 | it('should be readable as soon as it has data', function (done) { 169 | var buf = []; 170 | var stream = new types.ResultStream(); 171 | 172 | stream.on('end', function streamEnd() { 173 | assert.equal(Buffer.concat(buf).toString(), 'Jimmy McNulty'); 174 | done(); 175 | }); 176 | stream.on('readable', function streamReadable() { 177 | var item; 178 | while (item = stream.read()) { 179 | buf.push(item); 180 | } 181 | }); 182 | 183 | stream.add(new Buffer('Jimmy')); 184 | stream.add(new Buffer(' ')); 185 | stream.add(new Buffer('McNulty')); 186 | stream.add(null); 187 | }); 188 | 189 | it('should buffer until is read', function (done) { 190 | var buf = []; 191 | var stream = new types.ResultStream(); 192 | stream.add(new Buffer('Stringer')); 193 | stream.add(new Buffer(' ')); 194 | stream.add(new Buffer('Bell')); 195 | stream.add(null); 196 | 197 | stream.on('end', function streamEnd() { 198 | assert.equal(Buffer.concat(buf).toString(), 'Stringer Bell'); 199 | done(); 200 | }); 201 | stream.on('readable', function streamReadable() { 202 | var item; 203 | while (item = stream.read()) { 204 | buf.push(item); 205 | } 206 | }); 207 | }); 208 | 209 | it('should be readable until the end', function (done) { 210 | var buf = []; 211 | var stream = new types.ResultStream(); 212 | stream.add(new Buffer('Omar')); 213 | stream.add(new Buffer(' ')); 214 | 215 | stream.on('end', function streamEnd() { 216 | assert.equal(Buffer.concat(buf).toString(), 'Omar Little'); 217 | done(); 218 | }); 219 | stream.on('readable', function streamReadable() { 220 | var item; 221 | while (item = stream.read()) { 222 | buf.push(item); 223 | } 224 | }); 225 | 226 | stream.add(new Buffer('Little')); 227 | stream.add(null); 228 | }); 229 | 230 | it('should be readable on objectMode', function (done) { 231 | var buf = []; 232 | var stream = new types.ResultStream({objectMode: true}); 233 | //Using QueryLiteral class but any would do it 234 | stream.add(new types.QueryLiteral('One')); 235 | stream.add(new types.QueryLiteral('Two')); 236 | stream.add(null); 237 | stream.on('end', function streamEnd() { 238 | assert.equal(buf.join(' '), 'One Two'); 239 | done(); 240 | }); 241 | stream.on('readable', function streamReadable() { 242 | var item; 243 | while (item = stream.read()) { 244 | buf.push(item.toString()); 245 | } 246 | }); 247 | }); 248 | }); 249 | 250 | describe('Row', function () { 251 | it('should get the value by column name or index', function () { 252 | var columnList = [{name: 'first'}, {name: 'second'}]; 253 | var row = new types.Row(columnList); 254 | row['first'] = 'value1'; 255 | row['second'] = 'value2'; 256 | 257 | assert.ok(row.get, 'It should contain a get method'); 258 | assert.strictEqual(row.get('first'), row['first']); 259 | assert.strictEqual(row.get(0), row['first']); 260 | assert.strictEqual(row.get('second'), row['second']); 261 | assert.strictEqual(row.get(1), row['second']); 262 | }) 263 | }) 264 | }); 265 | 266 | describe('utils', function () { 267 | describe('#syncEvent()', function () { 268 | it('should execute callback once for all emitters', function () { 269 | var emitter1 = new events.EventEmitter(); 270 | var emitter2 = new events.EventEmitter(); 271 | var emitter3 = new events.EventEmitter(); 272 | var callbackCounter = 0; 273 | utils.syncEvent([emitter1, emitter2, emitter3], 'dummy', this, function (text){ 274 | assert.strictEqual(text, 'bop'); 275 | callbackCounter = callbackCounter + 1; 276 | }); 277 | assert.ok(emitter1.emit('dummy', 'bip')); 278 | emitter1.emit('dummy', 'bop'); 279 | emitter2.emit('dummy', 'bip'); 280 | emitter2.emit('dummy', 'bop'); 281 | emitter3.emit('dummy', 'bop'); 282 | assert.strictEqual(callbackCounter, 1); 283 | }); 284 | }); 285 | 286 | describe('#parseCommonArgs()', function () { 287 | it('parses args and can be retrieved by name', function () { 288 | function testArgs(args, expectedLength) { 289 | assert.strictEqual(args.length, expectedLength, 'The arguments length do not match'); 290 | assert.ok(args.query, 'Query must be defined'); 291 | assert.strictEqual(typeof args.callback, 'function', 'Callback must be a function '); 292 | if (args && args.length > 2) { 293 | assert.ok(util.isArray(args.params) || args.params === null, 'params must be an array or null'); 294 | assert.ok(typeof args.consistency === 'number' || args.consistency === null, 'Consistency must be an int or null'); 295 | } 296 | } 297 | var args = utils.parseCommonArgs('A QUERY 1', function (){}); 298 | assert.ok(args && args.length == 2 && args.query && args.callback); 299 | assert.throws(utils.parseCommonArgs, Error, 'It must contain at least 2 arguments.'); 300 | args = utils.parseCommonArgs('A QUERY 2', [1, 2, 3], function (){}); 301 | testArgs(args, 3); 302 | assert.ok(util.isArray(args.params) && args.params.length === 3); 303 | args = utils.parseCommonArgs('A QUERY 3', types.consistencies.quorum, function (){}); 304 | testArgs(args, 3); 305 | assert.ok(args.params === null && args.consistency === types.consistencies.quorum, 'Consistency does not match'); 306 | args = utils.parseCommonArgs('A QUERY', [1, 2, 3], types.consistencies.quorum, function (){}); 307 | testArgs(args, 4); 308 | assert.ok(args.params && args.consistency, 'Params and consistency must not be null'); 309 | args = utils.parseCommonArgs('A QUERY', [1, 2, 3], types.consistencies.quorum, {}, function (){}); 310 | testArgs(args, 5); 311 | assert.ok(args.params && args.consistency && args.options, 'Params, consistency and options must not be null'); 312 | }); 313 | 314 | it('parses args and can be retrieved as an array', function () { 315 | var args = utils.parseCommonArgs('A QUERY', function (){}); 316 | assert.ok(util.isArray(args), 'The returned object must be an Array'); 317 | assert.strictEqual(args[0], 'A QUERY', 'The first element must be the query'); 318 | assert.strictEqual(args.length, 2, 'There must be 2 arguments in array'); 319 | }); 320 | }); 321 | 322 | describe('#extend()', function () { 323 | it('should allow null sources', function () { 324 | var originalObject = {}; 325 | var extended = utils.extend(originalObject, null); 326 | assert.strictEqual(originalObject, extended); 327 | }); 328 | }); 329 | }); -------------------------------------------------------------------------------- /lib/encoder.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var uuid = require('node-uuid'); 3 | 4 | var types = require('./types.js'); 5 | var dataTypes = types.dataTypes; 6 | var Long = types.Long; 7 | var utils = require('./utils.js'); 8 | /** 9 | * Encodes and decodes from a type to Cassandra bytes 10 | */ 11 | var encoder = (function(){ 12 | /** 13 | * Decodes Cassandra bytes into Javascript values. 14 | */ 15 | function decode(bytes, type) { 16 | if (bytes === null) { 17 | return null; 18 | } 19 | switch(type[0]) { 20 | case dataTypes.custom: 21 | case dataTypes.decimal: 22 | case dataTypes.inet: 23 | case dataTypes.varint: 24 | //return buffer and move on :) 25 | return utils.copyBuffer(bytes); 26 | case dataTypes.ascii: 27 | return bytes.toString('ascii'); 28 | case dataTypes.bigint: 29 | case dataTypes.counter: 30 | return decodeBigNumber(utils.copyBuffer(bytes)); 31 | case dataTypes.timestamp: 32 | return decodeTimestamp(utils.copyBuffer(bytes)); 33 | case dataTypes.blob: 34 | return utils.copyBuffer(bytes); 35 | case dataTypes.boolean: 36 | return !!bytes.readUInt8(0); 37 | case dataTypes.double: 38 | return bytes.readDoubleBE(0); 39 | case dataTypes.float: 40 | return bytes.readFloatBE(0); 41 | case dataTypes.int: 42 | return bytes.readInt32BE(0); 43 | case dataTypes.uuid: 44 | case dataTypes.timeuuid: 45 | return uuid.unparse(bytes); 46 | case dataTypes.text: 47 | case dataTypes.varchar: 48 | return bytes.toString('utf8'); 49 | case dataTypes.list: 50 | case dataTypes.set: 51 | var list = decodeList(bytes, type[1][0]); 52 | return list; 53 | case dataTypes.map: 54 | var map = decodeMap(bytes, type[1][0][0], type[1][1][0]); 55 | return map; 56 | } 57 | 58 | throw new Error('Unknown data type: ' + type[0]); 59 | } 60 | 61 | function decodeBigNumber (bytes) { 62 | return Long.fromBuffer(bytes); 63 | } 64 | 65 | function decodeTimestamp (bytes) { 66 | var value = decodeBigNumber(bytes); 67 | if (value.greaterThan(Long.fromNumber(Number.MIN_VALUE)) && value.lessThan(Long.fromNumber(Number.MAX_VALUE))) { 68 | return new Date(value.toNumber()); 69 | } 70 | return value; 71 | } 72 | 73 | /* 74 | * Reads a list from bytes 75 | */ 76 | function decodeList (bytes, type) { 77 | var offset = 0; 78 | //a short containing the total items 79 | var totalItems = bytes.readUInt16BE(offset); 80 | offset += 2; 81 | var list = []; 82 | for(var i = 0; i < totalItems; i++) { 83 | //bytes length of the item 84 | var length = bytes.readUInt16BE(offset); 85 | offset += 2; 86 | //slice it 87 | list.push(decode(bytes.slice(offset, offset+length), [type])); 88 | offset += length; 89 | } 90 | return list; 91 | } 92 | 93 | function normalizeTypedValue (item, allowLiterals) { 94 | if (item === null || (allowLiterals && item === undefined)) { 95 | if (allowLiterals) { 96 | return {value: 'NULL', literal: true}; 97 | } 98 | return null; 99 | } 100 | var value = null; 101 | var type = null; 102 | var subtype = null; 103 | var keyType = null; 104 | var valueType = null; 105 | if (item.hint) { 106 | value = item.value; 107 | if (value === null || (allowLiterals && value === undefined)) { 108 | if (allowLiterals) { 109 | return {value: 'NULL', literal: true}; 110 | } 111 | return null; 112 | } 113 | type = item.hint; 114 | var typeInfo = null; 115 | if (typeof type === 'string') { 116 | typeInfo = dataTypes.getByName(type); 117 | } 118 | else if (typeof type === 'object' && type) { 119 | typeInfo = type; 120 | } 121 | if (typeInfo) { 122 | type = typeInfo.type; 123 | if (typeInfo.subtype) { 124 | subtype = typeInfo.subtype; 125 | } 126 | if (typeInfo.keyType) { 127 | keyType = typeInfo.keyType; 128 | } 129 | if (typeInfo.valueType) { 130 | valueType = typeInfo.valueType; 131 | } 132 | } 133 | } 134 | else { 135 | value = item; 136 | } 137 | if (!type) { 138 | type = guessDataType(value); 139 | if (allowLiterals && !type && value instanceof types.QueryLiteral) { 140 | return {value: value.toString(), literal: true}; 141 | } 142 | if (!type) { 143 | throw new TypeError('Target data type could not be guessed, you must specify a hint.', value); 144 | } 145 | } 146 | return { 147 | value: value, 148 | type: type, 149 | subtype: subtype, 150 | keyType: keyType, 151 | valueType: valueType}; 152 | } 153 | 154 | /* 155 | * Reads a map (key / value) from bytes 156 | */ 157 | function decodeMap (bytes, type1, type2) { 158 | var offset = 0; 159 | //a short containing the total items 160 | var totalItems = bytes.readUInt16BE(offset); 161 | offset += 2; 162 | var map = {}; 163 | for(var i = 0; i < totalItems; i++) { 164 | var keyLength = bytes.readUInt16BE(offset); 165 | offset += 2; 166 | var key = decode(bytes.slice(offset, offset+keyLength), [type1]); 167 | offset += keyLength; 168 | var valueLength = bytes.readUInt16BE(offset); 169 | offset += 2; 170 | map[key] = decode(bytes.slice(offset, offset+valueLength), [type2]); 171 | offset += valueLength; 172 | } 173 | return map; 174 | } 175 | 176 | function encode (item) { 177 | var typedValue = normalizeTypedValue(item); 178 | if (typedValue === null) { 179 | return null; 180 | } 181 | var value = typedValue.value; 182 | var type = typedValue.type; 183 | switch (type) { 184 | case dataTypes.int: 185 | return encodeInt(value); 186 | case dataTypes.float: 187 | return encodeFloat(value); 188 | case dataTypes.double: 189 | return encodeDouble(value); 190 | case dataTypes.boolean: 191 | return encodeBoolean(value); 192 | case dataTypes.text: 193 | case dataTypes.varchar: 194 | return encodeString(value); 195 | case dataTypes.ascii: 196 | return encodeString(value, 'ascii'); 197 | case dataTypes.uuid: 198 | case dataTypes.timeuuid: 199 | return encodeUuid(value); 200 | case dataTypes.custom: 201 | case dataTypes.decimal: 202 | case dataTypes.inet: 203 | case dataTypes.varint: 204 | case dataTypes.blob: 205 | return encodeBlob(value, type); 206 | case dataTypes.bigint: 207 | case dataTypes.counter: 208 | return encodeBigNumber(value, type); 209 | case dataTypes.timestamp: 210 | return encodeTimestamp(value, type); 211 | case dataTypes.list: 212 | case dataTypes.set: 213 | return encodeList(value, type, typedValue.subtype); 214 | case dataTypes.map: 215 | return encodeMap(value, typedValue.keyType, typedValue.valueType); 216 | default: 217 | throw new TypeError('Type not supported ' + type, value); 218 | } 219 | } 220 | 221 | /** 222 | * Try to guess the Cassandra type to be stored, based on the javascript value type 223 | */ 224 | function guessDataType (value) { 225 | var dataType = null; 226 | if (typeof value === 'number') { 227 | dataType = dataTypes.int; 228 | if (value % 1 !== 0) { 229 | dataType = dataTypes.double; 230 | } 231 | } 232 | else if(value instanceof Date) { 233 | dataType = dataTypes.timestamp; 234 | } 235 | else if(value instanceof Long) { 236 | dataType = dataTypes.bigint; 237 | } 238 | else if (typeof value === 'string') { 239 | dataType = dataTypes.text; 240 | if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)){ 241 | dataType = dataTypes.uuid; 242 | } 243 | } 244 | else if (value instanceof Buffer) { 245 | dataType = dataTypes.blob; 246 | } 247 | else if (util.isArray(value)) { 248 | dataType = dataTypes.list; 249 | } 250 | else if (value === true || value === false) { 251 | dataType = dataTypes.boolean; 252 | } 253 | return dataType; 254 | } 255 | 256 | function encodeInt (value) { 257 | if (typeof value !== 'number') { 258 | throw new TypeError(null, value, 'number'); 259 | } 260 | var buf = new Buffer(4); 261 | buf.writeInt32BE(value, 0); 262 | return buf; 263 | } 264 | 265 | function encodeFloat (value) { 266 | if (typeof value !== 'number') { 267 | throw new TypeError(null, value, 'number'); 268 | } 269 | var buf = new Buffer(4); 270 | buf.writeFloatBE(value, 0); 271 | return buf; 272 | } 273 | 274 | function encodeDouble (value) { 275 | if (typeof value !== 'number') { 276 | throw new TypeError(null, value, 'number'); 277 | } 278 | var buf = new Buffer(8); 279 | buf.writeDoubleBE(value, 0); 280 | return buf; 281 | } 282 | 283 | function encodeTimestamp (value, type) { 284 | if (value instanceof Date) { 285 | value = value.getTime(); 286 | } 287 | return encodeBigNumber (value, type); 288 | } 289 | 290 | function encodeUuid (value) { 291 | if (typeof value === 'string') { 292 | value = uuid.parse(value, new Buffer(16)); 293 | } 294 | if (!(value instanceof Buffer)) { 295 | throw new TypeError('Only Buffer and string objects allowed for UUID values', value, Buffer); 296 | } 297 | return value; 298 | } 299 | 300 | function encodeBigNumber (value, type) { 301 | var buf = getBigNumberBuffer(value); 302 | if (buf === null) { 303 | throw new TypeError(null, value, Buffer, null, type); 304 | } 305 | return buf; 306 | } 307 | 308 | function getBigNumberBuffer (value) { 309 | var buf = null; 310 | if (value instanceof Buffer) { 311 | buf = value; 312 | } else if (value instanceof Long) { 313 | buf = Long.toBuffer(value); 314 | } else if (typeof value === 'number') { 315 | buf = Long.toBuffer(Long.fromNumber(value)); 316 | } 317 | return buf; 318 | } 319 | 320 | function encodeString (value, encoding) { 321 | if (typeof value !== 'string') { 322 | throw new TypeError(null, value, 'string'); 323 | } 324 | return new Buffer(value, encoding); 325 | } 326 | 327 | function encodeBlob (value, type) { 328 | if (!(value instanceof Buffer)) { 329 | throw new TypeError(null, value, Buffer, null, type); 330 | } 331 | return value; 332 | } 333 | 334 | function encodeBoolean(value) { 335 | return new Buffer([(value ? 1 : 0)]); 336 | } 337 | 338 | function encodeList(value, type, subtype) { 339 | if (!util.isArray(value)) { 340 | throw new TypeError(null, value, Array, null, type); 341 | } 342 | if (value.length === 0) { 343 | return null; 344 | } 345 | var parts = []; 346 | parts.push(getLengthBuffer(value)); 347 | for (var i=0;i self.connections.length-1) { 139 | self.connectionIndex = 0; 140 | } 141 | var c = self.connections[self.connectionIndex]; 142 | if (self._isHealthy(c)) { 143 | callback(null, c); 144 | } 145 | else if (Date.now() - startTime > self.options.getAConnectionTimeout) { 146 | callback(new types.TimeoutError('Get a connection timed out')); 147 | } 148 | else if (!c.connecting && self._canReconnect(c)) { 149 | self.emit('log', 'info', 'Retrying to open #' + c.indexInPool); 150 | //try to reconnect 151 | c.open(function(err){ 152 | if (err) { 153 | //This connection is still not good, go for the next one 154 | self._setUnhealthy(c); 155 | setImmediate(function () { 156 | checkNextConnection(callback); 157 | }); 158 | } 159 | else { 160 | //this connection is now good 161 | self._setHealthy(c); 162 | callback(null, c); 163 | } 164 | }); 165 | } 166 | else { 167 | //this connection is not good, try the next one 168 | setImmediate(function () { 169 | checkNextConnection(callback); 170 | }); 171 | } 172 | } 173 | checkNextConnection(callback); 174 | }); 175 | }; 176 | 177 | /** 178 | * Executes a query in an available connection. 179 | * @param {String} query The query to execute 180 | * @param {Array} [param] Array of params to replace 181 | * @param {Number} [consistency] Consistency level 182 | * @param [options] 183 | * @param {function} callback Executes callback(err, result) when finished 184 | */ 185 | Client.prototype.execute = function () { 186 | var args = utils.parseCommonArgs.apply(null, arguments); 187 | var self = this; 188 | //Get stack trace before sending query so the user knows where errored 189 | //queries come from 190 | var stackContainer = {}; 191 | Error.captureStackTrace(stackContainer); 192 | 193 | function tryAndRetry(retryCount) { 194 | retryCount = retryCount || 0; 195 | self._getAConnection(function(err, c) { 196 | if (err) { 197 | args.callback(err); 198 | return; 199 | } 200 | self.emit('log', 'info', 'connection #' + c.indexInPool + ' acquired, executing: ' + args.query); 201 | c.execute(args.query, args.params, args.consistency, function(err, result) { 202 | //Determine if its a fatal error 203 | if (self._isServerUnhealthy(err)) { 204 | //if its a fatal error, the server died 205 | self._setUnhealthy(c); 206 | if (retryCount === self.options.maxExecuteRetries) { 207 | args.callback(err, result, retryCount); 208 | return; 209 | } 210 | //retry: it will get another connection 211 | self.emit('log', 'error', 'There was an error executing a query, retrying execute (will get another connection)', err); 212 | tryAndRetry(retryCount+1); 213 | } 214 | else { 215 | if (err) { 216 | utils.fixStack(stackContainer.stack, err); 217 | err.query = args.query; 218 | } 219 | //If the result is OK or there is error (syntax error or an unauthorized for example), callback 220 | args.callback(err, result); 221 | } 222 | }); 223 | }); 224 | } 225 | tryAndRetry(0); 226 | }; 227 | 228 | /** 229 | * Prepares (the first time) and executes the prepared query, retrying on multiple hosts if needed. 230 | * @param {String} query The query to prepare and execute 231 | * @param {Array} [param] Array of params 232 | * @param {Number} [consistency] Consistency level 233 | * @param [options] 234 | * @param {function} callback Executes callback(err, result) when finished 235 | */ 236 | Client.prototype.executeAsPrepared = function () { 237 | var args = utils.parseCommonArgs.apply(null, arguments); 238 | var self = this; 239 | //Get stack trace before sending query so the user knows where errored 240 | //queries come from 241 | var stackContainer = {}; 242 | Error.captureStackTrace(stackContainer); 243 | 244 | self._getPrepared(args.query, function preparedCallback(err, con, queryId) { 245 | if (self._isServerUnhealthy(err)) { 246 | //its a fatal error, the server died 247 | self._setUnhealthy(con); 248 | args.options = utils.extend({retryCount: 0}, args.options); 249 | if (args.options.retryCount === self.options.maxExecuteRetries) { 250 | return args.callback(err); 251 | } 252 | //retry: it will get another connection 253 | self.emit('log', 'info', 'Retrying to prepare "' + args.query + '"'); 254 | args.options.retryCount = args.options.retryCount + 1; 255 | self.executeAsPrepared(args.query, args.params, args.consistency, args.options, args.callback); 256 | } 257 | else if (err) { 258 | //its syntax or other normal error 259 | utils.fixStack(stackContainer.stack, err); 260 | err.query = args.query; 261 | if (args.options && args.options.resultStream) { 262 | args.options.resultStream.emit('error', err); 263 | } 264 | args.callback(err); 265 | } 266 | else { 267 | //it is prepared on the connection 268 | self._executeOnConnection(con, args.query, queryId, args.params,args.consistency, args.options, args.callback); 269 | } 270 | }); 271 | }; 272 | 273 | /** 274 | * Prepares (the first time on each host), executes the prepared query and streams the last field of each row. 275 | * It executes the callback per each row as soon as the first chunk of the last field is received. 276 | * Retries on multiple hosts if needed. 277 | * @param {String} query The query to prepare and execute 278 | * @param {Array} [param] Array of params 279 | * @param {Number} [consistency] Consistency level 280 | * @param [options] 281 | * @param {function} rowCallback Executes rowCallback(n, row, fieldStream) per each row 282 | * @param {function} [callback] Executes callback(err) when finished or there is an error 283 | */ 284 | Client.prototype.streamField = function () { 285 | var args = Array.prototype.slice.call(arguments); 286 | var rowCallback; 287 | //accepts an extra callback 288 | if(typeof args[args.length-1] === 'function' && typeof args[args.length-2] === 'function') { 289 | //pass it through the options parameter 290 | rowCallback = args.splice(args.length-2, 1)[0]; 291 | } 292 | args = utils.parseCommonArgs.apply(null, args); 293 | if (!rowCallback) { 294 | //only one callback has been defined 295 | rowCallback = args.callback; 296 | args.callback = function () {}; 297 | } 298 | args.options = utils.extend({}, args.options, { 299 | byRow: true, 300 | streamField: true, 301 | rowCallback: rowCallback 302 | }); 303 | this.executeAsPrepared(args.query, args.params, args.consistency, args.options, args.callback); 304 | }; 305 | 306 | /** 307 | * Prepares (the first time), executes the prepared query and calls rowCallback for each row as soon as they are received. 308 | * Calls endCallback after all rows have been sent, or when there is an error. 309 | * Retries on multiple hosts if needed. 310 | * @param {String} query The query to prepare and execute 311 | * @param {Array} [param] Array of params 312 | * @param {Number} [consistency] Consistency level 313 | * @param [options] 314 | * @param {function} rowCallback, executes callback(n, row) per each row received. (n = index) 315 | * @param {function} [endcallback], executes endCallback(err, totalCount) after all rows have been received. 316 | */ 317 | Client.prototype.eachRow = function () { 318 | var args = Array.prototype.slice.call(arguments); 319 | var rowCallback; 320 | //accepts an extra callback 321 | if(typeof args[args.length-1] === 'function' && typeof args[args.length-2] === 'function') { 322 | //pass it through the options parameter 323 | rowCallback = args.splice(args.length-2, 1)[0]; 324 | } 325 | args = utils.parseCommonArgs.apply(null, args); 326 | if (!rowCallback) { 327 | //only one callback has been defined 328 | rowCallback = args.callback; 329 | args.callback = function () {}; 330 | } 331 | args.options = utils.extend({}, args.options, { 332 | byRow: true, 333 | rowCallback: rowCallback 334 | }); 335 | this.executeAsPrepared(args.query, args.params, args.consistency, args.options, args.callback); 336 | }; 337 | 338 | 339 | /** 340 | * Prepares (the first time), executes the prepared query and pushes the rows to the result stream 341 | * as soon as they received. 342 | * Calls callback after all rows have been sent, or when there is an error. 343 | * Retries on multiple hosts if needed. 344 | * @param {String} query The query to prepare and execute 345 | * @param {Array} [param] Array of params 346 | * @param {Number} [consistency] Consistency level 347 | * @param [options] 348 | * @param {function} [callback], executes callback(err) after all rows have been received or if there is an error 349 | * @returns {ResultStream} 350 | */ 351 | Client.prototype.stream = function () { 352 | var args = Array.prototype.slice.call(arguments); 353 | if (typeof args[args.length-1] !== 'function') { 354 | //the callback is not required 355 | args.push(function noop() {}); 356 | } 357 | args = utils.parseCommonArgs.apply(null, args); 358 | var resultStream = new types.ResultStream({objectMode: 1}) 359 | args.options = utils.extend({}, args.options, {resultStream: resultStream}); 360 | this.executeAsPrepared(args.query, args.params, args.consistency, args.options, args.callback); 361 | return resultStream; 362 | }; 363 | 364 | Client.prototype.streamRows = Client.prototype.eachRow; 365 | 366 | Client.prototype.executeBatch = function () { 367 | throw new Error('Method not supported by this version of the driver. Try using the driver version that implements Cassandra native protocol v2.'); 368 | }; 369 | 370 | /** 371 | * Executes a prepared query on a given connection 372 | */ 373 | Client.prototype._executeOnConnection = function (c, query, queryId, params, consistency, options, callback) { 374 | this.emit('log', 'info', 'Executing prepared query "' + query + '"'); 375 | var self = this; 376 | c.executePrepared(queryId, params, consistency, options, function(err, result1, result2) { 377 | if (self._isServerUnhealthy(err)) { 378 | //There is a problem with the connection/server that had a prepared query 379 | //forget about this connection for now 380 | self._setUnhealthy(c); 381 | //retry the whole thing, it will get another connection 382 | self.executeAsPrepared(query, params, consistency, options, callback); 383 | } 384 | else if (err && err.code === types.responseErrorCodes.unprepared) { 385 | //Query expired at the server 386 | //Clear the connection from prepared info and 387 | //trying to re-prepare query 388 | self.emit('log', 'info', 'Unprepared query "' + query + '"'); 389 | var preparedInfo = self.preparedQueries[query]; 390 | preparedInfo.removeConnectionInfo(c.indexInPool); 391 | self.executeAsPrepared(query, params, consistency, options, callback); 392 | } 393 | else { 394 | callback(err, result1, result2); 395 | } 396 | }); 397 | }; 398 | 399 | /** 400 | * It gets an active connection and prepares the query on it, queueing the callback in case its already prepared. 401 | * @param {String} query Query to prepare with ? as placeholders 402 | * @param {function} callback Executes callback(err, con, queryId) when there is a prepared statement on a connection or there is an error. 403 | */ 404 | Client.prototype._getPrepared = function (query, callback) { 405 | var preparedInfo = this.preparedQueries[query]; 406 | if (!preparedInfo) { 407 | preparedInfo = new PreparedInfo(query); 408 | this.preparedQueries[query] = preparedInfo; 409 | } 410 | var self = this; 411 | this._getAConnection(function(err, con) { 412 | if (err) { 413 | return callback(err); 414 | } 415 | var conInfo = preparedInfo.getConnectionInfo(con.indexInPool); 416 | if (conInfo.queryId !== null) { 417 | //is already prepared on this connection 418 | return callback(null, con, conInfo.queryId); 419 | } 420 | else if (conInfo.preparing) { 421 | //Its being prepared, queue until finish 422 | return conInfo.once('prepared', callback); 423 | } 424 | //start preparing 425 | conInfo.preparing = true; 426 | conInfo.once('prepared', callback); 427 | return self._prepare(conInfo, con, query); 428 | }); 429 | }; 430 | 431 | /** 432 | * Prepares a query on a connection. If it fails (server unhealthy) it retries all the preparing process with a new connection. 433 | */ 434 | Client.prototype._prepare = function (conInfo, con, query) { 435 | this.emit('log', 'info', 'Preparing the query "' + query + '" on connection #' + con.indexInPool); 436 | var self = this; 437 | con.prepare(query, function (err, result) { 438 | conInfo.preparing = false; 439 | if (!err) { 440 | self._setAsPrepared(conInfo, query, result.id); 441 | } 442 | conInfo.emit('prepared', err, con, result ? result.id : null); 443 | }); 444 | }; 445 | 446 | 447 | Client.prototype._setAsPrepared = function (conInfo, query, queryId) { 448 | conInfo.queryId = queryId; 449 | var preparedOnConnection = this.preparedQueries["_" + conInfo.id]; 450 | if (!preparedOnConnection) { 451 | preparedOnConnection = []; 452 | this.preparedQueries["_" + conInfo.id] = preparedOnConnection; 453 | } 454 | preparedOnConnection.push(query); 455 | }; 456 | /** 457 | * Removes all previously stored queries assigned to a connection 458 | */ 459 | Client.prototype._removeAllPrepared = function (con) { 460 | var conKey = "_" + con.indexInPool; 461 | var preparedOnConnection = this.preparedQueries[conKey]; 462 | if (!preparedOnConnection) { 463 | return; 464 | } 465 | for (var i = 0; i < preparedOnConnection.length; i++) { 466 | var query = preparedOnConnection[i]; 467 | this.preparedQueries[query].removeConnectionInfo(con.indexInPool); 468 | } 469 | this.emit('log', 'info', 'Removed ' + preparedOnConnection.length + ' prepared queries for con #' + con.indexInPool); 470 | delete this.preparedQueries[conKey]; 471 | }; 472 | 473 | Client.prototype._isServerUnhealthy = function (err) { 474 | return err && err.isServerUnhealthy; 475 | }; 476 | 477 | Client.prototype._setUnhealthy = function (connection) { 478 | if (!connection.unhealthyAt) { 479 | this.emit('log', 'error', 'Connection #' + connection.indexInPool + ' is being set to Unhealthy'); 480 | connection.unhealthyAt = new Date().getTime(); 481 | this._removeAllPrepared(connection); 482 | } 483 | }; 484 | 485 | Client.prototype._setHealthy = function (connection) { 486 | connection.unhealthyAt = 0; 487 | this.emit('log', 'info', 'Connection #' + connection.indexInPool + ' was set to healthy'); 488 | }; 489 | 490 | Client.prototype._canReconnect = function (connection) { 491 | var timePassed = new Date().getTime() - connection.unhealthyAt; 492 | return timePassed > this.options.staleTime; 493 | }; 494 | 495 | /** 496 | * Determines if a connection can be used 497 | */ 498 | Client.prototype._isHealthy = function (connection) { 499 | return !connection.unhealthyAt; 500 | }; 501 | 502 | /** 503 | * Closes all connections 504 | */ 505 | Client.prototype.shutdown = function (callback) { 506 | async.each(this.connections, function(c, eachCallback) { 507 | c.close(eachCallback); 508 | }, function() { 509 | if (callback) { 510 | callback(); 511 | } 512 | }); 513 | }; 514 | 515 | /** 516 | * Holds the information of the connections in which a query is prepared 517 | */ 518 | function PreparedInfo(query) { 519 | this.query = query; 520 | //stores information for the prepared statement on a connection 521 | this._connectionData = {}; 522 | } 523 | 524 | PreparedInfo.prototype.getConnectionInfo = function (conId) { 525 | conId = conId.toString(); 526 | var info = this._connectionData[conId]; 527 | if (!info) { 528 | info = new events.EventEmitter(); 529 | info.setMaxListeners(0); 530 | info.preparing = false; 531 | info.queryId = null; 532 | info.id = conId; 533 | this._connectionData[conId] = info; 534 | } 535 | return info; 536 | }; 537 | 538 | PreparedInfo.prototype.removeConnectionInfo = function (conId) { 539 | delete this._connectionData[conId]; 540 | }; 541 | 542 | /** 543 | * Represents a error while trying to connect the pool, all the connections failed. 544 | */ 545 | function PoolConnectionError(individualErrors) { 546 | this.name = 'PoolConnectionError'; 547 | this.info = 'Represents a error while trying to connect the pool, all the connections failed.'; 548 | this.individualErrors = individualErrors; 549 | } 550 | util.inherits(PoolConnectionError, Error); 551 | 552 | exports.Client = Client; 553 | exports.Connection = Connection; 554 | exports.types = types; 555 | -------------------------------------------------------------------------------- /test/clientTests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('async'); 3 | var util = require('util'); 4 | var Client = require('../index.js').Client; 5 | var Connection = require('../index.js').Connection; 6 | var utils = require('../lib/utils.js'); 7 | var types = require('../lib/types.js'); 8 | var config = require('./config.js'); 9 | var helper = require('./testHelper.js'); 10 | var keyspace = new types.QueryLiteral('unittestkp1_2'); 11 | types.consistencies.getDefault = function () { return this.one; }; 12 | 13 | var client = null; 14 | var clientOptions = { 15 | hosts: [config.host + ':' + config.port.toString(), config.host2 + ':' + config.port.toString()], 16 | username: config.username, 17 | password: config.password, 18 | keyspace: keyspace 19 | }; 20 | 21 | describe('Client', function () { 22 | before(function (done) { 23 | setup(function () { 24 | client = new Client(clientOptions); 25 | createTable(); 26 | }); 27 | 28 | //recreates a keyspace, using a connection object 29 | function setup(callback) { 30 | var con = new Connection(utils.extend({}, config)); 31 | con.open(function (err) { 32 | if (err) throw err; 33 | else { 34 | con.execute("DROP KEYSPACE ?;", [keyspace], function () { 35 | createKeyspace(con, callback); 36 | }); 37 | } 38 | }); 39 | } 40 | 41 | function createKeyspace(con, callback) { 42 | con.execute("CREATE KEYSPACE ? WITH replication = {'class': 'SimpleStrategy','replication_factor': '3'};", [keyspace], function (err) { 43 | if (err) throw err; 44 | con.close(function () { 45 | callback(); 46 | }); 47 | }); 48 | } 49 | 50 | function createTable() { 51 | client.execute( 52 | "CREATE TABLE sampletable2 (" + 53 | "id int PRIMARY KEY," + 54 | "big_sample bigint," + 55 | "timestamp_sample timestamp," + 56 | "blob_sample blob," + 57 | "decimal_sample decimal," + 58 | "float_sample float," + 59 | "uuid_sample uuid," + 60 | "boolean_sample boolean," + 61 | "double_sample double," + 62 | "list_sample list," + 63 | "set_sample set," + 64 | "map_sample map," + 65 | "int_sample int," + 66 | "inet_sample inet," + 67 | "text_sample text);", [], function (err) { 68 | if (err) throw err; 69 | done(); 70 | }); 71 | } 72 | }); 73 | 74 | describe('constructor', function () { 75 | it('should create the connection pool', function () { 76 | //The pool should be created like [conHost1, conHost2, conHost3, conHost1, conHost2, conHost3, conHost1, ...] 77 | var poolSize = 3; 78 | var localClient = getANewClient({hosts: ['host1', 'host2', 'host3'], poolSize: poolSize}); 79 | var connections = localClient.connections; 80 | assert.ok(connections.length, 9, 'There must be 9 connections (amount hosts * pool size)'); 81 | for (var i = 0; i < poolSize; i++) { 82 | assert.ok( 83 | connections[0 + (i * poolSize)].options.host === 'host1' && 84 | connections[1 + (i * poolSize)].options.host === 'host2' && 85 | connections[2 + (i * poolSize)].options.host === 'host3', 'The connections inside the pool are not correctly ordered'); 86 | } 87 | }); 88 | }); 89 | 90 | describe('#connect()', function () { 91 | it('possible to call connect multiple times in parallel', function (done) { 92 | var localClient = getANewClient(); 93 | async.times(5, function (n, next) { 94 | localClient.connect(next); 95 | }, function (err) { 96 | assert.ok(!err, err); 97 | localClient.shutdown(done); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('#execute()', function () { 103 | it('should allow different argument lengths', function (done) { 104 | async.series([ 105 | function (callback) { 106 | //all params 107 | client.execute('SELECT * FROM system.schema_keyspaces', [], types.consistencies.one, callback); 108 | }, 109 | function (callback) { 110 | //no consistency specified 111 | client.execute('SELECT * FROM system.schema_keyspaces', [], callback); 112 | }, 113 | function (callback) { 114 | //change the meaning of the second parameter to consistency 115 | client.execute('SELECT * FROM system.schema_keyspaces', types.consistencies.one, callback); 116 | }, 117 | function (callback) { 118 | //query params but no params args, consistency specified, must fail 119 | client.execute('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', types.consistencies.one, function(err, result){ 120 | if (!err) { 121 | callback(new Error('Consistency should not be treated as query parameters')); 122 | } 123 | else { 124 | callback(null, result); 125 | } 126 | }); 127 | }, 128 | function (callback) { 129 | //no query params 130 | client.execute('SELECT * FROM system.schema_keyspaces', function(err) { 131 | callback(err); 132 | }); 133 | } 134 | ], 135 | //all finished 136 | function(err, results){ 137 | assert.strictEqual(err, null, err); 138 | assert.ok(results[1], 'The result of a query must not be null nor undefined'); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('should callback with error for Syntax error in the query', function (done) { 144 | client.execute('SELECT WHERE Fail Miserable', function (err, result) { 145 | assert.ok(err, 'Error must be defined and not null'); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('should retry in case the node is down', function (done) { 151 | var localClient = getANewClient(); 152 | //Only 1 retry 153 | localClient.options.maxExecuteRetries = 1; 154 | localClient.options.getAConnectionTimeout = 300; 155 | //Change the behaviour so every err is a "server error" 156 | localClient._isServerUnhealthy = function (err) { 157 | return true; 158 | }; 159 | 160 | localClient.execute('WILL FAIL AND EXECUTE THE METHOD FROM ABOVE', function (err, result, retryCount){ 161 | assert.ok(err, 'The execution must fail'); 162 | assert.equal(retryCount, localClient.options.maxExecuteRetries, 'It must retry executing the times specified ' + retryCount); 163 | localClient.shutdown(done); 164 | }); 165 | }); 166 | 167 | it('should callback after timeout passed', function (done) { 168 | var localClient = getANewClient(); 169 | //wait for short amount of time 170 | localClient.options.getAConnectionTimeout = 200; 171 | //mark all connections as unhealthy 172 | localClient._isHealthy = function() { 173 | return false; 174 | }; 175 | //disallow reconnection 176 | localClient._canReconnect = localClient._isHealthy; 177 | localClient.execute('badabing', function (err) { 178 | assert.ok(err, 'Callback must return an error'); 179 | assert.strictEqual(err.name, 'TimeoutError', 'The error must be a TimeoutError'); 180 | localClient.shutdown(done); 181 | }); 182 | }); 183 | 184 | it('should callback when the keyspace does not exist', function (done) { 185 | var localClient = getANewClient({keyspace: 'this__keyspace__does__not__exist'}); 186 | localClient.execute('SELECT * FROM system.schema_keyspaces', function (err, result) { 187 | assert.ok(err, 'It should return an error as the keyspace does not exist'); 188 | localClient.shutdown(done); 189 | }); 190 | }); 191 | 192 | it('should callback when it was not possible to connect to any host', function (done) { 193 | var localClient = getANewClient({hosts: ['localhost:8080', 'localhost:8080']}); 194 | var errors = []; 195 | async.series([ 196 | function (callback){ 197 | localClient.execute('badabing', function (err) { 198 | if (err) { 199 | errors.push(err); 200 | callback(); 201 | } 202 | }); 203 | }, 204 | function (callback){ 205 | localClient.execute('badabang', function (err) { 206 | if (err) { 207 | errors.push(err); 208 | } 209 | callback(); 210 | }); 211 | } 212 | ], 213 | function () { 214 | assert.strictEqual(errors.length, 2, 'It must callback with an err each time trying to execute'); 215 | if (errors.length === 2) { 216 | assert.strictEqual(errors[0].name, 'PoolConnectionError', 'Errors should be of type PoolConnectionError'); 217 | } 218 | localClient.shutdown(done); 219 | }); 220 | }); 221 | }); 222 | 223 | describe('#executeAsPrepared()', function () { 224 | it('should prepare and execute a query', function (done) { 225 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', [keyspace.toString()], 226 | types.consistencies.one, 227 | function (err, result) { 228 | assert.ok(!err, err); 229 | assert.strictEqual(result.rows.length, 1, 'There should be a row'); 230 | done(); 231 | }); 232 | }); 233 | 234 | it('should retry in case the node goes down', function (done) { 235 | var localClient = getANewClient({maxExecuteRetries: 3, staleTime: 150}); 236 | var counter = -1; 237 | localClient._isServerUnhealthy = function() { 238 | //change the behaviour set it to unhealthy the first time 239 | if (counter < 2) { 240 | counter++; 241 | return true; 242 | } 243 | return false; 244 | }; 245 | 246 | localClient.executeAsPrepared('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', [keyspace.toString()], 247 | types.consistencies.one, 248 | function (err, result) { 249 | assert.ok(!err, err); 250 | assert.ok(result && result.rows.length === 1, 'There should be one row'); 251 | localClient.shutdown(done); 252 | }); 253 | }); 254 | 255 | it('should stop retrying when the limit has been reached', function (done) { 256 | var localClient = getANewClient({maxExecuteRetries: 4, staleTime: 150}); 257 | var counter = -1; 258 | localClient._isServerUnhealthy = function() { 259 | //change the behaviour set it to unhealthy the first time 260 | counter++; 261 | return true; 262 | }; 263 | 264 | localClient.executeAsPrepared('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', [keyspace.toString()], 265 | types.consistencies.one, 266 | function (err, result) { 267 | assert.ok(!result, 'It must stop retrying and callback without a result'); 268 | assert.strictEqual(counter, localClient.options.maxExecuteRetries, 'It must retry an specific amount of times'); 269 | localClient.shutdown(done); 270 | }); 271 | }); 272 | 273 | it('should allow from 2 to 4 arguments', function (done) { 274 | async.series([ 275 | function (callback) { 276 | //all params 277 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces', [], types.consistencies.one, callback); 278 | }, 279 | function (callback) { 280 | //no consistency specified 281 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', [keyspace.toString()], callback); 282 | }, 283 | function (callback) { 284 | //change the meaning of the second parameter to consistency 285 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces', types.consistencies.one, callback); 286 | }, 287 | function (callback) { 288 | //query params but no params args, consistency specified, must fail 289 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', types.consistencies.one, function(err, result){ 290 | if (!err) { 291 | callback(new Error('Consistency should not be treated as query parameters')); 292 | } 293 | else { 294 | callback(null, result); 295 | } 296 | }); 297 | }, 298 | function (callback) { 299 | //no query params 300 | client.executeAsPrepared('SELECT * FROM system.schema_keyspaces', function(err, result) { 301 | assert.ok(result && result.rows && result.rows.length, 'There should at least a row returned'); 302 | callback(err, result); 303 | }); 304 | } 305 | ], 306 | //all finished 307 | function(err, results){ 308 | assert.ok(!err, err); 309 | done(); 310 | }); 311 | }); 312 | 313 | it('should callback with error when the parameter can not be guessed', function (done) { 314 | client.executeAsPrepared( 315 | 'SELECT * FROM system.schema_keyspaces WHERE keyspace_name = ?', 316 | //pass an object: should not be guessed 317 | [{whatever: 1}], 318 | function (err, result){ 319 | assert(err, 'It should callback with error'); 320 | done() 321 | }); 322 | }); 323 | 324 | it('should failover to other nodes and reconnect', function (done) { 325 | this.timeout(10000); 326 | //the client must reconnect and continue 327 | var localClient = getANewClient(); 328 | assert.ok(localClient.connections.length > 1, 'There should be more than 1 connection to test failover'); 329 | async.timesSeries(12, function (n, next) { 330 | if (n == 2) { 331 | //The next write attempt will fail for this connection. 332 | localClient.connections[0].netClient.destroy(); 333 | } 334 | else if (n == 6) { 335 | //Force to no more IO on these socket. 336 | localClient.connections[0].netClient.destroy(); 337 | localClient.connections[1].netClient.destroy(); 338 | } 339 | else if (n == 9 && localClient.connections.length > 1) { 340 | //Force to no more IO on this socket. The next write attempt will fail 341 | localClient.connections[0].netClient.end(); 342 | localClient.connections[1].netClient.end(); 343 | } 344 | localClient.execute('SELECT * FROM system.schema_keyspaces', function (err) { 345 | next(err); 346 | }); 347 | }, function (err) { 348 | assert.ok(!err, 'There must not be an error returned. It must retried.'); 349 | localClient.shutdown(done); 350 | }); 351 | }); 352 | 353 | it('should failover to other nodes and reconnect, in parallel executes', function (done) { 354 | //the client must reconnect and continue 355 | var localClient = getANewClient(); 356 | assert.ok(localClient.connections.length > 1, 'There should be more than 1 connection to test failover'); 357 | async.times(10, function (n, next) { 358 | localClient.execute('SELECT * FROM system.schema_keyspaces', function (err) { 359 | if (n === 3) { 360 | localClient.connections[0].netClient.destroy(); 361 | localClient.connections[1].netClient.destroy(); 362 | } 363 | next(err); 364 | }) 365 | }, function (err) { 366 | assert.ok(!err, 'There must not be an error returned. All parallel queries should be retried.'); 367 | localClient.shutdown(done); 368 | }); 369 | }); 370 | 371 | it('should prepare query again when expired from the server', function (done) { 372 | var query = 'SELECT * FROM system.schema_keyspaces'; 373 | var localClient = getANewClient({hosts: [config.host + ':' + config.port]}); 374 | var con = localClient.connections[0]; 375 | async.series([ 376 | localClient.connect.bind(localClient), 377 | function (next) { 378 | localClient.executeAsPrepared(query, next); 379 | }, 380 | function (next) { 381 | var queryId = localClient.preparedQueries[query].getConnectionInfo(con.indexInPool).queryId; 382 | //Change the queryId 383 | queryId[0] = 0; 384 | queryId[1] = 0; 385 | next(); 386 | }, 387 | function (next) { 388 | localClient.executeAsPrepared(query, next); 389 | } 390 | ], done); 391 | }); 392 | 393 | it('should add the query to the error object', function (done) { 394 | var query = 'SELECT WILL FAIL MISERABLY'; 395 | client.executeAsPrepared(query, function (err) { 396 | assert.ok(err, 'There should be an error.'); 397 | assert.ok(err.query, query); 398 | done(); 399 | }); 400 | }); 401 | }); 402 | 403 | 404 | describe('#streamField()', function () { 405 | it('should yield a readable stream', function (done) { 406 | var id = 100; 407 | var blob = new Buffer('Freaks and geeks 1999'); 408 | async.series([ 409 | function (next) { 410 | client.execute(queryStreamInsert, [id, blob], next); 411 | }, 412 | function (next) { 413 | client.streamField(queryStreamSelect, [id], function (n, row, blobStream) { 414 | assert.strictEqual(row.get('id'), id, 'The row must be retrieved'); 415 | assert.strictEqual(n, 0); 416 | assertStreamReadable(blobStream, blob, next); 417 | }); 418 | } 419 | ], done); 420 | }); 421 | 422 | it('should callback with error', function (done) { 423 | client.streamField('SELECT TO FAIL MISERABLY', function (n, row, stream) { 424 | assert.ok(fail, 'It should never execute the row callback'); 425 | }, function (err) { 426 | assert.ok(err, 'It should yield an error'); 427 | done(); 428 | }); 429 | }); 430 | 431 | it('should yield a null stream when the field is null', function (done) { 432 | var id = 110; 433 | var blob = null; 434 | 435 | async.series([ 436 | function (next) { 437 | client.execute(queryStreamInsert, [id, blob], next); 438 | }, 439 | function (next) { 440 | client.streamField(queryStreamSelect, [id], function (n, row, blobStream) { 441 | assert.strictEqual(row.get('id'), id, 'The row must be retrieved'); 442 | assert.equal(blobStream, null, 'The file stream must be NULL'); 443 | next(); 444 | }); 445 | } 446 | ], done); 447 | }); 448 | }); 449 | 450 | describe('#eachRow()', function () { 451 | it('should callback and return all fields', function (done) { 452 | var id = 150; 453 | var blob = new Buffer('Frank Gallagher'); 454 | 455 | async.series([ 456 | function (next) { 457 | client.execute(queryStreamInsert, [id, blob], next); 458 | }, 459 | function (next) { 460 | client.eachRow(queryStreamSelect, [id], function (n, row) { 461 | assert.ok(row && row.get('id') && row.get('blob_sample') && row.get('blob_sample').toString() === blob.toString()); 462 | }, function (err, totalRows) { 463 | assert.strictEqual(totalRows, 1); 464 | next(err); 465 | }); 466 | } 467 | ], done); 468 | }); 469 | 470 | it('should callback once per row', function (done) { 471 | var id = 160; 472 | var blob = new Buffer('Jack Bauer'); 473 | var counter = 0; 474 | async.timesSeries(4, function (n, next) { 475 | client.execute(queryStreamInsert, [id+n, blob], next); 476 | }, function (err) { 477 | if (err) return done(err); 478 | client.eachRow('SELECT id, blob_sample FROM sampletable2 WHERE id IN (?, ?, ?, ?);', [id, id+1, id+2, id+3], function (n, row) { 479 | assert.strictEqual(n, counter); 480 | assert.ok(row && row.get('id') && row.get('blob_sample')); 481 | counter++; 482 | }, function (err, totalRows) { 483 | assert.strictEqual(totalRows, counter); 484 | done(err); 485 | }); 486 | }); 487 | }); 488 | 489 | it('should callback when there is an error', function (done) { 490 | async.series([ 491 | function prepareQueryFail(next) { 492 | //it should fail when preparing 493 | client.eachRow('SELECT TO FAIL MISERABLY', [], function () { 494 | assert.ok(false, 'This callback should not be called'); 495 | }, function (err) { 496 | assert.ok(err, 'There should be an error yielded'); 497 | next(); 498 | }); 499 | }, 500 | function executeQueryFail(next) { 501 | //it should fail when executing the query: There are more parameters than expected 502 | client.eachRow('SELECT * FROM system.schema_keyspaces', [1, 2, 3], function () { 503 | assert.ok(false, 'This callback should not be called'); 504 | }, function (err) { 505 | assert.ok(err, 'There should be an error yielded'); 506 | next(); 507 | }); 508 | } 509 | ], done); 510 | 511 | }); 512 | }); 513 | 514 | describe('#stream()', function () { 515 | var selectInQuery = 'SELECT * FROM sampletable2 where id IN (?, ?, ?, ?)'; 516 | 517 | before(function (done) { 518 | var insertQuery = 'INSERT INTO sampletable2 (id, text_sample) values (?, ?)'; 519 | helper.batchInsert(client, insertQuery, [[200, '200-Z'], [201, '201-Z'], [202, '202-Z']], done); 520 | }); 521 | 522 | it('should stream rows', function (done) { 523 | var rows = []; 524 | var stream = client.stream(selectInQuery, [200, 201, -1, -1], helper.throwop); 525 | stream 526 | .on('end', function () { 527 | assert.strictEqual(rows.length, 2); 528 | done(); 529 | }).on('readable', function () { 530 | var row; 531 | while (row = this.read()) { 532 | assert.ok(row.get('id'), 'It should yield the id value'); 533 | assert.strictEqual(row.get('id').toString()+'-Z', row.get('text_sample'), 534 | 'The id and the text value should be related'); 535 | rows.push(row); 536 | } 537 | }).on('error', done); 538 | }); 539 | 540 | it('should end when no rows', function (done) { 541 | var stream = client.stream(selectInQuery, [-1, -1, -1, -1], helper.throwop); 542 | stream 543 | .on('end', done) 544 | .on('readable', function () { 545 | assert.ok(false, 'Readable event should not be fired'); 546 | }).on('error', done); 547 | }); 548 | 549 | it('should emit a ResponseError', function (done) { 550 | var counter = 0; 551 | var stream = client.stream(selectInQuery, [0], function (err) { 552 | assert.ok(err, 'It should callback with error'); 553 | if (++counter === 2) done(); 554 | }); 555 | stream.on('error', function (err) { 556 | assert.strictEqual(err.name, 'ResponseError'); 557 | if (++counter === 2) done(); 558 | }); 559 | }); 560 | 561 | it('bad query should emit a ResponseError', function (done) { 562 | var counter = 0; 563 | var stream = client.stream('SELECT SHOULD FAIL', function (err) { 564 | assert.ok(err, 'It should callback with error'); 565 | if (++counter === 2) done(); 566 | }); 567 | stream.on('error', function (err) { 568 | assert.strictEqual(err.name, 'ResponseError'); 569 | if (++counter === 2) done(); 570 | }); 571 | }); 572 | 573 | it('should be optional to provide a callback', function (done) { 574 | var rows = []; 575 | client.stream(selectInQuery, [200, 201, 202, -1]) 576 | .on('end', function () { 577 | assert.strictEqual(rows.length, 3); 578 | done(); 579 | }) 580 | .on('readable', function () { 581 | var row; 582 | while (row = this.read()) { 583 | rows.push(row); 584 | } 585 | }).on('error', done); 586 | }); 587 | }); 588 | 589 | after(function (done) { 590 | client.shutdown(done); 591 | }); 592 | }); 593 | 594 | function getANewClient (options) { 595 | return new Client(utils.extend({}, clientOptions, options || {})); 596 | } 597 | 598 | function assertStreamReadable(stream, originalBlob, callback) { 599 | var length = 0; 600 | var firstByte = null; 601 | var lastByte = null; 602 | stream.on('readable', function () { 603 | var chunk = null; 604 | while (chunk = stream.read()) { 605 | length += chunk.length; 606 | if (firstByte === null) { 607 | firstByte = chunk[0]; 608 | } 609 | if (length === originalBlob.length) { 610 | lastByte = chunk[chunk.length-1]; 611 | } 612 | } 613 | }); 614 | stream.on('end', function () { 615 | assert.equal(length, originalBlob.length, 'The blob returned should be the same size'); 616 | assert.equal(firstByte, originalBlob[0], 'The first byte of the stream and the blob dont match'); 617 | assert.equal(lastByte, originalBlob[originalBlob.length-1], 'The last byte of the stream and the blob dont match'); 618 | callback(); 619 | }); 620 | } 621 | 622 | var queryStreamInsert = 'INSERT INTO sampletable2 (id, blob_sample) VALUES (?, ?)'; 623 | var queryStreamSelect = 'SELECT id, blob_sample FROM sampletable2 WHERE id = ?'; -------------------------------------------------------------------------------- /test/connectionTests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var util = require('util'); 3 | var async = require('async'); 4 | 5 | var Connection = require('../index.js').Connection; 6 | var types = require('../lib/types.js'); 7 | var utils = require('../lib/utils.js'); 8 | var dataTypes = types.dataTypes; 9 | var helper = require('./testHelper.js'); 10 | var keyspace = new types.QueryLiteral('unittestkp1_1'); 11 | var config = require('./config.js'); 12 | 13 | types.consistencies.getDefault = function () {return this.one}; 14 | 15 | var con = new Connection(utils.extend({}, config, {maxRequests: 32})); 16 | 17 | describe('Connection', function () { 18 | before(function (done) { 19 | this.timeout(5000); 20 | async.series([ 21 | function open (callback) { 22 | con.open(callback); 23 | }, 24 | function dropKeyspace(callback) { 25 | con.execute("DROP KEYSPACE ?;", [keyspace], function (err) { 26 | if (err && err.name === 'ResponseError') { 27 | //don't mind if there is an response error 28 | err = null; 29 | } 30 | callback(err); 31 | }); 32 | }, 33 | function createKeyspace(callback) { 34 | con.execute("CREATE KEYSPACE ? WITH replication = {'class': 'SimpleStrategy','replication_factor': '3'};", [keyspace], callback); 35 | }, 36 | function useKeyspace(callback) { 37 | con.execute("USE ?;", [keyspace], callback); 38 | }, 39 | function createTestTable(callback) { 40 | con.execute( 41 | "CREATE TABLE sampletable1 (" + 42 | "id int PRIMARY KEY," + 43 | "big_sample bigint," + 44 | "timestamp_sample timestamp," + 45 | "blob_sample blob," + 46 | "decimal_sample decimal," + 47 | "float_sample float," + 48 | "uuid_sample uuid," + 49 | "timeuuid_sample timeuuid," + 50 | "boolean_sample boolean," + 51 | "double_sample double," + 52 | "list_sample list," + 53 | "list_float_sample list," + 54 | "set_sample set," + 55 | "map_sample map," + 56 | "int_sample int," + 57 | "inet_sample inet," + 58 | "text_sample text);" 59 | , callback); 60 | } 61 | ], done); 62 | }); 63 | 64 | describe('#open()', function () { 65 | it('should fail when the host does not exits', function (done) { 66 | this.timeout(5000); 67 | var localCon = new Connection(utils.extend({}, config, {host: 'not-existent-host'})); 68 | localCon.open(function (err) { 69 | assert.ok(err, 'Must return a connection error'); 70 | assert.ok(!localCon.connected && !localCon.connecting); 71 | localCon.close(done); 72 | }); 73 | }); 74 | it('should fail when the keyspace does not exist', function (done) { 75 | var localCon = new Connection(utils.extend({}, con.options, {keyspace: 'this__keyspace__does__not__exist'})); 76 | localCon.open(function (err) { 77 | assert.ok(err, 'An error must be returned as the keyspace does not exist'); 78 | localCon.close(done); 79 | }); 80 | }); 81 | it('should fail when the username and password are incorrect', function (done) { 82 | this.timeout(5000); 83 | var localCon = new Connection(utils.extend({}, con.options, {password: 'invalidpassword789'})); 84 | localCon.open(function (err) { 85 | //it could be that authentication is off on the node 86 | //in that case, do not assert anything 87 | if (err) { 88 | assert.strictEqual(err.name, 'ResponseError'); 89 | assert.strictEqual(err.code, types.responseErrorCodes.badCredentials); 90 | } 91 | localCon.close(done); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('#execute()', function () { 97 | it('should allow from 2 to 4 arguments and use defaults', function (done) { 98 | async.series([ 99 | function (callback) { 100 | //all params 101 | con.execute('SELECT * FROM system.schema_keyspaces', [], types.consistencies.one, function(err){ 102 | callback(err); 103 | }); 104 | }, 105 | function (callback) { 106 | //no consistency specified 107 | con.execute('SELECT * FROM system.schema_keyspaces', [], function(err){ 108 | callback(err); 109 | }); 110 | }, 111 | function (callback) { 112 | //change the meaning of the second parameter to consistency 113 | con.execute('SELECT * FROM system.schema_keyspaces', types.consistencies.one, function(err){ 114 | callback(err); 115 | }); 116 | }, 117 | function (callback) { 118 | //query params but no params args, consistency specified, must fail 119 | con.execute('SELECT * FROM system.schema_keyspaces keyspace_name = ?', types.consistencies.one, function(err){ 120 | if (!err) { 121 | callback(new Error('Consistency should not be treated as query parameters')); 122 | } 123 | else { 124 | callback(null); 125 | } 126 | }); 127 | }, 128 | function (callback) { 129 | //no query params 130 | con.execute('SELECT * FROM system.schema_keyspaces', function(err){ 131 | callback(err); 132 | }); 133 | } 134 | ], done); 135 | }); 136 | 137 | it ('should return a zero-length Array of rows when no matches', function (done) { 138 | con.execute('SELECT * FROM sampletable1 WHERE ID = -1', function (err, result) { 139 | assert.ok(!err, err); 140 | assert.ok(result.rows && result.rows.length === 0, 'Rows must be defined and the length of the rows array must be zero'); 141 | done(); 142 | }); 143 | }); 144 | 145 | it('should retrieve all the columns in the select statement in order', function (done) { 146 | con.execute('select keyspace_name, strategy_class, strategy_options from system.schema_keyspaces;', [], function(err, result) { 147 | assert.ok(!err, err); 148 | assert.ok(result.rows.length > 0, 'No keyspaces'); 149 | assert.strictEqual(result.meta.columns.length, 3, 'There must be 2 columns'); 150 | var row = result.rows[0]; 151 | assert.ok(row.get('keyspace_name'), 'Get cell by column name failed'); 152 | assert.strictEqual(row.get(0), row['keyspace_name']); 153 | assert.strictEqual(row.get(1), row['strategy_class']); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('should return javascript null for null stored values', function (done) { 159 | con.execute('INSERT INTO sampletable1 (id) VALUES(1)', null, function(err) { 160 | assert.ok(!err, err); 161 | con.execute('select * from sampletable1 where id=?', [1], function (err, result) { 162 | assert.ok(!err, err); 163 | assert.ok(result.rows && result.rows.length === 1); 164 | var row = result.rows[0]; 165 | assert.strictEqual(row.get('big_sample'), null); 166 | assert.strictEqual(row.get('blob_sample'), null); 167 | assert.strictEqual(row.get('decimal_sample'), null); 168 | assert.strictEqual(row.get('list_sample'), null); 169 | assert.strictEqual(row.get('map_sample'), null); 170 | assert.strictEqual(row.get('set_sample'), null); 171 | assert.strictEqual(row.get('text_sample'), null); 172 | done(); 173 | }); 174 | }); 175 | }); 176 | 177 | it('should callback with a ResponseError when the query is malformed', function (done) { 178 | con.execute("Malformed SLEECT SELECT * FROM sampletable1;", null, function(err) { 179 | assert.ok(err, 'This query must yield an error.'); 180 | assert.ok(!err.isServerUnhealthy, 'The connection should be reusable and the server should be healthy even if a query fails.'); 181 | assert.strictEqual(err.name, 'ResponseError', 'The error should be of type ResponseError'); 182 | done(); 183 | }); 184 | }); 185 | 186 | it('should store and retrieve the same bigint value', function (done) { 187 | var big = new types.Long('0x0123456789abcdef0'); 188 | con.execute('INSERT INTO sampletable1 (id, big_sample) VALUES(100, ?)', [big], function(err) { 189 | assert.ok(!err, err); 190 | con.execute('select id, big_sample from sampletable1 where id=100;', null, function (err, result) { 191 | assert.ok(!err, err); 192 | assert.strictEqual( 193 | types.Long.toBuffer(big).toString('hex'), 194 | types.Long.toBuffer(result.rows[0].big_sample).toString('hex'), 195 | 'Retrieved bigint does not match.'); 196 | done(); 197 | }); 198 | }); 199 | }); 200 | 201 | it('should store and retrieve the same uuid value', function (done) { 202 | var uuidValue = types.uuid(); 203 | con.execute('INSERT INTO sampletable1 (id, uuid_sample) VALUES(150, ?)', [uuidValue], function (err, result) { 204 | assert.ok(!err, err); 205 | con.execute('SELECT id, uuid_sample FROM sampletable1 WHERE ID=150', function (err, result) { 206 | assert.ok(!err, err); 207 | assert.strictEqual(result.rows[0].uuid_sample, uuidValue); 208 | done(); 209 | }); 210 | }); 211 | }); 212 | 213 | it('should store and retrieve the same timeuuid value', function (done) { 214 | var timeUuidValue = types.timeuuid(); 215 | con.execute('INSERT INTO sampletable1 (id, timeuuid_sample) VALUES(151, ?)', [timeUuidValue], function (err, result) { 216 | assert.ok(!err, err); 217 | con.execute('SELECT id, timeuuid_sample, dateOf(timeuuid_sample) as date1 FROM sampletable1 WHERE ID=151', function (err, result) { 218 | assert.ok(!err, err); 219 | assert.ok(result.rows[0].date1 instanceof Date, 'The dateOf function should yield an instance of date'); 220 | assert.strictEqual(result.rows[0].timeuuid_sample, timeUuidValue); 221 | done(); 222 | }); 223 | }); 224 | }); 225 | 226 | it('should insert the same value as param or as query literal', function (done) { 227 | var insertQueries = [ 228 | ["INSERT INTO sampletable1 (id, big_sample, blob_sample, decimal_sample, list_sample, list_float_sample, set_sample, map_sample, text_sample)" + 229 | " values (200, 1, 0x48656c6c6f, 1, [1, 2, 3], [1.1, 1, 1.02], {1, 2, 3}, {'a': 'value a', 'b': 'value b'}, 'abc');"], 230 | ["INSERT INTO sampletable1 (id, big_sample, blob_sample, decimal_sample, list_sample, list_float_sample, set_sample, map_sample, text_sample)" + 231 | " values (201, ?, ?, ?, ?, ?, ?, ?, ?);", [1, new Buffer('Hello', 'utf-8'), 1, 232 | [1, 2, 3], {hint: 'list', value: [1.1, 1, 1.02]}, {hint: 'set', value: [1, 2, 3]}, {hint: 'map', value: {'a': 'value a', 'b': 'value b'}}, 'abc']], 233 | ["INSERT INTO sampletable1 (id, big_sample, blob_sample, decimal_sample, list_sample, list_float_sample, set_sample, map_sample, text_sample)" + 234 | " values (202, NULL, NULL, NULL, NULL, NULL, NULL, NULL, null);"], 235 | ["INSERT INTO sampletable1 (id, big_sample, blob_sample, decimal_sample, list_sample, list_float_sample, set_sample, map_sample, text_sample)" + 236 | " values (203, ?, ?, ?, ?, ?, ?, ?, ?);", [null, null, null, null, null, null, null, null]] 237 | ]; 238 | async.each(insertQueries, function(query, callback) { 239 | con.execute(query[0], query[1], function(err, result) { 240 | callback(err); 241 | }); 242 | }, function (err) { 243 | assert.ok(!err, err); 244 | con.execute("select id, big_sample, blob_sample, decimal_sample, list_sample, list_float_sample, set_sample, map_sample, text_sample from sampletable1 where id IN (200, 201, 202, 203);", null, function(err, result) { 245 | assert.ok(!err, err); 246 | setRowsByKey(result.rows, 'id'); 247 | //sort the rows retrieved 248 | var rows = [result.rows.get(200), result.rows.get(201), result.rows.get(202), result.rows.get(203)] 249 | 250 | //test that coming from parameters or hardcoded query, it stores and yields the same values 251 | compareRows(rows[0], rows[1], ['big_sample', 'blob_sample', 'decimal_sample', 'list_sample', 'list_float_sample', 'set_sample', 'map_sample', 'text_sample']); 252 | compareRows(rows[2], rows[3], ['big_sample', 'blob_sample', 'decimal_sample', 'list_sample', 'list_float_sample', 'set_sample', 'map_sample', 'text_sample']); 253 | done(); 254 | }); 255 | }); 256 | }); 257 | 258 | it('should handle multiple parallel queries and callback per each', function (done) { 259 | async.times(15, function(n, callback) { 260 | con.execute('SELECT * FROM sampletable1', [], function (err, result) { 261 | assert.ok(result && result.rows && result.rows.length, 'It should return all rows'); 262 | callback(err, result); 263 | }); 264 | }, function (err, results) { 265 | done(err); 266 | }); 267 | }); 268 | 269 | it('queue the query when the amount of parallel requests is reached', function (done) { 270 | //tests that max streamId is reached and the connection waits for a free id 271 | var options = utils.extend({}, con.options, {maxRequests: 10, maxRequestsRetry: 0}); 272 | //total amount of queries to issue 273 | var totalQueries = 50; 274 | var timeoutId; 275 | var localCon = new Connection(options); 276 | localCon.open(function (err) { 277 | assert.ok(!err, err); 278 | async.times(totalQueries, function (n, callback) { 279 | localCon.execute('SELECT * FROM ?.sampletable1 WHERE ID IN (?, ?, ?);', [keyspace, 1, 100, 200], callback); 280 | }, done); 281 | }); 282 | }); 283 | }); 284 | 285 | describe('#prepare()', function () { 286 | it('should prepare a query and yield the id', function (done) { 287 | con.prepare('select id, big_sample from sampletable1 where id = ?', function (err, result) { 288 | assert.ok(result.id, 'The query id must be defined'); 289 | done(err); 290 | }); 291 | }); 292 | 293 | it('should prepare the queries and yield the ids', function (done) { 294 | async.series([ 295 | function (callback) { 296 | con.prepare("select id, big_sample from sampletable1 where id = ?", callback); 297 | }, 298 | function (callback) { 299 | con.prepare("select id, big_sample from sampletable1 where id = ?", callback); 300 | }, 301 | function (callback) { 302 | con.prepare("select id, big_sample, map_sample from sampletable1 where id = ?", callback); 303 | } 304 | ], function (err, results) { 305 | assert.ok(!err, err); 306 | assert.ok(results[0].id.toString('hex').length > 0); 307 | assert.ok(results[0].id.toString('hex') === results[1].id.toString('hex'), 'Ids to same queries should be the same'); 308 | assert.ok(results[1].id.toString('hex') !== results[2].id.toString('hex'), 'Ids to different queries should be different'); 309 | done(); 310 | }); 311 | }); 312 | }); 313 | 314 | describe('#executePrepared()', function () { 315 | it('should serialize all types correctly', function (done) { 316 | function prepareInsertTest(idValue, columnName, columnValue, hint, compareFunc) { 317 | if (!compareFunc) { 318 | compareFunc = function (value) {return value}; 319 | } 320 | var paramValue = columnValue; 321 | if (hint) { 322 | paramValue = {value: paramValue, hint: hint}; 323 | } 324 | return function (callback) { 325 | con.prepare("INSERT INTO sampletable1 (id, " + columnName + ") VALUES (" + idValue + ", ?)", function (err, result) { 326 | assert.ok(!err, err); 327 | con.executePrepared(result.id, [paramValue], types.consistencies.one, function (err, result) { 328 | assert.ok(!err, err); 329 | con.execute("SELECT id, " + columnName + " FROM sampletable1 WHERE ID=" + idValue, function (err, result) { 330 | assert.ok(!err, err); 331 | assert.ok(result.rows && result.rows.length === 1, 'There must be a row'); 332 | var newValue = compareFunc(result.rows[0].get(columnName)); 333 | assert.strictEqual(newValue, compareFunc(columnValue), 'The value does not match: ' + newValue + ':' + compareFunc(columnValue)); 334 | callback(err); 335 | }); 336 | }); 337 | }); 338 | } 339 | } 340 | var toStringCompare = function (value) { return value.toString(); }; 341 | var toTimeCompare = function (value) { 342 | if (value instanceof Date) { 343 | return value.getTime(); 344 | } 345 | if (value instanceof types.Long) { 346 | return Long.toBuffer(value).toString('hex'); 347 | } 348 | return value; 349 | }; 350 | var roundNumberCompare = function (digits) { 351 | return function toNumber(value) { 352 | return value.toFixed(digits); 353 | }; 354 | }; 355 | async.series([ 356 | prepareInsertTest(300, 'text_sample', 'Señor Dexter', dataTypes.varchar), 357 | prepareInsertTest(301, 'text_sample', 'Morgan', dataTypes.ascii), 358 | prepareInsertTest(302, 'text_sample', null, dataTypes.varchar), 359 | prepareInsertTest(303, 'int_sample', 1500, dataTypes.int), 360 | prepareInsertTest(304, 'float_sample', 123.6, dataTypes.float, roundNumberCompare(1)), 361 | prepareInsertTest(305, 'float_sample', 123.001, dataTypes.float, roundNumberCompare(3)), 362 | prepareInsertTest(306, 'double_sample', 123.00123, dataTypes.double, roundNumberCompare(5)), 363 | prepareInsertTest(307, 'boolean_sample', true, dataTypes.boolean), 364 | //lists, sets and maps 365 | prepareInsertTest(310, 'list_sample', [1,2,80], dataTypes.list, toStringCompare), 366 | prepareInsertTest(311, 'set_sample', [1,2,80,81], dataTypes.set, toStringCompare), 367 | prepareInsertTest(312, 'list_sample', [], dataTypes.list, function (value) { 368 | if (value != null && value.length === 0) { 369 | //empty sets and lists are stored as null values 370 | return null; 371 | } 372 | return value; 373 | }), 374 | prepareInsertTest(313, 'map_sample', {key1: "value1", key2: "value2"}, dataTypes.map, toStringCompare), 375 | prepareInsertTest(314, 'list_sample', [50,30,80], 'list', toStringCompare), 376 | prepareInsertTest(315, 'set_sample', [1,5,80], 'set', toStringCompare), 377 | prepareInsertTest(316, 'list_float_sample', [1,5], 'list', util.inspect), 378 | //ip addresses 379 | prepareInsertTest(320, 'inet_sample', new Buffer([192,168,0,50]), dataTypes.inet, toStringCompare), 380 | //ip 6 381 | prepareInsertTest(321, 'inet_sample', new Buffer([1,0,0,0,1,0,0,0,1,0,0,0,192,168,0,50]), dataTypes.inet, toStringCompare), 382 | prepareInsertTest(330, 'big_sample', new types.Long(1010, 10), dataTypes.bigint, toStringCompare), 383 | prepareInsertTest(331, 'big_sample', 10, dataTypes.bigint, toStringCompare), 384 | prepareInsertTest(332, 'timestamp_sample', 1372753805600, dataTypes.timestamp, toTimeCompare), 385 | prepareInsertTest(333, 'timestamp_sample', new Date(2013,5,20,19,1,1,550), dataTypes.timestamp, toTimeCompare), 386 | prepareInsertTest(340, 'uuid_sample', types.uuid(), dataTypes.uuid, toStringCompare), 387 | prepareInsertTest(341, 'uuid_sample', types.timeuuid(), dataTypes.timeuuid, toStringCompare) 388 | ], function (err) { 389 | assert.ok(!err, err); 390 | done(); 391 | }); 392 | }); 393 | 394 | it('should callback with err when param cannot be guessed', function (done) { 395 | var query = 'SELECT * FROM sampletable1 WHERE ID=?'; 396 | var unspecifiedParam = {random: 'param'}; 397 | con.execute(query, [unspecifiedParam], function (err) { 398 | assert.ok(err, 'An error must be yielded in the callback'); 399 | prepareAndExecute(con, query, [unspecifiedParam], function (err) { 400 | assert.ok(err, 'An error must be yielded in the callback'); 401 | done(); 402 | }); 403 | }); 404 | }); 405 | 406 | it('should callback per each row and at the end when rowCb is specified', function (done) { 407 | con.prepare('select * from system.schema_keyspaces', function (err, result) { 408 | assert.ok(!err, err); 409 | var rows = 0; 410 | con.executePrepared(result.id, [], types.consistencies.one, { 411 | byRow: true, 412 | rowCallback: function (n, row) { 413 | rows++; 414 | } 415 | }, function (err, rowCount) { 416 | assert.ok(rows > 0, 'it should callback per each row'); 417 | done(err); 418 | }); 419 | }); 420 | }); 421 | 422 | describe('Field streaming', function () { 423 | it('should stream the last column and be readable', function (done) { 424 | var blob = new Buffer(2*1024*1024); 425 | blob[0] = 245; 426 | var id = 400; 427 | insertAndStream(con, blob, id, true, function (err, row, stream) { 428 | assert.equal(row.get('id'), id); 429 | //test that stream is readable 430 | assertStreamReadable(stream, blob, done); 431 | }); 432 | }); 433 | 434 | it('should stream the last column and buffer if not read', function (done) { 435 | var blob = new Buffer(2048); 436 | blob[2047] = 0xFA; 437 | var id = 401; 438 | insertAndStream(con, blob, id, true, function (err, row, stream) { 439 | //delay some milliseconds the read of the stream and hope it's buffering 440 | setTimeout(function () { 441 | assertStreamReadable(stream, blob, done); 442 | }, 700); 443 | }); 444 | }); 445 | 446 | it('should return a null value when streaming a null field', function (done) { 447 | var id = 402; 448 | insertAndStream(con, null, id, true, function (err, row, stream) { 449 | assert.equal(stream, null, 'The stream must be null'); 450 | done(); 451 | }); 452 | }); 453 | 454 | it('should callback with an error when there is a bad input', function (done) { 455 | con.prepare('SELECT id, blob_sample FROM sampletable1 WHERE ID = ?', function (err, result) { 456 | assert.ok(!err, err); 457 | con.executePrepared(result.id, ['BAD INPUT'], types.consistencies.one, { 458 | byRow: true, 459 | streamField: true, 460 | rowCallback: function (){ 461 | assert.ok(false, 'row callback should not be called'); 462 | } 463 | }, 464 | function (err) { 465 | assert.ok(err, 'There must be an error returned, as the query is not valid.'); 466 | done(); 467 | }); 468 | }); 469 | }); 470 | 471 | it('should callback when there is no match', function (done) { 472 | con.prepare('SELECT id, blob_sample FROM sampletable1 WHERE ID = ?', function (err, result) { 473 | assert.ok(!err, err); 474 | con.executePrepared(result.id, [-1], types.consistencies.one, { 475 | byRow: true, 476 | streamField: true, 477 | rowCallback: function () { 478 | assert.ok(false, 'row callback should not be called'); 479 | } 480 | }, done); 481 | }); 482 | }); 483 | 484 | it('should callback one time per row', function (done) { 485 | var values = [[410, new Buffer(1021)],[411, new Buffer(10*1024)],[412, new Buffer(4)],[413, new Buffer(256)]]; 486 | values[1][1][0] = 100; 487 | function insert(item, callback) { 488 | con.execute('INSERT INTO sampletable1 (id, blob_sample) VALUES (?, ?)', item, callback); 489 | } 490 | 491 | function testInsertedValues() { 492 | var rowCounter = 0; 493 | var totalValues = values.length; 494 | var blobStreamArray = []; 495 | //index the values by id for easy access 496 | for (var i = 0; i < totalValues; i++) { 497 | values[values[i][0].toString()] = values[i][1]; 498 | } 499 | con.prepare('SELECT id, blob_sample FROM sampletable1 WHERE ID IN (?, ?, ?, ?)', function (err, result) { 500 | assert.ok(!err, err); 501 | con.executePrepared(result.id, 502 | [values[0][0], values[1][0], values[2][0], values[3][0]], types.consistencies.one, { 503 | byRow: true, 504 | streamField: true, 505 | rowCallback: function (n, row, stream) { 506 | var originalBlob = values[row.get('id').toString()]; 507 | rowCounter++; 508 | blobStreamArray.push([stream, originalBlob]); 509 | } 510 | }, 511 | function (err, rowLength) { 512 | assert.ok(!err, err); 513 | assert.strictEqual(rowCounter, rowLength); 514 | assert.strictEqual(rowCounter, totalValues); 515 | async.mapSeries(blobStreamArray, function (item, next) { 516 | assertStreamReadable(item[0], item[1], next); 517 | }, done); 518 | } 519 | ); 520 | }); 521 | } 522 | 523 | async.map(values, insert, function (err, results) { 524 | assert.ok(!err, err); 525 | testInsertedValues(); 526 | }); 527 | }); 528 | 529 | }); 530 | }); 531 | 532 | describe('#stream()', function () { 533 | var queryId; 534 | before(function (done) { 535 | var insertQuery = 'INSERT INTO sampletable1 (id, text_sample) values (?, ?)'; 536 | var selectQuery = 'SELECT * FROM sampletable1 where id IN (?, ?, ?, ?)'; 537 | helper.batchInsert( 538 | con, insertQuery, [[500, '500-Z'], [501, '501-Z'], [502, '502-Z']], function (err) { 539 | if (err) done(err); 540 | con.prepare(selectQuery, function (err, result) { 541 | assert.ok(!err, err); 542 | queryId = result.id; 543 | done(); 544 | }); 545 | }); 546 | }); 547 | 548 | it('should stream rows', function (done) { 549 | var rows = []; 550 | var stream = con.stream(queryId, [500, 501, -1, -1], helper.throwop); 551 | stream 552 | .on('end', function () { 553 | assert.strictEqual(rows.length, 2); 554 | done(); 555 | }).on('readable', function () { 556 | var row; 557 | while (row = this.read()) { 558 | assert.ok(row.get('id'), 'It should yield the id value'); 559 | assert.strictEqual(row.get('id').toString()+'-Z', row.get('text_sample'), 560 | 'The id and the text value should be related'); 561 | rows.push(row); 562 | } 563 | }).on('error', done); 564 | }); 565 | }); 566 | 567 | describe('#close()', function () { 568 | it('should callback even if its already closed', function (done) { 569 | var localCon = new Connection(utils.extend({}, config)); 570 | localCon.open(function (err) { 571 | assert.ok(!err, err); 572 | async.timesSeries(10, function (n, callback) { 573 | localCon.close(callback); 574 | }, done); 575 | }); 576 | }); 577 | }); 578 | 579 | after(function (done) { 580 | con.close(done); 581 | }); 582 | }); 583 | 584 | function setRowsByKey(arr, key) { 585 | for (var i=0;i' + originalBlob.length); 622 | assert.strictEqual(firstByte, originalBlob[0], 'The first byte of the stream and the blob dont match'); 623 | assert.strictEqual(lastByte, originalBlob[originalBlob.length-1], 'The last byte of the stream and the blob dont match'); 624 | callback(); 625 | }); 626 | } 627 | 628 | function prepareAndExecute(con, query, params, callback) { 629 | con.prepare(query, function (err, result) { 630 | if (err) return callback(err); 631 | con.executePrepared(result.id, params, callback); 632 | }); 633 | } 634 | 635 | function compareRows(rowA, rowB, fieldList) { 636 | for (var i = 0; i < fieldList.length; i++) { 637 | var field = fieldList[i]; 638 | assert.equal(util.inspect(rowA.get(field)), util.inspect(rowB.get(field)), '#' + rowA.get('id') + ' field ' + field + ' does not match'); 639 | } 640 | } --------------------------------------------------------------------------------