query aggregation
MODIFY
packages/isar/lib/src/isar_connect.dart
packages/isar/lib/src/isar_connect.dart
@@ -17,6 +17,7 @@ abstract class _IsarConnect {
ConnectAction.editProperty: _editProperty,
ConnectAction.addInList: _addInList,
ConnectAction.removeFromList: _removeFromList,
+ ConnectAction.aggregation: _aggregation,
};
static bool _initialized = false;
@@ -302,6 +303,15 @@ abstract class _IsarConnect {
await collection.isar.writeTxn(() async => collection.importJson(objects));
}
+ static Future<num?> _aggregation(Map<String, dynamic> params) async {
+ final cQuery = ConnectQuery.fromJson(
+ params['query'] as Map<String, dynamic>,
+ );
+
+ final query = _getQuery(cQuery);
+ return query.aggregate<num>(AggregationOp.values[params['op'] as int]);
+ }
+
static Query<dynamic> _getQuery(ConnectQuery query) {
final collection = Isar.getInstance(query.instance)!
.getCollectionByNameInternal(query.collection)!;
@@ -325,6 +335,7 @@ abstract class _IsarConnect {
offset: query.offset,
limit: query.limit,
sortBy: [if (sortProperty != null) sortProperty],
+ property: query.property,
);
}
}

