diff --git a/CHANGELOG.md b/CHANGELOG.md index cb88dd53..8b6bd148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,17 +33,22 @@ ### Breaking changes * None -### Enhancements -* Add a callback function used to verify SSL certificates in the sync config. - -### Bug fixes -* Fixed port conflict between RN >= 0.48 inspector proxy and RPC server used for Chrome debugging (#1294). - ### Internal * Alignment of permission schemas. * Updating sync (2.0.0-rc24). +1.13.0 Release notes (to be released) +============================================================= +### Breaking changes +* None. +### Enhancements +* Add a callback function used to verify SSL certificates in the sync config. +* 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). +* Workaround for RN >= 0.49 metro-bundler check for single string literal argument to `require()` (#1342) 2.0.0-rc10 Release notes (2017-9-19) diff --git a/docs/collection.js b/docs/collection.js index 0db883aa..6310262f 100644 --- a/docs/collection.js +++ b/docs/collection.js @@ -233,6 +233,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 3957bfcf..ee2ee3ca 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -143,6 +143,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/lib/index.js b/lib/index.js index aa7bb148..813cde3a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,9 +18,11 @@ 'use strict'; +const require_method = require; + // Prevent React Native packager from seeing modules required with this function nodeRequire(module) { - return require(module); + return require_method(module); } function getContext() { @@ -91,7 +93,7 @@ switch(getContext()) { var pkg = path.resolve(path.join(__dirname,'../package.json')); var binding_path = binary.find(pkg); - realmConstructor = require(binding_path).Realm; + realmConstructor = require_method(binding_path).Realm; break; case 'reactnative': diff --git a/lib/user-methods.js b/lib/user-methods.js index de390447..ba21ddc4 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -21,8 +21,10 @@ const AuthError = require('./errors').AuthError; const permissionApis = require('./permission-api'); +const require_method = require; + function node_require(module) { - return require(module); + return require_method(module); } function checkTypes(args, types) { diff --git a/scripts/test.sh b/scripts/test.sh index c12393db..1bc08ccb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -57,7 +57,7 @@ start_server() { } stop_server() { - echo stopping server + echo stopping server if [[ ${SERVER_PID} -gt 0 ]] ; then echo server is running. killing it kill -9 ${SERVER_PID} || true @@ -352,12 +352,12 @@ case "$TARGET" in ;; "node") npm run check-environment - if [ "$(uname)" = 'Darwin' ]; then + if [ "$(uname)" = 'Darwin' ]; then echo "downloading server" download_server echo "starting server" start_server - + npm_tests_cmd="npm run test" npm install --build-from-source=realm --realm_enable_sync diff --git a/src/js_list.hpp b/src/js_list.hpp index 0bb33ef2..b96b103a 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; @@ -77,11 +78,17 @@ struct ListClass : ClassDefinition, CollectionClass> { static void is_valid(ContextType, ObjectType, Arguments, ReturnValue &); static void index_of(ContextType, ObjectType, Arguments, 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, ObjectType, Arguments, ReturnValue &); static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); - + std::string const name = "List"; MethodMap const methods = { @@ -95,6 +102,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}, @@ -124,7 +135,27 @@ void ListClass::get_length(ContextType, ObjectType object, ReturnValue &retur } template -void ListClass::get_type(ContextType, ObjectType object, ReturnValue &return_value) { +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_type(ContextType ctx, ObjectType object, ReturnValue &return_value) { auto list = get_internal>(object); return_value.set(string_for_property_type(list->get_type() & ~realm::PropertyType::Flags)); } @@ -229,7 +260,7 @@ void ListClass::splice(ContextType ctx, ObjectType this_object, Arguments arg remove = std::max(Value::to_number(ctx, args[1]), 0); remove = std::min(remove, size - index); } - + std::vector removed_objects; removed_objects.reserve(remove); @@ -263,7 +294,7 @@ void ListClass::sorted(ContextType ctx, ObjectType this_object, Arguments arg auto list = get_internal>(this_object); return_value.set(ResultsClass::create_instance(ctx, list->sort(ResultsClass::get_keypaths(ctx, args)))); } - + template void ListClass::is_valid(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); @@ -284,7 +315,7 @@ void ListClass::add_listener(ContextType ctx, ObjectType this_object, Argumen auto list = get_internal>(this_object); ResultsClass::add_listener(ctx, *list, this_object, args); } - + template void ListClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); diff --git a/src/js_results.hpp b/src/js_results.hpp index afb6e43f..45e68a18 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" @@ -53,6 +54,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; @@ -84,7 +86,13 @@ struct ResultsClass : ClassDefinition, CollectionClass< template static void index_of(ContextType, Fn&, Arguments, 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, ObjectType, Arguments, ReturnValue &); static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); @@ -94,7 +102,7 @@ struct ResultsClass : ClassDefinition, CollectionClass< static void add_listener(ContextType, U&, ObjectType, Arguments); template static void remove_listener(ContextType, U&, ObjectType, Arguments); - + std::string const name = "Results"; MethodMap const methods = { @@ -102,18 +110,22 @@ struct ResultsClass : ClassDefinition, CollectionClass< {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, + {"min", wrap}, + {"max", wrap}, + {"sum", wrap}, + {"avg", wrap}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, {"indexOf", wrap}, }; - + PropertyMap const properties = { {"length", {wrap, nullptr}}, {"type", {wrap, nullptr}}, {"optional", {wrap, nullptr}}, }; - + IndexPropertyType const index_accessor = {wrap, nullptr}; }; @@ -199,6 +211,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_type(ContextType, ObjectType object, ReturnValue &return_value) { auto results = get_internal>(object); @@ -211,7 +243,6 @@ void ResultsClass::get_optional(ContextType, ObjectType object, ReturnValue & return_value.set(is_nullable(results->get_type())); } - template void ResultsClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { auto results = get_internal>(object); @@ -242,7 +273,7 @@ template void ResultsClass::is_valid(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); } - + template template void ResultsClass::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnValue &return_value) { @@ -287,7 +318,7 @@ void ResultsClass::add_listener(ContextType ctx, U& collection, ObjectType th Protected protected_callback(ctx, callback); Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - + auto token = collection.add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) { HANDLESCOPE ValueType arguments[] { @@ -333,6 +364,6 @@ void ResultsClass::remove_all_listeners(ContextType ctx, ObjectType this_obje auto results = get_internal>(this_object); results->m_notification_tokens.clear(); } - + } // js } // realm diff --git a/src/js_types.hpp b/src/js_types.hpp index 24f13c33..09b5e05a 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)) @@ -136,6 +138,8 @@ struct Value { static ValueType from_nonnull_string(ContextType, const String&); static ValueType from_nonnull_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 &); @@ -436,5 +440,37 @@ inline bool Value::is_valid_for_property_type(ContextType context, const Valu return true; } +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..cf803798 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,51 @@ 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: { + util::Optional avg = list->average(property->table_column); + if (!avg) { + return_value.set_undefined(); + } + else { + return_value.set(*avg); + } + return; + } + 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 e7a84420..b05fc302 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -1004,4 +1004,199 @@ module.exports = { TestCase.assertEqual(list.isValid(), false); TestCase.assertThrowsContaining(() => list.length, 'invalidated'); }, + + 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 b8458870..71cf3ab5 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') + }); + } };