Add support for aggregates on arrays of primitives (#1401)

* Fix an unused variable warning in js_sync.hpp
* Add support for aggregates on arrays of primitives
* Update documentation and typescript declarations
* Update collection.js
This commit is contained in:
Thomas Goyne 2017-10-11 05:54:52 -07:00 committed by Kenneth Geisshirt
parent 4bcef8baff
commit 643c19ea75
11 changed files with 194 additions and 202 deletions

View File

@ -5,6 +5,7 @@ X.Y.Z Release notes
### Enhancements
* Added `update` method to `Realm.Results` to support bulk updates (#808).
* Added support for aggregate functions on `Realm.Results` and `Realm.List` of primitive types.
### Bug fixes
* None

View File

@ -239,8 +239,14 @@ class Collection {
indexOf(object) {}
/**
* Computes the minimum value of a property.
* @param {string} property - The name of the property.
* Returns the minimum value of the values in the collection or of the
* given property among all the objects in the collection, or `undefined`
* if the collection is empty.
*
* Only supported for int, float, double and date properties. `null` values
* are ignored entirely by this method and will not be returned.
*
* @param {string} [property] - For a collection of objects, the property to take the minimum of.
* @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
@ -248,8 +254,14 @@ class Collection {
min(property) {}
/**
* Computes the maximum value of a property.
* @param {string} property - The name of the property.
* Returns the maximum value of the values in the collection or of the
* given property among all the objects in the collection, or `undefined`
* if the collection is empty.
*
* Only supported for int, float, double and date properties. `null` values
* are ignored entirely by this method and will not be returned.
*
* @param {string} [property] - For a collection of objects, the property to take the maximum of.
* @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
@ -257,19 +269,29 @@ class Collection {
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.
* Computes the sum of the values in the collection or of the given
* property among all the objects in the collection, or 0 if the collection
* is empty.
*
* Only supported for int, float and double properties. `null` values are
* ignored entirely by this method.
* @param {string} [property] - For a collection of objects, the property to take the sum of.
* @throws {Error} If no property with the name exists or if property is not numeric.
* @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.
* Computes the average of the values in the collection or of the given
* property among all the objects in the collection, or `undefined` if the collection
* is empty.
*
* Only supported for int, float and double properties. `null` values are
* ignored entirely by this method and will not be factored into the average.
* @param {string} [property] - For a collection of objects, the property to take the average of.
* @throws {Error} If no property with the name exists or if property is not numeric.
* @returns {number} the sum.
* @since 1.12.1
*/
avg(property) {}

31
lib/index.d.ts vendored
View File

@ -144,33 +144,10 @@ 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;
min(property?: string): number|Date|null;
max(property?: string): number|Date|null;
sum(property?: string): number|null;
avg(property?: string): number;
/**
* @param {string} query

View File

@ -78,12 +78,6 @@ struct ListClass : ClassDefinition<T, realm::js::List<T>, CollectionClass<T>> {
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 &);
@ -102,10 +96,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>},
{"min", wrap<compute_aggregate_on_collection<ListClass<T>, AggregateFunc::Min>>},
{"max", wrap<compute_aggregate_on_collection<ListClass<T>, AggregateFunc::Max>>},
{"sum", wrap<compute_aggregate_on_collection<ListClass<T>, AggregateFunc::Sum>>},
{"avg", wrap<compute_aggregate_on_collection<ListClass<T>, AggregateFunc::Avg>>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
@ -134,26 +128,6 @@ 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_type(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto list = get_internal<T, ListClass<T>>(object);

View File

@ -89,12 +89,6 @@ struct ResultsClass : ClassDefinition<T, realm::js::Results<T>, CollectionClass<
static void update(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, ObjectType, Arguments, ReturnValue &);
static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &);
@ -112,10 +106,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>},
{"min", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Min>>},
{"max", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Max>>},
{"sum", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Sum>>},
{"avg", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Avg>>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
@ -214,26 +208,6 @@ 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_type(ContextType, ObjectType object, ReturnValue &return_value) {
auto results = get_internal<T, ResultsClass<T>>(object);

View File

@ -18,10 +18,6 @@
#pragma once
#include <list>
#include <map>
#include <set>
#include <regex>
#include <mutex>
#include <condition_variable>
@ -40,11 +36,11 @@ namespace realm {
namespace js {
inline realm::SyncManager& syncManagerShared() {
static bool configured = []{
static std::once_flag flag;
std::call_once(flag, [] {
ensure_directory_exists_for_file(default_realm_file_directory());
SyncManager::shared().configure_file_system(default_realm_file_directory(), SyncManager::MetadataMode::NoEncryption);
return true;
}();
});
return SyncManager::shared();
}
@ -627,8 +623,10 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
std::string raw_realm_url = Object::validated_get_string(ctx, sync_config_object, "url");
if (shared_user->token_type() == SyncUser::TokenType::Admin) {
static std::regex tilde("/~/");
raw_realm_url = std::regex_replace(raw_realm_url, tilde, "/__auth/");
size_t pos = raw_realm_url.find("/~/");
if (pos != std::string::npos) {
raw_realm_url.replace(pos + 1, 1, "__auth");
}
}
bool client_validate_ssl = true;

View File

@ -139,7 +139,7 @@ struct Value {
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<Mixed> &);
static ValueType from_mixed(ContextType, const util::Optional<Mixed> &);
static ObjectType to_array(ContextType, const ValueType &);
static bool to_boolean(ContextType, const ValueType &);
@ -446,7 +446,7 @@ inline typename T::Value Value<T>::from_timestamp(typename T::Context ctx, Times
}
template<typename T>
inline typename T::Value Value<T>::from_mixed(typename T::Context ctx, util::Optional<Mixed>& mixed) {
inline typename T::Value Value<T>::from_mixed(typename T::Context ctx, const util::Optional<Mixed>& mixed) {
if (!mixed) {
return from_undefined(ctx);
}

View File

@ -83,50 +83,42 @@ 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]);
template<typename T, AggregateFunc func>
void compute_aggregate_on_collection(typename T::ContextType ctx, typename T::ObjectType this_object,
typename T::Arguments args, typename T::ReturnValue &return_value) {
auto list = get_internal<typename T::Type, T>(this_object);
size_t column = 0;
if (list->get_type() == realm::PropertyType::Object) {
const ObjectSchema& object_schema = list->get_object_schema();
std::string property_name = T::Value::validated_to_string(ctx, args[0]);
const Property* property = object_schema.property_for_name(property_name);
if (!property) {
throw std::invalid_argument(util::format("No such property: %1", property_name));
throw std::invalid_argument(util::format("Property '%1' does not exist on object '%2'",
property_name, object_schema.name));
}
column = property->table_column;
}
else {
args.validate_maximum(0);
}
util::Optional<Mixed> mixed;
switch (func) {
case AggregateFunc::Min: {
mixed = list->min(property->table_column);
case AggregateFunc::Min:
return_value.set(list->min(column));
break;
case AggregateFunc::Max:
return_value.set(list->max(column));
break;
case AggregateFunc::Sum:
return_value.set(list->sum(column));
break;
case AggregateFunc::Avg:
return_value.set(list->average(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<double> 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

View File

@ -53,12 +53,26 @@ class ReturnValue<jsc::Types> {
void set(uint32_t number) {
m_value = JSValueMakeNumber(m_context, number);
}
void set(const util::Optional<realm::Mixed>& mixed) {
m_value = Value<jsc::Types>::from_mixed(m_context, mixed);
}
void set_null() {
m_value = JSValueMakeNull(m_context);
}
void set_undefined() {
m_value = JSValueMakeUndefined(m_context);
}
template<typename T>
void set(const util::Optional<T>& value) {
if (value) {
set(*value);
}
else {
set_undefined();
}
}
operator JSValueRef() const {
return m_value;
}

View File

@ -64,12 +64,25 @@ class ReturnValue<node::Types> {
void set(uint32_t number) {
m_value.Set(number);
}
void set(realm::Mixed mixed) {
m_value.Set(Value<node::Types>::from_mixed(nullptr, mixed));
}
void set_null() {
m_value.SetNull();
}
void set_undefined() {
m_value.SetUndefined();
}
template<typename T>
void set(util::Optional<T> value) {
if (value) {
set(*value);
}
else {
m_value.SetUndefined();
}
}
};
} // js

View File

@ -1009,17 +1009,14 @@ module.exports = {
const NullableBasicTypesList = {
name: 'NullableBasicTypesList',
properties: {
list: {type: 'list', objectType: 'NullableBasicTypesObject'},
list: 'NullableBasicTypesObject[]',
}
};
var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
var object;
const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
const N = 50;
var list = [];
for(var i = 0; i < N; i++) {
const list = [];
for (let i = 0; i < N; i++) {
list.push({
intCol: i+1,
floatCol: i+1,
@ -1028,6 +1025,7 @@ module.exports = {
});
}
let object;
realm.write(() => {
object = realm.create('NullableBasicTypesList', {list: list});
});
@ -1051,19 +1049,17 @@ module.exports = {
const NullableBasicTypesList = {
name: 'NullableBasicTypesList',
properties: {
list: {type: 'list', objectType: 'NullableBasicTypesObject'},
list: 'NullableBasicTypesObject[]',
}
};
var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
var object;
var objectEmptyList;
const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
const N = 50;
const M = 10;
var list = [];
for(var i = 0; i < N; i++) {
const list = [];
for (let i = 0; i < N; i++) {
list.push({
intCol: i+1,
floatCol: i+1,
@ -1072,15 +1068,11 @@ module.exports = {
});
}
for(var j = 0; j < M; j++) {
list.push({
intCol: null,
floatCol: null,
doubleCol: null,
dateCol: null
});
for (let j = 0; j < M; j++) {
list.push({});
}
let object, objectEmptyList;
realm.write(() => {
object = realm.create('NullableBasicTypesList', {list: list});
objectEmptyList = realm.create('NullableBasicTypesList', {list: []});
@ -1088,7 +1080,6 @@ module.exports = {
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 => {
@ -1115,6 +1106,44 @@ module.exports = {
TestCase.assertUndefined(objectEmptyList.list.max('dateCol'));
},
testPrimitiveListAggregateFunctions: function() {
const realm = new Realm({schema: [schemas.PrimitiveArrays]});
let object;
realm.write(() => {
object = realm.create('PrimitiveArrays', {
int: [1, 2, 3],
float: [1.1, 2.2, 3.3],
double: [1.11, 2.22, 3.33],
date: [DATE1, DATE2, DATE3],
optInt: [1, null, 2],
optFloat: [1.1, null, 3.3],
optDouble: [1.11, null, 3.33],
optDate: [DATE1, null, DATE3]
});
});
for (let prop of ['int', 'float', 'double', 'date', 'optInt', 'optFloat', 'optDouble', 'optDate']) {
const list = object[prop];
TestCase.assertSimilar(list.type, list.min(), list[0]);
TestCase.assertSimilar(list.type, list.max(), list[2]);
if (list.type === 'date') {
TestCase.assertThrowsContaining(() => list.sum(), "Cannot sum 'date' array: operation not supported")
TestCase.assertThrowsContaining(() => list.avg(), "Cannot average 'date' array: operation not supported")
continue;
}
const sum = list[0] + list[1] + list[2];
const avg = sum / (list[1] === null ? 2 : 3);
TestCase.assertSimilar(list.type, list.sum(), sum);
TestCase.assertSimilar(list.type, list.avg(), avg);
}
TestCase.assertThrowsContaining(() => object.bool.min(), "Cannot min 'bool' array: operation not supported")
TestCase.assertThrowsContaining(() => object.int.min("foo"), "Invalid arguments: at most 0 expected, but 1 supplied")
},
testListAggregateFunctionsUnsupported: function() {
const NullableBasicTypesList = {
name: 'NullableBasicTypesList',
@ -1123,13 +1152,12 @@ module.exports = {
}
};
var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
var object;
const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]});
const N = 5;
var list = [];
for(var i = 0; i < N; i++) {
for (let i = 0; i < N; i++) {
list.push({
intCol: i+1,
floatCol: i+1,
@ -1138,6 +1166,7 @@ module.exports = {
});
}
let object;
realm.write(() => {
object = realm.create('NullableBasicTypesList', {list: list});
});
@ -1145,38 +1174,33 @@ module.exports = {
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'].forEach(colName => {
TestCase.assertThrowsContaining(() => object.list.min(colName + 'Col'),
`Cannot min property '${colName}Col': operation not supported for '${colName}' properties`);
});
// bool, string & data columns don't support 'max'
['boolCol', 'stringCol', 'dataCol'].forEach(colName => {
TestCase.assertThrows(function() {
object.list.max(colName);
}
)});
['bool', 'string', 'data'].forEach(colName => {
TestCase.assertThrowsContaining(() => object.list.max(colName + 'Col'),
`Cannot max property '${colName}Col': operation not supported for '${colName}' properties`);
});
// 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'].forEach(colName => {
TestCase.assertThrowsContaining(() => object.list.avg(colName + 'Col'),
`Cannot average property '${colName}Col': operation not supported for '${colName}' properties`);
});
// bool, string, date & data columns don't support 'sum'
['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => {
TestCase.assertThrows(function() {
object.list.sum(colName);
}
)});
['bool', 'string', 'date', 'data'].forEach(colName => {
TestCase.assertThrowsContaining(() => object.list.sum(colName + 'Col'),
`Cannot sum property '${colName}Col': operation not supported for '${colName}' properties`);
});
},
testListAggregateFunctionsWrongProperty: function() {
var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]});
var object;
var list;
const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]});
let object;
realm.write(() => {
object = realm.create('PersonList', {list: [
{name: 'Ari', age: 10},
@ -1185,18 +1209,21 @@ module.exports = {
]});
});
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')
});
TestCase.assertThrowsContaining(() => object.list.min('foo'),
"Property 'foo' does not exist on object 'PersonObject'");
TestCase.assertThrowsContaining(() => object.list.max('foo'),
"Property 'foo' does not exist on object 'PersonObject'");
TestCase.assertThrowsContaining(() => object.list.sum('foo'),
"Property 'foo' does not exist on object 'PersonObject'");
TestCase.assertThrowsContaining(() => object.list.avg('foo'),
"Property 'foo' does not exist on object 'PersonObject'");
TestCase.assertThrowsContaining(() => object.list.min(),
"JS value must be of type 'string', got (undefined)");
TestCase.assertThrowsContaining(() => object.list.max(),
"JS value must be of type 'string', got (undefined)");
TestCase.assertThrowsContaining(() => object.list.sum(),
"JS value must be of type 'string', got (undefined)");
TestCase.assertThrowsContaining(() => object.list.avg(),
"JS value must be of type 'string', got (undefined)");
},
};