MODIFY
packages/isar/lib/src/isar_connect_api.dart
packages/isar/lib/src/isar_connect_api.dart
@@ -14,7 +14,8 @@ enum ConnectAction {
exportJson('ext.isar.exportJson'),
editProperty('ext.isar.editProperty'),
addInList('ext.isar.addInList'),
- removeFromList('ext.isar.removeFromList');
+ removeFromList('ext.isar.removeFromList'),
+ aggregation('ext.isar.aggregation');
const ConnectAction(this.method);
@@ -39,6 +40,7 @@ class ConnectQuery {
this.offset,
this.limit,
this.sortProperty,
+ this.property,
});
factory ConnectQuery.fromJson(Map<String, dynamic> json) {
@@ -56,6 +58,7 @@ class ConnectQuery {
sort: Sort.values[json['sortProperty']['sort'] as int],
)
: null,
+ property: json['property'] as String?,
);
}
@@ -65,6 +68,7 @@ class ConnectQuery {
final int? offset;
final int? limit;
final SortProperty? sortProperty;
+ final String? property;
Map<String, dynamic> toJson() {
return {
@@ -78,6 +82,7 @@ class ConnectQuery {
'property': sortProperty!.property,
'sort': sortProperty!.sort.index,
},
+ if (property != null) 'property': property
};
}

MODIFY
packages/isar_inspector/lib/query_table.dart
packages/isar_inspector/lib/query_table.dart
@@ -7,6 +7,7 @@ import 'package:isar/isar.dart';
import 'package:isar_inspector/common.dart';
import 'package:isar_inspector/edit_popup.dart';
import 'package:isar_inspector/prev_next.dart';
+import 'package:isar_inspector/query_builder.dart';
import 'package:isar_inspector/schema.dart';
import 'package:isar_inspector/state/collections_state.dart';
import 'package:isar_inspector/state/instances_state.dart';
@@ -26,6 +27,8 @@ typedef Editor = void Function(
EditorType editing,
);
+typedef Aggregate = Future<num?> Function(String property, AggregationOp op);
+
enum EditorType { add, edit, remove }
class QueryTable extends ConsumerWidget {
@@ -69,22 +72,35 @@ class QueryTable extends ConsumerWidget {
value: value,
);
+ final pod = ref.read(isarConnectPod.notifier);
switch (editing) {
case EditorType.add:
- ref.read(isarConnectPod.notifier).addInList(edit);
+ pod.addInList(edit);
break;
case EditorType.edit:
- ref.read(isarConnectPod.notifier).editProperty(edit);
+ pod.editProperty(edit);
break;
case EditorType.remove:
- ref
- .read(isarConnectPod.notifier)
- .removeFromList(edit);
+ pod.removeFromList(edit);
break;
}
},
+ aggregate: (property, op) async {
+ final query = ConnectQuery(
+ instance: ref.read(selectedInstancePod).value!,
+ collection: collection.name,
+ filter: collection.uiFilter == null
+ ? null
+ : QueryBuilderUI.parseQuery(collection.uiFilter!),
+ property: property,
+ );
+
+ return ref
+ .read(isarConnectPod.notifier)
+ .aggregate(query, op);
+ },
),
Align(
alignment: Alignment.topLeft,
@@ -141,11 +157,13 @@ class TableBlock extends StatefulWidget {
required this.collection,
required this.object,
required this.editor,
+ required this.aggregate,
});
final ICollection collection;
final QueryObject object;
final Editor editor;
+ final Aggregate aggregate;
@override
State<TableBlock> createState() => _TableBlockState();
@@ -173,6 +191,7 @@ class _TableBlockState extends State<TableBlock> {
data: node.data as TreeViewHelper,
editor: widget.editor,
objectId: _id,
+ aggregate: widget.aggregate,
);
},
onExpansionChanged: (key, expanded) {
@@ -324,11 +343,13 @@ class TableItem extends StatefulWidget {
required this.data,
required this.editor,
required this.objectId,
+ required this.aggregate,
});
final TreeViewHelper data;
final int objectId;
final Editor editor;
+ final Aggregate aggregate;
@override
State<TableItem> createState() => _TableItemState();
@@ -337,7 +358,7 @@ class TableItem extends StatefulWidget {
class _TableItemState extends State<TableItem> {
bool _entered = false;
- late final _options = [
+ late final _options = <PopupMenuEntry<dynamic>>[
if (widget.data is LinkHelper && widget.data.value != null)
PopupMenuItem<dynamic>(
onTap: () => _copy(true),
@@ -450,6 +471,53 @@ class _TableItemState extends State<TableItem> {
text: 'Set value to null',
),
),
+ if (widget.data is PropertyHelper &&
+ !(widget.data as PropertyHelper).subLink &&
+ (widget.data as PropertyHelper).index == null &&
+ (widget.data as PropertyHelper).property.type.isNum)
+ PopupMenuItem(
+ padding: EdgeInsets.zero,
+ child: PopupMenuButton(
+ position: PopupMenuPosition.under,
+ tooltip: '',
+ onCanceled: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ }
+ },
+ itemBuilder: (BuildContext context) => [
+ PopupMenuItem<dynamic>(
+ onTap: () => _aggregate(AggregationOp.min),
+ child: const PopUpMenuRow(text: 'Min'),
+ ),
+ PopupMenuItem<dynamic>(
+ onTap: () => _aggregate(AggregationOp.max),
+ child: const PopUpMenuRow(text: 'Max'),
+ ),
+ PopupMenuItem<dynamic>(
+ onTap: () => _aggregate(AggregationOp.sum),
+ child: const PopUpMenuRow(text: 'Sum'),
+ ),
+ PopupMenuItem<dynamic>(
+ onTap: () => _aggregate(AggregationOp.average),
+ child: const PopUpMenuRow(text: 'Average'),
+ ),
+ ],
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ constraints: const BoxConstraints(
+ minHeight: kMinInteractiveDimension,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: const [
+ PopUpMenuRow(text: 'Aggregation'),
+ Icon(Icons.arrow_right),
+ ],
+ ),
+ ),
+ ),
+ ),
];
@override
@@ -590,6 +658,38 @@ class _TableItemState extends State<TableItem> {
);
}
+ Future<void> _aggregate(AggregationOp op) async {
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
+ final property = (widget.data as PropertyHelper).property;
+ dynamic result;
+
+ if (property.type == IsarType.Int ||
+ property.type == IsarType.Long ||
+ property.type == IsarType.Byte) {
+ result = (await widget.aggregate(property.name, op))?.toInt();
+ } else {
+ result = await widget.aggregate(property.name, op);
+ }
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ duration: const Duration(seconds: 8),
+ width: 300,
+ behavior: SnackBarBehavior.floating,
+ content: Text(
+ '${op.name}(${property.name}) = $result',
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ );
+ }
+ }
+
Widget _createText() {
var value = '';
var property = '';

MODIFY
packages/isar_inspector/lib/schema.dart
packages/isar_inspector/lib/schema.dart
import 'dart:core';
-import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:isar_inspector/query_builder.dart';
@@ -217,4 +216,18 @@ enum IsarType {
}
throw IsarError('new IsarType ($name), rule not defined');
}
+
+ bool get isNum {
+ //ignore: missing_enum_constant_in_switch
+ switch (this) {
+ case IsarType.Int:
+ case IsarType.Float:
+ case IsarType.Long:
+ case IsarType.Byte:
+ case IsarType.Double:
+ return true;
+ }
+
+ return false;
+ }
}

MODIFY
packages/isar_inspector/lib/state/isar_connect_state_notifier.dart
packages/isar_inspector/lib/state/isar_connect_state_notifier.dart
@@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:isar/isar.dart';
import 'package:isar/src/isar_connect_api.dart';
import 'package:isar_inspector/state/collections_state.dart';
import 'package:isar_inspector/state/instances_state.dart';
@@ -197,6 +198,14 @@ class IsarConnectStateNotifier
);
}
+ Future<num?> aggregate(ConnectQuery query, AggregationOp op) async {
+ return _call<num?>(
+ ConnectAction.aggregation,
+ args: {'query': query.toJson(), 'op': op.index},
+ timeout: kLongTimeout,
+ );
+ }
+
Future<void> disconnect() async {
await state.value?.vmService.dispose();
}

query aggregation