message: Added String & DateTime converter tests

diff:
@@ -326,5 +326,34 @@ void main() {
[childObj0],
);
});
+
+ isarTest('Sort by boolEnum', () async {
+ await qEqual(
+ isar.boolModels.where().sortByBoolEnum().tFindAll(),
+ [obj0, obj1, obj2],
+ );
+
+ await qEqual(
+ isar.boolModels.where().sortByBoolEnumDesc().tFindAll(),
+ [obj1, obj2, obj0],
+ );
+
+ await qEqual(
+ isar.childBoolModels.where().sortByBoolEnumDesc().tFindAll(),
+ [childObj0, childObj1, childObj2],
+ );
+ });
+
+ isarTest('Sort by mayBoolEnum', () async {
+ await qEqual(
+ isar.boolModels.where().sortByMaybeBoolEnum().tFindAll(),
+ [obj0, obj2, obj1],
+ );
+
+ await qEqual(
+ isar.boolModels.where().sortByMaybeBoolEnumDesc().tFindAll(),
+ [obj1, obj0, obj2],
+ );
+ });
});
}

diff:
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import '../util/common.dart';
+import '../util/sync_async_helper.dart';
+
+part 'date_time_converter_test.g.dart';
+
+@Collection()
+class DateTimeModel {
+ DateTimeModel({
+ required this.dateTime,
+ required this.nullableDateTime,
+ });
+
+ int id = Isar.autoIncrement;
+
+ @UnixToDateTimeTypeConverter()
+ final DateTime dateTime;
+
+ @UnixToNullableDateTimeTypeConverter()
+ final DateTime? nullableDateTime;
+
+ @override
+ String toString() {
+ return 'DateTimeModel{id: $id, dateTime: $dateTime, nullableDateTime: $nullableDateTime}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is DateTimeModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ dateTime == other.dateTime &&
+ nullableDateTime == other.nullableDateTime;
+}
+
+class UnixToDateTimeTypeConverter extends TypeConverter<int, DateTime> {
+ const UnixToDateTimeTypeConverter();
+
+ @override
+ int fromIsar(DateTime date) => date.millisecondsSinceEpoch ~/ 1000;
+
+ @override
+ DateTime toIsar(int unixTimestamp) {
+ return DateTime.fromMillisecondsSinceEpoch(unixTimestamp * 1000);
+ }
+}
+
+class UnixToNullableDateTimeTypeConverter
+ extends TypeConverter<int?, DateTime?> {
+ const UnixToNullableDateTimeTypeConverter();
+
+ @override
+ int? fromIsar(DateTime? date) {
+ if (date == null) return null;
+ return date.millisecondsSinceEpoch ~/ 1000;
+ }
+
+ @override
+ DateTime? toIsar(int? unixTimestamp) {
+ if (unixTimestamp == null) return null;
+ return DateTime.fromMillisecondsSinceEpoch(unixTimestamp * 1000);
+ }
+}
+
+void main() {
+ group('DateTime converter', () {
+ late Isar isar;
+
+ late DateTimeModel obj0;
+ late DateTimeModel obj1;
+ late DateTimeModel obj2;
+ late DateTimeModel obj3;
+ late DateTimeModel obj4;
+ late DateTimeModel obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([DateTimeModelSchema]);
+
+ obj0 = DateTimeModel(
+ dateTime: DateTime(1980, 1, 12),
+ nullableDateTime: DateTime(1992, 7, 21),
+ );
+ obj1 = DateTimeModel(
+ dateTime: DateTime(1500, 3, 27),
+ nullableDateTime: null,
+ );
+ obj2 = DateTimeModel(
+ dateTime: DateTime(2100, 6, 2),
+ nullableDateTime: null,
+ );
+ obj3 = DateTimeModel(
+ dateTime: DateTime(2010, 12, 13),
+ nullableDateTime: DateTime(1942, 3, 3),
+ );
+ obj4 = DateTimeModel(
+ dateTime: DateTime(-42, 5, 22),
+ nullableDateTime: null,
+ );
+ obj5 = DateTimeModel(
+ dateTime: DateTime(2003, 6, 24),
+ nullableDateTime: DateTime(2200, 3, 10),
+ );
+
+ await isar.tWriteTxn(
+ () => isar.dateTimeModels.tPutAll([obj0, obj1, obj2, obj3, obj4, obj5]),
+ );
+ });
+
+ isarTest('Query by dateTime', () async {
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .dateTimeEqualTo(DateTime(2010, 12, 13))
+ .tFindAll(),
+ [obj3],
+ );
+
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .dateTimeGreaterThan(DateTime(2000))
+ .tFindAll(),
+ [obj2, obj3, obj5],
+ );
+
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .dateTimeBetween(DateTime(1980, 1, 2), DateTime(2005, 1, 2))
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ await qEqual(
+ isar.dateTimeModels.filter().dateTimeLessThan(DateTime(0)).tFindAll(),
+ [obj4],
+ );
+ });
+
+ isarTest('Query by nullableDateTime', () async {
+ await qEqual(
+ isar.dateTimeModels.filter().nullableDateTimeIsNull().tFindAll(),
+ [obj1, obj2, obj4],
+ );
+
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .nullableDateTimeGreaterThan(DateTime(2000))
+ .tFindAll(),
+ [obj5],
+ );
+
+ // FIXME: lessThan queries on nullable dates also returns objects where
+ // the field is null
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .nullableDateTimeLessThan(DateTime(1969))
+ .tFindAll(),
+ [obj3],
+ );
+
+ await qEqual(
+ isar.dateTimeModels
+ .filter()
+ .nullableDateTimeBetween(DateTime(1950), DateTime(9999))
+ .tFindAll(),
+ [obj0, obj5],
+ );
+ });
+
+ isarTest('Sort by dateTime', () async {
+ await qEqual(
+ isar.dateTimeModels.where().sortByDateTime().tFindAll(),
+ [obj4, obj1, obj0, obj5, obj3, obj2],
+ );
+
+ await qEqual(
+ isar.dateTimeModels.where().sortByDateTimeDesc().tFindAll(),
+ [obj2, obj3, obj5, obj0, obj1, obj4],
+ );
+ });
+
+ isarTest('Sort by nullableDateTime', () async {
+ await qEqual(
+ isar.dateTimeModels.where().sortByNullableDateTime().tFindAll(),
+ [obj1, obj2, obj4, obj3, obj0, obj5],
+ );
+
+ await qEqual(
+ isar.dateTimeModels.where().sortByNullableDateTimeDesc().tFindAll(),
+ [obj5, obj0, obj3, obj1, obj2, obj4],
+ );
+ });
+ });
+}

diff:
@@ -149,6 +149,22 @@ void main() {
.tFindAll(),
[],
);
+
+ await qEqual(
+ isar.doubleModels
+ .where()
+ .someIntEqualToValueLessThan(0, Values.first)
+ .tFindAll(),
+ [],
+ );
+
+ await qEqual(
+ isar.doubleModels
+ .where()
+ .someIntEqualToValueLessThan(0, Values.second)
+ .tFindAll(),
+ [obj1],
+ );
});
});
}

diff:
@@ -12,6 +12,7 @@ class IntModel {
required this.offsettedInt,
required this.day,
required this.otherDay,
+ required this.nullableDay,
});
int id = Isar.autoIncrement;
@@ -26,9 +27,13 @@ class IntModel {
@DaysTypeConverter()
final Days otherDay;
+ @NullableDaysTypeConverter()
+ @Size32()
+ final Days? nullableDay;
+
@override
String toString() {
- return 'IntModel{id: $id, offsettedInt: $offsettedInt, day: $day}';
+ return 'IntModel{id: $id, offsettedInt: $offsettedInt, day: $day, otherDay: $otherDay, nullableDay: $nullableDay}';
}
@override
@@ -39,7 +44,9 @@ class IntModel {
runtimeType == other.runtimeType &&
id == other.id &&
offsettedInt == other.offsettedInt &&
- day == other.day;
+ day == other.day &&
+ otherDay == other.otherDay &&
+ nullableDay == other.nullableDay;
}
}
@@ -79,6 +86,19 @@ class DaysTypeConverter extends TypeConverter<Days, int> {
int toIsar(Days day) => day.value;
}
+class NullableDaysTypeConverter extends TypeConverter<Days?, int?> {
+ const NullableDaysTypeConverter();
+
+ @override
+ Days? fromIsar(int? value) {
+ if (value == null) return null;
+ return Days.values.firstWhere((element) => element.value == value);
+ }
+
+ @override
+ int? toIsar(Days? day) => day?.value;
+}
+
void main() {
group('Int converter', () {
late Isar isar;
@@ -99,41 +119,49 @@ void main() {
offsettedInt: 0,
day: Days.wednesday,
otherDay: Days.sunday,
+ nullableDay: null,
);
obj1 = IntModel(
offsettedInt: 24,
day: Days.saturday,
otherDay: Days.monday,
+ nullableDay: Days.tuesday,
);
obj2 = IntModel(
offsettedInt: 42,
day: Days.thursday,
otherDay: Days.tuesday,
+ nullableDay: Days.sunday,
);
obj3 = IntModel(
offsettedInt: -1024,
day: Days.saturday,
otherDay: Days.wednesday,
+ nullableDay: Days.friday,
);
obj4 = IntModel(
offsettedInt: 2048,
day: Days.monday,
otherDay: Days.thursday,
+ nullableDay: null,
);
obj5 = IntModel(
offsettedInt: -0,
day: Days.sunday,
otherDay: Days.friday,
+ nullableDay: null,
);
obj6 = IntModel(
offsettedInt: 5673873209,
day: Days.saturday,
otherDay: Days.saturday,
+ nullableDay: Days.wednesday,
);
obj7 = IntModel(
offsettedInt: 40,
day: Days.friday,
otherDay: Days.saturday,
+ nullableDay: null,
);
await isar.tWriteTxn(
@@ -241,5 +269,25 @@ void main() {
[obj1, obj6],
);
});
+
+ isarTest('Query by nullableDay', () async {
+ await qEqual(
+ isar.intModels.filter().nullableDayIsNull().tFindAll(),
+ [obj0, obj4, obj5, obj7],
+ );
+
+ await qEqual(
+ isar.intModels
+ .filter()
+ .nullableDayGreaterThan(Days.wednesday)
+ .tFindAll(),
+ [obj3],
+ );
+
+ await qEqual(
+ isar.intModels.filter().nullableDayEqualTo(Days.saturday).tFindAll(),
+ [],
+ );
+ });
});
}

