From 199210eb68a24950467099080789473fb740431b Mon Sep 17 00:00:00 2001 From: Ashwin Phatak Date: Fri, 29 Sep 2017 16:53:37 +0530 Subject: [PATCH] Add support for aggregates on collections (#807) (#1350) --- CHANGELOG.md | 2 +- docs/collection.js | 36 +++++++ lib/browser/lists.js | 4 + lib/browser/results.js | 4 + lib/index.d.ts | 28 ++++++ src/js_list.hpp | 31 ++++++ src/js_results.hpp | 32 +++++++ src/js_types.hpp | 36 +++++++ src/js_util.hpp | 48 ++++++++++ tests/js/list-tests.js | 195 ++++++++++++++++++++++++++++++++++++++ tests/js/results-tests.js | 150 +++++++++++++++++++++++++++++ 11 files changed, 565 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b234a1ea..1db7a19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ X.Y.Z Release notes * None ### Enhancements -* None +* Added aggregate functions `min()`, `max()`, `sum()`, and `avg()` to `Realm.Results` and `Realm.List` (#807). ### Bug fixes * Fixed port conflict between RN >= 0.48 inspector proxy and RPC server used for Chrome debugging (#1294). diff --git a/docs/collection.js b/docs/collection.js index 5cb8852e..02bbd12b 100644 --- a/docs/collection.js +++ b/docs/collection.js @@ -174,6 +174,42 @@ class Collection { */ indexOf(object) {} + /** + * Computes the minimum value of a property. + * @param {string} property - The name of the property. + * @throws {Error} If no property with the name exists or if property is not numeric/date. + * @returns {number} the minimum value. + * @since 1.12.1 + */ + min(property) {} + + /** + * Computes the maximum value of a property. + * @param {string} property - The name of the property. + * @throws {Error} If no property with the name exists or if property is not numeric/date. + * @returns {number} the maximum value. + * @since 1.12.1 + */ + max(property) {} + + /** + * Computes the sum of a property. + * @param {string} property - The name of the property. + * @throws {Error} If no property with the name exists or if property is not numeric/date. + * @returns {number} the sum. + * @since 1.12.1 + */ + sum(property) {} + + /** + * Computes the average of a property. + * @param {string} property - The name of the property. + * @throws {Error} If no property with the name exists or if property is not numeric/date. + * @returns {number} the average value. + * @since 1.12.1 + */ + avg(property) {} + /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach Array.prototype.forEach} * @param {function} callback - Function to execute on each object in the collection. diff --git a/lib/browser/lists.js b/lib/browser/lists.js index 3b67de52..5e803d5a 100644 --- a/lib/browser/lists.js +++ b/lib/browser/lists.js @@ -32,6 +32,10 @@ createMethods(List.prototype, objectTypes.LIST, [ 'snapshot', 'isValid', 'indexOf', + 'min', + 'max', + 'sum', + 'avg', 'addListener', 'removeListener', 'removeAllListeners', diff --git a/lib/browser/results.js b/lib/browser/results.js index 72abba67..efb0306e 100644 --- a/lib/browser/results.js +++ b/lib/browser/results.js @@ -31,6 +31,10 @@ createMethods(Results.prototype, objectTypes.RESULTS, [ 'snapshot', 'isValid', 'indexOf', + 'min', + 'max', + 'sum', + 'avg', 'addListener', 'removeListener', 'removeAllListeners', diff --git a/lib/index.d.ts b/lib/index.d.ts index 941fe647..dfc06f31 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -140,6 +140,34 @@ declare namespace Realm { */ isValid(): boolean; + /** + * Computes the minimum value. + * @param {string} property + * @returns number + */ + min(property: string): number; + + /** + * Computes the maximum value. + * @param {string} property + * @returns number + */ + max(property: string): number; + + /** + * Computes the sum. + * @param {string} property + * @returns number + */ + sum(property: string): number; + + /** + * Computes the average. + * @param {string} property + * @returns number + */ + avg(property: string): number; + /** * @param {string} query * @param {any[]} ...arg diff --git a/src/js_list.hpp b/src/js_list.hpp index 6b201cf4..4931433b 100644 --- a/src/js_list.hpp +++ b/src/js_list.hpp @@ -47,6 +47,7 @@ class List : public realm::List { template struct ListClass : ClassDefinition, CollectionClass> { + using Type = T; using ContextType = typename T::Context; using ObjectType = typename T::Object; using ValueType = typename T::Value; @@ -74,6 +75,12 @@ struct ListClass : ClassDefinition, CollectionClass> { static void is_valid(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void index_of(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // aggregate functions + static void min(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void max(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void sum(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void avg(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // observable static void add_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); @@ -92,6 +99,10 @@ struct ListClass : ClassDefinition, CollectionClass> { {"sorted", wrap}, {"isValid", wrap}, {"indexOf", wrap}, + {"min", wrap}, + {"max", wrap}, + {"sum", wrap}, + {"avg", wrap}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, @@ -115,6 +126,26 @@ void ListClass::get_length(ContextType, ObjectType object, ReturnValue &retur return_value.set((uint32_t)list->size()); } +template +void ListClass::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value); +} + +template +void ListClass::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value); +} + +template +void ListClass::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value); +} + +template +void ListClass::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value); +} + template void ListClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { auto list = get_internal>(object); diff --git a/src/js_results.hpp b/src/js_results.hpp index 5f64cd74..b54b4e7f 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -20,6 +20,7 @@ #include "js_collection.hpp" #include "js_realm_object.hpp" +#include "js_util.hpp" #include "results.hpp" #include "list.hpp" @@ -49,6 +50,7 @@ class Results : public realm::Results { template struct ResultsClass : ClassDefinition, CollectionClass> { + using Type = T; using ContextType = typename T::Context; using ObjectType = typename T::Object; using ValueType = typename T::Value; @@ -76,6 +78,12 @@ struct ResultsClass : ClassDefinition, CollectionClass< static void index_of(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // aggregate functions + static void min(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void max(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void sum(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void avg(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // observable static void add_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); @@ -88,6 +96,10 @@ struct ResultsClass : ClassDefinition, CollectionClass< {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, + {"min", wrap}, + {"max", wrap}, + {"sum", wrap}, + {"avg", wrap}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, @@ -195,6 +207,26 @@ void ResultsClass::get_length(ContextType ctx, ObjectType object, ReturnValue return_value.set((uint32_t)results->size()); } +template +void ResultsClass::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value); +} + +template +void ResultsClass::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value); +} + +template +void ResultsClass::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value); +} + +template +void ResultsClass::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + compute_aggregate_on_collection>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value); +} + template void ResultsClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { auto results = get_internal>(object); diff --git a/src/js_types.hpp b/src/js_types.hpp index 1e32c2b6..a91d9dda 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #if defined(__GNUC__) && !(defined(DEBUG) && DEBUG) # define REALM_JS_INLINE inline __attribute__((always_inline)) @@ -124,6 +126,8 @@ struct Value { static ValueType from_string(ContextType, const String &); static ValueType from_binary(ContextType, BinaryData); static ValueType from_undefined(ContextType); + static ValueType from_timestamp(ContextType, Timestamp); + static ValueType from_mixed(ContextType, util::Optional &); static ObjectType to_array(ContextType, const ValueType &); static bool to_boolean(ContextType, const ValueType &); @@ -424,5 +428,37 @@ inline std::string js_type_name_for_property_type(PropertyType type) return ""; } +template +inline typename T::Value Value::from_timestamp(typename T::Context ctx, Timestamp ts) { + return Object::create_date(ctx, ts.get_seconds() * 1000 + ts.get_nanoseconds() / 1000000); +} + +template +inline typename T::Value Value::from_mixed(typename T::Context ctx, util::Optional& mixed) { + if (!mixed) { + return from_undefined(ctx); + } + + Mixed value = *mixed; + switch (value.get_type()) { + case type_Bool: + return from_boolean(ctx, value.get_bool()); + case type_Int: + return from_number(ctx, static_cast(value.get_int())); + case type_Float: + return from_number(ctx, value.get_float()); + case type_Double: + return from_number(ctx, value.get_double()); + case type_Timestamp: + return from_timestamp(ctx, value.get_timestamp()); + case type_String: + return from_string(ctx, value.get_string().data()); + case type_Binary: + return from_binary(ctx, value.get_binary()); + default: + throw std::invalid_argument("Value not convertible."); + } +} + } // js } // realm diff --git a/src/js_util.hpp b/src/js_util.hpp index 07ba6e30..d2b2bdce 100644 --- a/src/js_util.hpp +++ b/src/js_util.hpp @@ -22,11 +22,19 @@ #include #include +#include "object_schema.hpp" #include "shared_realm.hpp" namespace realm { namespace js { +enum class AggregateFunc { + Min, + Max, + Sum, + Avg +}; + template class RealmDelegate; @@ -75,5 +83,45 @@ static inline void validate_argument_count_at_least(size_t count, size_t expecte } } +template +static inline void compute_aggregate_on_collection(AggregateFunc func, typename T::ContextType ctx, typename T::ObjectType this_object, size_t argc, const typename T::ValueType arguments[], typename T::ReturnValue &return_value) { + validate_argument_count(argc, 1); + std::string property_name = T::Value::validated_to_string(ctx, arguments[0]); + + auto list = get_internal(this_object); + + const ObjectSchema& object_schema = list->get_object_schema(); + const Property* property = object_schema.property_for_name(property_name); + if (!property) { + throw std::invalid_argument(util::format("No such property: %1", property_name)); + } + + util::Optional mixed; + switch (func) { + case AggregateFunc::Min: { + mixed = list->min(property->table_column); + break; + } + case AggregateFunc::Max: { + mixed = list->max(property->table_column); + break; + } + case AggregateFunc::Sum: { + mixed = list->sum(property->table_column); + break; + } + case AggregateFunc::Avg: { + mixed = list->average(property->table_column); + break; + } + default: { + REALM_ASSERT(false && "Unknown aggregate function"); + REALM_UNREACHABLE(); + } + } + + return_value.set(T::Value::from_mixed(ctx, mixed)); +} + } // js } // realm diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index 8fc71910..b589d9ed 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -649,4 +649,199 @@ module.exports = { list.length; }); }, + + testListAggregateFunctions: function() { + const NullableBasicTypesList = { + name: 'NullableBasicTypesList', + properties: { + list: {type: 'list', objectType: 'NullableBasicTypesObject'}, + } + }; + + var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); + var object; + + const N = 50; + + var list = []; + for(var i = 0; i < N; i++) { + list.push({ + intCol: i+1, + floatCol: i+1, + doubleCol: i+1, + dateCol: new Date(i+1) + }); + } + + realm.write(() => { + object = realm.create('NullableBasicTypesList', {list: list}); + }); + + TestCase.assertEqual(object.list.length, N); + + // int, float & double columns support all aggregate functions + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertEqual(object.list.min(colName), 1); + TestCase.assertEqual(object.list.max(colName), N); + TestCase.assertEqual(object.list.sum(colName), N*(N+1)/2); + TestCase.assertEqual(object.list.avg(colName), (N+1)/2); + }); + + // date columns support only 'min' & 'max' + TestCase.assertEqual(object.list.min('dateCol').getTime(), new Date(1).getTime()); + TestCase.assertEqual(object.list.max('dateCol').getTime(), new Date(N).getTime()); + }, + + testListAggregateFunctionsWithNullColumnValues: function() { + const NullableBasicTypesList = { + name: 'NullableBasicTypesList', + properties: { + list: {type: 'list', objectType: 'NullableBasicTypesObject'}, + } + }; + + var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); + var object; + var objectEmptyList; + + const N = 50; + const M = 10; + + var list = []; + for(var i = 0; i < N; i++) { + list.push({ + intCol: i+1, + floatCol: i+1, + doubleCol: i+1, + dateCol: new Date(i+1) + }); + } + + for(var j = 0; j < M; j++) { + list.push({ + intCol: null, + floatCol: null, + doubleCol: null, + dateCol: null + }); + } + + realm.write(() => { + object = realm.create('NullableBasicTypesList', {list: list}); + objectEmptyList = realm.create('NullableBasicTypesList', {list: []}); + }); + + TestCase.assertEqual(object.list.length, N + M); + + + // int, float & double columns support all aggregate functions + // the M null valued objects should be ignored + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertEqual(object.list.min(colName), 1); + TestCase.assertEqual(object.list.max(colName), N); + TestCase.assertEqual(object.list.sum(colName), N*(N+1)/2); + TestCase.assertEqual(object.list.avg(colName), (N+1)/2); + }); + + // date columns support only 'min' & 'max' + TestCase.assertEqual(object.list.min('dateCol').getTime(), new Date(1).getTime()); + TestCase.assertEqual(object.list.max('dateCol').getTime(), new Date(N).getTime()); + + // call aggregate functions on empty list + TestCase.assertEqual(objectEmptyList.list.length, 0); + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertUndefined(objectEmptyList.list.min(colName)); + TestCase.assertUndefined(objectEmptyList.list.max(colName)); + TestCase.assertEqual(objectEmptyList.list.sum(colName), 0); + TestCase.assertUndefined(objectEmptyList.list.avg(colName)); + }); + + TestCase.assertUndefined(objectEmptyList.list.min('dateCol')); + TestCase.assertUndefined(objectEmptyList.list.max('dateCol')); + }, + + testListAggregateFunctionsUnsupported: function() { + const NullableBasicTypesList = { + name: 'NullableBasicTypesList', + properties: { + list: {type: 'list', objectType: 'NullableBasicTypesObject'}, + } + }; + + var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); + var object; + + const N = 5; + + var list = []; + for(var i = 0; i < N; i++) { + list.push({ + intCol: i+1, + floatCol: i+1, + doubleCol: i+1, + dateCol: new Date(i+1) + }); + } + + realm.write(() => { + object = realm.create('NullableBasicTypesList', {list: list}); + }); + + TestCase.assertEqual(object.list.length, N); + + // bool, string & data columns don't support 'min' + ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + object.list.min(colName); + } + )}); + + // bool, string & data columns don't support 'max' + ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + object.list.max(colName); + } + )}); + + // bool, string, date & data columns don't support 'avg' + ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + object.list.avg(colName); + } + )}); + + // bool, string, date & data columns don't support 'sum' + ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + object.list.sum(colName); + } + )}); + }, + + testListAggregateFunctionsWrongProperty: function() { + var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); + var object; + var list; + realm.write(() => { + object = realm.create('PersonList', {list: [ + {name: 'Ari', age: 10}, + {name: 'Tim', age: 11}, + {name: 'Bjarne', age: 12}, + ]}); + }); + + TestCase.assertThrows(function() { + object.list.min('foo') + }); + TestCase.assertThrows(function() { + object.list.max('foo') + }); + TestCase.assertThrows(function() { + object.list.sum('foo') + }); + TestCase.assertThrows(function() { + object.list.avg('foo') + }); + + }, }; diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index 99ea0353..87024316 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -455,6 +455,156 @@ module.exports = { }); resolve = r; }); + }) + }, + + testResultsAggregateFunctions: function() { + var realm = new Realm({ schema: [schemas.NullableBasicTypes] }); + const N = 50; + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + intCol: i+1, + floatCol: i+1, + doubleCol: i+1, + dateCol: new Date(i+1) + }); + } }); + + var results = realm.objects('NullableBasicTypesObject'); + TestCase.assertEqual(results.length, N); + + // int, float & double columns support all aggregate functions + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertEqual(results.min(colName), 1); + TestCase.assertEqual(results.max(colName), N); + TestCase.assertEqual(results.sum(colName), N*(N+1)/2); + TestCase.assertEqual(results.avg(colName), (N+1)/2); + }); + + // date columns support only 'min' & 'max' + TestCase.assertEqual(results.min('dateCol').getTime(), new Date(1).getTime()); + TestCase.assertEqual(results.max('dateCol').getTime(), new Date(N).getTime()); + }, + + testResultsAggregateFunctionsWithNullColumnValues: function() { + var realm = new Realm({ schema: [schemas.NullableBasicTypes] }); + + const N = 50; + const M = 10; + + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + intCol: i+1, + floatCol: i+1, + doubleCol: i+1, + dateCol: new Date(i+1) + }); + } + + // add some null valued data, which should be ignored by the aggregate functions + for(var j = 0; j < M; j++) { + realm.create('NullableBasicTypesObject', { + intCol: null, + floatCol: null, + doubleCol: null, + dateCol: null + }); + } + }); + + var results = realm.objects('NullableBasicTypesObject'); + + TestCase.assertEqual(results.length, N + M); + + // int, float & double columns support all aggregate functions + // the M null valued objects should be ignored + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertEqual(results.min(colName), 1); + TestCase.assertEqual(results.max(colName), N); + TestCase.assertEqual(results.sum(colName), N*(N+1)/2); + TestCase.assertEqual(results.avg(colName), (N+1)/2); + }); + + // date columns support only 'min' & 'max' + TestCase.assertEqual(results.min('dateCol').getTime(), new Date(1).getTime()); + TestCase.assertEqual(results.max('dateCol').getTime(), new Date(N).getTime()); + + // call aggregate functions on empty results + var emptyResults = realm.objects('NullableBasicTypesObject').filtered('intCol < 0'); + TestCase.assertEqual(emptyResults.length, 0); + ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { + TestCase.assertUndefined(emptyResults.min(colName)); + TestCase.assertUndefined(emptyResults.max(colName)); + TestCase.assertEqual(emptyResults.sum(colName), 0); + TestCase.assertUndefined(emptyResults.avg(colName)); + }); + + TestCase.assertUndefined(emptyResults.min('dateCol')); + TestCase.assertUndefined(emptyResults.max('dateCol')); + }, + + testResultsAggregateFunctionsUnsupported: function() { + var realm = new Realm({ schema: [schemas.NullableBasicTypes] }); + realm.write(() => { + realm.create('NullableBasicTypesObject', { + boolCol: true, + stringCol: "hello", + dataCol: new ArrayBuffer(12), + }); + }); + + var results = realm.objects('NullableBasicTypesObject'); + + // bool, string & data columns don't support 'min' + ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + results.min(colName); + } + )}); + + // bool, string & data columns don't support 'max' + ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + results.max(colName); + } + )}); + + // bool, string, date & data columns don't support 'avg' + ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + results.avg(colName); + } + )}); + + // bool, string, date & data columns don't support 'sum' + ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { + TestCase.assertThrows(function() { + results.sum(colName); + } + )}); + }, + + testResultsAggregateFunctionsWrongProperty: function() { + var realm = new Realm({ schema: [ schemas.TestObject ]}); + realm.write(() => { + realm.create('TestObject', { doubleCol: 42 }); + }); + var results = realm.objects('TestObject'); + TestCase.assertThrows(function() { + results.min('foo') + }); + TestCase.assertThrows(function() { + results.max('foo') + }); + TestCase.assertThrows(function() { + results.sum('foo') + }); + TestCase.assertThrows(function() { + results.avg('foo') + }); + } };