message:
accept negative numbers in inspector
better text input formatter
fix bug in listview

diff:
@@ -24,6 +24,7 @@ class CollectionsList extends ConsumerWidget {
final selectedInstance = ref.watch(selectedInstancePod).value!;
return ListView.builder(
+ primary: false,
itemBuilder: (BuildContext context, int index) {
final collection = collections.elementAt(index);
final info = collectionInfo[collection.name];

diff:
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:isar/isar.dart';
+import 'package:isar_inspector/schema.dart';
class IsarCard extends StatelessWidget {
IsarCard({
@@ -85,3 +88,65 @@ class CheckBoxLabel extends StatelessWidget {
);
}
}
+
+class CustomTextInputFormatter extends TextInputFormatter {
+ CustomTextInputFormatter(this.type) {
+ if (type == IsarType.Byte) {
+ _callback = _byte;
+ } else if (type == IsarType.Long || type == IsarType.Int) {
+ _callback = _int;
+ } else if (type == IsarType.Double || type == IsarType.Float) {
+ _callback = _double;
+ } else {
+ throw IsarError('new IsarType (${type.name}), rule not defined');
+ }
+ }
+
+ final IsarType type;
+ late TextEditingValue Function(TextEditingValue, TextEditingValue) _callback;
+
+ @override
+ TextEditingValue formatEditUpdate(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ return _callback(oldValue, newValue);
+ }
+
+ TextEditingValue _byte(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ if (newValue.text.isEmpty ||
+ newValue.text.length <= 3 && int.tryParse(newValue.text) != null) {
+ return newValue;
+ }
+ return oldValue;
+ }
+
+ TextEditingValue _int(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ if (newValue.text.isEmpty ||
+ (newValue.text.length == 1 && newValue.text == '-') ||
+ int.tryParse(newValue.text) != null) {
+ return newValue;
+ }
+
+ return oldValue;
+ }
+
+ TextEditingValue _double(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ if (newValue.text.isEmpty ||
+ (newValue.text.length == 1 && newValue.text == '-') ||
+ double.tryParse(newValue.text) != null) {
+ return newValue;
+ }
+
+ return oldValue;
+ }
+}

diff:
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:isar/isar.dart';
+import 'package:isar_inspector/common.dart';
import 'package:isar_inspector/schema.dart';
class EditPopup extends StatefulWidget {
@@ -23,7 +22,7 @@ class _EditPopupState extends State<EditPopup> {
final _formKey = GlobalKey<FormState>();
final _controller = TextEditingController();
final _focus = FocusNode();
- RegExp? _re;
+ CustomTextInputFormatter? _inputFormatter;
bool? _boolValue;
bool _null = false;
@@ -43,16 +42,8 @@ class _EditPopupState extends State<EditPopup> {
extentOffset: _controller.text.length,
);
- if ([IsarType.Float, IsarType.Double].contains(widget.type)) {
- _re = RegExp(r'^(\d+)?\.?\d*$');
- } else if ([IsarType.Int, IsarType.Long].contains(widget.type)) {
- _re = RegExp(r'^\d*$');
- } else if (widget.type == IsarType.Byte) {
- _re = RegExp(r'^\d{0,3}$');
- } else if (widget.type == IsarType.String) {
- _re = null;
- } else {
- throw IsarError('new IsarType (${widget.type.name}), rule not defined');
+ if (widget.type != IsarType.String) {
+ _inputFormatter = CustomTextInputFormatter(widget.type);
}
if (!_null) {
@@ -127,7 +118,7 @@ class _EditPopupState extends State<EditPopup> {
contentPadding: const EdgeInsets.all(20),
),
inputFormatters: [
- if (_re != null) FilteringTextInputFormatter.allow(_re!)
+ if (_inputFormatter != null) _inputFormatter!
],
maxLines: widget.type == IsarType.String ? 3 : 1,
validator: (value) {

diff:
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:isar/isar.dart';
import 'package:isar_inspector/common.dart';
@@ -506,8 +505,8 @@ class _ConditionUIState extends State<_ConditionUI> {
contentPadding: const EdgeInsets.all(20),
),
inputFormatters: [
- if (widget.helper.textRe != null)
- FilteringTextInputFormatter.allow(widget.helper.textRe!)
+ if (widget.helper.textFormatter != null)
+ widget.helper.textFormatter!
],
style: GoogleFonts.sourceCodePro(),
),
@@ -686,7 +685,7 @@ class _ConditionHelper extends QueryBuilderUIHelper {
}
}
- RegExp? get textRe {
+ CustomTextInputFormatter? get textFormatter {
switch (property.type) {
case IsarType.String:
case IsarType.StringList:
@@ -698,17 +697,17 @@ class _ConditionHelper extends QueryBuilderUIHelper {
case IsarType.LongList:
case IsarType.Int:
case IsarType.Long:
- return RegExp(r'^\d*$');
+ return CustomTextInputFormatter(IsarType.Int);
case IsarType.ByteList:
case IsarType.Byte:
- return RegExp(r'^\d{0,3}$');
+ return CustomTextInputFormatter(IsarType.Byte);
case IsarType.FloatList:
case IsarType.DoubleList:
case IsarType.Float:
case IsarType.Double:
- return RegExp(r'^(\d+)?\.?\d*$');
+ return CustomTextInputFormatter(IsarType.Double);
}
}

diff:
@@ -43,7 +43,7 @@ class QueryTable extends ConsumerWidget {
children: [
Expanded(
child: ListView.separated(
- controller: ScrollController(),
+ primary: true,
itemCount: objects.length,
itemBuilder: (context, index) {
return Stack(


message: fix null lists

change_type: MODIFY

diff:
@@ -184,7 +184,7 @@ class _TableBlockState extends State<TableBlock> {
final children = <Node<TreeViewHelper>>[];
final value = widget.object.getValue(property.name);
- if (property.type.isList) {
+ if (property.type.isList && value != null) {
final list = value as List<dynamic>;
for (var index = 0; index < list.length; index++) {
@@ -345,7 +345,7 @@ class _TableItemState extends State<TableItem> {
var value = '';
var color = Colors.white;
- if (widget.data.value == null) {
+ if (widget.data.value == null && !widget.data.property.type.isList) {
value = 'null';
color = _boolColor;
} else {
@@ -376,8 +376,10 @@ class _TableItemState extends State<TableItem> {
case IsarType.DoubleList:
case IsarType.StringList:
case IsarType.BoolList:
- value = '${widget.data.property.type.name}'
- ' [${(widget.data.value as List).length}]';
+ value = '${widget.data.property.type.name} [';
+ value += widget.data.value == null
+ ? 'null]'
+ : '${(widget.data.value as List).length.toString()}]';
color = _disableColor;
}
}

license: Apache License 2.0

repo: isar/isar

original_message: fix null lists


message:
PopupMenu for inspector
moved making property null to popup menu

diff:
@@ -28,10 +28,13 @@ class _AppState extends State<App> {
scaffoldBackgroundColor: Colors.transparent,
primaryColor: Colors.blue,
cardColor: const Color(0xff1f2128),
- dividerColor: const Color.fromARGB(255, 40, 41, 46),
+ dividerColor: Colors.grey,
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.all(Colors.blue),
),
+ popupMenuTheme: const PopupMenuThemeData(
+ color: Color.fromARGB(255, 40, 41, 46),
+ ),
buttonTheme: const ButtonThemeData(
alignedDropdown: true,
),

diff:
@@ -7,12 +7,10 @@ class EditPopup extends StatefulWidget {
super.key,
required this.type,
required this.value,
- required this.enableNull,
});
final IsarType type;
final dynamic value;
- final bool enableNull;
@override
State<EditPopup> createState() => _EditPopupState();
@@ -25,18 +23,13 @@ class _EditPopupState extends State<EditPopup> {
CustomTextInputFormatter? _inputFormatter;
bool? _boolValue;
- bool _null = false;
@override
void initState() {
- if (widget.enableNull && widget.value == null) {
- _null = true;
- }
-
if (widget.type == IsarType.Bool) {
- _boolValue = _null || widget.value as bool;
+ _boolValue = widget.value == null || widget.value as bool;
} else {
- _controller.text = _null ? '' : widget.value.toString();
+ _controller.text = widget.value == null ? '' : widget.value.toString();
_controller.selection = TextSelection(
baseOffset: 0,
extentOffset: _controller.text.length,
@@ -46,10 +39,8 @@ class _EditPopupState extends State<EditPopup> {
_inputFormatter = CustomTextInputFormatter(widget.type);
}
- if (!_null) {
_focus.requestFocus();
}
- }
super.initState();
}
@@ -61,20 +52,6 @@ class _EditPopupState extends State<EditPopup> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- if (widget.enableNull) ...[
- CheckboxListTile(
- value: _null,
- title: const Text('NULL'),
- onChanged: (value) {
- if (value != null) {
- setState(() {
- _null = value;
- });
- }
- },
- ),
- const SizedBox(height: 15),
- ],
if (widget.type == IsarType.Bool)
DropdownButtonHideUnderline(
child: DropdownButton<bool>(
@@ -89,9 +66,7 @@ class _EditPopupState extends State<EditPopup> {
child: Text('FALSE'),
),
],
- onChanged: _null
- ? null
- : (value) {
+ onChanged: (value) {
if (value != null) {
setState(() {
_boolValue = value;
@@ -106,7 +81,6 @@ class _EditPopupState extends State<EditPopup> {
child: TextFormField(
controller: _controller,
focusNode: _focus,
- enabled: !_null,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
@@ -152,9 +126,6 @@ class _EditPopupState extends State<EditPopup> {
void _save() {
dynamic value;
- if (_null) {
- value = null;
- } else {
if (widget.type == IsarType.Bool) {
value = _boolValue;
} else {
@@ -177,7 +148,6 @@ class _EditPopupState extends State<EditPopup> {
return;
}
}
- }
Navigator.pop(context, {'value': value});
}

diff:
@@ -245,16 +245,8 @@ class _TableItemState extends State<TableItem> {
@override
Widget build(BuildContext context) {
return MouseRegion(
- onEnter: (_) {
- setState(() {
- _entered = true;
- });
- },
- onExit: (e) {
- setState(() {
- _entered = false;
- });
- },
+ onEnter: (_) => setState(() => _entered = true),
+ onExit: (_) => setState(() => _entered = false),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
@@ -267,11 +259,54 @@ class _TableItemState extends State<TableItem> {
)
: _createText(),
),
- if (_entered) ...[
+ if (_entered)
+ PopupMenuButton<dynamic>(
+ elevation: 20,
+ itemBuilder: (context) => [
+ PopupMenuItem<dynamic>(
+ onTap: _copy,
+ child: const PopUpMenuRow(
+ icon: Icon(Icons.copy, size: PopUpMenuRow.iconSize),
+ text: 'Copy to clipboard',
+ ),
+ ),
if (!widget.data.property.isId &&
- !widget.data.property.type.isList) ...[
- GestureDetector(
- onTap: () async {
+ !widget.data.property.type.isList)
+ PopupMenuItem<dynamic>(
+ onTap: () {
+ WidgetsBinding.instance
+ .addPostFrameCallback((_) => _edit());
+ },
+ child: const PopUpMenuRow(
+ icon: Icon(Icons.edit, size: PopUpMenuRow.iconSize),
+ text: 'Edit item',
+ ),
+ ),
+ if (widget.data.property.type.isList)
+ PopupMenuItem<dynamic>(
+ onTap: () => _setValue(<dynamic>[]),
+ child: const PopUpMenuRow(
+ text: 'Clear list',
+ ),
+ ),
+ if (!widget.data.property.isId && widget.data.value != null)
+ PopupMenuItem<dynamic>(
+ onTap: () => _setValue(null),
+ child: const PopUpMenuRow(
+ text: 'Set value to null',
+ ),
+ ),
+ ],
+ tooltip: 'Options',
+ child: const Icon(Icons.more_vert, size: 18),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future<void> _edit() async {
final result = await showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) {
@@ -279,30 +314,17 @@ class _TableItemState extends State<TableItem> {
content: EditPopup(
type: widget.data.property.type,
value: widget.data.value,
- enableNull: widget.data.index == null,
),
);
},
);
if (result != null) {
- widget.editor(
- widget.objectId,
- widget.data.property.name,
- widget.data.index,
- result['value'],
- );
+ await _setValue(result['value']);
}
- },
- child: const Tooltip(
- message: 'Edit property',
- child: Icon(Icons.edit, size: 18),
- ),
- ),
- const SizedBox(width: 10),
- ],
- GestureDetector(
- onTap: () async {
+ }
+
+ Future<void> _copy() async {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
await Clipboard.setData(
@@ -326,18 +348,14 @@ class _TableItemState extends State<TableItem> {
),
);
}
- },
- child: const Center(
- child: Tooltip(
- message: 'Copy to clipboard',
- child: Icon(Icons.copy, size: 18),
- ),
- ),
- ),
- ],
- ],
- ),
- ),
+ }
+
+ Future<void> _setValue(dynamic value) async {
+ widget.editor(
+ widget.objectId,
+ widget.data.property.name,
+ widget.data.index,
+ value,
);
}
@@ -421,6 +439,26 @@ class _TableItemState extends State<TableItem> {
}
}
+class PopUpMenuRow extends StatelessWidget {
+ const PopUpMenuRow({super.key, this.icon, required this.text});
+
+ final Icon? icon;
+ final String text;
+
+ static const double iconSize = 18;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ if (icon != null) icon! else const SizedBox(width: iconSize),
+ const SizedBox(width: 15),
+ Text(text, style: const TextStyle(fontSize: 15)),
+ ],
+ );
+ }
+}
+
class TreeViewHelper {
const TreeViewHelper({
required this.property,

diff:
@@ -56,10 +56,13 @@ class App extends StatelessWidget {
scaffoldBackgroundColor: Colors.transparent,
primaryColor: Colors.blue,
cardColor: const Color(0xff1f2128),
- dividerColor: const Color.fromARGB(255, 40, 41, 46),
+ dividerColor: Colors.grey,
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.all(Colors.blue),
),
+ popupMenuTheme: const PopupMenuThemeData(
+ color: Color.fromARGB(255, 40, 41, 46),
+ ),
buttonTheme: const ButtonThemeData(
alignedDropdown: true,
),


message: Implemented put by index test

diff:
-void main() {}
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import '../util/common.dart';
+import '../util/sync_async_helper.dart';
+import '../util/sync_future.dart';
+
+part 'put_by_test.g.dart';
+
+@Collection()
+class BoolIndexModel {
+ BoolIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true)
+ final bool value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'BoolIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is BoolIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+@Collection()
+class IntIndexModel {
+ IntIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true)
+ final int value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'IntIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is IntIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+@Collection()
+class DoubleIndexModel {
+ DoubleIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true)
+ final double value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'DoubleIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is DoubleIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+@Collection()
+class StringValueIndexModel {
+ StringValueIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true, type: IndexType.value)
+ final String value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'StringValueIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is StringValueIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+@Collection()
+class StringHashIndexModel {
+ StringHashIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true, type: IndexType.hash)
+ final String value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'StringHashIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is StringHashIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+@Collection()
+class StringInsensitiveIndexModel {
+ StringInsensitiveIndexModel({
+ required this.value,
+ required this.index,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(unique: true, caseSensitive: false)
+ final String value;
+
+ final int index;
+
+ @override
+ String toString() {
+ return 'StringInsensitiveIndexModel{id: $id, value: $value, index: $index}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is StringInsensitiveIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ value == other.value &&
+ index == other.index;
+}
+
+void main() {
+ group('Put by index', () {
+ late Isar isar;
+
+ setUp(() async {
+ isar = await openTempIsar([
+ BoolIndexModelSchema,
+ IntIndexModelSchema,
+ DoubleIndexModelSchema,
+ StringValueIndexModelSchema,
+ StringHashIndexModelSchema,
+ StringInsensitiveIndexModelSchema,
+ ]);
+ });
+
+ tearDown(() => isar.close());
+
+ isarTest('Put by bool index', () async {
+ final obj0 = BoolIndexModel(value: true, index: 0);
+ final obj1 = BoolIndexModel(value: false, index: 1);
+ final obj2 = BoolIndexModel(value: true, index: 2);
+
+ await isar.tWriteTxn(() async {
+ await isar.boolIndexModels.tPutAllByValue([obj0, obj1]);
+ await isar.boolIndexModels.tPutByValue(obj2);
+ });
+
+ await qEqualSet(
+ isar.boolIndexModels.where().tFindAll(),
+ [obj1, obj2],
+ );
+ expect(obj0.id, obj2.id);
+
+ await isar.tWriteTxn(() => isar.boolIndexModels.tPutByValue(obj0));
+ await qEqualSet(
+ isar.boolIndexModels.where().tFindAll(),
+ [obj0, obj1],
+ );
+ expect(obj0.id, obj2.id);
+ });
+
+ isarTest('Put by int index', () async {
+ final obj0 = IntIndexModel(value: 42, index: 0);
+ final obj1 = IntIndexModel(value: 20, index: 1);
+ final obj2 = IntIndexModel(value: 42, index: 2);
+ final obj3 = IntIndexModel(value: 3, index: 3);
+
+ await isar.tWriteTxn(
+ () => isar.intIndexModels.tPutAllByValue([obj0, obj1, obj3]),
+ );
+ await qEqualSet(
+ isar.intIndexModels.where().tFindAll(),
+ [obj0, obj1, obj3],
+ );
+
+ await isar.tWriteTxn(() => isar.intIndexModels.tPutByValue(obj2));
+ await qEqualSet(
+ isar.intIndexModels.where().tFindAll(),
+ [obj1, obj2, obj3],
+ );
+ expect(obj0.id, obj2.id);
+ });
+
+ isarTest('Put by double index', () async {
+ final obj0 = DoubleIndexModel(value: 15.23, index: 0);
+ final obj1 = DoubleIndexModel(value: 15.23, index: 1);
+ final obj2 = DoubleIndexModel(value: 15.23, index: 2);
+ final obj3 = DoubleIndexModel(value: 0, index: 3);
+
+ await isar.tWriteTxn(
+ () => isar.doubleIndexModels.tPutAllByValue([obj0, obj1, obj2, obj3]),
+ );
+
+ await qEqualSet(
+ isar.doubleIndexModels.where().tFindAll(),
+ [obj2, obj3],
+ );
+ expect(obj0.id, obj1.id);
+ expect(obj0.id, obj2.id);
+ });
+
+ isarTest('Put by string value index', () async {
+ final obj0 = StringValueIndexModel(value: 'Foo bar', index: 0);
+ final obj1 = StringValueIndexModel(value: 'foo Bar', index: 1);
+ final obj2 = StringValueIndexModel(value: 'Foo bar', index: 2);
+ final obj3 = StringValueIndexModel(value: 'John Doe', index: 3);
+
+ await isar.tWriteTxn(
+ () => isar.stringValueIndexModels.tPutAllByValue([obj0, obj1]),
+ );
+ await qEqualSet(
+ isar.stringValueIndexModels.where().tFindAll(),
+ [obj0, obj1],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringValueIndexModels.tPutAllByValue([obj2, obj3]),
+ );
+ await qEqualSet(
+ isar.stringValueIndexModels.where().tFindAll(),
+ [obj1, obj2, obj3],
+ );
+ expect(obj0.id, obj2.id);
+
+ await isar.tWriteTxn(
+ () => isar.stringValueIndexModels.tPutByValue(obj0),
+ );
+ await qEqualSet(
+ isar.stringValueIndexModels.where().tFindAll(),
+ [obj0, obj1, obj3],
+ );
+ expect(obj0.id, obj2.id);
+ });
+
+ isarTest('Put by string hash index', () async {
+ final obj0 = StringHashIndexModel(value: 'abc', index: 0);
+ final obj1 = StringHashIndexModel(value: 'xyz', index: 1);
+ final obj2 = StringHashIndexModel(value: '123', index: 2);
+ final obj3 = StringHashIndexModel(value: 'xyz', index: 3);
+ final obj4 = StringHashIndexModel(value: 'abc', index: 4);
+
+ await isar.tWriteTxn(
+ () => isar.stringHashIndexModels.tPutAllByValue([obj0, obj1, obj2]),
+ );
+ await qEqualSet(
+ isar.stringHashIndexModels.where().tFindAll(),
+ [obj0, obj1, obj2],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringHashIndexModels.tPutAllByValue([obj3, obj4]),
+ );
+ await qEqualSet(
+ isar.stringHashIndexModels.where().tFindAll(),
+ [obj2, obj3, obj4],
+ );
+ expect(obj0.id, obj4.id);
+ expect(obj1.id, obj3.id);
+
+ await isar.tWriteTxn(
+ () => isar.stringHashIndexModels.tPutByValue(obj0),
+ );
+ await qEqualSet(
+ isar.stringHashIndexModels.where().tFindAll(),
+ [obj0, obj2, obj3],
+ );
+ expect(obj0.id, obj4.id);
+ });
+
+ isarTest('Put by string insensitive index', () async {
+ final obj0 = StringInsensitiveIndexModel(value: 'foo', index: 0);
+ final obj1 = StringInsensitiveIndexModel(value: 'BAR', index: 1);
+ final obj2 = StringInsensitiveIndexModel(value: 'Foo', index: 2);
+ final obj3 = StringInsensitiveIndexModel(value: 'abc', index: 3);
+ final obj4 = StringInsensitiveIndexModel(value: 'FoO', index: 4);
+ final obj5 = StringInsensitiveIndexModel(value: 'Bar', index: 5);
+
+ await isar.tWriteTxn(
+ () => isar.stringInsensitiveIndexModels.tPutAllByValue([
+ obj0,
+ obj1,
+ obj3,
+ ]),
+ );
+ await qEqualSet(
+ isar.stringInsensitiveIndexModels.where().tFindAll(),
+ [obj0, obj1, obj3],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringInsensitiveIndexModels.tPutAllByValue([
+ obj0,
+ obj3,
+ obj5,
+ ]),
+ );
+ await qEqualSet(
+ isar.stringInsensitiveIndexModels.where().tFindAll(),
+ [obj0, obj3, obj5],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringInsensitiveIndexModels.tPutAllByValue([
+ obj2,
+ obj4,
+ obj5,
+ ]),
+ );
+ await qEqualSet(
+ isar.stringInsensitiveIndexModels.where().tFindAll(),
+ [obj3, obj4, obj5],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringInsensitiveIndexModels.tPutByValue(obj1),
+ );
+ await qEqualSet(
+ isar.stringInsensitiveIndexModels.where().tFindAll(),
+ [obj1, obj3, obj4],
+ );
+ });
+
+ isarTest('Put all by without items', () async {
+ final obj0 = BoolIndexModel(value: true, index: 0);
+ final obj1 = BoolIndexModel(value: true, index: 1);
+
+ await isar.tWriteTxn(
+ () => isar.boolIndexModels.tPutAllByValue([]),
+ );
+ await qEqualSet(
+ isar.boolIndexModels.where().tFindAll(),
+ [],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.boolIndexModels.tPut(obj0),
+ );
+ await qEqualSet(
+ isar.boolIndexModels.where().tFindAll(),
+ [obj0],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.boolIndexModels.tPutAllByValue([]),
+ );
+ await isar.tWriteTxn(
+ () => isar.boolIndexModels.tPutAllByValue([obj1]),
+ );
+ await isar.tWriteTxn(
+ () => isar.boolIndexModels.tPutAllByValue([]),
+ );
+
+ await qEqualSet(
+ isar.boolIndexModels.where().tFindAll(),
+ [obj1],
+ );
+ });
+ });
+}
+
+// Extension methods for collections, in order to use
+// `tPutByValue` / `tPutAllByValue` on them
+extension TBoolIndexModelCollectionExt on IsarCollection<BoolIndexModel> {
+ Future<int> tPutByValue(BoolIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<BoolIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}
+
+extension TIntIndexModelCollectionExt on IsarCollection<IntIndexModel> {
+ Future<int> tPutByValue(IntIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<IntIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}
+
+extension TDoubleIndexModelCollectionExt on IsarCollection<DoubleIndexModel> {
+ Future<int> tPutByValue(DoubleIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<DoubleIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}
+
+extension TStringValueIndexModelCollectionExt
+ on IsarCollection<StringValueIndexModel> {
+ Future<int> tPutByValue(StringValueIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<StringValueIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}
+
+extension TStringHashIndexModelCollectionExt
+ on IsarCollection<StringHashIndexModel> {
+ Future<int> tPutByValue(StringHashIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<StringHashIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}
+
+extension TStringInsensitiveIndexModelCollectionExt
+ on IsarCollection<StringInsensitiveIndexModel> {
+ Future<int> tPutByValue(StringInsensitiveIndexModel obj) {
+ if (syncTest) {
+ return SynchronousFuture(putByValueSync(obj));
+ }
+ return putByValue(obj);
+ }
+
+ Future<List<int>> tPutAllByValue(List<StringInsensitiveIndexModel> objs) {
+ if (syncTest) {
+ return SynchronousFuture(putAllByValueSync(objs));
+ }
+ return putAllByValue(objs);
+ }
+}

original_message: Implemented put by index test


message: add item in lists

diff:
@@ -15,6 +15,7 @@ abstract class _IsarConnect {
ConnectAction.removeQuery: _removeQuery,
ConnectAction.exportJson: _exportJson,
ConnectAction.editProperty: _editProperty,
+ ConnectAction.addInList: _addInList,
};
static bool _initialized = false;
@@ -173,9 +174,7 @@ abstract class _IsarConnect {
return query.exportJson();
}
- static Future<bool> _editProperty(
- Map<String, dynamic> params,
- ) async {
+ static Future<void> _editProperty(Map<String, dynamic> params) async {
final cEdit = ConnectEdit.fromJson(params);
final collection = Isar.getInstance(cEdit.instance)!
.getCollectionByNameInternal(cEdit.collection)!;
@@ -198,7 +197,32 @@ abstract class _IsarConnect {
}
await collection.isar.writeTxn(() async => collection.importJson(objects));
- return true;
+ }
+
+ static Future<void> _addInList(Map<String, dynamic> params) async {
+ final cEdit = ConnectEdit.fromJson(params);
+ final collection = Isar.getInstance(cEdit.instance)!
+ .getCollectionByNameInternal(cEdit.collection)!;
+
+ final query = collection.buildQuery<dynamic>(
+ whereClauses: [IdWhereClause.equalTo(value: cEdit.id)],
+ );
+
+ final objects = await query.exportJson();
+
+ if (objects.isEmpty ||
+ !objects[0].containsKey(cEdit.property) ||
+ objects[0][cEdit.property] is! List) {
+ throw IsarError('Cant get object or property is wrong for add');
+ }
+
+ if (cEdit.index == null) {
+ (objects[0][cEdit.property] as List).add(cEdit.value);
+ } else {
+ (objects[0][cEdit.property] as List).insert(cEdit.index!, cEdit.value);
+ }
+
+ await collection.isar.writeTxn(() async => collection.importJson(objects));
}
static Query<dynamic> _getQuery(Map<String, dynamic> params) {

diff:
@@ -12,7 +12,8 @@ enum ConnectAction {
removeQuery('ext.isar.removeQuery'),
exportQuery('ext.isar.exportQuery'),
exportJson('ext.isar.exportJson'),
- editProperty('ext.isar.editProperty');
+ editProperty('ext.isar.editProperty'),
+ addInList('ext.isar.addInList');
const ConnectAction(this.method);
@@ -56,6 +57,7 @@ class ConnectQuery {
: null,
);
}
+
final String instance;
final String collection;
final FilterOperation? filter;

diff:
@@ -23,6 +23,7 @@ typedef Editor = void Function(
String property,
int? index,
dynamic value,
+ bool editing,
);
class QueryTable extends ConsumerWidget {
@@ -55,7 +56,7 @@ class QueryTable extends ConsumerWidget {
),
collection: collection,
object: objects[index],
- editor: (id, property, index, value) {
+ editor: (id, property, index, value, editing) {
final edit = ConnectEdit(
instance: ref.read(selectedInstancePod).value!,
collection: collection.name,
@@ -64,7 +65,12 @@ class QueryTable extends ConsumerWidget {
index: index,
value: value,
);
+
+ if (editing) {
ref.read(isarConnectPod.notifier).editProperty(edit);
+ } else {
+ ref.read(isarConnectPod.notifier).addInList(edit);
+ }
},
),
Align(
@@ -272,16 +278,64 @@ class _TableItemState extends State<TableItem> {
),
if (!widget.data.property.isId &&
!widget.data.property.type.isList)
+ PopupMenuItem<dynamic>(
+ onTap: () {
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) => _edit(),
+ );
+ },
+ child: PopUpMenuRow(
+ icon: const Icon(
+ Icons.edit,
+ size: PopUpMenuRow.iconSize,
+ ),
+ text: widget.data.index != null
+ ? 'Edit item ${widget.data.index!}'
+ : 'Edit property',
+ ),
+ ),
+ if (widget.data.property.type.isList &&
+ widget.data.value != null)
PopupMenuItem<dynamic>(
onTap: () {
WidgetsBinding.instance
- .addPostFrameCallback((_) => _edit());
+ .addPostFrameCallback((_) => _addInList(null));
},
child: const PopUpMenuRow(
- icon: Icon(Icons.edit, size: PopUpMenuRow.iconSize),
- text: 'Edit item',
+ icon: Icon(Icons.add, size: PopUpMenuRow.iconSize),
+ text: 'Add item',
+ ),
+ ),
+ if (widget.data.index != null) ...[
+ PopupMenuItem<dynamic>(
+ onTap: () {
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) => _addInList(widget.data.index),
+ );
+ },
+ child: PopUpMenuRow(
+ icon: const Icon(
+ Icons.add,
+ size: PopUpMenuRow.iconSize,
+ ),
+ text: 'Add item before ${widget.data.index!}',
+ ),
+ ),
+ PopupMenuItem<dynamic>(
+ onTap: () {
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) => _addInList(widget.data.index! + 1),
+ );
+ },
+ child: PopUpMenuRow(
+ icon: const Icon(
+ Icons.add,
+ size: PopUpMenuRow.iconSize,
+ ),
+ text: 'Add item after ${widget.data.index!}',
),
),
+ ],
if (widget.data.property.type.isList)
PopupMenuItem<dynamic>(
onTap: () => _setValue(<dynamic>[]),
@@ -324,6 +378,32 @@ class _TableItemState extends State<TableItem> {
}
}
+ Future<void> _addInList(int? index) async {
+ final result = await showDialog<Map<String, dynamic>?>(
+ context: context,
+ builder: (context) {
+ return AlertDialog(
+ content: EditPopup(
+ type: widget.data.property.type.isList
+ ? widget.data.property.type.childType
+ : widget.data.property.type,
+ value: null,
+ ),
+ );
+ },
+ );
+
+ if (result != null) {
+ widget.editor(
+ widget.objectId,
+ widget.data.property.name,
+ index,
+ result['value'],
+ false,
+ );
+ }
+ }
+
Future<void> _copy() async {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
@@ -356,6 +436,7 @@ class _TableItemState extends State<TableItem> {
widget.data.property.name,
widget.data.index,
value,
+ true,
);
}

diff:
@@ -181,6 +181,14 @@ class IsarConnectStateNotifier
);
}
+ Future<void> addInList(ConnectEdit edit) async {
+ await _call<dynamic>(
+ ConnectAction.addInList,
+ args: edit.toJson(),
+ timeout: kLongTimeout,
+ );
+ }
+
Future<void> disconnect() async {
await state.value?.vmService.dispose();
}

original_message: add item in lists


message: remove item from list

diff:
@@ -16,6 +16,7 @@ abstract class _IsarConnect {
ConnectAction.exportJson: _exportJson,
ConnectAction.editProperty: _editProperty,
ConnectAction.addInList: _addInList,
+ ConnectAction.removeFromList: _removeFromList,
};
static bool _initialized = false;
@@ -280,6 +281,27 @@ abstract class _IsarConnect {
await collection.isar.writeTxn(() async => collection.importJson(objects));
}
+ static Future<void> _removeFromList(Map<String, dynamic> params) async {
+ final cEdit = ConnectEdit.fromJson(params);
+ final collection = Isar.getInstance(cEdit.instance)!
+ .getCollectionByNameInternal(cEdit.collection)!;
+
+ final query = collection.buildQuery<dynamic>(
+ whereClauses: [IdWhereClause.equalTo(value: cEdit.id)],
+ );
+
+ final objects = await query.exportJson();
+
+ if (objects.isEmpty ||
+ !objects[0].containsKey(cEdit.property) ||
+ objects[0][cEdit.property] is! List) {
+ throw IsarError('Cant get object or property is wrong for remove');
+ }
+
+ (objects[0][cEdit.property] as List).removeAt(cEdit.index!);
+ await collection.isar.writeTxn(() async => collection.importJson(objects));
+ }
+
static Query<dynamic> _getQuery(ConnectQuery query) {
final collection = Isar.getInstance(query.instance)!
.getCollectionByNameInternal(query.collection)!;

diff:
@@ -13,7 +13,8 @@ enum ConnectAction {
exportQuery('ext.isar.exportQuery'),
exportJson('ext.isar.exportJson'),
editProperty('ext.isar.editProperty'),
- addInList('ext.isar.addInList');
+ addInList('ext.isar.addInList'),
+ removeFromList('ext.isar.removeFromList');
const ConnectAction(this.method);

diff:
@@ -23,9 +23,11 @@ typedef Editor = void Function(
String property,
int? index,
dynamic value,
- bool editing,
+ EditorType editing,
);
+enum EditorType { add, edit, remove }
+
class QueryTable extends ConsumerWidget {
const QueryTable({super.key});
@@ -67,10 +69,20 @@ class QueryTable extends ConsumerWidget {
value: value,
);
- if (editing) {
- ref.read(isarConnectPod.notifier).editProperty(edit);
- } else {
+ switch (editing) {
+ case EditorType.add:
ref.read(isarConnectPod.notifier).addInList(edit);
+ break;
+
+ case EditorType.edit:
+ ref.read(isarConnectPod.notifier).editProperty(edit);
+ break;
+
+ case EditorType.remove:
+ ref
+ .read(isarConnectPod.notifier)
+ .removeFromList(edit);
+ break;
}
},
),
@@ -360,6 +372,19 @@ class _TableItemState extends State<TableItem> {
: 'Edit property',
),
),
+ if (widget.data is PropertyHelper &&
+ !(widget.data as PropertyHelper).subLink &&
+ widget.data.index != null)
+ PopupMenuItem<dynamic>(
+ onTap: _removeListItem,
+ child: PopUpMenuRow(
+ icon: const Icon(
+ Icons.delete,
+ size: PopUpMenuRow.iconSize,
+ ),
+ text: 'Remove item ${widget.data.index!}',
+ ),
+ ),
if (widget.data is PropertyHelper &&
!(widget.data as PropertyHelper).subLink &&
(widget.data as PropertyHelper).property.type.isList &&
@@ -495,7 +520,7 @@ class _TableItemState extends State<TableItem> {
property.name,
index,
result['value'],
- false,
+ EditorType.add,
);
}
}
@@ -550,7 +575,17 @@ class _TableItemState extends State<TableItem> {
(widget.data as PropertyHelper).property.name,
widget.data.index,
value,
- true,
+ EditorType.edit,
+ );
+ }
+
+ Future<void> _removeListItem() async {
+ widget.editor(
+ widget.objectId,
+ (widget.data as PropertyHelper).property.name,
+ widget.data.index,
+ null,
+ EditorType.remove,
);
}

diff:
@@ -189,6 +189,14 @@ class IsarConnectStateNotifier
);
}
+ Future<void> removeFromList(ConnectEdit edit) async {
+ await _call<dynamic>(
+ ConnectAction.removeFromList,
+ args: edit.toJson(),
+ timeout: kLongTimeout,
+ );
+ }
+
Future<void> disconnect() async {
await state.value?.vmService.dispose();
}

original_message: remove item from list


message: fix bug in tree view

diff:
@@ -230,6 +230,7 @@ class _TableBlockState extends State<TableBlock> {
Node(
key: '$prefixKey${property.name}_$index',
label: '',
+ expanded: _isExpanded('$prefixKey${property.name}_$index'),
data: PropertyHelper(
property: IProperty(
name: property.name,
@@ -245,12 +246,9 @@ class _TableBlockState extends State<TableBlock> {
}
return Node<TreeViewHelper>(
- key: '${prefixKey}_${property.name}',
+ key: '$prefixKey${property.name}',
label: '',
- expanded: _treeViewController
- .getNode<TreeViewHelper>(property.name)
- ?.expanded ??
- false,
+ expanded: _isExpanded('$prefixKey${property.name}'),
children: children,
data: PropertyHelper(
property: property,
@@ -285,6 +283,7 @@ class _TableBlockState extends State<TableBlock> {
Node<TreeViewHelper>(
key: '${link.name}_$index',
label: '',
+ expanded: _isExpanded('${link.name}_$index'),
children: _createProperties(
properties: link.target.allProperties,
data: list[index] as Map<String, dynamic>,
@@ -304,9 +303,7 @@ class _TableBlockState extends State<TableBlock> {
return Node<TreeViewHelper>(
key: link.name,
label: '',
- expanded:
- _treeViewController.getNode<TreeViewHelper>(link.name)?.expanded ??
- false,
+ expanded: _isExpanded(link.name),
children: children,
data: LinkHelper(
link: link,
@@ -315,6 +312,10 @@ class _TableBlockState extends State<TableBlock> {
);
}).toList();
}
+
+ bool _isExpanded(String key) {
+ return _treeViewController.getNode<TreeViewHelper>(key)?.expanded ?? false;
+ }
}
class TableItem extends StatefulWidget {

original_message: fix bug in tree view


message:
added links in query builder
some fixes and cleanup in query builder

diff:
@@ -93,6 +93,12 @@ class ConnectQuery {
type: FilterGroupType.values[json['type'] as int],
filters: filters,
);
+ } else if (json.containsKey('linkName')) {
+ return LinkFilter(
+ filter: _filterFromJson(json['filter'] as Map<String, dynamic>)!,
+ linkName: json['linkName'] as String,
+ targetCollection: json['targetCollection'] as String,
+ );
} else {
return FilterCondition(
type: FilterConditionType.values[json['type'] as int],
@@ -122,6 +128,12 @@ class ConnectQuery {
'type': filter.type.index,
'filters': filter.filters.map(_filterToJson).toList(),
};
+ } else if (filter is LinkFilter) {
+ return {
+ 'filter': _filterToJson(filter.filter),
+ 'targetCollection': filter.targetCollection,
+ 'linkName': filter.linkName,
+ };
} else {
throw UnimplementedError();
}

diff:
@@ -24,24 +24,18 @@ class QueryBuilderUI extends StatefulWidget {
for (final item in group.children) {
if (item is QueryBuilderUIGroupHelper) {
filters.add(parseQuery(item));
- } else {
- filters.add(_parseCondition(item as _ConditionHelper));
+ } else if (item is _ConditionHelper) {
+ filters.add(_parseCondition(item));
+ } else if (item is _LinkHelper) {
+ filters.add(_parseLink(item));
}
}
- FilterOperation filter;
+ final filter = FilterGroup(
+ filters: filters,
+ type: group.operation,
+ );
- switch (group.operation) {
- case QueryBuilderUIOP.and:
- filter = FilterGroup.and(filters);
- break;
- case QueryBuilderUIOP.or:
- filter = FilterGroup.or(filters);
- break;
- case QueryBuilderUIOP.xor:
- filter = FilterGroup.xor(filters);
- break;
- }
return group.not ? FilterGroup.not(filter) : filter;
}
@@ -114,6 +108,24 @@ class QueryBuilderUI extends StatefulWidget {
return condition.not ? FilterGroup.not(ret) : ret;
}
+
+ static FilterOperation _parseLink(_LinkHelper link) {
+ final filters = <FilterOperation>[];
+
+ for (final item in link.children) {
+ if (item is QueryBuilderUIGroupHelper) {
+ filters.add(parseQuery(item));
+ } else if (item is _ConditionHelper) {
+ filters.add(_parseCondition(item));
+ }
+ }
+
+ return LinkFilter(
+ filter: FilterGroup(filters: filters, type: link.operation),
+ linkName: link.link.name,
+ targetCollection: link.link.target.name,
+ );
+ }
}
class _QueryBuilderUIState extends State<QueryBuilderUI> {
@@ -126,10 +138,9 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
super.initState();
if (widget.filter != null) {
- _filter = QueryBuilderUIGroupHelper(widget.filter!.operation)
- ..children.addAll(widget.filter!.children);
+ _filter = widget.filter!.clone();
} else {
- _filter = QueryBuilderUIGroupHelper(QueryBuilderUIOP.and);
+ _filter = QueryBuilderUIGroupHelper();
}
_sortProps =
@@ -170,10 +181,7 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
onPressed: () {
Navigator.pop(
context,
- {
- 'filter': QueryBuilderUIGroupHelper(QueryBuilderUIOP.and),
- 'sort': null
- },
+ {'filter': QueryBuilderUIGroupHelper(), 'sort': null},
);
},
child: const Text('Clear'),
@@ -258,14 +266,16 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
class _GroupUI extends StatefulWidget {
const _GroupUI({
required this.helper,
+ required this.collection,
this.index,
this.removeItem,
- required this.collection,
+ this.link,
});
final QueryBuilderUIGroupHelper helper;
final ICollection collection;
final int? index;
+ final ILink? link;
final void Function(int index)? removeItem;
@override
@@ -287,10 +297,10 @@ class _GroupUIState extends State<_GroupUI> {
_GroupButton(
onTap: () {
setState(() {
- widget.helper.operation = QueryBuilderUIOP.and;
+ widget.helper.operation = FilterGroupType.and;
});
},
- color: widget.helper.operation == QueryBuilderUIOP.and
+ color: widget.helper.operation == FilterGroupType.and
? Colors.blue
: null,
child: const Text('And'),
@@ -299,10 +309,10 @@ class _GroupUIState extends State<_GroupUI> {
_GroupButton(
onTap: () {
setState(() {
- widget.helper.operation = QueryBuilderUIOP.or;
+ widget.helper.operation = FilterGroupType.or;
});
},
- color: widget.helper.operation == QueryBuilderUIOP.or
+ color: widget.helper.operation == FilterGroupType.or
? Colors.blue
: null,
child: const Text('Or'),
@@ -311,10 +321,10 @@ class _GroupUIState extends State<_GroupUI> {
_GroupButton(
onTap: () {
setState(() {
- widget.helper.operation = QueryBuilderUIOP.xor;
+ widget.helper.operation = FilterGroupType.xor;
});
},
- color: widget.helper.operation == QueryBuilderUIOP.xor
+ color: widget.helper.operation == FilterGroupType.xor
? Colors.blue
: null,
child: const Text('Xor'),
@@ -330,6 +340,18 @@ class _GroupUIState extends State<_GroupUI> {
},
),
const Spacer(),
+ if (widget.link == null && widget.collection.links.isNotEmpty)
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ widget.helper.children.add(
+ _LinkHelper(widget.collection.links.first),
+ );
+ });
+ },
+ icon: const Icon(Icons.link),
+ label: const Text('Link'),
+ ),
TextButton.icon(
onPressed: () {
setState(() {
@@ -344,9 +366,7 @@ class _GroupUIState extends State<_GroupUI> {
TextButton.icon(
onPressed: () {
setState(() {
- widget.helper.children.add(
- QueryBuilderUIGroupHelper(QueryBuilderUIOP.and),
- );
+ widget.helper.children.add(QueryBuilderUIGroupHelper());
});
},
icon: const Icon(Icons.add),
@@ -383,6 +403,182 @@ class _GroupUIState extends State<_GroupUI> {
_GroupUI(
helper: widget.helper.children[index] as QueryBuilderUIGroupHelper,
collection: widget.collection,
+ link: widget.link,
+ removeItem: _removeItem,
+ index: index,
+ ),
+ );
+ } else if (widget.helper.children[index] is _ConditionHelper) {
+ children.add(
+ _ConditionUI(
+ helper: widget.helper.children[index] as _ConditionHelper,
+ collection: widget.collection,
+ link: widget.link,
+ removeItem: _removeItem,
+ index: index,
+ ),
+ );
+ } else if (widget.helper.children[index] is _LinkHelper) {
+ children.add(
+ _LinkUI(
+ helper: widget.helper.children[index] as _LinkHelper,
+ collection: widget.collection,
+ removeItem: _removeItem,
+ index: index,
+ ),
+ );
+ }
+
+ children.add(const SizedBox(height: 15));
+ }
+ return children;
+ }
+
+ void _removeItem(int index) {
+ setState(() {
+ widget.helper.children.removeAt(index);
+ });
+ }
+}
+
+class _LinkUI extends StatefulWidget {
+ const _LinkUI({
+ required this.helper,
+ required this.index,
+ required this.removeItem,
+ required this.collection,
+ });
+
+ final _LinkHelper helper;
+ final ICollection collection;
+ final int index;
+ final void Function(int index) removeItem;
+
+ @override
+ State<_LinkUI> createState() => _LinkUIState();
+}
+
+class _LinkUIState extends State<_LinkUI> {
+ @override
+ Widget build(BuildContext context) {
+ return IsarCard(
+ side: const BorderSide(color: Colors.green),
+ radius: BorderRadius.circular(10),
+ child: Padding(
+ padding: const EdgeInsets.all(15),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ DropdownButtonHideUnderline(
+ child: DropdownButton<ILink>(
+ value: widget.helper.link,
+ items: widget.collection.links.map((e) {
+ return DropdownMenuItem(value: e, child: Text(e.name));
+ }).toList(),
+ onChanged: widget.helper.children.isNotEmpty
+ ? null
+ : (link) {
+ if (link != null) {
+ setState(() {
+ widget.helper.link = link;
+ });
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 5),
+ _GroupButton(
+ onTap: () {
+ setState(() {
+ widget.helper.operation = FilterGroupType.and;
+ });
+ },
+ color: widget.helper.operation == FilterGroupType.and
+ ? Colors.blue
+ : null,
+ child: const Text('And'),
+ ),
+ const SizedBox(width: 5),
+ _GroupButton(
+ onTap: () {
+ setState(() {
+ widget.helper.operation = FilterGroupType.or;
+ });
+ },
+ color: widget.helper.operation == FilterGroupType.or
+ ? Colors.blue
+ : null,
+ child: const Text('Or'),
+ ),
+ const SizedBox(width: 5),
+ _GroupButton(
+ onTap: () {
+ setState(() {
+ widget.helper.operation = FilterGroupType.xor;
+ });
+ },
+ color: widget.helper.operation == FilterGroupType.xor
+ ? Colors.blue
+ : null,
+ child: const Text('Xor'),
+ ),
+ const Spacer(),
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ widget.helper.children.add(
+ _ConditionHelper(
+ widget.helper.link.target.allProperties.first,
+ ),
+ );
+ });
+ },
+ icon: const Icon(Icons.add),
+ label: const Text('Condition'),
+ ),
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ widget.helper.children.add(
+ QueryBuilderUIGroupHelper(),
+ );
+ });
+ },
+ icon: const Icon(Icons.add),
+ label: const Text('(Group)'),
+ ),
+ IconButton(
+ color: Colors.blue,
+ onPressed: () {
+ widget.removeItem(widget.index);
+ },
+ icon: const Icon(Icons.clear),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 50, top: 20),
+ child: Column(
+ children: _generateChildren(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ List<Widget> _generateChildren() {
+ final children = <Widget>[];
+
+ for (var index = 0; index < widget.helper.children.length; index++) {
+ if (widget.helper.children[index] is QueryBuilderUIGroupHelper) {
+ children.add(
+ _GroupUI(
+ helper: widget.helper.children[index] as QueryBuilderUIGroupHelper,
+ collection: widget.collection,
+ link: widget.helper.link,
removeItem: _removeItem,
index: index,
),
@@ -392,6 +588,7 @@ class _GroupUIState extends State<_GroupUI> {
_ConditionUI(
helper: widget.helper.children[index] as _ConditionHelper,
collection: widget.collection,
+ link: widget.helper.link,
removeItem: _removeItem,
index: index,
),
@@ -416,18 +613,23 @@ class _ConditionUI extends StatefulWidget {
required this.index,
required this.removeItem,
required this.collection,
+ this.link,
});
final _ConditionHelper helper;
final int index;
final void Function(int index) removeItem;
final ICollection collection;
+ final ILink? link;
@override
State<_ConditionUI> createState() => _ConditionUIState();
}
class _ConditionUIState extends State<_ConditionUI> {
+ late final properties =
+ widget.link?.target.allProperties ?? widget.collection.allProperties;
+
@override
Widget build(BuildContext context) {
return Row(
@@ -435,7 +637,7 @@ class _ConditionUIState extends State<_ConditionUI> {
DropdownButtonHideUnderline(
child: DropdownButton<IProperty>(
value: widget.helper.property,
- items: widget.collection.allProperties
+ items: properties
.map((e) => DropdownMenuItem(value: e, child: Text(e.name)))
.toList(),
onChanged: (property) {
@@ -582,16 +784,25 @@ class _GroupButton extends StatelessWidget {
}
}
-enum QueryBuilderUIOP { and, or, xor }
-
-abstract class QueryBuilderUIHelper {}
+//ignore: one_member_abstracts
+abstract class QueryBuilderUIHelper {
+ QueryBuilderUIHelper clone();
+}
class QueryBuilderUIGroupHelper extends QueryBuilderUIHelper {
- QueryBuilderUIGroupHelper(this.operation);
+ QueryBuilderUIGroupHelper();
- QueryBuilderUIOP operation;
+ FilterGroupType operation = FilterGroupType.and;
bool not = false;
final children = <QueryBuilderUIHelper>[];
+
+ @override
+ QueryBuilderUIGroupHelper clone() {
+ return QueryBuilderUIGroupHelper()
+ ..operation = operation
+ ..not = not
+ ..children.addAll(children.map((e) => e.clone()).toList());
+ }
}
enum _ConditionType {
@@ -624,6 +835,16 @@ class _ConditionHelper extends QueryBuilderUIHelper {
IProperty get property => _property;
+ @override
+ _ConditionHelper clone() {
+ return _ConditionHelper(property)
+ ..not = not
+ ..caseSensitive = caseSensitive
+ ..boolValue = boolValue
+ ..type = type
+ ..controller = controller;
+ }
+
set property(IProperty property) {
_property = property;
not = false;
@@ -753,3 +974,18 @@ class _ConditionHelper extends QueryBuilderUIHelper {
}
}
}
+
+class _LinkHelper extends QueryBuilderUIHelper {
+ _LinkHelper(this.link);
+
+ FilterGroupType operation = FilterGroupType.and;
+ ILink link;
+ final children = <QueryBuilderUIHelper>[];
+
+ @override
+ _LinkHelper clone() {
+ return _LinkHelper(link)
+ ..operation = operation
+ ..children.addAll(children.map((e) => e.clone()).toList());
+ }
+}

diff:
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';
@@ -55,6 +57,7 @@ class ICollection {
];
}
+@immutable
class IProperty {
const IProperty({
required this.name,
@@ -76,6 +79,18 @@ class IProperty {
final IsarType type;
final bool isId;
final bool isIndex;
+
+ @override
+ bool operator ==(Object other) {
+ return other is IProperty &&
+ other.name == name &&
+ other.type == type &&
+ other.isId == isId &&
+ other.isIndex == isIndex;
+ }
+
+ @override
+ int get hashCode => Object.hash(name, type, isId, isIndex);
}
class ILink {


message: query aggregation

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

diff:
@@ -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
};
}

diff:
@@ -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 = '';

diff:
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;
+ }
}

diff:
@@ -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();
}

original_message: query aggregation


message: Implemented query build test

change_type: ADD

diff:
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import '../util/common.dart';
+import '../util/sync_async_helper.dart';
+
+part 'query_build_test.g.dart';
+
+@Collection()
+class Model {
+ Model(this.name, this.index);
+
+ Id id = Isar.autoIncrement;
+
+ @Index()
+ final int index;
+
+ final String name;
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Model &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ index == other.index &&
+ name == other.name;
+}
+
+void main() {
+ group('Query build', () {
+ late Isar isar;
+
+ late Model obj0;
+ late Model obj1;
+ late Model obj2;
+ late Model obj3;
+ late Model obj4;
+ late Model obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([ModelSchema]);
+
+ obj0 = Model('obj0', 0);
+ obj1 = Model('obj1', 1);
+ obj2 = Model('obj2', 2);
+ obj3 = Model('obj3', 3);
+ obj4 = Model('obj4', 4);
+ obj5 = Model('obj5', 5);
+
+ await isar.tWriteTxn(
+ () => isar.models.tPutAll([obj0, obj1, obj2, obj3, obj4, obj5]),
+ );
+ });
+
+ tearDown(() => isar.close());
+
+ isarTest('Build and query', () async {
+ final query1 = isar.models
+ .where()
+ .indexGreaterThan(2)
+ .filter()
+ .nameContains('5')
+ .or()
+ .nameContains('3')
+ .build();
+
+ await qEqualSet(
+ query1.tFindAll(),
+ [obj3, obj5],
+ );
+
+ final query2 = isar.models.where().build();
+
+ await qEqualSet(
+ query2.tFindAll(),
+ [obj0, obj1, obj2, obj3, obj4, obj5],
+ );
+
+ final query3 = isar.models.where().sortByNameDesc().build();
+
+ await qEqual(
+ query3.tFindAll(),
+ [obj5, obj4, obj3, obj2, obj1, obj0],
+ );
+
+ final query4 = isar.models.where().sortByIndex().build();
+
+ await qEqual(
+ query4.tFindAll(),
+ [obj0, obj1, obj2, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Build and reuse query', () async {
+ final query1 = isar.models
+ .where()
+ .indexLessThan(4)
+ .sortByIndexDesc()
+ .thenByIndexDesc()
+ .thenByIndexDesc()
+ .build();
+
+ await qEqualSet(
+ query1.tFindAll(),
+ [obj3, obj2, obj1, obj0],
+ );
+
+ await qEqualSet(
+ query1.tFindAll(),
+ [obj3, obj2, obj1, obj0],
+ );
+
+ await qEqualSet(
+ query1.tFindAll(),
+ [obj3, obj2, obj1, obj0],
+ );
+
+ await qEqualSet(
+ query1.tFindAll(),
+ [obj3, obj2, obj1, obj0],
+ );
+
+ final query2 =
+ isar.models.where().indexEqualTo(3).or().indexEqualTo(1).build();
+ final query3 = isar.models.where().build();
+ final query4 =
+ isar.models.filter().nameContains('4').or().nameContains('0').build();
+
+ await qEqualSet(
+ query2.tFindAll(),
+ [obj1, obj3],
+ );
+
+ await qEqualSet(
+ query3.tFindAll(),
+ [obj0, obj1, obj2, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query4.tFindAll(), [obj0, obj4]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query3.tFindAll(), [obj0, obj1, obj2, obj3, obj4, obj5]);
+ await qEqualSet(query3.tFindAll(), [obj0, obj1, obj2, obj3, obj4, obj5]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query4.tFindAll(), [obj0, obj4]);
+ await qEqualSet(query4.tFindAll(), [obj0, obj4]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query4.tFindAll(), [obj0, obj4]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query3.tFindAll(), [obj0, obj1, obj2, obj3, obj4, obj5]);
+ await qEqualSet(query2.tFindAll(), [obj1, obj3]);
+ await qEqualSet(query4.tFindAll(), [obj0, obj4]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ await qEqualSet(query1.tFindAll(), [obj3, obj2, obj1, obj0]);
+ });
+ });
+}

original_message: Implemented query build test


message: Added missing property in schema json

diff:
@@ -255,8 +255,8 @@ void main() {
}
],
'links': [
- {'name': r'$dollar$Link', 'target': r'$Dollar$Model'},
- {'name': r'$dollar$Links', 'target': r'$Dollar$Model'}
+ {'name': r'$dollar$Link', 'target': r'$Dollar$Model', 'single': true},
+ {'name': r'$dollar$Links', 'target': r'$Dollar$Model', 'single': false}
]
});
});
@@ -708,10 +708,10 @@ void main() {
}
],
'links': [
- {'name': 'link', 'target': 'SchemaTestModel'},
- {'name': 'links', 'target': 'SchemaTestModel'},
- {'name': 'renamedLink', 'target': 'SchemaTestModel'},
- {'name': 'renamedLinks', 'target': 'SchemaTestModel'}
+ {'name': 'link', 'target': 'SchemaTestModel', 'single': true},
+ {'name': 'links', 'target': 'SchemaTestModel', 'single': false},
+ {'name': 'renamedLink', 'target': 'SchemaTestModel', 'single': true},
+ {'name': 'renamedLinks', 'target': 'SchemaTestModel', 'single': false}
]
},
);

original_message: Added missing property in schema json


message: Implemented collection size test

diff:
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import 'util/common.dart';
+import 'util/sync_async_helper.dart';
+
+part 'collection_size_test.g.dart';
+
+@Collection()
+class ModelA {
+ ModelA({
+ required this.name,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index()
+ String name;
+
+ final bObj = IsarLink<ModelB>();
+ final bObjs = IsarLinks<ModelB>();
+
+ // We store a big buffer in order to better detect collection size changes,
+ // since reported collection size is based on block size (eg. 4096, 8192, ...)
+ final List<int> randomBuffer = List.filled(64000, 42);
+}
+
+@Collection()
+class ModelB {
+ Id id = Isar.autoIncrement;
+
+ final List<int> randomBuffer = List.filled(64000, 42);
+}
+
+void main() {
+ group('Collection size', () {
+ late Isar isar;
+
+ late ModelA objA0;
+ late ModelA objA1;
+ late ModelA objA2;
+ late ModelA objA3;
+ late ModelA objA4;
+ late ModelA objA5;
+
+ late ModelB objB0;
+ late ModelB objB1;
+ late ModelB objB2;
+ late ModelB objB3;
+ late ModelB objB4;
+ late ModelB objB5;
+
+ setUp(() async {
+ isar = await openTempIsar([ModelASchema, ModelBSchema]);
+
+ objA0 = ModelA(name: 'Obj A0');
+ objA1 = ModelA(name: 'Obj A1');
+ objA2 = ModelA(name: 'Obj A2');
+ objA3 = ModelA(name: 'Obj A3');
+ objA4 = ModelA(name: 'Obj A4');
+ objA5 = ModelA(name: 'Obj A5');
+
+ objB0 = ModelB();
+ objB1 = ModelB();
+ objB2 = ModelB();
+ objB3 = ModelB();
+ objB4 = ModelB();
+ objB5 = ModelB();
+ });
+
+ tearDown(() => isar.close());
+
+ isarTest('Size should start at 0', () async {
+ expect(await isar.modelAs.tGetSize(), 0);
+ expect(await isar.modelAs.tGetSize(), 0);
+
+ expect(await isar.modelAs.tGetSize(includeIndexes: true), 0);
+ expect(await isar.modelAs.tGetSize(includeIndexes: true), 0);
+
+ expect(await isar.modelAs.tGetSize(includeLinks: true), 0);
+ expect(await isar.modelAs.tGetSize(includeLinks: true), 0);
+
+ expect(
+ await isar.modelAs.tGetSize(includeIndexes: true, includeLinks: true),
+ 0,
+ );
+ expect(
+ await isar.modelAs.tGetSize(includeIndexes: true, includeLinks: true),
+ 0,
+ );
+ });
+
+ isarTest('Size should increase with more entries', () async {
+ await isar.tWriteTxn(() => isar.modelAs.tPut(objA0));
+ final sizeA0 = await isar.modelAs.tGetSize();
+ expect(sizeA0, greaterThan(0));
+
+ await isar.tWriteTxn(() => isar.modelAs.tPutAll([objA1, objA2, objA3]));
+ final sizeA1 = await isar.modelAs.tGetSize();
+ expect(sizeA1, greaterThan(sizeA0));
+
+ await isar.tWriteTxn(() => isar.modelAs.tPutAll([objA4, objA5]));
+ final sizeA2 = await isar.modelAs.tGetSize();
+ expect(sizeA2, greaterThan(sizeA1));
+
+ expect(await isar.modelBs.tGetSize(), 0);
+
+ await isar.tWriteTxn(() => isar.modelBs.tPut(objB0));
+ final sizeB0 = await isar.modelBs.tGetSize();
+ expect(sizeB0, greaterThan(0));
+
+ await isar.tWriteTxn(() => isar.modelBs.tPutAll([objB1, objB2, objB3]));
+ final sizeB1 = await isar.modelBs.tGetSize();
+ expect(sizeB1, greaterThanOrEqualTo(sizeB0));
+
+ await isar.tWriteTxn(() => isar.modelBs.tPutAll([objB4, objB5]));
+ final sizeB2 = await isar.modelBs.tGetSize();
+ expect(sizeB2, greaterThanOrEqualTo(sizeB1));
+ });
+
+ isarTest('includeIndexes should change size', () async {
+ await isar.tWriteTxn(() => isar.modelAs.tPutAll([objA0, objA1, objA3]));
+
+ final sizeAWithoutIndexes = await isar.modelAs.tGetSize();
+ final sizeAWithIndexes =
+ await isar.modelAs.tGetSize(includeIndexes: true);
+ expect(sizeAWithIndexes, greaterThan(sizeAWithoutIndexes));
+
+ await isar.tWriteTxn(() => isar.modelBs.tPutAll([objB0, objB3, objB4]));
+
+ final sizeBWithoutIndexes = await isar.modelBs.tGetSize();
+ final sizeBWithIndexes =
+ await isar.modelBs.tGetSize(includeIndexes: true);
+ // ModelB has no indexes, so should stay the same
+ expect(sizeBWithIndexes, sizeBWithoutIndexes);
+ });
+
+ isarTest('includeLinks should change size', () async {
+ await isar.tWriteTxn(
+ () => Future.wait([
+ isar.modelAs.tPutAll([objA0, objA1, objA2, objA3]),
+ isar.modelBs.tPutAll([objB0, objB1, objB2, objB3, objB4]),
+ ]),
+ );
+
+ objA1.bObj.value = objB0;
+ objA2.bObjs.addAll([objB0, objB1, objB4]);
+ objA3.bObj.value = objB0;
+ objA3.bObjs.addAll([objB0, objB1, objB3, objB4]);
+
+ final size1 = await isar.modelAs.tGetSize();
+ final size2 = await isar.modelAs.tGetSize(includeLinks: true);
+ expect(size1, size2);
+
+ await isar.tWriteTxn(
+ () => Future.wait([
+ objA1.bObj.tSave(),
+ objA2.bObjs.tSave(),
+ objA3.bObj.tSave(),
+ objA3.bObjs.tSave(),
+ ]),
+ );
+
+ final size3 = await isar.modelAs.tGetSize();
+ final size4 = await isar.modelAs.tGetSize(includeLinks: true);
+ expect(size3, lessThan(size4));
+ });
+
+ isarTest('includeIndexes and includeLinks should change size', () async {
+ await isar.tWriteTxn(
+ () => Future.wait([
+ isar.modelAs.tPutAll([objA0, objA1, objA2, objA3]),
+ isar.modelBs.tPutAll([objB0, objB1, objB2, objB3, objB4]),
+ ]),
+ );
+
+ objA1.bObj.value = objB0;
+ objA2.bObjs.addAll([objB0, objB1, objB4]);
+ objA3.bObj.value = objB0;
+ objA3.bObjs.addAll([objB0, objB1, objB3, objB4]);
+
+ await isar.tWriteTxn(
+ () => Future.wait([
+ objA1.bObj.tSave(),
+ objA2.bObjs.tSave(),
+ objA3.bObj.tSave(),
+ objA3.bObjs.tSave(),
+ ]),
+ );
+
+ final size = await isar.modelAs.tGetSize();
+ final sizeWithIndexes = await isar.modelAs.tGetSize(includeIndexes: true);
+ final sizeWithLinks = await isar.modelAs.tGetSize(includeLinks: true);
+ final sizeWithIndexesAndLinks = await isar.modelAs.tGetSize(
+ includeIndexes: true,
+ includeLinks: true,
+ );
+ expect(size, lessThan(sizeWithIndexes));
+ expect(size, lessThan(sizeWithLinks));
+ expect(sizeWithIndexes, lessThan(sizeWithIndexesAndLinks));
+ expect(sizeWithLinks, lessThan(sizeWithIndexesAndLinks));
+ });
+
+ isarTest('Delete should decrease size', () async {
+ await isar.tWriteTxn(
+ () => Future.wait([
+ isar.modelAs.tPutAll([objA0, objA1, objA2, objA3]),
+ isar.modelBs.tPutAll([objB0, objB1, objB2, objB3, objB4]),
+ ]),
+ );
+
+ final sizeA1 = await isar.modelAs.tGetSize();
+
+ await isar.tWriteTxn(() => isar.modelAs.tDelete(objA0.id));
+ final sizeA2 = await isar.modelAs.tGetSize();
+
+ expect(sizeA2, lessThan(sizeA1));
+
+ await isar.tWriteTxn(() => isar.modelAs.tClear());
+ final sizeA3 = await isar.modelAs.tGetSize();
+ expect(sizeA3, 0);
+
+ final sizeB1 = await isar.modelBs.tGetSize();
+
+ await isar.tWriteTxn(() => isar.modelBs.tDeleteAll([objB0.id, objB1.id]));
+ final sizeB2 = await isar.modelBs.tGetSize();
+
+ expect(sizeB2, lessThan(sizeB1));
+
+ await isar.tWriteTxn(() => isar.modelBs.tDeleteAll([objB2.id, objB3.id]));
+ final sizeB3 = await isar.modelBs.tGetSize();
+
+ expect(sizeB3, lessThan(sizeB2));
+ expect(sizeB3, greaterThan(0));
+ });
+
+ isarTest('Update should change size', () async {
+ await isar.tWriteTxn(
+ () => Future.wait([
+ isar.modelAs.tPutAll([objA0, objA1, objA2, objA3]),
+ isar.modelBs.tPutAll([objB0, objB1, objB2, objB3, objB4]),
+ ]),
+ );
+
+ final sizeA1 = await isar.modelAs.tGetSize();
+ final sizeB1 = await isar.modelBs.tGetSize();
+
+ objA0.name += String.fromCharCodes(List.filled(64000, 42));
+
+ await isar.tWriteTxn(() => isar.modelAs.tPut(objA0));
+ final sizeA2 = await isar.modelAs.tGetSize();
+ final sizeB2 = await isar.modelBs.tGetSize();
+
+ expect(sizeA2, greaterThan(sizeA1));
+ expect(sizeB2, sizeB1);
+
+ objA0.name += String.fromCharCodes(List.filled(64000, 42));
+ objA1.name += String.fromCharCodes(List.filled(64000, 42));
+
+ await isar.tWriteTxn(() => isar.modelAs.tPutAll([objA0, objA1]));
+ final sizeA3 = await isar.modelAs.tGetSize();
+ final sizeB3 = await isar.modelBs.tGetSize();
+
+ expect(sizeA3, greaterThan(sizeA2));
+ expect(sizeB3, sizeB2);
+ });
+ });
+}

diff:
@@ -135,6 +135,23 @@ extension TIsarCollection<OBJ> on IsarCollection<OBJ> {
return importJson(json);
}
}
+
+ Future<int> tGetSize({
+ bool includeIndexes = false,
+ bool includeLinks = false,
+ }) {
+ if (syncTest) {
+ return SynchronousFuture(getSizeSync(
+ includeIndexes: includeIndexes,
+ includeLinks: includeLinks,
+ ));
+ } else {
+ return getSize(
+ includeIndexes: includeIndexes,
+ includeLinks: includeLinks,
+ );
+ }
+ }
}
extension QueryBuilderExecute<OBJ, R>

original_message: Implemented collection size test


message: Implemented list length test

diff:
@@ -140,6 +140,11 @@ class QueryBuilderInternal<OBJ> {
int upper,
bool includeUpper,
) {
+ assert(lower >= 0, 'Lower bound must be positive.');
+ assert(
+ upper >= lower,
+ 'Upper bound must be positive and may no be less than lower bound.',
+ );
if (!includeLower) {
lower += 1;
}

diff:
@@ -145,7 +145,7 @@ class IsarAnalyzer {
properties.length) {
err(
'Two or more properties have the same name.',
- constructor.enclosingElement2,
+ constructor.enclosingElement,
);
}

diff:
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import '../util/common.dart';
+import '../util/sync_async_helper.dart';
+
+part 'filter_list_length_test.g.dart';
+
+@Collection()
+class Model {
+ Model({
+ required this.bools,
+ required this.ints,
+ required this.doubles,
+ required this.strings,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ final List<bool> bools;
+
+ @Index()
+ final List<int>? ints;
+
+ final List<double?> doubles;
+
+ final List<String?>? strings;
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Model &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ listEquals(bools, other.bools) &&
+ listEquals(ints, other.ints) &&
+ listEquals(doubles, other.doubles) &&
+ listEquals(strings, other.strings);
+
+ @override
+ String toString() {
+ return '''Model{id: $id, bools: $bools, ints: $ints, doubles: $doubles, strings: $strings}''';
+ }
+}
+
+void main() {
+ group('Filter list length', () {
+ late Isar isar;
+ late Model obj1;
+ late Model obj2;
+ late Model obj3;
+ late Model obj4;
+ late Model obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([ModelSchema]);
+
+ obj1 = Model(
+ bools: [],
+ ints: [1, 42, -128],
+ doubles: [1.123, 4, 5.333, 1.22, 1.22, 1.22],
+ strings: null,
+ );
+ obj2 = Model(
+ bools: [true, true, false],
+ ints: [4],
+ doubles: [null],
+ strings: ['Foo', 'bar'],
+ );
+ obj3 = Model(
+ bools: [true],
+ ints: [0, 123],
+ doubles: [3.141592653, null, null],
+ strings: ['a', 'b', 'c', 'd'],
+ );
+ obj4 = Model(
+ bools: [true, true, false, true],
+ ints: [-1, 1],
+ doubles: [4, 12.2, 12.43],
+ strings: ['', 'abc', null],
+ );
+ obj5 = Model(
+ bools: [true, true, true, true, true, false],
+ ints: [],
+ doubles: [],
+ strings: [null, null],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.models.tPutAll([obj1, obj2, obj3, obj4, obj5]),
+ );
+ });
+
+ tearDown(() => isar.close(deleteFromDisk: true));
+
+ isarTest('.lengthEqualTo()', () async {
+ await qEqualSet(
+ isar.models.filter().boolsLengthEqualTo(1).tFindAll(),
+ [obj3],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsLengthEqualTo(2).tFindAll(),
+ [obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesLengthEqualTo(6).tFindAll(),
+ [obj1],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesLengthEqualTo(42).tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .doublesLengthEqualTo(9223372036854775807)
+ .tFindAll(),
+ [],
+ );
+ });
+
+ isarTest('.lengthGreaterThan()', () async {
+ await qEqualSet(
+ isar.models.filter().boolsLengthGreaterThan(3).tFindAll(),
+ [obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().boolsLengthGreaterThan(4).tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .boolsLengthGreaterThan(4, include: true)
+ .tFindAll(),
+ [obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsLengthGreaterThan(3).tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.models.filter().boolsLengthGreaterThan(0).tFindAll(),
+ [obj2, obj3, obj4, obj5],
+ );
+
+ // FIXME: length of max i64 returns values with any length
+ // The problem seems to be that since greaterThan are includeLower: false
+ // by default, the lower becomes `lower += 1`, which overflows to min i64
+ // await qEqualSet(
+ // isar.models
+ // .filter()
+ // .stringsLengthGreaterThan(9223372036854775807)
+ // .tFindAll(),
+ // [],
+ // );
+ });
+
+ isarTest('.lengthLessThan()', () async {
+ await qEqualSet(
+ isar.models.filter().boolsLengthLessThan(3).tFindAll(),
+ [obj1, obj3],
+ );
+
+ await qEqualSet(
+ isar.models.filter().boolsLengthLessThan(3, include: true).tFindAll(),
+ [obj1, obj2, obj3],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsLengthLessThan(2).tFindAll(),
+ [obj2, obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesLengthLessThan(0, include: true).tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().stringsLengthLessThan(42).tFindAll(),
+ [obj2, obj3, obj4, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().stringsLengthLessThan(2).tFindAll(),
+ [],
+ );
+ });
+
+ isarTest('.lengthBetween()', () async {
+ await qEqualSet(
+ isar.models.filter().boolsLengthBetween(2, 6).tFindAll(),
+ [obj2, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .boolsLengthBetween(2, 6, includeUpper: false)
+ .tFindAll(),
+ [obj2, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsLengthBetween(0, 42).tFindAll(),
+ [obj1, obj2, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .intsLengthBetween(0, 42, includeLower: false)
+ .tFindAll(),
+ [obj1, obj2, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesLengthBetween(2, 3).tFindAll(),
+ [obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesLengthBetween(0, 0).tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .doublesLengthBetween(
+ 0,
+ 1,
+ includeLower: false,
+ )
+ .tFindAll(),
+ [obj2],
+ );
+
+ await qEqualSet(
+ isar.models.filter().stringsLengthLessThan(3).tFindAll(),
+ [obj2, obj5],
+ );
+ });
+
+ isarTest('.isEmpty() / .isNotEmpty()', () async {
+ await qEqualSet(
+ isar.models.filter().boolsIsEmpty().tFindAll(),
+ [obj1],
+ );
+
+ await qEqualSet(
+ isar.models.filter().boolsIsNotEmpty().tFindAll(),
+ [obj2, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsIsEmpty().tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().intsIsNotEmpty().tFindAll(),
+ [obj1, obj2, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesIsEmpty().tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.models.filter().doublesIsNotEmpty().tFindAll(),
+ [obj1, obj2, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models.filter().stringsIsEmpty().tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.models.filter().stringsIsNotEmpty().tFindAll(),
+ [obj2, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Grouped length filters', () async {
+ await qEqualSet(
+ isar.models
+ .filter()
+ .boolsIsEmpty()
+ .and()
+ .intsLengthGreaterThan(1)
+ .tFindAll(),
+ [obj1],
+ );
+
+ await qEqualSet(
+ isar.models.filter().boolsIsEmpty().and().boolsIsNotEmpty().tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .doublesLengthLessThan(5)
+ .and()
+ .doublesLengthGreaterThan(2)
+ .tFindAll(),
+ [obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .boolsIsNotEmpty()
+ .and()
+ .intsIsNotEmpty()
+ .and()
+ .doublesIsNotEmpty()
+ .and()
+ .stringsIsNotEmpty()
+ .tFindAll(),
+ [obj2, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.models
+ .filter()
+ .boolsLengthBetween(3, 5)
+ .or()
+ .intsIsEmpty()
+ .tFindAll(),
+ [obj2, obj4, obj5],
+ );
+ });
+ });
+}

diff:
@@ -141,10 +141,12 @@ extension TIsarCollection<OBJ> on IsarCollection<OBJ> {
bool includeLinks = false,
}) {
if (syncTest) {
- return SynchronousFuture(getSizeSync(
+ return SynchronousFuture(
+ getSizeSync(
includeIndexes: includeIndexes,
includeLinks: includeLinks,
- ));
+ ),
+ );
} else {
return getSize(
includeIndexes: includeIndexes,

original_message: Implemented list length test


message: Implemented copyToFile test

diff:
@@ -318,7 +318,7 @@ abstract class Isar {
/// This operation can only be performed while a database is being opened and
/// should only be used if absolutely necessary.
class CompactCondition {
- /// Compaction whill happen if all of the specified conditions are true.
+ /// Compaction will happen if all of the specified conditions are true.
const CompactCondition({
this.minFileSize,
this.minBytes,
@@ -332,7 +332,7 @@ class CompactCondition {
/// is highly discouraged to trigger compaction solely on this condition.
final int? minFileSize;
- /// The minumum number of bytes that can be freed with compaction.
+ /// The minimum number of bytes that can be freed with compaction.
final int? minBytes;
/// The minimum compaction ration. For example `2.0` would trigger compaction

diff:
+import 'dart:io';
+
+import 'package:isar/isar.dart';
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+
+import 'util/common.dart';
+import 'util/sync_async_helper.dart';
+
+part 'copy_to_file_test.g.dart';
+
+@Collection()
+class Model {
+ Id id = Isar.autoIncrement;
+
+ List<int> buffer = List.filled(16000, 42);
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Model &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ listEquals(buffer, other.buffer);
+
+ @override
+ String toString() {
+ return 'Model{id: $id}';
+ }
+}
+
+void main() {
+ group(
+ 'Copy to file',
+ skip: kIsWeb, // Unsupported on web
+ () {
+ late Isar isar;
+ late File originalDbFile;
+
+ late Directory directory;
+
+ setUp(() async {
+ isar = await openTempIsar([ModelSchema]);
+ originalDbFile = File(isar.path!);
+
+ await isar.tWriteTxn(
+ () => isar.models.tPutAll(List.filled(100, Model())),
+ );
+
+ directory = Directory('copy-to-file-test-database-copies');
+ await directory.create(recursive: true);
+ });
+
+ tearDown(() async {
+ await isar.close(deleteFromDisk: true);
+ await directory.delete(recursive: true);
+ });
+
+ isarTest('.copyToFile() should create a new file', () async {
+ final copiedDbFile = File(path.join(directory.path, getRandomName()));
+ expect(copiedDbFile.existsSync(), false);
+
+ await isar.copyToFile(copiedDbFile.path);
+
+ expect(copiedDbFile.existsSync(), true);
+ expect(copiedDbFile.lengthSync(), greaterThan(0));
+ });
+
+ isarTest('.copyToFile() should keep the same content', () async {
+ final copiedDbFilename = getRandomName();
+ final copiedDbFile = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename.isar',
+ ),
+ );
+
+ await isar.copyToFile(copiedDbFile.path);
+
+ final copiedIsar = await tOpen(
+ schemas: [ModelSchema],
+ directory: directory.path,
+ name: copiedDbFilename,
+ );
+ addTearDown(() => copiedIsar.close(deleteFromDisk: true));
+
+ final originalObjs = await isar.models.where().tFindAll();
+ await qEqualSet(
+ copiedIsar.models.where().tFindAll(),
+ originalObjs,
+ );
+ });
+
+ isarTest('.copyToFile() should compact copied file', () async {
+ await isar.tWriteTxn(() => isar.models.where().limit(50).tDeleteAll());
+
+ final copiedDbFilename1 = getRandomName();
+ final copiedDbFile1 = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename1.isar',
+ ),
+ );
+
+ await isar.copyToFile(copiedDbFile1.path);
+
+ final isarCopy1 = await tOpen(
+ schemas: [ModelSchema],
+ directory: directory.path,
+ name: copiedDbFilename1,
+ );
+ addTearDown(() => isarCopy1.close(deleteFromDisk: true));
+
+ expect(copiedDbFile1.lengthSync(), greaterThan(0));
+ expect(
+ copiedDbFile1.lengthSync(),
+ lessThan(originalDbFile.lengthSync()),
+ );
+
+ await isarCopy1.tWriteTxn(
+ () => isarCopy1.models.where().limit(25).tDeleteAll(),
+ );
+
+ final copiedDbFilename2 = getRandomName();
+ final copiedDbFile2 = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename2.isar',
+ ),
+ );
+
+ final isarCopy2 = await tOpen(
+ schemas: [ModelSchema],
+ directory: directory.path,
+ name: copiedDbFilename2,
+ );
+ addTearDown(() => isarCopy2.close(deleteFromDisk: true));
+
+ expect(copiedDbFile2.lengthSync(), greaterThan(0));
+ expect(
+ copiedDbFile2.lengthSync(),
+ lessThan(copiedDbFile1.lengthSync()),
+ );
+ });
+
+ isarTest('Copies should be the same size', () async {
+ final copiedDbFilename1 = getRandomName();
+ final copiedDbFile1 = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename1.isar',
+ ),
+ );
+
+ final copiedDbFilename2 = getRandomName();
+ final copiedDbFile2 = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename2.isar',
+ ),
+ );
+
+ await isar.copyToFile(copiedDbFile1.path);
+ await isar.copyToFile(copiedDbFile2.path);
+
+ expect(copiedDbFile1.lengthSync(), copiedDbFile2.lengthSync());
+
+ final isarCopy = await tOpen(
+ schemas: [ModelSchema],
+ directory: directory.path,
+ name: copiedDbFilename1,
+ );
+ addTearDown(() => isarCopy.close(deleteFromDisk: true));
+
+ final copiedDbFilename3 = getRandomName();
+ final copiedDbFile3 = File(
+ path.join(
+ directory.path,
+ '$copiedDbFilename3.isar',
+ ),
+ );
+
+ await isarCopy.copyToFile(copiedDbFile3.path);
+
+ expect(copiedDbFile3.lengthSync(), copiedDbFile1.lengthSync());
+ });
+ },
+ );
+}

original_message: Implemented copyToFile test


message: Added filter link nested test

diff:
@@ -398,6 +398,10 @@ void main() {
// FIXME: IsarError: Cannot perform this operation from within an active
// transaction.
// Are we suppose to a able to reset backlinks?
+ //
+ // It seems that a sync method is called in an async transaction
+ // Also seems to only happen on `.reset()` of backlinks
+
// await isar.tWriteTxn(() => target1.backlinks.tReset());
// await qEqualSet(
@@ -422,6 +426,10 @@ void main() {
// FIXME: IsarError: Cannot perform this operation from within an active
// transaction.
// Are we suppose to a able to reset backlinks?
+ //
+ // It seems that a sync method is called in an async transaction
+ // Also seems to only happen on `.reset()` of backlinks
+
// await isar.tWriteTxn(() => target1.backlinks.tReset());
// await qEqualSet(

diff:
import 'package:isar/isar.dart';
import 'package:test/test.dart';
+import '../../util/common.dart';
+import '../../util/sync_async_helper.dart';
+
+part 'filter_link_nested_test.g.dart';
+
+@Collection()
+class SourceModel {
+ SourceModel(this.name);
+
+ Id id = Isar.autoIncrement;
+
+ String name;
+
+ final links = IsarLinks<TargetModel>();
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is SourceModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id;
+
+ @override
+ String toString() {
+ return 'SourceModel{id: $id}';
+ }
+}
+
+@Collection()
+class TargetModel {
+ TargetModel(this.name);
+
+ Id id = Isar.autoIncrement;
+
+ final String name;
+
+ final nestedLinks = IsarLinks<NestedTargetModel>();
+
+ @Backlink(to: 'links')
+ final linksBacklinks = IsarLinks<SourceModel>();
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is TargetModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ name == other.name;
+
+ @override
+ String toString() {
+ return 'TargetModel{id: $id, name: $name}';
+ }
+}
+
+@Collection()
+class NestedTargetModel {
+ NestedTargetModel(this.name);
+
+ Id id = Isar.autoIncrement;
+
+ final String name;
+
+ @Backlink(to: 'nestedLinks')
+ final nestedLinksBacklinks = IsarLinks<TargetModel>();
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is NestedTargetModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ name == other.name;
+
+ @override
+ String toString() {
+ return 'NestedTargetModel{id: $id, name: $name}';
+ }
+}
+
void main() {
group('Filter link nested', () {
late Isar isar;
+
+ late SourceModel source1;
+ late SourceModel source2;
+ late SourceModel source3;
+ late SourceModel source4;
+ late SourceModel source5;
+ late SourceModel source6;
+
+ late TargetModel target1;
+ late TargetModel target2;
+ late TargetModel target3;
+ late TargetModel target4;
+ late TargetModel target5;
+ late TargetModel target6;
+
+ late NestedTargetModel nestedTarget1;
+ late NestedTargetModel nestedTarget2;
+ late NestedTargetModel nestedTarget3;
+ late NestedTargetModel nestedTarget4;
+ late NestedTargetModel nestedTarget5;
+ late NestedTargetModel nestedTarget6;
+
+ setUp(() async {
+ isar = await openTempIsar([
+ SourceModelSchema,
+ TargetModelSchema,
+ NestedTargetModelSchema,
+ ]);
+
+ source1 = SourceModel('source 1');
+ source2 = SourceModel('source 2');
+ source3 = SourceModel('source 3');
+ source4 = SourceModel('source 4');
+ source5 = SourceModel('source 5');
+ source6 = SourceModel('source 6');
+
+ target1 = TargetModel('target 1');
+ target2 = TargetModel('target 2');
+ target3 = TargetModel('target 3');
+ target4 = TargetModel('target 4');
+ target5 = TargetModel('target 5');
+ target6 = TargetModel('target 6');
+
+ nestedTarget1 = NestedTargetModel('nested target 1');
+ nestedTarget2 = NestedTargetModel('nested target 2');
+ nestedTarget3 = NestedTargetModel('nested target 3');
+ nestedTarget4 = NestedTargetModel('nested target 4');
+ nestedTarget5 = NestedTargetModel('nested target 5');
+ nestedTarget6 = NestedTargetModel('nested target 6');
+
+ await isar.tWriteTxn(
+ () => Future.wait([
+ isar.sourceModels.tPutAll([
+ source1,
+ source2,
+ source3,
+ source4,
+ source5,
+ source6,
+ ]),
+ isar.targetModels.tPutAll([
+ target1,
+ target2,
+ target3,
+ target4,
+ target5,
+ target6,
+ ]),
+ isar.nestedTargetModels.tPutAll([
+ nestedTarget1,
+ nestedTarget2,
+ nestedTarget3,
+ nestedTarget4,
+ nestedTarget5,
+ nestedTarget6,
+ ]),
+ ]),
+ );
+
+ source1.links.add(target1);
+ source2.links.addAll([target1, target2]);
+ source3.links.addAll([target1, target2, target3]);
+ source4.links.addAll([target3, target4]);
+ source5.links.add(target5);
+
+ target1.nestedLinks.add(nestedTarget1);
+ target2.nestedLinks.addAll([nestedTarget1, nestedTarget2]);
+ target3.nestedLinks.addAll([nestedTarget1, nestedTarget2, nestedTarget3]);
+ target4.nestedLinks.addAll([nestedTarget3, nestedTarget4, nestedTarget5]);
+ target6.nestedLinks.add(nestedTarget5);
+
+ await isar.tWriteTxn(
+ () => Future.wait([
+ source1.links.tSave(),
+ source2.links.tSave(),
+ source3.links.tSave(),
+ source4.links.tSave(),
+ source5.links.tSave(),
+ target1.nestedLinks.tSave(),
+ target2.nestedLinks.tSave(),
+ target3.nestedLinks.tSave(),
+ target4.nestedLinks.tSave(),
+ ]),
+ );
+ });
+
+ group('Links', () {
+ isarTest('.links()', () async {
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameStartsWith('target'))
+ .tFindAll(),
+ [source1, source2, source3, source4, source5],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('target 1'))
+ .tFindAll(),
+ [source1, source2, source3],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('target 2'))
+ .tFindAll(),
+ [source2, source3],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('target 3'))
+ .tFindAll(),
+ [source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('target 4'))
+ .tFindAll(),
+ [source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('target 5'))
+ .tFindAll(),
+ [source5],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nameEqualTo('non existing'))
+ .tFindAll(),
+ [],
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest('.links() with .nestedLinks()', () async {
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameStartsWith('nested target')),
+ )
+ .tFindAll(),
+ [source1, source2, source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 1')),
+ )
+ .tFindAll(),
+ [source1, source2, source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 2')),
+ )
+ .tFindAll(),
+ [source2, source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 3')),
+ )
+ .tFindAll(),
+ [source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 4')),
+ )
+ .tFindAll(),
+ [source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 5')),
+ )
+ .tFindAll(),
+ [source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('nested target 6')),
+ )
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links(
+ (q) => q.nestedLinks((q) => q.nameEqualTo('non existing')),
+ )
+ .tFindAll(),
+ [],
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest('.links() with .nestedLinksLengthEqualTo()', () async {
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksLengthEqualTo(0))
+ .tFindAll(),
+ [source5],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksLengthEqualTo(1))
+ .tFindAll(),
+ [source1, source2, source3],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksLengthEqualTo(2))
+ .tFindAll(),
+ [source2, source3],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksLengthEqualTo(3))
+ .tFindAll(),
+ [source3, source4],
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksLengthEqualTo(4))
+ .tFindAll(),
+ [],
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest('.links() with .nestedLinkIsEmpty()', () async {
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksIsEmpty())
+ .tFindAll(),
+ [source5],
+ );
+
+ await isar.tWriteTxn(() => target1.nestedLinks.tReset());
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksIsEmpty())
+ .tFindAll(),
+ [source1, source5],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.nestedTargetModels.where().tDeleteAll(),
+ );
+
+ await qEqualSet(
+ isar.sourceModels
+ .filter()
+ .links((q) => q.nestedLinksIsEmpty())
+ .tFindAll(),
+ [source1, source2, source3, source4, source5],
+ );
+ });
+ });
+
+ group('Backlinks', () {
+ isarTest('.nestedLinksBacklinks()', () async {
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target'))
+ .tFindAll(),
+ [
+ nestedTarget1,
+ nestedTarget2,
+ nestedTarget3,
+ nestedTarget4,
+ nestedTarget5,
+ ],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 1'))
+ .tFindAll(),
+ [nestedTarget1],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 2'))
+ .tFindAll(),
+ [nestedTarget1, nestedTarget2],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 3'))
+ .tFindAll(),
+ [nestedTarget1, nestedTarget2, nestedTarget3],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 4'))
+ .tFindAll(),
+ [nestedTarget3, nestedTarget4, nestedTarget5],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 5'))
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('target 6'))
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.nameStartsWith('non existing'))
+ .tFindAll(),
+ [],
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest('.nestedLinksBackLinks', () async {
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameStartsWith('source')),
+ )
+ .tFindAll(),
+ [
+ nestedTarget1,
+ nestedTarget2,
+ nestedTarget3,
+ nestedTarget4,
+ nestedTarget5,
+ ],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 1')),
+ )
+ .tFindAll(),
+ [nestedTarget1],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 2')),
+ )
+ .tFindAll(),
+ [nestedTarget1, nestedTarget2],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 3')),
+ )
+ .tFindAll(),
+ [nestedTarget1, nestedTarget2, nestedTarget3],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 4')),
+ )
+ .tFindAll(),
+ [
+ nestedTarget1,
+ nestedTarget2,
+ nestedTarget3,
+ nestedTarget4,
+ nestedTarget5,
+ ],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 5')),
+ )
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('source 6')),
+ )
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks(
+ (q) => q.linksBacklinks((q) => q.nameEqualTo('non existing')),
+ )
+ .tFindAll(),
+ [],
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest(
+ '.nestedLinksBacklinks() with .linksBacklinksLengthEqualTo()',
+ () async {
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(0))
+ .tFindAll(),
+ [nestedTarget5],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(1))
+ .tFindAll(),
+ [nestedTarget3, nestedTarget4, nestedTarget5],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(2))
+ .tFindAll(),
+ [nestedTarget1, nestedTarget2, nestedTarget3],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(3))
+ .tFindAll(),
+ [nestedTarget1],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(4))
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(5))
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksLengthEqualTo(6))
+ .tFindAll(),
+ [],
+ );
+ },
+ );
+ });
+
+ // FIXME(severe): Nested links filters returns all values
+ // Seems that the nested query is completely ignored
+ isarTest(
+ '.nestedLinksBacklinks() with .linksBacklinksIsEmpty()',
+ () async {
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksIsEmpty())
+ .tFindAll(),
+ [nestedTarget5],
+ );
+
+ return;
+ // FIXME: IsarError: Cannot perform this operation from within an active
+ // transaction.
+ // Are we suppose to a able to reset backlinks?
+ //
+ // It seems that a sync method is called in an async transaction
+ // Also seems to only happen on `.reset()` of backlinks
+ await isar.tWriteTxn(() => target1.linksBacklinks.tReset());
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksIsEmpty())
+ .tFindAll(),
+ [nestedTarget1, nestedTarget5],
+ );
+
+ await isar.tWriteTxn(() => isar.sourceModels.where().tDeleteAll());
+
+ await qEqualSet(
+ isar.nestedTargetModels
+ .filter()
+ .nestedLinksBacklinks((q) => q.linksBacklinksIsEmpty())
+ .tFindAll(),
+ [
+ nestedTarget1,
+ nestedTarget2,
+ nestedTarget3,
+ nestedTarget4,
+ nestedTarget5,
+ ],
+ );
+ },
+ );
});
}

original_message: Added filter link nested test