diff:
+import 'package:isar/isar.dart';
+import 'package:test/test.dart';
+
+import '../util/common.dart';
+import '../util/sync_async_helper.dart';
+
+part 'string_converter_test.g.dart';
+
+@Collection()
+class StringModel {
+ StringModel({
+ required this.authProvider,
+ required this.number,
+ required this.nullableNumber,
+ });
+
+ final id = Isar.autoIncrement;
+
+ @AuthProvidersTypeConverter()
+ final AuthProviders authProvider;
+
+ @NumStringifierTypeConverter()
+ @Index()
+ final num number;
+
+ @NullableNumStringifierTypeConverter()
+ final num? nullableNumber;
+
+ @override
+ String toString() {
+ return 'StringModel{id: $id, authProvider: $authProvider, number: $number, nullableNumber: $nullableNumber}';
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is StringModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ authProvider == other.authProvider &&
+ number == other.number &&
+ nullableNumber == other.nullableNumber;
+
+ @override
+ int get hashCode =>
+ id.hashCode ^
+ authProvider.hashCode ^
+ number.hashCode ^
+ nullableNumber.hashCode;
+}
+
+enum AuthProviders {
+ password('password'),
+ google('google.com'),
+ facebook('facebook.com'),
+ apple('apple.com'),
+ github('github.com'),
+ none(null);
+
+ const AuthProviders(this.value);
+
+ final String? value;
+}
+
+class AuthProvidersTypeConverter extends TypeConverter<AuthProviders, String?> {
+ const AuthProvidersTypeConverter();
+
+ @override
+ AuthProviders fromIsar(String? value) {
+ return AuthProviders.values.firstWhere(
+ (provider) => provider.value == value,
+ );
+ }
+
+ @override
+ String? toIsar(AuthProviders provider) => provider.value;
+}
+
+class NumStringifierTypeConverter extends TypeConverter<num, String> {
+ const NumStringifierTypeConverter();
+
+ @override
+ num fromIsar(String value) => num.parse(value);
+
+ @override
+ String toIsar(num value) => value.toString();
+}
+
+class NullableNumStringifierTypeConverter extends TypeConverter<num?, String?> {
+ const NullableNumStringifierTypeConverter();
+
+ @override
+ num? fromIsar(String? value) {
+ if (value == null) return null;
+ return num.parse(value);
+ }
+
+ @override
+ String? toIsar(num? value) => value?.toString();
+}
+
+void main() {
+ group('String converter', () {
+ late Isar isar;
+
+ late StringModel obj0;
+ late StringModel obj1;
+ late StringModel obj2;
+ late StringModel obj3;
+ late StringModel obj4;
+ late StringModel obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([StringModelSchema]);
+
+ obj0 = StringModel(
+ authProvider: AuthProviders.github,
+ number: 1,
+ nullableNumber: null,
+ );
+ obj1 = StringModel(
+ authProvider: AuthProviders.facebook,
+ number: 42.12,
+ nullableNumber: 123,
+ );
+ obj2 = StringModel(
+ authProvider: AuthProviders.none,
+ number: 0,
+ nullableNumber: null,
+ );
+ obj3 = StringModel(
+ authProvider: AuthProviders.password,
+ number: 1024,
+ nullableNumber: null,
+ );
+ obj4 = StringModel(
+ authProvider: AuthProviders.apple,
+ number: 0.5,
+ nullableNumber: 92,
+ );
+ obj5 = StringModel(
+ authProvider: AuthProviders.github,
+ number: -2048,
+ nullableNumber: 42.42,
+ );
+
+ await isar.tWriteTxn(
+ () => isar.stringModels.tPutAll([obj0, obj1, obj2, obj3, obj4, obj5]),
+ );
+ });
+
+ tearDown(() => isar.close());
+
+ // FIXME: This seems to be an issue with the generator, that generates
+ // methods which don't support nullable string as argument
+ //
+ // test/converter/string_converter_test.g.dart:608:64:
+ // Error: The argument type 'String?' can't be assigned to the parameter
+ // type 'String' because 'String?' is nullable and 'String' isn't.
+ //
+ isarTest('Query by authProvider', () async {
+ await qEqual(
+ isar.stringModels
+ .filter()
+ .authProviderEqualTo(AuthProviders.github)
+ .tFindAll(),
+ [obj0, obj5],
+ );
+ });
+
+ isarTest('Query by number index', () async {
+ await qEqual(
+ isar.stringModels
+ .where()
+ .numberEqualTo(1)
+ .or()
+ .numberEqualTo(-2048)
+ .tFindAll(),
+ [obj0, obj5],
+ );
+ });
+
+ // TODO(jtplouffe): implement more tests once the generator issues are fixed
+ });
+}

original_message: Added String & DateTime converter tests


message: Refactored old converter test

change_type: RENAME

