From 31d201fd4c26f4271af1b36a4179a7c523ac1420 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sat, 12 Nov 2016 19:40:54 +1100 Subject: [PATCH 1/7] COMPASS-287 upgrade old language-model to pegjs --- src/app/home/collection.js | 2 +- src/app/models/editable-query.js | 72 ------------------- src/app/models/query-options.js | 15 ++-- .../lib/store/load-more-documents-store.js | 2 +- .../lib/component/create-index-text-field.jsx | 17 ++--- .../query/lib/store/index.js | 23 +++--- 6 files changed, 25 insertions(+), 106 deletions(-) delete mode 100644 src/app/models/editable-query.js diff --git a/src/app/home/collection.js b/src/app/home/collection.js index a016465a7cc..66c1b0be153 100644 --- a/src/app/home/collection.js +++ b/src/app/home/collection.js @@ -213,7 +213,7 @@ var MongoDBCollectionView = View.extend({ this.loadIndexesAction(); this.fetchExplainPlanAction(); }); - Action.filterChanged(app.queryOptions.query.serialize()); + Action.filterChanged(app.queryOptions.query); this.switchView(this.activeView); }, onCollectionFetched: function(model) { diff --git a/src/app/models/editable-query.js b/src/app/models/editable-query.js deleted file mode 100644 index 78002e7d007..00000000000 --- a/src/app/models/editable-query.js +++ /dev/null @@ -1,72 +0,0 @@ -var Model = require('ampersand-model'); -var EJSON = require('mongodb-extended-json'); -var Query = require('mongodb-language-model').Query; -var _ = require('lodash'); -// var debug = require('debug')('mongodb-compass:models:editable-query'); - -/** - * Editable Query, for the Refine Bar. Wrapper around a string with cleanup and validation. - */ -module.exports = Model.extend({ - props: { - rawString: { - type: 'string', - default: '{}', - required: true - }, - _queryObject: { - type: 'object', - default: null - } - }, - derived: { - cleanString: { - deps: ['rawString'], - fn: function() { - var output = this.rawString; - // accept whitespace-only input as empty query - if (_.trim(output) === '') { - output = '{}'; - } - // wrap field names in double quotes. I appologize for the next line of code. - // @see http://stackoverflow.com/questions/6462578/alternative-to-regex-match-all-instances-not-inside-quotes - // @see https://regex101.com/r/xM7iH6/1 - output = output.replace(/([{,])\s*([^,{\s\'"]+)\s*:(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, '$1"$2":'); - // replace multiple whitespace with single whitespace - output = output.replace(/\s+/g, ' '); - return output; - } - }, - // @todo - // displayString: { - // deps: ['cleanString'], - // fn: function() { - // // return the string without key quotes for display in RefineBarView - // } - // }, - /* eslint no-new: 0 */ - valid: { - deps: ['cleanString'], - fn: function() { - try { - // is it valid eJSON? - var parsed = EJSON.parse(this.cleanString); - // is it a valid parsable Query according to the language? - this._queryObject = new Query(parsed, { - parse: true - }); - } catch (e) { - this._queryObject = null; - return false; - } - return true; - } - }, - queryObject: { - deps: ['_queryObject'], - fn: function() { - return this._queryObject; - } - } - } -}); diff --git a/src/app/models/query-options.js b/src/app/models/query-options.js index 3a3f1e427f8..783045b2694 100644 --- a/src/app/models/query-options.js +++ b/src/app/models/query-options.js @@ -1,7 +1,6 @@ var ms = require('ms'); var Model = require('ampersand-model'); var EJSON = require('mongodb-extended-json'); -var Query = require('mongodb-language-model').Query; // var debug = require('debug')('mongodb-compass:models:query-options'); var DEFAULT_QUERY = {}; @@ -12,12 +11,6 @@ var DEFAULT_SIZE = 1000; var DEFAULT_SKIP = 0; var DEFAULT_MAX_TIME_MS = ms('10 seconds'); -var getDefaultQuery = function() { - return new Query(DEFAULT_QUERY, { - parse: true - }); -}; - /** * Options for reading a collection of documents from MongoDB. */ @@ -26,7 +19,7 @@ module.exports = Model.extend({ query: { type: 'state', default: function() { - return getDefaultQuery(); + return DEFAULT_QUERY; } }, sort: { @@ -53,18 +46,18 @@ module.exports = Model.extend({ deps: ['query'], cache: false, fn: function() { - return EJSON.stringify(this.query.serialize()); + return EJSON.stringify(this.query); } } }, serialize: function() { var res = Model.prototype.serialize.call(this); - res.query = this.query.serialize(); + res.query = this.query; return res; }, reset: function() { this.set({ - query: getDefaultQuery(), + query: DEFAULT_QUERY, sort: DEFAULT_SORT, size: DEFAULT_SIZE, skip: DEFAULT_SKIP, diff --git a/src/internal-packages/crud/lib/store/load-more-documents-store.js b/src/internal-packages/crud/lib/store/load-more-documents-store.js index 0a4f598b1c4..82b5338be18 100644 --- a/src/internal-packages/crud/lib/store/load-more-documents-store.js +++ b/src/internal-packages/crud/lib/store/load-more-documents-store.js @@ -27,7 +27,7 @@ const LoadMoreDocumentsStore = Reflux.createStore({ * @param {Integer} skip - The number of documents to skip. */ loadMoreDocuments: function(skip) { - const filter = app.queryOptions.query.serialize(); + const filter = app.queryOptions.query; const options = { skip: skip, limit: 20, sort: [[ '_id', 1 ]], readPreference: READ }; app.dataService.find(NamespaceStore.ns, filter, options, (error, documents) => { if (!error) { diff --git a/src/internal-packages/indexes/lib/component/create-index-text-field.jsx b/src/internal-packages/indexes/lib/component/create-index-text-field.jsx index 7fe5b9f5aa7..588dad92745 100644 --- a/src/internal-packages/indexes/lib/component/create-index-text-field.jsx +++ b/src/internal-packages/indexes/lib/component/create-index-text-field.jsx @@ -1,6 +1,6 @@ const React = require('react'); const Action = require('../action/index-actions'); -const Query = require('mongodb-language-model').Query; +const accepts = require('mongodb-language-model').accepts; const EJSON = require('mongodb-extended-json'); const _ = require('lodash'); @@ -69,20 +69,17 @@ class CreateIndexTextField extends React.Component { * @returns {Object|Boolean} false if invalid, otherwise the query */ _validateQueryString(queryString) { - let parsed; try { - // is it valid eJSON? const cleaned = this._cleanQueryString(queryString); - parsed = EJSON.parse(cleaned); - // is it a valid parsable Query according to the language? - /* eslint no-unused-vars: 0 */ - const query = new Query(parsed, { - parse: true - }); + // is it valid eJSON? + const parsed = EJSON.parse(cleaned); + // can it be serialized to JSON? + const stringified = JSON.stringify(parsed); + // is it a valid MongoDB query according to the language? + return accepts(stringified); } catch (e) { return false; } - return parsed; } /** diff --git a/src/internal-packages/query/lib/store/index.js b/src/internal-packages/query/lib/store/index.js index f5469866f0c..c74baa39ab9 100644 --- a/src/internal-packages/query/lib/store/index.js +++ b/src/internal-packages/query/lib/store/index.js @@ -4,7 +4,7 @@ const NamespaceStore = require('hadron-reflux-store').NamespaceStore; const StateMixin = require('reflux-state-mixin'); const QueryAction = require('../action'); const EJSON = require('mongodb-extended-json'); -const Query = require('mongodb-language-model').Query; +const accepts = require('mongodb-language-model').accepts; const _ = require('lodash'); const hasDistinctValue = require('../util').hasDistinctValue; const filterChanged = require('hadron-action').filterChanged; @@ -115,23 +115,24 @@ const QueryStore = Reflux.createStore({ * validates whether a string is a valid query. * * @param {Object} queryString a string to validate - * @return {Object|Boolean} false if invalid, otherwise the query + * @returns {Object|Boolean} false if invalid, otherwise the query */ _validateQueryString(queryString) { - let parsed; try { - // is it valid eJSON? const cleaned = this._cleanQueryString(queryString); - parsed = EJSON.parse(cleaned); - // is it a valid parsable Query according to the language? - /* eslint no-unused-vars: 0 */ - const query = new Query(parsed, { - parse: true - }); + debug('cleaned', cleaned); + // is it valid eJSON? + const parsed = EJSON.parse(cleaned); + debug('parsed', parsed); + debug('accepted', accepts(cleaned)); + // can it be serialized to JSON? + // const stringified = JSON.stringify(parsed); + // debug('stringified', stringified, accepts(stringified)); + // is it a valid MongoDB query according to the language? + return accepts(cleaned); } catch (e) { return false; } - return parsed; }, _validateFeatureFlag(queryString) { From 66c3e5f350bbd1df2640220118a27cdac2e7d154 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sun, 13 Nov 2016 18:16:46 +1100 Subject: [PATCH 2/7] fix bar selection rounding bug for negative, very large numbers and 0. --- src/internal-packages/query/lib/util/index.js | 18 +- test/query.ranges.test.js | 229 ++++++++++++++++++ 2 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 test/query.ranges.test.js diff --git a/src/internal-packages/query/lib/util/index.js b/src/internal-packages/query/lib/util/index.js index e9afc5bfa8f..010605af58f 100644 --- a/src/internal-packages/query/lib/util/index.js +++ b/src/internal-packages/query/lib/util/index.js @@ -1,11 +1,17 @@ const _ = require('lodash'); -// const debug = require('debug')('mongodb-compass:schema:test'); +// const debug = require('debug')('mongodb-compass:query:utils'); function bsonEqual(value, other) { const bsontype = _.get(value, '_bsontype', undefined); if (bsontype === 'ObjectID') { return value.equals(other); } + if (_.includes(['Decimal128', 'Long'], bsontype)) { + return value.toString() === other.toString(); + } + if (_.includes(['Int32', 'Double'], bsontype)) { + return value.value === other.value; + } // for all others, use native comparisons return undefined; } @@ -27,7 +33,9 @@ function hasDistinctValue(field, value) { if (_.has(field, '$in')) { // check if $in array contains the value const inArray = field.$in; - return (_.contains(inArray, value)); + return (_.some(inArray, (other) => { + return _.isEqual(value, other, bsonEqual); + })); } } // it is not a $in operator, check value directly @@ -129,10 +137,10 @@ function inValueRange(field, d) { */ const results = _.map(bounds, function(bound, i) { // adjust the upper bound slightly as it represents an exclusive bound - // getting this right would require a lot more code to check for all 4 - // edge cases. + // getting this perfectly right would require a lot more code to check for + // all 4 edge cases. if (i > 0) { - bound *= 0.999999; + bound -= (0.00001 * Math.abs(bound)) + 0.00001; } return _.every(_.map(conditions, function(cond) { return cond(bound); diff --git a/test/query.ranges.test.js b/test/query.ranges.test.js new file mode 100644 index 00000000000..0302f99880d --- /dev/null +++ b/test/query.ranges.test.js @@ -0,0 +1,229 @@ +/* eslint no-var: 0 */ +var inValueRange = require('../src/internal-packages/query/lib/util').inValueRange; +var assert = require('assert'); +var bson = require('bson'); + +describe('inValueRange', function() { + describe('equality queries', function() { + var query; + beforeEach(function() { + query = 15; + }); + it('should detect a match', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match', function() { + assert.equal(inValueRange(query, {value: 14, dx: 2}), 'partial'); + }); + it('should detect a miss', function() { + assert.equal(inValueRange(query, {value: 14.99, dx: 0}), 'no'); + assert.equal(inValueRange(query, {value: 15.01, dx: 0}), 'no'); + }); + }); + describe('closed ranges with $gte and $lt', function() { + var query; + beforeEach(function() { + query = {$gte: 15, $lt: 30}; + }); + it('should detect a match', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match at the lower bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); + }); + it('should detect a partial match across the upper bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 20}), 'partial'); + }); + it('should detect a partial match across the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 10}), 'partial'); + }); + it('should detect a miss exactly at the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a miss exactly at the upper bound', function() { + assert.equal(inValueRange(query, {value: 30, dx: 5}), 'no'); + }); + it('should detect a miss just below the lower bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 4.99}), 'no'); + }); + it('should detect edge case where range wraps around both bounds', function() { + assert.equal(inValueRange(query, {value: 0, dx: 100}), 'partial'); + }); + }); + + describe('closed ranges for negative values with $gte and $lt', function() { + var query; + beforeEach(function() { + query = {$gte: -30, $lt: -15}; + }); + it('should detect a match', function() { + assert.equal(inValueRange(query, {value: -20, dx: 5}), 'yes'); + }); + it('should detect a match at the lower bound', function() { + assert.equal(inValueRange(query, {value: -30, dx: 5}), 'yes'); + }); + it('should detect a partial match across the upper bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 20}), 'partial'); + }); + it('should detect a partial match across the lower bound', function() { + assert.equal(inValueRange(query, {value: -35, dx: 10}), 'partial'); + }); + it('should detect a miss exactly at the lower bound', function() { + assert.equal(inValueRange(query, {value: -35, dx: 5}), 'no'); + }); + it('should detect a miss exactly at the upper bound', function() { + assert.equal(inValueRange(query, {value: -15, dx: 5}), 'no'); + }); + it('should detect a miss just below the lower bound', function() { + assert.equal(inValueRange(query, {value: -35, dx: 4.99}), 'no'); + }); + it('should detect edge case where range wraps around both bounds', function() { + assert.equal(inValueRange(query, {value: -100, dx: 100}), 'partial'); + }); + }); + + + describe('open ranges with $gte', function() { + var query; + beforeEach(function() { + query = {$gte: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); + }); + it('should detect a match for a range, starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); + }); + it('should detect a miss for a range, ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a match for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a miss for a range below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $gt', function() { + var query; + beforeEach(function() { + query = {$gt: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range, starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); + }); + it('should detect a miss for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a miss for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); + }); + it('should detect a miss for a range below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value below the bound', function() { + assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $lte', function() { + var query; + beforeEach(function() { + query = {$lte: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); + }); + it('should detect a match for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a match for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); + }); + it('should detect a partial match for a range starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); + }); + it('should detect a miss for a range above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); + }); + }); + describe('open ranges with $lt', function() { + var query; + beforeEach(function() { + query = {$lt: 15}; + }); + it('should detect a match for a range', function() { + assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); + }); + it('should detect a match for single value', function() { + assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); + }); + it('should detect a miss for a range starting at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 5}), 'no'); + }); + it('should detect a miss for single value at the bound', function() { + assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); + }); + it('should detect a partial match for a range across the bound', function() { + assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); + }); + it('should detect a match for a range ending at the bound', function() { + assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); + }); + it('should detect a miss for a range above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); + }); + it('should detect a miss for a single value above the bound', function() { + assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); + }); + }); + + describe('non-numeric types', function() { + it('should work for dates', function() { + var query = {$gte: new Date('2011-01-01'), $lte: new Date('2013-01-01')}; + assert.equal(inValueRange(query, {value: new Date('2012-01-01')}), 'yes'); + assert.equal(inValueRange(query, {value: new Date('2015-01-01')}), 'no'); + }); + it('should work for objectids', function() { + var query = { + $gte: new bson.ObjectId('578cfb38d5021e616087f53f'), + $lte: new bson.ObjectId('578cfb42d5021e616087f541') + }; + assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb3ad5021e616087f540')}), 'yes'); + assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb6fd5021e616087f542')}), 'no'); + }); + }); + + describe('special edge cases', function() { + it('should detect a miss exactly at the lower bound for very large numbers', function() { + var query = {$gte: 10000000000, $lt: 10100000000}; + assert.equal(inValueRange(query, {value: 9900000000, dx: 100000000}), 'no'); + }); + }); +}); From d2d2d153dfc99ac20f5a479f496731ccfc61f52c Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sun, 13 Nov 2016 18:17:04 +1100 Subject: [PATCH 3/7] =?UTF-8?q?don=E2=80=99t=20promote=20values.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- .../lib/component/create-index-text-field.jsx | 2 +- .../query/lib/store/index.js | 5 +- .../query/test/index.test.js | 9 - .../query/test/ranges.test.js | 197 ------------------ .../schema/lib/component/minichart.jsx | 14 +- .../schema/lib/component/unique.jsx | 34 ++- src/internal-packages/schema/lib/helpers.js | 6 + .../schema/lib/store/index.jsx | 2 + 9 files changed, 50 insertions(+), 227 deletions(-) delete mode 100644 src/internal-packages/query/test/index.test.js delete mode 100644 src/internal-packages/query/test/ranges.test.js create mode 100644 src/internal-packages/schema/lib/helpers.js diff --git a/package.json b/package.json index 7071ae32fc0..5ba04df682c 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "d3-timer": "^1.0.3", "debug": "mongodb-js/debug#v2.2.3", "debug-menu": "^0.3.0", - "detect-coordinates": "^0.1.0", + "detect-coordinates": "^0.2.0", "electron-squirrel-startup": "^1.0.0", "font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.4.0.tar.gz", "get-object-path": "azer/get-object-path#74eb42de0cfd02c14ffdd18552f295aba723d394", @@ -125,15 +125,15 @@ "mongodb": "^2.2.8", "mongodb-collection-model": "^0.3.1", "mongodb-connection-model": "^6.3.1", - "mongodb-data-service": "^2.1.0", + "mongodb-data-service": "^2.1.1", "mongodb-database-model": "^0.1.2", "mongodb-explain-plan-model": "^0.2.2", "mongodb-extended-json": "^1.7.0", "mongodb-instance-model": "^3.3.0", "mongodb-js-metrics": "^1.5.2", - "mongodb-language-model": "^0.3.3", + "mongodb-language-model": "^1.0.0", "mongodb-ns": "^1.0.3", - "mongodb-schema": "^5.0.0", + "mongodb-schema": "^6.0.1", "mongodb-shell-to-url": "^0.1.0", "ms": "^0.7.1", "ncp": "^2.0.0", diff --git a/src/internal-packages/indexes/lib/component/create-index-text-field.jsx b/src/internal-packages/indexes/lib/component/create-index-text-field.jsx index 588dad92745..92f981aa998 100644 --- a/src/internal-packages/indexes/lib/component/create-index-text-field.jsx +++ b/src/internal-packages/indexes/lib/component/create-index-text-field.jsx @@ -104,7 +104,7 @@ class CreateIndexTextField extends React.Component { if (this.props.units) inputClassName += ' inline-option-field'; if (this.props.option === 'partialFilterExpression') { inputClassName += ' partial-filter-input'; - const valid = Boolean(this._validateQueryString(this.state.value)); + const valid = this._validateQueryString(this.state.value); if (!valid) groupClassName += ' has-error'; } return ( diff --git a/src/internal-packages/query/lib/store/index.js b/src/internal-packages/query/lib/store/index.js index c74baa39ab9..041ff059028 100644 --- a/src/internal-packages/query/lib/store/index.js +++ b/src/internal-packages/query/lib/store/index.js @@ -125,11 +125,8 @@ const QueryStore = Reflux.createStore({ const parsed = EJSON.parse(cleaned); debug('parsed', parsed); debug('accepted', accepts(cleaned)); - // can it be serialized to JSON? - // const stringified = JSON.stringify(parsed); - // debug('stringified', stringified, accepts(stringified)); // is it a valid MongoDB query according to the language? - return accepts(cleaned); + return accepts(cleaned) ? parsed : false; } catch (e) { return false; } diff --git a/src/internal-packages/query/test/index.test.js b/src/internal-packages/query/test/index.test.js deleted file mode 100644 index 8e581382891..00000000000 --- a/src/internal-packages/query/test/index.test.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint no-var: 0 */ -var QueryBar = require('../lib/component'); -var assert = require('assert'); - -describe('QueryBar', function() { - it('should work', function() { - assert.ok(QueryBar); - }); -}); diff --git a/src/internal-packages/query/test/ranges.test.js b/src/internal-packages/query/test/ranges.test.js deleted file mode 100644 index d1b222c63a9..00000000000 --- a/src/internal-packages/query/test/ranges.test.js +++ /dev/null @@ -1,197 +0,0 @@ -/* eslint no-var: 0 */ -var inValueRange = require('../lib/util').inValueRange; -var assert = require('assert'); -var bson = require('bson'); - -describe('inValueRange', function() { - describe('equality queries', function() { - var query; - beforeEach(function() { - query = 15; - }); - it('should detect a match', function() { - assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); - }); - it('should detect a partial match', function() { - assert.equal(inValueRange(query, {value: 14, dx: 2}), 'partial'); - }); - it('should detect a miss', function() { - assert.equal(inValueRange(query, {value: 14.99, dx: 0}), 'no'); - assert.equal(inValueRange(query, {value: 15.01, dx: 0}), 'no'); - }); - }); - describe('closed ranges with $gte and $lt', function() { - var query; - beforeEach(function() { - query = {$gte: 15, $lt: 30}; - }); - it('should detect a match', function() { - assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); - }); - it('should detect a match at the lower bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); - }); - it('should detect a partial match across the upper bound', function() { - assert.equal(inValueRange(query, {value: 20, dx: 20}), 'partial'); - }); - it('should detect a partial match across the lower bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 10}), 'partial'); - }); - it('should detect a miss exactly at the lower bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); - }); - it('should detect a miss exactly at the upper bound', function() { - assert.equal(inValueRange(query, {value: 30, dx: 5}), 'no'); - }); - it('should detect a miss just below the lower bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 4.99}), 'no'); - }); - it('should detect edge case where range wraps around both bounds', function() { - assert.equal(inValueRange(query, {value: 0, dx: 100}), 'partial'); - }); - }); - - describe('open ranges with $gte', function() { - var query; - beforeEach(function() { - query = {$gte: 15}; - }); - it('should detect a match for a range', function() { - assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); - }); - it('should detect a match for single value', function() { - assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); - }); - it('should detect a match for a range, starting at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 5}), 'yes'); - }); - it('should detect a miss for a range, ending at the bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); - }); - it('should detect a match for single value at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); - }); - it('should detect a partial match for a range across the bound', function() { - assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); - }); - it('should detect a miss for a range below the bound', function() { - assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); - }); - it('should detect a miss for a single value below the bound', function() { - assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); - }); - }); - describe('open ranges with $gt', function() { - var query; - beforeEach(function() { - query = {$gt: 15}; - }); - it('should detect a match for a range', function() { - assert.equal(inValueRange(query, {value: 20, dx: 5}), 'yes'); - }); - it('should detect a match for single value', function() { - assert.equal(inValueRange(query, {value: 20, dx: 0}), 'yes'); - }); - it('should detect a partial match for a range, starting at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); - }); - it('should detect a miss for single value at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); - }); - it('should detect a partial match for a range across the bound', function() { - assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); - }); - it('should detect a miss for a range ending at the bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 5}), 'no'); - }); - it('should detect a miss for a range below the bound', function() { - assert.equal(inValueRange(query, {value: -20, dx: 5}), 'no'); - }); - it('should detect a miss for a single value below the bound', function() { - assert.equal(inValueRange(query, {value: -20, dx: 0}), 'no'); - }); - }); - describe('open ranges with $lte', function() { - var query; - beforeEach(function() { - query = {$lte: 15}; - }); - it('should detect a match for a range', function() { - assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); - }); - it('should detect a match for single value', function() { - assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); - }); - it('should detect a match for single value at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 0}), 'yes'); - }); - it('should detect a partial match for a range across the bound', function() { - assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); - }); - it('should detect a match for a range ending at the bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); - }); - it('should detect a partial match for a range starting at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 5}), 'partial'); - }); - it('should detect a miss for a range above the bound', function() { - assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); - }); - it('should detect a miss for a single value above the bound', function() { - assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); - }); - }); - describe('open ranges with $lt', function() { - var query; - beforeEach(function() { - query = {$lt: 15}; - }); - it('should detect a match for a range', function() { - assert.equal(inValueRange(query, {value: 5, dx: 5}), 'yes'); - }); - it('should detect a match for single value', function() { - assert.equal(inValueRange(query, {value: 10, dx: 0}), 'yes'); - }); - it('should detect a miss for a range starting at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 5}), 'no'); - }); - it('should detect a miss for single value at the bound', function() { - assert.equal(inValueRange(query, {value: 15, dx: 0}), 'no'); - }); - it('should detect a partial match for a range across the bound', function() { - assert.equal(inValueRange(query, {value: 12, dx: 5}), 'partial'); - }); - it('should detect a match for a range ending at the bound', function() { - assert.equal(inValueRange(query, {value: 10, dx: 5}), 'yes'); - }); - it('should detect a miss for a range above the bound', function() { - assert.equal(inValueRange(query, {value: 20, dx: 5}), 'no'); - }); - it('should detect a miss for a single value above the bound', function() { - assert.equal(inValueRange(query, {value: 20, dx: 0}), 'no'); - }); - }); - - describe('non-numeric types', function() { - it('should work for dates', function() { - var query = {$gte: new Date('2011-01-01'), $lte: new Date('2013-01-01')}; - assert.equal(inValueRange(query, {value: new Date('2012-01-01')}), 'yes'); - assert.equal(inValueRange(query, {value: new Date('2015-01-01')}), 'no'); - }); - it('should work for objectids', function() { - var query = { - $gte: new bson.ObjectId('578cfb38d5021e616087f53f'), - $lte: new bson.ObjectId('578cfb42d5021e616087f541') - }; - assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb3ad5021e616087f540')}), 'yes'); - assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb6fd5021e616087f542')}), 'no'); - }); - }); - - describe('special edge cases', function() { - it('should detect a miss exactly at the lower bound for very large numbers', function() { - var query = {$gte: 10000000000, $lt: 10100000000}; - assert.equal(inValueRange(query, {value: 9900000000, dx: 100000000}), 'no'); - }); - }); -}); diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx index ab0b7704494..dddf4b8ae1f 100644 --- a/src/internal-packages/schema/lib/component/minichart.jsx +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -8,9 +8,7 @@ const D3Component = require('./d3component'); const vizFns = require('../d3'); const Actions = require('../action'); -const STRING = 'String'; -const NUMBER = 'Number'; -const DECIMAL_128 = 'Decimal128'; +const { STRING, DECIMAL_128, DOUBLE, LONG, INT_32 } = require('../helpers'); const Minichart = React.createClass({ @@ -72,15 +70,17 @@ const Minichart = React.createClass({ }, minichartFactory() { - /* eslint camelcase: 0 */ - const typeName = this.props.type.name; + // cast all numeric types to Number minichart + const typeName = _.includes([ DECIMAL_128, DOUBLE, INT_32 ], + this.props.type.name) ? 'Number' : this.props.type.name; + const fieldName = this.props.fieldName; const queryClause = this.state.query[fieldName]; - const has_duplicates = this.props.type.has_duplicates; + const hasDuplicates = this.props.type.has_duplicates; const fn = vizFns[typeName.toLowerCase()]; const width = this.state.containerWidth; - if (_.includes([ STRING, NUMBER, DECIMAL_128 ], typeName) && !has_duplicates) { + if (_.includes([ STRING, LONG ], typeName) && !hasDuplicates) { return ( - {value.toString()} + {value} ); @@ -73,10 +97,10 @@ const UniqueMinichart = React.createClass({ const sample = this.state.sample || []; const fieldName = this.props.fieldName.toLowerCase(); const typeName = this.props.type.name.toLowerCase(); - const randomValueList = sample.map((value) => { + const randomValueList = sample.map((value, i) => { return ( Date: Sun, 13 Nov 2016 18:46:24 +1100 Subject: [PATCH 4/7] allow dotted field names and fix type switching bug --- package.json | 2 +- src/internal-packages/schema/lib/component/minichart.jsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ba04df682c..b5a9fd22926 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "mongodb-extended-json": "^1.7.0", "mongodb-instance-model": "^3.3.0", "mongodb-js-metrics": "^1.5.2", - "mongodb-language-model": "^1.0.0", + "mongodb-language-model": "^1.0.1", "mongodb-ns": "^1.0.3", "mongodb-schema": "^6.0.1", "mongodb-shell-to-url": "^0.1.0", diff --git a/src/internal-packages/schema/lib/component/minichart.jsx b/src/internal-packages/schema/lib/component/minichart.jsx index dddf4b8ae1f..173f9754a3e 100644 --- a/src/internal-packages/schema/lib/component/minichart.jsx +++ b/src/internal-packages/schema/lib/component/minichart.jsx @@ -83,6 +83,7 @@ const Minichart = React.createClass({ if (_.includes([ STRING, LONG ], typeName) && !hasDuplicates) { return ( Date: Sun, 13 Nov 2016 18:58:27 +1100 Subject: [PATCH 5/7] remove debug statements. --- src/internal-packages/query/lib/store/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/internal-packages/query/lib/store/index.js b/src/internal-packages/query/lib/store/index.js index 041ff059028..a4af0bdcb67 100644 --- a/src/internal-packages/query/lib/store/index.js +++ b/src/internal-packages/query/lib/store/index.js @@ -120,11 +120,8 @@ const QueryStore = Reflux.createStore({ _validateQueryString(queryString) { try { const cleaned = this._cleanQueryString(queryString); - debug('cleaned', cleaned); // is it valid eJSON? const parsed = EJSON.parse(cleaned); - debug('parsed', parsed); - debug('accepted', accepts(cleaned)); // is it a valid MongoDB query according to the language? return accepts(cleaned) ? parsed : false; } catch (e) { From 575bd978379c1d8c312621b72ff98777b133f5eb Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sun, 13 Nov 2016 21:47:58 +1100 Subject: [PATCH 6/7] handle number ranges correctly for unpromoted bson types --- package.json | 4 +-- src/internal-packages/query/lib/util/index.js | 6 +++-- src/internal-packages/schema/lib/d3/many.js | 4 +-- src/internal-packages/schema/lib/d3/number.js | 25 ++++++++++++++++-- test/query.ranges.test.js | 26 +++++++++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b5a9fd22926..9b820fe8997 100644 --- a/package.json +++ b/package.json @@ -128,12 +128,12 @@ "mongodb-data-service": "^2.1.1", "mongodb-database-model": "^0.1.2", "mongodb-explain-plan-model": "^0.2.2", - "mongodb-extended-json": "^1.7.0", + "mongodb-extended-json": "^1.8.0", "mongodb-instance-model": "^3.3.0", "mongodb-js-metrics": "^1.5.2", "mongodb-language-model": "^1.0.1", "mongodb-ns": "^1.0.3", - "mongodb-schema": "^6.0.1", + "mongodb-schema": "^6.0.2", "mongodb-shell-to-url": "^0.1.0", "ms": "^0.7.1", "ncp": "^2.0.0", diff --git a/src/internal-packages/query/lib/util/index.js b/src/internal-packages/query/lib/util/index.js index 010605af58f..f0116bf8a52 100644 --- a/src/internal-packages/query/lib/util/index.js +++ b/src/internal-packages/query/lib/util/index.js @@ -119,8 +119,10 @@ function inValueRange(field, d) { } const dx = _.get(d, 'dx', null); - // extract bound(s) - const bounds = dx === null ? [d.value] : _.uniq([d.value, d.value + dx]); + // extract bound(s): if a dx value is set, create a 2-element array of + // upper and lower bound. otherwise create a single value array of the + // original bson type (if available) or the extracted value. + const bounds = dx ? _.uniq([d.value, d.value + dx]) : [d.bson || d.value]; /* * Logic to determine if the query covers the value (or value range) diff --git a/src/internal-packages/schema/lib/d3/many.js b/src/internal-packages/schema/lib/d3/many.js index bc848764934..030c1f671df 100644 --- a/src/internal-packages/schema/lib/d3/many.js +++ b/src/internal-packages/schema/lib/d3/many.js @@ -94,7 +94,7 @@ const minicharts_d3fns_many = function() { // if not binned and values are the same, single equality query QueryAction.setValue({ field: options.fieldName, - value: minValue.value + value: minValue.bson }); return; } @@ -172,7 +172,7 @@ const minicharts_d3fns_many = function() { // bars don't represent bins, build single value query QueryAction.setValue({ field: options.fieldName, - value: d.value, + value: d.bson, unsetIfSet: true }); } diff --git a/src/internal-packages/schema/lib/d3/number.js b/src/internal-packages/schema/lib/d3/number.js index 254660b4faf..d992695386c 100644 --- a/src/internal-packages/schema/lib/d3/number.js +++ b/src/internal-packages/schema/lib/d3/number.js @@ -5,6 +5,25 @@ const many = require('./many'); const shared = require('./shared'); // const debug = require('debug')('mongodb-compass:minicharts:number'); +/** +* extracts a Javascript number from a BSON type. +* +* @param {Any} value value to be converted to a number +* @return {Number} converted value +*/ +function extractNumericValueFromBSON(value) { + if (_.has(value, '_bsontype')) { + if (_.includes([ 'Decimal128', 'Long' ], value._bsontype)) { + return parseFloat(value.toString(), 10); + } + if (_.includes([ 'Double', 'Int32' ], value._bsontype)) { + return value.value; + } + } + // unknown value, leave as is. + return value; +} + const minicharts_d3fns_number = function() { let width = 400; let height = 100; @@ -26,7 +45,7 @@ const minicharts_d3fns_number = function() { if (options.unique < 20) { grouped = _(data) .groupBy(function(d) { - return d; + return extractNumericValueFromBSON(d); }) .map(function(v, k) { v.label = k; @@ -34,6 +53,7 @@ const minicharts_d3fns_number = function() { v.value = v.x; v.dx = 0; v.count = v.length; + v.bson = v[0]; // original BSON type return v; }) .sortBy(function(v) { @@ -49,7 +69,8 @@ const minicharts_d3fns_number = function() { // Generate a histogram using approx. twenty uniformly-spaced bins const ticks = xBinning.ticks(20); const hist = d3.layout.histogram() - .bins(ticks); + .bins(ticks) + .value(extractNumericValueFromBSON); grouped = hist(data); diff --git a/test/query.ranges.test.js b/test/query.ranges.test.js index 0302f99880d..bb4003261b6 100644 --- a/test/query.ranges.test.js +++ b/test/query.ranges.test.js @@ -218,6 +218,32 @@ describe('inValueRange', function() { assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb3ad5021e616087f540')}), 'yes'); assert.equal(inValueRange(query, {value: new bson.ObjectId('578cfb6fd5021e616087f542')}), 'no'); }); + it('should work for $numberDecimal', function() { + var query = { + $gte: bson.Decimal128.fromString('1.5'), + $lte: bson.Decimal128.fromString('2.5') + }; + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.8')}), 'yes'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('4.4')}), 'no'); + }); + it('should work for $numberDecimal with a dx of 0', function() { + var query = { + $gte: bson.Decimal128.fromString('1.5'), + $lte: bson.Decimal128.fromString('2.5') + }; + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.8'), dx: 0}), 'yes'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('4.4'), dx: 0}), 'no'); + }); + it('should work for $numberDecimal with a single equality query', function() { + var query = bson.Decimal128.fromString('1.5'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.5')} ), 'yes'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.6')} ), 'no'); + }); + it('should work for $numberDecimal with a single equality query and dx of 0', function() { + var query = bson.Decimal128.fromString('1.5'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.5'), dx: 0} ), 'yes'); + assert.equal(inValueRange(query, {value: bson.Decimal128.fromString('1.6'), dx: 0} ), 'no'); + }); }); describe('special edge cases', function() { From 13e315214b9231ded3fc256f084d06d70d486b0c Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Sun, 13 Nov 2016 22:54:39 +1100 Subject: [PATCH 7/7] fix query building for negative long values. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b820fe8997..1f04c659948 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "mongodb-extended-json": "^1.8.0", "mongodb-instance-model": "^3.3.0", "mongodb-js-metrics": "^1.5.2", - "mongodb-language-model": "^1.0.1", + "mongodb-language-model": "^1.0.2", "mongodb-ns": "^1.0.3", "mongodb-schema": "^6.0.2", "mongodb-shell-to-url": "^0.1.0",