Add support for aggregates on collections (#807) (#1350)

This commit is contained in:
Ashwin Phatak 2017-09-29 16:53:37 +05:30 committed by GitHub
parent c550105b2c
commit 199210eb68
11 changed files with 565 additions and 1 deletions

View File

@ -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).

View File

@ -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.

View File

@ -32,6 +32,10 @@ createMethods(List.prototype, objectTypes.LIST, [
'snapshot',
'isValid',
'indexOf',
'min',
'max',
'sum',
'avg',
'addListener',
'removeListener',
'removeAllListeners',

View File

@ -31,6 +31,10 @@ createMethods(Results.prototype, objectTypes.RESULTS, [
'snapshot',
'isValid',
'indexOf',
'min',
'max',
'sum',
'avg',
'addListener',
'removeListener',
'removeAllListeners',

28
lib/index.d.ts vendored
View File

@ -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

View File

@ -47,6 +47,7 @@ class List : public realm::List {
template<typename T>
struct ListClass : ClassDefinition<T, realm::js::List<T>, CollectionClass<T>> {
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<T, realm::js::List<T>, CollectionClass<T>> {
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<T, realm::js::List<T>, CollectionClass<T>> {
{"sorted", wrap<sorted>},
{"isValid", wrap<is_valid>},
{"indexOf", wrap<index_of>},
{"min", wrap<min>},
{"max", wrap<max>},
{"sum", wrap<sum>},
{"avg", wrap<avg>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
@ -115,6 +126,26 @@ void ListClass<T>::get_length(ContextType, ObjectType object, ReturnValue &retur
return_value.set((uint32_t)list->size());
}
template<typename T>
void ListClass<T>::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ListClass<T>>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ListClass<T>::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ListClass<T>>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ListClass<T>::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ListClass<T>>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ListClass<T>::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ListClass<T>>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ListClass<T>::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) {
auto list = get_internal<T, ListClass<T>>(object);

View File

@ -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<typename T>
struct ResultsClass : ClassDefinition<T, realm::js::Results<T>, CollectionClass<T>> {
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<T, realm::js::Results<T>, 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<T, realm::js::Results<T>, CollectionClass<
{"filtered", wrap<filtered>},
{"sorted", wrap<sorted>},
{"isValid", wrap<is_valid>},
{"min", wrap<min>},
{"max", wrap<max>},
{"sum", wrap<sum>},
{"avg", wrap<avg>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
@ -195,6 +207,26 @@ void ResultsClass<T>::get_length(ContextType ctx, ObjectType object, ReturnValue
return_value.set((uint32_t)results->size());
}
template<typename T>
void ResultsClass<T>::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ResultsClass<T>>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ResultsClass<T>::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ResultsClass<T>>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ResultsClass<T>::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ResultsClass<T>>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ResultsClass<T>::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
compute_aggregate_on_collection<ResultsClass<T>>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value);
}
template<typename T>
void ResultsClass<T>::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) {
auto results = get_internal<T, ResultsClass<T>>(object);

View File

@ -29,6 +29,8 @@
#include <realm/binary_data.hpp>
#include <realm/string_data.hpp>
#include <realm/util/to_string.hpp>
#include <realm/util/optional.hpp>
#include <realm/mixed.hpp>
#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<T> &);
static ValueType from_binary(ContextType, BinaryData);
static ValueType from_undefined(ContextType);
static ValueType from_timestamp(ContextType, Timestamp);
static ValueType from_mixed(ContextType, util::Optional<Mixed> &);
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 "<unknown>";
}
template<typename T>
inline typename T::Value Value<T>::from_timestamp(typename T::Context ctx, Timestamp ts) {
return Object<T>::create_date(ctx, ts.get_seconds() * 1000 + ts.get_nanoseconds() / 1000000);
}
template<typename T>
inline typename T::Value Value<T>::from_mixed(typename T::Context ctx, util::Optional<Mixed>& 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<double>(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

View File

@ -22,11 +22,19 @@
#include <sstream>
#include <stdexcept>
#include "object_schema.hpp"
#include "shared_realm.hpp"
namespace realm {
namespace js {
enum class AggregateFunc {
Min,
Max,
Sum,
Avg
};
template<typename T>
class RealmDelegate;
@ -75,5 +83,45 @@ static inline void validate_argument_count_at_least(size_t count, size_t expecte
}
}
template<typename T>
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<typename T::Type, T>(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> 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

View File

@ -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')
});
},
};

View File

@ -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')
});
}
};