diff:
import 'package:isar/isar.dart';
import 'package:test/test.dart';
-import 'util/common.dart';
+import '../util/common.dart';
-part 'converter_test.g.dart';
+part 'multiple_converter_test.g.dart';
@Collection()
-class ConverterModel {
+class MultiConverterModel {
+ MultiConverterModel({
+ required this.id,
+ required this.boolValue,
+ required this.intValue,
+ required this.longValue,
+ required this.floatValue,
+ required this.doubleValue,
+ required this.dateValue,
+ required this.stringValue,
+ });
+
@Id()
int? id;
@@ -43,7 +54,7 @@ class ConverterModel {
@override
// ignore: hash_and_equals
bool operator ==(Object other) {
- if (other is ConverterModel) {
+ if (other is MultiConverterModel) {
return boolValue == other.boolValue &&
intValue == other.intValue &&
longValue == other.longValue &&
@@ -128,15 +139,16 @@ class StringConverter extends TypeConverter<String, int> {
}
void main() {
- final converterObject = ConverterModel()
- ..id = 123
- ..boolValue = true
- ..intValue = 25
- ..floatValue = 17.17
- ..longValue = 123123
- ..doubleValue = 123.123
- ..dateValue = DateTime.fromMillisecondsSinceEpoch(123123)
- ..stringValue = 'five';
+ final converterObject = MultiConverterModel(
+ id: 123,
+ boolValue: true,
+ intValue: 25,
+ floatValue: 17.17,
+ longValue: 123123,
+ doubleValue: 123.123,
+ dateValue: DateTime.fromMillisecondsSinceEpoch(123123),
+ stringValue: 'five',
+ );
final converterObjectJson = <String, Object>{
'id': 123,
@@ -149,29 +161,29 @@ void main() {
'stringValue': 5,
};
- group('Converter', () {
- /*isarTest('toIsar()', () async {
- final isar = await openTempIsar([ConverterModelSchema]);
+ group('Multiple converter', () {
+ isarTest('toIsar()', () async {
+ final isar = await openTempIsar([MultiConverterModelSchema]);
await isar.writeTxn(() async {
- await isar.converterModels.put(_converterObject);
+ await isar.multiConverterModels.put(converterObject);
});
- final json = await isar.converterModels.where().exportJson();
- expect(json[0], _converterObjectJson);
+ final json = await isar.multiConverterModels.where().exportJson();
+ expect(json[0], converterObjectJson);
await isar.close();
- });*/
+ });
isarTest('fromIsar()', () async {
- final isar = await openTempIsar([ConverterModelSchema]);
+ final isar = await openTempIsar([MultiConverterModelSchema]);
await isar.writeTxn(() async {
- await isar.converterModels.importJson([converterObjectJson]);
+ await isar.multiConverterModels.importJson([converterObjectJson]);
});
expect(
- await isar.converterModels.get(123),
+ await isar.multiConverterModels.get(123),
converterObject,
);

license: Apache License 2.0

repo: isar/isar

original_message: Refactored old converter test


message: new isar inspector

change_type: MODIFY

diff:
@@ -7,6 +7,7 @@ class IsarCard extends StatelessWidget {
this.color,
this.onTap,
this.onLongPress,
+ this.padding = EdgeInsets.zero,
BorderRadius? radius,
}) : radius = radius ?? BorderRadius.circular(20);
final Widget child;
@@ -14,6 +15,7 @@ class IsarCard extends StatelessWidget {
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final BorderRadius radius;
+ final EdgeInsets padding;
@override
Widget build(BuildContext context) {
@@ -25,11 +27,14 @@ class IsarCard extends StatelessWidget {
),
clipBehavior: Clip.antiAlias,
elevation: 0,
+ child: Padding(
+ padding: padding,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: child,
),
+ ),
);
}
}

diff:
@@ -37,9 +37,9 @@ class _ConnectedLayoutState extends ConsumerState<ConnectedLayout> {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
- children: const [
- FilterField(),
- SizedBox(height: 25),
+ children: [
+ const FilterField(),
+ const SizedBox(height: 25),
Expanded(
child: QueryTable(),
),

diff:
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:flutter_treeview/flutter_treeview.dart';
import 'package:isar/isar.dart';
import 'package:isar_inspector/common.dart';
@@ -11,10 +12,15 @@ import 'package:isar_inspector/state/instances_state.dart';
import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
import 'package:isar_inspector/state/query_state.dart';
-const double _deleteColWidth = 60;
+const _stringColor = Color(0xFF6A8759);
+const _numberColor = Color(0xFF6897BB);
+const _boolColor = Color(0xFFCC7832);
+const _disableColor = Colors.grey;
class QueryTable extends ConsumerWidget {
- const QueryTable({super.key});
+ QueryTable({super.key});
+
+ final _controller = ScrollController();
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -25,35 +31,60 @@ class QueryTable extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Column(
- children: [
- IsarCard(
- child: Row(
- mainAxisSize: MainAxisSize.min,
+ child: RawScrollbar(
+ controller: _controller,
+ thumbColor: Colors.grey,
+ child: ListView.separated(
+ controller: _controller,
+ itemCount: objects.length,
+ itemBuilder: (context, index) {
+ return Stack(
children: [
- for (var property in collection.allProperties)
- HeaderProperty(property: property),
- const SizedBox(width: _deleteColWidth),
- ],
+ TableBlock(
+ key: Key(objects[index].hashCode.toString()),
+ collection: collection,
+ object: objects[index],
),
+ Align(
+ alignment: Alignment.topLeft,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 10, right: 10),
+ child: Tooltip(
+ message: 'Delete object',
+ child: IconButton(
+ icon: const Icon(Icons.delete),
+ splashRadius: 25,
+ onPressed: () {
+ final collection =
+ ref.read(selectedCollectionPod).valueOrNull;
+ if (collection == null) {
+ return;
+ }
+
+ final query = ConnectQuery(
+ instance: ref.read(selectedInstancePod).value!,
+ collection: collection.name,
+ filter: FilterCondition.equalTo(
+ property: collection.idName,
+ value: objects[index].getValue(
+ collection.idName,
),
- Expanded(
- child: SingleChildScrollView(
- child: Column(
- children: [
- for (int i = 0; i < objects.length; i++)
- TableRow(
- collection: collection,
- object: objects[i],
- index: i,
),
- ],
+ );
+ ref
+ .read(isarConnectPod.notifier)
+ .removeQuery(query);
+ },
+ ),
),
),
),
],
+ );
+ },
+ separatorBuilder: (BuildContext context, int index) {
+ return const SizedBox(height: 15);
+ },
),
),
),
@@ -64,147 +95,246 @@ class QueryTable extends ConsumerWidget {
}
}
-class HeaderProperty extends ConsumerWidget {
- const HeaderProperty({super.key, required this.property});
- final IProperty property;
+class TableBlock extends StatefulWidget {
+ const TableBlock({
+ super.key,
+ required this.collection,
+ required this.object,
+ });
+
+ final ICollection collection;
+ final QueryObject object;
@override
- Widget build(BuildContext context, WidgetRef ref) {
- final theme = Theme.of(context);
- final sortProperty = ref.watch(querySortPod);
+ State<TableBlock> createState() => _TableBlockState();
+}
- return IsarCard(
- onTap: () {
- SortProperty? newSortProperty;
- if (sortProperty?.property == property.name) {
- if (sortProperty!.sort == Sort.asc) {
- newSortProperty = SortProperty(
- property: property.name,
- sort: Sort.desc,
+class _TableBlockState extends State<TableBlock> {
+ late TreeViewController _treeViewController = TreeViewController(
+ children: widget.collection.allProperties.map((property) {
+ final children = <Node<TreeViewHelper>>[];
+ final rawValue = widget.object.getValue(property.name);
+ String value;
+ Color color;
+
+ switch (property.type) {
+ case IsarType.Bool:
+ value = rawValue.toString();
+ color = _boolColor;
+ break;
+
+ case IsarType.Int:
+ case IsarType.Float:
+ case IsarType.Long:
+ case IsarType.Double:
+ value = rawValue.toString();
+ color = _numberColor;
+ break;
+
+ case IsarType.String:
+ value = '"$rawValue"';
+ color = _stringColor;
+ break;
+
+ case IsarType.ByteList:
+ case IsarType.IntList:
+ case IsarType.FloatList:
+ case IsarType.BoolList:
+ case IsarType.LongList:
+ case IsarType.DoubleList:
+ case IsarType.StringList:
+ final list = rawValue as List<dynamic>;
+
+ value = '${property.type.toString().replaceAll('IsarType.', '')}'
+ ' [${list.length}]';
+ color = _disableColor;
+
+ for (var index = 0; index < list.length; index++) {
+ children.add(
+ Node(
+ key: '${property.name}_$index',
+ label: '',
+ data: TreeViewHelper(
+ name: index.toString(),
+ value: property.type == IsarType.StringList
+ ? '"${list[index]}"'
+ : list[index].toString(),
+ rawValue: list[index],
+ valueColor: property.type == IsarType.StringList
+ ? _stringColor
+ : property.type == IsarType.BoolList
+ ? _boolColor
+ : _numberColor,
+ ),
+ ),
);
}
- } else if (property.type.sortable) {
- newSortProperty = SortProperty(
- property: property.name,
- sort: sortProperty == null && property.isId ? Sort.desc : Sort.asc,
- );
+ break;
}
- ref.read(querySortPod.state).state = newSortProperty;
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 15),
- child: SizedBox(
- width: property.type.width,
- child: Row(
- children: [
- Expanded(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- property.name,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 16,
- ),
+
+ return Node<TreeViewHelper>(
+ key: property.name,
+ label: '',
+ children: children,
+ data: TreeViewHelper(
+ name: property.name,
+ value: value,
+ rawValue: rawValue,
+ valueColor: color,
),
- const SizedBox(height: 5),
- Text(
- property.isId ? 'Id' : property.type.name,
- style: TextStyle(
- color: theme.primaryColor,
- fontSize: 12,
+ );
+ }).toList(),
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ return IsarCard(
+ color: const Color(0xFF1F2128),
+ padding: const EdgeInsets.fromLTRB(25, 15, 15, 15),
+ radius: BorderRadius.circular(5),
+ child: TreeView(
+ theme: const TreeViewTheme(
+ expanderTheme: ExpanderThemeData(color: Colors.white),
),
+ controller: _treeViewController,
+ shrinkWrap: true,
+ nodeBuilder: (context, node) {
+ final data = node.data as TreeViewHelper;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5),
+ child: TableItem(data: data),
+ );
+ },
+ onExpansionChanged: (key, expanded) {
+ _expanding(_treeViewController.getNode(key)!, expanded);
+ },
+ onNodeTap: (key) {
+ final node = _treeViewController.getNode<TreeViewHelper>(key)!;
+ _expanding(node, !node.expanded);
+ },
),
- ],
+ );
+ }
+
+ void _expanding(Node<TreeViewHelper> node, bool expanded) {
+ setState(() {
+ _treeViewController = _treeViewController.copyWith(
+ children: _treeViewController.updateNode(
+ node.key,
+ node.copyWith(expanded: expanded),
),
+ );
+ });
+ }
+}
+
+class TableItem extends StatefulWidget {
+ const TableItem({super.key, required this.data});
+
+ final TreeViewHelper data;
+
+ @override
+ State<TableItem> createState() => _TableItemState();
+}
+
+class _TableItemState extends State<TableItem> {
+ bool _entered = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final richText = RichText(
+ overflow: TextOverflow.ellipsis,
+ text: TextSpan(
+ text: '${widget.data.name}: ',
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
),
- if (sortProperty?.property == property.name)
- Icon(
- sortProperty!.sort == Sort.asc
- ? FontAwesomeIcons.caretUp
- : FontAwesomeIcons.caretDown,
- size: 20,
+ children: [
+ TextSpan(
+ text: widget.data.value,
+ style: TextStyle(color: widget.data.valueColor),
)
- else if (sortProperty == null && property.isId)
- const Icon(
- FontAwesomeIcons.caretUp,
- size: 20,
- ),
],
),
- ),
- ),
);
- }
-}
-class TableRow extends ConsumerWidget {
- const TableRow({
- super.key,
- required this.collection,
- required this.index,
- required this.object,
+ return MouseRegion(
+ onEnter: (_) {
+ setState(() {
+ _entered = true;
});
- final ICollection collection;
- final int index;
- final QueryObject object;
-
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- return IsarCard(
- color: index.isEven ? Colors.transparent : null,
- radius: BorderRadius.circular(15),
- onTap: () {},
+ },
+ onExit: (e) {
+ setState(() {
+ _entered = false;
+ });
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
- mainAxisSize: MainAxisSize.min,
children: [
- for (IProperty property in collection.allProperties)
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 15),
- child: SizedBox(
- width: property.type.width,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- object.getValue(property.name),
- style: TextStyle(color: Colors.grey[500]),
- ),
- ],
+ Expanded(
+ child: widget.data.rawValue is String
+ ? Tooltip(
+ message: widget.data.value,
+ child: richText,
+ )
+ : richText,
),
+ if (_entered)
+ InkWell(
+ onTap: () async {
+ ScaffoldMessenger.of(context).removeCurrentSnackBar();
+
+ await Clipboard.setData(
+ ClipboardData(text: widget.data.rawValue.toString()),
+ );
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ duration: Duration(seconds: 1),
+ width: 200,
+ behavior: SnackBarBehavior.floating,
+ content: Text(
+ 'Copied To Clipboard',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
),
),
- SizedBox(
- width: _deleteColWidth,
- child: Center(
- child: IconButton(
- icon: const Icon(Icons.delete),
- splashRadius: 25,
- onPressed: () {
- final collection =
- ref.read(selectedCollectionPod).valueOrNull;
- if (collection == null) {
- return;
- }
-
- final query = ConnectQuery(
- instance: ref.read(selectedInstancePod).value!,
- collection: collection.name,
- filter: FilterCondition.equalTo(
- property: collection.idName,
- value: int.parse(object.getValue(collection.idName)),
),
);
- ref.read(isarConnectPod.notifier).removeQuery(query);
+ }
},
+ child: const Center(
+ child: Tooltip(
+ message: 'Copy to clipboard',
+ child: Icon(Icons.copy, size: 18),
),
),
),
],
),
+ ),
);
}
}
+
+class TreeViewHelper {
+ const TreeViewHelper({
+ required this.name,
+ required this.value,
+ required this.rawValue,
+ required this.valueColor,
+ });
+
+ final String name;
+ final String value;
+ final dynamic rawValue;
+ final Color valueColor;
+}

diff:
+import 'dart:core';
+
import 'package:json_annotation/json_annotation.dart';
part 'schema.g.dart';
@@ -58,23 +60,17 @@ class ILink {
// ignore_for_file: constant_identifier_names
enum IsarType {
- Bool(true, 80),
- Int(true, 80),
- Float(true, 80),
- Long(true, 80),
- Double(true, 80),
- String(true, 200),
- Bytes(false, 200),
- IntList(false, 200),
- FloatList(false, 200),
- LongList(false, 200),
- DoubleList(false, 200),
- StringList(false, 200);
-
- final bool sortable;
- final double width;
-
- // False warning
- // ignore: sort_constructors_first
- const IsarType(this.sortable, this.width);
+ Bool,
+ Int,
+ Float,
+ Long,
+ Double,
+ String,
+ ByteList,
+ IntList,
+ FloatList,
+ LongList,
+ DoubleList,
+ StringList,
+ BoolList;
}

diff:
@@ -44,12 +44,13 @@ const _$IsarTypeEnumMap = {
IsarType.Long: 'Long',
IsarType.Double: 'Double',
IsarType.String: 'String',
- IsarType.Bytes: 'Bytes',
+ IsarType.ByteList: 'ByteList',
IsarType.IntList: 'IntList',
IsarType.FloatList: 'FloatList',
IsarType.LongList: 'LongList',
IsarType.DoubleList: 'DoubleList',
IsarType.StringList: 'StringList',
+ IsarType.BoolList: 'BoolList',
};
ILink _$ILinkFromJson(Map<String, dynamic> json) => ILink(

diff:
@@ -13,9 +13,7 @@ class QueryObject {
const QueryObject(this.data);
final Map<String, dynamic> data;
- String getValue(String propertyName) {
- return data[propertyName]?.toString() ?? '';
- }
+ dynamic getValue(String propertyName) => data[propertyName];
}
final queryPagePod = StateProvider((ref) => 0);

diff:
@@ -11,6 +11,7 @@ dependencies:
sdk: flutter
flutter_riverpod: ^2.0.0-dev.7
flutter_svg: any
+ flutter_treeview: ^1.0.7+1
font_awesome_flutter: any
go_router: ^4.1.0
google_fonts: ^3.0.1

original_message: new isar inspector


message: added XOR logical operator in query

diff:
@@ -36,6 +36,13 @@ class QueryParser {
),
);
+ builder.group().left(
+ string('^').trim(),
+ (Object? l, _, Object? r) => FilterGroup.xor(
+ [l as FilterOperation, r as FilterOperation],
+ ),
+ );
+
_parser = builder.build();
}
final List<IProperty> properties;
@@ -161,38 +168,3 @@ class QueryGrammar {
static Parser<List<dynamic>> get condition =>
identifier.trim() & cmpOperator.trim() & valueToken.trim();
}
-
-class AndOr {
- AndOr(this.conditions, this.and);
- final List<dynamic> conditions;
- final bool and;
-
- AndOr flatten() {
- final newConditions = <dynamic>[];
- for (final condition in conditions) {
- if (condition is AndOr) {
- final flatCondition = condition.flatten();
- if (flatCondition.and == and) {
- newConditions.addAll(flatCondition.conditions);
- } else {
- newConditions.add(flatCondition);
- }
- } else {
- newConditions.add(condition);
- }
- }
- return AndOr(newConditions, and);
- }
-
- @override
- String toString() {
- String seperator;
- if (and) {
- seperator = ' && ';
- } else {
- seperator = ' || ';
- }
- final joined = conditions.join(seperator);
- return '($joined)';
- }
-}

original_message: added XOR logical operator in query


message: fixed wrong padding for properties

diff:
@@ -199,12 +199,7 @@ class _TableBlockState extends State<TableBlock> {
controller: _treeViewController,
shrinkWrap: true,
nodeBuilder: (context, node) {
- final data = node.data as TreeViewHelper;
-
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 5),
- child: TableItem(data: data),
- );
+ return TableItem(data: node.data as TreeViewHelper);
},
onExpansionChanged: (key, expanded) {
_expanding(_treeViewController.getNode(key)!, expanded);

original_message: fixed wrong padding for properties


message: Added missing tearDown

diff:
@@ -345,6 +345,8 @@ void main() {
);
});
+ tearDown(() => isar.close());
+
isarTest('Query grandparent', () async {
expect(
GrandparentSchema.propertyIds.containsKey('ignoredFieldGrandparent'),

original_message: Added missing tearDown


message:
provide option to download all collection data as json
fixed some typo
refactored desktop and web directories
fixed desktop URL parser
changed ValueKey to ObjectKey in QueryTable

diff:
@@ -124,7 +124,7 @@ class CollectionSchema<OBJ> {
if (propertyId != null) {
return propertyId;
} else {
- throw IsarError('Unknown propery "$propertyName"');
+ throw IsarError('Unknown property "$propertyName"');
}
}

diff:
@@ -12,6 +12,7 @@ abstract class _IsarConnect {
ConnectAction.watchInstance: _watchInstance,
ConnectAction.executeQuery: _executeQuery,
ConnectAction.removeQuery: _removeQuery,
+ ConnectAction.exportJson: _exportJson,
};
static bool _initialized = false;
@@ -161,6 +162,12 @@ abstract class _IsarConnect {
return true;
}
+ static Future<List<dynamic>> _exportJson(Map<String, dynamic> params) async {
+ final collection = Isar.getInstance(params['instance'] as String)!
+ .getCollectionByNameInternal(params['collection'] as String)!;
+ return collection.where().exportJson();
+ }
+
static Query<dynamic> _getQuery(Map<String, dynamic> params) {
final query = ConnectQuery.fromJson(params);
final collection = Isar.getInstance(query.instance)!

diff:
@@ -9,7 +9,8 @@ enum ConnectAction {
watchInstance('ext.isar.watchInstance'),
executeQuery('ext.isar.executeQuery'),
removeQuery('ext.isar.removeQuery'),
- exportQuery('ext.isar.exportQuery');
+ exportQuery('ext.isar.exportQuery'),
+ exportJson('ext.isar.exportJson');
const ConnectAction(this.method);

diff:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar_inspector/common.dart';
+import 'package:isar_inspector/desktop/download.dart'
+ if (dart.library.html) 'package:isar_inspector/web/download.dart';
import 'package:isar_inspector/state/collections_state.dart';
+import 'package:isar_inspector/state/instances_state.dart';
+import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
class CollectionsList extends ConsumerWidget {
const CollectionsList({super.key});
@@ -12,11 +16,14 @@ class CollectionsList extends ConsumerWidget {
final collections = ref.watch(collectionsPod).valueOrNull ?? [];
final collectionInfo = ref.watch(collectionInfoPod);
final selectedCollection = ref.watch(selectedCollectionPod).valueOrNull;
+ final isarConnect = ref.watch(isarConnectPod.notifier);
+ final selectedInstance = ref.watch(selectedInstancePod).value!;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
final collection = collections.elementAt(index);
final info = collectionInfo[collection.name];
+
return SizedBox(
height: 55,
child: IsarCard(
@@ -26,7 +33,7 @@ class CollectionsList extends ConsumerWidget {
ref.read(selectedCollectionNamePod.state).state = collection.name;
},
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 25),
+ padding: const EdgeInsets.only(left: 25, right: 10),
child: Row(
children: [
Text(
@@ -37,7 +44,7 @@ class CollectionsList extends ConsumerWidget {
),
),
const Spacer(),
- if (info != null)
+ if (info != null) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
@@ -57,6 +64,29 @@ class CollectionsList extends ConsumerWidget {
),
],
),
+ const SizedBox(width: 5),
+ ],
+ GestureDetector(
+ onTap: () async {
+ await download(
+ await isarConnect.exportJson(
+ selectedInstance,
+ collection.name,
+ ),
+ '${selectedInstance}_${collection.name}.json',
+ );
+ },
+ child: Tooltip(
+ message: 'Download collection',
+ child: Icon(
+ Icons.download,
+ size: 30,
+ color: info != null && info.count == 0
+ ? Colors.grey
+ : null,
+ ),
+ ),
+ ),
],
),
),

diff:
@@ -14,7 +14,7 @@ class App extends StatefulWidget {
class _AppState extends State<App> {
final _textController = TextEditingController();
- final urlRE = RegExp(r'^https:\/\/inspect\.isar\.dev\/#\/(\d+)\/(\w+)$');
+ final urlRE = RegExp(r'^https:\/\/inspect\.isar\.dev\/#\/(\d+)\/([\w-]+)$');
String? _port;
String? _secret;
String? _error;
@@ -77,10 +77,24 @@ class _AppState extends State<App> {
errorText: _error,
labelText: 'Enter URL',
),
+ onSubmitted: (_) => _connect(),
style: GoogleFonts.sourceCodePro(),
),
ElevatedButton(
- onPressed: () {
+ onPressed: _connect,
+ child: const Text('Connect'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _connect() {
final url = _textController.text.trim();
if (!urlRE.hasMatch(url)) {
setState(() {
@@ -96,16 +110,5 @@ class _AppState extends State<App> {
_secret = match.group(2);
_error = null;
});
- },
- child: const Text('Connect'),
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- );
}
}

change_type: ADD

diff:
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/foundation.dart';
+
+Future<void> download(List<dynamic> data, String fileName) async {
+ if (data.isNotEmpty) {
+ // Calling file selectors without isolate blocks main isolate (windows)
+ await compute(_saveFile, {
+ 'fileName': fileName,
+ 'data': jsonEncode(data),
+ });
+ }
+}
+
+Future<void> _saveFile(Map<String, String> args) async {
+ final result = await FilePicker.platform.saveFile(
+ dialogTitle: 'Save file',
+ fileName: args['fileName'],
+ );
+
+ if (result != null) {
+ try {
+ final file = File(result);
+ await file.writeAsString(args['data']!);
+ } catch (_) {}
+ }
+}

diff:
@@ -67,7 +67,7 @@ class _InstanceSelectorState extends ConsumerState<InstanceSelector>
InstanceButton(
instance: instance,
onTap: () {
- selectInstane(ref, instance);
+ selectInstance(ref, instance);
_controller.reverse();
},
),

diff:
import 'package:flutter/material.dart';
-import 'package:isar_inspector/app_io.dart'
- if (dart.library.html) 'package:isar_inspector/app_web.dart';
+import 'package:isar_inspector/desktop/app.dart'
+ if (dart.library.html) 'package:isar_inspector/web/app.dart';
void main() async {
//ignore: prefer_const_constructors

diff:
@@ -41,7 +41,7 @@ class QueryTable extends ConsumerWidget {
return Stack(
children: [
TableBlock(
- key: Key(objects[index].hashCode.toString()),
+ key: ObjectKey(objects[index]),
collection: collection,
object: objects[index],
),
@@ -97,7 +97,7 @@ class QueryTable extends ConsumerWidget {
class TableBlock extends StatefulWidget {
const TableBlock({
- super.key,
+ required super.key,
required this.collection,
required this.object,
});

diff:
@@ -9,7 +9,7 @@ final instancesPod = FutureProvider((ref) {
final _selectedInstanceNamePod = StateProvider((ref) => 'isar');
-void selectInstane(WidgetRef ref, String instance) {
+void selectInstance(WidgetRef ref, String instance) {
ref.read(_selectedInstanceNamePod.state).state = instance;
}

diff:
@@ -43,6 +43,7 @@ class IsarConnectStateNotifier
}) : super(const AsyncValue.loading()) {
connect();
}
+
static const Duration kNormalTimeout = Duration(seconds: 4);
static const Duration kLongTimeout = Duration(seconds: 10);
@@ -163,6 +164,16 @@ class IsarConnectStateNotifier
);
}
+ Future<List<dynamic>> exportJson(String instance, String collection) async {
+ final data = await _call<List<dynamic>>(
+ ConnectAction.exportJson,
+ args: {'instance': instance, 'collection': collection},
+ timeout: kLongTimeout,
+ );
+
+ return data.cast();
+ }
+
Future<void> disconnect() async {
await state.value?.vmService.dispose();
}

diff:
+import 'dart:convert';
+
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html';
+
+Future<void> download(List<dynamic> data, String fileName) async {
+ if (data.isEmpty) {
+ return;
+ }
+
+ try {
+ final base64 = base64Encode(utf8.encode(jsonEncode(data)));
+ final anchor =
+ AnchorElement(href: 'data:application/octet-stream;base64,$base64')
+ ..target = 'blank'
+ ..download = fileName;
+
+ document.body!.append(anchor);
+ anchor.click();
+ anchor.remove();
+ } catch (_) {}
+}

diff:
@@ -7,6 +7,7 @@ environment:
dependencies:
dartx: ^1.0.0
+ file_picker: ^4.6.1
flutter:
sdk: flutter
flutter_riverpod: ^2.0.0-dev.7
@@ -29,6 +30,8 @@ dev_dependencies:
very_good_analysis: ^3.0.1
dependency_overrides:
+ ffi: '>=2.0.0 <3.0.0'
+
isar:
path: ../isar


message:
fixed long collection name overlap
added download query json

diff:
@@ -163,9 +163,8 @@ abstract class _IsarConnect {
}
static Future<List<dynamic>> _exportJson(Map<String, dynamic> params) async {
- final collection = Isar.getInstance(params['instance'] as String)!
- .getCollectionByNameInternal(params['collection'] as String)!;
- return collection.where().exportJson();
+ final query = _getQuery(params);
+ return query.exportJson();
}
static Query<dynamic> _getQuery(Map<String, dynamic> params) {

diff:
@@ -36,14 +36,19 @@ class CollectionsList extends ConsumerWidget {
padding: const EdgeInsets.only(left: 25, right: 10),
child: Row(
children: [
- Text(
+ Expanded(
+ child: Tooltip(
+ message: collection.name,
+ child: Text(
collection.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
),
),
- const Spacer(),
if (info != null) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
@@ -68,11 +73,15 @@ class CollectionsList extends ConsumerWidget {
],
GestureDetector(
onTap: () async {
+ final query = ConnectQuery(
+ instance: selectedInstance,
+ collection: collection.name,
+ );
+
+ final data = await isarConnect.exportJson(query);
+
await download(
- await isarConnect.exportJson(
- selectedInstance,
- collection.name,
- ),
+ data,
'${selectedInstance}_${collection.name}.json',
);
},

diff:
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:isar/isar.dart';
+import 'package:isar_inspector/desktop/download.dart'
+ if (dart.library.html) 'package:isar_inspector/web/download.dart';
import 'package:isar_inspector/query_parser.dart';
import 'package:isar_inspector/schema.dart';
import 'package:isar_inspector/state/collections_state.dart';
@@ -59,6 +61,22 @@ class _FilterFieldState extends ConsumerState<FilterField> {
child: const Text('Query'),
),
const SizedBox(width: 20),
+ ElevatedButton(
+ onPressed: () async {
+ final selectedCollection = ref.read(selectedCollectionPod).value!;
+ final filter = _parseFilter(selectedCollection);
+ final q = ConnectQuery(
+ instance: ref.read(selectedInstancePod).value!,
+ collection: selectedCollection.name,
+ filter: filter,
+ );
+
+ final data = await ref.read(isarConnectPod.notifier).exportJson(q);
+ await download(data, '${q.instance}_${q.collection}_query.json');
+ },
+ child: const Text('Download'),
+ ),
+ const SizedBox(width: 20),
ElevatedButton(
onPressed: () {
final selectedCollection = ref.read(selectedCollectionPod).value!;
@@ -71,7 +89,7 @@ class _FilterFieldState extends ConsumerState<FilterField> {
ref.read(isarConnectPod.notifier).removeQuery(query);
},
child: const Text('Remove'),
- )
+ ),
],
);
}

diff:
@@ -164,10 +164,10 @@ class IsarConnectStateNotifier
);
}
- Future<List<dynamic>> exportJson(String instance, String collection) async {
+ Future<List<dynamic>> exportJson(ConnectQuery query) async {
final data = await _call<List<dynamic>>(
ConnectAction.exportJson,
- args: {'instance': instance, 'collection': collection},
+ args: query.toJson(),
timeout: kLongTimeout,
);


message:
added property editor
fixed properties with null value
better presentation for multi line strings

diff:
@@ -13,9 +13,11 @@ abstract class _IsarConnect {
ConnectAction.executeQuery: _executeQuery,
ConnectAction.removeQuery: _removeQuery,
ConnectAction.exportJson: _exportJson,
+ ConnectAction.editProperty: _editProperty,
};
static bool _initialized = false;
+
// ignore: cancel_subscriptions
static StreamSubscription<void>? _querySubscription;
static final List<StreamSubscription<void>> _collectionSubscriptions =
@@ -167,6 +169,34 @@ abstract class _IsarConnect {
return query.exportJson();
}
+ static Future<bool> _editProperty(
+ 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)) {
+ throw IsarError('Cant get object or property is wrong for edit');
+ }
+
+ if (cEdit.index == null) {
+ objects[0][cEdit.property] = cEdit.value;
+ } else {
+ //ignore: avoid_dynamic_calls
+ objects[0][cEdit.property][cEdit.index] = cEdit.value;
+ }
+
+ await collection.isar.writeTxn(() async => collection.importJson(objects));
+ return true;
+ }
+
static Query<dynamic> _getQuery(Map<String, dynamic> params) {
final query = ConnectQuery.fromJson(params);
final collection = Isar.getInstance(query.instance)!

diff:
@@ -10,7 +10,8 @@ enum ConnectAction {
executeQuery('ext.isar.executeQuery'),
removeQuery('ext.isar.removeQuery'),
exportQuery('ext.isar.exportQuery'),
- exportJson('ext.isar.exportJson');
+ exportJson('ext.isar.exportJson'),
+ editProperty('ext.isar.editProperty');
const ConnectAction(this.method);
@@ -123,6 +124,46 @@ class ConnectQuery {
}
}
+class ConnectEdit {
+ ConnectEdit({
+ required this.instance,
+ required this.collection,
+ required this.id,
+ required this.property,
+ this.index,
+ required this.value,
+ });
+
+ factory ConnectEdit.fromJson(Map<String, dynamic> json) {
+ return ConnectEdit(
+ instance: json['instance'] as String,
+ collection: json['collection'] as String,
+ id: json['id'] as int,
+ index: json['index'] as int?,
+ property: json['property'] as String,
+ value: json['value'],
+ );
+ }
+
+ final String instance;
+ final String collection;
+ final int id;
+ final String property;
+ final int? index;
+ final dynamic value;
+
+ Map<String, dynamic> toJson() {
+ return {
+ 'instance': instance,
+ 'collection': collection,
+ 'id': id,
+ 'index': index,
+ 'property': property,
+ 'value': value,
+ };
+ }
+}
+
class ConnectCollectionInfo {
ConnectCollectionInfo({
required this.instance,

diff:
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:isar/isar.dart';
+import 'package:isar_inspector/schema.dart';
+
+class EditPopup extends StatefulWidget {
+ const EditPopup({
+ 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();
+}
+
+class _EditPopupState extends State<EditPopup> {
+ final _formKey = GlobalKey<FormState>();
+ final _controller = TextEditingController();
+ final _focus = FocusNode();
+ RegExp? _re;
+
+ 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;
+ } else {
+ _controller.text = _null ? '' : widget.value.toString();
+ _controller.selection = TextSelection(
+ baseOffset: 0,
+ 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 (!_null) {
+ _focus.requestFocus();
+ }
+ }
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: widget.type == IsarType.String ? 500 : 300,
+ //height: 210,
+ 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>(
+ value: _boolValue,
+ items: const [
+ DropdownMenuItem(
+ value: true,
+ child: Text('TRUE'),
+ ),
+ DropdownMenuItem(
+ value: false,
+ child: Text('FALSE'),
+ ),
+ ],
+ onChanged: _null
+ ? null
+ : (value) {
+ if (value != null) {
+ setState(() {
+ _boolValue = value;
+ });
+ }
+ },
+ ),
+ ),
+ const SizedBox(height: 15),
+ ],
+ if (widget.type != IsarType.Bool) ...[
+ Form(
+ key: _formKey,
+ child: TextFormField(
+ controller: _controller,
+ focusNode: _focus,
+ enabled: !_null,
+ decoration: InputDecoration(
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(20),
+ borderSide: const BorderSide(color: Colors.white),
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ contentPadding: const EdgeInsets.all(20),
+ ),
+ inputFormatters: [
+ if (_re != null) FilteringTextInputFormatter.allow(_re!)
+ ],
+ maxLines: widget.type == IsarType.String ? 3 : 1,
+ validator: (value) {
+ if (widget.type == IsarType.Byte) {
+ final val = int.parse(value!);
+ if (val < 0 || val > 255) {
+ return 'Byte values must between 0-255';
+ }
+ }
+ return null;
+ },
+ onFieldSubmitted: (_) => _save(),
+ ),
+ ),
+ const SizedBox(height: 15),
+ ],
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton(onPressed: _save, child: const Text('Save')),
+ ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ ],
+ )
+ ],
+ ),
+ );
+ }
+
+ void _save() {
+ dynamic value;
+
+ if (_null) {
+ value = null;
+ } else {
+ if (widget.type == IsarType.Bool) {
+ value = _boolValue;
+ } else {
+ if (_formKey.currentState!.validate()) {
+ //ignore: missing_enum_constant_in_switch
+ switch (widget.type) {
+ case IsarType.Float:
+ case IsarType.Double:
+ value = double.tryParse(_controller.text) ?? 0.0;
+ break;
+
+ case IsarType.Byte:
+ case IsarType.Int:
+ case IsarType.Long:
+ value = int.tryParse(_controller.text) ?? 0;
+ break;
+ }
+ value ??= _controller.text;
+ }
+ }
+ }
+
+ Navigator.pop(context, {'value': value});
+ }
+}

diff:
import 'dart:core';
+import 'package:isar/isar.dart';
import 'package:json_annotation/json_annotation.dart';
part 'schema.g.dart';
@@ -64,6 +65,7 @@ enum IsarType {
Int,
Float,
Long,
+ Byte,
Double,
String,
ByteList,
@@ -73,4 +75,43 @@ enum IsarType {
DoubleList,
StringList,
BoolList;
+
+ bool get isList {
+ return const [
+ ByteList,
+ IntList,
+ FloatList,
+ LongList,
+ DoubleList,
+ StringList,
+ BoolList
+ ].contains(this);
+ }
+
+ IsarType get childType {
+ //ignore: missing_enum_constant_in_switch
+ switch (this) {
+ case ByteList:
+ return Byte;
+
+ case IntList:
+ return Int;
+
+ case FloatList:
+ return Float;
+
+ case LongList:
+ return Long;
+
+ case DoubleList:
+ return Double;
+
+ case StringList:
+ return String;
+
+ case BoolList:
+ return Bool;
+ }
+ throw IsarError('new IsarType ($name), rule not defined');
+ }
}

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


message:
Show query count and pages, some fixes
scrollbar fixed

diff:
@@ -137,25 +137,28 @@ abstract class _IsarConnect {
);
}
- static Future<List<Map<String, dynamic>>> _executeQuery(
+ static Future<Map<String, dynamic>> _executeQuery(
Map<String, dynamic> params,
) async {
if (_querySubscription != null) {
unawaited(_querySubscription!.cancel());
}
_querySubscription = null;
- if (params.isEmpty) {
- return <Map<String, dynamic>>[];
- }
final query = _getQuery(params);
+ params.remove('limit');
+ params.remove('offset');
+ final countQuery = _getQuery(params);
final stream = query.watchLazy();
_querySubscription = stream.listen((event) {
postEvent(ConnectEvent.queryChanged.event, {});
});
- return query.exportJson();
+ return {
+ 'results': await query.exportJson(),
+ 'count': await countQuery.count(),
+ };
}
static Future<bool> _removeQuery(Map<String, dynamic> params) async {

diff:
@@ -6,6 +6,7 @@ import 'package:isar_inspector/desktop/download.dart'
import 'package:isar_inspector/state/collections_state.dart';
import 'package:isar_inspector/state/instances_state.dart';
import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
+import 'package:isar_inspector/state/query_state.dart';
class CollectionsList extends ConsumerWidget {
const CollectionsList({super.key});
@@ -30,7 +31,12 @@ class CollectionsList extends ConsumerWidget {
color: collection == selectedCollection ? theme.primaryColor : null,
radius: BorderRadius.circular(60),
onTap: () {
- ref.read(selectedCollectionNamePod.state).state = collection.name;
+ if (ref.read(selectedCollectionNamePod.state).state !=
+ collection.name) {
+ ref.read(selectedCollectionNamePod.state).state =
+ collection.name;
+ ref.read(queryPagePod.state).state = 1;
+ }
},
child: Padding(
padding: const EdgeInsets.only(left: 25, right: 10),

diff:
@@ -37,9 +37,9 @@ class _ConnectedLayoutState extends ConsumerState<ConnectedLayout> {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- const FilterField(),
- const SizedBox(height: 25),
+ children: const [
+ FilterField(),
+ SizedBox(height: 25),
Expanded(
child: QueryTable(),
),

diff:
@@ -44,6 +44,9 @@ class _AppState extends State<App> {
secondary: Colors.blue,
brightness: Brightness.dark,
),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: MaterialStateProperty.all(Colors.grey),
+ ),
),
home: ColoredPage(
child: _port != null

diff:
@@ -85,7 +85,7 @@ class _EditPopupState extends State<EditPopup> {
),
const SizedBox(height: 15),
],
- if (widget.type == IsarType.Bool) ...[
+ if (widget.type == IsarType.Bool)
DropdownButtonHideUnderline(
child: DropdownButton<bool>(
value: _boolValue,
@@ -109,10 +109,8 @@ class _EditPopupState extends State<EditPopup> {
}
},
),
- ),
- const SizedBox(height: 15),
- ],
- if (widget.type != IsarType.Bool) ...[
+ )
+ else
Form(
key: _formKey,
child: TextFormField(
@@ -146,7 +144,6 @@ class _EditPopupState extends State<EditPopup> {
),
),
const SizedBox(height: 15),
- ],
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [

diff:
@@ -56,7 +56,7 @@ class _FilterFieldState extends ConsumerState<FilterField> {
final selectedCollection = ref.read(selectedCollectionPod).value!;
final filter = _parseFilter(selectedCollection);
ref.read(queryFilterPod.state).state = filter;
- ref.read(queryPagePod.state).state = 0;
+ ref.read(queryPagePod.state).state = 1;
},
child: const Text('Query'),
),

diff:
@@ -11,35 +11,72 @@ class PrevNext extends ConsumerWidget {
final theme = Theme.of(context);
final page = ref.watch(queryPagePod);
final result = ref.watch(queryResultsPod).valueOrNull;
- return Column(
+
+ int from, to, total, pages = 1;
+ if (result != null && result.count > 0) {
+ from = (page - 1) * objectsPerPage + 1;
+ to = from + objectsPerPage - 1;
+ total = result.count;
+
+ if (to > total) {
+ to = total;
+ }
+
+ pages = (total / objectsPerPage).ceil();
+ } else {
+ from = to = total = 0;
+ }
+
+ return Stack(
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ Align(
+ alignment: Alignment.bottomLeft,
+ child: Text('Displaying objects $from - $to of $total'),
+ ),
+ Align(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
children: [
_Button(
label: 'Prev',
- onPressed: () {
- if (page > 0) {
- ref.read(queryPagePod.state).state -= 1;
- }
- },
+ onPressed: page > 1
+ ? () => ref.read(queryPagePod.state).state -= 1
+ : null,
),
const SizedBox(width: 10),
- Text(
- 'Page ${page + 1}',
- style: TextStyle(fontSize: 12, color: theme.hintColor),
+ DropdownButtonHideUnderline(
+ child: ButtonTheme(
+ alignedDropdown: true,
+ child: DropdownButton<int>(
+ icon: const Icon(Icons.arrow_drop_up),
+ value: page,
+ items: List.generate(
+ pages,
+ (page) => DropdownMenuItem(
+ value: page + 1,
+ child: Center(child: Text('Page ${page + 1}')),
+ ),
+ ),
+ onChanged: pages > 1
+ ? (p) {
+ if (p != null) {
+ ref.read(queryPagePod.state).state = p;
+ }
+ }
+ : null,
+ ),
+ ),
),
const SizedBox(width: 10),
_Button(
label: 'Next',
- onPressed: () {
- if (result?.hasMore ?? false) {
- ref.read(queryPagePod.state).state += 1;
- }
- },
+ onPressed: to == total
+ ? null
+ : () => ref.read(queryPagePod.state).state += 1,
),
],
),
+ ),
],
);
}
@@ -52,6 +89,7 @@ class _Button extends StatelessWidget {
required this.label,
this.onPressed,
});
+
final String label;
final VoidCallback? onPressed;

diff:
@@ -26,9 +26,7 @@ typedef Editor = void Function(
);
class QueryTable extends ConsumerWidget {
- QueryTable({super.key});
-
- final _controller = ScrollController();
+ const QueryTable({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -44,11 +42,8 @@ class QueryTable extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
- child: RawScrollbar(
- controller: _controller,
- thumbColor: Colors.grey,
child: ListView.separated(
- controller: _controller,
+ controller: ScrollController(),
itemCount: objects.length,
itemBuilder: (context, index) {
return Stack(
@@ -112,7 +107,6 @@ class QueryTable extends ConsumerWidget {
},
),
),
- ),
const SizedBox(height: 20),
const PrevNext(),
],

diff:
@@ -147,13 +147,12 @@ class IsarConnectStateNotifier
);
}
- Future<List<Map<String, Object?>>> executeQuery(ConnectQuery query) async {
- final objects = await _call<List<dynamic>>(
+ Future<Map<String, Object?>> executeQuery(ConnectQuery query) async {
+ return _call<Map<String, Object?>>(
ConnectAction.executeQuery,
args: query.toJson(),
timeout: kLongTimeout,
);
- return objects.cast();
}
Future<void> removeQuery(ConnectQuery query) async {

diff:
-import 'dart:async';
-
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
@@ -11,21 +9,26 @@ const int objectsPerPage = 50;
class QueryObject {
const QueryObject(this.data);
+
final Map<String, dynamic> data;
dynamic getValue(String propertyName) => data[propertyName];
}
-final queryPagePod = StateProvider((ref) => 0);
+final queryPagePod = StateProvider((ref) => 1);
final queryFilterPod = StateProvider<FilterOperation?>((ref) => null);
final querySortPod = StateProvider<SortProperty?>((ref) => null);
class QueryResult {
- QueryResult({required this.objects, required this.hasMore});
+ QueryResult({
+ required this.objects,
+ required this.count,
+ });
+
final List<QueryObject> objects;
- final bool hasMore;
+ final int count;
}
final queryResultsPod = FutureProvider<QueryResult>((ref) async {
@@ -42,23 +45,20 @@ final queryResultsPod = FutureProvider<QueryResult>((ref) async {
collection: selectedCollection.name,
filter: filter,
sortProperty: sort,
- offset: page * objectsPerPage,
- limit: objectsPerPage + 1,
+ offset: (page - 1) * objectsPerPage,
+ limit: objectsPerPage,
),
);
- final objects = result.map(QueryObject.new).toList();
-
- if (objects.isEmpty && page != 0) {
- Timer.run(() {
- ref.read(queryPagePod.state).state = 0;
- });
- }
+ //ignore: avoid_dynamic_calls
+ final objects = (result['results']! as List<dynamic>)
+ .map((e) => e as Map<String, dynamic>)
+ .toList()
+ .map(QueryObject.new)
+ .toList();
return QueryResult(
- objects: objects.length > objectsPerPage
- ? objects.sublist(0, objectsPerPage - 1)
- : objects,
- hasMore: objects.length > objectsPerPage,
+ objects: objects,
+ count: result['count']! as int,
);
});

diff:
@@ -72,6 +72,9 @@ class App extends StatelessWidget {
secondary: Colors.blue,
brightness: Brightness.dark,
),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: MaterialStateProperty.all(Colors.grey),
+ ),
),
),
);


message: Implemented multi entry 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';
+
+part 'multi_entry_test.g.dart';
+
+@Collection()
+class MultiEntryIndexModel {
+ MultiEntryIndexModel({
+ required this.bools,
+ required this.ints,
+ required this.doubles,
+ required this.dateTimes,
+ required this.stringsSensitive,
+ required this.stringsInsensitive,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(type: IndexType.value)
+ final List<bool> bools;
+
+ @Index(type: IndexType.value)
+ final List<int> ints;
+
+ @Index(type: IndexType.value)
+ final List<double> doubles;
+
+ @Index(type: IndexType.value)
+ final List<DateTime> dateTimes;
+
+ @Index(type: IndexType.value, caseSensitive: true)
+ final List<String> stringsSensitive;
+
+ @Index(type: IndexType.value, caseSensitive: false)
+ final List<String> stringsInsensitive;
+
+ @override
+ String toString() {
+ return 'MultiEntryIndexModel{id: $id}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is MultiEntryIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ listEquals(bools, other.bools) &&
+ listEquals(ints, other.ints) &&
+ listEquals(doubles, other.doubles) &&
+ listEquals(dateTimes, other.dateTimes) &&
+ listEquals(stringsSensitive, other.stringsSensitive) &&
+ listEquals(stringsInsensitive, other.stringsInsensitive);
+}
+
+@Collection()
+class MultiEntryNullableIndexModel {
+ MultiEntryNullableIndexModel({
+ required this.bools,
+ required this.ints,
+ required this.doubles,
+ required this.dateTimes,
+ required this.stringsSensitive,
+ required this.stringsInsensitive,
+ });
+
+ Id id = Isar.autoIncrement;
+
+ @Index(type: IndexType.value)
+ final List<bool?>? bools;
+
+ @Index(type: IndexType.value)
+ final List<int?>? ints;
+
+ @Index(type: IndexType.value)
+ final List<double?>? doubles;
+
+ @Index(type: IndexType.value)
+ final List<DateTime?>? dateTimes;
+
+ @Index(type: IndexType.value, caseSensitive: true)
+ final List<String?>? stringsSensitive;
+
+ @Index(type: IndexType.value, caseSensitive: false)
+ final List<String?>? stringsInsensitive;
+
+ @override
+ String toString() {
+ return 'MultiEntryIndexModel{id: $id}';
+ }
+
+ @override
+ // ignore: hash_and_equals
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is MultiEntryNullableIndexModel &&
+ runtimeType == other.runtimeType &&
+ id == other.id &&
+ listEquals(bools, other.bools) &&
+ listEquals(ints, other.ints) &&
+ listEquals(doubles, other.doubles) &&
+ listEquals(dateTimes, other.dateTimes) &&
+ listEquals(stringsSensitive, other.stringsSensitive) &&
+ listEquals(stringsInsensitive, other.stringsInsensitive);
+}
+
+void main() {
+ group('Multi entry index', () {
+ late Isar isar;
+
+ late MultiEntryIndexModel obj0;
+ late MultiEntryIndexModel obj1;
+ late MultiEntryIndexModel obj2;
+ late MultiEntryIndexModel obj3;
+ late MultiEntryIndexModel obj4;
+ late MultiEntryIndexModel obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([MultiEntryIndexModelSchema]);
+
+ obj0 = MultiEntryIndexModel(
+ bools: [true, true],
+ ints: [0, 12, 0],
+ doubles: [0.0, 42.4, 52, 1],
+ dateTimes: [DateTime(2020)],
+ stringsSensitive: ['Tomatoes', 'Foo'],
+ stringsInsensitive: ['FOO', 'baR', 'FoO'],
+ );
+ obj1 = MultiEntryIndexModel(
+ bools: [],
+ ints: [28, 65535, 400],
+ doubles: [3.141592658979],
+ dateTimes: [DateTime(1920, 8, 9), DateTime(2000, 2, 2)],
+ stringsSensitive: ['books', 'foo'],
+ stringsInsensitive: ['BAR'],
+ );
+ obj2 = MultiEntryIndexModel(
+ bools: [],
+ ints: [],
+ doubles: [],
+ dateTimes: [],
+ stringsSensitive: [],
+ stringsInsensitive: [],
+ );
+ obj3 = MultiEntryIndexModel(
+ bools: [false],
+ ints: [123, -123, -0],
+ doubles: [123.512312512, 1123.43],
+ dateTimes: [DateTime(1921, 1, 29), DateTime(1456, 3, 11)],
+ stringsSensitive: ['fork', 'John'],
+ stringsInsensitive: ['PoTaToEs', ''],
+ );
+ obj4 = MultiEntryIndexModel(
+ bools: [true, true, true, false],
+ ints: [42, 0042, 0xffff],
+ doubles: [0x0042, 43.4123],
+ dateTimes: [DateTime(2022, 1, 3), DateTime(1921, 1, 29)],
+ stringsSensitive: ['forks'],
+ stringsInsensitive: ['potato', 'TOMATO'],
+ );
+ obj5 = MultiEntryIndexModel(
+ bools: [false, false, false],
+ ints: [-154123, 3],
+ doubles: [234.34, -123e3],
+ dateTimes: [DateTime(1204512, 1, 3), DateTime(1970)],
+ stringsSensitive: ['FORK', 'Joe'],
+ stringsInsensitive: ['foo', 'bar', 'tomato', 'fries'],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.multiEntryIndexModels.tPutAll([
+ obj0,
+ obj1,
+ obj2,
+ obj3,
+ obj4,
+ obj5,
+ ]),
+ );
+ });
+
+ tearDown(() => isar.close());
+
+ isarTest('Bools query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().boolsElementEqualTo(true).tFindAll(),
+ [obj0, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .boolsElementNotEqualTo(false)
+ .tFindAll(),
+ [obj0, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .boolsElementEqualTo(false)
+ .tFindAll(),
+ [obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .boolsElementNotEqualTo(true)
+ .tFindAll(),
+ [obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().anyBoolsElement().tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .boolsElementEqualTo(true)
+ .or()
+ .boolsElementEqualTo(true)
+ .or()
+ .boolsElementEqualTo(true)
+ .tFindAll(),
+ [obj0, obj4],
+ );
+ return;
+ });
+
+ isarTest('Ints query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().intsElementEqualTo(42).tFindAll(),
+ [obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementEqualTo(0xffff)
+ .tFindAll(),
+ [obj1, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementNotEqualTo(0xffff)
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().intsElementLessThan(0).tFindAll(),
+ [obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementLessThan(0, include: true)
+ .tFindAll(),
+ [obj0, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementGreaterThan(42)
+ .tFindAll(),
+ [obj1, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().intsElementNotEqualTo(0).tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().intsElementBetween(0, 20).tFindAll(),
+ [obj0, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementEqualTo(0xffff)
+ .or()
+ .intsElementLessThan(-42)
+ .tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .intsElementBetween(0, 8)
+ .or()
+ .intsElementBetween(0, 9)
+ .tFindAll(),
+ [obj0, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().anyIntsElement().tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Doubles query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().doublesElementLessThan(0).tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .doublesElementGreaterThan(123.456)
+ .tFindAll(),
+ [obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .doublesElementBetween(2, 64)
+ .tFindAll(),
+ [obj0, obj1, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().anyDoublesElement().tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .doublesElementGreaterThan(123.456)
+ .or()
+ .doublesElementGreaterThan(123.456)
+ .tFindAll(),
+ [obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .doublesElementBetween(0.0000001, 0.0002)
+ .tFindAll(),
+ [],
+ );
+ });
+
+ isarTest('DateTimes query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementEqualTo(DateTime(1970))
+ .tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementEqualTo(DateTime(2000, 2, 2))
+ .tFindAll(),
+ [obj1],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementNotEqualTo(DateTime(2020))
+ .tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementLessThan(DateTime(2000))
+ .tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementLessThan(DateTime(0))
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementGreaterThan(DateTime(2010, 4, 22))
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementBetween(DateTime(2005), DateTime(2030))
+ .tFindAll(),
+ [obj0, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels.where().anyDateTimesElement().tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .dateTimesElementEqualTo(DateTime(1970))
+ .or()
+ .dateTimesElementBetween(DateTime(1969), DateTime(1971))
+ .tFindAll(),
+ [obj5],
+ );
+ });
+
+ isarTest('Strings sensitive query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementEqualTo('fork')
+ .tFindAll(),
+ [obj3],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementNotEqualTo('forks')
+ .tFindAll(),
+ [obj0, obj1, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementLessThan('Greek')
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementGreaterThan('bee')
+ .tFindAll(),
+ [obj1, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementBetween('Fork', 'Potato')
+ .tFindAll(),
+ [obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('FO')
+ .tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('fork')
+ .tFindAll(),
+ [obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('Qwerty')
+ .tFindAll(),
+ [],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .anyStringsSensitiveElement()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsSensitiveElementEqualTo('foo')
+ .or()
+ .stringsSensitiveElementEqualTo('foo')
+ .tFindAll(),
+ [obj1],
+ );
+ });
+
+ isarTest('Strings insensitive query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementEqualTo('foo')
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementEqualTo('')
+ .tFindAll(),
+ [obj3],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementNotEqualTo('bar')
+ .tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementLessThan('D')
+ .tFindAll(),
+ [obj0, obj1, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementGreaterThan('KeTcHuP')
+ .tFindAll(),
+ [obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementBetween('abc', 'def')
+ .tFindAll(),
+ [obj0, obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .anyStringsInsensitiveElement()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Multi index query', () async {
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .boolsElementEqualTo(true)
+ .or()
+ .dateTimesElementEqualTo(DateTime(2020))
+ .or()
+ .intsElementEqualTo(123)
+ .tFindAll(),
+ [obj0, obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryIndexModels
+ .where()
+ .stringsInsensitiveElementStartsWith('po')
+ .or()
+ .stringsSensitiveElementStartsWith('for')
+ .or()
+ .boolsElementNotEqualTo(false)
+ .tFindAll(),
+ [obj0, obj3, obj4],
+ );
+ });
+ });
+
+ group('Multi entry nullable index', () {
+ late Isar isar;
+
+ late MultiEntryNullableIndexModel obj0;
+ late MultiEntryNullableIndexModel obj1;
+ late MultiEntryNullableIndexModel obj2;
+ late MultiEntryNullableIndexModel obj3;
+ late MultiEntryNullableIndexModel obj4;
+ late MultiEntryNullableIndexModel obj5;
+
+ setUp(() async {
+ isar = await openTempIsar([MultiEntryNullableIndexModelSchema]);
+
+ obj0 = MultiEntryNullableIndexModel(
+ bools: [true, null],
+ ints: null,
+ doubles: [42.42, 20, -400],
+ dateTimes: [DateTime(2020), DateTime(2030, 3, 23)],
+ stringsSensitive: [null, ''],
+ stringsInsensitive: ['FOO', 'baR'],
+ );
+ obj1 = MultiEntryNullableIndexModel(
+ bools: [null],
+ ints: [42, 64, 32],
+ doubles: null,
+ dateTimes: null,
+ stringsSensitive: ['Tomatoes', 'Potatoes'],
+ stringsInsensitive: ['foo', 'BAR', null],
+ );
+ obj2 = MultiEntryNullableIndexModel(
+ bools: null,
+ ints: null,
+ doubles: null,
+ dateTimes: null,
+ stringsSensitive: null,
+ stringsInsensitive: null,
+ );
+ obj3 = MultiEntryNullableIndexModel(
+ bools: [null],
+ ints: [null],
+ doubles: [null],
+ dateTimes: [null],
+ stringsSensitive: [null],
+ stringsInsensitive: [null],
+ );
+ obj4 = MultiEntryNullableIndexModel(
+ bools: [true, true, false],
+ ints: [null, null, 29],
+ doubles: [3.14159265358979, null, 0],
+ dateTimes: [
+ DateTime(1970, 2, 8),
+ DateTime(2000, 4, 3),
+ DateTime(1560, 8, 27),
+ ],
+ stringsSensitive: ['potato', 'fries', 'rice'],
+ stringsInsensitive: ['BAr', null, ''],
+ );
+ obj5 = MultiEntryNullableIndexModel(
+ bools: [true, null, false, null, true],
+ ints: [99, 0xffff, 42, 32],
+ doubles: [null, 24.32, 41.43],
+ dateTimes: [null, DateTime(1999, 3, 21)],
+ stringsSensitive: ['', '', 'Potatoes'],
+ stringsInsensitive: ['bar', 'BAR', null, 'foo'],
+ );
+
+ await isar.tWriteTxn(
+ () => isar.multiEntryNullableIndexModels.tPutAll([
+ obj0,
+ obj1,
+ obj2,
+ obj3,
+ obj4,
+ obj5,
+ ]),
+ );
+ });
+
+ tearDown(() => isar.close());
+
+ isarTest('Bools query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .boolsElementEqualTo(true)
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .boolsElementNotEqualTo(false)
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .boolsElementIsNull()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .boolsElementIsNotNull()
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels.where().anyBoolsElement().tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Ints query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementEqualTo(42)
+ .tFindAll(),
+ [obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementNotEqualTo(42)
+ .tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+
+ // FIXME: lessThan on nullable fields also return null values.
+ // We could fix this be adding a lower bound in the query and excluding it
+ // await qEqualSet(
+ // isar.multiEntryNullableIndexModels
+ // .where()
+ // .intsElementLessThan(0)
+ // .tFindAll(),
+ // [],
+ // );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementGreaterThan(90)
+ .tFindAll(),
+ [obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementBetween(0, 50)
+ .tFindAll(),
+ [obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementIsNotNull()
+ .tFindAll(),
+ [obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementIsNull()
+ .tFindAll(),
+ [obj3, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels.where().anyIntsElement().tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Doubles query', () async {
+ // FIXME: Same as ints query
+ // await qEqualSet(
+ // isar.multiEntryNullableIndexModels
+ // .where()
+ // .doublesElementLessThan(10)
+ // .tFindAll(),
+ // [obj0],
+ // );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .doublesElementGreaterThan(40)
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .doublesElementBetween(0, 32)
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .doublesElementIsNull()
+ .tFindAll(),
+ [obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .doublesElementIsNotNull()
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .anyDoublesElement()
+ .tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('DateTimes query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementEqualTo(DateTime(1999, 3, 21))
+ .or()
+ .dateTimesElementEqualTo(DateTime(2020))
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementNotEqualTo(DateTime(2020))
+ .tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
+
+ // FIXME: Same as ints lessThan
+ // Could fix with lower bound in generator
+ // await qEqualSet(
+ // isar.multiEntryNullableIndexModels
+ // .where()
+ // .dateTimesElementLessThan(DateTime(2000))
+ // .tFindAll(),
+ // [obj4, obj5],
+ // );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementGreaterThan(DateTime(2005, 1, 4))
+ .tFindAll(),
+ [obj0],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementBetween(DateTime(2000), DateTime(2020))
+ .tFindAll(),
+ [obj0, obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementIsNull()
+ .tFindAll(),
+ [obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementIsNotNull()
+ .tFindAll(),
+ [obj0, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .anyDateTimesElement()
+ .tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Strings sensitive query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementEqualTo('Potatoes')
+ .tFindAll(),
+ [obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementEqualTo('')
+ .tFindAll(),
+ [obj0, obj5],
+ );
+
+ // FIXME: Same issue as ints lessThan
+ // await qEqualSet(
+ // isar.multiEntryNullableIndexModels
+ // .where()
+ // .stringsSensitiveElementLessThan('aaaa')
+ // .tFindAll(),
+ // [obj0, obj1, obj5],
+ // );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementGreaterThan('aaaa')
+ .tFindAll(),
+ [obj4],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementBetween('P', 'b')
+ .tFindAll(),
+ [obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementIsNull()
+ .tFindAll(),
+ [obj0, obj3],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementIsNotNull()
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('Po')
+ .tFindAll(),
+ [obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('')
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .anyStringsSensitiveElement()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Strings insensitive query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementEqualTo('bAR')
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementEqualTo('')
+ .tFindAll(),
+ [obj4],
+ );
+
+ // FIXME: Same issue as ints lessThan
+ // await qEqualSet(
+ // isar.multiEntryNullableIndexModels
+ // .where()
+ // .stringsInsensitiveElementLessThan('desert')
+ // .tFindAll(),
+ // [obj0, obj1, obj4, obj5],
+ // );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementGreaterThan('bar')
+ .tFindAll(),
+ [obj0, obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementBetween('a', 'C')
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementIsNull()
+ .tFindAll(),
+ [obj1, obj3, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementIsNotNull()
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementStartsWith('ba')
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementStartsWith('f')
+ .tFindAll(),
+ [obj0, obj1, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .anyStringsInsensitiveElement()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+
+ isarTest('Multi index query', () async {
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .boolsElementIsNull()
+ .or()
+ .dateTimesElementIsNull()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementIsNotNull()
+ .or()
+ .dateTimesElementEqualTo(DateTime(2020))
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementStartsWith('fo')
+ .or()
+ .stringsInsensitiveElementStartsWith('b')
+ .tFindAll(),
+ [obj0, obj1, obj4, obj5],
+ );
+
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementGreaterThan(DateTime(2020))
+ .or()
+ .stringsInsensitiveElementIsNull()
+ .or()
+ .stringsInsensitiveElementIsNull()
+ .or()
+ .stringsInsensitiveElementIsNull()
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
+ });
+ });
+}

original_message: Implemented multi entry index test


message: Fixed nullable less than tests

diff:
@@ -736,15 +736,13 @@ void main() {
[obj1, obj3, obj4, obj5],
);
- // FIXME: lessThan on nullable fields also return null values.
- // We could fix this be adding a lower bound in the query and excluding it
- // await qEqualSet(
- // isar.multiEntryNullableIndexModels
- // .where()
- // .intsElementLessThan(0)
- // .tFindAll(),
- // [],
- // );
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .intsElementLessThan(0)
+ .tFindAll(),
+ [obj3, obj4],
+ );
await qEqualSet(
isar.multiEntryNullableIndexModels
@@ -785,14 +783,13 @@ void main() {
});
isarTest('Doubles query', () async {
- // FIXME: Same as ints query
- // await qEqualSet(
- // isar.multiEntryNullableIndexModels
- // .where()
- // .doublesElementLessThan(10)
- // .tFindAll(),
- // [obj0],
- // );
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .doublesElementLessThan(10)
+ .tFindAll(),
+ [obj0, obj3, obj4, obj5],
+ );
await qEqualSet(
isar.multiEntryNullableIndexModels
@@ -854,15 +851,13 @@ void main() {
[obj0, obj3, obj4, obj5],
);
- // FIXME: Same as ints lessThan
- // Could fix with lower bound in generator
- // await qEqualSet(
- // isar.multiEntryNullableIndexModels
- // .where()
- // .dateTimesElementLessThan(DateTime(2000))
- // .tFindAll(),
- // [obj4, obj5],
- // );
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .dateTimesElementLessThan(DateTime(2000))
+ .tFindAll(),
+ [obj3, obj4, obj5],
+ );
await qEqualSet(
isar.multiEntryNullableIndexModels
@@ -922,14 +917,13 @@ void main() {
[obj0, obj5],
);
- // FIXME: Same issue as ints lessThan
- // await qEqualSet(
- // isar.multiEntryNullableIndexModels
- // .where()
- // .stringsSensitiveElementLessThan('aaaa')
- // .tFindAll(),
- // [obj0, obj1, obj5],
- // );
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsSensitiveElementLessThan('aaaa')
+ .tFindAll(),
+ [obj0, obj1, obj3, obj5],
+ );
await qEqualSet(
isar.multiEntryNullableIndexModels
@@ -1005,14 +999,13 @@ void main() {
[obj4],
);
- // FIXME: Same issue as ints lessThan
- // await qEqualSet(
- // isar.multiEntryNullableIndexModels
- // .where()
- // .stringsInsensitiveElementLessThan('desert')
- // .tFindAll(),
- // [obj0, obj1, obj4, obj5],
- // );
+ await qEqualSet(
+ isar.multiEntryNullableIndexModels
+ .where()
+ .stringsInsensitiveElementLessThan('desert')
+ .tFindAll(),
+ [obj0, obj1, obj3, obj4, obj5],
+ );
await qEqualSet(
isar.multiEntryNullableIndexModels

original_message: Fixed nullable less than tests


message:
added SortBy in query builder
fix in query builder

diff:
@@ -38,10 +38,11 @@ class CollectionsList extends ConsumerWidget {
collection.name;
ref.read(queryFilterPod.state).state =
- collection.lastFilter == null
+ collection.uiFilter == null
? null
- : QueryBuilderUI.parseQuery(collection.lastFilter!);
+ : QueryBuilderUI.parseQuery(collection.uiFilter!);
+ ref.read(querySortPod.state).state = collection.uiSort;
ref.read(queryPagePod.state).state = 1;
}
},

diff:
@@ -61,9 +61,7 @@ class CheckBoxLabel extends StatelessWidget {
child: Row(
children: [
InkWell(
- onTap: onChanged != null
- ? () => onChanged!(!value)
- : null,
+ onTap: onChanged != null ? () => onChanged!(!value) : null,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(

diff:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:isar/isar.dart';
import 'package:isar_inspector/desktop/download.dart'
if (dart.library.html) 'package:isar_inspector/web/download.dart';
@@ -28,22 +29,28 @@ class _FilterFieldState extends ConsumerState<FilterField> {
ElevatedButton(
onPressed: () async {
final selectedCollection = ref.read(selectedCollectionPod).value!;
- final filter = await showDialog<QueryBuilderUIGroupHelper?>(
+ final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) {
return AlertDialog(
content: QueryBuilderUI(
collection: selectedCollection,
- filter: selectedCollection.lastFilter,
+ filter: selectedCollection.uiFilter,
+ sort: selectedCollection.uiSort,
),
);
},
);
- if (filter != null) {
- selectedCollection.lastFilter = filter;
+ if (result != null) {
+ final filter = result['filter'] as QueryBuilderUIGroupHelper;
+ final sort = result['sort'] as SortProperty?;
+
+ selectedCollection.uiFilter = filter;
+ selectedCollection.uiSort = sort;
ref.read(queryFilterPod.state).state =
QueryBuilderUI.parseQuery(filter);
+ ref.read(querySortPod.state).state = sort;
ref.read(queryPagePod.state).state = 1;
}
},
@@ -53,9 +60,9 @@ class _FilterFieldState extends ConsumerState<FilterField> {
ElevatedButton(
onPressed: () async {
final selectedCollection = ref.read(selectedCollectionPod).value!;
- final filter = selectedCollection.lastFilter == null
+ final filter = selectedCollection.uiFilter == null
? null
- : QueryBuilderUI.parseQuery(selectedCollection.lastFilter!);
+ : QueryBuilderUI.parseQuery(selectedCollection.uiFilter!);
final q = ConnectQuery(
instance: ref.read(selectedInstancePod).value!,
collection: selectedCollection.name,
@@ -71,9 +78,9 @@ class _FilterFieldState extends ConsumerState<FilterField> {
ElevatedButton(
onPressed: () {
final selectedCollection = ref.read(selectedCollectionPod).value!;
- final filter = selectedCollection.lastFilter == null
+ final filter = selectedCollection.uiFilter == null
? null
- : QueryBuilderUI.parseQuery(selectedCollection.lastFilter!);
+ : QueryBuilderUI.parseQuery(selectedCollection.uiFilter!);
final query = ConnectQuery(
instance: ref.read(selectedInstancePod).value!,
collection: selectedCollection.name,

diff:
@@ -6,10 +6,16 @@ import 'package:isar_inspector/common.dart';
import 'package:isar_inspector/schema.dart';
class QueryBuilderUI extends StatefulWidget {
- const QueryBuilderUI({super.key, required this.collection, this.filter});
+ const QueryBuilderUI({
+ super.key,
+ required this.collection,
+ this.filter,
+ this.sort,
+ });
final ICollection collection;
- final QueryBuilderUIHelper? filter;
+ final QueryBuilderUIGroupHelper? filter;
+ final SortProperty? sort;
@override
State<QueryBuilderUI> createState() => _QueryBuilderUIState();
@@ -113,14 +119,22 @@ class QueryBuilderUI extends StatefulWidget {
class _QueryBuilderUIState extends State<QueryBuilderUI> {
late final QueryBuilderUIGroupHelper _filter;
+ late SortProperty? _sort = widget.sort;
+ late final List<IProperty> _sortProps;
@override
void initState() {
super.initState();
- _filter = widget.filter == null
- ? QueryBuilderUIGroupHelper(QueryBuilderUIOP.and)
- : widget.filter! as QueryBuilderUIGroupHelper;
+ if (widget.filter != null) {
+ _filter = QueryBuilderUIGroupHelper(widget.filter!.operation)
+ ..children.addAll(widget.filter!.children);
+ } else {
+ _filter = QueryBuilderUIGroupHelper(QueryBuilderUIOP.and);
+ }
+
+ _sortProps =
+ widget.collection.allProperties.where((p) => !p.type.isList).toList();
}
@override
@@ -135,9 +149,7 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
Expanded(
child: SingleChildScrollView(
child: Container(
- constraints: BoxConstraints(
- minHeight: size.height - 260,
- ),
+ constraints: BoxConstraints(minHeight: size.height - 270),
child: _GroupUI(
helper: _filter,
collection: widget.collection,
@@ -150,7 +162,7 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
children: [
ElevatedButton(
onPressed: () {
- Navigator.pop(context, _filter);
+ Navigator.pop(context, {'filter': _filter, 'sort': _sort});
},
child: const Text('Build'),
),
@@ -159,13 +171,85 @@ class _QueryBuilderUIState extends State<QueryBuilderUI> {
onPressed: () {
Navigator.pop(
context,
- QueryBuilderUIGroupHelper(QueryBuilderUIOP.and),
+ {
+ 'filter': QueryBuilderUIGroupHelper(QueryBuilderUIOP.and),
+ 'sort': null
+ },
);
},
child: const Text('Clear'),
),
- ],
+ const Spacer(),
+ CheckBoxLabel(
+ value: _sort != null,
+ text: 'Sort By',
+ onChanged: (value) {
+ setState(() {
+ _sort = !value
+ ? null
+ : SortProperty(
+ property: _sortProps.first.name,
+ sort: Sort.asc,
+ );
+ });
+ },
+ ),
+ const SizedBox(width: 15),
+ DropdownButtonHideUnderline(
+ child: DropdownButton<String>(
+ value: _sort?.property ?? _sortProps.first.name,
+ items: _sortProps
+ .map(
+ (e) => DropdownMenuItem(
+ value: e.name,
+ child: Text(e.name),
+ ),
)
+ .toList(),
+ onChanged: _sort == null
+ ? null
+ : (value) {
+ if (value != null) {
+ setState(() {
+ _sort = SortProperty(
+ property: value,
+ sort: _sort!.sort,
+ );
+ });
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 15),
+ DropdownButtonHideUnderline(
+ child: DropdownButton<Sort>(
+ value: _sort?.sort ?? Sort.asc,
+ items: const [
+ DropdownMenuItem(
+ value: Sort.asc,
+ child: Text('ASC'),
+ ),
+ DropdownMenuItem(
+ value: Sort.desc,
+ child: Text('DESC'),
+ )
+ ],
+ onChanged: _sort == null
+ ? null
+ : (value) {
+ if (value != null) {
+ setState(() {
+ _sort = SortProperty(
+ property: _sort!.property,
+ sort: value,
+ );
+ });
+ }
+ },
+ ),
+ ),
+ ],
+ ),
],
),
);

diff:
@@ -49,8 +49,10 @@ class QueryTable extends ConsumerWidget {
return Stack(
children: [
TableBlock(
- key: ValueKey('${collection.name}'
- '_${objects[index].getValue(collection.idName)}'),
+ key: ValueKey(
+ '${collection.name}'
+ '_${objects[index].getValue(collection.idName)}',
+ ),
collection: collection,
object: objects[index],
editor: (id, property, index, value) {

diff:
@@ -26,7 +26,8 @@ class ICollection {
final String idName;
final List<IProperty> properties;
final List<ILink> links;
- QueryBuilderUIGroupHelper? lastFilter;
+ QueryBuilderUIGroupHelper? uiFilter;
+ SortProperty? uiSort;
late final List<IProperty> allProperties = [
IProperty(name: idName, type: IsarType.Long, isId: true),
@@ -41,8 +42,7 @@ class IProperty {
this.isId = false,
});
- factory IProperty.fromJson(Map<String, dynamic> json) =>
- IProperty(
+ factory IProperty.fromJson(Map<String, dynamic> json) => IProperty(
name: json['name'] as String,
type: IsarType.values.firstWhere((e) => e.name == json['type']),
isId: json['isId'] as bool? ?? false,

diff:
@@ -50,7 +50,6 @@ final queryResultsPod = FutureProvider<QueryResult>((ref) async {
),
);
- //ignore: avoid_dynamic_calls
final objects = (result['results']! as List<dynamic>)
.map((e) => e as Map<String, dynamic>)
.toList()


message:
Show indexes and Isar id
objectsPerPage changed to 20
some cleanup

diff:
+import 'dart:math';
+
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar_inspector/common.dart';
@@ -122,13 +124,8 @@ class CollectionsList extends ConsumerWidget {
}
String _formatSize(int bytes) {
- if (bytes < 10000) {
- return '${bytes.toStringAsFixed(2)}B';
- } else if (bytes < 1000000) {
- return '${(bytes / 1000).toStringAsFixed(2)}KB';
- } else if (bytes < 1000000000) {
- return '${(bytes / 1000000).toStringAsFixed(2)}MB';
- } else {
- return '${(bytes / 1000000000).toStringAsFixed(2)}GB';
- }
+ if (bytes <= 0) return '0 B';
+ const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ final i = (log(bytes) / log(1000)).floor();
+ return '${(bytes / pow(1000, i)).toStringAsFixed(2)} ${suffixes[i]}';
}

diff:
@@ -40,9 +40,7 @@ class _ConnectedLayoutState extends ConsumerState<ConnectedLayout> {
children: const [
FilterField(),
SizedBox(height: 25),
- Expanded(
- child: QueryTable(),
- ),
+ Expanded(child: QueryTable()),
],
),
)

diff:
@@ -28,7 +28,6 @@ class _AppState extends State<App> {
scaffoldBackgroundColor: Colors.transparent,
primaryColor: Colors.blue,
cardColor: const Color(0xff1f2128),
- // Color.fromARGB(255, 40, 41, 46),
dividerColor: const Color.fromARGB(255, 40, 41, 46),
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.all(Colors.blue),

diff:
@@ -67,7 +67,6 @@ class _EditPopupState extends State<EditPopup> {
Widget build(BuildContext context) {
return SizedBox(
width: widget.type == IsarType.String ? 500 : 300,
- //height: 210,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

diff:
@@ -10,19 +10,11 @@ import 'package:isar_inspector/state/instances_state.dart';
import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
import 'package:isar_inspector/state/query_state.dart';
-class FilterField extends ConsumerStatefulWidget {
+class FilterField extends ConsumerWidget {
const FilterField({super.key});
@override
- ConsumerState<FilterField> createState() => _FilterFieldState();
-}
-
-class _FilterFieldState extends ConsumerState<FilterField> {
- final TextEditingController controller = TextEditingController();
- String? error;
-
- @override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
return Row(
children: [
const SizedBox(width: 20),
@@ -93,10 +85,4 @@ class _FilterFieldState extends ConsumerState<FilterField> {
],
);
}
-
- @override
- void dispose() {
- controller.dispose();
- super.dispose();
- }
}

diff:
@@ -385,17 +385,34 @@ class _TableItemState extends State<TableItem> {
return RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
- text: '${widget.data.index ?? widget.data.property.name}: ',
+ text: '',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.white,
),
children: [
+ TextSpan(
+ text: '${widget.data.index ?? widget.data.property.name}',
+ style: TextStyle(
+ decoration: widget.data.property.isIndex
+ ? TextDecoration.underline
+ : null,
+ decorationThickness: 2,
+ decorationColor: Colors.green,
+ decorationStyle: TextDecorationStyle.wavy,
+ ),
+ ),
+ const TextSpan(text: ': '),
TextSpan(
text: value,
style: TextStyle(color: color),
),
+ if (widget.data.property.isId)
+ const TextSpan(
+ text: ' IsarId',
+ style: TextStyle(color: _disableColor),
+ ),
],
),
);

diff:
@@ -11,16 +11,33 @@ class ICollection {
required this.links,
});
- factory ICollection.fromJson(Map<String, dynamic> json) => ICollection(
+ factory ICollection.fromJson(Map<String, dynamic> json) {
+ final indexes = <String>{};
+ final jsonIndexes = json['indexes'] as List<dynamic>;
+
+ for (var i = 0; i < jsonIndexes.length; i++) {
+ indexes.addAll(
+ //ignore: avoid_dynamic_calls
+ (jsonIndexes[i]['properties'] as List<dynamic>).map(
+ (e) {
+ //ignore: avoid_dynamic_calls
+ return e['name'] as String;
+ },
+ ).toList(),
+ );
+ }
+
+ return ICollection(
name: json['name'] as String,
idName: json['idName'] as String,
properties: (json['properties'] as List<dynamic>)
- .map((e) => IProperty.fromJson(e as Map<String, dynamic>))
+ .map((e) => IProperty.fromJson(e as Map<String, dynamic>, indexes))
.toList(),
links: (json['links'] as List<dynamic>)
.map((e) => ILink.fromJson(e as Map<String, dynamic>))
.toList(),
);
+ }
final String name;
final String idName;
@@ -39,18 +56,23 @@ class IProperty {
const IProperty({
required this.name,
required this.type,
+ this.isIndex = false,
this.isId = false,
});
- factory IProperty.fromJson(Map<String, dynamic> json) => IProperty(
+ factory IProperty.fromJson(Map<String, dynamic> json, Set<String> indexes) {
+ return IProperty(
name: json['name'] as String,
type: IsarType.values.firstWhere((e) => e.name == json['type']),
isId: json['isId'] as bool? ?? false,
+ isIndex: indexes.contains(json['name']),
);
+ }
final String name;
final IsarType type;
final bool isId;
+ final bool isIndex;
}
class ILink {

diff:
@@ -5,7 +5,7 @@ import 'package:isar_inspector/state/collections_state.dart';
import 'package:isar_inspector/state/instances_state.dart';
import 'package:isar_inspector/state/isar_connect_state_notifier.dart';
-const int objectsPerPage = 50;
+const int objectsPerPage = 20;
class QueryObject {
const QueryObject(this.data);

diff:
@@ -56,7 +56,6 @@ class App extends StatelessWidget {
scaffoldBackgroundColor: Colors.transparent,
primaryColor: Colors.blue,
cardColor: const Color(0xff1f2128),
- // Color.fromARGB(255, 40, 41, 46),
dividerColor: const Color.fromARGB(255, 40, 41, 46),
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.all(Colors.blue),


message: fix loading links on unsaved object

diff:
@@ -203,7 +203,7 @@ abstract class IsarLinksCommon<OBJ> extends IsarLinkBaseImpl<OBJ>
bool get isLoaded => _isLoaded;
HashSet<OBJ> get _loadedObjects {
- if (!_isLoaded && !_kIsWeb) {
+ if (!_isLoaded && !_kIsWeb && _objectId != null) {
loadSync();
}
return _objects;

diff:
@@ -327,6 +327,21 @@ void main() {
expect(newA1.selfLinks, {objA2, objA3});
});
+ isarTest('save link in unsaved object', () async {
+ await isar.tWriteTxn(() => linksA.tPutAll([objA2, objA3]));
+
+ objA1.selfLinks.addAll([objA2, objA3]);
+
+ await isar.tWriteTxn(() async {
+ await linksA.tPut(objA1);
+ await objA1.selfLinks.tSave();
+ });
+
+ final newA1 = await linksA.tGet(objA1.id!);
+ await newA1!.selfLinks.tLoad();
+ expect(newA1.selfLinks, {objA2, objA3});
+ });
+
isarTest('delete source', () async {
await isar.tWriteTxn(() => linksA.tPutAll([objA1, objA2, objA3]));

original_message: fix loading links on unsaved object