Repository: isar-community/isar Branch: v3 Commit: 36e70529ac90 Files: 688 Total size: 22.4 MB Directory structure: gitextract_p8q538ro/ ├── .all-contributorsrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── actions/ │ │ └── prepare-build/ │ │ └── action.yaml │ ├── dependabot.yaml │ └── workflows/ │ ├── cron_test.yaml │ ├── docs.yaml │ ├── release.yaml │ ├── skynet.yaml │ ├── test.yaml │ └── testlab.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── TODO.md ├── docs/ │ ├── .gitignore │ ├── README.md │ ├── docs/ │ │ ├── .vuepress/ │ │ │ ├── config.ts │ │ │ ├── locales.ts │ │ │ ├── redirect.ts │ │ │ └── styles/ │ │ │ └── index.scss │ │ ├── README.md │ │ ├── crud.md │ │ ├── de/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── es/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── faq.md │ │ ├── fr/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── indexes.md │ │ ├── it/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── ja/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── ko/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── limitations.md │ │ ├── links.md │ │ ├── pt/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── queries.md │ │ ├── recipes/ │ │ │ ├── data_migration.md │ │ │ ├── full_text_search.md │ │ │ ├── multi_isolate.md │ │ │ └── string_ids.md │ │ ├── schema.md │ │ ├── transactions.md │ │ ├── tutorials/ │ │ │ └── quickstart.md │ │ ├── ur/ │ │ │ ├── README.md │ │ │ ├── crud.md │ │ │ ├── faq.md │ │ │ ├── indexes.md │ │ │ ├── limitations.md │ │ │ ├── links.md │ │ │ ├── queries.md │ │ │ ├── recipes/ │ │ │ │ ├── data_migration.md │ │ │ │ ├── full_text_search.md │ │ │ │ ├── multi_isolate.md │ │ │ │ └── string_ids.md │ │ │ ├── schema.md │ │ │ ├── transactions.md │ │ │ ├── tutorials/ │ │ │ │ └── quickstart.md │ │ │ └── watchers.md │ │ ├── watchers.md │ │ └── zh/ │ │ ├── README.md │ │ ├── crud.md │ │ ├── faq.md │ │ ├── indexes.md │ │ ├── limitations.md │ │ ├── links.md │ │ ├── queries.md │ │ ├── recipes/ │ │ │ ├── data_migration.md │ │ │ ├── full_text_search.md │ │ │ ├── multi_isolate.md │ │ │ └── string_ids.md │ │ ├── schema.md │ │ ├── transactions.md │ │ ├── tutorials/ │ │ │ └── quickstart.md │ │ └── watchers.md │ └── package.json ├── examples/ │ └── pub/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── lib/ │ │ ├── asset_loader.dart │ │ ├── main.dart │ │ ├── models/ │ │ │ ├── api/ │ │ │ │ ├── metrics.dart │ │ │ │ └── package.dart │ │ │ ├── asset.dart │ │ │ └── package.dart │ │ ├── package_manager.dart │ │ ├── provider.dart │ │ ├── repository.dart │ │ └── ui/ │ │ ├── app_bar.dart │ │ ├── detail_page.dart │ │ ├── home_page.dart │ │ ├── markdown_viewer.dart │ │ ├── package_metadata.dart │ │ ├── package_versions.dart │ │ ├── publisher.dart │ │ ├── search.dart │ │ └── search_page.dart │ └── pubspec.yaml ├── packages/ │ ├── isar/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── example/ │ │ │ └── README.md │ │ ├── lib/ │ │ │ ├── isar.dart │ │ │ └── src/ │ │ │ ├── annotations/ │ │ │ │ ├── backlink.dart │ │ │ │ ├── collection.dart │ │ │ │ ├── embedded.dart │ │ │ │ ├── enumerated.dart │ │ │ │ ├── ignore.dart │ │ │ │ ├── index.dart │ │ │ │ ├── name.dart │ │ │ │ └── type.dart │ │ │ ├── common/ │ │ │ │ ├── isar_common.dart │ │ │ │ ├── isar_link_base_impl.dart │ │ │ │ ├── isar_link_common.dart │ │ │ │ ├── isar_links_common.dart │ │ │ │ └── schemas.dart │ │ │ ├── isar.dart │ │ │ ├── isar_collection.dart │ │ │ ├── isar_connect.dart │ │ │ ├── isar_connect_api.dart │ │ │ ├── isar_error.dart │ │ │ ├── isar_link.dart │ │ │ ├── isar_reader.dart │ │ │ ├── isar_writer.dart │ │ │ ├── native/ │ │ │ │ ├── bindings.dart │ │ │ │ ├── encode_string.dart │ │ │ │ ├── index_key.dart │ │ │ │ ├── isar_collection_impl.dart │ │ │ │ ├── isar_core.dart │ │ │ │ ├── isar_impl.dart │ │ │ │ ├── isar_link_impl.dart │ │ │ │ ├── isar_reader_impl.dart │ │ │ │ ├── isar_writer_impl.dart │ │ │ │ ├── open.dart │ │ │ │ ├── query_build.dart │ │ │ │ ├── query_impl.dart │ │ │ │ ├── split_words.dart │ │ │ │ └── txn.dart │ │ │ ├── query.dart │ │ │ ├── query_builder.dart │ │ │ ├── query_builder_extensions.dart │ │ │ ├── query_components.dart │ │ │ ├── schema/ │ │ │ │ ├── collection_schema.dart │ │ │ │ ├── index_schema.dart │ │ │ │ ├── link_schema.dart │ │ │ │ ├── property_schema.dart │ │ │ │ └── schema.dart │ │ │ └── web/ │ │ │ ├── bindings.dart │ │ │ ├── isar_collection_impl.dart │ │ │ ├── isar_impl.dart │ │ │ ├── isar_link_impl.dart │ │ │ ├── isar_reader_impl.dart │ │ │ ├── isar_web.dart │ │ │ ├── isar_writer_impl.dart │ │ │ ├── open.dart │ │ │ ├── query_build.dart │ │ │ ├── query_impl.dart │ │ │ └── split_words.dart │ │ ├── pubspec.yaml │ │ ├── test/ │ │ │ └── isar_reader_writer_test.dart │ │ └── tool/ │ │ ├── get_version.dart │ │ └── verify_release_version.dart │ ├── isar_core/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── src/ │ │ │ ├── collection.rs │ │ │ ├── cursor.rs │ │ │ ├── error.rs │ │ │ ├── index/ │ │ │ │ ├── index_key.rs │ │ │ │ ├── index_key_builder.rs │ │ │ │ └── mod.rs │ │ │ ├── instance.rs │ │ │ ├── legacy/ │ │ │ │ ├── isar_object_v1.rs │ │ │ │ └── mod.rs │ │ │ ├── lib.rs │ │ │ ├── link.rs │ │ │ ├── mdbx/ │ │ │ │ ├── cursor.rs │ │ │ │ ├── db.rs │ │ │ │ ├── env.rs │ │ │ │ ├── mod.rs │ │ │ │ └── txn.rs │ │ │ ├── object/ │ │ │ │ ├── data_type.rs │ │ │ │ ├── id.rs │ │ │ │ ├── isar_object.rs │ │ │ │ ├── json_encode_decode.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── object_builder.rs │ │ │ │ └── property.rs │ │ │ ├── query/ │ │ │ │ ├── fast_wild_match.rs │ │ │ │ ├── filter.rs │ │ │ │ ├── id_where_clause.rs │ │ │ │ ├── index_where_clause.rs │ │ │ │ ├── link_where_clause.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── query_builder.rs │ │ │ │ └── where_clause.rs │ │ │ ├── schema/ │ │ │ │ ├── collection_schema.rs │ │ │ │ ├── index_schema.rs │ │ │ │ ├── link_schema.rs │ │ │ │ ├── migrate_v1.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── property_schema.rs │ │ │ │ └── schema_manager.rs │ │ │ ├── txn.rs │ │ │ └── watch/ │ │ │ ├── change_set.rs │ │ │ ├── isar_watchers.rs │ │ │ ├── mod.rs │ │ │ └── watcher.rs │ │ └── tests/ │ │ ├── binary_golden.json │ │ ├── test_binary.rs │ │ └── test_hash.rs │ ├── isar_core_ffi/ │ │ ├── .cargo/ │ │ │ └── config.toml │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── c_object_set.rs │ │ ├── crud.rs │ │ ├── dart.rs │ │ ├── error.rs │ │ ├── filter.rs │ │ ├── index_key.rs │ │ ├── instance.rs │ │ ├── lib.rs │ │ ├── link.rs │ │ ├── query.rs │ │ ├── query_aggregation.rs │ │ ├── txn.rs │ │ └── watchers.rs │ ├── isar_flutter_libs/ │ │ ├── .pubignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── android/ │ │ │ ├── .gitignore │ │ │ ├── build.gradle │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── settings.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── dev/ │ │ │ └── isar/ │ │ │ └── isar_flutter_libs/ │ │ │ └── IsarFlutterLibsPlugin.java │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ ├── Assets/ │ │ │ │ └── .gitkeep │ │ │ ├── Classes/ │ │ │ │ ├── IsarFlutterLibsPlugin.h │ │ │ │ ├── IsarFlutterLibsPlugin.m │ │ │ │ ├── SwiftIsarFlutterLibsPlugin.swift │ │ │ │ └── binding.h │ │ │ ├── Resources/ │ │ │ │ └── PrivacyInfo.xcprivacy │ │ │ └── isar_flutter_libs.podspec │ │ ├── lib/ │ │ │ └── isar_flutter_libs.dart │ │ ├── linux/ │ │ │ ├── CMakeLists.txt │ │ │ ├── include/ │ │ │ │ └── isar_flutter_libs/ │ │ │ │ └── isar_flutter_libs_plugin.h │ │ │ └── isar_flutter_libs_plugin.cc │ │ ├── macos/ │ │ │ ├── Classes/ │ │ │ │ └── IsarFlutterLibsPlugin.swift │ │ │ └── isar_flutter_libs.podspec │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── windows/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── include/ │ │ │ └── isar_flutter_libs/ │ │ │ └── isar_flutter_libs_plugin.h │ │ └── isar_flutter_libs_plugin.cpp │ ├── isar_generator/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── build.yaml │ │ ├── lib/ │ │ │ ├── isar_generator.dart │ │ │ └── src/ │ │ │ ├── code_gen/ │ │ │ │ ├── by_index_generator.dart │ │ │ │ ├── collection_schema_generator.dart │ │ │ │ ├── query_distinct_by_generator.dart │ │ │ │ ├── query_filter_generator.dart │ │ │ │ ├── query_filter_length.dart │ │ │ │ ├── query_link_generator.dart │ │ │ │ ├── query_object_generator.dart │ │ │ │ ├── query_property_generator.dart │ │ │ │ ├── query_sort_by_generator.dart │ │ │ │ ├── query_where_generator.dart │ │ │ │ └── type_adapter_generator.dart │ │ │ ├── collection_generator.dart │ │ │ ├── helper.dart │ │ │ ├── isar_analyzer.dart │ │ │ ├── isar_type.dart │ │ │ └── object_info.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── test/ │ │ ├── error_test.dart │ │ └── errors/ │ │ ├── class/ │ │ │ ├── abstract.dart │ │ │ ├── collection_supertype.dart │ │ │ ├── constructor_named.dart │ │ │ ├── constructor_unknown_parameter.dart │ │ │ ├── constructor_wrong_parameter.dart │ │ │ ├── enum.dart │ │ │ ├── invalid_name.dart │ │ │ ├── mixin.dart │ │ │ ├── private.dart │ │ │ └── variable.dart │ │ ├── id/ │ │ │ ├── duplicate.dart │ │ │ └── missing.dart │ │ ├── index/ │ │ │ ├── composite_double_not_last.dart │ │ │ ├── composite_non_hashed_list.dart │ │ │ ├── composite_string_value_not_last.dart │ │ │ ├── contains_id.dart │ │ │ ├── double_list_hashed.dart │ │ │ ├── duplicate_name.dart │ │ │ ├── duplicate_property.dart │ │ │ ├── invalid_name.dart │ │ │ ├── non_string_hashed.dart │ │ │ ├── non_string_list_hashed_elements.dart │ │ │ ├── non_unique_replace.dart │ │ │ ├── object_hashed.dart │ │ │ ├── object_list_hashed.dart │ │ │ └── property_does_not_exist.dart │ │ ├── link/ │ │ │ ├── backlink_target_does_no_exist.dart │ │ │ ├── backlink_target_is_backlink.dart │ │ │ ├── backlink_target_not_a_link.dart │ │ │ ├── duplicate_name.dart │ │ │ ├── invalid_name.dart │ │ │ ├── late.dart │ │ │ ├── nullable.dart │ │ │ ├── target_not_a_collection.dart │ │ │ └── type_nullable.dart │ │ └── property/ │ │ ├── duplicate_name.dart │ │ ├── enum_bool_type.dart │ │ ├── enum_double_type.dart │ │ ├── enum_duplicate.dart │ │ ├── enum_float_type.dart │ │ ├── enum_list_type.dart │ │ ├── enum_not_annotated.dart │ │ ├── enum_null_value.dart │ │ ├── enum_object_type.dart │ │ ├── invalid_name.dart │ │ ├── null_byte.dart │ │ ├── null_byte_element.dart │ │ └── unsupported_type.dart │ ├── isar_inspector/ │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── isar_inspector.iml │ │ ├── lib/ │ │ │ ├── collection/ │ │ │ │ ├── button_prev_next.dart │ │ │ │ ├── button_sort.dart │ │ │ │ ├── collection_area.dart │ │ │ │ └── objects_list_sliver.dart │ │ │ ├── collections_list.dart │ │ │ ├── connect_client.dart │ │ │ ├── connected_layout.dart │ │ │ ├── connection_screen.dart │ │ │ ├── error_screen.dart │ │ │ ├── instance_selector.dart │ │ │ ├── main.dart │ │ │ ├── object/ │ │ │ │ ├── isar_object.dart │ │ │ │ ├── object_view.dart │ │ │ │ ├── property_builder.dart │ │ │ │ ├── property_embedded_view.dart │ │ │ │ ├── property_link_view.dart │ │ │ │ ├── property_value.dart │ │ │ │ └── property_view.dart │ │ │ ├── query_builder/ │ │ │ │ ├── query_filter.dart │ │ │ │ └── query_group.dart │ │ │ ├── sidebar.dart │ │ │ └── util.dart │ │ ├── pubspec.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── isar_test/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── android/ │ │ │ ├── .gitignore │ │ │ ├── app/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ ├── androidTest/ │ │ │ │ │ └── java/ │ │ │ │ │ └── dev/ │ │ │ │ │ └── isar/ │ │ │ │ │ └── isar_test/ │ │ │ │ │ └── MainActivityTest.java │ │ │ │ └── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── dev/ │ │ │ │ │ └── isar/ │ │ │ │ │ └── isar_test/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values/ │ │ │ │ └── styles.xml │ │ │ ├── build.gradle │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── settings.gradle │ │ │ └── settings_aar.gradle │ │ ├── integration_test/ │ │ │ └── integration_test.dart │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ ├── Debug.xcconfig │ │ │ │ └── Release.xcconfig │ │ │ ├── Podfile │ │ │ ├── Runner/ │ │ │ │ ├── AppDelegate.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Base.lproj/ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ └── Main.storyboard │ │ │ │ ├── Info.plist │ │ │ │ └── Runner-Bridging-Header.h │ │ │ ├── Runner.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Runner.xcscheme │ │ │ └── Runner.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ ├── lib/ │ │ │ ├── isar_test.dart │ │ │ └── src/ │ │ │ ├── common.dart │ │ │ ├── init_native.dart │ │ │ ├── init_web.dart │ │ │ ├── isar_web_src.dart │ │ │ ├── listener.dart │ │ │ ├── matchers.dart │ │ │ ├── sync_async_helper.dart │ │ │ ├── sync_future.dart │ │ │ └── twitter/ │ │ │ ├── entities.dart │ │ │ ├── geo.dart │ │ │ ├── media.dart │ │ │ ├── tweet.dart │ │ │ ├── user.dart │ │ │ └── util.dart │ │ ├── linux/ │ │ │ ├── .gitignore │ │ │ ├── CMakeLists.txt │ │ │ ├── flutter/ │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── generated_plugin_registrant.cc │ │ │ │ ├── generated_plugin_registrant.h │ │ │ │ └── generated_plugins.cmake │ │ │ ├── main.cc │ │ │ ├── my_application.cc │ │ │ └── my_application.h │ │ ├── macos/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ │ └── Flutter-Release.xcconfig │ │ │ ├── Podfile │ │ │ ├── Runner/ │ │ │ │ ├── AppDelegate.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Base.lproj/ │ │ │ │ │ └── MainMenu.xib │ │ │ │ ├── Configs/ │ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ │ ├── Debug.xcconfig │ │ │ │ │ ├── Release.xcconfig │ │ │ │ │ └── Warnings.xcconfig │ │ │ │ ├── DebugProfile.entitlements │ │ │ │ ├── Info.plist │ │ │ │ ├── MainFlutterWindow.swift │ │ │ │ └── Release.entitlements │ │ │ ├── Runner.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Runner.xcscheme │ │ │ └── Runner.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── pubspec.yaml │ │ ├── test/ │ │ │ ├── clear_test.dart │ │ │ ├── collection_size_test.dart │ │ │ ├── compact_on_launch_test.dart │ │ │ ├── constructor_test.dart │ │ │ ├── copy_to_file_test.dart │ │ │ ├── crud_test.dart │ │ │ ├── default_value/ │ │ │ │ ├── common.dart │ │ │ │ ├── default_test.dart │ │ │ │ ├── no_default_test.dart │ │ │ │ └── nullable_test.dart │ │ │ ├── embedded_test.dart │ │ │ ├── enum_test.dart │ │ │ ├── filter/ │ │ │ │ ├── filter_bool_list_test.dart │ │ │ │ ├── filter_bool_test.dart │ │ │ │ ├── filter_byte_list_test.dart │ │ │ │ ├── filter_byte_test.dart │ │ │ │ ├── filter_date_time_list_test.dart │ │ │ │ ├── filter_date_time_test.dart │ │ │ │ ├── filter_embedded_list_test.dart │ │ │ │ ├── filter_embedded_test.dart │ │ │ │ ├── filter_float_list_test.dart │ │ │ │ ├── filter_float_test.dart │ │ │ │ ├── filter_id_test.dart │ │ │ │ ├── filter_int_test.dart │ │ │ │ ├── filter_list_length_test.dart │ │ │ │ ├── filter_string_list_test.dart │ │ │ │ ├── filter_string_test.dart │ │ │ │ └── link/ │ │ │ │ ├── filter_backlink_test.dart │ │ │ │ ├── filter_backlinks_test.dart │ │ │ │ ├── filter_link_circular_direct_test.dart │ │ │ │ ├── filter_link_circular_indirect_test.dart │ │ │ │ ├── filter_link_nested_test.dart │ │ │ │ ├── filter_link_self_test.dart │ │ │ │ ├── filter_link_test.dart │ │ │ │ ├── filter_links_self_test.dart │ │ │ │ └── filter_links_test.dart │ │ │ ├── id_test.dart │ │ │ ├── index/ │ │ │ │ ├── composite2_test.dart │ │ │ │ ├── composite3_test.dart │ │ │ │ ├── composite_string_test.dart │ │ │ │ ├── get_by_delete_by_test.dart │ │ │ │ ├── multi_entry_test.dart │ │ │ │ ├── put_by_test.dart │ │ │ │ ├── where_bool_list_test.dart │ │ │ │ ├── where_bool_test.dart │ │ │ │ ├── where_byte_list_test.dart │ │ │ │ ├── where_byte_test.dart │ │ │ │ ├── where_date_time_list_test.dart │ │ │ │ ├── where_date_time_test.dart │ │ │ │ ├── where_float_list_test.dart │ │ │ │ ├── where_float_test.dart │ │ │ │ ├── where_id_test.dart │ │ │ │ ├── where_int_test.dart │ │ │ │ ├── where_string_list_test.dart │ │ │ │ └── where_string_test.dart │ │ │ ├── inheritance_test.dart │ │ │ ├── instance_test.dart │ │ │ ├── isolate_test.dart │ │ │ ├── json_test.dart │ │ │ ├── link_test.dart │ │ │ ├── links/ │ │ │ │ ├── backlink_test.dart │ │ │ │ ├── link_test.dart │ │ │ │ └── links_test.dart │ │ │ ├── max_size_test.dart │ │ │ ├── migration/ │ │ │ │ ├── add_remove_collection_test.dart │ │ │ │ ├── add_remove_embedded_field_test.dart │ │ │ │ ├── add_remove_field_test.dart │ │ │ │ ├── add_remove_index_test.dart │ │ │ │ ├── add_remove_link_test.dart │ │ │ │ ├── change_field_embedded_test.dart │ │ │ │ ├── change_field_nullability_test.dart │ │ │ │ ├── change_field_type_test.dart │ │ │ │ └── change_link_links_test.dart │ │ │ ├── mutli_type_model.dart │ │ │ ├── name_test.dart │ │ │ ├── open_close_isar_listener_test.dart │ │ │ ├── other_test.dart │ │ │ ├── query/ │ │ │ │ ├── aggregation_test.dart │ │ │ │ ├── embedded_test.dart │ │ │ │ ├── group_test.dart │ │ │ │ ├── is_empty_is_not_empty_test.dart │ │ │ │ ├── multi_filter_test.dart │ │ │ │ ├── offset_limit_test.dart │ │ │ │ ├── property_test.dart │ │ │ │ ├── sort_by_distinct_by_test.dart │ │ │ │ └── where_sort_distinct_test.dart │ │ │ ├── regression/ │ │ │ │ └── issue_235_rename_field_test.dart │ │ │ ├── schema_test.dart │ │ │ ├── stress/ │ │ │ │ ├── long_string_test.dart │ │ │ │ └── twitter_test.dart │ │ │ ├── transaction_test.dart │ │ │ ├── type_models.dart │ │ │ ├── user_model.dart │ │ │ └── watcher_test.dart │ │ ├── tool/ │ │ │ ├── generate_all_tests.dart │ │ │ └── generate_long_double_test.dart │ │ ├── web/ │ │ │ ├── index.html │ │ │ └── manifest.json │ │ └── windows/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── flutter/ │ │ │ ├── CMakeLists.txt │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ └── generated_plugins.cmake │ │ └── runner/ │ │ ├── CMakeLists.txt │ │ ├── Runner.rc │ │ ├── flutter_window.cpp │ │ ├── flutter_window.h │ │ ├── main.cpp │ │ ├── resource.h │ │ ├── runner.exe.manifest │ │ ├── utils.cpp │ │ ├── utils.h │ │ ├── win32_window.cpp │ │ └── win32_window.h │ ├── isar_web/ │ │ ├── .eslintrc.yml │ │ ├── .gitignore │ │ ├── .prettierrc │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── bulk-delete.ts │ │ │ ├── collection.ts │ │ │ ├── cursor.ts │ │ │ ├── index.ts │ │ │ ├── instance.ts │ │ │ ├── link.ts │ │ │ ├── open.ts │ │ │ ├── query.ts │ │ │ ├── schema.ts │ │ │ ├── txn.ts │ │ │ └── watcher.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ └── mdbx_sys/ │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ └── lib.rs └── tool/ ├── build.sh ├── build_android.sh ├── build_ios.sh ├── build_linux.sh ├── build_macos.sh ├── build_windows.sh ├── cbindgen.toml ├── download_binaries.sh ├── ffigen.yaml ├── generate_bindings.sh ├── prepare_tests.sh ├── publish.sh └── replace-versions.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "files": [ "packages/isar/README.md" ], "imageSize": 100, "commit": false, "contributors": [ { "login": "Jtplouffe", "name": "JT", "avatar_url": "https://avatars.githubusercontent.com/u/32107801?v=4", "profile": "https://github.com/Jtplouffe", "contributions": [ "test", "bug" ] }, { "login": "leisim", "name": "Simon Leier", "avatar_url": "https://avatars.githubusercontent.com/u/13610195?v=4", "profile": "https://www.linkedin.com/in/simon-leier/", "contributions": [ "bug", "code", "doc", "test", "example" ] }, { "login": "h1376h", "name": "Hamed H.", "avatar_url": "https://avatars.githubusercontent.com/u/3498335?v=4", "profile": "https://github.com/h1376h", "contributions": [ "code", "maintenance" ] }, { "login": "Viper-Bit", "name": "Peyman", "avatar_url": "https://avatars.githubusercontent.com/u/24822764?v=4", "profile": "https://github.com/Viper-Bit", "contributions": [ "bug", "code" ] }, { "login": "blendthink", "name": "blendthink", "avatar_url": "https://avatars.githubusercontent.com/u/32213113?v=4", "profile": "https://github.com/blendthink", "contributions": [ "maintenance" ] }, { "login": "Moseco", "name": "Moseco", "avatar_url": "https://avatars.githubusercontent.com/u/10720298?v=4", "profile": "https://github.com/Moseco", "contributions": [ "bug" ] }, { "login": "Frostedfox", "name": "Frostedfox", "avatar_url": "https://avatars.githubusercontent.com/u/84601232?v=4", "profile": "https://github.com/Frostedfox", "contributions": [ "doc" ] }, { "login": "nohli", "name": "Joachim Nohl", "avatar_url": "https://avatars.githubusercontent.com/u/43643339?v=4", "profile": "http://achim.io", "contributions": [ "maintenance" ] }, { "login": "VoidxHoshi", "name": "LaLucid", "avatar_url": "https://avatars.githubusercontent.com/u/55886143?v=4", "profile": "https://github.com/VoidxHoshi", "contributions": [ "maintenance" ] }, { "login": "vothvovo", "name": "Johnson", "avatar_url": "https://avatars.githubusercontent.com/u/20894472?v=4", "profile": "https://github.com/vothvovo", "contributions": [ "bug" ] }, { "login": "ika020202", "name": "Ura", "avatar_url": "https://avatars.githubusercontent.com/u/42883378?v=4", "profile": "https://zenn.dev/urasan", "contributions": [ "translation" ] }, { "login": "mnkeis", "name": "mnkeis", "avatar_url": "https://avatars.githubusercontent.com/u/41247357?v=4", "profile": "https://github.com/mnkeis", "contributions": [ "translation" ] }, { "login": "CarloDotLog", "name": "Carlo Loguercio", "avatar_url": "https://avatars.githubusercontent.com/u/13763473?v=4", "profile": "https://github.com/CarloDotLog", "contributions": [ "translation" ] }, { "login": "hafeezrana", "name": "Hafeez Rana", "avatar_url": "https://avatars.githubusercontent.com/u/87476445?v=4", "profile": "https://g.dev/hafeezrana", "contributions": [ "doc" ] }, { "login": "inkomomutane", "name": "Nelson Mutane", "avatar_url": "https://avatars.githubusercontent.com/u/57417802?v=4", "profile": "https://github.com/inkomomutane", "contributions": [ "translation" ] }, { "login": "lodisy", "name": "Michael", "avatar_url": "https://avatars.githubusercontent.com/u/8101584?v=4", "profile": "https://github.com/lodisy", "contributions": [ "translation" ] }, { "login": "ritksm", "name": "Jack Rivers", "avatar_url": "https://avatars.githubusercontent.com/u/111809?v=4", "profile": "http://blog.jackrivers.me/", "contributions": [ "translation" ] }, { "login": "buraktabn", "name": "Burak", "avatar_url": "https://avatars.githubusercontent.com/u/49204989?v=4", "profile": "http://buraktaban.ca", "contributions": [ "bug" ] }, { "login": "AlexisL61", "name": "Alexis", "avatar_url": "https://avatars.githubusercontent.com/u/30233189?v=4", "profile": "https://github.com/AlexisL61", "contributions": [ "bug" ] }, { "login": "letyletylety", "name": "Lety", "avatar_url": "https://avatars.githubusercontent.com/u/16468579?v=4", "profile": "https://letyarch.blogspot.com/", "contributions": [ "doc" ] }, { "login": "nobkd", "name": "nobkd", "avatar_url": "https://avatars.githubusercontent.com/u/44443899?v=4", "profile": "https://github.com/nobkd", "contributions": [ "doc" ] } ], "contributorTemplate": "\">\" width=\"<%= options.imageSize %>px;\" alt=\"\"/>
<%= contributor.name %>
", "contributorsPerLine": 7, "contributorsSortAlphabetically": true, "projectName": "isar", "projectOwner": "isar", "repoType": "github", "repoHost": "https://github.com", "skipCi": true, "commitConvention": "angular" } ================================================ FILE: .github/FUNDING.yml ================================================ github: simc ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: problem assignees: leisim --- ### Steps to Reproduce Please describe exactly how to reproduce the problem you are running into. ### Code sample ```dart Provide a few simple lines of code to show your problem. ``` ### Details - Platform: iPhone 13 Pro, Galaxy S7, x86 Android Emulator on Windows etc. - Flutter version: [e.g. 3.0.0] - Isar version: [e.g. 2.5.0] --- - [ ] I searched for similar issues already - [ ] I filled the details section with the exact device model and version - [ ] I am able to provide a reproducible example ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/isar-community/isar/discussions about: Ask questions and discuss with other community members ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Version** - Platform: iOS, Android, Mac, Windows, Linux, Web - Flutter version: [e.g. 1.5.4] - Isar version: [e.g. 0.5.0] ================================================ FILE: .github/actions/prepare-build/action.yaml ================================================ name: "Prepare Build" description: "Prepares the build for Isar Core" runs: using: "composite" steps: - name: Install LLVM and Clang if: runner.os == 'Windows' uses: KyleMayes/install-llvm-action@v1 with: version: "11.0" directory: ${{ runner.temp }}/llvm - name: Set LIBCLANG_PATH if: runner.os == 'Windows' shell: bash run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV # See https://github.com/godot-rust/godot-rust/pull/920 - name: "Workaround Android NDK due to Rust bug" if: runner.os == 'Linux' || runner.os == 'macOS' shell: bash run: > find -L $ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION -name libunwind.a -execdir sh -c 'echo "INPUT(-lunwind)" > libgcc.a' \; ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/packages/isar" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/packages/isar_flutter_libs" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/packages/isar_generator" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/packages/isar_inspector" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/packages/isar_test" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "pub" directory: "/examples/pub" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "gradle" directory: "/packages/isar_flutter_libs/android" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "gradle" directory: "/packages/isar_test/android" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "npm" directory: "/docs" schedule: interval: "weekly" reviewers: - "leisim" - package-ecosystem: "npm" directory: "/packages/isar_web" schedule: interval: "weekly" reviewers: - "leisim" ================================================ FILE: .github/workflows/cron_test.yaml ================================================ name: Dart CI Cron on: schedule: - cron: "0 0 * * 0" jobs: testlab: uses: ./.github/workflows/testlab.yaml secrets: inherit ================================================ FILE: .github/workflows/docs.yaml ================================================ name: Unified Deploy Docs on: push: branches: - main - v3 jobs: deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} permissions: contents: write id-token: write pages: write steps: - name: Checkout v3 branch uses: actions/checkout@v4 with: ref: "v3" path: "v3" - name: Build v3 docs run: | cd v3 git fetch --unshallow git fetch --tags tool/replace-versions.sh cd docs sed -i'.bak' "s|base:.*|base: '/v3/',|" docs/.vuepress/config.ts sed -i 's|text: "vx.x"|text: "v3.x"|' docs/.vuepress/config.ts npm ci npm run build mv ./docs/.vuepress/dist ../../v3-docs - name: Checkout main branch uses: actions/checkout@v4 with: ref: "main" path: "main" - name: Build main docs run: | cd main git fetch --tags tool/replace-versions.sh cd docs sed -i'.bak' "s|base:.*|base: '/',|" docs/.vuepress/config.ts sed -i 's|text: "vx.x"|text: "v4.x"|' docs/.vuepress/config.ts npm ci npm run build mv ./docs/.vuepress/dist ../../main-docs - name: Prepare deployment directory run: | mkdir deploy mv main-docs/* deploy/ mkdir deploy/v3 mv v3-docs/* deploy/v3/ - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: deploy - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/release.yaml ================================================ name: Isar release on: push: tags: - "*" jobs: verify_version: name: Verify version matches release runs-on: ubuntu-latest strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] sdk: [3.0.0] steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Verify release version run: | flutter pub get dart tool/verify_release_version.dart ${{ github.ref_name }} working-directory: packages/isar build_binaries: name: Build Binaries needs: verify_version strategy: fail-fast: false matrix: include: - os: ubuntu-latest artifact_name: libisar_android_arm64.so script: build_android.sh - os: ubuntu-latest artifact_name: libisar_android_armv7.so script: build_android.sh armv7 - os: ubuntu-latest artifact_name: libisar_android_x64.so script: build_android.sh x64 - os: ubuntu-latest artifact_name: libisar_android_x86.so script: build_android.sh x86 - os: macos-latest artifact_name: isar_ios.xcframework.zip script: build_ios.sh - os: ubuntu-20.04 artifact_name: libisar_linux_x64.so script: build_linux.sh x64 - os: macos-latest artifact_name: libisar_macos.dylib script: build_macos.sh - os: windows-latest artifact_name: isar_windows_arm64.dll script: build_windows.sh - os: windows-latest artifact_name: isar_windows_x64.dll script: build_windows.sh x64 runs-on: ${{ matrix.os }} permissions: contents: write steps: - uses: actions/checkout@v4 - name: Prepare Build uses: ./.github/actions/prepare-build - name: Set env run: echo "ISAR_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV - name: Build binary run: bash tool/${{ matrix.script }} - name: Upload binary uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: ${{ matrix.artifact_name }} asset_name: ${{ matrix.artifact_name }} tag: ${{ github.ref }} testlab: needs: build_binaries uses: ./.github/workflows/testlab.yaml secrets: inherit build_inspector: name: Build Inspector needs: build_binaries runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Build run: flutter build web --base-href /${{ github.ref_name }}/ working-directory: packages/isar_inspector - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 with: folder: packages/isar_inspector/build/web repository-name: isar-community/inspector token: ${{ secrets.TOKEN }} target-folder: ${{ github.ref_name }} clean: false upload_to_repo: needs: build_binaries runs-on: ubuntu-latest steps: - name: Download all artifacts uses: actions/download-artifact@v2 with: path: binaries/ - name: List contents of downloaded artifacts run: | echo "Listing contents of all downloaded artifacts..." ls -Rlh binaries/ echo "Listing complete." - name: Setup Git and clone target repository run: | git config --global user.email "vicente.russo@gmail.com" git config --global user.name "GitHub Actions" git clone https://github.com/isar-community/binaries repo cd repo git checkout main || git checkout -b main env: GITHUB_TOKEN: ${{ secrets.DEPLOY_TOKEN }} - name: Copy binaries to repository and push run: | cd repo ISAR_VERSION=$(echo "${{ github.ref_name }}" | sed 's/refs\/tags\///') echo "Deploying binaries to version: $ISAR_VERSION" mkdir -p "$ISAR_VERSION" cp ../binaries/**/* "$ISAR_VERSION" git add . git commit -m "Deploy binaries for version $ISAR_VERSION" || echo "No changes to commit" git push https://x-access-token:${GITHUB_TOKEN}@github.com/isar-community/binaries.git main env: GITHUB_TOKEN: ${{ secrets.DEPLOY_TOKEN }} # publish: # name: Publish # needs: build_inspector # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - uses: subosito/flutter-action@v2 # - name: pub get # run: dart pub get # working-directory: packages/isar # - name: Download Binaries # run: sh tool/download_binaries.sh # - name: pub.dev credentials # run: | # mkdir -p $HOME/.config/dart # echo '${{ secrets.PUB_JSON }}' >> $HOME/.config/dart/pub-credentials.json # - name: Publish isar # run: dart pub publish --force # working-directory: packages/isar # - name: Publish isar_generator # run: dart pub publish --force # working-directory: packages/isar_generator # - name: Publish isar_flutter_libs # run: dart pub publish --force # working-directory: packages/isar_flutter_libs ================================================ FILE: .github/workflows/skynet.yaml ================================================ name: Triggers remote jenkins on: push: branches: - main - v3 permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: true jobs: trigger_remote: name: Trigger remote jenkins instance runs-on: ubuntu-latest steps: - name: Invoke trigger run: | curl -s 'https://isar-community.dev/git/notifyCommit?url=https://github.com/isar-community/isar.git&token=a641b59c61c22effd6dd258f8e713c7b' ================================================ FILE: .github/workflows/test.yaml ================================================ name: Dart CI on: push: branches: - v3 pull_request: branches: - v3 jobs: version: name: Version Display runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - run: flutter --version format: name: Check formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Check formatting run: dart format --set-exit-if-changed . lint: name: Check lints runs-on: ubuntu-latest if: ${{ false }} steps: - uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - run: flutter pub get working-directory: packages/isar - run: flutter pub get working-directory: packages/isar_flutter_libs - run: flutter pub get working-directory: packages/isar_generator - run: flutter pub get working-directory: packages/isar_inspector - run: flutter pub get working-directory: examples/pub - run: | flutter pub get flutter pub run build_runner build dart tool/generate_all_tests.dart working-directory: packages/isar_test - name: Lint run: flutter analyze test: name: Dart Test strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] fail-fast: false runs-on: ${{ matrix.os }} steps: - run: echo "$OSTYPE" - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: sh tool/build.sh - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Unit tests run: flutter test -j 1 working-directory: packages/isar_test valgrind: name: Valgrind runs-on: ubuntu-latest if: ${{ false }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Install valgrind and llvm run: sudo apt update && sudo apt install -y valgrind libclang-dev - name: Build Isar Core run: sh tool/build.sh - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Valgrind run: | dart compile exe integration_test/all_tests.dart valgrind \ --leak-check=full \ --error-exitcode=1 \ --show-mismatched-frees=no \ --show-possibly-lost=no \ --errors-for-leak-kinds=definite \ integration_test/all_tests.exe working-directory: packages/isar_test coverage: name: Code Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: sh tool/build.sh - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Add packages run: | flutter pub add json_annotation flutter pub add intl flutter pub add isar_test --path ../isar_test working-directory: packages/isar - name: Collect isar Coverage run: | flutter test --coverage --coverage-path lcov_isar.info working-directory: packages/isar - name: Collect isar_test Coverage run: | flutter test --coverage ../isar_test/test --coverage-path lcov_isar_test.info working-directory: packages/isar - name: Upload isar Coverage uses: codecov/codecov-action@v3 with: files: packages/isar/lcov_isar.info - name: Upload isar_test Coverage uses: codecov/codecov-action@v3 with: files: packages/isar/lcov_isar_test.info test_generator: name: Generator Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Run Generator Unit tests run: | dart pub get dart test working-directory: packages/isar_generator integration_test_ios: name: Integration Test iOS runs-on: macos-12 steps: - uses: actions/checkout@v4 - name: Start simulator uses: futureware-tech/simulator-action@v3 with: model: iPhone 13 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: | bash tool/build_ios.sh unzip isar_ios.xcframework.zip -d packages/isar_flutter_libs/ios - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Integration tests run: flutter test integration_test/integration_test.dart --dart-define STRESS=true working-directory: packages/isar_test integration_test_android: name: Integration Test Android runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: "11" distribution: "zulu" - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: | bash tool/build_android.sh x64 mkdir -p packages/isar_flutter_libs/android/src/main/jniLibs/x86_64 mv libisar_android_x64.so packages/isar_flutter_libs/android/src/main/jniLibs/x86_64/libisar.so - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Integration tests continue-on-error: true timeout-minutes: ${{ inputs.timeout_minutes }} uses: Wandalen/wretry.action@v1.0.36 with: action: reactivecircus/android-emulator-runner@v2 with: | api-level: 29 arch: x86_64 profile: pixel working-directory: packages/isar_test script: flutter test integration_test/integration_test.dart --dart-define STRESS=true integration_test_macos: name: Integration Test macOS runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: # flutter-version: "3.3.10" # https://github.com/flutter/flutter/issues/118469 flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: | bash tool/build_macos.sh install_name_tool -id @rpath/libisar.dylib libisar_macos.dylib mv libisar_macos.dylib packages/isar_flutter_libs/macos/libisar.dylib - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Driver tests run: | flutter config --enable-macos-desktop flutter test -d macos integration_test/integration_test.dart --dart-define STRESS=true working-directory: packages/isar_test integration_test_linux: name: Integration Test Linux runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Install Linux requirements run: sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev - name: Setup headless display uses: pyvista/setup-headless-display-action@v1 - name: Build Isar Core run: | bash tool/build_linux.sh x64 mv libisar_linux_x64.so packages/isar_flutter_libs/linux/libisar.so - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Driver tests run: | flutter config --enable-linux-desktop flutter test -d linux integration_test/integration_test.dart --dart-define STRESS=true working-directory: packages/isar_test integration_test_windows: name: Integration Test Windows runs-on: windows-2019 if: ${{ false }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core run: | bash tool/build_windows.sh x64 mv isar_windows_x64.dll packages/isar_flutter_libs/windows/libisar.dll - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Run Flutter Driver tests run: | flutter config --enable-windows-desktop flutter test -d windows integration_test/integration_test.dart --dart-define STRESS=true working-directory: packages/isar_test drive_chrome: runs-on: ubuntu-latest if: ${{ false }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Install chromedricer uses: nanasess/setup-chromedriver@v1 - name: Prepare chromedricer run: chromedriver --port=4444 & - name: Run Dart tests in browser run: | flutter pub get dart tool/generate_long_double_test.dart dart tool/generate_all_tests.dart flutter pub run build_runner build flutter drive --driver=isar_driver.dart --target=isar_driver_target.dart -d web-server --browser-name chrome working-directory: packages/isar_test drive_safari: runs-on: macos-latest if: ${{ false }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare safaridricer run: | sudo safaridriver --enable safaridriver --port=4444 & - name: Run Dart tests in browser run: | flutter pub get dart tool/generate_long_double_test.dart flutter pub run build_runner build dart tool/generate_all_tests.dart flutter drive --driver=isar_driver.dart --target=isar_driver_target.dart -d web-server --browser-name safari working-directory: packages/isar_test drive_firefox: runs-on: ubuntu-latest if: ${{ false }} steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Install geckodriver uses: browser-actions/setup-geckodriver@latest - name: Prepare geckodriver run: geckodriver --port=4444 & - name: Run Dart tests in browser run: | flutter pub get dart tool/generate_long_double_test.dart flutter pub run build_runner build dart tool/generate_all_tests.dart flutter drive --driver=isar_driver.dart --target=isar_driver_target.dart -d web-server --browser-name firefox working-directory: packages/isar_test ================================================ FILE: .github/workflows/testlab.yaml ================================================ on: workflow_call jobs: firebase_testlab_android: name: Firebase Testlab Android runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: flutter-version: ${{ vars.FLUTTER_VERSION }} - name: Prepare Build uses: ./.github/actions/prepare-build - name: Build Isar Core arm64 run: | bash tool/build_android.sh arm64 mkdir -p packages/isar_flutter_libs/android/src/main/jniLibs/arm64-v8a mv libisar_android_arm64.so packages/isar_flutter_libs/android/src/main/jniLibs/arm64-v8a/libisar.so - name: Build Isar Core armv7 run: | bash tool/build_android.sh armv7 mkdir -p packages/isar_flutter_libs/android/src/main/jniLibs/armeabi-v7a mv libisar_android_armv7.so packages/isar_flutter_libs/android/src/main/jniLibs/armeabi-v7a/libisar.so - name: Prepare Tests run: sh tool/prepare_tests.sh - name: Build dummy APK run: flutter build apk integration_test/integration_test.dart working-directory: packages/isar_test - name: Build APKs run: | ./gradlew app:assembleAndroidTest ./gradlew app:assembleDebug -Ptarget=integration_test/integration_test.dart working-directory: packages/isar_test/android - name: Login to Google Cloud uses: "google-github-actions/auth@v1" with: credentials_json: "${{ secrets.FIREBASE_JSON }}" - name: Run tests run: | gcloud firebase test android run \ --project isar-community \ --type instrumentation \ --timeout 5m \ --device model=starqlteue,version=26 \ --device model=cheetah,version=33 \ --device model=shiba,version=34 \ --app build/app/outputs/apk/debug/app-debug.apk \ --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk working-directory: packages/isar_test ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.lock *.log .DS_Store .vscode/ .idea # Dart related .dart_tool/ .flutter-plugins .flutter-plugins-dependencies **/generated_plugin_registrant.dart .packages .pub-cache/ .pub/ build/ # Rust related target/ *.a *.so *.dylib *.dll *.zip *.xcframework/ isar-dart.h # Android related **/android/**/gradle-wrapper.jar .gradle/ **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/.last_build_id **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/ephemeral **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # macOS **/macos/Flutter/GeneratedPluginRegistrant.swift **/ephemeral **/.plugin_symlinks/ # Coverage coverage/ ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "packages/isar_core", "packages/isar_core_ffi", "packages/mdbx_sys" ] [profile.release] lto = true codegen-units = 1 panic = "abort" strip = "symbols" ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: TODO.md ================================================

Roadmap and TODOs

# Documentation ## API Docs - [ ] Document all public APIs ## Schema - [x] Update schema migration instructions - [ ] Document all annotation options ## CRUD - [ ] Document sync operations - [x] `getAll()`, `putAll`, `deleteAll()` - [ ] `getBy...()`, `deleteBy...()` ## Queries - [x] Filter groups - [x] Boolean operators `and()`, `or()`, `not()` - [x] Offset, limit - [x] Distinct where clauses - [x] Different filter operations (`equalTo`, `beginsWith()` etc.) - [ ] Better explanation for distinct and sorted where clauses - [ ] Watching queries ## Indexes - [ ] Intro - [x] What are they - [ ] Why use them - [x] How to in isar? ## Examples - [ ] Create minimal example - [ ] Create complex example with indexes, filter groups etc. - [ ] More Sample Apps ## Tutorials - [ ] How to write fast queries - [ ] Build a simple offline first app - [ ] Advanced queries ---- # Isar Dart ## Features - [x] Distinct by - [x] Offset, Limit - [x] Sorted by ## Fixes - [x] Provide an option to change collection accessor names ## Unit tests - [x] Download binaries automatically for tests ### Queries - [x] Restructure query tests to make them less verbose - [x] Define models that can be reused across tests - [x] Where clauses with string indexes (value, hash, words, case-sensitive) - [x] Distinct where clauses - [x] String filter operations ---- # Isar Core ## Features (low priority) - [ ] Draft Synchronization - [x] Relationships ## Unit tests - [ ] Make mdbx unit tests bulletproof - [x] Migration tests - [x] Binary format - [x] CRUD - [x] Links - [ ] QueryBuilder - [ ] WhereClause - [ ] WhereExecutor - [x] CollectionMigrator - [ ] Watchers ---- # Isar Web - [ ] MVP ================================================ FILE: docs/.gitignore ================================================ .DS_Store node_modules .temp .cache dist ================================================ FILE: docs/README.md ================================================ # Isar Docs Run the docs locally: ``` npm install npm run dev ``` ## Create a new language 1. Create a new folder in `docs` with the language code (e.g. `de` for German). 2. Add the locale config to `.vueepress/locales.ts`. 3. Start translating the existing pages. ================================================ FILE: docs/docs/.vuepress/config.ts ================================================ import { shikiPlugin } from '@vuepress/plugin-shiki' import { DefaultThemeLocaleData, defineUserConfig, LocaleConfig, SiteLocaleConfig, } from 'vuepress' import { defaultTheme } from 'vuepress' import { viteBundler } from 'vuepress' import { getLocalePath, locales } from './locales' import * as path from 'path' import * as fs from 'fs' const vueLocales: SiteLocaleConfig = {} for (const locale of locales) { vueLocales[getLocalePath(locale.code)] = { lang: locale.language, title: locale.dbName, description: locale.dbDescription, } } const themeLocales: LocaleConfig = {} for (const locale of locales) { themeLocales[getLocalePath(locale.code)] = { selectLanguageName: locale.language, selectLanguageText: locale.selectLanguage, editLinkText: locale.editPage, lastUpdatedText: locale.lastUpdated, contributorsText: locale.contributors, tip: locale.tip, warning: locale.warning, danger: locale.danger, notFound: locale.notFound, backToHome: locale.backToHome, sidebar: getSidebar({ locale: locale.code, tutorials: locale.tutorials, concepts: locale.concepts, recipes: locale.recipes, sampleApps: locale.sampleApps, chnagelog: locale.changelog, contributors: locale.contributors, }), } } export default defineUserConfig({ locales: vueLocales, bundler: viteBundler({}), base: '/v3/', theme: defaultTheme({ logo: "/isar.svg", repo: "isar-community/isar", docsRepo: "isar-community/isar", docsDir: "docs/docs", contributors: true, locales: themeLocales, navbar: [ { text: "pub.dev", link: "https://pub.dev/packages/isar", }, { text: "API", link: "https://pub.dev/documentation/isar/latest/isar/isar-library.html", }, { text: "Telegram", link: "https://t.me/isardb", }, { text: "v3.x", children: [ { text: "v4.x", link: "https://isar-community.dev", }, { text: "v3.x", link: "https://isar-community.dev/v3", }, ], }, ], sidebarDepth: 1, }), markdown: { code: { lineNumbers: false, }, }, plugins: [ [ shikiPlugin({ theme: "one-dark-pro", }), { name: 'redirect-locale', clientConfigFile: path.resolve(__dirname, 'redirect.ts'), }, ], ], head: [ [ "link", { rel: "icon", type: "image/png", sizes: "256x256", href: `/icon-256x256.png`, }, ], [ "link", { rel: "icon", type: "image/png", sizes: "512x512", href: `/icon-512x512.png`, }, ], [ "link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Montserrat:wght@800&display=swap", }, ], ["meta", { name: "application-name", content: "Isar Database" }], ["meta", { name: "apple-mobile-web-app-title", content: "Isar Database" }], [ "meta", { name: "apple-mobile-web-app-status-bar-style", content: "black" }, ], [ "script", { async: "", src: "https://www.googletagmanager.com/gtag/js?id=G-36LNDL9RHB", }, ], [ "script", {}, `window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-36LNDL9RHB');`, ], [ "script", {}, `(function(c,l,a,r,i,t,y){ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); })(window, document, "clarity", "script", "lkyzg3xacc");`, ], ], }) function getSidebar({ locale, tutorials, concepts, recipes, sampleApps, chnagelog, contributors }) { return [ { text: tutorials, children: getSidebarChildren(locale, ["tutorials/quickstart.md"]) }, { text: concepts, children: getSidebarChildren( locale, [ "schema.md", "crud.md", "queries.md", "transactions.md", "indexes.md", "links.md", "watchers.md", "limitations.md", "faq.md", ], ), }, { text: recipes, children: getSidebarChildren( locale, [ "recipes/full_text_search.md", "recipes/multi_isolate.md", "recipes/string_ids.md", "recipes/data_migration.md", ] ), }, { text: sampleApps, link: "https://github.com/isar-community/isar/tree/main/examples", }, { text: chnagelog, link: "https://github.com/isar-community/isar/blob/main/packages/isar/CHANGELOG.md", }, { text: contributors, link: "https://github.com/isar-community/isar#contributors-", }, ] } function getSidebarChildren(locale: string, children: string[]) { const localePath = getLocalePath(locale) return children.map((child) => { if (locale === "en") { return '/' + child } const file = path.resolve(__dirname, '../', localePath.substring(1), child) if (fs.existsSync(file)) { return localePath + child } else { return '/' + child } }); } ================================================ FILE: docs/docs/.vuepress/locales.ts ================================================ export interface LocalConfig { code: string; language: string; selectLanguage: string; editPage: string; lastUpdated: string; tip: string; warning: string; danger: string; notFound: string[]; backToHome: string; translationOutdated: string; dbName: string; dbDescription: string; tutorials: string; concepts: string; recipes: string; sampleApps: string; changelog: string; contributors: string; } export function getLocalePath(code: string): string { if (code === "en") { return "/"; } else { return "/" + code + "/"; } } export const locales: LocalConfig[] = [ { code: "en", language: "English", selectLanguage: "Select Language", editPage: "Edit Page", lastUpdated: "Last Updated", tip: "Tip", warning: "Warning", danger: "Danger", notFound: [ "Nothing to see here.", "How did we end up here?", "This is a four-oh-four...", "Looks like we have a broken link.", ], backToHome: "Back to Home", translationOutdated: "Translation is outdated. Please help us update it!", dbName: "Isar Database", dbDescription: "Super Fast Cross-Platform Database for Flutter", tutorials: "TUTORIALS", concepts: "CONCEPTS", recipes: "RECIPES", sampleApps: "Sample Apps", changelog: "Changelog", contributors: "Contributors", }, { code: "de", language: "Deutsch", selectLanguage: "Sprache wählen", editPage: "Seite bearbeiten", lastUpdated: "Zuletzt aktualisiert", tip: "Tipp", warning: "Warnung", danger: "Achtung", notFound: [ "Hier gibt es nichts zu sehen.", "Wie sind wir hier gelandet?", "Das ist ein vier-null-vier...", "Sieht aus als hätten wir einen kaputten Link.", ], backToHome: "Zurück zur Startseite", translationOutdated: "Übersetzung ist veraltet. Bitte hilf uns, sie zu aktualisieren!", dbName: "Isar Datenbank", dbDescription: "Super Schnelle Cross-Platform Flutter Datenbank", tutorials: "TUTORIALS", concepts: "KONZEPTE", recipes: "REZEPTE", sampleApps: "Beispiel Apps", changelog: "Änderungsprotokoll", contributors: "Mitwirkende", }, { code: "ja", language: "日本語", selectLanguage: "言語の選択", editPage: "編集ページ", lastUpdated: "最終更新日", tip: "ヒント", warning: "警告", danger: "危険", notFound: [ "何も見つかりませんでした.", "どうしてこんなところに辿り着いたのだろう...", "ここは404ページのようです...", "リンク切れのようです。", ], backToHome: "ホームに戻る", translationOutdated: "翻訳は古くなっています。翻訳の更新にご協力頂けませんか?", dbName: "Isar Database", dbDescription: "Flutterのための超高速クロスプラットフォームDatabase", tutorials: "チュートリアル", concepts: "コンセプト", recipes: "レシピ集", sampleApps: "サンプルアプリ", changelog: "変更履歴", contributors: "貢献者の方々", }, { code: "ko", language: "한국어", selectLanguage: "언어 선택", editPage: "페이지 편집", lastUpdated: "마지막 업데이트", tip: "팁", warning: "경고", danger: "위험", notFound: [ "여기는 볼 것이 없다.", "우리가 어떻게 여기까지 오게 되었나요?", "여기는 404...", "연결이 끊어진 것 같습니다.", ], backToHome: "홈으로 돌아가기", translationOutdated: "번역이 낡았습니다. 업데이트 도와주세요!", dbName: "Isar 데이터베이스", dbDescription: "플러터를 위한 초고속 크로스 플랫폼 데이터베이스", tutorials: "튜토리얼", concepts: "개념", recipes: "레시피", sampleApps: "샘플 앱", changelog: "체인지로그", contributors: "기여자들", }, { code: "es", language: "Español", selectLanguage: "Seleccionar Idioma", editPage: "Editar Página", lastUpdated: "Última actualización", tip: "Consejo", warning: "Advertencia", danger: "Peligro", notFound: [ "No hay nada para ver aquí.", "Cómo llegamos aquí?", "Esto es vergonzoso, no tenemos nada...", "Parece que hay un enlace roto.", ], backToHome: "Volver al inicio", translationOutdated: "Esta traducción está desactualizada. Por favor ayúdanos a mantenerla al día!", dbName: "Isar Database", dbDescription: "Base de Datos Super rápida, Multiplataforma para Flutter", tutorials: "TUTORIALES", concepts: "CONCEPTOS", recipes: "RECETAS", sampleApps: "Aplicaciones de Ejemplo", changelog: "Registro de cambios", contributors: "Colaboradores", }, { code: "it", language: "Italiano", selectLanguage: "Seleziona Lingua", editPage: "Modifica Pagina", lastUpdated: "Ultimo aggiornamento", tip: "Suggerimento", warning: "Attenzione", danger: "Pericolo", notFound: [ "Nulla da vedere qui.", "Come ci siamo finiti qui?", "Questa è una quattro-zero-quattro...", "Sembra che abbiamo un collegamento rotto.", ], backToHome: "Indietro alla Home", translationOutdated: "La traduzione è obsoleta. Per favore aiutaci ad aggiornarla!", dbName: "Isar Database", dbDescription: "Database multipiattaforma super veloce per Flutter", tutorials: "TUTORIALS", concepts: "CONCETTI", recipes: "RICETTE", sampleApps: "App d'esempio", changelog: "Registro delle modifiche", contributors: "Contributori", }, { code: "pt", language: "Português", selectLanguage: "Selecione o idioma", editPage: "Editar página", lastUpdated: "Ultima atualização", tip: "Dica", warning: "Aviso", danger: "Perigo", notFound: [ "Nada para ver aqui.", "Como chegamos aqui?", "Isso é embaraçoso, não temos nada...", "Parece que temos um link inválido.", ], backToHome: "Voltar para Início", translationOutdated: "A tradução está desatualizada. Por favor, ajude-nos a atualizá-lo!", dbName: "Isar Database", dbDescription: "Banco de dados multiplataforma super rápido para Flutter", tutorials: "TUTORIAIS", concepts: "CONCEITOS", recipes: "RECEITAS", sampleApps: "Aplicativos de amostra", changelog: "Registro de alterações", contributors: "Contribuidores", }, { code: "ur", language: "اردو", selectLanguage: "زبان منتخب کریں", editPage: "صفحہ میں ترمیم کریں", lastUpdated: "آخری تازہ کاری", tip: "ٹپ", warning: "انتباہ", danger: "خطرہ", notFound: [ "یہاں دیکھنے کے لیے کچھ نہیں ہے۔", "ہم یہاں کیسے پہنچے؟", " یہ چار اوہ چار ہے۔۔۔", "لگتا ہے ہمارے پاس کوئی ٹوٹا ہوا لنک ہے۔", ], backToHome: "گھر پر واپس", translationOutdated: "ترجمہ پرانا ہے۔ براہ کرم اسے تروتازہ کرنے میں ہماری مدد کریں!", dbName: "Isar Database", dbDescription: " ڈیٹا بیس کے لیے سپر فاسٹ کراس پلیٹ فارم Flutter", tutorials: "اسباق", concepts: "تصورات", recipes: "تراکیب", sampleApps: "نمونہ ایپس", changelog: "چینج لاگ", contributors: "شراکت دار", }, { code: "fr", language: "Français", selectLanguage: "Sélectionner la langue", editPage: "Modifier la page", lastUpdated: "Dernière modification", tip: "Conseil", warning: "Avertissement", danger: "Danger", notFound: [ 'Il n"y a rien a voir ici.', "Comment en sommes-nous arrivés là ?", "Ceci est un quatre-cent-quatre...", "Il semble que nous avons un lien brisé.", ], backToHome: "Retour à l'acceuil", translationOutdated: "Translation is outdated. Please help us update it!", dbName: "Base de données Isar", dbDescription: "Base de données multiplateforme super rapide pour Flutter", tutorials: "TUTORIELS", concepts: "CONCEPTS", recipes: "RECETTES", sampleApps: "Exemples d'applications", changelog: "Changements", contributors: "Contributeurs", }, { code: "zh", language: "简体中文", selectLanguage: "选择语言", editPage: "编辑页面", lastUpdated: "更新日期", tip: "提示", warning: "警告", danger: "危险", notFound: [ "这里什么都没有。", "怎么会来到这个页面?", "404...", "看起来链接失效了。", ], backToHome: "回到主页", translationOutdated: "翻译已过期,请帮助我们更新。", dbName: "Isar 数据库", dbDescription: "专门为 Flutter 打造的超高速跨平台数据库", tutorials: "教程", concepts: "概念", recipes: "专题", sampleApps: "示例 App", changelog: "更新记录", contributors: "贡献者", }, ]; ================================================ FILE: docs/docs/.vuepress/redirect.ts ================================================ import { defineClientConfig } from '@vuepress/client' import { locales } from './locales' export default defineClientConfig({ enhance({ app, router, siteData }) { router.beforeEach((to, from) => { // open vuepress for the first time let isFirstStart = to.fullPath == from.fullPath // Whether the home page is about to be displayed let isHome = to.fullPath == "/" if (typeof navigator != 'undefined' && isFirstStart && isHome) { const lang = navigator.language.split("-")[0].toLowerCase() if (lang != "en" && locales.some((l) => l.code === lang)) { const redirectUrl = "/" + lang + "/" // Avoid infinite redirection if (to.fullPath != redirectUrl) { return redirectUrl } } } }) } }) ================================================ FILE: docs/docs/.vuepress/styles/index.scss ================================================ :root { --c-brand: #4799fc; --c-brand-light: #67abfd; --c-text: rgb(30, 30, 30); // normal text --c-text-light: rgb(30, 30, 30); --c-text-lighter: rgb(30, 30, 30); // code block text --c-text-lightest: rgba(30, 30, 30, 0.7); .custom-container.tip { color: rgb(57, 146, 255) !important; border-color: rgb(57, 146, 255) !important; background-color: rgba(57, 146, 255, 0.1) !important; } .custom-container.warning { color: rgb(39, 31, 6) !important; border-color: rgb(39, 31, 6) !important; background-color: rgba(131, 122, 11, 0.15) !important; } .custom-container.danger { color: rgb(170, 37, 58) !important; border-color: rgb(170, 37, 58) !important; background-color: rgba(170, 37, 58, 0.1) !important; } } html.dark { --c-brand: #67abfd; --c-brand-light: #4799fc; --c-bg: rgb(18, 18, 18); --c-bg-light: rgb(30, 30, 30); // code block background --code-bg-color: #1e1e1e; // code background --c-text: rgb(183, 188, 190); // normal text --c-text-light: rgba(183, 188, 190); --c-text-lighter: rgb(183, 188, 190); // code block text --c-text-lightest: rgba(183, 188, 190, 0.7); --c-border: rgb(45, 45, 45); --c-border-dark: rgb(60, 60, 60); .custom-container.tip { color: rgb(57, 146, 255) !important; border-color: rgb(57, 146, 255) !important; background-color: rgba(57, 146, 255, 0.1) !important; } .custom-container.warning { color: rgb(248, 239, 159) !important; border-color: rgb(248, 239, 159) !important; background-color: rgba(131, 122, 11, 0.15) !important; } .custom-container.danger { color: rgb(240, 158, 183) !important; border-color: rgb(240, 158, 183) !important; background-color: rgba(240, 158, 183, 0.1) !important; } } h1 { font-family: "Montserrat", sans-serif; font-size: 38px; @media (min-width: 750px) { font-size: 55px; } } h2 { font-family: "Montserrat", sans-serif; font-size: 27px; @media (min-width: 750px) { font-size: 38px; } } h3 { font-family: "Montserrat", sans-serif; font-size: 20px; @media (min-width: 750px) { font-size: 27px; } } h4 { font-family: "Montserrat", sans-serif; font-size: 16px; } mark { padding: 2px; } .action-button.primary { font-weight: 800; } summary { cursor: pointer; } details[open] summary { margin-bottom: 0.5rem; } .custom-container { border-radius: 10px; border-left-width: 0.25rem !important; border-left-style: solid; border-right-style: solid; border-right-width: 0.25rem; } .custom-container-title { display: none; } .video-block { position: relative; padding-bottom: 56.25%; /* 16:9 */ height: 0; overflow: hidden; width: 100%; height: auto; } .video-block iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } ================================================ FILE: docs/docs/README.md ================================================ --- home: true title: Home heroImage: /isar.svg actions: - text: Let's Get Started! link: /tutorials/quickstart.html type: primary features: - title: 💙 Made for Flutter details: Minimal setup, Easy to use, no config, no boilerplate. Just add a few lines of code to get started. - title: 🚀 Highly scalable details: Store hundreds of thousands of records in a single NoSQL database and query them efficiently and asynchronously. - title: 🍭 Feature-rich details: Isar has a rich set of features to help you manage your data. Composite & multi-entry indexes, query modifiers, JSON support, and more. - title: 🔎 Full-text search details: Isar has built-in full-text search. Create a multi-entry index and search for records easily. - title: 🧪 ACID semantics details: Isar is ACID compliant and handles transactions automatically. It rolls back changes if an error occurs. - title: 💃 Static typing details: Isar's queries are statically typed and compile-time checked. No need to worry about runtime errors. - title: 📱 Multiplatform details: iOS, Android, Desktop, and FULL WEB SUPPORT! - title: ⏱ Asynchronous details: Parallel query operations & multi-isolate support out-of-the-box - title: 🦄 Open Source details: Everything is open source and free forever! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/crud.md ================================================ --- title: Create, Read, Update, Delete --- # Create, Read, Update, Delete When you have your collections defined, learn how to manipulate them! ## Opening Isar Before you can do anything, we need an Isar instance. Each instance requires a directory with write permission where the database file can be stored. If you don't specify a directory, Isar will find a suitable default directory for the current platform. Provide all the schemas you want to use with the Isar instance. If you open multiple instances, you still have to provide the same schemas to each instance. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` You can use the default config or provide some of the following parameters: | Config | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Open multiple instances with distinct names. By default, `"default"` is used. | | `directory` | The storage location for this instance. Not required for web. | | `maxSizeMib` | The maximum size of the database file in MiB. Isar uses virtual memory which is not an endless resource so be mindful with the value here. If you open multiple instances they share the available virtual memory so each instance should have a smaller `maxSizeMib` . The default is 2048. | | `relaxedDurability` | Relaxes the durability guarantee to increase write performance. In case of a system crash (not app crash), it is possible to lose the last committed transaction. Corruption is not possible | | `compactOnLaunch` | Conditions to check whether the database should be compacted when the instance is opened. | | `inspector` | Enabled the Inspector for debug builds. For profile and release builds this option is ignored. | If an instance is already open, calling `Isar.open()` will yield the existing instance regardless of the specified parameters. That's useful for using Isar in an isolate. :::tip Consider using the [path_provider](https://pub.dev/packages/path_provider) package to get a valid path on all platforms. ::: The storage location of the database file is `directory/name.isar` ## Reading from the database Use `IsarCollection` instances to find, query, and create new objects of a given type in Isar. For the examples below, we assume that we have a collection `Recipe` defined as follows: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Get a collection All your collections live in the Isar instance. You can get the recipes collection with: ```dart final recipes = isar.recipes; ``` That was easy! If you don't want to use collection accessors, you can also use the `collection()` method: ```dart final recipes = isar.collection(); ``` ### Get an object (by id) We don't have data in the collection yet but let's pretend we do so we can get an imaginary object by the id `123` ```dart final recipe = await isar.recipes.get(123); ``` `get()` returns a `Future` with either the object or `null` if it does not exist. All Isar operations are asynchronous by default, and most of them have a synchronous counterpart: ```dart final recipe = isar.recipes.getSync(123); ``` :::warning You should default to the asynchronous version of methods in your UI isolate. Since Isar is very fast, it is often acceptable to use the synchronous version. ::: If you want to get multiple objects at once, use `getAll()` or `getAllSync()`: ```dart final recipe = await isar.recipes.getAll([1, 2]); ``` ### Query objects Instead of getting objects by id you can also query a list of objects matching certain conditions using `.where()` and `.filter()`: ```dart final allRecipes = await isar.recipes.where().findAll(); final favorites = await isar.recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ Learn more: [Queries](queries) ## Modifying the database It's finally time to modify our collection! To create, update, or delete objects, use the respective operations wrapped in a write transaction: ```dart await isar.writeTxn(() async { final recipe = await isar.recipes.get(123) recipe.isFavorite = false; await isar.recipes.put(recipe); // perform update operations await isar.recipes.delete(123); // or delete operations }); ``` ➡️ Learn more: [Transactions](transactions) ### Insert object To persist an object in Isar, insert it into a collection. Isar's `put()` method will either insert or update the object depending on whether it already exists in the collection. If the id field is `null` or `Isar.autoIncrement`, Isar will use an auto-increment id. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await isar.recipes.put(pancakes); }) ``` Isar will automatically assign the id to the object if the `id` field is non-final. Inserting multiple objects at once is just as easy: ```dart await isar.writeTxn(() async { await isar.recipes.putAll([pancakes, pizza]); }) ``` ### Update object Both creating and updating works with `collection.put(object)`. If the id is `null` (or does not exist), the object is inserted; otherwise, it is updated. So if we want to unfavorite our pancakes, we can do the following: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await isar.recipes.put(pancakes); }); ``` ### Delete object Want to get rid of an object in Isar? Use `collection.delete(id)`. The delete method returns whether an object with the specified id was found and deleted. If you want to delete the object with id `123`, for example, you can do: ```dart await isar.writeTxn(() async { final success = await isar.recipes.delete(123); print('Recipe deleted: $success'); }); ``` Similarly to get and put, there is also a bulk delete operation that returns the number of deleted objects: ```dart await isar.writeTxn(() async { final count = await isar.recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` If you don't know the ids of the objects you want to delete, you can use a query: ```dart await isar.writeTxn(() async { final count = await isar.recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/de/README.md ================================================ --- home: true title: Home heroImage: /isar.svg actions: - text: Auf Los geht's los! link: /de/tutorials/quickstart.html type: primary features: - title: 💙 Für Flutter gemacht details: Minimales Setup, einfach zu bedienen, keine Konfiguration, kein Boilerplate. Mit ein paar Zeilen Code geht's los. - title: 🚀 Skalierbar details: Speichere Hunderttausende von Datensätzen und rufe sie effizient und asynchron ab. - title: 🍭 Viele Features details: Isar hat unzählige Features. Komposit- und Mehrfach-Indizes, Query-Modifikatoren, JSON und mehr. - title: 🔎 Volltextsuche details: Volltextsuche ist integriert. Erstelle einen Mehrfach-Index und suche nach Datensätzen. - title: 🧪 ACID Semantik details: Isar ist ACID-konform und verwaltet Transaktionen automatisch. Änderungen werden rückgängig gemacht, falls ein Fehler auftritt. - title: 💃 Statische Typisierung details: Abfragen sind statisch typisiert und werden zur Kompilierzeit überprüft. Laufzeitfehler sind ein Problem von gestern. - title: 📱 Multiplatform details: iOS, Android, Desktop und VOLLE WEB UNTERSTÜTZUNG! - title: ⏱ Asynchron details: Parallelle Abfragen und Multi-Isolate-Unterstützung. - title: 🦄 Open Source details: Komplett Open Source und für immer kostenlos! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/de/crud.md ================================================ --- title: Erstellen, Lesen, Aktualisieren und Löschen --- # Erstellen, Lesen, Aktualisieren und Löschen Lerne wie du Collections in Isar nutzt nachdem du sie definiert hast. ## Öffnen von Isar Als Erstes benötigen wir eine Isar Instanz. Jede Instanz erfordert einen Ordner mit Schreibrechten, in dem die Datenbankdatei gespeichert werden kann. Wenn du keinen Ordner angibst, wird Isar einen geeigneten Standardordner für die aktuelle Plattform finden. Gib alle Schemas an, die du mit der Isar-Instanz verwenden möchtest. Wenn du mehrere Instanzen öffnest, musst du trotzdem jeder Instanz die gleichen Schemas mitgeben. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` Du kannst die Standardkonfiguration verwenden oder einige der folgenden Parameter setzen: | Konfiguration | Beschreibung | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Öffne mehrere Instanzen mit unterschiedlichen Namen. Standardmäßig wird `"default"` verwendet. | | `directory` | Der Speicherort für diese Instanz. Standardmäßig wird `NSDocumentDirectory` für iOS und `getDataDirectory` für Android verwendet. Nicht erforderlich für Web. | | `relaxedDurability` | Entspannt die durability-Garantie, um die Schreibleistung zu erhöhen. Im Falle eines Systemabsturzes (nicht App-Absturz) ist es möglich, die letzte Transaktion zu verlieren. Datenbankkorruption ist nicht möglich | Wenn eine Instanz bereits geöffnet ist, wird `Isar.open()` die vorhandene Instanz unabhängig von den angegebenen Parametern zurückgeben. Das ist nützlich, um Isar in einem Isolate zu verwenden. :::tip Verwende das [path_provider](https://pub.dev/packages/path_provider)-Paket, um einen gültigen Pfad auf allen Plattformen zu erhalten. ::: Der Speicherort der Datenbankdatei ist `directory/name.isar` ## Aus der Datenbank lesen Verwende `IsarCollection`-Instanzen um Objekte eines bestimmten Typs in Isar zu finden, abzufragen und neu zu erstellen. Den folgenden Beispielen liegt die Collection `Recipe` zugrunde, die wie folgt definiert ist: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Eine Collection erhalten Alle deine Collections befinden sich in der Isar Instanz. Erhalte die Recipes-Collection über den Accessor: ```dart final recipes = isar.recipes; ``` Das war einfach! Wenn du keine Collection-Accessors verwenden möchtest, ist alternativ die `collection()`-Methode verfügbar: ```dart final recipes = isar.collection(); ``` ### Objekt abrufen (per ID) Wir haben noch keine Daten in der Collection, aber wir nehmen an, dass bereits ein Objekt mit der ID `123` existiert. ```dart final recipe = await recipes.get(123); ``` Die `get()`-Methode gibt ein `Future` zurück, das entweder das Objekt enthält, oder `null`, wenn die ID nicht existiert. Alle Isar-Operationen sind standardmäßig asynchron, auch wenn die meisten ein synchrones Gegenstück haben: ```dart final recipe = recipes.getSync(123); ``` :::warning Normalerweise solltest du die asynchrone Version der Methoden in deinem UI-Isolate bevorzugen. Da Isar sehr schnell ist, sind die synchronen Methoden oft auch in Ordnung. ::: Wenn du mehrere Objekte auf einmal abrufen möchtest, kannst du `getAll()` oder `getAllSync()` verwenden: ```dart final recipe = await recipes.getAll([1, 2]); ``` ### Abfragen von Objekten Anstatt Objekte über die ID zu erhalten, kannst du mittels `.where()` und `.filter()` auch eine Liste von Objekten abfragen, die bestimmten Bedingungen entsprechen: ```dart final allRecipes = await recipes.where().findAll(); final favourites = await recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ Lerne mehr: [Abfragen](queries) ## Ändern der Datenbank Jetzt ist es endlich an der Zeit, unsere Collection zu verändern! Um Objekte zu erstellen, zu aktualisieren oder zu löschen, rufe die entsprechenden Operationen innerhalb einer Schreibtransaktion auf: ```dart await isar.writeTxn(() async { final recipe = await recipes.get(123) recipe.isFavorite = false; await recipes.put(recipe); // Aktualisierungsoperationen await recipes.delete(123); // oder Löschoperationen durchführen }); ``` ➡️ Lerne mehr: [Transaktionen](transactions) ### Objekt erstellen Erstelle ein Objekt in einer Collection um es in Isar zu speichern. Die `put()`-Methode von Isar erstellt das Objekt entweder oder aktualisiert es, je nachdem, ob es bereits in der Collection existiert. Wenn das ID-Feld `null` oder `Isar.autoIncrement` ist, verwendet Isar eine automatisch generierte ID. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await recipes.put(pancakes); }) ``` Ist das ID-Feld nicht-final, weist Isar die generierte ID automatisch dem Objekt zu. Das Erstellen von mehreren Objekten auf einmal ist genauso einfach: ```dart await isar.writeTxn(() async { await recipes.putAll([pancakes, pizza]); }) ``` ### Objekt aktualisieren Sowohl das Erstellen als auch das Aktualisieren funktioniert mit `collection.put(object)`. Wenn die ID `null` ist (oder nicht existiert), wird das Objekt erstellt, andernfalls wird es aktualisiert. Wenn wir also Pfannkuchen nicht mehr mögen, können wir Folgendes tun: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await recipes.put(recipe); }); ``` ### Objekt löschen Willst du ein Objekt in Isar loswerden? Verwende `collection.delete(id)`. Die delete-Methode gibt zurück, ob ein Objekt mit der angegebenen ID gefunden und gelöscht wurde. Lass uns z.B. das Objekt mit der ID `123` löschen: ```dart await isar.writeTxn(() async { final success = await recipes.delete(123); print('Recipe deleted: $success'); }); ``` Ähnlich wie bei `get()` und `put()` gibt es auch einen Massenlöschvorgang, der die Anzahl der gelöschten Objekte zurückgibt: ```dart await isar.writeTxn(() async { final count = await recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` Wenn du die IDs der zu löschenden Objekte nicht kennst ist es auch möglich eine Abfrage zu verwenden: ```dart await isar.writeTxn(() async { final count = await recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/de/faq.md ================================================ --- title: FAQ --- # Häufig gestellte Fragen Eine zufällige Zusammenstellung an häufig gestellten Fragen zu Isar und Flutter-Datenbanken. ### Warum brauche ich eine Datenbank? > Ich speichere meine Daten in einer Backend-Datenbank, warum benötige ich Isar? Sogar heute kommt es vor, dass du keine Internetverbindung hast, wenn du in einer U-Bahn, einem Flugzeug oder zu Besuch bei deiner Oma bist, die kein WLAN und einen sehr schlechten Mobilfunkempfang hat. Du solltest deine App nicht durch schlechte Verbindung lahmlegen lassen. ### Isar vs Hive Die Antwort ist leicht: Isar wurde [als Ersatz für Hive begonnen](https://github.com/hivedb/hive/issues/246) und ist nun an einem Punkt, wo ich immer empfehlen würde, Isar statt Hive zu benutzen. ### Where-Klauseln?! > Warum muss **_ich_** wählen, welcher Index genutzt wird? Es gibt mehrere Gründe. Viele Datenbanken benutzen Heuristik um den besten Index für eine bestimmte Abfrage zu nutzen. Die Datenbank muss zusätzliche Nutzungsdaten sammeln (-> Overhead) und verwendet möglicherweise immer noch den falschen Index. Es dauert dadurch auch länger eine Abfrage zu starten. Niemand kennt deine Daten besser, als du, der Entwickler. Also kannst du den besten Index wählen und z.B. entscheiden, ob du einen Index zum Abfragen oder Sortieren verwenden willst. ### Muss ich Indizes / Where-Klauseln benutzen? Nö! Isar ist vermutlich schnell genug, auch wenn du nur Filter verwendest. ### Ist Isar schnell genug? Isar ist unter den schnellsten Datenbanken für Mobilgeräte, also sollte es in den meisten Fällen schnellgenug sein. Wenn du auf Leistungsprobleme stößt, besteht die Möglichkeit, dass du was falschmachst. ### Steigert Isar die Größe meiner App? Ja, ein bisschen. Isar wird die Download-Größe deiner App um 1 - 1,5 MB erhöhen. Isar Web fügt nur wenige KB hinzu. ### Die Docs sind falsch / Da ist ein Tippfehler Oh nein, sorry. Bitte [öffne ein Issue](https://github.com/isar-community/isar/issues/new/choose), oder noch besser, mach einen PR um den Fehler zu beheben 💪. ================================================ FILE: docs/docs/de/indexes.md ================================================ --- title: Indizes --- # Indizes Indizes sind Isars mächtigstes Feature. Viele eingebettete Datenbanken bieten "normale" Indizes (wenn überhaupt), aber Isar hat auch Komposit- und Mehrfach-Indizes. Zu verstehen, wie Indizes funktionieren ist grundlegend um die Abfrageleistung zu optimieren. Isar lässt dich wählen welchen Index du verwenden möchtest und wie du ihn benutzen willst. Wir beginnen mit einer schnellen Einführung was Indizes sind. ## Was sind Indizes? Wenn eine Collection nicht indiziert ist, wird die Reihenfolge der Zeilen von der Abfrage aus sicherlich nicht als in irgendeiner Weise optimiert erkennbar sein. Daher muss die Abfrage linear alle Objekte durchsuchen. In anderen Worten, die Abfrage muss alle Objekte durchsuchen, um diejenigen zu finden, die zu den Bedingungen passen. Wie du dir bestimmt vorstellen kannst, kann das seine Zeit dauern. Durch jedes einzelne Objekt zu gucken ist nicht sehr effizient. Zum Beispiel ist diese `Product`-Collection komplett unsortiert. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Daten:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | Eine Abfrage, die versucht alle Produkte zu finden, die mehr als 30€ kosten, muss alle neun Zeilen durchsuchen. Das ist kein Problem für nur neun Zeilen, aber könnte ein Problem für 100k Zeilen werden. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` Um die Leistung dieser Abfrage zu verbessern, indizieren wir die Eigenschaft `price`. Ein Index ist wie eine sortierte Nachschlagetabelle. ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Generierter Index:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Jetzt kann die Abfrage deutlich schneller durchgeführt werden. Es kann direkt zu den letzten drei Indexzeilen gesprungen werden und die entsprechenden Objekte anhand ihrer ID gefunden werden. ### Sortierung Eine andere coole Sache: Indizes können superschnell sortieren. Sortierte Abfragen sind kostenintensiv, weil die Datenbank alle Ergebnisse in den Speicher laden muss, bevor sie sortiert werden. Sogar wenn du einen Offset oder eine Limitierung angibst, werden diese erst nach dem Sortieren angewandt. Stell dir vor, wir wollten die vier günstigsten Produkte finden. Wir könnten die folgende Abfrage verwenden: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` In diesem Beispiel müsste die Datenbank alle (!) Objekte laden, sie nach dem Preis sortieren und die vier Produkte mit dem niedrigsten Preis zurückgeben. Wie du dir vermutlich vorstellen kannst, kann das mit dem vorherigen Index sehr viel effizienter gemacht werden. Die Datenbank nimmt die ersten vier Zeilen des Indexes und gibt die zugehörigen Objekte zurück, da sie schon in der korrekten Reihenfolge sind. Um einen Index zum Sortieren zu verwenden würden wir die Abfrage so schreiben: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` Die `.anyX()` Where-Klausel teilt Isar mit, einen Index nur zum Sortieren zu verwenden. Du kannst also eine Where-Klausel wie `.priceGreaterThan()` benutzen und sortierte Ergenisse erhalten. ## Eindeutige Indizes Ein eindeutiger Index stellt sicher, dass der Index keine doppelten Werte enthält. Er kann aus einem oder mehreren Eigenschaften bestehen. Wenn ein eindeutiger Index eine Eigenschaft hat, sind die Werte dieser Eigenschaft eindeutig. Wenn ein eindeutiger Index mehr als eine Eigenschaft hat, dann ist die Kombination der Werte dieser Eigenschaften eindeutig. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Jeder Versuch Daten in einen eindeutigen Index einzufügen oder zu aktualisieren, die ein Dukplikat verursachen würden, resultieren in einem Fehler: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> Ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // Versucht einen Benutzer mit dem gleichen Benutzernamen einzufügen await isar.users.put(user2); // -> Fehler: Eindeutigkeitsbeschränkung verletzt print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Indizes ersetzen Manchmal ist es nicht von Vorteil einen Fehler zu verursachen, wenn eine Eindeutigkeitsbeschränkung verletzt wird. Stattdessen möchtest du vielleicht das vorhandene Objekt mit dem Neuen ersetzen. Das kann erreicht werden, indem die Eigenschaft `replace` des Indexes auf `true` gesetzt wird. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Jetzt, wenn wir versuchen einen Benutzer mit einem vorhandenen Benutzernamen einzufügen, wird Isar den Vorhandenen mit dem neuen Benutzer ersetzen. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Ersetzbare Indizes generieren auch `putBy()`-Methoden, die es dir ermöglichen Objekte zu aktualisieren statt sie zu ersetzen. Die vorhandene ID wird wiederverwendet und Links bleiben erhalten. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // Nutzer existiert nicht, also ist es das gleiche wie put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` Wie du sehen kannst, wird die ID des zuerst eingefügten Benutzers wiederverwendet. ## Indizes ohne Berücksichtigung auf Groß-/Kleinschreibung Alle Indizes auf `String`- und `List`-Eigenschaften beachten standardmäßig die Groß-/Kleinschreibung. Wenn du einen Index erstellen willst, der die Groß-/Kleinschreibung nicht berücksichtigt, kannst du die `caseSensitive`-Option verwenden: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Index-Typen Es gibt verschiedene Typen von Indizes. Meistens wirst du einen `IndexType.value`-Index verwenden wollen, aber Hash-Indizes sind effizienter. ### Wert-Index Wert-Indizes sind der Standardtyp und der Einzige, der für alle Eigenschaften erlaubt ist, die nicht Strings oder Listen enthalten. Eigenschaftswerte werden verwendet, um den Index zu erstellen. Im Fall von Listen, werden die Elemente der Liste verwendet. Es ist der flexibelste, aber auch platzraubendste der drei Index-Typen. :::tip Benutze `IndexType.value` für Primitives, Strings, wenn du `startsWith()`-Where-Klauseln brauchst, und Listen, wenn du nach einzelnen Elementen suchst. ::: ### Hash-Index Strings und Listen können gehasht werden um den für den Index benötigten Speicher drastisch zu verringern. Der Nachteil eines Hash-Indexes ist, dass sie nicht für Präfixsuchen (`startsWith()`-Where-Klauseln) verwendet werden können. :::tip Verwende `IndexType.hash` für Strings und Listen, wenn du die `startsWith`- und `elementEqualTo`-Where-Klauseln nicht benötigst. ::: ### HashElements-Index Stringlisten können als Ganzes gehasht werden (indem man `IndexType.hash` verwendet) oder die Elemente der Liste können seperat gehasht werden (indem man `IndexType.hashElements` nutzt) wodurch ein Mehreintragsindex mit gehashten Elementen erzeugt wird. :::tip Nutze `IndexType.hashElements` für `List` bei denen du `elementEqualTo`-Where-Klauseln benötigst. ::: ## Komposit-Indizes Ein Komposit-Index ist ein Index auf mehrere Eigenschaften. Isar erlaubt es dir zusammengesetzte Indizes mit bis zu drei Eigenschaften zu erstellen. Komposit-Indizes sind auch als Mehr-Spalten-Indizes bekannt. Es ist vermutlich am besten mit einem Beispiel zu starten. Wir erstellen eine Personen-Collection und definieren einen zusammengesetzten Index auf die Alters- und Namenseigenschaften: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Daten:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Generierter Index:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | Der generierte zusammengesetzte Index enthält alle Personen sortiert nach ihrem Alter und ihrem Namen. Komposit-Indizes sind super, wenn du effiziente Abfragen, sortiert nach mehreren Eigenschaften, stellen willst. Sie erlauben auch anspruchsvolle Where-Klauseln mit mehreren Eigenschaften: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` Die letzte Eigenschaft eines zusammengesetzten Index unterstützt auch Bedingungen wie `startsWith()` oder `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Mehrfach-Indizes Wenn du eine Liste mit `IndexType.value` indizierst, wird Isar automatische einen Mehrfach-Index erzeugen und jeder Eintrag in der Liste wird mit dem Objekt indiziert. Das funktioniert für alle Listentypen. Zu sinnvollen Anwendungen für Mehrfach-Indizes zählen das Indizieren einer Liste an Tags oder einen Volltext-Index zu erstellen. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` trennt einen String nach der [Unicode Annex #29](https://unicode.org/reports/tr29/)-Spezifikation in Worte, sodass es für fast alle Sprachen richtig funktioniert. **Daten:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Einträge mit doppelten Worten tauchen nur einmal im Index auf. **Generierter Index:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | Dieser Index kann nun für (Gleichheits- oder) Präfix-Where-Klauseln der individuellen Worte der Beschreibung verwendet werden. :::tip Statt Worte direkt zu speichern kannst du auch in Betracht ziehen das Ergebnis einer [Phonetischen Suche](https://de.wikipedia.org/wiki/Phonetische_Suche) wie von dem Algorithmus [Soundex](https://de.wikipedia.org/wiki/Soundex) zu verwenden. ::: ================================================ FILE: docs/docs/de/limitations.md ================================================ --- title: Limitationen --- # Limitationen Wie du weißt, funktioniert Isar auf Mobilgeräten und Desktops und läuft sowohl auf der VM, als auch im Web. Die beiden Plattformen sind sehr verschieden und haben unterschiedliche Limitationen. ## VM Limitationen - Nur die ersten 1024 Bytes eines Strings können für eine Präfix-Where-Klausel verwendet werden - Objekte können höchstens 16MB groß sein ## Web Limitationen Weil Isar Web auf IndexedDB beruht, gibt es dort mehr Limitationen, aber sie sind kaum zu merken, während du Isar benutzt. - Synchrone Methoden werden nicht unterstützt - Zurzeit sind die `Isar.splitWords()`- und `.matches()`-Filter noch nicht implementiert - Schemaänderungen werden nicht so genau wie in der VM überprüft, also achte darauf die Regeln einzuhalten - Alle Zahlen-Typen werden als Double (dem einzigen JS Zahlen-Typ) gespeichert, also hat `@Size32` keine Wirkung - Indizes werden anders dargestellt, wodurch Hash-Indizes nicht weniger Platz benötigen (auch wenn sie gleich funktionieren) - `col.delete()` und `col.deleteAll()` funktionieren korrekt, aber der Rückgabewert ist nicht richtig - `col.clear()` setzt den auto-increment-Wert nicht zurück - `NaN` wird als Wert nicht unterstützt ================================================ FILE: docs/docs/de/links.md ================================================ --- title: Links --- # Links Links ermöglichen es dir Verhältnisse zwischen Objekten, wie z.B. dem Autor (Benutzer) eines Kommentars, auszudrücken. Du kannst `1:1`, `1:n`, `n:m` Verhältnisse mit Isar-Links modellieren. Links zu nutzen ist unpraktischer als eingebettete Objekte zu benutzen, und du solltest eingebettete Objekte, wann immer möglich, verwenden. Stell dir den Link wie eine separate Tabelle vor, die die Beziehung enthält. Links ähneln SQL-Beziehungen, haben aber einen anderen Funktionsumfang und eine andere API. ## IsarLink `IsarLink` kann keines oder ein zugehöriges Objekt enthalten und kann genutzt werden um eine zu-einem-Relation darzustellen. `IsarLink` hat eine einzige Eigenschaft genannt `value`, die das verlinkte Objekt enthält. Links sind lazy, also musst du dem `IsarLink` explizit sagen den `value` zu Laden oder zu Speichern. Das kannst du erreichen, indem du `linkProperty.load()` und `linkProperty.save()` aufrufst. :::tip Die ID-Eigenschaft der Quell- und Ziel-Collections sollte nicht-final sein. ::: Für nicht-Web-Ziele werden Links automatisch geladen, wenn du sie zum ersten Mal verwendest. Fangen wir damit an einen IsarLink zu einer Collection hinzuzufügen: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` Wir haben einen Link zwischen Lehrern und Schülern definiert. Jeder Schüler kann in diesem Beispiel genau einen Lehrer haben. Zuerst legen wir einen Lehrer an und fügen ihn dann einem Schüler hinzu. Wir müssen den Lehrer mit der `.put()`-Methode einfügen und den Link manuell speichern. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teachers.save(); }); ``` Wir können den Link jetzt nutzen: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Versuchen wir das gleiche mit synchronem Code. Wir brauchen den Link nicht manuell zu speichern, weil `.putSync()` automatisch alle Links speichert. Es erzeugt sogar den Lehrer für uns. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks Es würde mehr Sinn ergeben, wenn der Schüler aus dem vorherigen Beispiel mehrere Lehrer haben kann. Glücklicherweise hat Isar `IsarLinks`, was mehrere zugehörige Objekte beinhalten kann und eine zu-vielen-Relation ausdrückt. `IsarLinks` wird von `Set` erweitert und stellt alle Methoden die auf Sets angewandt werden können zur Verfügung. `IsarLinks` verhält sich ähnlich wie `IsarLink` und ist auch lazy. Um alle verlinkten Objekte zu laden, musst du die Methode `linkProperty.load()` aufrufen. Um die Änderungen persistent zu machen, musst du `linkProperty.save()` aufrufen. Intern werden `IsarLink` und `IsarLinks` auf die gleiche Weise dargestellt. Wir können den `IsarLink` von vorher zu einem `IsarLinks` ausbauen, um mehrere Lehrer einem einzelnen Schüler zuzuweisen (ohne Daten zu verlieren). ```dart @collection class Student { Id? id; late String name; final teacher = IsarLinks(); } ``` Das funktioniert, weil wir den Namen des Links (`teacher`) nicht verändert haben, weshalb sich Isar von vorher daran erinnert. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Rückverlinkungen Ich höre dich schon, "Was, wenn wir umgekehrte Relationen ausdrücken möchten?", fragen. Mach dir keine Sorgen; wir führen jetzt Rückverlinkungen ein. Rückverlinkungen sind Links in umgekerhrter Richtung. Jeder Link hat implizit immer eine Rückverlinkung. Du kannst sie in deiner App verfügbar machen, indem du `IsarLink` oder `IsarLinks` mit `@Backlink()` annotierst. Rückverlinkungen benötigen keinen zusätzlichen Speicher oder Ressourcen; du kannst sie frei hinzufügen, löschen und umbenennen, ohne Daten zu verlieren. Wir wollen wissen, welche Schüler ein spezifischer Lehrer hat, also definieren wir eine Rückverlinkung: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` Wir müssen angeben, auf welchen Link die Rückverlinkung zeigt. Es ist möglich, mehrere verschiedene Links zwischen zwei Objekten zu haben. ## Links initialisieren `IsarLink` und `IsarLinks` haben Konstruktoren ohne Argumente und sollten verwendet werden um die Link-Eigenschaft anzugeben, wenn das Objekt erstellt wird. Es hat sich bewährt Link-Eigenschaften `final` zu setzen. Wenn du dein Objekt zum ersten Mal mit der `put()`-Methode speicherst, wird der Link mit Quell- und Ziel-Collection initialisiert und du kannst Methoden wie `load()` und `save()` benutzen. Ein Link fängt sofort an Änderungen zu verfolgen, nachdem er erzeugt wurde, sodass du Relationen sogar anlegen oder entfernen kannst, bevor der Link initialisiert wurde. :::danger Es ist verboten einen Link zu einem anderen Objekt zu übertragen. ::: ================================================ FILE: docs/docs/de/queries.md ================================================ --- title: Abfragen --- # Abfragen Mit Abfragen kannst du Einträge finden, die bestimmten Bedingungen entsprechen, zum Beispiel: - Finde alle markierten Kontakte - Finde eindeutige Vornamen in den Kontakten - Lösche alle Kontakte, die keinen Nachnamen definiert haben Weil Abfragen nicht in Dart, sondern auf der Datenbank ausgeführt werden, sind sie sehr schnell. Wenn du Indizes sinnvoll benutzt, kannst du deine Abfrageleistung sogar weiter steigern. Als nächstes lernst du, wie man Abfragen schreibt und wie du sie so schnell wie möglich machen kannst. Es gibt zwei verschiedene Methoden um Einträge zu filtern: Filter und Where-Klauseln. Wir beginnen indem wir uns die Funktionsweise von Filtern ansehen. ## Filter Filter sind leicht zu benutzen und zu verstehen. Abhängig von den Typen deiner Eigenschaften gibt es verschiedene verfügbare Filteroperationen mit größtenteils selbsterklärenden Namen. Filter funktionieren, indem sie einen Ausdruck für jedes Objekt der zu filternden Collection auswerten. Wenn der Ausdruck `true` ergibt, fügt Isar das Objekt zu den Ergebnissen hinzu. Filter haben keinen Einfluss auf die Reihenfolge der Ergebnisse. Wir benutzen das folgende Modell für die Beispiele weiter unten: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Abfragebedingungen Abhängig vom Feld-Typen gibt es verschiedene mögliche Bedingungen. | Bedingung | Beschreibung | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `.equalTo(value)` | Trifft auf Werte zu, die mit dem angegebenen `value` übereinstimmen. | | `.between(lower, upper)` | Trifft auf Werte zu, die zwischen `lower` und `upper` liegen. | | `.greaterThan(bound)` | Trifft auf Werte zu, de größer als `bound` sind. | | `.lessThan(bound)` | Trifft auf Werte zu, die kleiner als `bound` sind. `null`-Werte werden eingeschlossen, da `null` als kleiner als jeder andere Wert betrachtet wird. | | `.isNull()` | Trifft auf Werte zu, die `null` sind. | | `.isNotNull()` | Trifft auf Werte zu, die nicht `null` sind. | | `.length()` | Abfragen nach Längen von Listen, Strings und Links filtern Objekte basierend auf der Anzahl der Elemente in einer Liste oder in einem Link. | Nehmen wir an, dass die Datenbank vier Schuhe mit den Gößen 39, 40, 46 und einen mit einer nicht festgelegten Größe (`null`) hat. Wenn du keine Sortierung durchführst, werden die Werte nach ID geordnet zurückgegeben. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Logische Operatoren Du kannst Bedingungen verbinden, indem du die folgenden logischen Operatoren verwendest: | Operator | Beschreibung | | ---------- | ------------------------------------------------------------------------------------ | | `.and()` | Ergibt `true`, wenn von linkem und rechtem Ausdruck beide `true` ergeben. | | `.or()` | Ergibt `true`, wenn mindestens einer von beiden Ausdrücken `true` ergibt. | | `.xor()` | Ergibt `true`, wenn genau einer von beiden Ausdrücken `true` ergibt. | | `.not()` | Negiert das Ergebnis des nachfolgenden Ausdrucks. | | `.group()` | Gruppiert Bedingungen und ermöglicht es eine Reihenfolge der Auswertung festzulegen. | Wenn du alle Schuhe mit der Größe 46 finden möchstest, kannst du die folgende Abfrage verwenden: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` Wenn du mehr als eine Bedingung angeben möchtest, kannst du mehrere Filter verbinden, indem du sie mit logischem **und** `.and()`, logischem **oder** `.or()` oder logischem **exklusiven oder** `.xor()` verbindest. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filter werden implizit mit einem logischen UND verbunden. .isUnisexEqualTo(true) .findAll(); ``` Diese Abfrage ist äquivalent zu: `size == 46 && isUnisex == true`. Du kannst auch Bedingungen gruppieren, indem du `.group()` benutzt: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` Diese Abfrage ist äquivalent zu: `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. Um eine Bedingung oder Gruppe zu negieren kannst du das logische **oder** `.not()` verwenden: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` Diese Abfrage ist äquivalent zu: `size != 46 && isUnisex != true`. ### String-Bedingungen Zusätzlich zu den vorher genannten Abfragebedingungen, bieten String-Werte ein paar mehr Bedingungen. Platzhalter, ähnlich zu beispielsweise Regex, erlauben mehr Flexibilität beim Suchen. | Bedingung | Beschreibung | | -------------------- | ------------------------------------------------------------------------------ | | `.startsWith(value)` | Trifft auf String-Werte zu, die mit dem angegebenen `value` beginnen. | | `.contains(value)` | Trifft auf String-Werte zu, die das angegebene `value` enthalten. | | `.endsWith(value)` | Trifft auf String-Werte zu, die mit dem angegebenen `value` enden. | | `.matches(wildcard)` | Trifft auf String-Werte zu, die dem angegebenen `wildcard`-Muster entsprechen. | **Groß-/Kleinschreibung** Alle String-Operationen haben eine optionale `caseSensitive`-Eigenschaft, die standardmäßig `true` ist. **Platzhalter** Der [Ausdruck eines Platzhalter-Strings](https://de.wikipedia.org/wiki/Wildcard_(Informatik)) ist ein String, der normale Zeichen mit zwei speziellen Platzhalter-Zeichen verwendet: - Der `*` Platzhalter trifft auf keines oder mehr von jedem Zeichen zu. - Der `?` Platzhalter trifft auf jedes Einzelzeichen zu. Zum Beispiel trifft der Platzhalter-String `"d?g"` auf `"dog"`, `"dig"` und `"dug"` zu, nicht aber auf `"ding"`, `"dg"` oder `"a dog"`. ### Abfragemodifikatoren Manchmal ist es notwendig eine Abfrage auf Bedingungen aufzubauen oder für verschiedene Werte zu bauen. Isar hat ein sehr mächtiges Werkzeug um bedingte Abfragen zu bauen: | Modifikator | Beschreibung | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.optional(cond, qb)` | Erweitert die Abfrage nur, wenn die Bedingung `cond`, `true` ist. Das kann fast überall in einer Abfrage verwendet werden, beispielsweise um sie über eine Bedingung zu sortieren oder begrenzen. | | `.anyOf(list, qb)` | Erweitert die Abfrage für jeden Wert in `values` und verbindet die Bedingungen mit einem logischen **oder**. | | `.allOf(list, qb)` | Erweitert die Abfrage für jeden Wert in `values` und verbindet die Bedingungen mit einem logischen **und**. | In diesem Beispiel bauen wir eine Methode, die Schuhe mit einem optionale Filter finden kann: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // Wendet den Filter nur an, wenn sizeFilter != null ist (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` Wenn du alle Schuhe finden möchtest, die eine von mehreren Schuhgrößen haben, kannst du entweder eine konventionelle Abfrage schreiben oder den `anyOf()` Modifikator verwenden: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` Abfragemodifikatoren sind besonders dann sinnvoll, wenn du dynamische Abfragen bauen möchtest. ### Listen Abfragen können sogar auf Listen gestellt werden: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` Du kannst eine Abfrage auf Basis der Listenlänge bauen: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` Diese sind äquivalent zu dem Dart-Code `tweets.where((t) => t.hashtags.isEmpty);` und `tweets.where((t) => t.hashtags.length > 5);`. Du kannst auch Abfragen basierend auf Listenelementen stellen: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` Das ist äquivalent zum Dart-Code `tweets.where((t) => t.hashtags.contains('flutter'));`. ### Eingebettete Objekte Eingebettete Objekte sind eines von Isars nützlichsten Features. Sie können sehr einfach abgefragt werden mit gleichen Bedingungen für Objekte der obersten Ebene. Nehmen wir an, dass wir das folgende Modell haben: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` Wir wollen alle Autos abfragen, die eine Marke mit dem Namen `"BMW"` und dem Land `"Germany"` haben. Wir können das mit der folgenden Abfrage erreichen: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Versuche immer verschachtelte Abfragen zu gruppieren. Die vorherige Abfrage ist effizienter als die folgende, auch wenn das Ergebnis gleich ist: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Links Wenn dein Modell [Links oder Rückverlinkungen](links) enthält, kannst du deine Abfrage auf Basis der verlinkten Objekte oder der Anzahl an verlinkten Objekten filtern. :::warning Beachte, dass Link-Abfragen teuer sein können, weil Isar die verlinkten Objekte abrufen muss. Versuche stattdessen eingebettete Objekte zu verwenden. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Wir wollen alle Schüler finden, die einen Mathe- oder Englischlehrer haben: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Link-Filter resultieren in `true`, wenn mindestens eines der verlinkten Objekte den Bedingungen entspricht. Suchen wir nach allen Schülern, die keine Lehrer haben: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` oder alternativ: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where-Klauseln Where-Klauseln sind eine sehr mächtiges Werkzeug, aber es kann ein bisschen herausfordernd sein sie zu meistern. Im Gegensatz zu Filtern nutzen Where-Klauseln die Indizes, die du im Schema definiert hast, um die Abfragebedingungen zu überprüfen. Einen Index abzufragen ist deutlich schneller als jeden Eintrag einzeln zu filtern. ➡️ Lerne mehr: [Indizes](indexes) :::tip Als eine einfache Regel solltest du immer versuchen die Einträge so weit wie möglich mit Where-Klauseln einzugrenzen und das restliche Filtern mit Filtern machen. ::: Du kannst Where-Klauseln nur mit logischem **oder** verbinden. In anderen Worten, kannst du mehrere Where-Klauseln zusammenfügen, aber nicht die Überschneidung mehrerer Where-Klauseln abfragen. Lass uns Indizes zu der Schuh-Collection hinzufügen: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` Hier gibt es zwei Indizes. Der Index auf `size` erlaubt es uns Where-Klauseln wie `.sizeEqualTo()` zu verwenden. Der zusammengesetzte Index auf `isUnisex` erlaubt es uns Where-Klauseln wie `.isUnisexSizeEqualTo()` zu nutzen. Aber auch `.isUnisexEqualTo()` ist möglich, weil du immer jedes Präfix eines Indexes benutzen kannst. Wir können unsere Abfrage von vorher, die Unisex-Schuhe der Größe 46 findet, also mithilfe des zusammengesetzten Indexes umschreiben. Diese Abfrage sollte deutlich schneller sein, als die vorherige: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where-Klauseln haben zwei weitere Superkräfte: Sie geben dir "kostenloses" Sortieren und eine superschnelle Eindeutigkeitsoperation. ### Where-Klauseln und Filter verbinden Erinnerst du dich an die `shoes.filter()`-Abfragen? Das ist in Wirklichkeit nur eine Kurzform für `shoes.where().filter()`. Du kannst (und solltest) Where-Klauseln und Filter in der gleichen Abfrage verbinden, um die Vorteile beider zu nutzen: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` Die Where-Klausel wird zuerst angewendet, um die Anzahl an Objekten, die gefiltert werden müssen, zu reduzieren. Dann wird der Filter auf die übrig gebliebenen Objekte angewendet. ## Sortierung Du kannst definieren, wie Ergebnisse deiner Abfrage sortiert werden sollen, indem du die Methoden `.sortBy()`, `.sortByDesc()`, `.thenBy()` und `.thenByDesc()` nutzt. Um alle Schuhe nach Modellnamen in aufsteigender und nach der Größe in absteigender Reihenfolge sortiert zu bekommen, ohne einen Index zu benutzen, aknnst du die folgende Abfrage stellen: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Viele Ergebnisse zu sortieren kann teuer sein, besonders, weil das Sortieren vor dem Offset und vor der Limitierung stattfindet. Die Sortiermethoden benutzen niemals Indizes. Glücklicherweise können wir wieder Sortierung mit Where-Klauseln verwenden und so unsere Abfrage blitzschnell machen, auch wenn wir eine Million Objekte sortieren müssen. ### Sortierung mit Where-Klauseln Wenn du eine **einzige** Where-Klausel in deiner Abfrage nutzt, sind die Ergebnisse schon nach dem Index sortiert. Das ist eine große Sache! Nehmen wir an, wir haben Schuhe in den Größen `[43, 39, 48, 40, 42, 45]` und wir wollen alle Schuhe mit einer Größe größer als `42` haben und sie auch nach Größe sortiert haben: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // Sortiert die Ergebnisse auch nach Größe .findAll(); // -> [43, 45, 48] ``` Wie du sehen kannst, sind die Ergebnisse nach dem `size`-Index sortiert. Wenn du die Reihenfolge der Where-Klausel umkehren möchtest, kannst du `sort` auf `Sort.desc` setzen: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` Manchmal willst du keine Where-Klausel verwenden, aber trotzdem von der impliziten Sortierung profitieren. Dann kannst du die Where-Klausel `any` verwenden: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` Wenn du einen Komposit-Index verwendest, werden die Ergebnisse nach allen Feldern des Indexes sortiert. :::tip Für den Fall, dass deine Ergebnisse sortiert sein müssen, versuche einen Index zu benutzen. Besonders wenn du mit `offset()` oder `limit()` arbeitest: ::: Manchmal ist es nicht möglich oder sinnvoll einen Index zum Sortieren zu nutzen. Für solche Fälle solltest du Indizes benutzen, um zumindest die Anzahl an zu sortierenden Einträgen so weit wie möglich einzugrenzen. ## Eindeutige Werte Um nur Einträge mit eindeutigen Werten zurückzubekommen, kannst du das Unterscheidbarkeitsprädikat verwenden. Zum Beispiel, um herauszufinden, wie viele unterscheidbare Schuhmodelle es in deiner Isar-Datenbank gibt: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` Du kannst auch mehrere Unterscheidbarkeitsbedingungen verketten, um alle Schuhe mit unterscheidbaren Modell-Größe-Kombinationen zu finden: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Nur das erste Ergebnis jeder Unterscheidbarkeitskombination wird zurückgegeben. Um das zu überprüfen kannst du Where-Klauseln und Sortieroperationen verwenden. ### Unterscheidbare Where-Klauseln Wenn du einen nicht eindeutigen Index hast, kann es sein, dass du alle seine unterscheidbaren Werte haben möchtest. Du könntest die `distinctBy`-Operation des vorherigen Abschnitts verwenden, aber sie wird erst nach dem Sortieren und Filtern angewandt, sodass ein bisschen Overhead entsteht. Wenn du nur eine einzelne Where-Klausel verwendest, kannst du stattdessen dem Index vertrauen die Unterscheidbarkeitsoperation durchzuführen. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip Theoretisch könntest du sogar mehrere Where-Klauseln für Sortierung und Unterscheidbarkeit nutzen. Die einzige Einschränkung besteht darin, dass sich diese Where-Klauseln nicht überschneiden, also nicht denselben Index verwenden dürfen. Für die richtige Sortierung müssen sie auch in Sortierreihenfolge angewandt werden. Sei sehr vorsichtig, wenn du dich darauf verlässt. ::: ## Offset & Limitierung Es ist oft eine gute Idee die Anzahl an Ergebnissen einer Abfrage zu beschränken, für beispielsweise lazy Listenansichten. Du kannst das erreichen, indem du ein `limit()` setzt: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` Indem du ein `offset()` setzt, kannst du die Ergebnisse deiner Abfrage in mehrere Auflistungen aufteilen. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Weil das instanziieren eines Dart-Objekts meistens der teuerste Teil beim Ausführen einer Abfrage ist, ist es eine gute Idee nur die Objekte zu laden, die du benötigst. ## Reihenfolge der Ausführung Isar führt Abfragen immer in der gleichen Reihenfolge aus: 1. Primär- oder Sekundärindex durchlaufen, um Objekte zu finden (Where-Klauseln anwenden) 2. Objekte filtern 3. Ergebnisse sortieren 4. Unterscheidbarkeitsoperation durchführen 5. Offset & Limit auf Ergebnisse anwenden 6. Ergebnisse zurückgeben ## Abfrageoperationen In den vorangegangenen Beispielen haben wir `.findAll()` verwendet, um alle passenden Objekte zu erhalten. Es sind jedoch mehr Operationen verfügbar: | Operation | Beschreibung | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `.findFirst()` | Erhalte nur das erste passende Objekt oder `null` wenn kein passendes gefunden wird. | | `.findAll()` | Erhalte alle passenden Objekte. | | `.count()` | Zählt, wieviele Objekte der Abfrage entsprechen. | | `.deleteFirst()` | Löscht das erste passende Objekt aus der Collection. | | `.deleteAll()` | Löscht alle passenden Objekte aus der Collection. | | `.build()` | Konstruiert eine Abfrage um sie später wiederzuverwenden. Das erspart die Kosten, eine Abfrage erneut zu bauen, wenn du sie mehrfach ausführen willst. | ## Abfragen auf Eigenschaften Wenn du nur an den Werten einer bestimmten Eigenschaft interessiert bist, kannst du Abfragen auf Eigenschaften machen. Baue einfach eine normale Abfrage und wähle eine Eigenschaft: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` Nur eine einzige Eigenschaft zu nutzen erspart Zeit bei der Deserialisierung. Abfragen auf Eigenschaften funktionieren auch bei eingebetteten Objekten und Listen. ## Aggregation Isar unterstützt die Aggregation der Werte einer Abfrage auf Eigenschaften. Die folgenden Aggregatoroperationen sind verfügbar: | Operation | Beschreibung | | ------------ | -------------------------------------------------------------------- | | `.min()` | Findet den minimalen Wert oder `null`, wenn keiner passt. | | `.max()` | Findet den maximalen Wert oder `null`, wenn keiner passt. | | `.sum()` | Addiert alle Werte. | | `.average()` | Berechnet den Durchschnitt aller Werte oder `NaN` wenn keiner passt. | Aggregatoren zu nutzen ist deutlich schneller, als alle passenden Objekte zu finden und die Aggregation manuell durchzuführen. ## Dynamische Abfragen :::danger Dieser Abschnitt ist höchstwahrscheinlich nicht wichtig für dich. Es ist davon abzuraten dynamische Abfragen zu nutzen, es sei denn du benötigst sie wirklich (was selten vorkommt). ::: Alle der vorherigen Beispiele haben den QueryBuilder und seine statischen Erweiterungsmethoden genutzt. Vielleicht möchtest du dynamische Abfragen oder eine benutzerdefinierte Abfragesprache (wie den Isar Inspektor) bauen. In dem Fall kannst du die Methode `buildQuery()` verwenden: | Parameter | Beschreibung | | --------------- | ---------------------------------------------------------------------------------------------------------- | | `whereClauses` | Die Where-Klauseln der Abfrage. | | `whereDistinct` | Ob Where-Klauseln nur unterscheidbare Werte zurückgeben sollen (nur sinnvoll für einzelne Where-Klauseln). | | `whereSort` | Die Durchlaufreihenfolge der Where-Klauseln (nur sinnvoll für einzelne Where-Klauseln). | | `filter` | Die Filter, die auf die Ergebnisse angewendet werden sollen. | | `sortBy` | Eine Liste an Eigenschaften nach denen sortiert werden soll. | | `distinctBy` | Eine Liste an Eigenschaften, an denen die Unterscheidbarkeit festgemacht wird. | | `offset` | Der Offset der Ergebnisse. | | `limit` | Die maximale Anzahl an Ergebnissen, die zurückgegeben werden. | | `property` | Wenn nicht-null, werden nur die Werte dieser Eigenschaft zurückgegeben. | Bauen wir eine dynamische Abfrage: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` Die folgende Abfrage ist äquivalent: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/de/recipes/data_migration.md ================================================ --- title: Migration von Daten --- # Migration vron Daten Isar migriert deine Datenbankschemas automatisch, wenn du Collections, Felder oder Indizes hinzufügst oder entfernst. Manchmal möchtest du möglicherweise auch deine Daten migrieren. Isar liefert keine eingebaute Lösung, weil das willkürliche Migrationsbeschränkungen festlegen würde. Es ist leicht eine passende Migrationslogik zu implementieren. Wir wollen in diesem Beispiel eine einzige Version für die gesamte Datenbank verwenden. Wir benutzen Shared Preferences um die derzeitige Version zu speichern und vergleichen diese mit der Version zu der wir unsere Daten migrieren wollen. Wenn die Versionen nicht übereinstimmen, migrieren wir die Daten und aktualisieren die Version. :::tip Du kannst auch jeder Collection seine eigene Version zuweisen und sie individuell migrieren. ::: Stell dir vor, wie haben eine Benutzer-Collection mit einem Feld Geburtstag. In Version 2 unserer App benötigen wir ein zusätzliches Feld Geburtsjahr um Benutzer anhand des Alters abzufragen. Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` Das Problem ist, dass vorhandene Benutzermodelle ein leeres `birthYear`-Feld haben werden, weil es in Version 1 nicht existiert hat. Wir müssen die Daten migrieren, um das `birthYear`-Feld zu setzen. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // Wenn die Version nicht gesetzt (neue Installation), oder schon 2 ist, müssen wir nicht migrieren return; default: throw Exception('Unknown version: $currentVersion'); } // Version aktualisieren await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // Wir paginieren durch die Benutzer, um zu vermeiden, dass wir alle Benutzer gleichzeitig in den Speicher laden for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // Wir müssen nichts aktualisieren, weil der birthYear-Getter verwendet wird await isar.users.putAll(users); }); } } ``` :::warning Wenn du viele Daten migrieren musst, solltest du überlegen einen Hintergrund-Isolate zu verwenden, um eine Belastung des UI-Threads zu verhindern. ::: ================================================ FILE: docs/docs/de/recipes/full_text_search.md ================================================ --- title: Volltextsuche --- # Volltextsuche Volltextsuche ist ein mächtiges Werkzeug um Text in der Datenbank zu suchen. Du solltest schon damit vertraut sein, wie [Indizes](/indexes) funktionieren, aber wir schauen uns die Grundlagen an. Ein Index funktioniert wie eine Nachschlagetabelle, die es der Abfrage-Engine ermöglicht Einträge mit einem bestimmten Wert schnell zu finden. Zum Beispiel, wenn du ein `title`-Feld in deinem Objekt hast, kannst du einen Index auf das Feld anlegen, um die Geschwindigkeit zu erhöhen, ein Objekt mit bestimmtem Titel zu finden. ## Warum ist Volltextsuche sinnvoll? Du kannst Text leicht durchsuchen, indem du Filter verwendest. Es gibt mehrere unterschiedliche String-Operationen, zum Beispiel `.startsWith()`, `.contains()` und `.matches()`. Das Problem mit Filtern ist, dass ihre Laufzeit `O(n)` ist, wobei `n` die Anzahl der Einträge in der Collection ist. String-Operationen wie `.matches()` sind besonders teuer. :::tip Volltextsuche ist deutlich schneller als Filter, aber Indizes haben ein paar Einschränkungen. In diesem Rezept wollen wir uns angucken, wie man diese Limitationen umgeht. ::: ## Grundlegendes Beispiel Die Idee ist immer die Gleiche: Anstatt den ganzen Text zu indizieren, indizieren wir die Worte im Text, sodass wir individuell nach ihnen suchen können. Bauen wir den grundlegendsten Volltext-Index: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` Wir können jetzt nach Nachrichten suchen, die spezifische Worte enthalten: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` Diese Abfrage ist superschnell, aber es gibt ein paar Probleme: 1. Wir können nur nach ganzen Worten suchen 2. Wir missachten Zeichensetzung 3. Wir unterstützen keine anderen Leerzeichen ## Text richtig trennen Versuchen wir das vorherige Beispiel zu verbessern. Wir könnten versuchen einen komplizierten Regex zu entwickeln, um Worte zu trennen, aber das ist vermutlich langsam und in Grenzfällen falsch. Der [Unicode Annex #29](https://unicode.org/reports/tr29/) definiert wie man, für fast alle Sprachen, Text richtig in Worte trennt. Das ist ziemlich kompliziert, aber glücklicherweise macht Isar den schwierigsten Teil der Arbeit für uns: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## Ich will mehr Kontrolle Das ist kinderleicht! Wir können unseren Index so ändern, dass er auch Präfixe findet und Groß-/Kleinschreibung ignoriert: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` Isar speichert die Worte standardmäßig als gehashte Werte, was schnell und platzsparend ist. Aber Hashes können nicht für die Präfixüberprüfung verwendet werden. Wenn wir `IndexType.value` verwenden, können wir den Index ändern, um direkt Worte zu benutzen. Das ermöglicht uns die `.titleWordsAnyStartsWith()`-Where-Klausel benutzen zu können: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## Ich brauche auch `.endsWith()` Klar! Wir werden einen Trick verwenden, um `.endsWith()` verwenden zu können: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` Vergiss nicht das Wortende umzukehren nach dem du suchen willst: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Abstammungsalgorithmen Leider unterstützen Indizes nicht die `.contains()`-Methode (das stimmt auch für andere Datenbanken). Aber es gibt ein paar Alternativen, die es wert sind, erkundet zu werden. Eine Wahl hängt stark von deinem Verwendungszweck ab. Ein Beispiel ist, den Ursprung von Worten, statt ganzer Worte, zu indizieren. Ein Abstammungsalgorithmus ist der Prozess einer linguistischen Normalisierung, bei dem die Varianten eines Wortes in eine gleichmäßige Form reduziert werden: ``` connection connections connective ---> connect connected connecting ``` Beliebte Algorithmen sind der [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) und der [Snowball stemming algorithms](https://snowballstem.org/algorithms/). Es gibt auch fortgeschrittenere Formen wie der [Lemmatisierung](https://de.wikipedia.org/wiki/Lemma_(Lexikographie)#Lemmatisierung). ## Phonetische Suche Eine [Phonetische Suche](https://de.wikipedia.org/wiki/Phonetische_Suche) ist ein Algorithmus, um Worte nach ihrer Aussprache zu indizieren. Anders augedrückt, erlaubt es dir Worte zu finden, die ähnlich zu den Gesuchten klingen. :::warning Die meisten phonetischen Algorithmen unterstützen nur eine einzige Sprache. ::: ### Soundex [Soundex](https://de.wikipedia.org/wiki/Soundex) ist ein phonetischer Algorithmus um Namen danach zu indizieren, wie sie im Englischen ausgesprochen werden. Das Ziel ist es Homophone in die gleiche Repräsentation zu übertragen, sodass sie gefunden werden, trotz der kleinen Unterschiede in der Rechtschreibung. Es ist ein unkomplizierter Algorithmus, von dem es mehrere verbesserte Versionen gibt. Wenn du diesen Algorithmus verwendest, wegeben `"Robert"` und `"Rupert"` beide den String `"R163"`, während `"Rubin"` `"R150"` ergibt. `"Ashcraft"` und `"Ashcroft"` erzeugen beide `"A261"`. ### Double Metaphone Der phonetische Umwandlungsalgorithmus [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) ist die zweite Generation dieses Algorithmus. Er macht mehrere fundamentale Designverbesserungen gegenüber dem originalen Metaphone-Algorithmus. Double Metaphone klärt verschiedene Unregelmäßigkeiten im Englischen aufgrund von slawischer, germanischer, keltischer, griechischer, französischer, italienischer, spanischer, chinesischer und anderer Herkunft. ================================================ FILE: docs/docs/de/recipes/multi_isolate.md ================================================ --- title: Nutzung von mehreren Isolates --- # Nutzung von mehreren Isolates Statt in Threads, läuft der gesamte Dart-Code innerhalb von Isolates. Jeder Isolate hat einen eigenen Memory-Heap, was dafür sorgt, dass der Status eines Isolates von keinem anderen Isolate erreichbar ist. Auf Isar kann von mehreren Isolates gleichzeitig zugegriffen werden und sogar Watcher funktionieren über Isolates hinweg. In diesem Rezept werden wir prüfen, wie man Isar in einem Umfeld mit mehreren Isolates nutzt. ## Wann man mehrere Isolates benutzt Isar-Transaktionen werden parallel ausgeführt, auch wenn sie im gleichen Isolate laufen. In manchen Fällen ist es dennoch von Vorteil von mehreren Isolates auf Isar zuzugreifen. Der Grund ist, dass Isar einige Zeit benötigt, um Daten von und zu Dart-Objekten zu codieren und decodieren. Du kannst es dir vorstellen, als würdest du JSON codieren und decodieren (nur effizienter). Diese Operationen laufen innerhalb des Isolates, von dem auf die Daten zugegriffen wird und blockieren daher natürlich anderen Code in dem Isolate. In anderen Worten: Isar führt einen Teil der Arbeit in deinem Dart-Isolate aus. Wenn du nur ein paar hundert Objekte gleichzeitig lesen oder schreiben musst, ist es kein Problem, das im UI-Isolate zu tun. Aber für riesige Transaktionen oder wenn dein UI-Thread schon zu tun hat, solltest du überlegen ein seperates Isolate zu verwenden. ## Beispiel Die erste Sache, die wir machen müssen, ist Isar in einem neuen Isolate zu öffnen. Weil eine Instanz von Isar schon im zentralen Isolate offen ist, wird `Isar.open()` diese Instanz zurückgeben. :::warning Stelle sicher, dass du die gleichen Schemas wie im zentralen Isolate zur Verfügung stellst. Sonst wirst du einen Fehler erhalten. ::: `compute()` startet ein neues Isolate in Flutter und führt die angegebene Funktion in ihm aus. ```dart void main() { // Isar im UI-Isolate öffnen final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // Auf Änderungen in der Datenbank warten isar.messages.watchLazy(() { print('omg the messages changed!'); }); // Startet ein neues Isolate und erzeugt 10000 Nachrichten compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // Nach einiger Zeit: // > omg the messages changed! // > isolate finished } // Funktion, die im neuen Isolate ausgeführt werden soll Future createDummyMessages(int count) async { // Wir benötigen hier keinen Pfad, weil die Instanz schon offen ist final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // Wir benutzen synchrone Transaktionen in Isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` Es gibt ein paar interessante Dinge, die in dem Beispiel von eben auffallen: - `isar.messages.watchLazy()` wird im UI-Isolate aufgerufen und wird über Änderungen durch ein anderes Isolate benachrichtigt. - Instanzen werden über den Namen referenziert. Der Standardname ist `default`, aber in diesem Beispiel haben wir ihn auf `myInstance` gesetzt. - Wir haben eine synchrone Transaktion benutzt, um die Nachrichten zu erzeugen. Unser neues Isolate zu blockieren ist kein Problem und synchrone Transaktionen sind ein bisschen schneller. ================================================ FILE: docs/docs/de/recipes/string_ids.md ================================================ --- title: String-IDs --- # String-IDs Das hier ist eine der häufigsten Anfragen, die ich erhalte, daher ist hier ein Tutorial zur Verwendung von String-IDs. Isar unterstützt String-IDs nicht nativ, was einen guten Grund hat: Integer-IDs sind viel effizienter und schneller. Besonders bei Links ist der Overhead einer String-ID zu signifikant. Ich verstehe, dass du manchmal externe Daten speichern musst, die UUIDs oder andere nicht-Integer-IDs verwenden. Ich empfehle, die String-ID als Eigenschaft in deinem Objekt zu speichern und eine schnelle Hash-Implementation um 64-bit Integer zu generieren und als ID zu verwenden. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` Mit diesem Ansatz bekommst du das Beste aus beiden Welten: Effiziente Integer-IDs für Links und die Fähigkeit String-IDs zu nutzen. ## Schnelle Hash-Funktion Idealerweise sollte deine Hash-Funktion eine hohe Qualität haben (du willst keine Kollisionen) und schnell sein. Ich empfehle die folgende Implementation: ```dart /// FNV-1a 64bit Hash-Algorithmus optimiert für Dart-Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` Wenn du eine andere Hash-Funktion wählst, stelle sicher, dass sie einen 64-bit Integer zurückgibt und vermeide kryptographische Hash-Funktionen, weil die sehr viel langsamer sind. :::warning Vermeide es `string.hashCode` zu verwenden, weil nicht garantiert werden kann, dass die Methode über verschiedenen Plattformen und Versionen von Dart hinweg stabil ist. ::: ================================================ FILE: docs/docs/de/schema.md ================================================ --- title: Schema --- # Schema Wenn du Isar benutzt, um deine App-Daten zu speichern, dann hast du mit Collections zu tun. Eine Collection ist wie die Tabelle einer Datenbank in der angeschlossenen Isar-Datenbank und kann nur einen einzigen Typen von Dart Objekt enthalten. Jedes Collection-Objekt repräsentiert eine Zeile mit Daten in der zugehörigen Collection. Die Definition einer Collection wird "Schema" genannt. Der Isar-Generator macht die meiste Arbeit für dich und generiert den Großteil des Codes den du benötigst, um die Collection zu benutzen. ## Aufbau einer Collection Jede Collection in Isar wird über die Annotation `@collection` oder `@Collection()` an einer Klasse definiert. Eine Isar-Collection enthält Felder für jede Spalte in der zugehörigen Tabelle der Datenbank, auch eine, die dem Primärschlüssel entspricht. Der folgende Code ist ein Beispiel einer simplen Collection, welche eine `User`-Tabelle mit den Spalten ID, Vorname und Nachname definiert: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip Um ein Feld persistent zu machen, muss Isar Zugriff auf das Feld haben. Du kannst sicherstellen, dass Isar Zugriff auf ein Feld hat, indem du es öffentlich machst, oder indem du Getter- und Setter-Methoden definierst. ::: Es gibt ein paar optionale Parameter, um die Collection anzupassen: | Konfiguration | Beschreibung | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `inheritance` | Steuert, ob Felder von Elternklassen und Mixins in Isar gespeichert werden. Standardmäßig aktiviert. | | `accessor` | Erlaubt dir den Standardnamen des Collection-Accessors umzubenennen (zum Beispiel zu `isar.contacts` für die `Contact`-Collection). | | `ignore` | Erlaubt es bestimmte Eigenschaften zu ignorieren. Diese werden auch bei Superklassen angewendet. | ### Isar ID Jede Collection-Klasse muss eine ID-Eigenschaft vom Typen `Id` definieren, die ein Objekt eindeutig identifiziert. `Id` ist eigentlich nur ein Alias für `int`, der es dem Isar Generator ermöglicht die ID-Eigenschaft zu erkennen. Isar indiziert ID Felder automatisch, was dir ermöglicht Objekte effizient anhand ihrer ID zu erhalten und modifizieren. Du kannst eintweder IDs selbst zuweisen oder Isar fragen eine sich automatisch erhöhende ID festzulegen. Wenn das `id`-Feld `null` und nicht `final` ist, wird Isar eine auto-increment ID setzen. Wenn du eine nicht null-bare auto-increment ID haben möchtest, kannst du `Isar.autoIncrement` anstatt von `null` verwenden. :::tip Auto-increment IDs werden nicht wiederverwendet, wenn ein Objekt gelöscht wird. Der einzige Weg auto-increment IDs zurückzusetzen ist die Datenbank zu leeren. ::: ### Collections und Felder umbenennen Isar benutzt standardmäßig den Klassennamen als Collectionnamen. Genauso verwendet Isar in der Datenbank Feldnamen als Spaltennamen. Wenn du willst, dass eine Collection oder ein Feld einen anderen Namen hat, dann füge die Annotation `@Name` hinzu. Das folgende Beispiel demonstriert angepasste Namen für Collections und Felder: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Du solltest besonders dann, wenn du Dart-Felder oder -Klassen umbenennen willst, überlegen, die `@Name`-Annotation zu verwenden. Sonst wird die Datenbank das Feld oder die Collection löschen und neu erzeugen. ### Felder ignorieren Isar stell sicher, dass alle öffentlichen Felder einer Collecion-Klasse erhalten bleiben. Wenn du eine Eigenschaft oder einen Getter mit `@ignore` annotierst, kannst du diese von der Sicherstellung ausschließen, wie im folgenden Code-Schnipsel gezeigt: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` In Fällen, in denen deine Collection Felder von der Eltern-Collection erhält, ist es meist leichter die `ignore`-Eigenschaft der `@Collection`-Annotation zu verwenden: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` Wenn eine Collection ein Feld mit einem Typen enthält, der nicht von Isar unterstützt wird, musst du das Feld ignorieren. :::warning Beachte, dass es keine gute Vorgehensweise ist, Informationen in Isar-Objekten zu speichern, die nicht gesichert werden. ::: ## Unterstützte Typen Isar unterstützt die folgenden Datentypen: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` Zusätzlich sind eingebettete Objekte und Enums unterstützt. Wir behandeln diese weiter unten. ## byte, short, float In vielen Fällen benötigst du nicht den gesamten Bereich eines 64-bit Integers oder Doubles. Isar unterstützt zusätzliche Typen, die es dir erlauben Speicherpaltz beim Speichern kleinerer Zahlen zu sparen. | Typ | Größe in bytes | Bereich | | ---------- | -------------- | -------------------------------------------------------- | | **byte** | 1 | 0 bis 255 | | **short** | 4 | -2.147.483.647 bis 2.147.483.647 | | **int** | 8 | -9.223.372.036.854.775.807 bis 9.223.372.036.854.775.807 | | **float** | 4 | -3,4e38 bis 3,4e38 | | **double** | 8 | -1,7e308 bis 1,7e308 | Die zusätzlichen Zahl-Typen sind nur Aliase für die nativen Dart-Typen, also beispielsweise `short` zu benutzen funktioniert genau, wie wenn du `int` nutzen würdest. Hier ist eine Beispiel-Collection, welche alle der eben genannten Typen enthält: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` Alle Zahlen-Typen können auch in Listen verwendet werden. Um Bytes zu speichern solltest du `List` benutzen. ## Null-bare Typen Zu verstehen wie Null-barkeit in Isar funktioniert ist essentiell: Zahlen-Typen haben **KEINE** gemeinsame festgelegte `null`-Darstellung. Stattdessen wird ein bestimmter Wert genutzt: | Typ | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** | `int.MIN` | | **float** | `double.NaN` | | **double** | `double.NaN` | `bool`, `String`, und `List` haben eine seperate `null`-Darstellung. Dieses Verhalten erlaubt Leistungsverbesserungen, und ermöglicht es die Null-barkeit deiner Felder frei zu ändern, ohne eine Migration oder speziellen Code zum handhaben von `null`-Werten zu benötigen. :::warning Der `byte`-Typ unterstützt keine Null-Werte. ::: ## DateTime Isar speichert keine Zeitzoneninformationen von deinen Daten. Stattdessen wandelt es `DateTime`s zu UTC um, bervor es diese speichert. Isar gibt jedes Datum in lokaler Zeit zurück. `DateTime`s werden mit Mikrosekunden-Präzision gespeichert. In Browsern ist, aufgrund von JavaScript-Limitationen, nur Millisekunden-Präzision möglich. ## Enum Isar ermöglicht es Enums wie andere Isar-Typen zu nutzen und zu speichern. Du musst aber wählen, wie Isar den Enum auf dem Datenträger abbilden soll. Isar unterstützt vier verschiedene Strategien: | Enum-Typ | Beschreibung | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `ordinal` | Der Index des Enums wird als `byte` gespeichert. Das ist sehr effizienzt, aber erlaubt keine Null-baren Enums. | | `ordinal32` | Der Index des Enums wird als `short` (4-Byte-Integer) gespeichert. Erlaubt keine Null-baren Enums. | | `name` | Der Name des Enums wird als `String` gespeichert. | | `value` | Eine angepasste Eigenschaft wird genutzt, um den Enum-Wert abzurufen. | :::warning `ordinal` und `ordinal32` basieren auf der Reihenfolge der Enum-Werte. Wenn du die Reihenfolge änderst, werden existierende Datenbanken falsche Werte zurückgeben. ::: Schauen wir uns ein Beispiel für jede Strategie an. ```dart @collection class EnumCollection { Id? id; @enumerated // entspricht EnumType.ordinal late TestEnum byteIndex; // ist nicht Null-bar @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // ist nicht Null-bar @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Natürlich können Enums auch in Listen benutzt werden. ## Eingebettete Objekte Es ist oft hilfreich verschachtelte Objekte in deinem Collection-Modell zu haben. Daher gibt es keine Begrenzung, wie tief die Verschachtelung von Objekten sein kann. Beachte jedoch, dass der gesamte Objekt-Baum in die Datenbank geschrieben werden muss, um ein sehr tief verschachteltes Objekt zu aktualisieren. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Eingebettete Objekte können Null-bar sein und andere Objekte erweitern. Die einzige Voraussetzung ist, dass sie mit `@embedded` annotiert werden und einen Standardkonstruktor ohne erforderliche Parameter haben. ================================================ FILE: docs/docs/de/transactions.md ================================================ --- title: Transaktionen --- # Transaktionen In Isar verbinden Transaktionen mehrere Datenbankoperationen in einen einzigen Arbeitsvorgang. Die meisten Interaktionen mit Isar nutzen implizit Transaktionen. Lese- & Schreibzugriff ist in Isar [ACID](https://de.wikipedia.org/wiki/ACID)-konform. Transaktionen werden automatisch zurückgesetzt, wenn ein Fehler auftritt. ## Explizite Transaktionen In einer expliziten Transaktion kannst du einen konsistenten Schnappschuss der Datenbank erhalten. Versuche die Dauer einer Transaktion zu minimieren. Es ist verboten Netzwerkabfragen oder andere lang andauernde Operationen in einer Transaktion zu machen. Transaktionen (besonders Schreib-Transaktionen) sind sehr teuer. Du solltest immer versuchen aufeinander folgende Operationen in eine einzelne Transaktion zu vereinen. Transaktionen können entweder synchron oder asynchron sein. In synchronen Transaktionen kannst du nur synchrone Operationen verwenden. In asynchronen Transaktionen sind nur asynchrone Operationen möglich. | | Lesen | Lesen & Schreiben | | --------- | ------------ | ----------------- | | Synchron | `.txnSync()` | `.writeTxnSync()` | | Asynchron | `.txn()` | `.writeTxn()` | ### Lese-Transaktionen Explizite Lese-Transaktionen sind optional, aber sie erlauben es atomare Lesevorgänge durchzuführen und auf einem konsistenten Status der Datenbank innerhalb der Transaktion zu arbeiten. Intern nutzt Isar für alle Lese-Operationen immer Lese-Transaktionen. :::tip Asynchrone Lese-Transaktionen laufen parallel zu anderen Lese- und Schreib-Transaktionen. Ziemlich cool, oder? ::: ### Schreib-Transaktionen Anders als Lese-Operationen müssen Schreib-Operationen in Isar in einer expliziten Transaktion ausgeführt werden. Wenn eine Schreib-Transaktion erfolgreich beendet wird, wird sie automatisch festgesetzt und alle Änderungen werden auf den Datenträger geschrieben. Wenn ein Fehler auftritt, wird die Transaktion abgebrochen und alle Änderungen werden zurückgesetzt. Transaktionen sind „Alles oder Nichts”: Entweder sind alle Schreibvorgänge in der Transaktion erfolgreich oder keine von ihnen findet statt. Somit ist sichergestellt, dass die Daten konsistent sind. :::warning Wenn eine Datenbankoperation fehlschlägt, wird die Transaktion abgeborchen und darf nicht mehr verwendet werden, auch wenn der Fehler in Dart aufgefangen wird. ::: ```dart @collection class Contact { Id? id; String? name; } // GUT await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // SCHLECHT: Bewege die Schleife in die Transaktion for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/de/tutorials/quickstart.md ================================================ --- title: Schnellstart --- # Schnellstart Holla, die Waldfee! Du bist bestimmt hier um mit der coolsten Flutter-Datenbank zu starten... Dieser Schnellstart wird wenig um den heißen Brei herumreden und direkt mit dem Coden beginnen. ## 1. Abhängigkeiten hinzufügen Bevor es losgeht, müssen wir ein paar Pakete zur `pubspec.yaml` hinzufügen. Damit es schneller geht lassen wir pub das für uns erledigen. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Klassen annotieren Annotiere deine Collection-Klassen mit `@collection` und wähle ein `Id`-Feld. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // Für auto-increment kannst du auch id = null zuweisen String? name; int? age; } ``` IDs identifizieren Objekte in einer Collection eindeutig und erlauben es dir, sie später wiederzufinden. ## 3. Code-Generator ausführen Führe den folgenden Befehl aus, um den `build_runner` zu starten: ``` dart run build_runner build ``` Wenn du Flutter verwendest: ``` flutter pub run build_runner build ``` ## 4. Isar-Instanz öffnen Öffne eine neue Isar-Instanz und übergebe alle Collection-Schemata. Optional kannst du einen Instanznamen und ein Verzeichnis angeben. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Schreiben und lesen Wenn deine Instanz geöffnet ist, hast du Zugriff auf die Collections. Alle grundlegenden CRUD-Operationen sind über die `IsarCollection` verfügbar . ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // Einfügen & akualisieren }); final existingUser = await isar.users.get(newUser.id); // Erhalten await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // Löschen }); ``` ## Weitere Ressourcen Du lernst am besten visuell? Schau dir diese Videos an, um mit Isar zu starten:


================================================ FILE: docs/docs/de/watchers.md ================================================ --- title: Watcher --- # Watcher Isar ermöglicht es dir zu Änderungen in der Datenbank zu abbonieren. Du kannst Änderungen in einem Objekt, einer ganzen Collection oder einer Abfrage "beobachten". Watcher erlauben es dir auf Änderungen in der Datenbank effizient zu reagieren. Du kannst z.B. dein UI neuladen, wenn ein Kontakt hinzugefügt wurde, eine Netzwerkabfrage machen, wenn ein Dokument aktualisiert wurde, etc. Ein Watcher wird benachrichtigt, wenn eine Transaktion efolgreich stattfindet, und das Ziel sich wirklich ändert. ## Objekte beobachten Wenn du benachrichtigt werden möchtest, wenn ein spezifisches Objekt erstellt, aktualisiert oder gelöscht wird, solltest du ein Objekt beobachten: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // Ausgabe: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // Ausgabe: User changed: Mark await isar.users.delete(5); // Ausgabe: User changed: null ``` Wie du im eben gezeigten Beispiel sehen kannst, muss das Objekt noch nicht existieren. Der Watcher wird benachrichtigt, wenn es erzeugt wird. Es gibt den zusätzlichen Parameter `fireImmediately`. Wenn du ihn auf `true` gesetzt hast, wird Isar sofort die Werte des aktuellen Objekts in den Stream geben. ### Lazy Beobachtung Vielleicht möchtest du gar nicht den neuen Wert erhalten, sondern nur über die Änderungen informiert werden. Das erspart es Isar die Objekte abrufen zu müssen: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // Ausgabe: User 5 changed ``` ## Collections beobachten Statt ein einzelnes Objekt zu beobachten kannst du auch eine ganze Collection beobachten und benachrichtigt werden, wenn irgendein Objekt hinzugefügt, geändert oder gelöscht wird: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // Ausgabe: A User changed ``` ## Abfragen beobachten Es ist sogar möglich ganze Abfragen zu beobachten. Isar versucht sein Bestes dich nur zu benachrichtigen, wenn das Abfrageergebnis sich wirklich ändert. Du wirst nicht informiert, wenn Links darin resultieren, dass deine Abfrageergebnisse sich ändern. Benutze einen Collection-Watcher, wenn du über Linkänderungen benachrichtigt werden willst. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // Ausgabe: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // Ausgabe: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // keine Ausgabe await isar.users.put(User()..name = 'Antonia'); // Ausgabe: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning Wenn du einen Offset mit Limitierung oder Eindeutigkeitsabfragen benutzt, wird Isar dich auch informieren, wenn Ergebnisse innerhalb der Abfrage, aber außerhalb der Abfragegrenzen stattfinden. ::: Genau wie bei `watchObject()` kannst du `watchLazy()` verwenden, um über Änderungen in den Abfrageergebnissen benachrichtigt zu werden, ohne die Ergebnisse zu erhalten. :::danger Abfragen für jede Änderung erneut ablaufen zu lassen ist sehr ineffizient. Es wäre besser, wenn du stattdessen einen lazy Collection-Watcher verwenden würdest. ::: ================================================ FILE: docs/docs/es/README.md ================================================ --- home: true title: Home heroImage: /isar.svg actions: - text: Empecemos! link: /es/tutorials/quickstart.html type: primary features: - title: 💙 Hecho para Flutter details: Mínima inicialización, fácil de usar, sin configuración, sin repetición. Solo agrega algunas líneas de código para comenzar. - title: 🚀 Altamente escalable details: Almacena cientos de miles de registros en una sola base de datos NoSQL y consúltalos de forma eficiente y asíncrona. - title: 🍭 Múltiples características details: Isar posee una gran cantidad de características para ayudarte a administrar tus datos. Índices compuestos y multi-entrada, modificadores de consultas, soporte para JSON, y mucho más. - title: 🔎 Búsqueda de texto completo details: Isar tiene incorporado un buscador de texto completo. Crea un índice multi-entrada y busca texto de forma fácil. - title: 🧪 Semántica ACID details: Isar es compatible con ACID y maneja las transacciones automáticamente. Retrocede los cambios en caso de error. - title: 💃 Tipeado estático details: Las consultas de Isar son tipeadas estáticamente y verificadas en tiempo de compilación. No hay necesidad de preocuparse por errores en tiempo de ejecución. - title: 📱 Multiplataforma details: Soporte completo para iOS, Android, Desktop, WEB! - title: ⏱ Asíncrona details: Isar incluye operaciones de consulta en paralelo y soporte multi-isolate. - title: 🦄 Código abierto details: Completamente de código abierto y libre para siempre! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/es/crud.md ================================================ --- title: Crear, Leer, Actualizar, Eliminar --- # Crear, Leer, Actualizar, Eliminar (CRUD) Cuando ya has definido tus colecciones, aprende cómo manipularlas! ## Abriendo Isar Antes de hacer nada, necesitamos una instancia Isar. Cada instancia requiere un directorio con permisos de escritura donde el archivo de la base de datos pueda ser almacenado. Si no defines un directorio, Isar encontrará un directorio por defecto apropiado para la plataforma en uso. Provee todos los esquemas que quieras usar con la instancia Isar. Si abres múltiples instancias, aún tienes que proveer todos los esquemas a cada instancia. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [ContactSchema], directory: dir.path, ); ``` Puedes usar la configuración por defecto o proveer algunos de los siguientes parámetros: | Configuración | Descripción | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Abre múltiples instancias con distinto nombre. `"default"` es el nomre usado por defecto. | | `directory` | La ubicación de almacenamiento para esta instancia. Puedes usar una ruta relativa o absoluta. `NSDocumentDirectory` para iOS y `getDataDirectory` para Android son los usados por defecto. No se requiere para web. | | `relaxedDurability` | Relaja la garantía de durabilidad para incrementar el rendimiento de escritura. En caso de falla del sistema (no de la aplicación), es posible perder la última transacción ejecutada. La corrupción de los datos no es posible | | `compactOnLaunch` | Condiciones a verificar cuando la base de datos deba ser compactada cuando se abra la instancia. | | `inspector` | Habilita el inspector para las compilaciones de depuración. Esta opción se ignora para las compilaciones de perfil y entrega. | Si existiera una instancia ya abierta al momento de llamar a `Isar.open()`, ésta retornará la instancia existente independientemente de los parámetros especificados. Ésto es útil para usar Isar en un isolate. :::tip Considera usar el paquete [path_provider](https://pub.dev/packages/path_provider) para obtener una ruta válida en todas las plataformas. ::: La ubicación de almacenamiento del archivo de la base de datos Isar es `directory/name.isar` ## Leyendo la base de datos Usa instancias `IsarCollection` para buscar, consultar y crear nuevos objetos de un tipo dado en Isar. Para los ejemplos siguientes, asumimos que tenemos una colección `Recipe` definida como sigue: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Obtener una colección Todas tus colecciones viven en la instancia Isar. Puedes obtener tu colección Recipes con: ```dart final recipes = isar.recipes; ``` Eso fue fácil! Si no quieres usar los accesores de la colección, puedes usar el método `collection()`: ```dart final recipes = isar.collection(); ``` ### Obtener un objeto (por su id) Todavía no tenemos datos en la colección, pero pretendamos que tenemos así podemos obtener un objeto imaginario dado su id `123` ```dart final recipe = await recipes.get(123); ``` `get()` retorna un `Future` con el objeto o `null` si éste no existe. Por defecto todas las operaciones Isar son asíncronas, y la mayoría de ellas tienen su versión síncrona: ```dart final recipe = recipes.getSync(123); ``` :::warning En tus isolate de UI, por defecto deberías usar los métodos en su versión asíncrona. Debido a que Isar es súper rápido, a menudo es aceptable usar la versión síncrona. ::: Si quieres obtener múltiples objetos de una vez, utiliza `getAll()` o `getAllSync()`: ```dart final recipe = await recipes.getAll([1, 2]); ``` ### Consulta de objectos En lugar de obtener objetos por su id, puedes también consultar una lista objetos que coincidan con ciertas condiciones usando `.where()` y `.filter()`: ```dart final allRecipes = await recipes.where().findAll(); final favouires = await recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ Ver más en: [Consultas](queries) ## Modificando los datos Finalmente es momento de modificar los datos en nuestra colección! Para crear, actualizar o eliminar objectos, usa las respectivas operaciones juntas en una transacción de escritura: ```dart await isar.writeTxn(() async { final recipe = await recipes.get(123) recipe.isFavorite = false; await recipes.put(recipe); // perform update operations await recipes.delete(123); // or delete operations }); ``` ➡️ Ver más en: [Transacciones](transactions) ### Insertar objectos Para almacenar un objeto en Isar, insértalo en una colección. El método `put()` de Isar insertará o actualizará el objecto dependiendo si el mismo ya existe o no en la colección. Si el campo id es `null` o `Isar.autoIncrement`, Isar usará un id auto incrementable. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await recipes.put(pancakes); }) ``` Isar asignará automáticamente el id al objeto si el campo id es no-final. Insertar múltiples objetos de una sola vez es muy fácil: ```dart await isar.writeTxn(() async { await recipes.putAll([pancakes, pizza]); }) ``` ### Actualizar objectos Crear y actualizar objetos funcionan ambos con `collection.put(object)`. Si el id es `null` (o no existe), el object se crea; de otra manera será actualizado. Entonces si queremos quitar los pancakes de los favoritos, podemos hacer los siguiente: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await recipes.put(recipe); }); ``` ### Eliminar objetos Quieres eliminar un objeto en Isar? Usa `collection.delete(id)`. El método delete retorna verdadero si el objeto con el id especificado fue encontrado y eliminado. Por ejemplo, si quieres eliminar el objeto con el id `123`, puedes hacer: ```dart await isar.writeTxn(() async { final success = await recipes.delete(123); print('Recipe deleted: $success'); }); ``` De manera similar a get y put, también existe una operación para eliminar múltiples objetos de una vez que retorna la cantidad de objetos eliminados: ```dart await isar.writeTxn(() async { final count = await recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` Si no conoces los ids de los objetos que quieres eliminar, puedes utilizar una consulta: ```dart await isar.writeTxn(() async { final count = await recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/es/faq.md ================================================ --- title: FAQ --- # Preguntas frecuentes Una colección aleatoria de preguntas frecuentes sobre Isar y bases de datos en Flutter. ### Porqué necesito una base de datos? > Estoy almacenando mis datos es una base datos en mi backend, porqué necesito Isar?. Incluso hoy en día, es muy común no tener conexión a internet si estás en el subterráneo o en un avión o si visitaste a tu abuela, que no tiene WiFi y muy mala señal de celular. No deberías dejar que la mala conexión afectua a tu aplicación! ### Isar versus Hive La respuesta es fácil: Isar [inició como un reemplazo para Hive](https://github.com/hivedb/hive/issues/246) y está en un estado de madurez tal que se recomienda siempre usar Isar en lugar de Hive. ### Cláusulas `where`?! > Porqué **_YO_** tengo que elejir qué índice usar? Existen muchas razones. Muchas base de datos utilizan heurística para elegir el mejor índice para una determinada consulta. La base de datos necesita recolectar datos de uso adicionales (-> overhead) y aún así podría elegir un índice incorrecto. Además crear la consulta es más lento. Nadie conoce tus datos mejor que tú, el desarrollador. Entonces tú puedes elegir el índice óptimo y decidir por ejemplo si quieres usar un índice para consultas u ordenamiento. ### Tengo que usar índices / cláusulas `where`? No! Isar es lo suficientemente rápida si solo quieres confiar en filtros. ### Isar es lo suficientemente rápida? Isar está entre las bases de datos más rápidas para dispositivos móbiles, por lo que debería ser lo suficientemente rápida para las mayoría de los casos de uso. Si tienes problemas de rendimiento, hay posibilidades que estés haciendo algo mal. ### Isar incrementa el tamaño de mi aplicación? Un poco, sí. Isar incrementará el tamaño de descarga de tu aplicaicón alrededor de 1 - 1.5 MB. Isar Web agrega solo algunos KB. ### La documentación es incorrecta / hay un error de ortografía. Oh no, lo siento. Por favor [apunta el problema](https://github.com/isar-community/isar/issues/new/choose) o, mejor aún, un PR para solucionarlo! 💪. ================================================ FILE: docs/docs/es/indexes.md ================================================ --- title: Índices --- # Índices Los índices son la característica más poderosa de Isar. Muchas bases de datos embebidas ofrecen índices "normales" (o nada), pero Isar también tiene índices compuestos y multi-entrada. Entender cómo funcionan los índices es esencial para optimizar el rendimiento de las consultas. Isar te permite elegir qué índice quieres usar y cómo quieres usarlo. Comenzaremos con un inicio rápido sobre qué son los índices. ## Qué son los índices? Cuando una colección no está indexada, el orden de las filas no será identificable por la consulta como optimizada en ninguna forma, y tu consulta tendrá que buscar entonces a través de todos los objectos de forma lineal. En otras palabras, la consulta deberá buscar a través de cada objeto para encontrar los que coincidan con las condiciones. Como puedes imaginarte, eso puede tardar mucho. Buscar a través de cada objeto no es muy eficiente. Por ejemplo, esta colección `Product` está completamente desordenada. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Datos:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | Una consulta que intente buscar todos los productos que cuestan más de $30 tiene que buscar a través de todas las nueve filas. No es un problema para nueve filas, pero podría ser un problema para 100k filas. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` Para mejorar el rendimiento de esta consulta, indexamos la propiedad `price`. Un índice es como una tabla de búsqueda ordenada: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Índices generados:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Ahora, la ejecución de la consulta puede ser considerablemente más rápida. El ejecutor puede saltar directamente a los últimos 3 índices y buscar los objetos correspondientes por su id. ### Ordenando Otra cosa genial: los índices permiten ordenar súper rápido. Las consultas ordenadas son costosas porque la base de datos tiene que cargar todos los resultados en memoria antes de ordenarlos. Incluso si especificaste un offset y un límite, éstos se aplican después de ordenar. Imaginemos que queremos encontrar los cuatro productos más baratos. Podríamos usar la siguiente consulta: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` En este ejemplo, la base de datos tendría que cargar todos los objetos (!), ordenarlos por precio, y retornar los 4 productos con el menor precio. Como puedes imaginar, ésto puede hacerse mucho más eficiente usando el índice anterior. La base de datos toma las cuatro primeras filas del índice y retorna los objetos correspondientes ya que éstos ya están en el orden correcto. Para usar el índice para ordenar, escribiríamos la consulta como sigue: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` La cláusula `where` `.anyX()` le dice a Isar que use un ídice sólo para ordenar. También puedes usar una cláusula `where` como `.priceGreaterThan()` y obtener los resultados ordenados. ## Índices únicos Un índice único asegura que el índice no contiene valores duplicados. Puede consistir en una o múltiples propiedades. Si un índice único tiene una propiedad, los valores en esta propiedad serán únicos. Si el índice único tiene más de una pro[iedad, la combinación de los valores en estas propiedades es única. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Cualquier intento de insertar o actualizar datos en un índice único que provoque un duplicado resultará en un error: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // try to insert user with same username await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Índices con reemplazo A veces no es deseable arrojar un error si una condición de único es violada. En su lugar, podrías querer reemplazar el objeto existente con el nuevo. Ésto se puede lograr estableciendo la propiedad `replace` del índice a `true`. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Ahora cuando querramos insertar un usuario con nombre de usuario existente, Isar reemplazará el usuario existente con el nuevo. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Los índices con reemplazo también generan métodos `putBy()` que permiten actualizar los objetos en lugar de reemplazarlos. El id existente es reusado, **_and links are still populated_**. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user does not exist so this is the same as put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` Como puedes ver, el id del primer usuario insertado es reusado. ## Índices mayúsculas-minúsculas Todos los índices en las propiedades `String` y `List` por defecto distinguen entre mayúsculas y minúsculas. Si quieres que tu índice no haga esta distinción, puedes usar la opción `caseSensitive`: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Tipos de índices Existen diferentes tipos de índices. La mayoría del tiempo, querrás usar un índice tipo `IndexType.value`, pero los índices hash son más eficientes. ### Índice valor El índice valor es el tipo por defecto y el único posible para todas las propiedades que no sean se tipo String o List. Para construir el índice se utilizan los valores de las propiedades. En el caso de las listas, se utilizan sus elementos. De los tres tipos de índices disponibles, es el más flexible como así también el que más espacio utiliza. :::tip Usa `IndexType.value` para primitivas, Strings donde necesites una cláusula `startsWith()`, y listas si quieres buscar por elementos individuales. ::: ### Índice hash Los strings y las listas pueden reducirse para disminuir significativamente el espacio en disco que requiere el índice. La desventaja es que no puede usarse para búsqueda por prefijo (cláusulas `startsWith`). :::tip Usa `IndexType.hash` para strings y listas si no necesitas utilizar cláusulas `startsWith` ni `elementEqualTo`. ::: ### Índice hashElements Las listas de string pueden reducirse como un todo (usando `IndexType.hash`), o los elementos de la lista pueden reducirse individualmente (usando `IndexType.hashElements`), creando un índice multi-entrada con los elementos reducidos. :::tip Usa `IndexType.hashElements` para `List` sin nevesitas aplicar cláusulas `elementEqualTo`. ::: ## Índices compuestos Un índice compuesto es un índice con múltiples propiedades. Isar te permite crear índices compuestos de hasta tres propiedades. Los índices compuestos también son conocidos como índices multi-columna. Probablemente sea mejor comenzar con un ejemplo. Creamos una colleción person y definimos un índice compuesto en las propiedades age y name: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Datos:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Índice generado:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | El índice compuesto generado contiene a todas las personas ordenadas por su edad y su nombre. Los índices compuestos son geniales si necesitas crear consultas eficientes ordenadas por propiedades múltiples. También te pemiten utilizar cláusulas `where` avanzadas: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` La última propiedad del índice compuesto también soporta condiciones como `startsWith()` o `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Índices multi-entrada Si indexas una lista usando `IndexType.value`, Isar automáticamente creará un índice multi-entrada, y cada elemento en la lista será indexado hacia el objeto, Funciona para cualquier tipo de lista. Aplicaciones prácticas del uso de índices multi-entrada incluyen indexar una lista de etiquetas o crear un índice de texto completo. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` divide la cadena en palabras de acuerdo con la especificación [Unicode Annex #29](https://unicode.org/reports/tr29/), por lo tanto funciona correctamente para cualquier idioma. **Data:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Entradas con palabras duplicadas paraecen sólo una vez en el índice. **Índice generado:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | Este índice ahora puede usarse para cláusulas por prefijo (o igualdad) de las palabras individuales de la descripción. :::tip En lugar de guardar las palabaras directamente, considera usar los resultados de un [algoritmo de fonética](https://en.wikipedia.org/wiki/Phonetic_algorithm) como [Soundex](https://es.wikipedia.org/wiki/Soundex). ::: ================================================ FILE: docs/docs/es/limitations.md ================================================ # Limitaciones Como ya sabes, Isar funciona en dispositivos móbiles y de escritorio corriendo en la VM así como en la web. Ambas plataformas son muy diferentes y tienen distintas limitaciones. ## Limitaciones de la VM - Para consultas `where` de prefijo sólo se pueden usar los primeros 1024 bytes - Los objetos pueden ser de 16MB en tamaño como máximo ## Limitaciones de la Web Dado que Isar Web confía en IndexedDB, hay más limitaciones pero apenas son notadas mientras se usa Isar. - No hay soporte para métodos síncronos - Actualmente, los filtros `Isar.splitWords()` y `.matches()` aún no están implementados - Los cambios en los esquemas no son estrechamente verificados como en la VM entonces sé cuidadoso de cumplir con las reglas - Todos los tipos numéricos se almacenan como `double` (el único tipo numérico de js) por lo tanto `@Size32` no tiene efecto - Lo índices se representan de forma diferente entonces los índices hash no usan menos espacio (pero funcionan de la misma manera) - `col.delete()` y `col.deleteAll()` funcionan correctamente pero el valor retornado es incorrecto - `col.clear()` no resetea el valor de auto incrementado - `NaN` no está soportado como valor ================================================ FILE: docs/docs/es/links.md ================================================ --- title: Enlaces --- # Enlaces Los enlaces permiten establecer relaciones entre objetos, como ser el autor de un comentario (User). Con los enlaces de Isar, se pueden modelar relaciones `1:1`, `1:n`, y `n:n`. Usar enlaces es menos ergonómico que usar objetos embebidos y se deberían usar los últimos siempre que sea posible. Piensa en el enlace como una tabla separada que contiene la relación. Es similar a las relaciones de SQL pero tiene una API y características diferentes. ## IsarLink `IsarLink` puede contener uno o nigún objeto relacionado, y puede ser usado para expresar una relación a uno. `IsarLink` tiene una sola propiedad llamada `value` que contiene el objeto enlazado. Los enlaces son perezosos, entonces tienes que decirle explícitamente al `IsarLink` que cargue o guarde el valor `value`. Puedes hacer esto llamando a `linkProperty.load()` y `linkProperty.save()` respectivamente. :::tip La propiedad id de las colecciones de origen y destino de un enlace deberían ser no final. ::: En las plataformas no web, los enlaces se cargan automáticamente cuando los usas por primera vez. Comencemos agregando un IsarLink a la colección: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` Definimos un enlace entre maestros y estudiante. En este ejemplo, cada estudiante puede tener exactamente un maestro. Primero, creamos el maestro y lo asignamos a un estudiante. Tendremos que insertar el maestro y guardar el enlace manualmente. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` Ahora podemos usar el enlace: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Probemos hacer los mismo usando código síncrono. No necesitamos guardar el enlace porque `.putSync()` guarda todos los enlaces automáticamente. Incluso crea el maestro por nosotros. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks Tendría más sentido si un estudiante del ejemplo anterior pudiera tener más de un maestro. Afortunadamente, Isar tiene `IsarLinks`, que pueden tener múltiples objetos relacionados y expresar relaciones `to-many`. `IsarLinks` extiende `Set` y expone todos los métodos que están permitidos para los sets. El comportamiento de `IsarLinks` es similar a `IsarLink` y también es perezoso. Para cargar todos los objetos enlazados se debe llamar a `linkProperty.load()`. Para guardar los cambios, llama a `linkProperty.save()`. Internamente ambos `IsarLink` y `IsarLinks` se representan de la misma forma. Podemos actualizar el `IsarLink` anterior a un `IsarLinks` para asignar múltiples maestros a un estudiante (sin perder datos). ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Esto funciona porque no cambiamos el nombre del enlace (`teacher`), entonces Isar lo recuerda de antes. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlinks Te escuché decir, "Y si necesito expresar relaciones a la inversa?". No te precupes! Te presento a los `backlinks`. Los backlinks son enlaces en la dirección inversa. Cada enlace tiene un backlink implícito. Puedes hacer que esté disponible para tu aplicación anotando un `IsarLink` o `IsarLinks` con `@Backlink()`. Los backlinks no requieren memoria o recursos adicionales; puedes agregarlos libremente, puedes borrarlos o renombrarlos sin perder datos. Queremos saber qué estudiantes tiene un maestro, entonces definimos un backlink: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` Necesitamos especificar el enlace al cual apunta el backlink. Es posible tener múltiples enlaces diferentes entre dos objetos. ## Inicializar enlaces Los `IsarLink` y `IsarLinks` tienen un constructor de cero argumentos,que debería ser usado para asignar la propiedad enlace que se crea el objeto. Es buena práctica hacer que las propiedades de los enlaces sean `final`. Cuando insertas (`put()`) tus objectos por primera vez, el enlace se inicializa con las collecciones origen y destino, y puedes llamar métodos como `load()` y `save()`. Un enlace comienza a serguir los cambios inmediatamente después de su creación, entonces puede agregar o quitar relaciones incluso antes que el enlace sea inicializado. :::danger Es ilegal mover un enlace a otro objeto. ::: ================================================ FILE: docs/docs/es/queries.md ================================================ --- title: Consultas --- # Consultas Las consultas se utilizan para buscar registros que coincidan con ciertas condiciones, por ejemplo: - Buscar todos los contactos favoritos - Buscar contactos con nombre distinto - Borrar todos los contactos que no tengan definido un apellido Dado que las consultas se ejecutan en la base de datos y no en Dart, son realmente rápidas. Si utilizas índices de manera inteligente, puedes mejorar el rendimiento de las consultas todavía más. A continuación, aprederás cómo esribir consultas y cómo lograr que sean lo más rápidas posible. Existen dos métodos diferentes para firltrar tus registros: Filtros y cláusulas where. Comenzaremos hechando un vistazo a cómo funcionan los filtros. ## Filtros Los filtros son fáciles de usar y de entener. Dependiendo del tipo de tus propiedades, existen operaciones de filtrado diferentes con nombres bien definidos. Los filtros funcionan evaluando una expresión para cada objeto en la colección que está siendo filtrada. Si la expresión resuelve en verdadero, Isar incluye el objeto en los resultados. Los filtros no afectan el orden de los resultados. Usaremos el modelo siguiente para los ejemplos: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Query conditions Dependiendo del tipo de campo, tienes diferentes condiciones disponibles. | Condición | Descripción | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.equalTo(value)` | Coincide con valores que son iguales a `value`. | | `.between(lower, upper)` | Conicide con valores que están entre `lower` y `upper`. | | `.greaterThan(bound)` | Coincide con valores que son mayores que `bound`. | | `.lessThan(bound)` | Coincide con valores que son menores que `bound`. Los valores `null` serán incluídos por defecto ya que `null` se considera menor que cualquier otro valor. | | `.isNull()` | Coincide con valores `null`. | | `.isNotNull()` | Coindice con valores que no son `null`. | | `.length()` | Las consultas de tamaño de listas, strings y links filtran objectos basados en el número de elementos en una lista o link. | Asumeindo que la base de datos contiene cuatro zapatos de talle 39, 40, 46 y uno con talle no asignado (`null`). A menos que utilice orden de resultados, los valores serán ordenados por su id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Operadores lógicos Puedes encadenar expresiones usando los operadores lógicos siguientes: | Operador | Descripción | | ---------- | ------------------------------------------------------------------------------------ | | `.and()` | Se evalúa como verdadero si ambas expresiones de izquierda y derecha son verdaderas. | | `.or()` | Se evalúa como verdadero si alguna de las expresiones es verdadera. | | `.xor()` | Se evalúa como verdadero si sólo una de las expresiones es verdadera. | | `.not()` | Invierte (niega) el resultado de la expresión siguiente. | | `.group()` | Agrupa condiciones y permite especificar un orden de evaluación. | Si quieres buscar todos los zapatos de talle 46, puedes hacer lo siguiqnte: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` Si quieres usar más de una condición, puedes combinar múltiples filtros usando los operadores **and** `.and()`, **or** `.or()` y **xor** `.xor()`. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filters are implicitly combined with logical and. .isUnisexEqualTo(true) .findAll(); ``` Esta consulta es equivalente a: `size == 46 && isUnisex == true`. También puedes agrupar condiciones usando `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` Esta consulta es equivalente a `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. Para negar una condición o grupo, usa el operador lógico **not** `.not()`: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` Esta consulta es equivalente a `size != 46 && isUnisex != true`. ### Condiciones sobre Strings Adicionalmente a las condiciones mencionadas anteriormente, los valores de tipo String ofrecen algunas condiciones más. Por ejemplo, los comodines para expresiones regulares permiten mayor flexibilidad en las búsquedas. | Condition | Description | | -------------------- | --------------------------------------------------- | | `.startsWith(value)` | Coincide con strings que comiencen con `value`. | | `.contains(value)` | Coincide con strings que contengan `value`. | | `.endsWith(value)` | Coincide con strings que terminen con `value`. | | `.matches(wildcard)` | Coincide según la evaluación del patrón `wildcard`. | **Sensibilidad a las mayúsculas y minúsculas** Todas las operaciones con strings tienen un parámetro opcional `caseSensitive` para distinguir entre mayúsculas y minúsculas que por defecto está seteado en verdadero. **Comodines:** Una [expresión comodín](https://es.wikipedia.org/wiki/Car%C3%A1cter_comod%C3%ADn) es una cadena de texto (string) que utiliza caracteres normales combinados con dos caracteres especiales comodines: - El comodín `*` coincide con ninguno o más de cualquier caracter. - El comodín `?` coincide con un caracter cualquiera. Por ejemplo, la cadena comodín `"d?g"` coincide con `"dog"`, `"dig"`, y `"dug"`, Pero no con `"ding"`, `"dg"`, o `"a dog"`. ### Modificadores de consultas A veces es necesario construir una consulta basándose en algunas condiciones o para diferentes valores. Isar posee una herramienta muy poderosa para construir consultas condicionales: | Modificador | Descripción | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.optional(cond, qb)` | Extiende la consulta únicamente si la `condición` es verdadera. Esto puede usarse en cualquier lugar en una consulta, por ejemplo para aplicar ordenamiento o límites de manera condicional. | | `.anyOf(list, qb)` | Extiende la consulta para cada valor en `values` y combina las condiciones usando el operador lógico **or**. | | `.allOf(list, qb)` | Extiende la consulta para cada valor en `values` y combina las condiciones usando el operador lógico **and**. | En este ejemplo, construiremos un método que puede buscar zapatos con un filtro opcional: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // only apply filter if sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` Si quieres buscar zapatos entre múltiples talles, puedes usar una consulta convencional o usar el modificador `anyOf()`: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` Los modificadores de consultas son especialmente útiles si quieres construir consultas dinámicas. ### Listas Incluso se puede construir consultas sobre listas: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` Puedes consultar basándote en la longitud de la lista: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` Éstos son equivalenets al código Dart `tweets.where((t) => t.hashtags.isEmpty);` y `tweets.where((t) => t.hashtags.length > 5);`. También puedes consultar basándote en los elementos de la lista: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` Esto es equivalente al código Dart `tweets.where((t) => t.hashtags.contains('flutter'));`. ### Objetos embebidos Los objetos embebidos son una de las funcionalidades más útiles de Isar. Se pueden consultar de manera muy eficiente usando las mismas condiciones disponibles para los objetos raíz. Asumiendo que tenemos el siguiente modelo: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` Necesitamos consultar todos los autos que sean de la marca `"BMW"` y del país `"Germany"`. Podemos hacerlo con la siguiente consulta: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Siempre trata de agrupar las consultas anidadas. La consulta anterior es más eficiente que ésta siguiente auque el resultado es el mismo: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Enlaces Si tus modelos contienen [links or backlinks](links) puedes filtrar tus consultas basándote en el objeto enlazado o la cantidad de objetos enlazados. :::warning Ten en cuenta que las consultas sobre enlaces pueden ser costosas ya que Isar necesita buscar en los objetos enlazados. Considera usar objetos embebidos en su lugar. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Podemos buscar todos los estudiantes que tienen un maestro de Matemáticas o de Inglés: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Los filtros sobre enlaces se evalúan en verdadero si al menos unos de los objetos enlazados coincide con la condición. Busquemos todos los estudiantes que no tienen maestro: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` o: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Cláusulas `where` Las cláusulas `where` son una herramienta muy poderosa, pero puede ser algo desafiante lograr usarlas de la manera correcta. En contraste con los filtros, las cláusulas `where` usan los índices que definiste en el esquema para verificar las condiciones de la consulta. Consultar un índice es mucho más rápido que filtrar cada registro individualmente. ➡️ Ver más en: [Índices](indexes) :::tip Como regla básica, deberías intentar reducir la cantidad de registros lo mayor posible usando cláusulas `where` y luego hacer el filtrado restante usando filtros. ::: Sólo puedes combinar cláusulas `where` usando operaciones lógicas **or**. En otras palabras, puedes sumar múltiples cláusulas `where`, pero no puedes consultar la intersección de múltiples de ellas. Agreguemos ídices a nuestra colección shoe: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` Tenemos dos índices. El índice en `size` nos permite usar cláusulas `where` como `.sizeEqualTo()`. El índice compuesto en `isUnisex` nos permite usar cláusulas como `isUnisexSizeEqualTo()`. Pero también `isUnisexEqualTo()` porque siempre puedes usar cualquier prefijo de un índice. Ahora podemos reescribir la consulta anterior que busca zapatos unisex de talle 46 usando el índice compuesto. Esta consulta será mucho más rápida que la anterior: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Las cláusulas `where` tienen dos superpoderes adicionales: Te brindad ordenado "libre" y una súper rápida operación `distinct`. ### Combinando cláusulas `where` y filtros Recuerdas la consulta `shoes.filter()`? Es en realidad un atajo para `shoes.where().filter()`. Puedes (y deberías) combinar cláusulas `where` y filtros en la misma consulta para usar los beneficios de ambos: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` La cláusula `where` se aplica primero para reducir el número de objetos a ser filtrados. Luego se aplica el filtro a los objetos restantes. ## Ordenando Puedes definir cómo se deben ordenar los resultados cuando se ejecuta una consulta usando los métodos `.sortBy()`, `.sortByDesc()`, `.thenBy()` y `.thenByDesc()`. Para buscar todos los zapatos ordenados por nombre de modelo en orden ascendente sin usar un índice: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Ordenar un gran número de resultados puede ser costoso, especialmente dado que el ordenamiento sucede antes que el salto y los límites. Los métodos de ordenamiento anteriores nunca hacen uso de índices. Afortunadamente, también podemos hacer ordenamiento usando cláusulas `where` y hacer que nuestra consulta sea rápida como un rayo aún si tenemos que ordenar un millón de objetos. ### Ordenando con cláusulas `where` Si usas una sola cláusula `where` en tu consulta, los resultados ya están ordenados por su índice. Eso ya es mucho! Supongamos que tenemos zapatos en talle `[43, 39, 48, 40, 42, 45]` y queremos buscar todos los zapatos de talle mayor a `42` y además los queremos ordenados por talle: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // also sorts the results by size .findAll(); // -> [43, 45, 48] ``` Como puedes ver, el resultado está ordenado por el índice `size`. Si quieres invertir el orden, puedes establecer `sort` a `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` Es posible que no quieras usa la cláusula `where` pero sí beneficiarte del ordenamiento implícito. Puedes usar la cláusula `any`: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` Si usas un índice compuesto, los resultados son ordenados según todos los campos en el índice. :::tip Si necesitas ordenar tus resultados, considera usar índices para eso. Especialmente si trabajas con `offset()` y `limit()`. ::: A veces no es posible o no es útil usar índices para ordenar. En esos casos, usa índices para reducir el número de resultados lo más posible. ## Valores únicos Para retornar sólo entradas con valores únicos, utiliza el predicado `distinct`. Por ejemplo, para saber cuántos modelos diferentes de zapatos tienes en base de datos Isar: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` También puedes encadenar múltiples condiciones `distinct` para buscar todos los zapatos con distinta combinación de modelo-talle: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Sólo se retorna el primer valor de cada combinación distinta. Puedes usar cláusulas `where` y operaciones de ordenamiento para controlarlos. ### Cláusula `where` `distinct` Si tienes un ídice que no es único, podrías querer obtener todos sus valores distintos. Podrías usar la operación `distinctBy` de la sección anterior, pero se ejecuta después del ordenamiento y filtrado, por lo que hay algunas operaciones adicionales. Si solo usas una sola cláusula `where`, puedes por el contrario confiar en el índice para ejecutar la operación `distinct`. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip En teoría, podrías incluso usar múltiples cláusulas `where` para ordenamiento y distintos. La única restricción es que aquellas cláusulas `where` no se superpongan y usen el mismo índice. Para un correcto ordenamiento, también tienen que ser aplicadas en el orden de ordenamiento. Debes ser muy cuidadoso si utilizas estos métodos! ::: ## Offset y Límite A menudo es buena idea limitar el número de resultados de una consulta para vistas de listas perezosas. Puedes hacer esto estableciendo un `limit()`: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` Estableciendo un `offset()` puedes también paginar los resultados de su consulta. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Dado que el instanciado de objetos Dart es a menudo la parte más costosa cuando se ejecuta una consulta, es una buena idea cargar sólo los objectos que necesitas. ## Orden de ejecución Isar ejecuta las consultas siempre en el mismo orden: 1. Atravesar índices primarios o secundarios para buscar objetos (aplicar las cláusulas `where`) 2. Filtrar objetos 3. Ordenar resultados 4. Aplicar operaciones `distinct` 5. Aplicar `offset` y `limit` a los resultados 6. Retornar los resultados ## Operaciones de consulta En los ejemplos anteriores, usamos `.findAll()` para recuperar todas las coincidencias de objectos. Sin embargo, hay más operaciones disponibles: | Operaciones | Descripción | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `.findFirst()` | Recupera el primer objeto coincidente con la consulta o `null` si no se encontró ninguna. | | `.findAll()` | Recupera todos los objetos para la consulta. | | `.count()` | Cuenta cuántos objetos coinciden con la consulta. | | `.deleteFirst()` | Elimina de la colección el primer objeto coincidente con la consulta. | | `.deleteAll()` | Elimina de la colección todos los objetos coincidentes con la consulta. | | `.build()` | Compila la consulta para ser usada luego. Esto ahorra el costo de contruir una consulta si tienes que ejecutarla muchas veces. | ## Consulta de propiedades Si estás interesado solamente en los valores de un propiedad simple, puedes usar consulta de propiedades. Simplemente construye una consulta regular y selecciona una propiedad: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` Usar una sola propiedad ahora tiempo durante el deserializado. Las consultas de propiedades también funcionan para los objetos embebidos y las listas. ## Agregación Isar soporta el agregado de los valores de una consulta de propiedad. Las siguientes operaciones de agregación están disponibles: | Operación | Descripción | | ------------ | --------------------------------------------------------------------- | | `.min()` | Busca el valor mínimo o `null` si ninguno coincide. | | `.max()` | Busca el valor máximo o `null` si ninguno coincide. | | `.sum()` | Suma todos los valores. | | `.average()` | Calcula el promedio de todos los valores o `NaN` si ninguno coincide. | Usar agregaciones es mucho más rápido que buscar todos los valores y realizar las operaciones de forma manual. ## Consultas dinámicas :::danger Esta sección no debería ser relevante. El uso de consultas dinámicas está desaconsejado a menos que sea abosulamente necesario (lo cual es poco probable). ::: Todos los ejemplos anteriores usan el QueryBuilder y los métodos estáticos generados. Podrías querer crear una consulta dinámica o un lenguaje de consultas personalizado (como el Isar Inspector). En ese caso, puedes usar el método `buildQuery()`: | Parámetro | Descripción | | --------------- | -------------------------------------------------------------------------------------------------- | | `whereClauses` | La cláusula `where` de la consulta. | | `whereDistinct` | Si las consultas deben retornan sólo valores distintos (solo útil para consultas `where` simples). | | `whereSort` | El orden de atravesado de la cláusula `where` (solo útil para consultas `where` simples). | | `filter` | El filtro a aplicar al resultado. | | `sortBy` | Una lista de propiedades para definir el orden del resultado. | | `distinctBy` | Una lista de propiedades para aplicar `distinct`. | | `offset` | El offset de los resultados. | | `limit` | El número máximo de resultados a retornar. | | `property` | Si no es null, sólo se retornan los valores de ésta propiedad. | Creemos una consulta dinámica: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` La siguiente consulta es equivalente: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/es/recipes/data_migration.md ================================================ --- title: Migración de datos --- # Migración de datos Isar migra automáticamente tus esquemas de la base de datos si agregas o quitas colecciones, campos o índices. Probablemente quieras migrar también tus datos. Isar no ofrece una solución incluída porque impondría restricciones arbitrarias a la migración. Es sencillo implementar una lógica de migración que se adecúe a tus necesidades. En este ejemplo usaremos una versión simple de la base de datos completa. Utilizamos `SharedPreferences` para almacenar la versión actual y compararla con la versión a la cual queremos migrar. Si la versión no coincide, migramos los datos y actualizamos la versión. :::tip También podrías darle a cada colección su propia versión y migrarlas individualmente. ::: Imagina que tenemos una colección de usuarios con un campo de cumpleaños. En la versión 2 de nuetra app, necesitamos agregar un campo adicional para el año de nacimiento para consultar usuarios por edad. Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` El problema es que el modelo existente para los usuarios tendrá un campo vacío `birthYear` porque no existía en la versión 1. Necesitamos migrar los datos para establecer el campo `birthYear`. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // If the version is not set (new installation) or already 2, we do not need to migrate return; default: throw Exception('Unknown version: $currentVersion'); } // Update version await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // We paginate through the users to avoid loading all users into memory at once for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // We don't need to update anything since the birthYear getter is used await isar.users.putAll(users); }); } } ``` :::warning Si tienes que migrar muchos datos, considera utilizar un isolate en segundo plano para prevenir efectos no deseados en la UI. ::: ================================================ FILE: docs/docs/es/recipes/full_text_search.md ================================================ --- title: Full-text search --- # Full-text search Full-text search es una manera poderosa de buscar texto en la base de datos. Ya deberías estar familiarizado con la forma en que funcionan los [índices](/es/indexes), pero vayamos sobre lo básico. Un índice funciona como una tabla de lookup, permitiéndole al motor de consulta encontrar rápidamente registros con un cierto valor. Por ejemplo, si tienes un campo `title` en tu objeto, puedes crear un índice en ese campo para hacer más rápida la búsqueda de objetos con un título determinado. ## Porqué full-text search es útil? Puedes buscar texto fácilmente usando filtros. Existen algunas operaciones sobre string como por ejemplo `.startsWith()`, `.contains()` y `.matches()`. El problema con los filtros es que su tiempo de ejecución es de `O(n)` donde `n` es la cantidad de registros en la colección. Operaciones sobre strings como `.matches()` son especialmente costosas. :::tip Full-text search es mucho más rápido que los filtros, pero los índices tienen cietas limitaciones. En este receta vamos a explorar cómo sobrepasar esas limitaciones. ::: ## Exemplo básico La idea es siempre la misma: En lugar de indexar el texto completo, indexamos las palabras en el texto así podemos buscar por ellos individualmente. Creamos el índice full-text más básico: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` Ahora podemos buscar mensajes que contengan palabras específicas en el contenido: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` Esta consulta es súper rápida, pero existen algunos problemas: 1. Sólo podemos buscar palabras completas 2. No consideramos puntuación 3. No soportamos otros caracteres de separación de palabras ## Separando el texto de la forma correcta Intentemos mejorar el ejemplo anterior. Podemos intentar desarrollar una expresión regular complicada para correjir la separación de las palabras, pero sería lento e incorrecto para casos de borde. El [Anexo Unicode #29](https://unicode.org/reports/tr29/) define cómo separar palabras correctamente para la mayoría de los idiomas. Es algo complicado, pero afortunadamente Isar hace el trabajo pesado por nosotros: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## Quiero más control Pan comido! Podemos cambiar nuestro índice para soportar también coincidencia por prefijo y por mayúsculas y minúsculas: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` De manera predeterminada, Isar almacenará las palabras como valores hash que es rápido y eficiente en cuanto a espacio. Pero los hashes nos pueden usarse para coincidencia por prefijo. Usando `IndexType.value`, podemos cambiar el índice para usar las palabras directamente. Ésto nos ofrece la cláusula `.titleWordsAnyStartsWith()`: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## También necesito `.endsWith()` Por supuesto! Usaremos un truco para conseguir la coincidencia `.endsWith()`: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` No olvides invertir la terminación que quieres buscar: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Algoritmos de derivación Desafortunadamente, los índices no soportan coincidencia por `.contains()` (ésto también es cierto para otras bases de datos). Pero existen algunas alternativas que vale la pena explorar. La elección depende fuertemente de su uso. Un ejemplo es indexar la raíz de la palabra en de la misma completa. Un algoritmo de derivación (stemming algorithm) es un proceso de normalización linguística en el cual las diferentes formas de una palabra se reducen a una forma común: ``` connection connections connective ---> connect connected connecting ``` Algoritmos populares son el [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) y el [Snowball stemming algorithms](https://snowballstem.org/algorithms/). Existen también formas avanzadas como [lematización](https://es.wikipedia.org/wiki/Lematizaci%C3%B3n). ## Algoritmos de fonética Un [algoritmo de fonética](https://en.wikipedia.org/wiki/Phonetic_algorithm) es un algoritmo para indexar palabras de acuerdo a su pronunciación. Es decir, te permite encontrar palabras que "suenan" parecido a las que estás buscando. :::warning La mayoría de los algoritmos de fonética sólo soportan un solo idioma. ::: ### Soundex [Soundex](https://es.wikipedia.org/wiki/Soundex) es un algoritmo de fonética para indexar nombres por sonido, como se pronuncian en inglés. El objetivo es que los homófonos se codifiquen con la misma representación para que produzcan coincidencias a pesar de alguna menor diferencia de tipeo. Es un algoritmo directo, y existen múltiples versiones mejoradas. Usando este algoritmo, ambos `"Robert"` y `"Rupert"` retornan la cadena `"R163"` mientras que `"Rubin"` entrega `"R150"`. `"Ashcraft"` y `"Ashcroft"` ambos entregan `"A261"`. ### Metáfono doble El algoritmo de codificado fonético [Metáfono doble](https://es.wikipedia.org/wiki/Metaphone) es la segunda generación de este tipo de algoritmos. Contiene varias mejoras fundamentales de diseño sobre el algoritmo de metáfono original. Doble Metáfono da cuenta de varias irregularidades en inglés de eslavo, alemán, celta, griego, francés, italiano, español, chino, y otros orígenes. ================================================ FILE: docs/docs/es/recipes/multi_isolate.md ================================================ --- title: Uso de múltiples Isolates --- # Uso de múltiples Isolates En lugar de tareas, todo el código de Dart corre dentro de isolates. Cada isolate tiene su propia cabecera de memoria, asegurando que ningún estado de un isolate es accesible desde otro isolate. Isar puede accederse desde múltiples isolates al mismo tiempo, e incluso los observadores funcionan entre isolates. En esta receta, veremos cómo utilizar Isar en un entorno con múltiples isolates. ## Cuándo usar múltiples isolates Las transacciones Isar se ejecutan en paralelo incluso dentro de un mismo isolate. En algunos casos, es también beneficioso acceder a Isar desde múltiples isolates. El motivo es que Isar tarda algo de tiempo codificando y decodificando datos desde y hacia objetos Dart. Puedes pensarlo como codificando y decodificando JSON (sólo que más eficiente). Estas operaciones corren dentro del isolate en el cual se acceden los datos y naturalmente bloquean otro código en el isolate. En otras palabras: Isar ejecuta parte del trabajo dentro de tu isolate Dart. Si sólo necesitas leer y escribir algunos cientos de objetos de una vez, hacerlo dentro del mismo isolate que la UI no es un problema. Pero para transacciones muy grandes o si el isolate de la UI ya está bastante ocupado, deberías considerar usar un isolate separado. ## Ejemplo Lo primero que debemos hacer es abrir Isar en el nuevo isolate. Dado que la instancia de Isar ya está abierta en el isolate principal, `Isar.open()` retornará la misma instancia. :::warning Asegúrate de proveer los mismos esquemas que en el isolate principal. De lo contrario, obtendrás un error. ::: `compute()` inicia un nuevo isolate en Flutter y ejecuta la función dada en él. ```dart void main() { // Open Isar in the UI isolate final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // listen to changes in the database isar.messages.watchLazy(() { print('omg the messages changed!'); }); // start a new isolate and create 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // after some time: // > omg the messages changed! // > isolate finished } // function that will be executed in the new isolate Future createDummyMessages(int count) async { // we don't need the path here because the instance is already open final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // we use a synchronous transactions in isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` Existen algunos aspectos interesantes a notar en este ejemplo: - `isar.messages.watchLazy()` se llama en el isolate de la UI y es notificado de los cambios desde otro isolate. - Las instancias son referenciadas por nombre. El nombre por defecto es `default`, pero en este ejemplo, utilizamos `myInstance`. - Utilizamos una transacción síncrona para crear los mensajes. Bloquear el nuevo isolate no es un problema, y las transacciones síncronas son algo más rápidas. ================================================ FILE: docs/docs/es/recipes/string_ids.md ================================================ --- title: Ids de texto --- # Ids de texto Esta es uno de los pedidos más frecuentes para Isar, por eso les dejamos este tutorial sobre como usar ids de texto. Isar no soporta ids de texto de forma nativa, y existe una buena razón para eso: los ids de enteros son mucho más eficientes y rápidos. Especialmente para enlaces, el gasto de un id de texto es demasiado significativo. Es probable que necesites almacenar datos externos que usen UUIDs o otro id no entero. Se recomienda almacenar el id de texto como una propiedad del objeto y usar una función rápida de hash para generar un entero de 64 bits que pueda ser usado como Id. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` De esta maneras obtienes lo mejor de dos mundos: Ids enteros eficientes para los enlaces y la posibilidad de usar ids de texto. ## Función rápida de hash Idealmente, tu función de hash debería ser rápida y de alta calidad (sin colisiones). La siguiente es una implementación recomendada: ```dart /// FNV-1a 64bit hash algorithm optimized for Dart Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` Si eliges una función de hash diferente, asegúrate de que retorne un entero de 64-bit. Evita usar funciones hash criptográficas porque son mucho más lentas. :::warning Evita usar `string.hashCode` porque no está garantizada la estabilidad entre distintas plataformas y versiones de Dart. ::: ================================================ FILE: docs/docs/es/schema.md ================================================ --- title: Esquema --- # Esquema Cuando usas Isar para almacenar los datos de tu aplicación, estás tratando con colecciones. Una colección es como una tabla en la base de datos Isar asociada y sólo puede contener un tipo de objeto Dart. Cada objeto de la colección representa una línea de datos en la tabla correspondiente. La definición de una colección es llamada "esquema" ("schema" en inglés). El generador Isar hará el trabajo pesado por ti y generará la mayoría del código que necesitas para usar tu colección. ## Anatomía de una colección Cada colección Isar se define anotando una clase con `@collection` o `@Collection()`. Una colección Isar incluye campos para cada columna en la tabla correspondiente en la base de datos, incluyendo uno que corresponde a la clave primaria. El código siguiente es un ejemplo de una colección simple que define una table `User` con columnas para ID, nombre, y apellido: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip Para almacenar un campo, Isar debe tener acceso al mismo. Puedes asegurarte que Isar tiene acceso a un campo haciéndolo público o proporcionando métodos getter y setter. ::: Existen algunos parámetros opcionales para personalizar la colección: | Configuración | Descripción | | ------------- | --------------------------------------------------------------------------------------------------------------------------- | | `inheritance` | Controla si los campos de la clase padre y mixins serán almacenados en Isar. Habilitado por defecto. | | `accessor` | Permite renombrar el punto de acceso por defecto de la colección (por ejemplo `isar.contacts` para la colección `Contact`). | | `ignore` | Permite ignorar ciertas propiedades. Éstas también son respetadas para las super clases. | ### Isar Id Cada clase que defina una colección Isar, debe definir una propiedad id y debe ser de tipo `Id` identificando inequívocamente un objecto. `Id` es simplemente un alias para `int` que le permite al generador Isar reconocer la propiedad id. Isar indexa automáticamente los campos id, que permite obtener y modificar objectos de manera eficiente basándose en su id. Puedes establecer tus propios ids o pedir a Isar que asigne un id auto-incrementable. Si el campo `id` es `null` y no `final`, Isar asignará un id auto-incrementable. Si quieres un id auto-incrementable y no-null, puedes usar `Isar.autoIncrement` en lugar de `null`. :::tip Los ids auto incrementables no se reusan cuando un objeto es eliminado. La única manera de reiniciar los ids auto incrementables es borrando la base de datos. ::: ### Renombrando colecciones y campos Por defecto, Isar usa el nombre de la clase como nombre de la colección. De manera similar, Isar usa los nombres de los campos como nombres de las columnas en la base de datos. Si quieres que una colección o campo tenga un nombre diferente, agrega la anotación `@Name`. El ejemplo siguiente demuestra el uso de nombres personalizados para colecciones y campos: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Específicamente si quieres renombrar objectos Dart o clases que ya están almacenados en la base de datos, deberías considerar usar la anotación `@Name`. De otra manera, la base de datos eliminiará y creará nuevamente el campo o la colección. ### Ignorando campos Isar almacena todos los campos públicos de una clase que defina una colección. Anotando una propiedad o getter con `@ignore`, puedes excluir dicha propiedad del almacenamiento, como se muestra en el siguiente extracto de código: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` En los casos donde una colección hereda los campos de una colección padre, es generalmente más fácil usar la propiedad `ignore` de la anotación `@Collection`: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` Si una colección contiene un campo con un tipo de dato no soportado por Isar, éste campo debe ser ignorado. :::warning Ten en cuenta que no es una buena práctica guardar información en objectos Isar que no serán almacenados. ::: ## Tipos de datos soportados Isar soporta los siguientes tipos de datos: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` Adicionalmente, Isar soporta objetos embebidos y enums. Explicaremos éstos más adelante. ## byte, short, float Para muchos casos de uso, no es necesario el rango completo de 64-bits de un entero o punto flotante (int o double). Isar contiene soporte para tipos adicionales que te permiten ahorrar espacio y memoria cuando se almacenan número más pequeños. | Tipo | Tamaño en bytes | Rango | | ---------- | --------------- | ------------------------------------------------------ | | **byte** | 1 | 0 a 255 | | **short** | 4 | -2,147,483,647 a 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 a 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 a 3.4e38 | | **double** | 8 | -1.7e308 a 1.7e308 | Los tipos numéricos adicionales con sólo aliases de los tipos de datos nativos de Dart, por lo que usar `short`, por ejemplo, funciona de la misma manera que usando `int`. El siguiente es un ejemplo de una colección que contiene todos los tipos de datos vistos anteriormente: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` Todos los tipos numéricos también pueden ser usados en listas. Para almacenar bytes, deberías usar `List`. ## Tipos nulables Entender cómo funciona la nulabilidad en Isar en esencial: los tipos numéricos **NO** tienen una representación `null` dedicada. Por el contrario, un valor específico es usado: | Tipo | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, y `List` tienen una representación `null` por separado. Este comportamiento habilita mejoras en el rendimiento, y te permite cambiar libremente la nulabilidad de tus campos sin requerir una migración o código especial para lidiar con valores `null`. :::warning El tipo `byte` no soporta valores null. ::: ## DateTime Isar no almacena información de zonas horarias en tus campos DateTime. En su lugar, convierte `DateTime`s a UTC antes de almacenarlos. Isar devuelve todas las fechas en hora local. Los `DateTime`s se almacenan con presición de microsegundos. En navegadores web, sólo se soporta presición de milisegundos debido a una limitación de Javascript. ## Enum Isar permite almacenar y usar enums como cualquier otro tipo de dato Isar. Sin embargo, tendrás que decidir cómo Isar debería representar el enum en el disco. Isar soporta cuatro estrategias diferentes: | EnumType | Descripción | | ----------- | ------------------------------------------------------------------------------------------------ | | `ordinal` | El índice del emun se almacena como `byte`. Esto es muy eficiente pero no permite enums nulables | | `ordinal32` | El índice del enum se almacena como `short` (entero de 4 bytes). | | `name` | El nombre del enum se almacena como `String`. | | `value` | Para recuperar el valor del enum se utiliza una propiedad personalizada. | :::warning `ordinal` y `ordinal32` dependen del orden de los valores en el enum. Si cambias el orden, bases de datos existentes retornarán valores incorrectos. ::: Veamos un ejemplo para cada estrategia. ```dart @collection class EnumCollection { Id? id; @enumerated // same as EnumType.ordinal late TestEnum byteIndex; // cannot be nullable @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // cannot be nullable @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Por supuesto, los Enums pueden usarse también en listas. ## Objetos Embebidos Con frecuencia es útil tener objetos anidados en tus colecciones. No hay límite en cuanto a la profundidad que un objeto anidado puede tener. Sin embargo, es necesario tener en cuenta que actualizar un objeto anidado requerirá escribir el árbol completo del objeto en la base de datos. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Los objectos embebidos pueden ser nulables y extender otros objectos. El único requerimiento es que sean anotados con `@embedded` y que tengan un constructor predeterminado sin parámetros requeridos. ================================================ FILE: docs/docs/es/transactions.md ================================================ --- title: Transacciones --- # Transacciones En Isar, las transacciones combinan múltiples operaciones en una sola unidad de trabajo. La mayoría de las interacciones con Isar utilizan transacciones de forma implícita. El acceso de lectura y escritura en Isar cumple está conforme con [ACID](https://es.wikipedia.org/wiki/ACID). Las transacciones se retroceden automáticamente en caso de error. ## Transacciones explícitas En una transacción explícita, obtienes una instantánea consistente de la base de datos. Intenta minimizar la duración de las transacciones. En una transacción stá prohibido hacer llamadas de red u otras operaciones de largo procesamiento. Las transacciones (especialmente las de escritura) tienen un costo, y siempre deberías agrupar operaciones sucesivas en una sola transacción. Las transacciones puede ser tanto síncronas como asíncronas. En las transacciones síncronas, sólo puedes utilizar operaciones síncronas. En las transacciones asíncronas, sólo operaciones asíncronas. | | Lectura | Lectura y Escritura | | ---------- | ------------ | ------------------- | | Síncronas | `.txnSync()` | `.writeTxnSync()` | | Asíncronas | `.txn()` | `.writeTxn()` | ### Transacciones de lectura Las transacciones explícitas de lectura son opcionales, pero te permiten hacer lecturas atómicas y confiar en que el estado de la base de datos dentro de la transacción será consistente. Internamente Isar utiliza transacciones de lectura implícitas para todas las operaciones de lectura. :::tip Las transacciones de lectura asíncronas se ejecutan en paralelo con otras transacciones de lectura y escritura. Genial verdad? ::: ### Transacciones de escritura A diferencia de las operaciones de lectura, las operaciones de escritura en Isar deben ser agrupadas en una transacción explícita. Cuando una transacción de escritura finaliza exitosamente, automáticamente es aplicada, y todos los cambios se escriben al disco. En case de error, se aborta y los cambios retroceden. Las transacciones son "todo o nada": o todas las escrituras de la transacción suceden, o ninguna de ellas tiene efecto, para garantizar la consitencia de los datos. :::warning Cuando una operación de la base de datos falla, la transacción se aborta y ya no debe ser utilizada. Incluso si capturaste el error en Dart. ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: move loop inside transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/es/tutorials/quickstart.md ================================================ --- title: Inicio rápido --- # Inicio rápido Increíble!, estás aquí! Vamos a empezar a usar la base de datos más genial que existe para Flutter... Vamos a ser cortos en palabras para ir inmediatamente al código en esta guía de inicio rápido. ## 1. Agrega las dependencias Antes de empezar la parte divertida, necesitamos agregar algunos paquetes al `pubspec.yaml`. Podemos usar pub para hacer el trabajo pesado por nosotros. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Anota las clases Anota tus clases de colecciones con `@collection` y elige un campo `Id`. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // you can also use id = null to auto increment String? name; int? age; } ``` Los Ids identifican inequívocamente los objetos en una colección y te permiten luego buscarlos nuevamente. ## 3. Ejecuta el generador de código Ejecuta el siguiente comando para iniciar el `build_runner`: ``` dart run build_runner build ``` Si estás usando Flutter, puedes usar el siguiente: ``` flutter pub run build_runner build ``` ## 4. Abre una instancia Isar Abre una nueva instalcia Isar y pásale todos los esquemas de tu colección. Opcionalmente puedes especificar un nombre para la instancia y un directorio. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Lee y escribe Una vez que tu base de datos está abierta, puedes comenzar a usar tus colecciones. Todas las operaciones CRUD básicas están disponibles a través del `IsarCollection`. ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // insert & update }); final existingUser = await isar.users.get(newUser.id); // get await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // delete }); ``` ## Otros recursos Gustas de aprender de manera visual? Dale un vistazo a estos videos para empezar con Isar (Advertencia, material en Inglés):


================================================ FILE: docs/docs/es/watchers.md ================================================ --- title: Watchers --- # Watchers Isar te permite suscribirte a los cambios en la base de datos. Puedes "observar" los cambios en un objeto específico, una colección entera, o una consulta. Los watchers te permiten reaccionar a los cambios en la base de datos de manera eficiente. Puedes por ejemplo refrescar la interfaz de usuario cuando se agrega un contacto, enviar una consulta de red cuando un documento se actualiza, etc. Un watcher es notificado después que una transacción finaliza exitosamente y el objeto realmente cambia. ## Observando objetos Si quieres ser notificado cuando un objeto específico se crea, actualiza o elimina, debes "observar" un objeto: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` Como puedes ver en el ejemplo anterior, el objeto no necesita existir aún. El watcher será notificado cuando se crea. Existe un parámetro adicional `fireImmediately`. Si lo seteas en `true`, Isar agregará inmediatamente el valor actual del objeto al stream. ### Lazy watching Tal vez no necesitas recibir el nuevo valor pero sólo ser notificado sobre el cambio. Esto evita que Isar tenga get obtener el objeto: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## Observando collections En lugar de observar un solo objeto, puedes hacerlo con una colección completa y ser notificado cuando cualquier objeto se agrega, actualiza o elimina: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## Observando consultas Incluso es posible observar consultas. Isar lo hace mejor incluso al notificarte sólo si el resultado de la consulta en realidad cambia. No serás notificado de los cambios provocados por un enlace. Observa una colección si quieres ser notificado acerca de los cambios en enlaces. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning Si en tus consultas usas offset y límite o distinct, Isar incluso te notificará cuando los objetos coinciden con el filtro pero caen fuera de la consulta. ::: Al igual que `watchObject()`, puedes usar `watchLazy()` para ser notificado cuando el resultado de la consulta cambia pero sin obtenerlos. :::danger Ejecutar consultas repetidamente para cada cambio es muy ineficiente. Sería mejor si usaras un watcher perezoso sobre la colección. ::: ================================================ FILE: docs/docs/faq.md ================================================ --- title: FAQ --- # Frequently Asked Questions A random collection of frequently asked questions about Isar and Flutter databases. ### Why do I need a database? > I store my data in a backend database, why do I need Isar?. Even today, it is very common to have no data connection if you are in a subway or a plane or if you visit your grandma, who has no Wi-Fi and a very bad cell signal. You shouldn't let bad connection cripple your app! ### Isar vs Hive The answer is easy: Isar was [started as a replacement for Hive](https://github.com/hivedb/hive/issues/246) and is now at a state where I recommend always using Isar over Hive. ### Where clauses?! > Why do **_I_** have to choose which index to use? There are multiple reasons. Many databases use heuristics to choose the best index for a given query. The database needs to collect additional usage data (-> overhead) and might still choose the wrong index. It also makes creating a query slower. Nobody knows your data better than you, the developer. So you can choose the optimal index and decide for example whether you want to use an index for querying or sorting. ### Do I have to use indexes / where clauses? Nope! Isar is most likely fast enough if you only rely on filters. ### Is Isar fast enough? Isar is among the fastest databases for mobile, so it should be fast enough for most use cases. If you run into performance issues, chances are that you are doing something wrong. ### Does Isar increase the size of my app? A little bit, yes. Isar will increase the download size of your app by about 1 - 1.5 MB. Isar Web adds only a few KB. ### The docs are incorrect / there is a typo. Oh no, sorry. Please [open an issue](https://github.com/isar-community/isar/issues/new/choose) or, even better, a PR to fix it 💪. ================================================ FILE: docs/docs/fr/README.md ================================================ --- home: true title: Acceuil heroImage: /isar.svg actions: - text: Commençons ! link: /fr/tutorials/quickstart.html type: primary features: - title: 💙 Fait pour Flutter details: Configuration minimale, facilité d'utilisation. Il suffit d'ajouter quelques lignes de code pour commencer. - title: 🚀 Hautement extensible details: Stockez des centaines de milliers d'entrées dans une seule base de données NoSQL et filtrer-les de manière efficace et asynchrone. - title: 🍭 Riche en fonctionnalités details: Isar dispose d'un riche ensemble de fonctionnalités pour vous aider à gérer vos données. Index composés et multi-entrées, modificateurs de requête, support JSON, etc. - title: 🔎 Recherche plein texte details: Isar dispose d'une recherche plein texte intégrée. Créez un index à entrées multiples et recherchez facilement des entrées. - title: 🧪 Sémantique ACID details: Isar est conforme à la norme ACID et gère les transactions automatiquement. Il annule les modifications si une erreur se produit. - title: 💃 Types statiques details: Les requêtes d'Isar sont typées statiquement et vérifiées à la compilation. Pas besoin de se soucier des erreurs d'exécution. - title: 📱 Multiplatforme details: Support pour iOS, Android, Desktop et WEB! - title: ⏱ Asynchrone details: Opérations de requête parallèles et support multi-Isolate prêts à l'emploi. - title: 🦄 Open Source details: Tout est open source et gratuit pour toujours! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/fr/crud.md ================================================ --- title: Création, lecture, modification, suppression --- # Création, lecture, modification, suppression Maintenant que nous avons défini nos collections, apprenons à les manipuler! ## Ouverture de Isar Avant de pouvoir faire quoi que ce soit, nous avons besoin d'une instance Isar. Chaque instance nécessite un répertoire avec droits d'écriture où le fichier de la base de données peut être stocké. Si vous ne spécifiez pas de répertoire, Isar trouvera un répertoire par défaut selon la plateforme actuelle. Fournissez tous les schémas que vous souhaitez utiliser avec l'instance Isar. Si nous ouvrons plusieurs instances, nous devons toujours fournir les mêmes schémas à chaque instance. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [ContactSchema], directory: dir.path, ); ``` Nous pouvons utiliser la configuration par défaut ou fournir certains des paramètres suivants: | Config | Description | |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | Ouvrez plusieurs instances avec des noms distincts. Par défaut, `"default"` est utilisé. | | `directory` | L'emplacement de stockage de cette instance. Nous pouvons passer un chemin relatif ou absolu. Par défaut, `NSDocumentDirectory` est utilisé pour iOS et `getDataDirectory` pour Android. Non requis pour Web. | | `relaxedDurability` | Assouplit la garantie de durabilité pour augmenter les performances d'écriture. En cas de crash du système (pas de crash de l'application), il est possible de perdre la dernière transaction validée. La corruption n'est pas possible. | | `compactOnLaunch` | Conditions pour vérifier si la base de données doit être compactée lors de l'ouverture de l'instance. | | `inspector` | Active l'inspecteur en mode debug. Cette option est ignorée en mode profile et release. | Si une instance est déjà ouverte, l'appel à `Isar.open()` donnera l'instance existante sans tenir compte des paramètres spécifiés. Utile pour utiliser Isar dans un isolat. :::tip Envisagez d'utiliser le package [path_provider](https://pub.dev/packages/path_provider) pour obtenir un chemin valide sur toutes les plateformes. ::: L'emplacement de stockage du fichier de la base de données est `répertoire/nom.isar`. ## Lecture de la base de données Utilisez les instances de `IsarCollection` pour trouver, filtrer et créer de nouveaux objets d'un type donné dans Isar. Pour les exemples ci-dessous, nous supposons que nous avons une collection `Recipe` définie comme suit: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Obtenir une collection Toutes nos collections vivent dans l'instance Isar. Nous pouvons obtenir la collection avec: ```dart final recipes = isar.recipes; ``` N'oubliez pas d'importer les méthodes d'extension afin d'accéder à la collection depuis l'instance isar. C'était facile! Si vous ne voulez pas utiliser les accesseurs de collection, vous pouvez aussi utiliser la méthode `collection()`: ```dart final recipes = isar.collection(); ``` ### Obtenir un objet (par id) Nous n'avons pas encore de données dans la collection, mais faisons comme si c'était le cas afin de récupérer un objet imaginaire avec l'identifiant `123`. ```dart final recipe = await recipes.get(123); ``` `get()` renvoie une `Future` avec soit l'objet, soit `null` s'il n'existe pas. Toutes les opérations d'Isar sont asynchrones par défaut, et la plupart d'entre elles ont un équivalent synchrone: ```dart final recipe = recipes.getSync(123); ``` :::warning Vous devriez utiliser la version asynchrone des méthodes par défaut dans votre isolat d'interface utilisateur. Comme Isar est très rapide, il est souvent acceptable d'utiliser la version synchrone. ::: Si nous voulons récupérer plusieurs objets à la fois, nous pouvons utiliser `getAll()` ou `getAllSync()`: ```dart final recipe = await recipes.getAll([1, 2]); ``` ### Recherche d'objets Au lieu de récupérer les objets par leur identifiant, nous pouvons également obtenir une liste d'objets répondant à certaines conditions en utilisant `.where()` et `.filter()`: ```dart final allRecipes = await recipes.where().findAll(); final favouires = await recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ En savoir plus: [Requêtes](queries) ## Modifier la base de données Il est enfin temps de modifier notre collection! Pour créer, mettre à jour ou supprimer des objets, utilisez les opérations respectives dans une transaction d'écriture: ```dart await isar.writeTxn(() async { final recipe = await recipes.get(123) recipe.isFavorite = false; await recipes.put(recipe); // Effectuer des opérations de mise à jour await recipes.delete(123); // Ou des opérations de suppression }); ``` ➡️ En savoir plus: [Transactions](transactions) ### Insertion d'objets Pour faire persister un objet dans Isar, insérons-le dans une collection. La méthode `put()` d'Isar va soit insérer, soit mettre à jour l'objet selon s'il existe déjà dans la collection ou non. Si le champ id est `null` ou `Isar.autoIncrement`, Isar utilisera un id auto-incrémenté. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await recipes.put(pancakes); }) ``` Isar attribuera automatiquement l'id à l'objet si le champ `id` est non final. Il est tout aussi facile d'insérer plusieurs objets à la fois: ```dart await isar.writeTxn(() async { await recipes.putAll([pancakes, pizza]); }) ``` ### Mise à jour d'objets La création et la mise à jour fonctionnent toutes deux avec `collection.put(object)`. Si l'id est `null` (ou n'existe pas), l'objet est créé; sinon, il est mis à jour. Donc si nous voulons défavoriser nos crêpes, nous pouvons faire ce qui suit: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await recipes.put(pancakes); }); ``` ### Suppression d'objets Vous voulez vous débarrasser d'un objet dans Isar ? Utilisez `collection.delete(id)`. La méthode `delete` retourne si un objet avec l'identifiant spécifié a été trouvé et supprimé. Si nous désirons supprimer l'objet avec l'identifiant `123`, par exemple, nous pouvons faire: ```dart await isar.writeTxn(() async { final success = await recipes.delete(123); print('Recipe deleted: $success'); }); ``` Comme pour les opérations `get` et `put`, il existe également une opération de suppression en vrac qui renvoie le nombre d'objets supprimés: ```dart await isar.writeTxn(() async { final count = await recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` Si nous ne connaissons pas les identifiants des objets que nous voulons supprimer, nous pouvons utiliser une requête: ```dart await isar.writeTxn(() async { final count = await recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/fr/faq.md ================================================ --- title: FAQ --- # Foire aux questions Une compilation de questions fréquemment posées sur les bases de données Isar et Flutter. ### Pourquoi ai-je besoin d'une base de données? > Je stocke mes données dans une base de données backend, pourquoi ai-je besoin d'Isar? Aujourd'hui encore, il est très courant de ne pas avoir de connexion internet si vous êtes dans le métro, dans l'avion, ou si vous rendez visite à votre grand-mère, qui n'a pas de WiFi et un très mauvais signal cellulaire. Vous ne devez pas laisser une mauvaise connexion paralyser votre application! ### Isar vs Hive La réponse est simple: Isar a été [lancé comme un remplacement de Hive](https://github.com/hivedb/hive/issues/246) et est maintenant à un stade où on recommande de toujours utiliser Isar plutôt que Hive. ### Clauses `where`?! > Pourquoi est-ce que **_je_** dois choisir quel index utiliser? Il y a plusieurs raisons. De nombreuses bases de données utilisent des heuristiques pour choisir le meilleur index pour une requête donnée. La base de données doit collecter des données d'utilisation supplémentaires (-> temps de traitement plus grand) et peut toujours choisir le mauvais index. Cela rend également la création d'une requête plus lente. Personne ne connaît mieux vos données que vous, le développeur. Vous pouvez donc choisir l'index optimal et décider, par exemple, si vous voulez utiliser un index pour la requête ou le tri. ### Dois-je utiliser des index / clauses `where`? Non! Isar est très probablement assez rapide si vous ne comptez que sur les filtres. ### Isar est-il suffisamment rapide ? Isar est l'une des bases de données les plus rapides pour les appareils mobiles, et devrait donc être suffisamment rapide pour la plupart des cas d'utilisation. Si vous rencontrez des problèmes de performances, il y a de fortes chances que vous fassiez quelque chose de mal. ### Isar augmente-t-il la taille de mon application? Un peu, oui. Isar augmentera la taille de téléchargement de votre application d'environ 1 à 1,5 Mo. Isar Web n'ajoute que quelques Ko. ### La documentation est incorrecte / il y a une erreur de frappe. Oh non, désolé. Veuillez [ouvrir un ticket](https://github.com/isar-community/isar/issues/new/choose) ou, mieux encore, un PR pour le résoudre 💪. ================================================ FILE: docs/docs/fr/indexes.md ================================================ --- title: Indices --- # Indices Les indices (`index`) sont la fonctionnalité la plus puissante d'Isar. De nombreuses bases de données embarquées proposent des index "normaux" (voire aucun), mais Isar dispose également d'index composés et à entrées multiples. Il est essentiel de comprendre le fonctionnement des index pour optimiser les performances des requêtes. Isar vous permet de choisir l'index que vous voulez utiliser et comment vous voulez l'utiliser. Nous allons commencer par une introduction rapide à ce que sont les index. ## Que sont les indices? Lorsqu'une collection n'est pas indexée, l'ordre des lignes ne pourra probablement pas être discerné par la requête comme étant optimisé de quelconques manières, et votre requête devra donc rechercher les objets de façon linéaire. En d'autres termes, la requête devra parcourir chaque objet pour trouver ceux qui correspondent aux conditions. Comme vous pouvez l'imaginer, cela peut prendre du temps. La recherche dans chaque objet n'est pas très efficace. Par exemple, cette collection `Product` est entièrement non ordonnée. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Données:** | id | name | price | |-----|-----------|-------| | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | Une requête qui tente de trouver tous les produits dont le prix est supérieur à 30 € doit parcourir les neuf rangées. Ce n'est pas un problème pour neuf lignes, mais cela peut le devenir pour 100 000 lignes. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` Pour améliorer les performances de cette requête, nous indexons la propriété `price`. Un index est comme une table de recherche triée: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Index généré:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Maintenant, la requête peut être exécutée beaucoup plus rapidement. L'exécuteur peut directement sauter aux trois dernières lignes d'index et trouver les objets correspondants par leur id. ### Triage Autre point intéressant: les index peuvent effectuer des tris très rapides. Les requêtes triées sont coûteuses, car la base de données doit charger tous les résultats en mémoire avant de les trier. Même si vous spécifiez un décalage ou une limite, ils sont appliqués après le tri. Imaginons que nous voulions trouver les quatre produits les moins chers. Nous pourrions utiliser la requête suivante: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` Dans cet exemple, la base de données devrait charger tous (!) les objets, les trier par prix et renvoyer les quatre produits dont le prix est le plus bas. Comme vous pouvez probablement l'imaginer, cette opération peut être réalisée de manière beaucoup plus efficace avec l'index précédent. La base de données prend les quatre premières lignes de l'index et renvoie les objets correspondants puisqu'ils sont déjà dans le bon ordre. Pour utiliser l'index pour le tri, nous devons écrire la requête comme suit: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` La clause `where` `.anyX()` indique à Isar d'utiliser un index uniquement pour le tri. Nous pouvons également utiliser une clause `where` comme `.priceGreaterThan()` et obtenir des résultats triés. ## Indices uniques Un index unique garantit que l'index ne contient pas de valeurs en double. Il peut être composé d'une ou plusieurs propriétés. Si un index unique a une propriété, les valeurs de cette propriété seront uniques. Si l'index unique a plus d'une propriété, la combinaison des valeurs dans ces propriétés est unique. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Toute tentative d'insertion ou de mise à jour de données dans l'index unique qui provoque un doublon entraînera une erreur: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // Essayons d'insérer un utilisateur avec le même nom d'utilisateur await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Remplacement d'indices Il n'est parfois pas préférable d'envoyer une erreur si une contrainte unique n'est pas respectée. Au lieu de cela, nous pouvons vouloir remplacer l'objet existant par le nouvel objet. Pour cela, il suffit de mettre la propriété `replace` de l'index à `true`. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Maintenant, lorsque nous essayons d'insérer un utilisateur avec un nom déjà existant, Isar va remplacer l'utilisateur existant par le nouveau. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Les indices de remplacement génèrent également des méthodes `putBy()` qui nous permettent de mettre à jour les objets au lieu de les remplacer. L'identifiant existant est réutilisé, et les liens sont toujours présents. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // L'utilisateur n'existe pas, donc c'est la même chose que put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` Comme nous pouvons le constater, l'identifiant du premier utilisateur inséré est réutilisé. ## Index insensibles à la casse Tous les index sur les propriétés `String` et `List` sont sensibles à la casse par défaut. Si nous voulons créer un index insensible à la casse, nous pouvons utiliser l'option `caseSensitive`: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Type d'indice Il existe différents types d'index. La plupart du temps, nous voudrons utiliser un index `IndexType.value`, mais les index de hachage sont plus efficaces. ### Index `value` Les index de valeurs sont le type par défaut et le seul autorisé pour toutes les propriétés qui ne contiennent pas de chaînes de caractères ou de listes. Les valeurs des propriétés sont utilisées pour construire l'index. Dans le cas des listes, ce sont les éléments de la liste qui sont utilisés. Il s'agit du type d'index le plus flexible mais aussi le plus gourmand en espace parmi les trois types d'index. :::tip Utilisez `IndexType.value` pour les types primitifs, les chaînes de caractères lorsque vous avez besoin de clauses `startsWith()` et les listes si vous voulez rechercher des éléments individuels. ::: ### Index `hash` Les chaînes de caractères et les listes peuvent être hachées pour réduire de manière significative le stockage requis par l'index. L'inconvénient des index de hachage est qu'ils ne peuvent pas être utilisés pour les scans de préfixe (clauses `where` `startsWith`). :::tip Utilisez `IndexType.hash` pour les chaînes de caractères et les listes si vous n'avez pas besoin des clauses `where` `startsWith` et `elementEqualTo`. ::: ### Index `hashElements` Les listes de chaînes peuvent être hachées dans leur ensemble (à l'aide de `IndexType.hash`), ou les éléments de la liste peuvent être hachés séparément (à l'aide de `IndexType.hashElements`), créant ainsi un index à entrées multiples avec des éléments hachés. :::tip Utilisez `IndexType.hashElements` pour les `List` où vous avez besoin de clauses `where` `elementEqualTo`. ::: ## Indices composés Un index composite est un index sur plusieurs propriétés. Isar nous permet de créer des index composites sur un maximum de trois propriétés. Les index composés sont également connus sous le nom d'index à colonnes multiples. Il est probablement préférable de commencer par un exemple. Nous créons une collection de personnes et définissons un index composé sur les propriétés âge et nom: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Données:** | id | name | age | hometown | |-----|--------|-----|-----------| | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Index généré:** | age | name | id | |-----|--------|-----| | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | L'indice composé généré contient toutes les personnes triées par leur âge et leur nom. Les index composés sont parfaits si nous souhaitons créer des requêtes efficaces triées par plusieurs propriétés. Ils permettent également d'utiliser des clauses `where` avancées avec plusieurs propriétés : ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` La dernière propriété d'un index composé supporte également des conditions telles que `startsWith()` ou `lessThan()` : ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Indices à entrées multiples Si nous indexons une liste en utilisant `IndexType.value`, Isar va automatiquement créer un index à entrées multiples, et chaque élément de la liste est indexé vers l'objet. Cela fonctionne pour tous les types de listes. Les applications pratiques des index à entrées multiples comprennent l'indexation d'une liste de balises ou la création d'un index en texte intégral. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` divise une chaîne de caractères en mots selon la spécification [Unicode Annex #29](https://unicode.org/reports/tr29/), ce qui fait qu'il fonctionne correctement pour presque toutes les langues. **Data:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Les entrées comportant des mots en double n'apparaissent qu'une seule fois dans l'index. **Index généré:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | Cet index peut maintenant être utilisé pour les clauses de préfixe (ou d'égalité) des mots individuels de la description. :::tip Au lieu de stocker les mots directement, vous pouvez également envisager d'utiliser le résultat d'un [algorithme phonétique](https://fr.wikipedia.org/wiki/Algorithme_phon%C3%A9tique) comme [Soundex](https://fr.wikipedia.org/wiki/Soundex). ::: ================================================ FILE: docs/docs/fr/limitations.md ================================================ # Limitations Comme vous le savez, Isar fonctionne sur les appareils mobiles et les ordinateurs de bureau fonctionnant sur la VM ainsi que sur le Web. Ces deux plateformes sont très différentes, et ont donc des limitations différentes. ## Limitations de la VM - Seuls les 1024 premiers octets d'une chaîne peuvent être utilisés pour un préfixe de clause `where`. - Les objets ne peuvent avoir une taille supérieure à 16 Mo ## Limitations Web Comme Isar Web est basé sur `IndexedDB`, il y a plus de limitations, mais elles sont à peine perceptibles lors de l'utilisation d'Isar. - Les méthodes synchrones ne sont pas supportées - Les filtres `Isar.splitWords()` et `.matches()` ne sont pas encore implémentés - Les changements de schémas ne sont pas autant vérifiés que dans la VM, il faut donc faire attention à respecter les règles - Tous les types de nombres sont stockés en tant que double (le seul type de nombre js), donc `@Size32` n'a aucun effet - Les index sont représentés différemment, donc les index de hachage n'utilisent pas moins d'espace (ils fonctionnent toujours de la même manière) - `col.delete()` et `col.deleteAll()` fonctionnent correctement, mais la valeur de retour n'est pas correcte - `col.clear()` ne réinitialise pas la valeur d'auto-incrémentation - `NaN` n'est pas supporté comme valeur ================================================ FILE: docs/docs/fr/links.md ================================================ --- title: Liens --- # Liens Les liens nous permettent d'exprimer des relations entre objets, comme l'auteur d'un commentaire (`User`). Nous pouvons modéliser des relations `1:1`, `1:n`, et `n:n` avec les liens Isar. L'utilisation de liens est moins ergonomique que l'utilisation d'objets embarqués. Il est donc préférable d'utiliser des objets embarqués lorsque possible. Considérez le lien comme une table séparée qui contient la relation. Elle est similaire aux relations SQL, mais possède un ensemble de fonctionnalités et une API différente. ## IsarLink `IsarLink` peut contenir un ou plusieurs objets liés et peut être utilisé pour exprimer une relation de type "un-à-un". `IsarLink` a une seule propriété appelée `value` qui contient l'objet lié. Les liens ne sont pas chargés par default. Vous devez donc dire à `IsarLink` de charger ou de sauvegarder la `value` explicitement. Vous pouvez le faire en appelant `linkProperty.load()` et `linkProperty.save()`. :::tip La propriété `id` des collections source et cible d'un lien doit être non finale. ::: Pour les plateformes autres que web, les liens sont chargés automatiquement lorsque vous les utilisez pour la première fois. Commençons par ajouter un `IsarLink` à une collection: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` Nous avons défini un lien entre les enseignants et les élèves. Dans cet exemple, chaque élève peut avoir exactement un professeur. D'abord, nous créons le professeur et l'assignons à un étudiant. Nous devons `.put()` le professeur et sauvegarder le lien manuellement. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` Nous pouvons maintenant utiliser le lien : ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Essayons la même chose avec du code synchrone. Nous n'avons pas besoin de sauvegarder le lien manuellement, car `.putSync()` sauvegarde automatiquement tous les liens. Il crée même le professeur pour nous. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks Il serait plus logique que l'étudiant de l'exemple précédent puisse avoir plusieurs professeurs. Heureusement, Isar a `IsarLinks`, qui permet de contenir plusieurs objets liés et d'exprimer une relation de type "à plusieurs". `IsarLinks` implémente `Set` et expose toutes les méthodes qui sont autorisées pour les ensembles. `IsarLinks` se comporte comme `IsarLink` et n'est également pas changé par défaut. Pour charger tous les objets liés, nous devons utiliser `linkProperty.load()`. Pour persister les changements, `linkProperty.save()`. La représentation interne de `IsarLink` et `IsarLinks` est la même. Nous pouvons faire évoluer le `IsarLink` d'avant en un `IsarLinks` pour assigner plusieurs professeurs à un seul étudiant (sans perdre de données). ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Cela fonctionne étant donné que nous n'avons pas changé le nom du lien (`teacher`), donc Isar s'en souvient d'avant. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlinks Je vous entends demander: "Et si nous voulions exprimer des relations inverses?". Ne vous inquiétez pas, nous allons maintenant introduire les `Backlinks`. Les backlinks sont des liens en sens inverse. Chaque lien a toujours un backlink implicite. Nous pouvons le rendre disponible à notre application en annotant un `IsarLink` ou un `IsarLinks` avec `@Backlink()`. Les backlinks ne nécessitent pas de mémoire ou de ressources supplémentaires; nous pouvons librement les ajouter, les supprimer et les renommer sans perdre de données. Pour savoir quels sont les étudiants d'un enseignant spécifique, nous définissons donc un lien retour: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` Il faut préciser le lien vers lequel pointe le backlink. Il est possible d'avoir plusieurs liens différents entre deux objets. ## Initialisation des liens `IsarLink` et `IsarLinks` ont un constructeur sans argument, qui devrait être utilisé pour assigner la propriété de lien quand l'objet est créé. C'est une bonne pratique de rendre les propriétés de lien `final`. Lorsque nous sauvegardons (`put()`) notre objet pour la première fois, le lien est initialisé avec la collection source et cible, et nous pouvons appeler des méthodes comme `load()` et `save()`. Un lien commence à suivre les changements immédiatement après sa création, donc nous pouvons ajouter et supprimer des relations avant même que le lien soit initialisé. :::danger Il est illégal de déplacer un lien vers un autre objet. ::: ================================================ FILE: docs/docs/fr/queries.md ================================================ --- title: Requêtes --- # Requêtes Les requêtes nous permettent de trouver des enregistrements correspondant à certaines conditions, par exemple: - Trouver tous les contacts favoris. - Trouver des prénoms distincts dans les contacts. - Supprimez tous les contacts dont le nom de famille n'est pas défini. Comme les requêtes sont exécutées sur la base de données et non dans Dart, elles sont très rapides. Si nous utilisons intelligemment les index, nous pouvons encore améliorer les performances des requêtes. Dans ce qui suit, nous apprendrons comment écrire des requêtes et comment les rendre le plus rapide possible. Il existe deux méthodes différentes pour filtrer les enregistrements: les filtres et les indexes. Nous allons commencer par examiner le fonctionnement des filtres. ## Filtres Les filtres sont faciles à utiliser et à comprendre. Selon le type des champs, il existe différentes opérations de filtrage disponibles, dont la plupart ont des noms explicites. Les filtres fonctionnent en évaluant une expression pour chaque objet de la collection à filtrer. Si l'expression donne un résultat "vrai" (`true`), Isar l'inclura dans les résultats. Les filtres n'affectent pas l'ordre des résultats. Nous utiliserons le modèle suivant pour les exemples ci-dessous: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Conditions de requête Selon le type de champ, il existe différentes conditions. | Condition | Description | |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `.equalTo(value)` | Recherche les valeurs qui sont égales à `value`. | | `.between(lower, upper)` | Recherche les valeurs qui se situent entre `lower` et `upper`. | | `.greaterThan(bound)` | Recherche les valeurs qui sont supérieures à `bound`. | | `.lessThan(bound)` | Recherche les valeurs qui sont inférieures à `bound`. Les valeurs `null` seront incluses par défaut car `null` est considéré comme plus petit que toute autre valeur. | | `.isNull()` | Recherche les valeurs qui sont `null`. | | `.isNotNull()` | Recherche les valeurs qui ne sont pas `null`. | | `.length()` | Les requêtes sur la longueur des listes, Strings et liens filtrent les objets en fonction du nombre d'éléments dans une liste ou un lien. | Supposons que la base de données contienne quatre chaussures de tailles 39, 40, 46 et une de taille non définie (`null`). Si nous n'effectuons pas de tri, les valeurs seront retournées trier par id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Opérateurs logiques Nous pouvons composer des prédicats à l'aide des opérateurs logiques suivants: | Opérateur | Description | |------------|------------------------------------------------------------------------------| | `.and()` | Évalue à `true` si les expressions de gauche et de droite évaluent à `true`. | | `.or()` | Évalue à `true` si l'une des deux expressions évalue à `true`. | | `.xor()` | Évalue à `true` si exactement une expression évalue à `true`. | | `.not()` | Négativise le résultat de l'expression suivante. | | `.group()` | Regroupe les conditions et permet de spécifier l'ordre d'évaluation. | Si nous voulons trouver toutes les chaussures de taille 46, nous pouvons utiliser la requête suivante: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` Si nous voulons utiliser plus d'une condition, nous pouvons combiner plusieurs filtres à l'aide du **et** (`.and()`) logique, **ou** (`.or()`) logique et **xor** (`.xor()`) logique. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Facultatif. Les filtres sont implicitement combinés avec des et logiques. .isUnisexEqualTo(true) .findAll(); ``` Cette requête est équivalente à `size == 46 && isUnisex == true`. Nous pouvons également regrouper des conditions en utilisant `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` Cette requête est équivalente à `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. Pour nier une condition ou un groupe, utilisons l’opérateur logique **not** (`.not()`): ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` Cette requête est équivalente à `size != 46 && isUnisex != true`. ### Conditions de chaîne de caractères En plus des conditions de recherche ci-dessus, les valeurs de type `String` offrent quelques conditions supplémentaires que nous pouvons utiliser. Les caractères génériques de type Regex, par exemple, permettent une plus grande flexibilité dans la recherche. | Condition | Description | |----------------------|-----------------------------------------------------------------------| | `.startsWith(value)` | Recherche les valeurs qui commencent par la valeur `value` fournie. | | `.contains(value)` | Recherche les valeurs qui contiennent la valeur `value` fournie. | | `.endsWith(value)` | Recherche les valeurs qui se terminent par la valeur `value` fournie. | | `.matches(wildcard)` | Recherche les valeurs qui correspondent au motif `wildcard` fourni. | **Sensibilité à la casse** Toutes les opérations sur les chaînes de caractères ont un paramètre optionnel `caseSensitive` dont la valeur par défaut est `true`. **Motifs:** Une [expression de métacaractère](https://fr.wikipedia.org/wiki/M%C3%A9tacaract%C3%A8re) est une chaîne de caractères qui utilise des caractères normaux avec deux caractères génériques spéciaux: - Le caractère générique `*` correspond à zéro ou plus de n'importe quel caractère. - Le caractère générique `?` correspond à n'importe quel caractère. Par exemple, la chaîne générique `"d?g"` correspond à `"dog"`, `"dig"` et `"dug"`, mais pas à `"ding"`, `"dg"` ou `"a dog"`. ### Modificateurs de requête Il est parfois nécessaire de construire une requête basée sur certaines conditions ou pour différentes valeurs. Isar dispose d'un outil très puissant pour construire des requêtes conditionnelles: | Modificateur | Description | |-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `.optional(cond, qb)` | Étend la requête uniquement si la `condition` est `true`. Cela peut être utilisé presque partout dans une requête, par exemple pour la trier ou la limiter de manière conditionnelle. | | `.anyOf(list, qb)` | Étend la requête pour chaque valeur de `values` et combine les conditions en utilisant l’opérateur **ou**. | | `.allOf(list, qb)` | Étend la requête pour chaque valeur de `values` et combine les conditions en utilisant les **et** logiques. | Dans cet exemple, nous construisons une méthode qui trouve des chaussures avec un filtre optionnel: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // Seulement appliquer le filtre si sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` Si nous voulons trouver toutes les chaussures qui ont une ou plusieurs tailles, nous pouvons soit écrire une requête classique, soit utiliser le modificateur `anyOf()`: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` Les modificateurs de requête sont particulièrement utiles lorsque nous souhaitons construire des requêtes dynamiques. ### Listes Même les listes peuvent être utilisées dans les requêtes: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` Nous pouvons effectuer des requêtes en fonction de la longueur de la liste: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` Ces requêtes sont équivalentes au code Dart `tweets.where((t) => t.hashtags.isEmpty)` et `tweets.where((t) => t.hashtags.length > 5)`. Nous pouvons également effectuer des requêtes sur les éléments de la liste: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` Cette requête est équivalente au code Dart `tweets.where((t) => t.hashtags.contains('flutter'))`. ### Objets embarqués Les objets embarqués sont l'une des fonctionnalités les plus utiles d'Isar. Ils peuvent être filtrés très efficacement en utilisant les mêmes conditions que celles disponibles pour les objets de niveau supérieur. Supposons que nous ayons le modèle suivant: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` Nous voulons filtrer toutes les voitures qui ont une marque avec le nom `"BMW"` et le pays `"Allemagne"`. Nous pouvons le faire en utilisant la requête suivante: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Essayez toujours de regrouper les requêtes imbriquées. La requête ci-dessus est plus efficace que la suivante, même si le résultat est le même: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Liens Si nos modèles contiennent des [liens](links), nous pouvons filtrer sur les objets liés ou le nombre d'objets liés. :::warning Gardez en tête que les requêtes de liens peuvent être coûteuses, car Isar doit rechercher les objets liés. Pensez à utiliser des objets embarqués à la place. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Nous voulons trouver tous les élèves qui ont un professeur de mathématiques ou d'anglais: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Les filtres de liens sont évalués à `true` si au moins un objet lié correspond aux conditions. Cherchons tous les élèves qui n'ont pas de professeur: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` ou sinon: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Clauses `Where` Les clauses `where` sont un outil très puissant, mais il n'est pas toujours facile de les utiliser correctement. Contrairement aux filtres, les clauses `where` utilisent les index que nous définissons dans le schéma pour évaluer les conditions de la requête. La requête d'un index est beaucoup plus rapide que le filtrage individuel de chaque entrée. ➡️ En savoir plus: [Indices](indexes) :::tip En règle générale, vous devriez toujours essayer de réduire les entrées autant que possible à l'aide de clauses `where`, et effectuer le reste du filtrage à l'aide de filtres. ::: Nous pouvons uniquement combiner les clauses `where` en utilisant des **ou** logiques. En d'autres termes, nous pouvons additionner plusieurs clauses `where`, mais nous ne pouvons pas effectuer une requête sur l'intersection de plusieurs clauses `where`. Ajoutons des index à la collection `Shoe`: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` Il y a deux index. L'index sur `size` nous permet d'utiliser des clauses `where` comme `.sizeEqualTo()`. L'index composé sur `isUnisex` permet d'utiliser des clauses `where` comme `isUnisexSizeEqualTo()`, mais aussi `isUnisexEqualTo()`, car nous pouvons toujours utiliser n'importe quel préfixe d'un index. Nous pouvons maintenant réécrire la requête précédente qui trouve des chaussures unisexes de taille 46 en utilisant l'index composé. Cette requête sera beaucoup plus rapide que la précédente: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Les clauses `where` ont deux autres superpouvoirs: elles vous offrent un tri "gratuit" et une opération distincte super rapide. ### Combinaison de clauses `where` et de filtres Vous vous souvenez des requêtes `shoes.filter()`? Il s'agit en fait d'un raccourci pour `shoes.where().filter()`. Nous pouvons (et devrions) combiner les clauses `where` et les filtres dans une même requête pour bénéficier des avantages des deux: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` La clause `where` est d'abord appliquée pour réduire le nombre d'objets à filtrer. Ensuite, le filtre est appliqué aux objets restants. ## Triage Nous pouvons définir comment les résultats doivent être triés lors de l'exécution de la requête en utilisant les méthodes `.sortBy()`, `.sortByDesc()`, `.thenBy()` et `.thenByDesc()`. Pour trouver toutes les chaussures triées par nom de modèle en ordre croissant et par taille en ordre décroissant sans utiliser d'index: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Le tri de nombreux résultats peut s'avérer coûteux, d'autant plus que le tri intervient avant le `offset` et `limit`. Les méthodes de tri ci-dessus ne font jamais appel aux index. Heureusement, nous pouvons à nouveau utiliser le tri par clause `where` et rendre notre requête rapide comme l'éclair, même si nous devons trier un million d'objets. ### Tri par clause `where` Si nous utilisons une clause `where` **simple** dans notre requête, les résultats sont déjà triés par l'index. Ce n'est pas rien! Supposons que nous avons des chaussures de taille `[43, 39, 48, 40, 42, 45]` et que nous voulons trouver toutes les chaussures dont la taille est supérieure à `42` et les trier par taille: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // Trie également les résultats par taille .findAll(); // -> [43, 45, 48] ``` Comme nous pouvons le constater, le résultat est trié par l'index `size`. Si nous voulons inverser l'ordre de tri de la clause `where`, nous pouvons donner à `sort` la valeur `Sort.desc` : ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` Parfois, nous ne voulons pas utiliser des clauses `where`, mais nous pouvons tout de même bénéficier du tri implicite. Nous pouvons utiliser la clause `where` `any`: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` Si nous utilisons un index composé, les résultats sont triés par tous les champs de l'index. :::tip Si vous avez besoin que les résultats soient triés, pensez à utiliser un index dans ce but. Surtout si vous utilisez avec `offset()` et `limit()`. ::: Parfois, il n'est pas possible ou utile d'utiliser un index pour le tri. Dans ce cas, nous devons utiliser des index pour réduire autant que possible le nombre d'entrées résultantes. ## Valuers uniques Pour ne renvoyer que les entrées ayant des valeurs uniques, utilisez le prédicat `distinct`. Par exemple, pour savoir combien de modèles de chaussures différents nous avons dans votre base de données Isar: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` Nous pouvons également chaîner plusieurs conditions distinctes pour trouver toutes les chaussures avec des combinaisons modèle-taille distinctes: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Seul le premier résultat de chaque combinaison distincte est retourné. Nous pouvons utiliser des clauses `where` et des opérations de tri pour le contrôler. ### Clause `where` distincte Si nous avons un index non-unique, nous pouvons vouloir obtenir toutes ses valeurs distinctes. Nous pouvons utiliser l'opération `distinctBy` de la section précédente, mais elle est effectuée après le tri et les filtres, ce qui entraîne une certaine lourdeur. Si nous n'utilisons qu'une seule clause `where`, nous pouvons nous fier à l'index pour effectuer l'opération de distinction. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip En théorie, nous pouvons même utiliser plusieurs clauses `where` pour le tri et la distinction. La seule restriction est que ces clauses `where` ne doivent pas se chevaucher et utiliser le même index. Pour un tri correct, elles doivent également être appliquées dans l'ordre de tri. Soyez très prudent si vous vous fiez à cela! ::: ## Décalage et limite C'est souvent une bonne idée de limiter le nombre de résultats d'une requête pour les listes "lazy". Nous pouvons le faire en définissant un `limit()`: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` En définissant un `offset()`, nous pouvons également paginer les résultats de notre requête. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` L'instanciation des objets Dart étant souvent la partie la plus coûteuse de l'exécution d'une requête, il est judicieux de ne charger que les objets dont nous avons de besoin. ## Ordre d'exécution Isar exécute les requêtes toujours dans le même ordre : 1. Traverser l'index primaire ou secondaire pour trouver des objets (appliquer des clauses `where`) 2. Filtrer les objets 3. Trier les résultats 4. Appliquer l'opération distincte 5. Décalage et limite des résultats 6. Retour des résultats ## Opérations de requêtes Dans les exemples précédents, nous avons utilisé `.findAll()` pour récupérer tous les objets correspondants. Cependant, d'autres opérations sont disponibles: | Opération | Description | |------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| | `.findFirst()` | Retourne seulement le premier objet correspondant ou `null` si aucun ne correspond. | | `.findAll()` | Retourne tous les objets correspondants. | | `.count()` | Compte le nombre d'objets correspondant à la requête. | | `.deleteFirst()` | Supprime le premier objet correspondant de la collection. | | `.deleteAll()` | Supprime tous les objets correspondants de la collection. | | `.build()` | Compile la requête pour la réutiliser plus tard. Cela permet d'économiser le coût de construction d'une requête si nous souhaitons l'exécuter plusieurs fois. | ## Requêtes de propriété Si nous ne sommes intéressés que par les valeurs d'une seule propriété, nous pouvons utiliser une requête de propriété. Il suffit de construire une requête ordinaire et de sélectionner une propriété: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` L'utilisation d'une seule propriété permet de gagner du temps lors de la désérialisation. Les requêtes de propriétés fonctionnent également pour les objets embarqués et les listes. ## Agrégation Isar supporte l'agrégation des valeurs d'une requête de propriété. Les opérations d'agrégation disponibles sont les suivantes : | Opération | Description | |--------------|----------------------------------------------------------------------------| | `.min()` | Trouve la valeur minimale ou `null` si aucune ne correspond. | | `.max()` | Trouve la valeur maximale ou `null` si aucune ne correspond. | | `.sum()` | Additionne toutes les valeurs. | | `.average()` | Calcule la moyenne de toutes les valeurs ou `NaN` si aucune ne correspond. | L'utilisation des agrégations est beaucoup plus rapide que la recherche de tous les objets correspondants et l'exécution manuelle de l'agrégation. ## Requêtes dynamiques :::danger Cette section n'est probablement pas pertinente pour vous. Il est déconseillé d'utiliser des requêtes dynamiques, sauf si vous en avez absolument besoin (ce qui est rarement le cas). ::: Tous les exemples ci-dessus ont utilisé le `QueryBuilder` et les méthodes d'extension statiques générées. Peut-être voulez-vous créer des requêtes dynamiques ou un langage de requête personnalisé (comme l'inspecteur Isar). Dans ce cas, nous pouvons utiliser la méthode `buildQuery()` : | Paramètre | Description | |-----------------|----------------------------------------------------------------------------------------------------------------------| | `whereClauses` | Les clauses `where` de la requête. | | `whereDistinct` | Si les clauses `where` doivent retourner des valeurs distinctes (utile uniquement pour les clauses `where` uniques). | | `whereSort` | L'ordre de passage des clauses `where` (utile uniquement pour les clauses `where` uniques). | | `filter` | Le filtre à appliquer aux résultats. | | `sortBy` | Une liste de propriétés à trier. | | `distinctBy` | Une liste de propriétés à distinguer par. | | `offset` | Le décalage des résultats. | | `limit` | Le nombre maximum de résultats à retourner. | | `property` | Si non-nulle, seules les valeurs de cette propriété sont renvoyées. | Créons une requête dynamique: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` La requête suivante est équivalente: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/fr/recipes/data_migration.md ================================================ --- title: Migration des données --- # Migration des données Isar migre automatiquement les schémas de notre base de données si nous ajoutons ou supprimons des collections, champs ou index. Il peut arriver que nous souhaitions également migrer des données. Isar n'offre pas de solution intégrée, car cela imposerait des restrictions de migration arbitraires. Il est facile d'implémenter une logique de migration adaptée à nos besoins. Dans cet exemple, nous voulons utiliser une seule version pour l'ensemble de la base de données. Nous utilisons `shared_preferences` pour stocker la version actuelle et la comparer à la version vers laquelle nous désirons migrer. Si les versions ne correspondent pas, nous migrons les données et mettons à jour la version. :::tip Vous pouvez également donner à chaque collection sa propre version et les migrer individuellement. ::: Imaginons que nous avons une collection d'utilisateurs avec un champ d'anniversaire. Dans la version 2 de notre application, nous avons besoin d'un champ supplémentaire pour l'année de naissance afin de rechercher des utilisateurs en fonction de leur âge. Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` Le problème est que les modèles d'utilisateurs existants auront un champ `birthYear` vide, car il n'existait pas dans la version 1. Nous devons migrer les données pour définir le champ `birthYear`. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // Si la version n'est pas définie (nouvelle installation) ou si elle est déjà à 2, il n'est pas nécessaire de migrer. return; default: throw Exception('Unknown version: $currentVersion'); } // Mise à jour de la version await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // Nous paginons à travers les utilisateurs pour éviter de tous les charger en mémoire en même temps for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // Nous n'avons pas besoin de mettre à jour quoi que ce soit puisque le `getter` `birthYear` est utilisé. await isar.users.putAll(users); }); } } ``` :::warning Si vous devez migrer un grand nombre de données, envisagez d'utiliser un isolat en arrière plan pour éviter de surcharger le thread de l'interface utilisateur. ::: ================================================ FILE: docs/docs/fr/recipes/full_text_search.md ================================================ --- title: Recherche plein texte --- # Recherche plein texte La recherche plein texte est un moyen puissant de rechercher du texte dans la base de données. Vous devriez déjà être familiarisé avec le fonctionnement des [indices](../indexes), mais passons en revue les principes de base. Un index fonctionne comme une table de recherche, permettant au moteur de recherche de trouver rapidement les enregistrements ayant une valeur donnée. Par exemple, si nous avons un champ "titre" dans notre objet, nous pouvons créer un index sur ce champ afin de trouver plus rapidement les objets ayant un titre donné. ## Pourquoi la recherche plein texte est-elle utile? Nous pouvons facilement rechercher du texte en utilisant des filtres. Il existe plusieurs opérations de chaînes de caractères, par exemple `.startsWith()`, `.contains()` et `.matches()`. Le problème avec les filtres est que leur temps d'exécution est de `O(n)`, où `n` est le nombre d'enregistrements dans la collection. Les opérations sur chaînes de caractères comme `.matches()` sont particulièrement coûteuses. :::tip La recherche plein texte est beaucoup plus rapide que les filtres, mais les index ont certaines limites. Dans cette recette, nous allons explorer comment contourner ces limites. ::: ## Exemple de base L'idée est toujours la même: au lieu d'indexer l'ensemble du texte, nous indexons les mots du texte afin de pouvoir les rechercher individuellement. Créons l'index plein texte le plus basique: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` Nous pouvons maintenant rechercher des messages dont le contenu contient des mots spécifiques: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` Cette requête est super rapide, mais il y a quelques problèmes: 1. Nous ne pouvons rechercher que des mots entiers 2. Nous ne tenons pas compte de la ponctuation 3. Nous ne prenons pas en charge les autres caractères d'espacement ## Diviser le texte de la bonne manière Essayons d'améliorer l'exemple précédent. Nous pourrions essayer de développer une regex compliquée pour corriger le découpage de mots, mais cela sera probablement lent et incorrect dans certains cas. Le [Unicode Annex #29](https://unicode.org/reports/tr29/) définit comment diviser correctement un texte en mots pour presque toutes les langues. C'est assez compliqué, mais heureusement, Isar fait le gros du travail pour nous: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## Je veux plus de contrôle C'est simple et facile! Nous pouvons également modifier notre index pour supporter la comparaison des préfixes et la correspondance insensible à la casse: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` Par défaut, Isar stocke les mots sous forme de valeurs hachées, ce qui est rapide et peu encombrant. Mais les valeurs hachées ne peuvent pas être utilisées pour la comparaison des préfixes. En utilisant `IndexType.value`, nous pouvons changer l'index pour utiliser directement les mots à la place. Cela nous donne la clause `where` `.titleWordsAnyStartsWith()`: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## Je veux aussi `.endsWith()` Bien sûr! Nous allons utiliser une astuce pour réaliser la comparaison `.endsWith()`: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` N'oublions pas d'inverser la terminaison que nous voulons rechercher: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Algorithmes de racinisation (`stemming`) Malheureusement, les index ne supportent pas la comparaison `.contains()` (ceci est vrai pour d'autres bases de données également). Mais il y a quelques alternatives qui valent la peine d'être explorées. Le choix dépend fortement de votre utilisation. Un exemple est l'indexation des racines de mots au lieu du mot entier. Un algorithme de racinisation est un processus de normalisation linguistique dans lequel les différentes formes d'un mot sont réduites à une forme commune : ``` connexion connexions connectif ---> connect connecté connecter ``` Les algorithmes les plus populaires sont [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) et [Snowball stemming algorithms](https://snowballstem.org/algorithms/). Il existe également des formes plus avancées comme la [Lemmatisation](https://fr.wikipedia.org/wiki/Lemmatisation). ## Algorithmes phonétiques Un [algorithme phonétique](https://fr.wikipedia.org/wiki/Algorithme_phon%C3%A9tique) est un algorithme permettant d'indexer les mots en fonction de leur prononciation. En d'autres termes, il nous permet de trouver des mots dont la sonorité est similaire à celle des mots que nous voulons recherchez. :::warning La plupart des algorithmes phonétiques ne supportent qu'une seule langue. ::: ### Soundex [Soundex](https://fr.wikipedia.org/wiki/Soundex) est un algorithme phonétique d'indexation des noms par le son, tel qu'il est prononcé en anglais. Le but est que les homophones soient encodés dans la même représentation, afin qu'ils puissent être mis en relation malgré des différences mineures dans l'orthographe. Il s'agit d'un algorithme simple, et il existe de nombreuses versions améliorées. En utilisant cet algorithme, `"Robert"` et `"Rupert"` renvoient tous deux la chaîne `"R163"`, tandis que `"Rubin"` donne `"R150"`. `"Ashcraft"` et `"Ashcroft"` donnent tous deux `"A261"`. ### Double Metaphone L'algorithme de codage phonétique [Double Metaphone](https://fr.wikipedia.org/wiki/Metaphone) est la deuxième génération de cet algorithme. Il apporte plusieurs améliorations fondamentales à la conception de l'algorithme Metaphone original. Double Metaphone prend en compte diverses irrégularités de l'anglais d'origine slave, germanique, celtique, grecque, française, italienne, espagnole, chinoise et autres. ================================================ FILE: docs/docs/fr/recipes/multi_isolate.md ================================================ --- title: Utilisation multi-isolats --- # Utilisation multi-isolats Au lieu de threads, tout code Dart s'exécute dans des isolats. Chaque isolat possède son propre espace mémoire, ce qui garantit qu'aucun des états d'un isolat n'est accessible depuis un autre isolat. Il est possible d'accéder à Isar à partir de plusieurs isolats en même temps. Même les observateurs fonctionnent à travers les isolats. Dans cette recette, nous allons voir comment utiliser Isar dans un environnement multi-isolats. ## Quand utiliser plusieurs isolats Les transactions Isar sont exécutées en parallèle, même si elles sont exécutées dans le même isolat. Dans certains cas, il est toujours utile d'accéder à Isar à partir de plusieurs isolats. La raison en est qu'Isar passe un certain temps à encoder et décoder des données depuis et vers des objets Dart. Nous pouvons imaginer que c'est comme coder et décoder en JSON (en plus efficace). Ces opérations s'exécutent à l'intérieur de l'isolat à partir duquel on accède aux données et bloquent naturellement les autres codes de l'isolat. En d'autres termes: Isar effectue une partie du travail dans votre isolat Dart. Si nous n'avons besoin de lire ou d'écrire que quelques centaines d'objets à la fois, le faire dans l'isolat de l'interface utilisateur ne pose pas de problème. Mais pour les transactions importantes ou si le thread de l'interface utilisateur est déjà occupé, nous devrions envisager d'utiliser un isolat séparé. ## Exemple La première chose que nous devons faire est d'ouvrir Isar dans le nouvel isolat. Puisque l'instance de Isar est déjà ouverte dans l'isolat principal, `Isar.open()` retournera la même instance. :::warning Assurez-vous de fournir les mêmes schémas que dans l'isolat principal. Sinon, vous obtiendrez une erreur. ::: `compute()` démarre un nouvel isolat dans Flutter et y exécute la fonction donnée. ```dart void main() { // Ouvre Isar dans l'isolat de l'interface utilisateur final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // Écoute les changements dans la base de données isar.messages.watchLazy(() { print('omg the messages changed!'); }); // Démarre un nouvel isolat et crée 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // Après quelque temps: // > omg the messages changed! // > isolate finished } // Fonction qui sera exécutée dans le nouvel isolat Future createDummyMessages(int count) async { // Nous n'avons pas besoin du chemin du dossier ici étant donné que l'instance est déjà ouverte. final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // Nous utilisons une transaction synchrone en isolat isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` Il y a quelques éléments intéressants à noter dans l'exemple ci-dessus: - `isar.messages.watchLazy()` est appelé dans l'isolat UI et est notifié des changements provenant d'un autre isolat. - Les instances sont référencées par leur nom. Le nom par défaut est `default`, mais dans cet exemple, nous l'avons défini comme `myInstance`. - Nous avons utilisé une transaction synchrone pour créer les mesasges. Bloquer notre nouvel isolat n'est pas un problème, et les transactions synchrones sont un peu plus rapides. ================================================ FILE: docs/docs/fr/recipes/string_ids.md ================================================ --- title: Identifiants en chaîne de caractères --- # Identifiants en chaîne de caractères C'est l'une des demandes les plus fréquemment reçues. Voici donc un tutoriel sur l'utilisation des ids en `String`. Isar ne supporte pas nativement les ids `String`, et il y a une bonne raison à cela: les ids entiers sont beaucoup plus efficaces et rapides. En particulier pour les liens, la complexité d'un identifiant de type `String` est trop importante. Il arrive parfois que l'on doive stocker des données externes qui utilisent des UUID ou autres identifiants non entiers. Il est recommandé de stocker la chaîne id comme une propriété de votre objet et d'utiliser une implémentation de hachage rapide pour générer un int 64 bits qui peut être utilisé comme Id. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` Avec cette approche, nous obtenons le meilleur des deux mondes: des identifiants entiers efficaces pour les liens et la possibilité d'utiliser des identifiants de type `String`. ## Fonction de hachage rapide Idéalement, notre fonction de hachage devrait avoir une haute qualité (nous ne voulons pas de collisions) et être rapide. Il est recommandé d'utiliser l'implémentation suivante: ```dart /// Algorithme de hachage FNV-1a 64 bits optimisé pour les chaînes de caractères Dart int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` Si vous choisissez une fonction de hachage différente, assurez-vous qu'elle renvoie un int 64 bits et évitez d'utiliser une fonction de hachage cryptographique, car elle est beaucoup plus lente. :::warning Évitez d'utiliser `string.hashCode`, car sa stabilité n'est pas garantie sur les différentes plateformes et versions de Dart. ::: ================================================ FILE: docs/docs/fr/schema.md ================================================ --- title: Schéma --- # Schéma Lorsque vous utilisez Isar pour stocker les données de votre application, vous devez utiliser des collections. Une collection est comme une table de base de données, et ne peut contenir qu'un seul type d'objet Dart. Chaque objet de collection représente une entrée de données dans la collection correspondante. La définition d'une collection s'appelle "schéma". Le générateur Isar fera le gros du travail pour nous et générera la plupart du code dont nous avons besoin pour utiliser la collection. ## Anatomie d'une collection Nous définissons chaque collection Isar en annotant une classe avec `@collection` ou `@Collection()`. Une collection Isar comprend des champs pour chaque colonne de la table correspondante dans la base de données, y compris un champ qui comprend la clé primaire. Le code suivant est un exemple d'une collection simple qui définit une table `User` avec des colonnes pour l'ID, le prénom et le nom : ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip Pour faire persister un champ, Isar doit y avoir accès. Vous pouvez vous assurer que Isar y a accès en le rendant public ou en fournissant des méthodes `getter` et `setter`. ::: Il existe quelques paramètres facultatifs permettant de personnaliser la collection: | Config | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------- | | `inheritance` | Contrôle si les champs des classes parentes et des mixins seront stockés dans Isar. Activé par défaut. | | `accessor` | Permet de renommer l'accesseur de collection par défaut (par exemple `isar.contacts` pour la collection `Contact`). | | `ignore` | Permet d'ignorer certaines propriétés de la classe. Celles-ci sont également respectées pour les classes parentes. | ### Id Isar Chaque classe de collection doit définir une propriété id de type `Id`, qui identifie de façon unique un objet. `Id` est un alias pour `int` qui permet au générateur Isar de reconnaître la propriété id. Isar indexe automatiquement les champs id, ce qui nous permet d'obtenir et de modifier les objets en fonction de leur id de manière efficace. Vous pouvez soit définir les ids vous-même, soit demander à Isar d'attribuer un id auto-incrémenté. Si le champ `id` est `null` et non `final`, Isar assignera un id auto-incrémenté. Si vous voulez un identifiant auto-incrémenté non nul, vous pouvez utiliser `Isar.autoIncrement` au lieu de `null`. :::tip Les identifiants d'auto-incrémentation ne sont pas réutilisés lorsqu'un objet est supprimé. La seule façon de réinitialiser les identifiants d'auto-incrémentation est d'effacer la collection ou la base de données. ::: ### Renommer les collections et champs Par défaut, Isar utilise le nom de la classe comme nom de collection. De même, Isar utilise les noms de champs comme noms de colonnes dans la base de données. Si vous voulez qu'une collection ou un champ ait un nom différent, ajoutez l'annotation `@Name()`. L'exemple suivant montre des noms personnalisés pour les collections et les champs : ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Vous devriez envisager d'utiliser l'annotation `@Name()` si vous voulez renommer des champs ou des classes Dart qui sont déjà stockés dans la base de données. Sinon, la base de données supprimera et recréera le champ ou la collection. ### Ignorer des champs Isar persiste tous les champs publics d'une classe de collection. En annotant une propriété ou un `getter` avec `@ignore`, vous pouvez l'exclure de la persistance, comme le montre l'extrait de code suivant: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` Dans les cas où une collection hérite de champs d'une collection parente, il est généralement plus facile d'utiliser la propriété `ignore` de l'annotation `@Collection`: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` Si une collection contient un champ dont le type n'est pas supporté par Isar, vous devez ignorer ce champ. :::warning Gardez en tête qu'il n'est pas recommandé de stocker des informations dans des objets Isar qui ne sont pas persistants. ::: ## Types supportés Isar supporte les types de données suivants : - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` De plus, les objets embarqués (`embedded`) et les enums sont supportés. Nous les aborderons ci-dessous. ## byte, short, float Pour de nombreux cas d'utilisation, vous n'avez pas besoin de l'étendue complète d'un nombre entier ou double de 64 bits. Isar supporte des types supplémentaires qui vous permettent d'économiser de l'espace et de la mémoire lorsque vous stockez des nombres plus petits. | Type | Size in bytes | Range | | ---------- | ------------- | ------------------------------------------------------ | | **byte** | 1 | 0 à 255 | | **short** | 4 | -2,147,483,647 à 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 à 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 à 3.4e38 | | **double** | 8 | -1.7e308 à 1.7e308 | Les types supplémentaires sont simplement des alias pour les types natifs de Dart, donc utiliser `short`, par exemple, fonctionne de la même manière que `int`. Voici un exemple de collection contenant les types décrit ci-dessus: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` Tous les types de nombres peuvent également être utilisés dans des listes. Pour stocker des octets (`bytes`), vous devriez utiliser `List`. ## Types nullables Il est essentiel de comprendre comment la nullité fonctionne dans Isar: Les types de nombres n'ont **PAS** de représentation `null` dédiée. À la place, une valeur spécifique est utilisée: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** | `int.MIN` | | **float** | `double.NaN` | | **double** | `double.NaN` | `bool`, `String`, et `List` ont une représentation `null` séparée. Ce comportement permet d'améliorer les performances, et il vous permet de modifier librement la nullité de vos champs sans nécessiter de migration ou de code spécial pour gérer les valeurs "nulles". :::warning Le type `byte` ne supporte pas les valeurs nulles. ::: ## DateTime Isar ne stocke pas les informations de fuseau horaire de vos dates. À la place, il les convertit en UTC avant de les stocker. Isar retourne toutes les dates en heure locale. Les `DateTime` sont stockés avec une précision de l'ordre de la microseconde. Dans les navigateurs, seule la précision de la milliseconde est supportée en raison des limitations de JavaScript. ## Enum Isar permet de stocker et d'utiliser les enums comme tous les autres types Isar. Vous devez cependant choisir comment Isar doit représenter l'enum sur disque. Isar supporte quatre stratégies différentes : | EnumType | Description | | ----------- | ------------------------------------------------------------------------------------------------ | | `ordinal` | L'index de l'enum est stocké comme `byte`. Très efficace mais ne permet pas les enums nullables. | | `ordinal32` | L'index de l'enum est stocké comme `short` (entier de 4 octets). | | `name` | Le nom de l'enum est stocké comme `String`. | | `value` | Une propriété personnalisée est utilisée pour récupérer la valeur de l'enum. | :::warning `ordinal` et `ordinal32` dépendent de l'ordre des valeurs de l'enum. Si vous changez l'ordre, les bases de données existantes renverront des valeurs incorrectes. ::: Voici un exemple pour chaque stratégie: ```dart @collection class EnumCollection { Id? id; @enumerated // Même chose que EnumType.ordinal late TestEnum byteIndex; // Ne peut pas être nulle @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // Ne peut pas être nulle @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Bien entendu, les enums peuvent également être utilisés dans des listes. ## Objets embarqués Il est souvent utile d'avoir des objets imbriqués dans votre modèle de collection. Il n'y a pas de limite à la profondeur à laquelle vous pouvez imbriquer des objets. Gardez cependant à l'esprit que la mise à jour d'un objet profondément imbriqué nécessitera l'écriture de l'arbre d'objets complet dans la base de données. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Les objets embarqués peuvent être nullables et hériter d'autres objets. La seule condition est qu'ils soient annotés avec `@embedded` et qu'ils aient un constructeur par défaut sans paramètres requis. ================================================ FILE: docs/docs/fr/transactions.md ================================================ --- title: Transactions --- # Transactions Dans Isar, les transactions combinent plusieurs opérations de base de données en une seule unité de travail. La plupart des interactions avec Isar utilisent implicitement des transactions. L'accès en lecture et en écriture dans Isar est conforme à la norme [ACID](http://en.wikipedia.org/wiki/ACID). Les transactions sont automatiquement annulées en cas d'erreur. ## Transactions explicites Dans une transaction explicite, vous obtenez un instantané cohérent de la base de données. Essayez de minimiser la durée des transactions. Il est interdit d'effectuer des appels réseau ou d'autres opérations de longue durée dans une transaction. Les transactions (en particulier les transactions d'écriture) ont un coût, et nous devrions toujours essayer de regrouper les opérations successives en une seule transaction. Les transactions peuvent être soit synchrones ou asynchrones. Dans les transactions synchrones, nous ne pouvons utiliser que les opérations synchrones. Dans les transactions asynchrones, uniquement les opérations asynchrones. | | Lecture | Lecture et écriture | |-------------|--------------|---------------------| | Synchrones | `.txnSync()` | `.writeTxnSync()` | | Asynchrones | `.txn()` | `.writeTxn()` | ### Transactions de lecture Les transactions de lecture explicites sont facultatives, mais elles nous permettent d'effectuer des lectures atomiques et de compter sur un état cohérent de la base de données à l'intérieur de la transaction. À l'interne, Isar utilise toujours des transactions de lecture implicites pour toutes les opérations de lecture. :::tip Les transactions de lecture asynchrones s'exécutent en parallèle avec d'autres transactions de lecture et d'écriture. Plutôt cool, non? ::: ### Transactions d'écriture Contrairement aux opérations de lecture, les opérations d'écriture dans Isar doivent être enveloppées dans une transaction explicite. Lorsqu'une transaction d'écriture se termine avec succès, elle est automatiquement validée et toutes les modifications sont écrites sur disque. Si une erreur se produit, la transaction est abandonnée et toutes les modifications sont annulées. Les transactions sont "tout ou rien": soit toutes les écritures d'une transaction réussissent, soit aucune d'entre elles ne prend effet pour garantir la cohérence des données. :::warning Lorsqu'une opération de base de données échoue, la transaction est interrompue et ne doit plus être utilisée. Même si vous attrapez l'erreur dans Dart. ::: ```dart @collection class Contact { Id? id; String? name; } // BON await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // MAUVAIS : déplacer la boucle à l'intérieur de la transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/fr/tutorials/quickstart.md ================================================ --- title: Démarrage rapide --- # Démarrage rapide Vous revoilà! Commençons à utiliser la base de données Flutter la plus cool qui soit... Nous allons être brefs en mots et rapides en code dans ce démarrage rapide. ## 1. Ajout des dépendances Avant de débuter, nous devons ajouter quelques dépendances au fichier `pubspec.yaml`. Nous pouvons utiliser la commande `pub` pour faire le gros du travail à notre place. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Annotation de classes Annotez vos classes de collection avec `@collection` et choisissez un champ `Id`. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // Vous pouvez aussi utiliser id = null pour l'auto incrémentation String? name; int? age; } ``` Les Ids identifient de manière unique les objets d'une collection et vous permettent de les retrouver ultérieurement. ## 3. Exécuter le générateur de code Exécutez la commande suivante pour démarrer le `build_runner`: ```sh dart run build_runner build ``` Si vous utilisez Flutter: ```sh flutter pub run build_runner build ``` ## 4. Ouverture l'instance Isar Ouvrez une nouvelle instance d'Isar et passez tous vos schémas de collection. En option, vous pouvez spécifier un nom d'instance et un dossier. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Écriture et lecture Une fois que votre instance est ouverte, vous pouvez commencer à utiliser les collections. Toutes les opérations CRUD de base sont disponibles via `IsarCollection`. ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // Insertion & modification }); final existingUser = await isar.users.get(newUser.id); // Obtention await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // Suppression }); ``` ## Autre ressources Vous apprenez mieux visuellement ? Regardez ces vidéos pour commencer avec Isar:


================================================ FILE: docs/docs/fr/watchers.md ================================================ --- title: Observateurs --- # Observateurs Isar nous permet de nous abonner aux changements dans la base de données. Nous pouvons "observer" les modifications apportées à un objet spécifique, à une collection entière ou à une requête. Les observateurs (`Watchers`) nous permettent de réagir efficacement aux changements dans la base de données. Nous pouvons par exemple reconstruire une interface utilisateur lorsqu'un contact est ajouté, envoyer une requête réseau lorsqu'un document est mis à jour, etc. Un observateur est notifié lorsqu'une transaction est validée avec succès et que la cible est réellement modifiée. ## Observation d'objets Si nous voulons être notifié lorsqu'un objet spécifique est créé, mis à jour ou supprimé, nous devons observer un objet: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` Comme nous pouvons le voir dans l'exemple ci-dessus, l'objet ne doit pas encore exister. L'observateur sera notifié lorsqu'il sera créé. Il existe un paramètre supplémentaire, `fireImmediately`. Si nous le mettons à `true`, Isar ajoutera immédiatement la valeur courante de l'objet au flux. ### Observation paresseuse Peut-être n'avez-vous pas besoin de recevoir la nouvelle valeur, mais seulement d'être notifié du changement? Cela évite à Isar d'avoir à aller chercher l'objet: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## Observation de collections Au lieu d'observer un seul objet, nous pouvons observer une collection entière et être notifié lorsqu'un objet est ajouté, mis à jour ou supprimé: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## Observation de requêtes Il est même possible d'observer des requêtes entières. Isar fait de son possible pour nous notifier uniquement lorsque les résultats de la requête changent réellement. Nous ne serons pas notifiés si des liens entraînent une modification de la requête. Utilisez un observateur de collection si vous avez besoin d'être informé des changements de liens. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning Si vous utilisez des requêtes `offset`, `limit` ou `distinct`, Isar vous notifiera même si les changements y sont en dehors. ::: Tout comme `watchObject()`, nous pouvons utiliser `watchLazy()` pour être notifié lorsque les résultats de la requête changent, mais ne pas aller les chercher. :::danger Relancer les requêtes à chaque modification est très inefficace. Il serait préférable d'utiliser un observateur de collection paresseux (`lazy`) à la place. ::: ================================================ FILE: docs/docs/indexes.md ================================================ --- title: Indexes --- # Indexes Indexes are Isar's most powerful feature. Many embedded databases offer "normal" indexes (if at all), but Isar also has composite and multi-entry indexes. Understanding how indexes work is essential to optimize query performance. Isar lets you choose which index you want to use and how you want to use it. We'll start with a quick introduction to what indexes are. ## What are indexes? When a collection is unindexed, the order of the rows will likely not be discernible by the query as optimized in any way, and your query will therefore have to search through the objects linearly. In other words, the query will have to search through every object to find the ones matching the conditions. As you can imagine, that can take some time. Looking through every single object is not very efficient. For example, this `Product` collection is entirely unordered. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Data:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | A query that tries to find all products that cost more than €30 has to search through all nine rows. That's not an issue for nine rows, but it might become a problem for 100k rows. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` To improve the performance of this query, we index the `price` property. An index is like a sorted lookup table: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Generated index:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Now, the query can be executed a lot faster. The executor can directly jump to the last three index rows and find the corresponding objects by their id. ### Sorting Another cool thing: indexes can do super fast sorting. Sorted queries are costly because the database has to load all results in memory before sorting them. Even if you specify an offset or limit, they are applied after sorting. Let's imagine we want to find the four cheapest products. We could use the following query: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` In this example, the database would have to load all (!) objects, sort them by price, and return the four products with the lowest price. As you can probably imagine, this can be done much more efficiently with the previous index. The database takes the first four rows of the index and returns the corresponding objects since they are already in the correct order. To use the index for sorting, we would write the query like this: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` The `.anyX()` where clause tells Isar to use an index just for sorting. You can also use a where clause like `.priceGreaterThan()` and get sorted results. ## Unique indexes A unique index ensures the index does not contain any duplicate values. It may consist of one or multiple properties. If a unique index has one property, the values in this property will be unique. If the unique index has more than one property, the combination of values in these properties is unique. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Any attempt to insert or update data into the unique index that causes a duplicate will result in an error: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // try to insert user with same username await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Replace indexes It is sometimes not preferable to throw an error if a unique constraint is violated. Instead, you may want to replace the existing object with the new one. This can be achieved by setting the `replace` property of the index to `true`. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Now when we try to insert a user with an existing username, Isar will replace the existing user with the new one. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Replace indexes also generate `putBy()` methods that allow you to update objects instead of replacing them. The existing id is reused, and links are still populated. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user does not exist so this is the same as put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` As you can see, the id of the first inserted user is reused. ## Case-insensitive indexes All indexes on `String` and `List` properties are case-sensitive by default. If you want to create a case-insensitive index, you can use the `caseSensitive` option: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Index type There are different types of indexes. Most of the time, you'll want to use an `IndexType.value` index, but hash indexes are more efficient. ### Value index Value indexes are the default type and the only one allowed for all properties that don't hold Strings or Lists. Property values are used to build the index. In the case of lists, the elements of the list are used. It is the most flexible but also space-consuming of the three index types. :::tip Use `IndexType.value` for primitives, Strings where you need `startsWith()` where clauses, and Lists if you want to search for individual elements. ::: ### Hash index Strings and Lists can be hashed to reduce the storage required by the index significantly. The disadvantage of hash indexes is that they can't be used for prefix scans (`startsWith` where clauses). :::tip Use `IndexType.hash` for Strings and Lists if you don't need `startsWith`, and `elementEqualTo` where clauses. ::: ### HashElements index String lists can be hashed as a whole (using `IndexType.hash`), or the elements of the list can be hashed separately (using `IndexType.hashElements`), effectively creating a multi-entry index with hashed elements. :::tip Use `IndexType.hashElements` for `List` where you need `elementEqualTo` where clauses. ::: ## Composite indexes A composite index is an index on multiple properties. Isar allows you to create composite indexes of up to three properties. Composite indexes are also known as multiple-column indexes. It's probably best to start with an example. We create a person collection and define a composite index on the age and name properties: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Data:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Generated index:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | The generated composite index contains all persons sorted by their age their name. Composite indexes are great if you want to create efficient queries sorted by multiple properties. They also enable advanced where clauses with multiple properties: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` The last property of a composite index also supports conditions like `startsWith()` or `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Multi-entry indexes If you index a list using `IndexType.value`, Isar will automatically create a multi-entry index, and each item in the list is indexed toward the object. It works for all types of lists. Practical applications for multi-entry indexes include indexing a list of tags or creating a full-text index. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` splits a string into words according to the [Unicode Annex #29](https://unicode.org/reports/tr29/) specification, so it works for almost all languages correctly. **Data:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Entries with duplicate words only appear once in the index. **Generated index:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | This index can now be used for prefix (or equality) where clauses of the individual words of the description. :::tip Instead of storing the words directly, also consider using the result of a [phonetic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) like [Soundex](https://en.wikipedia.org/wiki/Soundex). ::: ================================================ FILE: docs/docs/it/README.md ================================================ --- home: true title: Home heroImage: /isar.svg actions: - text: Iniziamo! link: /it/tutorials/quickstart.html type: primary features: - title: 💙 Creato per Flutter details: Setup minimo, facile da usare, nessuna configurazione, niente codice boilerplate. Basta aggiungere poche linee di codice per iniziare. - title: 🚀 Altamente scalabile details: Salva centinaia di migliaia di records in un singolo NoSQL database ed interrogalo in maniera efficiente ed asincrona. - title: 🍭 Ricco di funzionalità details: Isa ha un insieme ricco di funzionalità per aiutare nella gestione dei tuoi dati. Indici composti & multi-entry, modificatori di query, supporto al JSON, e molto altro. - title: 🔎 Ricerca full-text details: Isar ha un sistema di ricerca built-in basato su full-text. Crea un inidice multi-entry e ricerca i record facilmente. - title: 🧪 Semantica ACID details: Isar è conforme con ACID e gestisce automaticamente le transazioni. In caso di errore effettua roll-back automaticamente. - title: 💃 Staticamente tipizzato details: Le query Isar sono tipizzate staticamente e controllate in fase di compilazione. Non è necessario preoccuparsi degli errori di runtime. - title: 📱 Multipiattaform details: iOS, Android, Desktop e PIENO SUPPORTO AL WEB! - title: ⏱ Asincrono details: Operazioni di query parallele e supporto per isolamento multiplo pronto all'uso - title: 🦄 Open Source details: Tutto è open source e gratuito per sempre! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/it/crud.md ================================================ --- title: Create, Read, Update, Delete --- # Create, Read, Update, Delete Quando hai definito le tue collezioni, impara a manipolarle! ## Apertura di Isar Prima che tu possa fare qualsiasi cosa, abbiamo bisogno di un'istanza Isar. Ogni istanza richiede una directory con autorizzazione di scrittura in cui è possibile archiviare il file di database. Se non specifichi una directory, Isar troverà una directory predefinita adatta per la piattaforma corrente. Fornisci tutti gli schemi che desideri utilizzare con l'istanza Isar. Se apri più istanze, devi comunque fornire gli stessi schemi a ciascuna istanza. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [ContactSchema], directory: dir.path, ); ``` È possibile utilizzare la configurazione predefinita o fornire alcuni dei seguenti parametri: | Config. | Descrizione | | -------| -------------| | `name` | Apri più istanze con nomi distinti. Per impostazione predefinita, viene utilizzato `"predefinito"`. | | `directory` | Il percorso di archiviazione per questa istanza. Puoi passare un percorso relativo o assoluto. Per impostazione predefinita, `NSDocumentDirectory` viene utilizzato per iOS e `getDataDirectory` per Android. Non richiesto per il web. | | `relaxedDurability` | Rilassa la garanzia di durata per aumentare le prestazioni di scrittura. In caso di arresto anomalo del sistema (non arresto anomalo dell'app), è possibile perdere l'ultima transazione impegnata. La corruzione non è possibile | | `compactOnLaunch` | Condizioni per verificare se il database deve essere compattato all'apertura dell'istanza. | | `inspector` | Abilita l'Inspector per le build di debug. Per le build di profili e versioni questa opzione viene ignorata. | Se un'istanza è già aperta, la chiamata a `Isar.open()` fornirà l'istanza esistente indipendentemente dai parametri specificati. È utile per usare Isar in un isolate. :::tip Prendi in considerazione l'utilizzo del pacchetto [path_provider](https://pub.dev/packages/path_provider) per ottenere un percorso valido su tutte le piattaforme. ::: Il percorso di archiviazione del file di database è `directory/name.isar`. ## Lettura dal database Usa le istanze di `IsarCollection` per trovare, interrogare e creare nuovi oggetti di un determinato tipo in Isar. Per gli esempi seguenti, assumiamo di avere una raccolta "Ricetta" definita come segue: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Ottieni una raccolta Tutte le tue raccolte vivono nell'istanza Isar. Puoi ottenere la raccolta di ricette con: ```dart final recipes = isar.recipes; ``` È stato facile! Se non vuoi usare le funzioni di accesso alla raccolta, puoi anche usare il metodo `collection()`: ```dart final recipes = isar.collection(); ``` ### Ottieni un oggetto (per ID) Non abbiamo ancora dati nella raccolta, ma facciamo finta di farlo in modo da poter ottenere un oggetto immaginario con l'id `123` ```dart final recipe = await recipes.get(123); ``` `get()` restituisce un `Future` con l'oggetto o `null` se non esiste. Tutte le operazioni Isar sono asincrone per impostazione predefinita e la maggior parte di esse ha una controparte sincrona: ```dart final recipe = recipes.getSync(123); ``` :::warning Per impostazione predefinita, dovresti utilizzare la versione asincrona dei metodi nell'isolato dell'interfaccia utente. Poiché Isar è molto veloce, è spesso accettabile utilizzare la versione sincrona. ::: Se vuoi ottenere più oggetti contemporaneamente, usa `getAll()` o `getAllSync()`: ```dart final recipe = await recipes.getAll([1, 2]); ``` ### Interroga gli oggetti Invece di ottenere oggetti per id puoi anche interrogare un elenco di oggetti che soddisfano determinate condizioni usando `.where()` e `.filter()`: ```dart final allRecipes = await recipes.where().findAll(); final favouires = await recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ Scopri di più: [Queries](queries) ## Modifica del database È finalmente arrivato il momento di modificare la nostra collezione! Per creare, aggiornare o eliminare oggetti, utilizzare le rispettive operazioni racchiuse in una transazione di scrittura: ```dart await isar.writeTxn(() async { final recipe = await recipes.get(123) recipe.isFavorite = false; await recipes.put(recipe); // perform update operations await recipes.delete(123); // or delete operations }); ``` ➡️ Scopri di più: [Transactions](transactions) ### Inserimento Per rendere persistente un oggetto in Isar, inserirlo in una collezione. Il metodo `put()` di Isar inserirà o aggiornerà l'oggetto a seconda che esista già nella raccolta. Se il campo id è `null` o `Isar.autoIncrement`, Isar utilizzerà un id di incremento automatico. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await recipes.put(pancakes); }) ``` Isar assegnerà automaticamente l'id all'oggetto se il campo `id` non è definitivo. Inserire più oggetti contemporaneamente è altrettanto facile: ```dart await isar.writeTxn(() async { await recipes.putAll([pancakes, pizza]); }) ``` ### Aggiornamento Sia la creazione che l'aggiornamento funzionano con `collection.put(object)`. Se l'id è `null` (o non esiste), l'oggetto viene inserito; in caso contrario, viene aggiornato. Quindi, se vogliamo eliminare i nostri pancake dai preferiti, possiamo fare quanto segue: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await recipes.put(recipe); }); ``` ### Eliminazione Vuoi sbarazzarti di un oggetto in Isar? Usa `collection.delete(id)`. Il metodo delete restituisce se un oggetto con l'ID specificato è stato trovato ed eliminato. Se vuoi eliminare l'oggetto con id `123`, ad esempio, puoi fare: ```dart await isar.writeTxn(() async { final success = await recipes.delete(123); print('Recipe deleted: $success'); }); ``` Allo stesso modo per get e put, esiste anche un'operazione di eliminazione in blocco che restituisce il numero di oggetti eliminati: ```dart await isar.writeTxn(() async { final count = await recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` Se non conosci gli ID degli oggetti che desideri eliminare, puoi utilizzare una query: ```dart await isar.writeTxn(() async { final count = await recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/it/faq.md ================================================ --- title: FAQ --- # Domande frequenti Una raccolta casuale di domande frequenti sui database Isar e Flutter. ### Perché ho bisogno di un database? > Conservo i miei dati in un database back-end, perché ho bisogno di Isar?. Ancora oggi è molto comune non avere connessione dati se sei in metropolitana o in aereo o se vai a trovare tua nonna, che non ha WiFi e un segnale cellulare pessimo. Non dovresti lasciare che una cattiva connessione paralizzi la tua app! ### Isar vs Hive La risposta è semplice: Isar è stato [nato come sostituto di Hive](https://github.com/hivedb/hive/issues/246) e ora si trova in uno stato in cui consiglio di utilizzare sempre Isar invece di Hive. ### Dove sono le clausole?! > Perché **_Io_** devo scegliere quale indice utilizzare? Ci sono più ragioni. Molti database utilizzano l'euristica per scegliere l'indice migliore per una determinata query. Il database deve raccogliere dati di utilizzo aggiuntivi (-> sovraccarico) e potrebbe comunque scegliere l'indice errato. Inoltre, rende più lenta la creazione di una query. Nessuno conosce i tuoi dati meglio di te, lo sviluppatore. Quindi puoi scegliere l'indice ottimale e decidere, ad esempio, se desideri utilizzare un indice per eseguire query o ordinare. ### Devo usare gli indici oppure le clausole where? No! Molto probabilmente Isar è abbastanza veloce se ti affidi solo ai filtri. ### Isar è abbastanza veloce? Isar è tra i database più veloci per dispositivi mobili, quindi dovrebbe essere abbastanza veloce per la maggior parte dei casi d'uso. Se riscontri problemi di prestazioni, è probabile che tu stia facendo qualcosa di sbagliato. ### Isar aumenta le dimensioni della mia app? Un po', sì. Isar aumenterà la dimensione del download della tua app di circa 1 - 1,5 MB. Isar Web aggiunge solo pochi KB. ### La documentazione non è corretta / c'è un errore di battitura. Oh no, mi dispiace. Per favore [apri un problema](https://github.com/isar-community/isar/issues/new/choose) o, ancora meglio, un PR per risolverlo 💪. ================================================ FILE: docs/docs/it/indexes.md ================================================ --- title: Indici --- # Indici Gli indici sono la caratteristica più potente di Isar. Molti database incorporati offrono indici "normali" (se non del tutto), ma Isar ha anche indici compositi e multi-voce. Comprendere il funzionamento degli indici è essenziale per ottimizzare le prestazioni delle query. Isar ti permette di scegliere quale indice vuoi usare e come usarlo. Inizieremo con una rapida introduzione a cosa sono gli indici. ## Cosa sono gli indici? Quando una raccolta non è indicizzata, è probabile che l'ordine delle righe non sia distinguibile dalla query in quanto non ottimizzato e la query dovrà quindi cercare tra gli oggetti in maniera lineare. In altre parole, la query dovrà cercare in ogni oggetto per trovare quelli che soddisfano le condizioni. Come puoi immaginare, può volerci del tempo. Guardare attraverso ogni singolo oggetto non è molto efficiente. Ad esempio, questa raccolta `Product` non è ordinata. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Dati:** | id | nome | prezzo | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | Una query che tenti di trovare tutti i prodotti che costano più di € 30 deve cercare in tutte e nove le righe. Questo non è un problema per nove righe, ma potrebbe diventare un problema per 100.000 righe. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` Per migliorare le prestazioni di questa query, indicizziamo la proprietà `price`. Un indice è come una tabella di ricerca ordinata: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Indici generati:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Ora, la query può essere eseguita molto più velocemente. L'esecutore può saltare direttamente alle ultime tre righe dell'indice e trovare gli oggetti corrispondenti in base al loro ID. ### Ordinamento Un'altra cosa interessante: gli indici possono eseguire un ordinamento super veloce. Le query ordinate sono costose perché il database deve caricare tutti i risultati in memoria prima di ordinarli. Anche se si specifica un offset o un limite, vengono applicati dopo l'ordinamento. Immaginiamo di voler trovare i quattro prodotti più economici. Potremmo usare la seguente query: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` In questo esempio, il database dovrebbe caricare tutti (!) gli oggetti, ordinarli per prezzo e restituire i quattro prodotti con il prezzo più basso. Come probabilmente puoi immaginare, questo può essere fatto in modo molto più efficiente con l'indice precedente. Il database prende le prime quattro righe dell'indice e restituisce gli oggetti corrispondenti poiché sono già nell'ordine corretto. Per utilizzare l'indice per l'ordinamento, scriveremo la query in questo modo: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` La clausola `.anyX()` indica a Isar di usare un indice solo per l'ordinamento. Puoi anche usare una clausola where come `.priceGreaterThan()` e ottenere risultati ordinati. ## Indici univoci Un indice univoco garantisce che l'indice non contenga valori duplicati. Può essere costituito da una o più proprietà. Se un indice univoco ha una proprietà, i valori in questa proprietà saranno univoci. Se l'indice univoco ha più di una proprietà, la combinazione di valori in queste proprietà è univoca. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Qualsiasi tentativo di inserire o aggiornare i dati nell'indice univoco che causa un duplicato risulterà in un errore: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // try to insert user with same username await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Sostituisci gli indici A volte non è preferibile generare un errore se viene violato un vincolo univoco. Invece, potresti voler sostituire l'oggetto esistente con quello nuovo. Questo può essere ottenuto impostando la proprietà `replace` dell'indice su `true`. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Ora quando proviamo a inserire un utente con un nome utente esistente, Isar sostituirà l'utente esistente con quello nuovo. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Sostituire gli indici genera anche metodi `putBy()` che ti consentono di aggiornare gli oggetti invece di sostituirli. L'ID esistente viene riutilizzato e i collegamenti continuano a essere popolati. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user does not exist so this is the same as put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` Come puoi vedere, l'id del primo utente inserito viene riutilizzato. ## Indici senza distinzione tra maiuscole e minuscole Tutti gli indici sulle proprietà `String` e `List` fanno distinzione tra maiuscole e minuscole per impostazione predefinita. Se desideri creare un indice senza distinzione tra maiuscole e minuscole, puoi utilizzare l'opzione `caseSensitive`: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Tipo di indice Esistono diversi tipi di indici. La maggior parte delle volte, vorrai usare un indice `IndexType.value`, ma gli indici hash sono più efficienti. ### Indice di valore Gli indici di valore sono il tipo predefinito e l'unico consentito per tutte le proprietà che non contengono stringhe o elenchi. I valori delle proprietà vengono utilizzati per creare l'indice. Nel caso di elenchi, vengono utilizzati gli elementi dell'elenco. È il più flessibile ma anche dispendioso in termini di spazio dei tre tipi di indice. :::tip Usa `IndexType.value` per le primitive, String dove hai bisogno della clausole-where `startsWith()` e List se vuoi cercare singoli elementi. ::: ### Indice hash È possibile eseguire l'hashing di stringhe ed elenchi per ridurre significativamente lo spazio di archiviazione richiesto dall'indice. Lo svantaggio degli indici hash è che non possono essere usati per scansioni di prefissi (clausole-where `startsWith`). :::tip Usa `IndexType.hash` per stringhe ed elenchi se non hai bisogno di clausole-where `startsWith` e `elementEqualTo`. ::: ### Indice HashElements Gli elenchi di stringhe possono essere sottoposti a hash per intero (usando `IndexType.hash`), oppure gli elementi dell'elenco possono essere sottoposti a hash separatamente (usando `IndexType.hashElements`), creando in modo efficace un indice multi-voce con elementi hash. :::tip Usa `IndexType.hashElements` per `List` dove hai bisogno di clausole-where `elementEqualTo`. ::: ## Indici compositi Un indice composito è un indice su più proprietà. Isar consente di creare indici compositi fino a tre proprietà. Gli indici compositi sono anche noti come indici a più colonne. Probabilmente è meglio iniziare con un esempio. Creiamo una collezione di persone e definiamo un indice composito sulle proprietà di età e nome: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Dati:** | id | nome | età | città natale | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Indici generati:** | età | nome | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | L'indice composito generato contiene tutte le persone ordinate per età e nome. Gli indici compositi sono ottimi se desideri creare query efficienti ordinate in base a più proprietà. Consentono anche clausole dove avanzate con più proprietà: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` L'ultima proprietà di un indice composito supporta anche condizioni come `startsWith()` o `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Indici a più voci Se indicizzi una lista usando 'IndexType.value', Isar creerà automaticamente un indice multi-voce e ogni voce nella lista viene indicizzata verso l'oggetto. Funziona per tutti i tipi di liste. Le applicazioni pratiche per gli indici a voci multiple includono l'indicizzazione di un elenco di tag o la creazione di un indice full-text. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` divide una stringa in parole secondo la specifica [Unicode Annex #29](https://unicode.org/reports/tr29/), quindi funziona correttamente per quasi tutte le lingue. **Dati:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Le voci con parole duplicate vengono visualizzate solo una volta nell'indice. **Indici generati:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | Questo indice può ora essere usato per prefisso (o uguaglianza) dove clausole delle singole parole della descrizione. :::tip Invece di memorizzare direttamente le parole, considera anche l'utilizzo del risultato di un [algoritmo fonetico](https://en.wikipedia.org/wiki/Algoritmo_fonetico) come [Soundex](https://en.wikipedia.org/wiki/ Soundex). ::: ================================================ FILE: docs/docs/it/limitations.md ================================================ # Limitazioni Come sapete, Isar funziona su dispositivi mobili e desktop in esecuzione su VM oltre che su Web. Entrambe le piattaforme sono molto diverse e hanno limitazioni diverse. ## Limitazioni VM - Solo i primi 1024 byte di una stringa possono essere usati per un prefisso clausola-where - Gli oggetti possono avere una dimensione di soli 16 MB ## Limitazioni Web Poiché Isar Web si basa su IndexedDB, ci sono più limitazioni ma sono appena percettibili durante l'utilizzo di Isar. - I metodi sincroni non sono supportati - Attualmente, i filtri `Isar.splitWords()` e `.matches()` non sono ancora implementati - Le modifiche allo schema non vengono controllate rigorosamente come nella VM, quindi fai attenzione a rispettare le regole - Tutti i tipi di numeri sono memorizzati come double (l'unico tipo di numero js) quindi `@Size32` non ha alcun effetto - Gli indici sono rappresentati in modo diverso, quindi gli indici hash non utilizzano meno spazio (funzionano comunque allo stesso modo) - `col.delete()` e `col.deleteAll()` funzionano correttamente ma il valore restituito non è corretto - `col.clear()` non reimposta il valore di incremento automatico - `NaN` non è supportato come valore ================================================ FILE: docs/docs/it/links.md ================================================ --- title: Collegamenti --- # Collegamenti I collegamenti consentono di esprimere relazioni tra oggetti, come l'autore di un commento (Utente). Puoi modellare le relazioni `1:1`, `1:n` e `n:n` con i collegamenti Isar. L'uso dei collegamenti è meno ergonomico rispetto all'utilizzo di oggetti incorporati e dovresti utilizzare oggetti incorporati quando possibile. Pensa al collegamento come a una tabella separata che contiene la relazione. È simile alle relazioni SQL ma ha un set di funzionalità e un'API diversi. ## IsarLink `IsarLink` può contenere nessuno o un oggetto correlato e può essere utilizzato per esprimere una relazione a uno. `IsarLink` ha una singola proprietà chiamata `value` che contiene l'oggetto collegato. I collegamenti sono pigri, quindi è necessario dire a `IsarLink` di caricare o salvare il `valore` in modo esplicito. Puoi farlo chiamando `linkProperty.load()` e `linkProperty.save()`. :::tip La proprietà id delle raccolte di origine e di destinazione di un collegamento deve essere non definitiva. ::: Per i target non web, i link vengono caricati automaticamente quando li usi per la prima volta. Iniziamo aggiungendo un IsarLink a una collezione: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` Abbiamo definito un legame tra insegnanti e studenti. Ogni studente può avere esattamente un insegnante in questo esempio. Innanzitutto, creiamo l'insegnante e lo assegniamo a uno studente. Dobbiamo effettuare un `.put()` per l'insegnante e salvare il collegamento manualmente. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` Ora possiamo usare il link: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Proviamo la stessa cosa con il codice sincrono. Non è necessario salvare il collegamento manualmente perché `.putSync()` salva automaticamente tutti i collegamenti. Crea anche l'insegnante per noi. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks Avrebbe più senso se lo studente dell'esempio precedente potesse avere più insegnanti. Fortunatamente, Isar ha `IsarLinks`, che può contenere più oggetti correlati ed esprimere una relazione a molti. `IsarLinks` estende `Set` ed espone tutti i metodi consentiti per gli insiemi. `IsarLinks` si comporta in modo molto simile a `IsarLink` ed è anche pigro. Per caricare tutti gli oggetti collegati chiama `linkProperty.load()`. Per rendere persistenti le modifiche, chiama `linkProperty.save()`. Internamente sia "IsarLink" che "IsarLinks" sono rappresentati allo stesso modo. Possiamo aggiornare `IsarLink` da prima a un `IsarLinks` per assegnare più insegnanti a un singolo studente (senza perdere dati). ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Funziona perché non abbiamo cambiato il nome del collegamento (`teacher`), quindi Isar lo ricorda da prima. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlink Vi sento chiedere: "E se volessimo esprimere relazioni inverse?". Non preoccuparti; ora introdurremo i backlink. I backlink sono collegamenti nella direzione inversa. Ogni link ha sempre un backlink implicito. Puoi renderlo disponibile per la tua app annotando un `IsarLink` o `IsarLinks` con `@Backlink()`. I backlink non richiedono memoria o risorse aggiuntive; puoi aggiungerli, rimuoverli e rinominarli liberamente senza perdere dati. Vogliamo sapere quali studenti ha un insegnante specifico, quindi definiamo un backlink: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` Dobbiamo specificare il collegamento a cui punta il backlink. È possibile avere più collegamenti diversi tra due oggetti. ## Inizializza i collegamenti `IsarLink` e `IsarLinks` hanno un costruttore arg zero, che dovrebbe essere usato per assegnare la proprietà link quando l'oggetto viene creato. È buona norma rendere le proprietà del collegamento "finali". Quando `metti()` il tuo oggetto per la prima volta, il collegamento viene inizializzato con la raccolta di origine e destinazione e puoi chiamare metodi come `load()` e `save()`. Un collegamento inizia a tenere traccia delle modifiche subito dopo la sua creazione, quindi puoi aggiungere e rimuovere relazioni anche prima che il collegamento venga inizializzato. :::danger È vietato spostare un collegamento a un altro oggetto. ::: ================================================ FILE: docs/docs/it/queries.md ================================================ --- title: Query --- # Query La query è il modo in cui trovi i record che soddisfano determinate condizioni, ad esempio: - Trova tutti i contatti speciali - Trova nomi distinti nei contatti - Elimina tutti i contatti che non hanno il cognome definito Poiché le query vengono eseguite sul database e non in Dart, sono molto veloci. Quando usi in modo intelligente gli indici, puoi migliorare ulteriormente le prestazioni delle query. Di seguito, imparerai come scrivere query e come renderle il più velocemente possibile. Esistono due diversi metodi per filtrare i record: i filtri e le clausole where. Inizieremo dando un'occhiata a come funzionano i filtri. ## Filtri I filtri sono facili da usare e da capire. A seconda del tipo di proprietà, sono disponibili diverse operazioni di filtro, la maggior parte delle quali ha nomi autoesplicativi. I filtri funzionano valutando un'espressione per ogni oggetto della raccolta che viene filtrata. Se l'espressione si risolve in `true`, Isar include l'oggetto nei risultati. I filtri non influiscono sull'ordine dei risultati. Utilizzeremo il seguente modello per gli esempi seguenti: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Condizioni di query A seconda del tipo di campo, sono disponibili diverse condizioni. | Condizione | Descrizione | | ----------| ------------| | `.equalTo(value)` | Corrisponde a valori uguali al `value` specificato. | | `.between(lower, upper)` | Corrisponde ai valori compresi tra `lower` and `upper`. | | `.greaterThan(bound)` | Corrisponde a valori maggiori di `bound`. | | `.lessThan(bound)` | Corrisponde a valori inferiori a `bound`. I valori `null` verranno inclusi per impostazione predefinita perché `null` è considerato inferiore a qualsiasi altro valore. | | `.isNull()` | Corrisponde a valori `null'.| | `.isNotNull()` | Corrisponde a valori che non sono `null'.| | `.length()` | Le query su List, String e lunghezza del collegamento filtrano gli oggetti in base al numero di elementi in un elenco o in un collegamento. | Supponiamo che il database contenga quattro scarpe con le taglie 39, 40, 46 e una con una taglia non impostata (`null`). A meno che non si esegua l'ordinamento, i valori verranno restituiti ordinati per id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Operatori logici È possibile comporre predicati utilizzando i seguenti operatori logici: | Operatore | Descrizione | | ---------- | ----------- | | `.and()` | Valuta come `true` se entrambe le espressioni della lato sinistro e della lato destro restituiscono `true`. | | `.or()` | Valuta come `true` se una delle espressioni restituisce `true`. | | `.xor()` | Valuta come `true` se esattamente un'espressione restituisce `true`. | | `.not()` | Nega il risultato della seguente espressione. | | `.group()` | Raggruppa le condizioni e consente di specificare l'ordine di valutazione. | Se vuoi trovare tutte le scarpe nella taglia 46, puoi utilizzare la seguente query: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` Se vuoi usare più di una condizione, puoi combinare più filtri usando **and** logico `.and()`, **or** logico `.or()` e **xor** logico `. xor()`. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filters are implicitly combined with logical and. .isUnisexEqualTo(true) .findAll(); ``` Questa query equivale a: `size == 46 && isUnisex == true`. Puoi anche raggruppare le condizioni usando `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` Questa query equivale a `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. Per negare una condizione o un gruppo, usa la logica **not** `.not()`: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` Questa query equivale a `size != 46 && isUnisex != true`. ### Condizioni di stringa Oltre alle condizioni di query precedenti, i valori String offrono alcune condizioni in più che puoi utilizzare. I caratteri jolly simili a Regex, ad esempio, consentono una maggiore flessibilità nella ricerca. | Condizione | Descrizione | | -------------------- | ----------------------------------------------------------------- | | `.startsWith(value)` | Corrisponde ai valori di stringa che iniziano con il `valore` fornito. | | `.contains(value)` | Corrisponde ai valori di stringa che contengono il `valore` fornito. | | `.endsWith(value)` | Corrisponde ai valori di stringa che terminano con il `valore` fornito. | | `.matches(wildcard)` | Corrisponde ai valori di stringa che corrispondono al modello `jolly` fornito. | **Maiuscole/minuscole** Tutte le operazioni sulle stringhe hanno un parametro `caseSensitive` opzionale che per impostazione predefinita è `true`. **Wildcards:** **Caratteri jolly:** Una [espressione di stringa con caratteri jolly](https://en.wikipedia.org/wiki/Wildcard_character) è una stringa che utilizza caratteri normali con due caratteri jolly speciali: - Il carattere jolly `*` corrisponde a zero o più caratteri - Il carattere jolly `?` corrisponde a qualsiasi carattere. Ad esempio, la stringa di caratteri jolly `"d?g"` corrisponde a `"dog"`, `"dig"` e `"dug"`, ma non a `"ding"`, `"dg"` o `" un cane"`. ### Modificatori di query A volte è necessario creare una query in base ad alcune condizioni o per valori diversi. Isar ha uno strumento molto potente per la creazione di query condizionali: | Modificatore | Descrizione | | --------------------- | ---------------------------------------------------- | | `.optional(cond, qb)` | Estende la query solo se la `condition` è `true`. Questo può essere utilizzato quasi ovunque in una query, ad esempio per ordinarlo o limitarlo in modo condizionale. | | `.anyOf(list, qb)` | Estende la query per ogni valore in `values` e combina le condizioni utilizzando la logica **or**. | | `.allOf(list, qb)` | Estende la query per ogni valore in `values` e combina le condizioni utilizzando **and** logici. | In questo esempio, costruiamo un metodo in grado di trovare scarpe con un filtro opzionale: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // only apply filter if sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` Se vuoi trovare tutte le scarpe che hanno una di più misure di scarpe, puoi scrivere una query convenzionale o utilizzare il modificatore `anyOf()`: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` I modificatori di query sono particolarmente utili quando si desidera creare query dinamiche. ### Liste Si possono interrogare anche le liste: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` È possibile eseguire query in base alla lunghezza della lista: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` Questi sono equivalenti al codice Dart `tweets.where((t) => t.hashtags.isEmpty);` e `tweets.where((t) => t.hashtags.length > 5);`. Puoi anche interrogare in base agli elementi dell'elenco: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` Questo equivale al codice Dart `tweets.where((t) => t.hashtags.contains('flutter'));`. ### Oggetti incorporati Gli oggetti incorporati sono una delle funzionalità più utili di Isar. Possono essere interrogati in modo molto efficiente utilizzando le stesse condizioni disponibili per gli oggetti di livello superiore. Supponiamo di avere il seguente modello: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` Vogliamo interrogare tutte le auto che hanno un marchio con il nome `"BMW"` e il paese `"Germania"`. Possiamo farlo usando la seguente query: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Cerca sempre di raggruppare le query nidificate. La query precedente è più efficiente della seguente. Anche se il risultato è lo stesso: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Collegamenti Se il tuo modello contiene [link o backlink](links) puoi filtrare la tua query in base agli oggetti collegati o al numero di oggetti collegati. :::warning Tieni presente che le query di collegamento possono essere costose perché Isar ha bisogno di cercare oggetti collegati. Considera invece l'utilizzo di oggetti incorporati. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Vogliamo trovare tutti gli studenti che hanno un insegnante di matematica o inglese: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` I filtri di collegamento restituiscono `true` se almeno un oggetto collegato soddisfa le condizioni. Cerchiamo tutti gli studenti che non hanno insegnanti: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` o in alternativa: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Clausole Where Le clausole where sono uno strumento molto potente, ma può essere un po' difficile metterle in pratica. A differenza dei filtri le clausole where utilizzano gli indici definiti nello schema per verificare le condizioni della query. Interrogare un indice è molto più veloce che filtrare ogni record individualmente. ➡️ Scopri di più: [Indici](indexes) :::tip Come regola di base, dovresti sempre cercare di ridurre il più possibile i record usando le clausole where e fare il filtraggio rimanente usando i filtri. ::: Puoi combinare solo le clausole where usando **or** logici. In altre parole, puoi sommare più clausole where insieme, ma non puoi interrogare l'intersezione di più clausole where. Aggiungiamo gli indici alla collezione di scarpe: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` Ci sono due indici. L'indice su `size` ci permette di usare clausole where come `.sizeEqualTo()`. L'indice composito su `isUnisex` consente dove clausole come `isUnisexSizeEqualTo()`. Ma anche `isUnisexEqualTo()` perché puoi sempre usare qualsiasi prefisso di un indice. Ora possiamo riscrivere la query precedente che trova scarpe unisex della taglia 46 utilizzando l'indice composito. Questa query sarà molto più veloce della precedente: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Le clausole where hanno altri due superpoteri: ti danno l'ordinamento "gratuito" e un'operazione distinta super veloce. ### Combinare clausole where e filtri Ricordi le query `shoes.filter()`? In realtà è solo una scorciatoia per `shoes.where().filter()`. Puoi (e dovresti) combinare dove clausole e filtri nella stessa query per utilizzare i vantaggi di entrambi: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` La clausola where viene applicata per prima per ridurre il numero di oggetti da filtrare. Quindi il filtro viene applicato agli oggetti rimanenti. ## Ordinamento È possibile definire come ordinare i risultati durante l'esecuzione della query utilizzando i metodi `.sortBy()`, `.sortByDesc()`, `.thenBy()` e `.thenByDesc()`. Per trovare tutte le scarpe ordinate per nome del modello in ordine crescente e taglia in ordine decrescente senza utilizzare un indice: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Ordinare molti risultati può essere costoso, soprattutto perché l'ordinamento avviene prima dell'offset e del limit. I metodi di ordinamento sopra non fanno mai uso di indici. Fortunatamente, possiamo di nuovo utilizzare l'ordinamento della clausola where e rendere la nostra query fulminea anche se dobbiamo ordinare un milione di oggetti. ### Ordinamento delle clausole where Se utilizzi una clausola **singola** nella query, i risultati sono già ordinati in base all'indice. Questo è un grosso problema! Supponiamo di avere scarpe nelle taglie `[43, 39, 48, 40, 42, 45]` e di voler trovare tutte le scarpe con una taglia maggiore di `42` e anche ordinarle per taglia: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // also sorts the results by size .findAll(); // -> [43, 45, 48] ``` Come puoi vedere, il risultato è ordinato in base all'indice `size`. Se vuoi invertire l'ordinamento della clausola where, puoi impostare `sort` su `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` A volte non si desidera utilizzare una clausola where ma comunque beneficiare dell'ordinamento implicito. Puoi usare la clausola `any` where: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` Se utilizzi un indice composto, i risultati vengono ordinati in base a tutti i campi dell'indice. :::tip Se hai bisogno che i risultati siano ordinati, considera l'utilizzo di un indice a tale scopo. Soprattutto se lavori con `offset()` e `limit()`. ::: A volte non è possibile o utile utilizzare un indice per l'ordinamento. In questi casi, dovresti utilizzare gli indici per ridurre il più possibile il numero di voci risultanti. ## Valori univoci Per restituire solo voci con valori univoci, utilizzare il predicato distinto. Ad esempio, per scoprire quanti diversi modelli di scarpe hai nel tuo database Isar: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` Puoi anche concatenare più condizioni distinte per trovare tutte le scarpe con combinazioni di taglia modello distinte: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Viene restituito solo il primo risultato di ogni combinazione distinta. È possibile utilizzare le clausole where e le operazioni di ordinamento per controllarlo. ### Clausola where distinta Se hai un indice non univoco, potresti voler ottenere tutti i suoi valori distinti. Potresti usare l'operazione `distinctBy` della sezione precedente, ma viene eseguita dopo l'ordinamento e i filtri, quindi c'è un po' di sovraccarico. Se utilizzi solo una singola clausola where, puoi invece fare affidamento sull'indice per eseguire l'operazione distinta. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip In teoria, potresti anche usare più clausole where per l'ordinamento e la distinzione. L'unica restrizione è per quelle clausole where che non si sovrappongono e utilizzano lo stesso indice. Per un corretto ordinamento, devono anche essere applicati in ordine di ordinamento. Stai molto attento se fai affidamento su questo! ::: ## Offset e limit Spesso è una buona idea limitare il numero di risultati di una query per le visualizzazioni lazy di liste. Puoi farlo impostando un `limit()`: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` Impostando un `offset()` puoi anche impaginare i risultati della tua query. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Poiché la creazione di un'istanza di oggetti Dart è spesso la parte più costosa dell'esecuzione di una query, è una buona idea caricare solo gli oggetti necessari. ## Ordine di esecuzione Isar esegue le query sempre nello stesso ordine: 1. Attraversa l'indice primario o secondario per trovare gli oggetti (applica le clausole where) 2. Filtra gli oggetti 3. Ordina i risultati 4. Applicare un'operazione distinta 5. Risultato offset e limite 6. Restituisci i risultati ## Operazioni di query Negli esempi precedenti, abbiamo usato `.findAll()` per recuperare tutti gli oggetti corrispondenti. Ci sono più operazioni disponibili, tuttavia: | Operazione | Descrizione | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | Recupera solo il primo oggetto corrispondente o `null` se nessuno corrisponde. | | `.findAll()` | Recupera tutti gli oggetti corrispondenti. | | `.count()` | Conta quanti oggetti corrispondono alla query. | | `.deleteFirst()` | Elimina il primo oggetto corrispondente dalla raccolta. | | `.deleteAll()` | Elimina tutti gli oggetti corrispondenti dalla raccolta. | | `.build()` | Compila la query per riutilizzarla in seguito. Ciò consente di risparmiare il costo per creare una query se si desidera eseguirla più volte. | ## Query sulla proprietà Se sei interessato solo ai valori di una singola proprietà, puoi utilizzare una query di proprietà. Basta creare una query normale e selezionare una proprietà: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` L'utilizzo di una sola proprietà consente di risparmiare tempo durante la deserializzazione. Le query sulle proprietà funzionano anche per gli oggetti e gli elenchi incorporati. ## Aggregazione Isar supporta l'aggregazione dei valori di una query di proprietà. Sono disponibili le seguenti operazioni di aggregazione: | Operazione | Descrizione | | ------------ | -------------------------------------------------------------- | | `.min()` | Trova il valore minimo o `null` se nessuno corrisponde. | | `.max()` | Trova il valore massimo o `null` se nessuno corrisponde. | | `.sum()` | Somma tutti i valori. | | `.average()` | Calcola la media di tutti i valori o 'NaN' se nessuno corrisponde. | L'utilizzo delle aggregazioni è molto più veloce rispetto alla ricerca di tutti gli oggetti corrispondenti e all'esecuzione manuale dell'aggregazione. ## Query dinamiche :::danger Questa sezione molto probabilmente non è rilevante per te. È sconsigliato utilizzare query dinamiche a meno che non sia assolutamente necessario (e raramente lo fai). ::: Tutti gli esempi precedenti hanno utilizzato QueryBuilder e i metodi di estensione statica generati. Forse vuoi creare query dinamiche o un linguaggio di query personalizzato (come Isar Inspector). In tal caso, puoi usare il metodo `buildQuery()`: | Parametro | Descrizione | | --------------- | ------------------------------------------------------------------------------------------- | | `whereClauses` | Le clausole where della query. | | `whereDistinct` | Se le clausole where devono restituire valori distinti (utile solo per clausole where singole). | | `whereSort` | L'ordine di scorrimento delle clausole where (utile solo per le clausole where singole). | | `filter` | Il filtro da applicare ai risultati. | | `sortBy` | Un elenco di proprietà da ordinare. | | `distinctBy` | Un elenco di proprietà da distinguere. | | `offset` | L'offset dei risultati. | | `limit` | Il numero massimo di risultati da restituire. | | `property` | Se non-null, vengono restituiti solo i valori di questa proprietà. | Creiamo una query dinamica: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` La seguente query è equivalente: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/it/recipes/data_migration.md ================================================ --- title: Migrazione dei dati --- # Migrazione dei dati Isar migra automaticamente gli schemi del database se aggiungi o rimuovi raccolte, campi o indici. A volte potresti voler migrare anche i tuoi dati. Isar non offre una soluzione integrata perché imporrebbe restrizioni alle migrazioni arbitrarie. È facile implementare la logica di migrazione adatta alle tue esigenze. Vogliamo utilizzare una singola versione per l'intero database in questo esempio. Utilizziamo le preferenze condivise per archiviare la versione corrente e confrontarla con la versione a cui vogliamo migrare. Se le versioni non corrispondono, migriamo i dati e aggiorniamo la versione. :::tip Puoi anche assegnare a ciascuna raccolta la propria versione e migrarle individualmente. ::: Immagina di avere una raccolta di utenti con un campo di compleanno. Nella versione 2 della nostra app, abbiamo bisogno di un campo aggiuntivo per l'anno di nascita per interrogare gli utenti in base all'età. Versione 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Versione 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` Il problema è che i modelli utente esistenti avranno un campo `birthYear` vuoto perché non esisteva nella versione 1. Abbiamo bisogno di migrare i dati per impostare il campo `birthYear`. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // If the version is not set (new installation) or already 2, we do not need to migrate return; default: throw Exception('Unknown version: $currentVersion'); } // Update version await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // We paginate through the users to avoid loading all users into memory at once for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // We don't need to update anything since the birthYear getter is used await isar.users.putAll(users); }); } } ``` :::warning Se devi migrare molti dati, prendi in considerazione l'utilizzo di un isolamento in background per evitare sollecitazioni sul thread dell'interfaccia utente. ::: ================================================ FILE: docs/docs/it/recipes/full_text_search.md ================================================ --- title: Ricerca full-text --- # Ricerca full-text La ricerca full-text è un modo efficace per cercare il testo nel database. Dovresti già avere familiarità con il funzionamento degli [indici](../indexes.md), ma andiamo oltre le basi. Un indice funziona come una tabella di ricerca, consentendo al motore di query di trovare rapidamente i record con un determinato valore. Ad esempio, se hai un campo `title` nel tuo oggetto, puoi creare un indice su quel campo per rendere più veloce la ricerca di oggetti con un determinato titolo. ## Perché la ricerca full-text è utile? Puoi cercare facilmente il testo usando i filtri. Esistono varie operazioni sulle stringhe, ad esempio `.startsWith()`, `.contains()` e `.matches()`. Il problema con i filtri è che il loro runtime è `O(n)` dove `n` è il numero di record nella raccolta. Le operazioni sulle stringhe come `.matches()` sono particolarmente costose. :::tip La ricerca full-text è molto più veloce dei filtri, ma gli indici presentano alcune limitazioni. In questa ricetta, esploreremo come aggirare queste limitazioni. ::: ## Esempio di base L'idea è sempre la stessa: invece di indicizzare l'intero testo, indicizziamo le parole nel testo in modo da poterle cercare singolarmente. Creiamo l'indice full-text più semplice: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` Ora possiamo cercare messaggi con parole specifiche nel contenuto: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` Questa query è super veloce, ma ci sono alcuni problemi: 1. Possiamo cercare solo parole intere 2. Non consideriamo la punteggiatura 3. Non supportiamo altri caratteri di spazio vuoto ## Dividere il testo nel modo giusto Proviamo a migliorare l'esempio precedente. Potremmo provare a sviluppare un'espressione regolare complicata per correggere la divisione delle parole, ma probabilmente sarà lenta e sbagliata per i casi limite. L'[Unicode Annex #29](https://unicode.org/reports/tr29/) definisce come dividere correttamente il testo in parole per quasi tutte le lingue. È piuttosto complicato, ma fortunatamente Isar fa il lavoro pesante per noi: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## Voglio più controllo Facilissimo! Possiamo modificare il nostro indice anche per supportare la corrispondenza dei prefissi e la corrispondenza senza distinzione tra maiuscole e minuscole: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` Per impostazione predefinita, Isar memorizzerà le parole come valori hash che sono veloci ed efficienti in termini di spazio. Ma gli hash non possono essere usati per la corrispondenza dei prefissi. Usando `IndexType.value`, possiamo cambiare l'indice per usare invece le parole direttamente. Ci fornisce la clausola where `.titleWordsAnyStartsWith()`: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## Ho anche bisogno di `.endsWith()` Sicuramente! Useremo un trucco per ottenere la corrispondenza `.endsWith()`: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` Non dimenticare di invertire il finale che vuoi cercare: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Algoritmi di derivazione Sfortunatamente, gli indici non supportano la corrispondenza `.contains()` (questo vale anche per altri database). Ma ci sono alcune alternative che vale la pena esplorare. La scelta dipende molto dal tuo utilizzo. Un esempio è l'indicizzazione delle radici delle parole anziché dell'intera parola. Un algoritmo stemming è un processo di normalizzazione linguistica in cui le forme varianti di una parola sono ridotte a una forma comune: ``` connection connections connective ---> connect connected connecting ``` Gli algoritmi più diffusi sono [Algoritmo di stemming di Porter](https://tartarus.org/martin/PorterStemmer/) e [Algoritmi di stemming di Snowball](https://snowballstem.org/algorithms/). Esistono anche forme più avanzate come [lemmatizzazione](https://en.wikipedia.org/wiki/Lemmatizzazione). ## Algoritmi fonetici Un [algoritmo fonetico](https://en.wikipedia.org/wiki/Phonetic_algorithm) è un algoritmo per indicizzare le parole in base alla loro pronuncia. In altre parole, ti permette di trovare parole che suonano simili a quelle che stai cercando. :::warning La maggior parte degli algoritmi fonetici supporta solo una singola lingua. ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex) è un algoritmo fonetico per indicizzare i nomi in base al suono, come si pronuncia in inglese. L'obiettivo è che gli omofoni siano codificati nella stessa rappresentazione in modo che possano essere abbinati nonostante piccole differenze nell'ortografia. È un algoritmo semplice e ci sono più versioni migliorate. Usando questo algoritmo, sia `"Robert"` che `"Rupert"` restituiscono la stringa `"R163"` mentre `"Rubin"` restituisce `"R150"`. `"Ashcraft"` e `"Ashcroft"` producono entrambi `"A261"`. ### Double Metaphone L'algoritmo di codifica fonetica [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) è la seconda generazione di questo algoritmo. Apporta diversi miglioramenti di progettazione fondamentali rispetto all'algoritmo Metaphone originale. Double Metaphone spiega varie irregolarità in inglese di origine slava, germanica, celtica, greca, francese, italiana, spagnola, cinese e di altro tipo. ================================================ FILE: docs/docs/it/recipes/multi_isolate.md ================================================ --- title: Utilizzo multi-isolate --- # Utilizzo multi-isolate Invece dei thread, tutto il codice Dart viene eseguito all'interno degli isolate. Ogni isolate ha il proprio heap di memoria, assicurando che nessuno degli stati in un isolate sia accessibile da qualsiasi altro isolate. È possibile accedere a Isar da più isolate contemporaneamente e anche gli osservatori lavorano tra gli isolate. In questa ricetta vedremo come utilizzare Isar in un ambiente multiisolate. ## Quando utilizzare più isolate Le transazioni Isar vengono eseguite in parallelo anche se eseguite nello stesso isolate. In alcuni casi, è comunque vantaggioso accedere all'Isar da più isolate. Il motivo è che Isar impiega parecchio tempo a codificare e decodificare i dati da e verso gli oggetti Dart. Puoi pensarlo come codifica e decodifica JSON (solo più efficiente). Queste operazioni vengono eseguite all'interno dell'isolate da cui si accede ai dati e bloccano naturalmente altro codice nell'isolate. In altre parole: Isar esegue parte del lavoro nel tuo isolate Dart. Se hai solo bisogno di leggere o scrivere poche centinaia di oggetti contemporaneamente, farlo nell'isolate dell'interfaccia utente non è un problema. Ma per transazioni enormi o se il thread dell'interfaccia utente è già occupato, dovresti considerare l'utilizzo di un isolate separato. ## Esempio La prima cosa che dobbiamo fare è aprire l'Isar nel nuovo isolate. Poiché l'istanza di Isar è già aperta nell'isolate principale, `Isar.open()` restituirà la stessa istanza. :::warning Assicurati di fornire gli stessi schemi dell'isolate principale. In caso contrario, riceverai un errore. ::: `compute()` avvia un nuovo isolate in Flutter ed esegue la funzione data in esso. ```dart void main() { // Open Isar in the UI isolate final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // listen to changes in the database isar.messages.watchLazy(() { print('omg the messages changed!'); }); // start a new isolate and create 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // after some time: // > omg the messages changed! // > isolate finished } // function that will be executed in the new isolate Future createDummyMessages(int count) async { // we don't need the path here because the instance is already open final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // we use a synchronous transactions in isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` Ci sono alcune cose interessanti da notare nell'esempio sopra: - `isar.messages.watchLazy()` viene chiamato nell'isolate dell'interfaccia utente e viene notificato delle modifiche da un altro isolate. - Le istanze sono referenziate per nome. Il nome predefinito è `default`, ma in questo esempio lo impostiamo su `myInstance`. - Abbiamo utilizzato una transazione sincrona per creare i messaggi. Bloccare il nostro nuovo isolate non è un problema e le transazioni sincrone sono un po' più veloci. ================================================ FILE: docs/docs/it/recipes/string_ids.md ================================================ --- title: ID stringa --- # ID stringa Questa è una delle richieste più frequenti che ricevo, quindi ecco un tutorial sull'uso di String ID. Isar non supporta nativamente gli ID di stringa e c'è una buona ragione: gli ID interi sono molto più efficienti e veloci. Soprattutto per i collegamenti, l'overhead di un ID stringa è troppo significativo. Comprendo che a volte devi archiviare dati esterni che utilizzano UUID o altri ID non interi. Consiglio di archiviare l'ID String come proprietà nell'oggetto e di utilizzare un'implementazione hash veloce per generare un int a 64 bit che può essere utilizzato come ID. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` Con questo approccio, ottieni il meglio da entrambi i mondi: ID interi efficienti per i collegamenti e la possibilità di utilizzare ID di stringa. ## Funzione hash veloce Idealmente, la tua funzione hash dovrebbe avere un'alta qualità (non vuoi collisioni) ed essere veloce. Consiglio di utilizzare la seguente implementazione: ```dart /// FNV-1a 64bit hash algorithm optimized for Dart Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` Se scegli una funzione hash diversa, assicurati che restituisca un int a 64 bit ed evita di usare una funzione hash crittografica perché sono molto più lente. :::warning Evita di usare `string.hashCode` perché non è garantito che sia stabile su piattaforme e versioni diverse di Dart. ::: ================================================ FILE: docs/docs/it/schema.md ================================================ --- title: Schema --- # Schema Quando utilizzi Isar per archiviare i dati della tua app, hai a che fare con le raccolte. Una raccolta è come una tabella di database nel database Isar associato e può contenere solo un singolo tipo di oggetto Dart. Ogni oggetto della raccolta rappresenta una riga di dati nella raccolta corrispondente. Una definizione di raccolta è chiamata "schema". Isar Generator farà il lavoro pesante per te e genererà la maggior parte del codice necessario per utilizzare la raccolta. ## Anatomia di una collezione Definisci ogni collezione Isar annotando una classe con `@collection` o `@Collection()`. Una raccolta Isar include campi per ogni colonna nella tabella corrispondente nel database, incluso uno che comprende la chiave primaria. Il codice seguente è un esempio di una raccolta semplice che definisce una tabella `User` con colonne per ID, nome e cognome: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip Per rendere permanente un campo, Isar deve avervi accesso. Puoi assicurarti che Isar abbia accesso a un campo rendendolo pubblico o fornendo metodi getter e setter. ::: Ci sono alcuni parametri opzionali per personalizzare la collezione: | Config | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `inheritance` | Controlla se i campi delle classi padre e dei mixin verranno archiviati in Isar. Abilitato per impostazione predefinita. | | `accessor` | Consente di rinominare la funzione di accesso predefinita della raccolta (ad esempio `isar.contacts` per la raccolta `Contact`). | | `ignore` | Consente di ignorare determinate proprietà. Questi sono anche rispettati per le super classi. | ### Isar ID Ogni classe di raccolta deve definire una proprietà id con il tipo 'Id' che identifica in modo univoco un oggetto. `Id` è solo un alias per `int` che permette a Isar Generator di riconoscere la proprietà id. Isar indicizza automaticamente i campi id, il che ti consente di ottenere e modificare gli oggetti in base al loro id in modo efficiente. Puoi impostare gli ID da solo o chiedere a Isar di assegnare un ID con incremento automatico. Se il campo `id` è `null` e non `finale`, Isar assegnerà un id di autoincremento. Se vuoi un ID di incremento automatico non annullabile, puoi usare `Isar.autoIncrement` invece di `null`. :::tip Gli ID di incremento automatico non vengono riutilizzati quando un oggetto viene eliminato. L'unico modo per reimpostare gli ID di incremento automatico è cancellare il database. ::: ### Rinominare raccolte e campi Per impostazione predefinita, Isar utilizza il nome della classe come nome della raccolta. Allo stesso modo, Isar utilizza i nomi dei campi come nomi di colonne nel database. Se desideri che una raccolta o un campo abbia un nome diverso, aggiungi l'annotazione `@Name`. L'esempio seguente mostra i nomi personalizzati per la raccolta e i campi: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Soprattutto se vuoi rinominare i campi o le classi Dart che sono già archiviati nel database, dovresti considerare di usare l'annotazione `@Name`. In caso contrario, il database eliminerà e ricreerà il campo o la raccolta. ### Ignorare i campi Isar mantiene tutti i campi pubblici di una classe di raccolta. Annotando una proprietà o un getter con `@ignore`, puoi escluderlo dalla persistenza, come mostrato nel seguente frammento di codice: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` Nei casi in cui una raccolta eredita i campi da una raccolta padre, di solito è più semplice utilizzare la proprietà `ignore` dell'annotazione `@Collection`: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` Se una collezione contiene un campo con un tipo non supportato da Isar, devi ignorare il campo. :::warning Tieni presente che non è buona norma memorizzare informazioni in oggetti Isar che non sono persistenti. ::: ## Tipi supportati Isar supporta i seguenti tipi di dati: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` Inoltre, sono supportati oggetti incorporati ed enumerazioni. Tratteremo quelli di seguito. ## byte, short, float Per molti casi d'uso, non è necessario l'intero intervallo di un intero o doppio a 64 bit. Isar supporta tipi aggiuntivi che consentono di risparmiare spazio e memoria durante la memorizzazione di numeri più piccoli. | Tipo | Dim. in bytes | Range | | ---------- | ------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | I tipi di numeri aggiuntivi sono solo alias per i tipi Dart nativi, quindi usare `short`, ad esempio, funziona come usare `int`. Ecco una raccolta di esempio contenente tutti i tipi di cui sopra: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` Tutti i tipi di numeri possono essere utilizzati anche negli elenchi. Per memorizzare i byte, dovresti usare `List`. ## Tipi annullabili Comprendere come funziona l'annullamento dei valori in Isar è essenziale: i tipi numerici **NON** hanno una rappresentazione `null` dedicata. Viene invece utilizzato un valore specifico: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String` e `List` hanno una rappresentazione `null` separata. Questo comportamento consente miglioramenti delle prestazioni e ti consente di modificare liberamente la capacità di Null dei tuoi campi senza richiedere la migrazione o codice speciale per gestire i valori `null`. :::warning Il tipo `byte` non supporta valori nulli. ::: ## DateTime Isar non memorizza le informazioni sul fuso orario delle tue date. Invece, converte `DateTime`s in UTC prima di archiviarli. Isar restituisce tutte le date nell'ora locale. I `DateTime`s vengono archiviati con una precisione di microsecondi. Nei browser è supportata solo la precisione in millisecondi a causa delle limitazioni di JavaScript. ## Enum Isar consente di archiviare e utilizzare le enumerazioni come altri tipi di Isar. Devi scegliere, tuttavia, come Isar deve rappresentare le enumerazioni sul disco. Isar supporta quattro diverse strategie: | EnumType | Descrizione | | ----------- | -------------------------------------------------------------------------------------------------------------- | | `ordinal` | TL'indice dell'enum è memorizzato come `byte`. Questo è molto efficiente ma non consente enumerazioni nullable | | `ordinal32` | L'indice dell'enumerazione viene archiviato come `short` (4-byte integer). | | `name` | Il nome dell'enumerazione viene memorizzato come `String`. | | `value` | Per recuperare il valore dell'enumerazioni viene utilizzata una proprietà personalizzata. | :::warning `ordinal` e `ordinal32` dipendono dall'ordine dei valori dell'enumerazione. Se modifichi l'ordine, i database esistenti restituiranno valori errati. ::: Diamo un'occhiata a un esempio per ciascuna strategia. ```dart @collection class EnumCollection { Id? id; @enumerated // same as EnumType.ordinal late TestEnum byteIndex; // cannot be nullable @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // cannot be nullable @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Naturalmente, Enums può essere utilizzato anche nelle liste. ## Oggetti incorporati Spesso è utile avere oggetti nidificati nel modello di raccolta. Non c'è limite a quanto in profondità puoi annidare gli oggetti. Tieni presente, tuttavia, che l'aggiornamento di un oggetto profondamente nidificato richiederà la scrittura dell'intero albero degli oggetti nel database. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Gli oggetti incorporati possono essere nulli ed estendere altri oggetti. L'unico requisito è che siano annotati con `@embedded` e abbiano un costruttore predefinito senza parametri richiesti. ================================================ FILE: docs/docs/it/transactions.md ================================================ --- title: Transazioni --- # Transazioni In Isar, le transazioni combinano più operazioni di database in un'unica unità di lavoro. La maggior parte delle interazioni con Isar utilizza implicitamente le transazioni. L'accesso in lettura e scrittura in Isar è conforme a [ACID](http://en.wikipedia.org/wiki/ACID). Le transazioni vengono automaticamente annullate se si verifica un errore. ## Transazioni esplicite In una transazione esplicita, ottieni uno snapshot coerente del database. Cerca di ridurre al minimo la durata delle transazioni. È vietato effettuare chiamate di rete o altre operazioni di lunga durata in una transazione. Le transazioni (in particolare le transazioni di scrittura) hanno un costo e dovresti sempre provare a raggruppare le operazioni successive in un'unica transazione. Le transazioni possono essere sincrone o asincrone. Nelle transazioni sincrone è possibile utilizzare solo operazioni sincrone. Nelle transazioni asincrone, solo operazioni asincrone. | | Read | Read & Write | |--------------|--------------|--------------------| | Synchronous | `.txnSync()` | `.writeTxnSync()` | | Asynchronous | `.txn()` | `.writeTxn()` | ### Transazioni di lettura Le transazioni di lettura esplicita sono facoltative, ma consentono di eseguire letture atomiche e fare affidamento su uno stato coerente del database all'interno della transazione. Internamente Isar utilizza sempre transazioni di lettura implicita per tutte le operazioni di lettura. :::tip Le transazioni di lettura asincrone vengono eseguite in parallelo ad altre transazioni di lettura e scrittura. Abbastanza bello, vero? ::: ### Transazioni di scrittura A differenza delle operazioni di lettura, le operazioni di scrittura in Isar devono essere racchiuse in una transazione esplicita. Quando una transazione di scrittura viene completata correttamente, viene automaticamente salvata e tutte le modifiche vengono scritte su disco. Se si verifica un errore, la transazione viene interrotta e tutte le modifiche vengono annullate. Le transazioni sono "tutto o niente": o tutte le scritture all'interno di una transazione hanno esito positivo o nessuna di esse ha effetto per garantire la coerenza dei dati. :::warning Quando un'operazione di database ha esito negativo, la transazione viene interrotta e non deve più essere utilizzata. Anche se catturi l'errore in Dart. ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: move loop inside transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/it/tutorials/quickstart.md ================================================ --- title: Avvio rapido --- # Avvio rapido Santi numi, sei qui! Iniziamo a usare il database Flutter più interessante in circolazione... In questa guida introduttiva saremo a corto di parole e veloci nel codice. ## 1. Aggiungi dipendenze Prima che inizii il divertimento, dobbiamo aggiungere alcuni pacchetti a `pubspec.yaml`. Possiamo usare il pub per facilitarci il lavoro pesante. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Annota le classi Annota le tue classi collection con `@collection` e scegli un campo `Id`. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // puoi anche usare id = null per incrementare automaticamente String? name; int? age; } ``` Gli ID identificano in modo univoco gli oggetti in una collezione e ti consentono di ritrovarli in seguito. ## 3. Esegui il generatore di codice Esegui il seguente comando per avviare `build_runner`: ``` dart run build_runner build ``` Se stai usando Flutter, usa quanto segue: ``` flutter pub run build_runner build ``` ## 4. Apri l'istanza Isar Apri una nuova istanza Isar e passa tutti i tuoi schemi di raccolte. Facoltativamente, puoi specificare un nome di istanza e una directory. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Scrivi e leggi Una volta aperta l'istanza, puoi iniziare a utilizzare le raccolte. Tutte le operazioni CRUD di base sono disponibili tramite "IsarCollection". ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // insert & update }); final existingUser = await isar.users.get(newUser.id); // get await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // delete }); ``` ## Altre risorse Sei uno studente visivo? Guarda questi video per iniziare con Isar:


================================================ FILE: docs/docs/it/watchers.md ================================================ --- title: Osservatori --- # Osservatori Isar permette di sottoscrivere le modifiche al database. Puoi "osservare" le modifiche in un oggetto specifico, un'intera raccolta o una query. Gli osservatori consentono di reagire in modo efficiente alle modifiche nel database. Ad esempio, puoi ricostruire la tua interfaccia utente quando viene aggiunto un contatto, inviare una richiesta di rete quando un documento viene aggiornato, ecc. Un osservatore riceve una notifica dopo che una transazione è stata eseguita correttamente e la destinazione cambia effettivamente. ## Osservare gli oggetti Se vuoi essere avvisato quando un oggetto specifico viene creato, aggiornato o eliminato, dovresti osservare un oggetto: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` Come puoi vedere nell'esempio sopra, l'oggetto non deve ancora esistere. L'osservatore riceverà una notifica quando verrà creato. C'è un parametro aggiuntivo `fireImmediately`. Se lo imposti su `true`, Isar aggiungerà immediatamente il valore corrente dell'oggetto allo stream. ### Osservazione pigra Forse non è necessario ricevere il nuovo valore ma solo essere avvisati della modifica. Ciò evita a Isar di dover recuperare l'oggetto: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## Osservare le raccolte Invece di guardare un singolo oggetto, puoi guardare un'intera raccolta e ricevere una notifica quando un oggetto viene aggiunto, aggiornato o eliminato: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## Osservare le query È anche possibile osservare intere query. Isar fa del suo meglio per avvisarti solo quando i risultati della query cambiano effettivamente. Non riceverai una notifica se i collegamenti causano la modifica della query. Utilizza un osservatore di raccolta se hai bisogno di essere informato sulle modifiche ai link. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning Se utilizzi query offset & limit o distinte, Isar ti avviserà anche quando gli oggetti corrispondono al filtro ma al di fuori della query, i risultati cambiano. ::: Proprio come `watchObject()`, puoi usare `watchLazy()` per ricevere una notifica quando i risultati della query cambiano ma non recupera i risultati. :::danger La ripetizione delle query per ogni modifica è molto inefficiente. Sarebbe meglio se invece utilizzassi un osservatore di raccolta pigro. ::: ================================================ FILE: docs/docs/ja/README.md ================================================ --- home: true title: ホーム heroImage: /isar.svg actions: - text: さっそく始めよう! link: /ja/tutorials/quickstart.html type: primary features: - title: 💙 Flutterのために details: 最小限のSetup、簡単に使えて、追加の設定やボイラープレートは不要。数行のコードを追加後にすぐに使用可能。 - title: 🚀 高い拡張性 details: 数十万件のレコードを1つのNoSQLデータベースに格納し、効率的かつ非同期にクエリを実行。 - title: 🍭 豊富な機能 details: 複合 & 複数条件対応インデックスやクエリ修飾子、JSONのサポートなど、データ管理を支援する豊富な機能を搭載。 - title: 🔎 全文検索機能 details: 全文検索機能を保持。複数の条件を設定したIndexを作成し、簡単にレコードを検索する事が可能。 - title: 🧪 ACID セマンティクス details: IsarはACIDに準拠しており、トランザクションを自動的に処理。エラーが発生しても変更をロールバック。 - title: 💃 静的型付け details: Isarのクエリは静的型付けされ、コンパイル時にチェックされます。実行時エラーを心配する必要はありません。 - title: 📱 マルチプラットフォーム対応 details: iOS, Android, Desktop, そしてWEBにも対応! - title: ⏱️ 非同期処理 details: 並列クエリ操作とMulti-Isolateをすぐに利用可能。 - title: 🦄 オープンソース details: すべてがオープンソースで、永久に無料! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/ja/crud.md ================================================ --- title: CRUD操作 --- # CRUD操作 コレクションを定義したら、それを操作する方法を学びましょう。 ## Isarを開く 何をするにしても、まずはIsarのインスタンスが必要です。各インスタンスには、データベースファイルを格納することができる書き込み権限のあるディレクトリが必要になります。ディレクトリを指定しない場合、Isarは現在のプラットフォームに適したデフォルトのディレクトリを見つけます。 Isarインスタンスで使用したいすべてのスキーマを指定します。複数のインスタンスを開いている場合でも、それぞれのインスタンスに同じスキーマを与える必要があります。 ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [ContactSchema], directory: dir.path, ); ``` 加えて、デフォルト設定を使用するか、もしくは以下のいくつかのパラメータを指定することができます: | Config | Description | | -------| -------------| | `name` | 複数のインスタンスを別々の名前で開きます。デフォルトでは、`"default"` が使用されます。 | | `directory` | このインスタンスの保存場所です。相対パスまたは絶対パスを渡すことができます。デフォルトでは、iOS では `NSDocumentDirectory` が、Android では `getDataDirectory` が使用されます。Webにおいては必要ありません。 | | `relaxedDurability` | 書き込み性能を向上させるために耐久性保証を緩和します。 アプリケーションのクラッシュではなく、システムクラッシュの場合、最後にコミットしたトランザクションが失われる可能性があります。破損する可能性はありません。 | | `compactOnLaunch` | インスタンスを開く際にデータベースの圧縮を行うかどうかを確認するための条件です。 | | `inspector` | デバッグビルドでインスペクターを有効にします。プロファイルとリリースビルドでは、このオプションは無視されます。 | 既にインスタンスが開かれている場合に `Isar.open()` を呼び出すと、指定したパラメータに関係なく既存のインスタンスを取得します。これはIsarをアイソレートで使用する場合に便利です。 :::tip すべてのプラットフォームで有効なパスを取得するために、[path_provider](https://pub.dev/packages/path_provider)パッケージの使用を検討してください。 ::: データベースファイルの保存場所は `directory/name.isar` です。 ## データベースからの読み込み `IsarCollection` インスタンスを使用して、Isar で指定した型のオブジェクトを検索したり、照会したり、新規に作成したりすることができます。 これ以降のサンプルコードでは、コレクション `Recipe` が以下のように定義されていると仮定した上で述べて行きます。 ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### コレクションの取得 すべてのコレクションは、Isarインスタンスに格納されています。あなたはrecipesコレクションを次のように取得できます: ```dart final recipes = isar.recipes; ``` 簡単ですよね? コレクションアクセサを使いたくない場合は、`collection()` メソッドを使うこともできます: ```dart final recipes = isar.collection(); ``` ### idを用いたオブジェクトの取得 まだコレクションにデータはありませんが、あるものと仮定して、 `123` という ID の架空のオブジェクトを取得してみましょう。 ```dart final recipe = await recipes.get(123); ``` `get()` はオブジェクトを含む `Future` を返しますが、オブジェクトが存在しない場合は `null` を返します。 Isar のすべての操作はデフォルトでは非同期ですが、ほとんどの操作には同期処理も対応しています: ```dart final recipe = recipes.getSync(123); ``` :::warning UIアイソレートでは、非同期バージョンのメソッドをデフォルトで使用する必要があります。ちなみに、Isarは非常に高速なので、多くの場合において同期バージョンを使用しても問題ありません。 ::: 複数のオブジェクトを一度に取得したい場合は、 `getAll()` または `getAllSync()` を使用してください: ```dart final recipe = await recipes.getAll([1, 2]); ``` ### オブジェクトのクエリ IDでオブジェクトを取得する代わりに、 `.where()` と `.filter()` を使って特定の条件に一致するオブジェクトのリストを取得することもできます: ```dart final allRecipes = await recipes.where().findAll(); final favouires = await recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ 詳しくはこちら: [クエリ](queries) ## データベースの書き換え いよいよコレクションを書き換えるときがやってきました! オブジェクトを作成、更新、削除するには、それぞれの操作をWriteトランザクション内でラップして使用します: ```dart await isar.writeTxn(() async { final recipe = await recipes.get(123) recipe.isFavorite = false; await recipes.put(recipe); // 更新操作の実行 await recipes.delete(123); // 削除操作の実行 }); ``` ➡️ 詳しくはこちら: [トランザクション](transactions) ### オブジェクトの挿入 Isar でオブジェクトを永続化するには、コレクションにオブジェクトを挿入(put)します。 Isar の `put()` メソッドは、そのオブジェクトが既にコレクションに存在するかどうかに応じて、オブジェクトの挿入もしくは更新を行います。 この時、id フィールドが `null` または `Isar.autoIncrement` の場合、Isar はオートインクリメントの id を使用します。 ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await recipes.put(pancakes); }) ``` Isarは `id` フィールドがfinalでは無い場合、オブジェクトに自動的にidを割り当てます。 複数のオブジェクトを一度に挿入することも簡単です。 ```dart await isar.writeTxn(() async { await recipes.putAll([pancakes, pizza]); }) ``` ### オブジェクトの更新 作成と更新の両方は `collection.put(object)` で行います。id が `null` (または存在しない) 場合はオブジェクトは挿入され、そうでない時は更新されます。 つまり、pancakesをunfavoriteにしたい場合は、以下のようになります: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await recipes.put(recipe); }); ``` ### オブジェクトの削除 オブジェクトを削除したい場合は、`collection.delete(id)`を使用してください. delete メソッドは、指定された id を持つオブジェクトを見つけて、それを削除したかどうかを返します。例えば、id が `123` のオブジェクトを削除したい場合、以下のようになります。 ```dart await isar.writeTxn(() async { final success = await recipes.delete(123); print('Recipe deleted: $success'); }); ``` getやputと同様に、削除されたオブジェクトの数を返す一括削除命令も存在します: ```dart await isar.writeTxn(() async { final count = await recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` 削除したいオブジェクトのidが分からない場合は、クエリを使用することができます: ```dart await isar.writeTxn(() async { final count = await recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/ja/faq.md ================================================ --- title: よくある質問 --- # よくある質問 IsarとFlutterのデータベースについてよくある質問を無作為に集めました。 ### 何でデータベースが必要なの? > 私はバックエンドDBにデータを保存しているけど, 何でIsarが必要なの? 地下鉄や飛行機に乗っているとき、あるいはWiFiがなく携帯の電波が非常に悪いおばあちゃんの家に行ったときなど、今日においてもデータ通信ができないことはよくあることです。接続が悪いとアプリ自体が使えないなんてことがあってはなりません。 ### Isar vs Hive 答えは簡単です: Isar は [Hiveの代替としてスタート](https://github.com/hivedb/hive/issues/246) し、現在はHiveよりもIsarを使うことを推奨する状態になっています。 ### WHERE節って!? > 何でwhere節にどのインデックスを使うのか選択しなきゃいけないの? これには複数の理由があります。ほとんどのデータベースは、与えられたクエリに対して最適なインデックスを選択するためにヒューリスティックを使用しています。データベースは追加の利用状況データを収集する必要があり、それがオーバーヘッドに繋がります。 加えて、それでも間違ったインデックスを選択する可能性があります。 更にはクエリの作成が遅くなるという点も懸念されます。 開発者であるあなた以上に、あなたのデータを知っている人はいません。ですから、あなた自身が最適なインデックスを選択し、例えば、クエリにインデックスを使うかソートにインデックスを使うかなどを決定することができるのです。 ### インデックスやwhere節を必ず使わないといけないの? いえ、必ずしもそんなことはありません。多くの場合、filterしか使用しなかったとしてもIsarは高速で処理されます。 ### Isarって速いの? Isarは、モバイル向けデータベースの中では最速クラスなので、ほとんどのケースでは十分な速度が出るはずです。もし、パフォーマンスで問題が発生した場合は、何か間違った操作をしている可能性があります。 ### Isarはアプリのサイズを増加させますか? そうですね、少しは。Isarは、アプリのダウンロードサイズを約1〜1.5MB増加させます。Isar Webは、数KBの追加にとどまります。 ### ドキュメントの内容が間違ってるよ / タイポ見つけた すみません、失礼致しました。 [issueを開く](https://github.com/isar-community/isar/issues/new/choose)か、もしくは、プルリクエストをして修正して頂けると助かります💪 ================================================ FILE: docs/docs/ja/indexes.md ================================================ --- title: インデックス --- # インデックス インデックスは、Isarの最も強力な機能です。多くの組み込み型データベースは、"通常の"インデックスを提供していますが、Isarは複合インデックスやマルチエントリーインデックスも提供しています。 クエリのパフォーマンスを最適化するためには、インデックスがどのように機能するかを理解することが重要です。 Isarでは、どのインデックスを、どのように使用するか事を選ぶ事が出来ます。 それではまず最初に、インデックスとは何かということを簡単に紹介します。 ## インデックスとは? コレクションにインデックスがない場合、行の順番はクエリによって最適化されていない可能性が高く、クエリはオブジェクトを直線的に検索しなければならなくなります。 言い換えれば、クエリはすべてのオブジェクトを検索して、条件にマッチするものを見つけなければならないのです。ご想像のとおり、これには時間がかかります。オブジェクトをひとつひとつ見ていくのは、あまり効率的ではありません。 例えば、この `Product` コレクションは完全に順不同です。 ```dart @collection class Product { Id? id; late String name; late int price; } ``` **データ:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | 30ユーロ以上の商品をすべて探そうとするクエリは、9行すべてを検索しなければなりません。9行では問題ないかもしれませんが、10万行になると問題になるでしょう。 ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` このクエリの性能を向上させるために、`price` プロパティにインデックスを付けます。インデックスとは、ソートされた検索テーブルのようなものです。: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **生成されたインデックス:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | これで、クエリの実行がかなり速くなりました。エクゼキュータは、最後の3つのインデックス行に直接ジャンプして、対応するオブジェクトをそのIDで見つけることができます。 ### ソート もうひとつ素晴らしいのは、インデックスを使うと超高速でソートができることです。ソートを指示するクエリは、ソートをする前にデータベースが全ての結果をメモリにロードする必要があるため、コストがかかります。offsetやlimitを指定しても、それはソート後に適用されます。 例えば、最も安い商品を4つ見つけたいとします。次のようなクエリを使うことができます: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` この例では、データベースはすべての(!)オブジェクトを読み込み、それらを価格順にソートして、最も安い価格の 4 つの製品を返さなければなりません。 想像がつくと思いますが、これは先ほどのインデックスを使えばもっと効率的に行えます。データベースはインデックスの最初の4行を受け取り、対応するオブジェクトを返します。なぜなら、これらはすでに適切な順番になっているからです。 インデックスをソートに使うには、次のようなクエリを書きます。 ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` `.anyX()` というwhere節は、Isarにソートのためだけにインデックスを使用するように指示します。また、`priceGreaterThan()`のようなwhere節を使用して、ソートされた結果を得ることもできます。 ## ユニークインデックス ユニークインデックス(一意なIndex)は、インデックスが重複した値を含まないことを保証します。これは、1つまたは複数のプロパティで構成されることがあります。ユニークインデックスが1つのプロパティを持つ場合、このプロパティの値は一意となります。ユニークインデックスが複数のプロパティを持つ場合、これらのプロパティの値の組み合わせは一意になります。 ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` ユニークインデックスに重複を引き起こすデータを挿入または更新しようとすると、エラーになります: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // 同じユーザー名でユーザーを挿入しようとする。 await isar.users.put(user2); // -> エラー: 一意制約に反しています。 print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## インデックスの置き換え 一意性制約に違反した場合にエラーを投げることが好ましくない場合もあります。エラーを投げる代わりに、既存のオブジェクトを新しいオブジェクトに置き換えたい場合があるかもしれまん。その場合は、インデックスの `replace` プロパティを `true` に設定することで実現できます。 ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` これで、既存のユーザー名でユーザーを挿入しようとすると、Isarは既存のユーザーを新しいユーザーで置き換えます。 ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Replaceインデックスは `putBy()` メソッドも生成し、オブジェクトを置き換えるのではなく、更新することができます。既存の ID は再利用され、リンクはそのまま反映されます。 ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // ユーザーが存在しないので、put()と同じです。 await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` 見ての通り、最初に挿入されたユーザーのidが再利用されています。 ## 大文字小文字を区別しないインデックス `String` と `List` プロパティに対するすべてのインデックスは、デフォルトで大文字と小文字を区別して表示されます。大文字小文字を区別しないインデックスを作成したい場合は、 `caseSensitive` オプションを使用することができます。 ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## インデックスの種類 インデックスにはさまざまな種類があります。ほとんどの場合、 `IndexType.value` インデックスを使用することになるでしょうが、Hashインデックスを使用するとより効率的です。 ### Valueインデックス Valueインデックスは既定の型であり、StringやListを保持しないすべてのプロパティで許可される唯一のものです。インデックスを構築するために、プロパティの値が使用されます。Listの場合は、Listの要素が使用されます。これは、3つのインデックスタイプの中で最も柔軟性がありますが、ストレージも消費します。 :::tip 基本データ型や、Strings(※where節において `startsWith()` を使いたい場合)、そしてLists(※個別の要素を検索したい場合)においてはIndexType.valueを使用しましょう。 ::: ### Hashインデックス 文字列やListをハッシュ化することで、インデックスに必要なストレージを大幅に削減することができます。Hashインデックスの欠点は、接頭辞の走査 (where節における `startsWith` ) に使用できないことです。 :::tip 文字列やListに対して、 `startsWith` や `elementEqualTo` という where 節が必要ない場合は、 `IndexType.hash` を使用しましょう。 ::: ### HashElementsインデックス 文字列Listは、全体を (`IndexType.hash` を用いて) ハッシュ化することができますし、Listの要素を個別に (`IndexType.hashElements` を用いて) ハッシュ化して、効率的に要素をハッシュ化したマルチエントリーインデックスを作成することができます。 :::tip `List` で `elementEqualTo` の where 節が必要な場合は、 `IndexType.hashElements` を使用します。 ::: ## 複合インデックス 複合インデックスとは、複数のプロパティに対するインデックスのことです。Isarでは、最大3つのプロパティのコンポジットインデックスを作成することができます。 複合インデックスは、複数列インデックスとも呼ばれます。 まず、例から始めるのが一番でしょう。person コレクションを作成し、age プロパティと name プロパティに複合インデックスを定義します。 ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **データ:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **生成されたインデックス:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | 生成された複合インデックスは、年齢と名前でソートされたすべての人物を含んでいます。 複合インデックスは、複数のプロパティでソートされた効率的なクエリを作成したい場合に最適です。また、複数のプロパティを持つ高度な where 節も作成できます。 ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` 複合インデックスの末尾のプロパティは、 `startsWith()` や `lessThan()` といった条件もサポートします。 ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## マルチエントリーインデックス IndexType.valueを使ってListのインデックスを作成すると、Isarは自動的にマルチエントリーのインデックスを作成し、List内の各項目がオブジェクトに対してインデックスされます。これはすべての型のListに対して機能します。 マルチエントリーインデックスの実用的な用途としては、タグのListのインデックス化や全文インデックスの作成などが挙げられます。 ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` は [Unicode Annex #29](https://unicode.org/reports/tr29/) の仕様に従って文字列を単語に分割するので、ほとんどすべての言語に対して正しく動作します。 **データ:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | 重複する単語を含むエントリは、インデックスに一度だけ表示されます。 **生成されたインデックス:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | これで、このインデックスは、個々の単語の接頭辞(または等号)をwhere節に使用できるようになりました。 :::tip 単語を直接保存する代わりに、[Soundex](https://en.wikipedia.org/wiki/Soundex) のような [音声アルゴリズム](https://en.wikipedia.org/wiki/Phonetic_algorithm) を使用する事も候補に入れてみてください。 ::: ================================================ FILE: docs/docs/ja/limitations.md ================================================ # 制限事項 ご存知のように、Isarはモバイル端末や VM上で動作するデスクトップ、そしてWeb上で動作します。この2つのプラットフォームは非常に異なっており、それぞれ異なる制限事項があります。 ## VMの制限事項 - 文字列の最初の1024バイトのみがwhere節の接頭辞として使用可能です。 - オブジェクトのサイズは 16MB までとなります。 ## Webの制限事項 Isar WebはIndexedDBに依存しているため、より多くの制限事項がありますが、 Isarを使用している間はほとんど気にはならないでしょう。 - 同期メソッドはサポートされていません。 - 現時点において、 `Isar.splitWords()`と`.matches()`フィルターは未実装です。 - スキーマの変更はVMほど厳密にはチェックされないので、規則に従うように注意してください。 - すべての数値型は double (唯一の js 数値型) として保存されるので、 `@Size32` は影響しません。 - インデックスの表現が異なるため、Hashインデックスの容量は減りません。(機能/動作は同じです) - `col.delete()` と `col.deleteAll()` は正しく動作しますが、戻り値が正しくありません。 - `col.clear()` はオートインクリメント値をリセットしません。 - 値として `NaN` はサポートされていません。 ================================================ FILE: docs/docs/ja/links.md ================================================ --- title: リンク --- # リンク リンクは、例えばコメントの作成者(User)のようなオブジェクト間の関係を表現することができ、 `1:1`、`1:n`、`n:n`の関係を IsarLinkで表現することができます。リンクの使用は、埋め込みオブジェクトの使用よりも人間工学的に劣るので、可能な限り埋め込みオブジェクトを使用するようにしましょう。 リンクはリレーションを含む別のテーブルだと考えてください。これはSQLのリレーションと似ていますが、異なった機能セットとAPIを持っています。 ## IsarLink `IsarLink` は関連するオブジェクトを含まないか、1つだけ含むことができ、一対一の関係を表現するために使用することができます。`IsarLink` はリンク先のオブジェクトを保持する `value` というプロパティをひとつだけ持っています。 リンクは遅延(lazy)する為、 `IsarLink` に対して、明示的に `value` を読み込みまたは保存するように指示する必要があります。これは、 `linkProperty.load()` と `linkProperty.save()` を呼び出すことで実現できます。 :::tip Linkの元(Source)コレクションと対象(Target)コレクションの id プロパティは非final値にすべきです。 ::: Web 以外の対象ついては、リンクは初めて使用する際に自動的に読み込まれます。まずは、IsarLinkをコレクションに追加してみましょう。 ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` 教師と生徒を介するリンクを定義しました。この例では、全ての生徒が1人だけの教師を持つことができます。 まず、教師を作成し、生徒に割り当てます。教師を `.put()` して、手動でリンクを保存する必要があります。 ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` これでリンクが使えるようになりました: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` 同じことを同期コードで試してみましょう。`.putSync()`が自動的にすべてのリンクを保存するので、手動でリンクを保存する必要はありません。さらには、教師も自動作成してくれます。 ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks 前の例の生徒が複数の教師を持つことができれば、理にかなっていますよね。幸いなことに、Isarには `IsarLinks` があり、複数の関連オブジェクトを含むことができ、対多関係を表現することができます。 `IsarLinks` は `Set` を継承しており、Setに対して許可されている全てのメソッドを実装しています。 `IsarLinks` は `IsarLink` と同じように動作し、遅延(lazy)します。リンクされたオブジェクトを全て読み込むには、 `linkProperty.load()` を呼び出します。変更を持続させるには、 `linkProperty.save()` を呼び出します。 内部的には、`IsarLink` と `IsarLinks` は同じように表現されています。先ほどの `IsarLink` を `IsarLinks` にアップグレードすれば、(データを失うことなく)一人の生徒に複数の教師を割り当てることができます。 ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` リンクの名前(`teacher`)を変更していないので、Isarがそれを記憶しています。 その為、データを失わずに機能します。 ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlinks 逆方向の関係を表現したい場合はどうすればいいのでしょうか?安心してください。これからバックリンクを紹介します。 バックリンクとは、逆方向のリンクのことです。各リンクは、常に暗黙のバックリンクを持っています。`IsarLink` や `IsarLinks` に `@Backlink()` というアノテーションをつけることで、アプリでバックリンクを利用できるようになります。 バックリンクは追加のメモリやリソースを必要としません。データを失うことなく、自由に追加、削除、名前の変更を行うことができます。 特定の教師がどのような生徒を持っているかを知りたいので、バックリンクを定義します。 ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` また、バックリンクが指し示すリンクを明示する必要があります。2つのオブジェクトの間に複数の異なるリンクを設定することが可能です。 ## リンクの初期化 `IsarLink` と `IsarLinks` にはゼロ引数のコンストラクタがあり、オブジェクトの生成時にリンクのプロパティを代入するために使用されます。リンクのプロパティを `final` にするのは良い習慣です。 オブジェクトを初めて `put()` したとき、リンクは元(Source)コレクションと対象(Target)コレクションで初期化され、 `load()` や `save()` といったメソッドを呼び出すことができるようになります。リンクは作成後すぐに変更の追跡を開始するので、リンクが初期化される前でもリレーションを追加したり削除したりすることができます。 :::danger リンクを他のオブジェクトに移動することは不正(illegal)です。 ::: ================================================ FILE: docs/docs/ja/queries.md ================================================ --- title: クエリ --- # クエリ クエリとは、ある条件に合致するレコードを探し出す方法です。例えば: - 星付きの連絡先をすべて検索 - 連絡先の名前を個別に検索する - 姓が定義されていないすべての連絡先を削除する クエリはDart内ではなくデータベース上で実行されるため、非常に速く実行することができます。インデックスを巧みに使えば、クエリの性能をさらに向上させることができます。 以降では、クエリの記述方法と、クエリを可能な限り高速化する方法について学びます。 レコードを絞り込むには、2種類の方法があります。フィルタとWHERE節です。まず、フィルターがどのように機能するかを見てみましょう。 ## フィルタ フィルタは使いやすく、わかりやすいです。プロパティの種類に応じて、さまざまなフィルタリング処理が用意されており、そのほとんどが一目でわかるような名前になっています。 フィルタは、フィルタリングされるコレクション内のすべてのオブジェクトに対して評価式を適用することで動作します。式の結果が `true` であった場合、Isar はそのオブジェクトを結果に含めます。フィルタは結果の順序に影響を与えません。 これから紹介する例では、次のようなモデルを使用します: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### クエリの条件 フィールドの種類に応じて、利用可能な条件が異なります。 | Condition | Description | | ----------| ------------| | `.equalTo(value)` | 指定した `value` と等しい値に一致する。 | | `.between(lower, upper)` | `lower` と `upper` の間にある値に一致する。 | | `.greaterThan(bound)` | `bound` よりも大きい値に一致する。 | | `.lessThan(bound)` | `bound` よりも小さい値に一致する。デフォルトでは `null` の値も含まれる。なぜなら `null` は他のどの値よりも小さいとみなされるからである。 | | `.isNull()` | `null` に一致する。| | `.isNotNull()` | `null` ではない値に一致する。| | `.length()` | List、String、linkの長さのクエリは、Listやlinkの要素数に基づいてオブジェクトをフィルタリングする。 | ここでは、データベースにsizeが39、40、46のshoeとサイズが設定されていない(`null`)1つのshoeの合計4つが含まれていると仮定します。ソートを行わない限り、値は id でソートされて返されます。 ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### 論理演算子 以下の論理演算子を使って述語を合成することもできます: | Operator | Description | | ---------- | ----------- | | `.and()` | 左側と右側の式の両方が `true` と評価された場合、`true` と評価される。| | `.or()` | どちらかの式が `true` と評価された場合、`true` と評価される。| | `.xor()` | ちょうど1つの式が `true` と評価される場合に、 `true` と評価される。 | | `.not()` | 次の式の結果を否定する。 | | `.group()` | 条件をグループ化し、評価順序を指定できるようにする。| sizeが46のshoesをすべて見つけたい場合は、次のようなクエリを使用します。 ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` 複数の条件を使用したい場合は、 **論理積** `.and()` や, **論理和** `.or()` 、 **排他的論理和** `.xor()`を組み合わせることが出来ます。 ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // オプション。 フィルターは暗黙的に論理積で結合される. .isUnisexEqualTo(true) .findAll(); ``` このクエリは次の式と同等です: `size == 46 && isUnisex == true`. また、`.group()` を使って条件をグループ化することもできます: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` このクエリは次の式と同等です: `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. 条件やグループを否定するには、**論理否定** `.not()` を使用します: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` このクエリは次の式と同等です: `size != 46 && isUnisex != true`. ### 文字列の条件 上記のクエリ条件に加えて、文字列値にはさらにいくつかの条件を使用することができます。たとえば、正規表現に似たワイルドカードを使用すると、より柔軟な検索が可能になります。 | Condition | Description | | -------------------- | ----------------------------------------------------------------- | | `.startsWith(value)` | 指定した `value` で始まる文字列値に一致する。 | | `.contains(value)` | 指定した `value` を含む文字列値に一致する。 | | `.endsWith(value)` | 指定した `value` で終わる文字列値に一致する。 | | `.matches(wildcard)` | 指定した `wildcard` パターンに適合する文字列値に一致する。 | **大文字小文字を区別する** すべての文字列操作には、オプションで `caseSensitive` パラメータがあり、デフォルトは `true` です。 **ワイルドカード:** [ワイルドカード文字列表現](https://en.wikipedia.org/wiki/Wildcard_character) は、通常の文字に2つの特殊なワイルドカード文字を使用した文字列です。: - ワイルドカードの `*` は、0個以上の任意の文字に一致します。 - ワイルドカードの `?` は、任意の文字に一致します。 たとえば, ワイルドカード文字列 `"d?g"` は `"dog"`, `"dig"`, および `"dug"` にマッチするが、 `"ding"`, `"dg"`, および `"a dog"` にマッチしません。 ### クエリ修飾子 時には、ある条件や異なる値に基づいてクエリを作成することが必要な場合があります。Isarは、条件付きクエリを作成するための非常に強力な機能を持っています。: | Modifier | Description | | --------------------- | ---------------------------------------------------- | | `.optional(cond, qb)` | 条件が `true` の場合のみ、クエリを拡張する。 これは、クエリ内のほぼすべての場所で使用することが出来ます。条件付きでソートしたり絞り込む為に用いるなどが使用例です。 | | `.anyOf(list, qb)` | `values` の各値に対してクエリを拡張し、 **論理和** を用いて条件を組み合わせる。 | | `.allOf(list, qb)` | `values` の各値に対してクエリを拡張し、 **論理積** を用いて条件を組み合わせる。 | このサンプルでは、optionalを使用してShoesを見つけることができるメソッドを構築しています: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // sizeFilter != null の場合のみ、フィルタを適用する。 (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` 複数の靴のサイズのいずれかを持つ靴をすべて見つけたい場合は、従来のクエリを書くか、 `anyOf()` 修飾子を使うことができます: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` クエリ修飾子は、動的なクエリを構築したい場合に特に有効です。 ### リスト Listにおいてもクエリが可能です: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` Listの長さ(length)に基づいてクエリを実行できます: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` これらは、Dartのコード `tweets.where((t) => t.hashtags.isEmpty);` や `tweets.where((t) => t.hashtags.length > 5);` に相当します。また、リストの要素をもとに問い合わせることもできます: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` これはDartのコード `tweets.where((t) => t.hashtags.contains('flutter'));` に相当します。 ### 埋め込みオブジェクト 組み込みオブジェクトは、Isarの最も便利な機能の一つです。トップレベルオブジェクトと同じ条件で非常に効率的に問い合わせることができます。例えば、次のようなモデルがあるとします: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` ブランド名が `"BMW"` で、国名が `"Germany"` である車をすべて問い合わせたいとします。これは以下のクエリで実現できます: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` ネストされたクエリは常にグループ化するようにしましょう。上記のクエリは以下のクエリと結果は同じですが、上記のクエリの方がより効率的に動作します: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### リンク モデルに[リンクもしくはバックリンク](links)が含まれている場合、リンクされたオブジェクトまたはリンクされたオブジェクトの数に基づいてクエリをフィルタリングすることができます。 :::warning リンククエリは、Isarがリンクされたオブジェクトを検索する必要があるため、コストがかかることに留意してください。また、代わりに埋め込みオブジェクトを使用することを検討してみてください。 ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` 数学または英語の先生を持つ全ての生徒を見つけたいとします: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` リンクフィルターは、少なくとも1つのリンクオブジェクトが条件にマッチすれば、`true`と評価されます。 教師を持たない全ての生徒を検索してみましょう。: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` もしくは: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where節 Where節は非常に強力な機能ですが、正しく使用するのは少し難しいかもしれません。 フィルターとは対照的に、where節はスキーマで定義したインデックスを使用してクエリ条件を確認しています。各レコードを個別にフィルタリングするより、インデックスを用いる方がはるかに高速です。 ➡️ 詳しくはこちら: [インデックス](indexes) :::tip 基本的なルールとして、Where節を使用してレコードをできる限り減らし、残りのフィルタリングはフィルタを使用して行うようにすることをお勧めします。 ::: where節を組み合わせるには、**論理和**しか使えません。言い換えると、複数のwhere節を合計することはできますが、複数のwhere節の交差部分を照会することはできません。 それではShoeコレクションにインデックスを追加してみましょう: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` ここではインデックスが2つあります。`size` のインデックスは、 `.sizeEqualTo()` のような where 節を使用可能にしています。`isUnisex` の複合インデックス(CompositeIndex)は、 `isUnisexSizeEqualTo()` のような where 節を使用できるようにしています。そしてまた、インデックスの接頭辞は常に任意のものを使用できる為、 `isUnisexEqualTo()` のような事も可能です。 これでサイズ46のユニセックスの靴を検索する以前見たクエリを、複合インデックスを使用して書き換えることができます。このクエリは前記で述べたクエリよりも高速に動作します: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where節には、さらに2つの強力な機能があります。 Where節は"Free"なソートと、超高速なDISTINCT命令を保持しています。 ### where節とフィルタの組み合わせ `shoes.filter()` というクエリを覚えていますか? 実はこれは `shoes.where().filter()` の短縮形なのです。where節とfilterを同じクエリで組み合わせて、両方の利点を利用することができます(そして、そうすべきです): ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` まず、where 節が適用され、フィルタリングされるオブジェクトの数が減ります。その後、残りのオブジェクトにフィルタが適用されます。 ## ソート クエリ実行結果のソート方法は、`.sortBy()`, `.sortByDesc()`, `.thenBy()`, `.thenByDesc()` メソッドを用いて定めることが可能です。 インデックスを使わずに、すべての靴をModel名の昇順とSizeの降順でソートして検索する方法です: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` 特に、ソートは offset と limit の前に行われるため、たくさんの結果をソートするのはコストがかかります。上記のソートメソッドでは、インデックスを使用することはありません。幸いなことに、Where節によるソートを使えば、100万個のオブジェクトをソートする場合でもクエリを高速に実行することができます。 ### Where節のソート クエリで **単一(single)** の where 節を使用した場合、結果はすでにインデックスでソートされています。これは非常に重要です。 例えば、サイズ `[43, 39, 48, 40, 42, 45]` の靴があり、サイズが `42` より大きい靴をすべて検索し、サイズ順に並べたいとしましょう。 ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // 加えて、結果がSizeでソートされる .findAll(); // -> [43, 45, 48] ``` 見ての通り、結果は `size` インデックスでソートされています。where 節のソート順を逆にしたい場合は、 `sort` に `Sort.desc` をセットします: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` 時には where 節を使いたくないけれども、暗黙のうちにソートが行われるという恩恵を受けたいこともあるでしょう。そのような場合には、 `any` という where 節を使用します: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` もし、あなたが複合インデックスを使用した場合、結果はそのインデックス内のすべてのフィールドでソートされます。 :::tip 結果をソートする必要がある場合は、インデックスを使用することを検討してください。`offset()` や `limit()` を使っている場合は特にそうです。 ::: 時には、ソートのためにインデックスを使用することが出来なかったり、有用ではない場合もあるかもしれません。そのような場合は、インデックスを使用して結果の項目数をできるだけ減らすのが良いでしょう。 ## ユニーク値 一意な値を持つ項目のみを返すには、distinct述語を使用します。たとえば、Isar データベースに何種類の異なる靴のModelがあるかを調べるには、 以下のようにします: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` また、複数のdistinctの条件を繋げて、異なるModelとSizeの組み合わせである全ての靴を検索することができます。 ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` 異なる組み合わせの最初の結果のみが返されます。これをコントロールするために、where句とソート操作を使用することも可能です。 ### WHERE節のdistinct 一意でないインデックスがある場合、それの全ての異なる値を取得したい時があると思います。前のセクションで紹介した `distinctBy` オペレーションを使うこともできますが、ソートやフィルタの後に実行されるため、若干のオーバーヘッドが発生します。 WHERE節を1つだけ使用するのであれば、代わりにインデックスに依拠してdistinct処理を実行することができます。 ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip 理論・仕組み的には、ソートとdistinctのために複数のwhere節を使うこともできます。複数のwhere節を使う唯一の制限は、これらのwhere節が重複しておらず、同じインデックスを使用していることです。正しいソートを行うには、ソート順で適用する必要があります。十分に注意をしてください。 ::: ## OffsetとLimit 遅延(lazy)リストビューのために、クエリ結果の数を制限することは良い方法だと思います。これを行うには、 `limit()` を設定します。 ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` `offset()` を設定することで、クエリの結果をページネイト(取得開始位置の指定)することもできます。 ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Dartオブジェクトのインスタンス化は、クエリ実行の中で最もコストのかかる部分であることが多いので、必要なオブジェクトだけを読み込むのが良いでしょう。 ## 実行順序 Isarは常に同じ順序でクエリーを実行します: 1. プライマリまたはセカンダリインデックスを走査してオブジェクトを見つける(where節の適用) 2. オブジェクトのフィルタリング 3. 結果のソート 4. distinct操作の適用 5. 結果のoffset と limit 6. 結果の返却 ## クエリの操作 これまでの例では、`.findAll()` を使ってマッチするオブジェクトをすべて取得しました。しかし、利用できる操作は他にも沢山あります。 | Operation | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | 最初にマッチしたオブジェクトのみを取得し、マッチしない場合は `null` を取得する。 | | `.findAll()` | マッチしたオブジェクトを全て取得する。 | | `.count()` | クエリにマッチするオブジェクトの数を数える。 | | `.deleteFirst()` | コレクションから、最初にマッチしたオブジェクトを削除する。 | | `.deleteAll()` | コレクションから、一致するすべてのオブジェクトを削除する。 | | `.build()` | クエリをコンパイルして、後で再利用することが出来る。これにより、クエリを複数回実行したい場合に、そのクエリを構築するためのコストを節約する事が出来る。 | ## プロパティクエリ 単一プロパティの値にしか関心が無く必要の無い場合、プロパティクエリを使用することができます。通常のクエリを構築し、プロパティを選択するだけです: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` 単一プロパティのみを使用することで、逆シリアル化の時間を節約できます。プロパティクエリは、埋め込みオブジェクトやリストに対しても機能します。 ## アグリゲーション(集約) Isarはプロパティクエリの値を集約する機能を持っています。以下の集約操作が可能です: | Operation | Description | | ------------ | -------------------------------------------------------------- | | `.min()` | 最小値を探す。該当するものがなければ `null` となる。 | | `.max()` | 最大値を探す。該当するものがなければ `null` となる。 | | `.sum()` | 全ての値を合計する。 | | `.average()` | すべての値の平均を計算し、一致するものがない場合は `NaN` を計算する。 | 一致するオブジェクトをすべて見つけて手動で集約するよりも、アグリゲーションを使用する方が、はるかに高速になります。 ## 動的なクエリ :::danger このセクションは、おそらくほとんどの方には関係ないでしょう。ダイナミッククエリの使用は、どうしても必要な場合(ほぼ無いです)を除き、お勧めしません。 ::: 今まで述べて来たすべての例は、QueryBuilderと生成された静的な拡張メソッドを使用しています。もしかしたら、動的なクエリや(Isar Inspectorのような)カスタムクエリ言語を作りたいかもしれません。その場合は、`buildQuery()` メソッドを使うことができます: | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------------- | | `whereClauses` | クエリのwhere節 | | `whereDistinct` | where 節が個別の値を返すかどうか(単一の where 節の場合のみ有効) | | `whereSort` | where節のトラバース(巡回)順序(単一のwhere節にのみ有効) | | `filter` | 結果に適用するフィル | | `sortBy` | ソートするプロパティの一覧 | | `distinctBy` | 区別するプロパティの一覧 | | `offset` | 結果のoffset | | `limit` | 返送する結果の最大数 | | `property` | nullで無い場合、このプロパティの値のみが返される。 | それでは動的なクエリを作成してみましょう: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` これらは以下のクエリに相当します。: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/ja/recipes/data_migration.md ================================================ --- title: データの移行 --- # データの移行 コレクション、フィールド、インデックスを追加または削除すると、Isarは自動的にデータベーススキーマを移行(マイグレート)します。時には、データも一緒に移行したい場合もあるでしょう。 Isarは組み込み解決法を提供していません。これはデタラメな移行制限が課される可能性があるためです。ただ、ニーズに合った移行ロジックを簡単に実装することができます。 この例では、データベース全体で1つのバージョンを使用したいと思います。共有環境設定を使って現在のバージョンを保存し、移行したいバージョンと比較します。バージョンが一致しない場合、データを移行し、バージョンを更新します。 :::tip 各コレクションに独自のバージョンを与え、個別に移行することも可能です。 ::: 誕生日フィールドを持つユーザーコレクションがあると仮定します。このアプリのバージョン2では、年齢に基づいてユーザーを照会するために、誕生年のフィールドを追加する必要があります。 Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` 問題は、バージョン1では `birthYear` フィールドが存在しないため、既存のUserモデルを作成しても空の `birthYear`が設定されることです。 `birthYear` フィールドを設定するために、データを移行する必要があります。 ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // バージョンが設定されていない場合(新規インストール)、または既にver.2の場合は移行する必要はない return; default: throw Exception('Unknown version: $currentVersion'); } // バージョンを更新する await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // すべてのユーザーを一度にメモリにロードするのを避けるため、ユーザーをページ分割する for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // birthYear ゲッターを使用しているため、何も更新する必要はありません await isar.users.putAll(users); }); } } ``` :::warning 多くのデータを移行する必要がある場合、UIスレッドに負担がかからないようにバックグラウンドアイソレートを使用することを検討してください。 ::: ================================================ FILE: docs/docs/ja/recipes/full_text_search.md ================================================ --- title: 全文検索 --- # 全文検索 全文検索は、データベース内のテキストを検索する強力な方法です。[インデックス](../indexes.md)がどのように機能するかについては既にご存じだとは思いますが、基本的なことを説明します。 インデックスはルックアップテーブルのように機能し、特定の値を持つレコードをクエリエンジンがすばやく検索できるようにします。たとえば、オブジェクトに `title` フィールドがある場合、そのフィールドにインデックスを作成することで、指定したタイトルを持つオブジェクトをより速く見つけることができます。 ## なぜ全文検索が便利なのか IsarDB ではフィルタを使って簡単にテキストを検索することができます。例えば、 `.startsWith()`, `.contains()`, `.matches()` のような様々な文字列操作があります。フィルタの問題は、その実行時間が `O(n)` (ここで `n` はコレクション内のレコードの数) であることです。特に、 `.matches()` のような文字列演算は時間がかかります。 :::tip 全文検索はフィルタよりはるかに高速ですが、インデックスにはいくつかの制限があります。このレシピでは、これらの制限を回避する方法を探ります。 ::: ## 基本例 考え方としては常に同じです:テキスト全体をインデックス化するのではなく、テキスト中の単語をインデックス化し、個別に検索できるようにします。 それではさっそく、基本的な全文インデックスを作成してみましょう: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` これで、content 内の特定の単語を検索できるようになりました: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` このクエリは高速に動作しますが、いくつかの問題があります: 1. 単語全体しか検索できない 2. 句読点は考慮しない 3. 他の空白文字の検索に対応していない ## テキストを正しく分割する 先ほどの例を改善してみましょう。単語分割を修正するために複雑な正規表現を開発しようとすることもできますが、おそらく時間がかかり、エッジケースで間違ってしまう可能性もあります。 [Unicode Annex #29](https://unicode.org/reports/tr29/)では、ほぼ全ての言語について、テキストを単語に正しく分割する方法を定義しています。これは非常に複雑ですが、幸いなことに、Isar は重い仕事を代わりにやってくれます。 ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## 他の機能の追加 他の機能も簡単に実装できますよ!プレフィックスマッチングや大文字小文字を区別しないマッチングをサポートするようにインデックスを変更することもできます。 ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` デフォルトでは、Isar は単語をハッシュ値として保存します。これは高速で容量効率のよい方法です。 しかし、ハッシュ値はプレフィックスマッチングに使用することはできません。インデックスを変更して、`IndexType.value` を使用すると、単語を直接利用することができます。これによって `.titleWordsAnyStartsWith()` という where 節を提供します。 ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## `.endsWith()` の実装 `.endsWith()` の実装も、勿論可能です!ここでは、`.endsWith()`のマッチングを実現するためのちょっとしたテクニックをお見せします。 ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` 検索したい語尾を反転(reversed)させることを忘れないようにしてください。 ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## ステミングアルゴリズム 残念ながら、インデックスは `.contains()` マッチングをサポートしていません (これは他のデータベースでも同様です)。しかし、いくつかの代替手段があり、検討する価値はあります。その選択肢は、用途に大きく依存します。その一例として、単語全体ではなく、単語の語幹をインデックス化する方法があります。 ステミングアルゴリズムは、言語の正規化プロセスで、単語のさまざまな形式を共通の形式に変換します: ``` connection connections connective ---> connect connected connecting ``` 一般的なアルゴリズムは、[Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) と [Snowball stemming algorithms](https://snowballstem.org/algorithms/) です。 また、[lemmatization](https://en.wikipedia.org/wiki/Lemmatisation) のような、より高度な形式もあります。 ## 音声学的アルゴリズム [音声アルゴリズム](https://en.wikipedia.org/wiki/Phonetic_algorithm)とは、発音によって単語を割り出すためのアルゴリズムです。つまり、探している単語と似た音の単語を見つけることができるのです。 :::warning 音声アルゴリズムの多くは、単一言語しかサポートしていません。 ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex)は、英語の発音で人名を索引付けするための音声アルゴリズムです。同音異義語が同じ表現にエンコードされ、スペルが多少違ってもマッチングできるようにすることが目的で作られています。これは簡単なアルゴリズムであり、複数の改良版が存在する。 このアルゴリズムを使うと、`"Robert"` と `"Rupert"` はともに `"R163"` という文字列を返し、 `"Rubin"` は `"R150"` を返します。`Ashcraft"` と `"Ashcroft"` は共に `"A261"` を返します。 ### Double Metaphone [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) 音素符号化アルゴリズムは、このアルゴリズムの第二世代です。このアルゴリズムは、オリジナルの Metaphone アルゴリズムと比較して、いくつかの基本的な設計上の改良がなされています。 Double Metaphone は、スラブ語、ゲルマン語、ケルト語、ギリシャ語、フランス語、イタリア語、スペイン語、中国語、およびその他の起源の英語におけるさまざまな不規則性を考慮しています。 ================================================ FILE: docs/docs/ja/recipes/multi_isolate.md ================================================ --- title: Multi-Isolateの使用法 --- # Multi-Isolateの使用法 スレッドの代わりに、すべてのDartのコードはアイソレートの内部で実行されます。それぞれのアイソレートは独自のメモリヒープを持ち、アイソレート内のどのステートも他のアイソレートからアクセスできないことを保証しています。 Isarは同時に複数のアイソレートからアクセスすることができ、ウォッチャーもアイソレートをまたいで動作します。このレシピでは、複数のアイソレート環境でIsarを使用する方法を確認します。 ## いつMulti-Isolateを使用すべきか Isarのトランザクションは、同じアイソレートで実行されても並列に実行されます。そうだとしても、場合によっては、複数のアイソレートからIsarにアクセスすることが 有益なこともあります。 その理由は、IsarはDartオブジェクトとの間でデータのエンコードとデコードにかなりの時間を費やしているからです。これはJSONのエンコードとデコードのようなものだと考えることができます。(ただ、より効率的です)これらの操作は、データがアクセスされるアイソレートの内部で実行され、当然アイソレート内の他のコードをブロックします。言い換えれば IsarはあなたのDartアイソレートで作業の一部を実行します。 一度に数百のオブジェクトを読み書きする必要があるだけなら、UIアイソレートで行うことは問題ではありません。しかし、巨大なトランザクションや、UIスレッドがすでにBusy状態である場合は、別のアイソレートを使用することを検討する必要があります。 ## 具体例 まず最初に行うべきことは、新しいアイソレートでIsarをオープンすることです。Isarのインスタンスは既にメインとなるアイソレートで開かれているので、 `Isar.open()` は同じインスタンスを返します。 :::warning メインアイソレートと同じスキーマを提供することを忘れないでください。そうでない場合は、エラーになります。 ::: `compute()` は Flutter で新しいアイソレートを開始し、その中で与えられた関数を実行します。 ```dart void main() { // UIアイソレートでIsarを開く final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // データベースの変更を監視する isar.messages.watchLazy(() { print('omg the messages changed!'); }); // 新しいアイソレートを開始し、10000メッセージを作成します。 compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // しばらくすると: // > omg the messages changed! // > isolate finished } // 新しいアイソレート内で実行される関数 Future createDummyMessages(int count) async { // インスタンスはすでに開かれているので、ここではPathは必要ありません。 final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // アイソレート内で同期トランザクションを使用する。 isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` 上記の例の中で、いくつか興味深い点があります: - `isar.messages.watchLazy()` は UI アイソレートで呼び出されていますが、他のアイソレートからの変更についても通知されている。 - インスタンスは名前(name)で参照されます。デフォルトの名前は `default` ですが、この例では `myInstance` に設定しました。 - メッセージを作成するために同期トランザクションを使用しました。新しいアイソレートをブロックすることは問題ありませんし、同期トランザクションは少し速くなります。 ================================================ FILE: docs/docs/ja/recipes/string_ids.md ================================================ --- title: 文字列のID --- # 文字列のID このチュートリアルは、私が最も頻繁に受け取るリクエストの一つです。 IsarはString IDを標準サポートしていませんが、それには理由があります。特にリンクの場合、String IDのオーバーヘッドが大きすぎるのです。 時には、UUIDやその他の整数型ではないidを利用する外部データを保存しなければならない場合があることは理解しています。 そこで、String idをオブジェクトのプロパティとして保存し、fastHashを実装して、Idとして使用できる64ビットintを生成することをお勧めします。 ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` この方法を使用すれば、リンク用の効率的な整数IDと、文字列IDを使用する機能という、両方の長所を得ることができます。 ## Fast hash 関数 ハッシュ関数は高品質で高速なものが理想的です(かつコリジョンは避けたい)。 そこで、以下のような実装をお勧めします。 ```dart /// Dart Stringsの為に最適化されたFNV-1a 64bitハッシュアルゴリズム int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` もし別のハッシュ関数を選択したい場合は、64ビットの整数値を返すことを確認する事に加えて、暗号化ハッシュ関数を使用することは避けてください(処理速度が大幅に低下します)。 :::warning `String.hashCode` は、異なるプラットフォームやバージョンの Dart で安定して動作することが保証されていないため、使用しないようにしましょう。 ::: ================================================ FILE: docs/docs/ja/schema.md ================================================ --- title: スキーマとは --- # スキーマとは Isar を使用してアプリのデータを保存する場合、コレクションを扱うことになります。コレクションとは、関連付けられた IsarDB 内のテーブルのようなもので、単一型の Dart オブジェクトのみを格納することができます。それぞれのコレクションオブジェクトは、対応するコレクションの行を表します。 コレクション定義は "スキーマ"と呼ばれます。Isar Generator は貴方のために手間のかかる面倒な作業を行い、コレクションを使用するのに必要なコードの大部分を生成してくれます。 ## コレクションの構造 Isar コレクションを定義するには、Class を `@collection` または `@Collection()` でアノテートします。 Isar コレクションは対応するテーブル内の各列となるフィールドを含みます。ここには、主キーを構成するフィールドも含めてください。 次のコードは、ID、名前、苗字の列を持つ `User` テーブルを定義するシンプルなコレクションの例です。 ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip フィールドを永続化するためには、Isar がそのフィールドにアクセスできる必要があります。フィールドを public にしたり、Getter や Setter のメソッドを用意したりすることで、Isar がフィールドにアクセスできるようになります。 ::: コレクションをカスタマイズするために、いくつかの任意のパラメータがあります: | Config | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `inheritance` | 親クラスや mixins のフィールドを Isar に保存するかどうかを管理します。デフォルトでは有効です。 | | `accessor` | デフォルトのコレクションアクセサの名前を変更できるようにします。 (たとえば、`Contact` コレクションには `isar.contacts` を指定など). | | `ignore` | 特定のプロパティを無視(除外)することができます。これらは、スーパークラスに対しても同様に適用されます。 | ### Isar の Id 各コレクションクラスは、オブジェクトを一意に識別する `Id` 型の id プロパティを定義する必要があります。`Id` は `int` の別名(エイリアス)で、IsarGenerator が id プロパティを識別できるようにするためのものです。 Isar は自動的に id フィールドにインデックスを作成するので、id に基づいて効率的にオブジェクトを取得したり変更したりすることができます。 id は自分で設定することもできますし、Isar にオートインクリメントの id を割り当ててもらうこともできます。もし`id` フィールドが `null` かつ `final` でない場合、Isar はオートインクリメントの id を割り当てます。NULL でないオートインクリメントの id が欲しい場合は、 `null` の代わりに `Isar.autoIncrement` を使用することができます。 :::tip オブジェクトが削除された場合、オートインクリメント ID は再利用されません。オートインクリメント ID をリセットする唯一の方法は、データベースを削除(Clear)することです。 ::: ### コレクションとフィールドの名前変更 デフォルトでは、Isar はクラス名をコレクション名として使用します。同様に、Isar はフィールド名をデータベースの列名として使用します。コレクションやフィールドに別の名前を付けたい場合は、 `@Name` アノテーションを追加します。次の例は、コレクションとフィールドの名前をカスタマイズする例です: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` 特に、既にデータベースに保存されている Dart のフィールドやクラスの名前を変更したい場合は、 `@Name` アノテーションの使用を検討する必要があります。そうしないと、データベースがそのフィールドやコレクションを削除したり、再作成したりすることになりかねません。 ### フィールドを無視する Isar は、コレクションクラスのすべての public フィールドを永続化します。プロパティや Getter に `@ignore` というアノテーションを付けると、次のコードスニペットのように永続化から除外することができます: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` コレクションが親コレクションからフィールドを継承しているような場合は、通常、 `@Collection` アノテーションの `ignore` プロパティを使用する方が簡単です: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` もし、コレクションに Isar がサポートしていない型のフィールドが含まれている場合、そのフィールドは無視しなければなりません。 :::warning 永続化されていない Isar オブジェクトに情報を保存することは、良い習慣ではないことに留意してください。 ::: ## 対応している型 Isar は以下のデータ型に対応しています: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` 加えて、埋め込み型オブジェクトと列挙型(Enum)もサポートされています。それらについては後述します。 ## byte, short, float 多くの場合、64 ビット整数型や double の全範囲は必要ありませんよね。Isar は、より小さな数値を保存する際の為に、容量とメモリを節約することができる追加の型をサポートしています。 | Type | Size in bytes | Range | | ---------- | ------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | 追加の数値型は Dart のネイティブ型の別名(エイリアス)に過ぎません。例えば `short` を使用すると、 `int` を使用するのと同じように動作します。 以下に、上記のすべての型を含むコレクションの例を示します: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` すべての数値型は List でも使用することができます。バイトを格納する場合は、`List` を使用してください。 ## Null 許容型 Isar で nullability(訳注:DB 関連用語では、列などの項目が NULL 値を受け入れる能力)がどのように機能するかを理解するのは非常に重要です: 数値型は、専用の `null` 表現を持ちません。その代わりに、特定の値が使用されます: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, `List` は、それぞれ別の `null` 表現を持ちます。 この動作によってパフォーマンスが向上し、 `null` 値を処理するためのマイグレーションや特別なコードを必要とせずに、フィールドの nullability を自由に変更することができるようになります。 :::warning `byte` 型は null 値をサポートしていません。 ::: ## DateTime Isar は、日付のタイムゾーン情報を保存しません。その代わり、`DateTime`を UTC に変換してから保存します。Isar はすべての日付をローカルタイムで返します。 `DateTime`はマイクロ秒の精度で保存されます。ただしブラウザ上においては、JavaScript の制限により、ミリ秒の精度しかサポートされていません。 ## 列挙型(Enum) Isar では他の型と同様に、列挙型を保存し使用することができます。しかし、Isar がディスク上でどのように enum を表すかを選択する必要があります。Isar は 4 つの異なる方法をサポートしています。: | EnumType | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------- | | `ordinal` | 列挙型のインデックスは `byte` として格納されます。これは非常に効率的ですが、null 値を許容する enum は使用できません。 | | `ordinal32` | 列挙型のインデックスは `short` (4 バイトの整数) として格納されます。 | | `name` | 列挙名称は `String` として格納されます。 | | `value` | 列挙値の取得には、カスタムプロパティを使用します。 | :::warning `ordinal` と `ordinal32` は、列挙された値の順番に依存します。この順序を変更すると、既存のデータベースは不正な値を返す可能性があります。 ::: それでは、それぞれの方法の例を確認してみましょう。 ```dart @collection class EnumCollection { Id? id; @enumerated // EnumType.ordinalと同様 late TestEnum byteIndex; // null 許容には出来ない @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // null 許容には出来ない @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` もちろん、Enum は List 内でも使用可能です。 ## 組み込みオブジェクト コレクションモデルでオブジェクトをネストさせると便利なことがよくあります。オブジェクトをネストさせる深さは無制限です。しかし、深くネストされたオブジェクトを更新するには、オブジェクトツリー全体をデータベースに書き込む必要があることを覚えておいてください。 ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` 埋め込みオブジェクトは null を許容する事も出来ますし、他のオブジェクトを拡張(extend)することも出来ます。唯一の要件は `@embedded` のアノテーションを付け、required パラメータの無いデフォルトのコンストラクタを持つことです。 ================================================ FILE: docs/docs/ja/transactions.md ================================================ --- title: トランザクション --- # トランザクション Isarにおいて、トランザクションは複数のデータベース操作を1つの作業単位にまとめます。Isarの大半の処理は、暗黙のうちにトランザクションを利用しています。IsarのRead & write操作は、[ACID](http://en.wikipedia.org/wiki/ACID)に準拠しており、トランザクションは、エラーが発生すると自動的にロールバックされます。 ## 明示的なトランザクション 明示的なトランザクションでは、データベースの整合性のあるスナップショットを取得したり、トランザクションの継続時間を最小限にするようにします。また、トランザクションの中でネットワークの呼び出しやその他の長時間実行される操作を行うことは禁じられています。 トランザクション(特に書き込みトランザクション)にはコストがかかるので、連続する操作は常に1つのトランザクションにまとめるようにしましょう。 トランザクションには、同期と非同期があります。同期トランザクションでは、同期操作のみを使用することができて、非同期トランザクションでは、非同期操作のみを使用することができます。 | | Read | Read & Write | |--------------|--------------|--------------------| | 同期 | `.txnSync()` | `.writeTxnSync()` | | 非同期 | `.txn()` | `.writeTxn()` | ### 読み取りトランザクション 明示的な読み取りトランザクションは任意ですが、Atomic(原子性の)読み取りを可能にし、トランザクション内のデータベースの一貫した状態に依存することができます。内部的には、Isarはすべての読み込み操作に対して常に暗黙的な読み込みトランザクションを使用します。 :::tip 非同期読み取りトランザクションは、他の読み取りおよび書き込みトランザクションと並行して実行されます。かなりイケてますよね? ::: ### 書き込みトランザクション 読み込み操作とは異なり、Isarでの書き込み操作は明示的なトランザクションに包まれる必要があります。 書き込みトランザクションが正常に終了すると、自動的にコミットされ、すべての変更がディスクに書き込まれます。もしエラーが発生した場合、トランザクションは中断され、すべての変更がロールバックされます。トランザクションは "all or nothing"です。トランザクション内のすべての書き込みが成功するか、データの一貫性を保証するために何も実行されないかのどちらかである。 :::warning データベース操作に失敗した場合、トランザクションは破棄され、それ以降使用してはいけません。たとえDartでエラーを捕捉したとしてもです。 ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: トランザクションの中にforループを移動させましょう。 for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/ja/tutorials/quickstart.md ================================================ --- title: クイックスタート --- # クイックスタート お待たせしました。さあ、最高にクールなFlutterのデータベースを使い始めましょう! この記事では、簡潔にコードを書いていきます。 ## 1. 依存関係を追加する Isarを使用する前に、いくつかのパッケージを `pubspec.yaml` に追加する必要があります。pubを使用する事で、面倒な作業を簡単に済ませることが出来ます。 ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. クラスの注釈(アノテーション) あなたの使用するコレクションクラスに `@collection` でアノテーションを付け、`Id` フィールドを設定します。 ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // id = nullでも自動インクリメントされます。 String? name; int? age; } ``` idはコレクション内のオブジェクトを一意に識別して、後で再び見つけられるようにします。 ## 3. コード生成ツールの実行 以下のコマンドを実行して、`build_runner`を起動します。: ``` dart run build_runner build ``` Flutterを使用している場合は、代わりに次のコマンドを使用してください: ``` flutter pub run build_runner build ``` ## 4. Isarインスタンスを開く 新規のIsarインスタンスを開き、コレクションのスキーマを渡します。必要に応じて、インスタンス名とディレクトリを指定することができます。 ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. 書き込みと読み込み Isarインスタンスを開いたら, コレクションを利用することができます. 基本的なCRUD操作は、全て `IsarCollection` を介して行う事が出来ます。 ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // 挿入と更新 }); final existingUser = await isar.users.get(newUser.id); // 取得 await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // 削除 }); ``` ## その他の資料 視覚的に学ぶ方が好みであれば、Isarを始めるためにこれらの動画をぜひご覧ください:


================================================ FILE: docs/docs/ja/watchers.md ================================================ --- title: ウォッチャー --- # ウォッチャー Isar では、データベースの変更を監視することができます。特定のオブジェクトやコレクション全体、あるいはクエリの変更を "監視" することができます。 ウォッチャーを使うと、データベースの変更に迅速に対応することができます。例えば、連絡先が追加されたときに UI を再構築したり、ドキュメントが更新されたときにネットワークリクエストを送ったりすることができます。 ウォッチャーは、トランザクションが正常にコミットされ、ターゲットが実際に変更された後に通知されます。 ## オブジェクトの監視 特定のオブジェクトが作成、更新、削除されたときに通知を受けたい場合、そのオブジェクトを監視する必要があります: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` 上記の例からわかるように、オブジェクトはまだ存在しなくてもかまいません。オブジェクトが作成されると、ウォッチャーに通知されます。 追加のパラメータとして `fireImmediately` があります。これを `true` に設定すると、Isar はオブジェクトの現在の値を即座に Stream に追加します。 ### レイジーウォッチング 新しい値を受け取る必要はなく、変更された事についてのみ通知して欲しい場合があるかもしれません。 その場合、Isarはオブジェクトを取得する手間を省くことができます。 ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## コレクションの監視 単一のオブジェクトを監視する代わりに、コレクション全体を監視し、いずれかのオブジェクトが追加、更新、または削除されたときに通知を受けることができます: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## クエリの監視 クエリ全体を監視することも可能です。Isarは、クエリの結果が実際に変更されたときのみ通知するよう最善を尽くします。ただ、リンクが原因でクエリが変更された場合は通知されません。リンクの変更について通知を受ける必要がある場合は、コレクションウォッチャーを使用してください。 ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning offset & limit や distinct クエリを使用する場合, オブジェクトがフィルタにマッチしたが、クエリの外でoffset & limitなどから結果が変化した場合にも、Isar は通知します。 ::: `watchObject()` と同様に、`watchLazy()` を使うと、クエリの結果が変わっても、結果を取得せずに通知を受けることができます。 :::danger 変更があるたびにクエリを再実行するのは非効率です。その代わりにLazyコレクションウォッチャーを使うとよいでしょう。 ::: ================================================ FILE: docs/docs/ko/README.md ================================================ --- home: true title: 홈 heroImage: /isar.svg actions: - text: 시작하기! link: /tutorials/quickstart.html type: primary features: - title: 💙 플러터를 위해 만들었어요 details: 설정은 최소로, 사용하기 쉽고, 구성도 없고, 보일러 플레이트도 없습니다. 코드 몇 줄만 추가하면 바로 시작할 수 있습니다. - title: 🚀 뛰어난 확장성 details: 수십만 개의 레코드를 단일 NoSQL 데이터베이스에 저장하고 효율적이고 비동기적으로 쿼리할 수 있습니다. - title: 🍭 풍부한 기능 details: Isar에는 데이터를 관리하기 위한 다양한 기능이 있습니다. 복합 & 다중 항목 인덱스, 쿼리 수정자, JSON 지원 등이 있습니다. - title: 🔎 전체 텍스트 검색 details: Isar는 전체 텍스트 검색 기능이 내장되어 있습니다. 다중 항목 색인을 작성하고 레코드를 쉽게 검색할 수 있습니다. - title: 🧪 ACID 시멘틱 details: Isar는 ACID를 준수하며 트랜잭션을 자동으로 처리합니다. 오류가 발생하면 변경 내용을 롤백합니다. - title: 💃 정적 타입 details: Isar의 쿼리는 정적 타입이고, 컴파일 시간에 검사됩니다. 런타임 오류에 대해 걱정할 필요가 없습니다. - title: 📱 다중 플랫폼 details: iOS, Android, Desktop 및 완전한 웹 지원! - title: ⏱ 비동기 details: 병렬 쿼리 작업 및 다중 Isolate 지원을 즉시 사용할 수 있습니다. - title: 🦄 오픈 소스입니다. details: 모든 것이 오픈 소스이며 영원히 무료입니다. footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/ko/crud.md ================================================ --- title: CRUD 조작 --- # CRUD 조작 컬렉션이 정의되었다면, 이제 조작하는 방법을 배워봅시다! ## Isar 열기 무엇을 하든 우선 Isar 인스턴스가 필요합니다. 각 인스턴스에는 데이터베이스 파일을 저장할 수 있도록 쓰기 권한이 있는 디렉토리가 필요합니다. 디렉토리를 지정하지 않는 경우 Isar는 현재 플랫폼에 적합한 기본 디렉토리를 찾습니다. Isar 인스턴스에서 사용하고 싶은 모든 스키마를 지정합니다. 여러 인스턴스를 열고 있는 경우에도 각각의 인스턴스에 동일한 스키마를 부여해야 합니다. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` 기본 구성을 사용하거나 다음 매개 변수 중 일부를 제공할 수 있습니다: | Config | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | 여러 인스턴스를 다른 이름으로 엽니다. 기본적으로 `"default"`가 사용됩니다. | | `directory` | 이 인스턴스의 저장 장소 입니다. 상대 경로 또는 절대 경로를 전달할 수 있습니다. 기본값으로, iOS 에서는 `NSDocumentDirectory`, Android 에서는 `getDataDirectory` 가 사용됩니다. 웹에서는 필요하지 않습니다. | | `maxSizeMib` | 데이터베이스 파일의 최대 크기(MiB)입니다. Isar는 무한하지 않은 가상 메모리를 사용하므로 여기 값을 명심하세요. 여러 인스턴스를 열면 사용 가능한 가상 메모리가 공유되므로 각 인스턴스의 `maxSizeMib` 가 더 작아집니다. 기본값은 2048 입니다. | | `relaxedDurability` | 내구성 보장을 완화하여 쓰기 성능을 향상 시킵니다. 시스템 충돌(앱 충돌이 아닌) 의 경우 마지막으로 커밋된 트랜잭션이 손실될 수 있습니다. 완전히 파손(Corruption)될 가능성은 없습니다. | | `compactOnLaunch` | 인스턴스를 열 때 데이터베이스를 압축해야 하는지 여부를 확인하는 조건입니다. | | `inspector` | 디버그 빌드에 대해서 Inspector 를 사용하도록 설정합니다. 프로파일이나 릴리즈 빌드에서는 이 옵션이 무시됩니다. | 인스턴스가 이미 열려 있는 경우 `Isar.open()` 을 호출하면 지정된 매개 변수에 관계없이 기존 인스턴스가 생성됩니다. isolate 안에서 Isar 를 사용할 때 유용합니다. :::tip 모든 플랫폼에서 유효한 저장 경로를 얻기 위해서 [path_provider](https://pub.dev/packages/path_provider) 패키지를 사용하는 것을 고려해보세요. ::: 데이터베이스 파일의 저장 위치는 `directory/name.isar` 입니다. ## 데이터베이스에서 읽기 Isar 에서 지정된 타입의 새로운 객체를 찾고 쿼리하고 생성할 때 `IsarCollection` 인스턴스를 사용합니다. 밑에 나올 예시들에서, 우리는 다음과 같이 정의된 `Recipe` 컬렉션이 있다고 가정합니다. ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### 컬렉션을 가져오기 모든 컬렉션들은 Isar 인스턴스 안에 있습니다. 레시피 컬렉션은 다음 방법으로 가져옵니다: ```dart final recipes = isar.recipes; ``` 너무 쉽죠! 컬렉션 접근자를 사용하기 싫다면, `collection()` 메서드를 사용해도 됩니다. ```dart final recipes = isar.collection(); ``` ### 객체 얻기 (id를 이용) 아직 컬렉션에 데이터가 들어있지 않지만, 아이디 `123` 의 가상의 객체가 있다고 가정하고 가져오겠습니다. ```dart final recipe = await isar.recipes.get(123); ``` `get()` 은 객체를 `Future` 로 반환하고, 해당 객체가 존재하지 않는 경우에는 `null` 을 반환합니다. 모든 Isar 작업들은 기본적으로 비동기적으로 작동합니다. 대부분의 경우는 동기적인 방법도 가지고 있습니다. ```dart final recipe = isar.recipes.getSync(123); ``` :::warning UI isolate 에서는 비동기 버전을 기본적으로 사용해야 합니다. 하지만 Isar 는 매우 빠르기 때문에, 동기식으로 사용하는 것도 종종 허용됩니다. ::: 한 번에 여러 객체를 가져오려면 `getAll()` 또는 `getAllSync()` 를 사용하세요: ```dart final recipe = await isar.recipes.getAll([1, 2]); ``` ### 객체 쿼리 id를 이용해서 객체를 가져오는 대신, `.where()` 과 `.filter()` 를 사용해서 특정 조건에 맞는 객체 목록을 쿼리할 수 있습니다: ```dart final allRecipes = await isar.recipes.where().findAll(); final favouires = await isar.recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ 더 알아보기: [Queries](queries) ## 데이터베이스 수정하기 드디어 컬렉션을 수정할 때가 됐습니다! 객체를 생성, 갱신, 삭제하려면 쓰기 트랜잭션 안에서 각각의 작업들을 수행하세요. ```dart await isar.writeTxn(() async { final recipe = await isar.recipes.get(123) recipe.isFavorite = false; await isar.recipes.put(recipe); // 갱신 작업을 수행합니다. await isar.recipes.delete(123); // 또는 삭제 작업 }); ``` ➡️ 더 알아보기: [Transactions](transactions) ### 객체 삽입 Isar 에 객체를 보존하기 위해서, 컬렉션에 집어넣어야 합니다. 컬렉션에 객체를 삽입할 때는 Isar의 `put()` 메소드를 이용합니다. 만약 이미 들어있는 객체라면 갱신을 합니다. id 필드가 `null` 이나 `Isar.autoIncrement` 라면, Isar 는 자동 증분 아이디를 사용합니다. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await isar.recipes.put(pancakes); }) ``` `id` 필드가 final 이 아닌 경우에 Isar 가 id를 객체에 자동으로 할당합니다. 여러 객체를 한 번에 삽입하는 것도 쉽습니다: ```dart await isar.writeTxn(() async { await isar.recipes.putAll([pancakes, pizza]); }) ``` ### 객체 갱신 `collection.put(object)` 를 이용해서 만들고 갱신하는 동작을 모두 할 수 있습니다. id가 `null`(또는 존재하지 않는 경우) 이라면, 객체는 추가됩니다. 그 이외의 경우에는 갱신됩니다. 만약 팬케익에 즐겨찾기를 해제하는 경우, 이렇게 할 수 있습니다. ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await isar.recipes.put(pancakes); }); ``` ### 객체 삭제 Isar 에 있는 것을 없애고 싶나요? `collection.delete(id)` 를 사용하세요. delete 메소드는 주어진 id 를 가진 객체를 찾아서 삭제합니다. id `123` 을 가지는 객체를 삭제하는 예시 입니다: ```dart await isar.writeTxn(() async { final success = await isar.recipes.delete(123); print('Recipe deleted: $success'); }); ``` get 과 put 과 마찬가지로 delete 에도 여러개를 한꺼번에 삭제하는 방법이 있습니다. 삭제된 객체의 수를 반환합니다. ```dart await isar.writeTxn(() async { final count = await isar.recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` 만약 삭제할 객체의 id 를 모른다면 query 를 사용할 수 있습니다. ```dart await isar.writeTxn(() async { final count = await isar.recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/ko/faq.md ================================================ --- title: 자주 묻는 질문들 --- # 자주 묻는 질문들 Isar 와 Flutter 데이터베이스에 대해서 자주 물어보는 질문들을 랜덤으로 뽑아봤습니다. ### 데이터베이스가 왜 필요하죠? > 저는 백엔드 데이터베이스에 데이터를 보관해요. 왜 Isar 가 필요하죠? 심지어 요즘에도, 지하철이나 비행기 안에 있거나 와이파이가 없는 할머니 집을 갈 때는 데이터 연결이 없는 경우가 흔하게 있습니다. 나쁜 연결로 인해서 앱이 먹통이 되는 일이 없어야 합니다! ### Isar vs Hive 답은 간단합니다: Isar 는 [Hive의 대체재로 시작](https://github.com/hivedb/hive/issues/246) 했었고 지금은 저는 항상 Hive보다 Isar 를 사용하는 것을 추천합니다. ### Where 절?! > 왜 **_내_** 가 어떤 인덱스를 사용할 지 선택해야 합니까? 여러 이유가 있습니다. 대부분의 데이터베이스는 휴리스틱을 사용해서 주어진 쿼리에 가장 적합한 인덱스를 선택합니다. 데이터베이스가 추가 사용량 데이터(-> 오버헤드) 를 수집해야 하지만 여전히 잘못된 인덱스를 선택할 수 있습니다. 또한 쿼리를 작성하는 속도가 느려지게 됩니다. 개발자인 여러분보다 당신의 데이터를 잘 아는 사람은 아무도 없습니다. 따라서 최적의 인덱스를 선택하고 쿼리나 정렬에 사용할 인덱스를 결정할 수 있습니다. ### 인덱스 / where 절을 사용해야 합니까? 아뇨! 필터에만 의존해도 Isar 는 충분히 빠릅니다. ### Isar 가 충분히 빠른가요? Isar는 모바일용 데이터베이스 중 가장 빠르기 때문에 대부분의 사용 사례에서 충분히 빠릅니다. 성능 문제가 발생하면 뭔가를 잘못했을 가능성이 높습니다. ### Isar 가 제 앱의 크기를 늘리나요? 조금은 그렇죠. Isar는 다운로드 크기를 1 - 1.5 MB 정도 늘릴 겁니다. Isar Web 은 몇 KB 만 추가합니다. ### 문서가 잘못됐네요 / 오타가 있어요. 이런, 죄송해요. [이슈 열기](https://github.com/isar-community/isar/issues/new/choose) 또는 PR 을 통해서 고쳐주세요. 💪. ================================================ FILE: docs/docs/ko/indexes.md ================================================ --- title: 인덱스 --- # 인덱스 인덱스는 Isar 의 가장 강력한 기능입니다. 대부분의 내장 데이터베이스는 "일반적인" 인덱스만을 제공하지만(인덱스가 있다면요), Isar 에는 복합 및 다중 항목 인덱스도 있습니다. 쿼리 성능을 최적화하려면 인덱스 작동 방식을 이해하는 것이 필수적입니다. Isar 를 사용하면 사용할 인덱스와 인덱스 사용 방법을 선택할 수 있습니다. 인덱스가 무엇인지에 대한 간단한 소개로 시작하겠습니다. ## 인덱스가 뭔가요? 컬렉션이 인덱싱되지 않은 경우, 쿼리 입장에서는 행의 순서가 전혀 최적화 되지 않은 것으로 식별되지 않을 수 있습니다. 그래서 쿼리는 객체를 선형으로 검색해야만 합니다. 즉, 쿼리는 조건과 일치하는 객체를 찾기 위해서 모든 객체를 검색해야 합니다. 예상한대로, 그건 시간이 오래 걸립니다. 모든 객체를 하나하나 훑어보는 것은 그다지 효율적이지 않습니다. 예를 들어 이 `Product` 컬렉션에는 전혀 순서가 없습니다. ```dart @collection class Product { Id? id; late String name; late int price; } ``` #### 데이터: | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | 가격이 30 유로 이상인 모든 제품을 찾는 쿼리는 9개 행을 모두 검색해야 합니다. 9개 행은 문제가 없지만, 10만 행이 되면 문제가 될 수 있습니다. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` 이 쿼리 성능을 개선하기 위해서 우리는 `price` 속성을 인덱스해야 합니다. 인덱스는 정렬된 룩업 테이블과 같습니다. ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` #### 생선된 인덱스: | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | 이제 쿼리는 훨씬 빠르게 실행할 수 있습니다. 실행자(executor) 는 마지막 3 개의 인덱스 행으로 바로 이동해서 ID 로 해당 객체를 찾을 수 있습니다. ### 정렬 또 다른 멋진 점은 인덱스가 매우 빠른 정렬을 할 수 있다는 것입니다. 정렬된 쿼리는 정렬하기 전에 데이터베이스가 모든 결과를 메모리에 로드해야 하므로 비용이 많이 듭니다. 오프셋이나 제한을 지정하더라도 정렬 이후에 적용됩니다. 가장 싼 4개의 제품을 찾고 싶다고 가정해 보겠습니다. 다음 쿼리를 사용할 수 있습니다. ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` 이 예에서 데이터베이스는 모든 (!) 객체를 로드하고 가격별로 정렬한 다음 가장 낮은 가격으로 4개의 제품을 반환해야 합니다. 예상대로, 전의 인덱스를 사용하면 훨씬 효율적으로 작업을 수행할 수 있습니다. 데이터베이스는 인덱스의 처음 4개 행을 사용하고 해당 객체가 이미 올바른 순서에 있으므로 해당 객체를 반환합니다. 정렬에 인덱스를 사용하려면 다음과 같이 쿼리를 작성합니다. ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` `.anyX()` 여기서 절은 Isar 에 정렬에만 인덱스를 사용하도록 지시합니다. `.priceGreaterThan()` 과 같은 where 절을 사용해서 정렬된 결과를 얻을 수도 있습니다. ## 고유 인덱스(Unique indexes) 고유 인덱스는 인덱스에 중복된 값이 포함되지 않게 합니다. 고유 인덱스는 하나 이상의 속성으로 이루어 집니다. 고유한 인덱스에 속성이 하나 있으면 이 속성의 값이 고유하게 됩니다(중복이 허용되지 않게 됩니다). 고유 인덱스에 둘 이상의 속성이 있는 경우 이러한 속성의 값 조합은 고유합니다. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` 중복을 유발하는 데이터 삽입이나 업데이트를 시도하면 오류가 발생합니다: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> 괜찮습니다. final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // 같은 유저 이름으로 유저 삽입을 시도 await isar.users.put(user2); // -> 에러: 고유 제약조건 위반 print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## 인덱스 대체 (replace indexes) 고유 제약조건을 위반할 경우에 에러가 발생하는 것이 좋지 않을 수도 있습니다. 대신에 기존 객체를 새로운 객체로 대체할 수 있습니다. 이는 인덱스의 `replace` 속성을 `true` 로 설정해서 수행할 수 있습니다. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` 이제 기존 사용자 이름을 가진 사용자를 삽입하려고 하면 Isar 가 기존 사용자를 새 사용자로 대체합니다. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` 인덱스 대체는 객체를 바꾸는 대신 업데이트할 수 있는 `putBy()` 메서드를 생성합니다. 기존 ID 는 재사용되고 링크는 여전히 채워집니다. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user does not exist so this is the same as put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` As you can see, the id of the first inserted user is reused. ## Case-insensitive indexes All indexes on `String` and `List` properties are case-sensitive by default. If you want to create a case-insensitive index, you can use the `caseSensitive` option: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## 인덱스 유형 There are different types of indexes. Most of the time, you'll want to use an `IndexType.value` index, but hash indexes are more efficient. ### Value index Value indexes are the default type and the only one allowed for all properties that don't hold Strings or Lists. Property values are used to build the index. In the case of lists, the elements of the list are used. It is the most flexible but also space-consuming of the three index types. :::tip Use `IndexType.value` for primitives, Strings where you need `startsWith()` where clauses, and Lists if you want to search for individual elements. ::: ### Hash index Strings and Lists can be hashed to reduce the storage required by the index significantly. The disadvantage of hash indexes is that they can't be used for prefix scans (`startsWith` where clauses). :::tip Use `IndexType.hash` for Strings and Lists if you don't need `startsWith`, and `elementEqualTo` where clauses. ::: ### HashElements index String lists can be hashed as a whole (using `IndexType.hash`), or the elements of the list can be hashed separately (using `IndexType.hashElements`), effectively creating a multi-entry index with hashed elements. :::tip Use `IndexType.hashElements` for `List` where you need `elementEqualTo` where clauses. ::: ## Composite indexes A composite index is an index on multiple properties. Isar allows you to create composite indexes of up to three properties. Composite indexes are also known as multiple-column indexes. It's probably best to start with an example. We create a person collection and define a composite index on the age and name properties: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` #### Data: | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | #### Generated index | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | The generated composite index contains all persons sorted by their age their name. Composite indexes are great if you want to create efficient queries sorted by multiple properties. They also enable advanced where clauses with multiple properties: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` The last property of a composite index also supports conditions like `startsWith()` or `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Multi-entry indexes If you index a list using `IndexType.value`, Isar will automatically create a multi-entry index, and each item in the list is indexed toward the object. It works for all types of lists. Practical applications for multi-entry indexes include indexing a list of tags or creating a full-text index. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` splits a string into words according to the [Unicode Annex #29](https://unicode.org/reports/tr29/) specification, so it works for almost all languages correctly. #### Data: | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Entries with duplicate words only appear once in the index. #### Generated index | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | This index can now be used for prefix (or equality) where clauses of the individual words of the description. :::tip Instead of storing the words directly, also consider using the result of a [phonetic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) like [Soundex](https://en.wikipedia.org/wiki/Soundex). ::: ================================================ FILE: docs/docs/ko/limitations.md ================================================ # 제한 사항 아시다시피 Isar 는 VM 에서 실행되는 모바일 장치 및 데스크톱과 웹에서 작동합니다. 두 플랫폼은 매우 다르고 다른 한계점을 가지고 있습니다. As you know, Isar works on mobile devices and desktops running on the VM as well as Web. Both platforms are very different and have different limitations. ## VM 에서의 제한사항 - where 절에는 문자열의 처음 1024바이트만 사용할 수 있습니다. - 객체의 크기는 16MB 를 넘을 수 없습니다. ## 웹 에서의 제한사항 Isar Web 은 IndexedDB 에 의존하고 있습니다. 그래서 더 많은 제약이 있지만, Isar 를 사용하는 동안 거의 눈치채기 어렵습니다. - 동기식 메서드들은 지원되지 않습니다. - 현재 `Isar.splitWords()` 및 `.matches()` 필터가 구현되지 않았습니다. - 스키마 변경 사항이 VM에서만큼 엄격하게 확인되지 않기 때문에 규칙을 준수하도록 주의하십시오. - 모든 숫자 유형이 두 배(js의 number 타입) 으로 저장되므로 `@Size32` 가 효과가 없습니다. - 해시 인덱스가 더 적은 공간을 사용하지 않도록 인덱스가 다르게 표기됩니다.(여전히 동일하게 작동합니다.) - `col.delete()` 및 `col.deleteAll()` 이 올바르게 작동하지만 반환 값은 올바르지 않습니다. - `col.clear()` 자동 증분 값을 초기화 하지 않습니다. - `NaN` 값으로 지원되지 않습니다. ================================================ FILE: docs/docs/ko/links.md ================================================ --- title: 링크 --- # 링크 링크를 사용해서 댓글 작성자(사용자) 같은 객체 간의 관계를 나타낼 수 있습니다. Isar 링크를 사용해서 `1:1`, `1:n`, 과 `n:n` 관계를 모델링할 수 있습니다. 링크를 사용하는 것은 내장된 객체를 사용하는 것보다 인체 공학적이지 않으므로(less ergonomic), 가능하다면 임베드된 객체를 사용해야 합니다. 링크를 관계를 포함하는 별도의 테이블로 간주합니다. SQL 관계와 비슷하지만 사용가능한 기능과 API 가 다릅니다. ## IsarLink `IsarLink` 는 관련 객체를 포함하지 않거나 하나만 포함할 수 있으며 일대일 관계를 표현하는데 사용할 수 있습니다. `IsarLink` 에는 연결된 객체를 가지는 `value` 라는 단일 속성이 있습니다. 링크는 게으르므로 `IsarLink` 에 `value` 를 명시적으로 로드하거나 저장하도록 지시해야 합니다. `linkProperty.load()` 및 `linkProperty.save()` 를 호출하여 이 작업을 수행할 수 있습니다. :::tip 링크의 원본 및 대상 컬렉션의 ID 타입은 final 이 아니어야 합니다. ::: 웹이 아닌 타겟에서는, 링크를 처음 사용할 때 링크가 자동 로드 됩니다. 먼저 IsarLink 를 컬렉션에 추가합니다: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` 우리는 선생님과 학생들 사이의 링크를 정의했습니다. 이 예에서 모든 학생은 정확히 한 명의 선생님만 가질 수 있습니다. 먼, 우리는 선생님을 만들어 학생에게 할당합니다. 우리는 선생님을 `.put()` 하고 링크를 수동으로 저장해야 합니다. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teachers.save(); }); ``` 우리는 이제 링크를 이용할 수 있습니다: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` 동기 코드로 똑같이 해보겠습니다. `.putSync()` 는 모든 링크를 자동으로 저장하므로 수동으로 링크를 저장할 필요가 없습니다. 심지어 우리를 위해서 선생님을 만듭니다. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks 이전 예시의 학생이 여러 명의 선생님을 가질 수 있다면 더 그럴듯할 것입니다. 다행히 Isar는 `IsarLinks` 를 가지고 있습니다. 여러 개의 관련 객체를 포함할 수 있으며 -N 관계(1:N, N:N)를 표현할 수 있습니다. `IsarLinks` 는 `Set` 을 확장하고, set에서 사용하는 모든 메서드들을 사용할 수 있습니다. `IsarLinks` 는 `IsarLink` 와 비슷한 행동을 하고 lazy 합니다. 연결된 모든 객체를 로드하려면 `linkProperty.load()`를 호출하세요. 변경 내용을 유지하려면, `linkProperty.save()` 를 호출하세요. 내부적으로 `IsarLink` 와 `IsarLinks` 는 동일한 방식으로 표현됩니다. 우리는 이전 예제의 `IsarLink` 를 `IsarLinks` 로 업그레이드 해서 한 학생에 여러 선생님들을 할당할 수 있습니다. ```dart @collection class Student { Id? id; late String name; final teacher = IsarLinks(); } ``` 우리가 링크(`teacher`)의 이름을 바꾸지 않았기 때문에 Isar 는 이전의 것들을 기억하고 있습니다. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## 백링크 (Backlinks) 이렇게 물을 수 있습니다. "만약 역관계를 표현하려면 어떻게 해야 하나요?". 걱정마세요; 백링크가 있습니다. 백링크는 역방향 링크입니다. 각 링크들은 항상 암시적인 백링크를 가지고 있습니다. `IsarLink`, `IsarLinks` 에 `@BackLink()` 어노테이션을 써서 만들 수 있습니다. 백링크는 추가적인 메모리나 자원을 필요로 하지 않습니다; 항상 데이터 손실 없이 자유롭게 추가하고 삭제하고 이름을 바꿀 수 있습니다. 우리는 특정한 선생님이 어떤 학생을 가지고 있는지를 알고 싶습니다, 그래서 백링크를 정의합니다: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` 우리는 백링크가 어느 링크를 가르키는지를 지정해야 합니다. 두 개의 객체 간에도 여러 링크가 있을 수 있습니다. ## 링크들을 초기화하기 `IsarLink` 와 `IsarLinks` 는 매개변수가 없는 생성자를 가지고, 객체가 만들어 질 때 링크 속성을 할당해야 합니다. 링크 속성을 `final` 로 만드는 것이 좋은 습관입니다. 객체를 처음으로 `put()` 할 때, 링크는 소스 컬렉션과 대상 컬렉션으로 초기화 됩니다. 그 이후, `load()` 와 `save()` 같은 메서드를 호출할 수 있습니다. 링크가 생성된 후 바로 변경 사항을 추적하기 시작하므로 링크가 초기화 되기 전에도 관계를 추가하거나 제거할 수 있습니다. :::danger 링크를 또 다른 개체로 옮기는 것은 금지되어 있습니다. ::: ================================================ FILE: docs/docs/ko/queries.md ================================================ --- title: 쿼리 --- # 쿼리 쿼리는 특정 조건들에 맞는 레코드들을 찾는 방법입니다. 예: - 별표로 표시된 모든 연락처를 찾습니다. - 연락처에서 고유한 이름들을 찾습니다. - 성이 정의되지 않은 모든 연락처를 삭제합니다. 쿼리는 다트가 아닌 데이터베이스에서 실행되기 때문에 매우 빠릅니다. 인덱스를 똑똑하게 사용하면 쿼리 성능을 더욱 더 향상시킬 수 있습니다. 아래에서는 쿼리를 작성하는 방법과 쿼리를 가능한 한 빨리 작성하는 방법에 대해 알아봅니다. 레코드들을 필터링하는 방법에는 2가지가 있습니다. 필터를 이용하는 방법과 where 절을 이용하는 방법입니다. 먼저 필터 사용법에 대해 알아보겠습니다. ## 필터 필터는 사용하기 쉽고 이해하기 쉽습니다. 속성들의 타입에 따라 다양한 필터 작업이 가능합니다. 필터 작업들은 대부분 알기 쉬운 이름들을 사용합니다. 필터는 필터링할 컬렉션의 모든 객체에 대한 식을 계산해서 작동합니다. 표현식이 `true` 로 결정되면 Isar 는 결과에 객체를 포함합니다. 필터는 결과 순서에 영향을 주지 않습니다. 아래에 나오는 예제들에서는 다음 모델을 사용합니다. ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### 쿼리 조건들 필드의 타입에 따라서, 다른 조건들을 사용할 수 있습니다. | 조건 | 설명 | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `.equalTo(value)` | 특정 `value` 와 일치하는 값들. | | `.between(lower, upper)` | `lower` 와 `upper` 사이에 있는 값들. | | `.greaterThan(bound)` | `bound` 보다 큰 값들. | | `.lessThan(bound)` | `bound` 보다 작은 값들. 기본적으로 `null` 값이 사용된다. `null` 은 모든 값들 중에 제일 작은 값으로 간주 되기 때문이다. | | `.isNull()` | `null` 인 값들. | | `.isNotNull()` | `null` 이 아닌 값들. | | `.length()` | List, String, 링크에 있는 요소의 개수를 기반으로 한 길이 쿼리 필터 | 데이터베이스에 크기가 39, 40, 46, `null` 인 신발 4켤레가 있다고 가정해보자. 정렬을 따로 하지 않으면, ID별로 정렬된 값이 반환됩니다. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### 논리 연산들 논리 연산자를 이용해서 구문을 합성할 수 있습니다. | Operator | Description | | ---------- | ------------------------------------------------------ | | `.and()` | 양 쪽의 식이 모두 `true` 인 경우 `true` 로 평가됩니다. | | `.or()` | 한 쪽의 식이라도 `true` 인 경우 `true` 로 평가됩니다. | | `.xor()` | 정확히 한 쪽의 식이 `true` 라면 `true` 로 평가됩니다. | | `.not()` | 다음 식이 부정되는 결과를 가져옵니다. | | `.group()` | 조건을 그룹화하고 평가 순서를 지정할 수 있습니다. | 만약 크기가 46인 모든 신발들을 원한다면, 다음 쿼리를 사용할 수 있습니다: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` 하나 이상의 조건이 필요하다면, 논리적 **and** `.and()`, 논리적 **or** `.or()`, 논리적 **xor** `.xor()` 을 이용해서 여러 필터들을 조합하세요. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // 선택적으로, 필터들을 논리 and 연산으로 조합합니다. .isUnisexEqualTo(true) .findAll(); ``` 이 쿼리는 다음과 같습니다: `size == 46 && isUnisex == true`. `group()` 으로 그룹 조건을 사용할 수도 있습니다: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` 이 쿼리는 `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)` 와 같습니다. 하나의 조건이나 그룹을 부정하려면, 논리적 **부정** 인 `.not()` 을 사용합니다: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` 이 쿼리는 `size != 46 && isUnisex != true` 와 같습니다. ### 문자열 조건들 위에 있는 쿼리 조건들 말고도, String 값에서는 좀 더 많은 조건들이 제공됩니다. 정규식과 유사한 와일드카드를 사용하면 검색의 유연성을 높일 수 있습니다. | 조건 | 설명 | | -------------------- | ---------------------------------------------- | | `.startsWith(value)` | 주어진 `value` 로 시작하는 문자열 값들. | | `.contains(value)` | 주어진 `value` 를 포함하는 문자열 값들. | | `.endsWith(value)` | 주어진 `value` 로 끝나는 문자열 값들. | | `.matches(wildcard)` | 주어진 `wildcard` 패턴과 일치하는 문자열 값들. | **대소문자 구분** 모든 문자열 연산은 추가적인 `caseSensitive` 매개변수를 가지고 있습니다. 기본값은 `true`. **와일드 카드:** [와일드카드 문자열 표현식](https://ko.wikipedia.org/wiki/%EC%99%80%EC%9D%BC%EB%93%9C%EC%B9%B4%EB%93%9C_%EB%AC%B8%EC%9E%90) 다음 2개의 특수한 와일드카드 문자를 포함한 문자열 입니다. - 와일드카드 `*` 는 0개 이상의 어떠한 문자열과 대응됩니다. - 와일드카드 `?` 은 어떠한 문자 하나와 대응됩니다. 예를 들어, 와일드카드 문자열 `"d?g"` 는 `"dog"`, `"dig"`, `"dug"` 와 일치하지만, `"ding"`, `"dg"`, `"a dog"` 와는 일치하지 않습니다. ### 쿼리 수정자 (query modifiers) 경우에 따라서 일부 조건이나 다른 값들을 기준으로 쿼리를 작성해야 할 수도 있습니다. Isar 에는 조건부 쿼리를 작성하기 위한 매우 강력한 도구가 있습니다. | 수정자 | 설명 | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `.optional(cond, qb)` | `condition` 이 `true` 인 경우에만 쿼리를 확장합니다. 조건부로 정렬하거나 제한하기 위해서 쿼리의 모든 곳에서 사용할 수 있습니다. | | `.anyOf(list, qb)` | `value` 의 각 값에 대한 쿼리를 확장하고 논리적 **or** 을 사용해서 조건을 결합합니다. | | `.allOf(list, qb)` | `value` 의 각 값에 대한 쿼리를 확장하고 논리적 **and** 를 사용해서 조건을 결합합니다. | | | 이 예시에서, 선택적 필터를 사용해서 신발을 찾는 메서드를 만듭니다. ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // sizeFilter != null 이 아닐 때만 적용됩니다. (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` 여러 신발 크기 중 하나를 가진 모든 신발을 찾으려면, 일반적인 쿼리를 작성하거나 `anyOf()` 수정자를 사용할 수 있습니다: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` 쿼리 수정자는 동적 쿼리를 작성할 때 특히 유용합니다. ### 리스트 심지어 리스트를 쿼리할 수도 있습니다: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` 리스트의 길이에 대해서 쿼리할 수 있습니다. ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` 다트 코드로 `tweets.where((t) => t.hashtags.isEmpty);` 와 `tweets.where((t) => t.hashtags.length > 5);` 같습니다. 리스트 요소에 대해서 쿼리할 수 있습니다. ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` 다트 코드로 `tweets.where((t) => t.hashtags.contains('flutter'));` 와 같습니다. ### 임베드된 객체들 임베드된 객체는 Isar 의 가장 유용한 기능 중 하나 입니다. 최상위 객체와 동일한 조건을 사용하여 매우 효율적으로 쿼리할 수 있습니다. 다음과 같은 모델이 있다고 가정합시다: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` `BMW` 라는 브랜드와 `"Germany"` 라는 나라를 갖는 모든 차들을 쿼리하고 싶습니다. 다음 쿼리를 사용해서 이 작업을 수행할 수 있습니다. ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` 항상 중첩된 쿼리들을 그룹화하세요. 위의 쿼리가 다음 쿼리보다 효율적입니다. 결과는 같겠지만요: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### 링크(Link) 모델이 [링크와 백링크](links) 를 포함하고 있다면 연결된 객체 또는 연결된 객체 수를 기준으로 쿼리를 필터링 할 수 있습니다. :::warning Isar 는 링크된 객체를 조회해야 하므로 링크 쿼리의 비용은 비쌀 수 있습니다. 대신 임베드된 객체를 사용하는 것을 고려해 보십시오. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` 수학이나 영어 선생님이 있는 모든 학생을 찾습니다: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` 링크 필터는 하나 이상의 연결된 객체가 조건과 일치하면 `true` 로 평가합니다. 선생님이 없는 모든 학생을 찾아봅시다: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` 또는 이렇게 할 수 있습니다: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where 절 Where 절은 매우 강력한 도구이지만, 제대로 이해하는 것은 약간 어렵습니다. filter 와 달리 where 절은 쿼리 조건을 검사하기 위해서 스키마에서 정의된 index 들을 사용합니다. 인덱스를 쿼리하는 것이 레코드 각각을 필터링하는 것보다 훨씬 빠릅니다. ➡️ 더 알아보기: [인덱스](indexes) :::팁 기본적으로 where 절을 사용해서 레코드를 최대한 줄이고 나머지에 대해 필터링을 수행해야 합니다. ::: 논리적 **or** 을 사용하여 where 절만 결합할 수 있습니다. 즉, 여러 where 절들의 합집합을 구할 수는 있지만, 여러 where 절들의 교집합을 쿼리할 수 는 없습니다. 신발 컬렉션에 인덱스를 추가합니다: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` 두 개의 인덱스가 있습니다. `size` 의 인덱스를 사용하면 `.sizeEqualTo()` 와 같은 절을 사용할 수 있습니다. `isUnisex` 의 합성 인덱스는 `isUnisexSizeEqualTo()` 와 같은 where 절을 가능하게 합니다. 하지만 인덱스의 접두사를 항상 사용할 수 있기 때문에 `isUnisexEqualTo()` 도 허용됩니다. 우리는 복합 인덱스를 사용해서 46사이즈의 남녀공용 신발을 찾는 이전의 쿼리를 다시 작성할 수 있습니다. 이 쿼리는 이전 쿼리보다 훨씬 빨라집니다: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` where 절은 2개의 초능력을 더 가지고 있습니다: "무료" 정렬과 초고속 구별(distinct) 작업을 제공합니다. ### where 절과 filter 결합하기 `shoes.filter()` 쿼리가 기억나죠? 그건 사실 `shoes.where().filter()` 의 줄임 표현입니다. 양 쪽의 장점들을 사용하기 위해서 하나의 쿼리 안에서 where 절과 filter 를 결합할 수 있습니다. ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` 필터링할 개체 수를 줄이기 위해서 where 절이 먼저 적용됩니다. 남은 객체들에 필터가 적용됩니다. The where clause is applied first to reduce the number of objects to be filtered. Then the filter is applied to the remaining objects. ## 정렬 `.sortBy()`, `.sortByDesc()`, `.thenBy()` 및 `.thenByDesc()` 메서드를 사용해서 쿼리를 실행할 때 결과를 정렬하는 방법을 정의합니다. 인덱스를 사용하지 않고 모델 이름 기준으로 오름차순, 크기 기준으로 내림차순 정렬된 모든 신발을 찾으려면 이렇게 합니다. ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` 특히 정렬은 오프셋과 제한 이전에 실행되기 때문에, 많은 결과를 정렬하는 것은 비용이 많이 듭니다. 위의 정렬 방법은 인덱스를 사용하지 않습니다. 다행히, 우리는 where 절 정렬을 다시 사용할 수 있고 백만 개의 객체를 정렬하는 경우에도 번개처럼 빠르게 수행할 수 있습니다. ### where 절 정렬 쿼리에 **단일** where 절을 사용하는 경우 결과가 이미 인덱스 기준으로 정렬되어 있습니다. 정말 큰일입니다! 신발의 크기가 `[43, 39, 48, 40, 42, 45]` 이고 `42` 보다 큰 모든 신발을 찾고 크기별로 정렬한다고 가정해 보겠습니다. ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // 크기 기준으로 정렬까지 됩니다. .findAll(); // -> [43, 45, 48] ``` 결과는 기본적으로 `size` 인덱스 기준으로 정렬됩니다. where 절의 정렬 순서를 반대로 하려면 `sort` 를 `Sort.desc` 로 설정하면 됩니다: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` 가끔 where 절을 이용하지 않지만 암시적인 정렬을 원하는 경우가 있습니다. `any` where 절을 사용하면 됩니다. ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` 복합 인덱스를 사용하는 경우, 인덱스의 모든 필드 별로 결과가 정렬됩니다. :::tip 결과를 정렬해야 하는 경우 인덱스를 사용하는 게 좋습니다. 특히 `offset()` 과 `limit()` 를 사용하여 작업하는 경우에는 더욱 그렇습니다. ::: 인덱스를 사용해서 정렬할 수 없거나 유용하지 않은 경우가 있습니다. 이러한 경우 인덱스를 사용하여 결과 항목 수를 최대한 줄여야 합니다. ## 고유한 값들 (Unique values) 고유한 값들로만 이루어진 항목들을 반환하려면 distinct 술어를 사용하세요. 예를 들어, Isar 데이터베이스에 있는 신발 모델의 수를 확인하려면 다음과 같이 하세요. ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` 여러 개의 개별 조건들을 체인으로 연결해서 모델 크기 조합이 다른 모든 신발을 찾을 수 있습니다. ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` 각 고유한 조합의 첫 번째 결과만 반환됩니다. where 절 및 정렬 작업을 사용하여 이를 제어할 수 있습니다. ### Where 절 구분 (Where clause distinct) 고유하지 않은 인덱스가 있는 경우, 구분된 값들을 모두 가져올 수 있습니다. 이전 섹션의 `distinctBy` 연산을 사용할 수 있지만, 정렬 및 필터 이후에 실행되므로 오버헤드가 있습니다. 단일 where 절만 사용하는 경우 인덱스를 사용하여 구분 작업을 수행할 수 있습니다. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip 이론적으로는 정렬 및 구분을 위해서 여러 개의 where 절을 사용하 수 있습니다. 유일한 제약은 where 절이 중복되지 않고 동일한 인덱스를 사용하는 것입니다. 올바른 정렬을 위해서는 정렬 순서로 적용해야 합니다. 이것에 의존하는 것은 매우 조심하세요! In theory, you could even use multiple where clauses for sorting and distinct. The only restriction is that those where clauses are not overlapping and use the same index. For correct sorting, they also need to be applied in sort order. Be very careful if you rely on this! ::: ## 오프셋과 제한(Offset & Limit) lazy 리스트 뷰를 위해서 쿼리 결과를 제한하는 것이 좋습니다. 다음과 같이 `limit()` 를 설정해서 할 수 있습니다. ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` `offset()` 을 이용해서 쿼리를 페이징할 수 있습니다. By setting an `offset()` you can also paginate the results of your query. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Dart 객체를 인스턴스화하는 것은 보통 쿼리 실행에서 비용이 가장 많이 드는 부분이기 때문에, 필요한 객체만 불러오는 것이 좋습니다. ## 실행 순서 Isar 는 항상 다음 순서로 쿼리들을 실행합니다. 1. 주 또는 보조 인덱스를 순회하면서 객체를 찾습니다. (where 절 적용) 2. Filter 3. 정렬 4. 구분 연산 5. 오프셋 & 제한 6. 결과 반환 ## 쿼리 연산들 이전 예제들에서 일치하는 모든 객체들을 검색하기 위해서 `.findAll()` 을 사용했습니다. 그러나 더 많은 연산을 사용할 수 있습니다. | 연산 | 설명 | | ---------------- | -------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | 일치하는 첫 객체 또는 일치하는 것이 없는 경우 `null` 을 반환합니다. | | `.findAll()` | 일치하는 모든 객체들을 검색합니다. | | `.count()` | 쿼리와 일치하는 객체의 수를 셉니다. | | `.deleteFirst()` | 컬렉션에서 일치하는 첫 객체를 제거합니다. | | `.deleteAll()` | 컬렉션에서 일치하는 모든 객체를 제거합니다. | | `.build()` | 쿼리를 나중에 사용하기 위해 컴파일 합니다. 이렇게 하면 쿼리를 여러 번 실행하는 경우 쿼리를 만드는 비용이 절약됩니다. | ## 속성 쿼리 (Property queries) 단일 속성 값에만 관심이 있는 경우 속성 쿼리를 사용하세요. 일반 쿼리를 만들고 속성을 선택하세요: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` 단일 속성만 이용하면 역직렬화에 걸리는 시간을 절약할 수 있습니다. 속성 쿼리는 임베드된 객체와 리스트에도 사용할 수 있습니다. ## 집계 (Aggregation) Isar 에서는 속성 쿼리의 값을 집계할 수 있습니다. 다음 집계 연산이 가능합니다. | 연산 | 설명 | | ------------ | --------------------------------------------------------------------------- | | `.min()` | 최소값 또는 일치하는 것이 없는 경우 `null` 을 반환합니다. | | `.max()` | 최대값 또는 일치하는 것이 없는 경우 `null` 을 반환합니다. | | `.sum()` | 모든 값들을 더합니다. | | `.average()` | 모든 값들의 평균을 계산합니다. 일치하는 값이 없는 경우 `NaN` 을 반환합니다. | 집계를 사용하는 것이 일치하는 모든 객체를 찾은 다음 집계를 수동으로 하는 것보다 훨씬 빠릅니다. ## 동적 쿼리 :::danger 이 섹션은 대부분 사용자와는 관련이 없습니다. 반드시 필요한 경우(거의 그럴 일은 없습니다.)가 아니면 동적 쿼리를 사용하지 않는 것이 좋습니다. ::: 위의 모든 예시에서 QueryBuilder와 생성된 정적 확장 메서드들을 사용했습니다. 동적 쿼리 또는 사용자 지정 쿼리 언어 (Isar Inspector 같은) 를 만들 수 있습니다. 이 경우 `buildQuery()` 메서드를 사용할 수 있습니다. | 매개변수 | 설명 | | --------------- | ------------------------------------------------------------------------------ | | `whereClauses` | 이 쿼리의 where 절들 입니다. | | `whereDistinct` | where 절이 구분된 값을 반환해야 하는 지 여부입니다. (단일 where 절에만 유효함) | | `whereSort` | where 절의 순회 순서 입니다. (단일 where 절에만 유효함) | | `filter` | 결과에 적용할 필터입니다. | | `sortBy` | 정렬의 기준으로 사용할 속성의 리스트입니다. | | `distinctBy` | 구분할 속성 리스트 입니다. | | `offset` | 결과의 오프셋 입니다. | | `limit` | 반환할 결과의 최대 개수입니다. | | `property` | null이 아닌 경우 이 속성의 값만 반환됩니다. | 동적 쿼리를 만들어 봅시다: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` 다음 쿼리와 동일합니다: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/ko/recipes/data_migration.md ================================================ --- title: 데이터 마이그레이션 (Data migration) --- # 데이터 마이그레이션 Isar 는 컬렉션, 속성, 인덱스를 추가하거나 삭제하면 데이터베이스 스키마를 자동으로 마이그레이션합니다. 가끔은 데이터도 마이그레이션해야 할 수 있습니다. Isar 는 임의 마이그레이션 제한을 적용하기 때문에 기본 제공 솔루션을 제공하지는 않습니다. 사용자의 요구사항에 맞는 마이그레이션 로직을 쉽게 구현할 수 있습니다. 이 예시에서는 전체 데이터베이스에서 하나의 버전을 이용하려고 합니다. shared preferences 를 사용해서 현재 버전을 저장하고 마이그레이션하려는 버전과 비교합니다. 버전이 일치하지 않으면 데이터를 마이그레이션하고 버전을 업데이트 합니다. :::tip 각 컬렉션에 자체 버전을 지정하고 개별적으로 마이그레이션 할 수 있습니다. ::: 생일 필드가 있는 사용자 컬렉션이 있다고 상상해 보십시오. 우리 앱의 버전 2에서는 나이를 기준으로 사용자를 조회할 수 있는 추가 출생 연도 필드가 필요합니다. 버전 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` 버전 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` 문제는 기존에 있던 사용자 모델들은 버전 1에서 `birthYear` 가 없었기 때문에 비어있는 `birthYear` 를 가지게 된다는 것입니다. 우리는 `birthYear` 필드를 설정하기 위해서 데이터를 마이그레이션 해야 합니다. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // 버전이 설정되지 않았거나(새로 설치한 경우), 이미 2인 경우 마이그레이션할 필요가 없습니다. return; default: throw Exception('Unknown version: $currentVersion'); } // 버전 업데이트 await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // 모든 사용자를 한 번에 메모리에 로드하지 않도록 사용자를 페이지 분할합니다. for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // 생년월일 게터를 사용하기 때문에 업데이트할 필요가 없습니다. await isar.users.putAll(users); }); } } ``` :::warning 많은 데이터를 마이그레이션 해야 하는 경우 UI 스레드에 부담이 가지 않도록 백그라운드 isolate 를 사용하는 것이 좋습니다. ::: ================================================ FILE: docs/docs/ko/recipes/full_text_search.md ================================================ --- title: Full-text search --- # Full-text search Full-text search is a powerful way to search text in the database. You should already be familiar with how [indexes](/indexes) work, but let's go over the basics. An index works like a lookup table, allowing the query engine to find records with a given value quickly. For example, if you have a `title` field in your object, you can create an index on that field to make it faster to find objects with a given title. ## Why is full-text search useful? You can easily search text using filters. There are various string operations for example `.startsWith()`, `.contains()` and `.matches()`. The problem with filters is that their runtime is `O(n)` where `n` is the number of records in the collection. String operations like `.matches()` are especially expensive. :::tip Full-text search is much faster than filters, but indexes have some limitations. In this recipe, we will explore how to work around these limitations. ::: ## Basic example The idea is always the same: Instead of indexing the whole text, we index the words in the text so we can search for them individually. Let's create the most basic full-text index: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` We can now search for messages with specific words in the content: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` This query is super fast, but there are some problems: 1. We can only search for entire words 2. We do not consider punctuation 3. We do not support other whitespace characters ## Splitting text the right way Let's try to improve the previous example. We could try to develop a complicated regex to fix word splitting, but it will likely be slow and wrong for edge cases. The [Unicode Annex #29](https://unicode.org/reports/tr29/) defines how to split text into words correctly for almost all languages. It is quite complicated, but fortunately, Isar does the heavy lifting for us: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## I want more control Easy peasy! We can change our index also to support prefix matching and case-insensitive matching: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` By default, Isar will store the words as hashed values which is fast and space efficient. But hashes can't be used for prefix matching. Using `IndexType.value`, we can change the index to use the words directly instead. It gives us the `.titleWordsAnyStartsWith()` where clause: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## I also need `.endsWith()` Sure thing! We will use a trick to achieve `.endsWith()` matching: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` Don't forget reversing the ending you want to search for: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Stemming algorithms Unfortunately, indexes do not support `.contains()` matching (this is true for other databases as well). But there are a few alternatives that are worth exploring. The choice highly depends on your use. One example is indexing word stems instead of the whole word. A stemming algorithm is a process of linguistic normalization in which the variant forms of a word are reduced to a common form: ``` connection connections connective ---> connect connected connecting ``` Popular algorithms are the [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) and the [Snowball stemming algorithms](https://snowballstem.org/algorithms/). There are also more advanced forms like [lemmatization](https://en.wikipedia.org/wiki/Lemmatisation). ## Phonetic algorithms A [phonetic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) is an algorithm for indexing words by their pronunciation. In other words, it allows you to find words that sound similar to the ones you are looking for. :::warning Most phonetic algorithms only support a single language. ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex) is a phonetic algorithm for indexing names by sound, as pronounced in English. The goal is for homophones to be encoded to the same representation so they can be matched despite minor differences in spelling. It is a straightforward algorithm, and there are multiple improved versions. Using this algorithm, both `"Robert"` and `"Rupert"` return the string `"R163"` while `"Rubin"` yields `"R150"`. `"Ashcraft"` and `"Ashcroft"` both yield `"A261"`. ### Double Metaphone The [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) phonetic encoding algorithm is the second generation of this algorithm. It makes several fundamental design improvements over the original Metaphone algorithm. Double Metaphone accounts for various irregularities in English of Slavic, Germanic, Celtic, Greek, French, Italian, Spanish, Chinese, and other origins. ================================================ FILE: docs/docs/ko/recipes/multi_isolate.md ================================================ --- title: 다중-Isolate 사용법 --- # 다중-Isolate 사용법 스레드 대신, 모든 다트 코드는 isolate 안에서 돌아갑니다. 각 isolate 에는 고유한 메모리 힙이 있으므로, isolate 의 어떤 상태도 다른 isolate 에서 접근할 수 없습니다. Isar can be accessed from multiple isolates at the same time, and even watchers work across isolates. In this recipe, we will check out how to use Isar in a multi-isolate environment. ## When to use multiple isolates Isar transactions are executed in parallel even if they run in the same isolate. In some cases, it is still beneficial to access Isar from multiple isolates. The reason is that Isar spends quite some time encoding and decoding data from and to Dart objects. You can think of it as encoding and decoding JSON (just more efficient). These operations run inside the isolate from which the data is accessed and naturally block other code in the isolate. In other words: Isar performs some of the work in your Dart isolate. If you only need to read or write a few hundred objects at once, doing it in the UI isolate is not a problem. But for huge transactions or if the UI thread is already busy, you should consider using a separate isolate. ## Example The first thing we need to do is to open Isar in the new isolate. Since the instance of Isar is already open in the main isolate, `Isar.open()` will return the same instance. :::warning Make sure to provide the same schemas as in the main isolate. Otherwise, you will get an error. ::: `compute()` starts a new isolate in Flutter and runs the given function in it. ```dart void main() { // Open Isar in the UI isolate final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // listen to changes in the database isar.messages.watchLazy(() { print('omg the messages changed!'); }); // start a new isolate and create 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // after some time: // > omg the messages changed! // > isolate finished } // function that will be executed in the new isolate Future createDummyMessages(int count) async { // we don't need the path here because the instance is already open final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // we use a synchronous transactions in isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` There are a few interesting things to note in the example above: - `isar.messages.watchLazy()` is called in the UI isolate and is notified of changes from another isolate. - Instances are referenced by name. The default name is `default`, but in this example, we set it to `myInstance`. - We used a synchronous transaction to create the mesasges. Blocking our new isolate is no problem, and synchronous transactions are a little faster. ================================================ FILE: docs/docs/ko/recipes/string_ids.md ================================================ --- title: String ids --- # String ids This is one of the most frequent requests I get, so here is a tutorial on using String ids. Isar does not natively support String ids, and there is a good reason for it: integer ids are much more efficient and faster. Especially for links, the overhead of a String id is too significant. I understand that sometimes you have to store external data that uses UUIDs or other non-integer ids. I recommend storing the String id as a property in your object and using a fast hash implementation to generate a 64-bit int that can be used as Id. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` With this approach, you get the best of both worlds: Efficient integer ids for links and the ability to use String ids. ## Fast hash function Ideally, your hash function should have high quality (you don't want collisions) and be fast. I recommend using the following implementation: ```dart /// FNV-1a 64bit hash algorithm optimized for Dart Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` If you choose a different hash function, ensure it returns a 64-bit int and avoid using a cryptographic hash function because they are much slower. :::warning Avoid using `string.hashCode` because it is not guaranteed to be stable across different platforms and versions of Dart. ::: ================================================ FILE: docs/docs/ko/schema.md ================================================ --- title: 스키마 --- # 스키마 앱의 데이터를 저장하기 위해 Isar를 사용할 때마다, 컬렉션을 이용하게 됩니다. 컬렉션은 연관된 Isar 데이터베이스의 데이터베이스 테이블과 같고, 하나의 다트 객체 타입만을 포함할 수 있습니다. 각 컬렉션 객체는 해당 컬렉션의 데이터 행을 나타냅니다. 컬렉션의 정의를 "스키마" 라고 합니다. Isar Generator 가 힘든 일을 대신 해주고, 컬렉션을 사용하기 위해 필요한 대부분의 코드를 생성해줍니다. ## 컬렉션의 구조 클래스에 `@collection` 또는 `@Collection()` 어노테이션을 붙여서 Isar 컬렉션을 정의합니다. 필드들을 포함하는 하나의 Isar 컬렉션은 데이터베이스의 해당하는 테이블에 있는 각각의 열과 같으며, 여기에는 기본 키를 구성하는 하나의 필드도 포함됩니다. 아래 코드는 `User` 테이블을 정의하는 하나의 컬렉션 예시를 보여줍니다. 테이블에는 ID, 성, 이름에 해당하는 열이 포합됩니다. ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip 필드를 저장하기 위해서 Isar 가 반드시 필드에 접근해야 합니다. 필드를 public 으로 만들거나 게터와 세터를 제공해서 Isar가 접근할 수 있도록 만들어야 됩니다. ::: 컬렉션을 커스터마이징할 수 있는 몇 가지 선택적인 매개변수들이 있습니다. | Config | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `inheritance` | Isar 에 부모 클래스들과 믹스인에 있는 필드를 저장할 지를 조정합니다. 기본적으로 활성화되어 있습니다. | | `accessor` | 기본 컬렉션 접근자의 이름을 바꿀 수 있게 해줍니다. (예: `Contact` 컬렉션에 사용되는 `isar.contacts`) | | `ignore` | 특정 속성을 제외할 수 있습니다. 이것은 상위 클래스에도 동일하게 적용될 수 있습니다. | ### Isar 의 Id 각 컬렉션 클래스는 객체를 고유하게 식별하는 `Id` 타입으로 id 속성을 정의해야 합니다. `Id` 는 Isar Generator 가 id 속성을 인식할 수 있도록 하는 int 의 별칭일 뿐입니다. Isar 는 id 필드를 자동으로 인덱싱합니다. id 를 기반으로 객체를 효율적으로 가져오고 수정할 수 있습니다. 사용자가 직접 id를 설정하거나 Isar 에 자동 증분 id를 할당하도록 요청할 수 있습니다. 만약 `id` 필드가 `null` 이고 `final` 이 아니라면 Isar 는 자동 증분 id 를 할당합니다. null 이 아닌 자동 증분 id를 원하는 경우에는 `Isar.autoincrement` 를 사용할 수 있습니다. ::tip 자동 증분 아이디들은 해당 객체가 삭제되어도 다시 사용할 수 없습니다. 자동 증분 id를 초기화하는 유일할 방법은 데이터베이스를 지우는 것 뿐입니다. ::: ### 컬렉션과 필드의 이름 바꾸기 기본적으로 Isar는 클래스 이름을 컬렉션 이름으로 사용합니다. 마찬가지로 Isar는 필드 이름을 데이터베이스 열 이름으로 사용합니다. 컬렉션이나 필드에 다른 이름을 붙이고 싶다면 `@Name` 어노테이션을 추가합니다. 다음 코드는 컬렉션과 필드의 이름을 바꾸는 예입니다: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` 특히, 이미 데이터베이스에 저장되어 있는 Dart 의 필드나 클래스의 이름을 변경하고 싶다면, `@Name` 어노테이션의 사용을 검토해야 합니다. 그렇지 않으면 데이터베이스가 해당 필드나 컬렉션을 삭제하거나 재작성하게 될 수 있습니다. ### 필드 무시하기 Isar 는 컬렉션 클래스의 모든 public 필드를 저장합니다. 속성이나 게터에 `@ignore` 어노테이션을 붙여서 저장하지 않을 수 있습니다. 다음 코드 조각을 보세요. ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` 컬렉션이 부모 컬렉션에서 필드를 상속하는 경우 일반적으로 `@Collection` 어노테이션의 ignore 속성을 사용하는 것이 더 쉽습니다. ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` 만약 컬렉션에 Isar가 지원하지 않는 유형의 필드가 포함되어 있다면 해당 필드는 무시해야 합니다. :::warning 저장되지 않은 Isar 객체에 정보를 저장하는 것은 좋지 않습니다. ::: ## 지원하는 타입 목록 Isar 는 아래의 데이터 타입들을 지원합니다: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` 추가적으로 임베드된 객체와 enum 도 지원합니다. 이것들은 아래에서 다룰 것입니다. ## byte, short, float 대부분의 경우 64비트 정수형이나 double 의 전체 범위는 필요하지 않습니다. Isar는 더 작은 수치를 저장할 때를 위해서 용량과 메모리를 절약할 수 있는 추가 유형을 지원합니다. | Type | Size in bytes | Range | | ---------- | ------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | 추가적인 숫자 타입들은 native 다트 타입들의 별칭일 뿐입니다. 예를 들어 `short` 를 사용해도 `int` 를 사용하는 것과 같이 동작합니다. 아래에 위의 모든 유형을 포함하는 컬렉션의 예를 보여줍니다. ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` 모든 숫자 유형은 리스트로 사용할 수도 있습니다. 바이트들을 저장하려면 `List` 를 사용해야 합니다. ## 널이 허용되는 타입들 Isar 에서 nullability(DB 테이블의 열 항목이 NULL 값을 가질 수 있는지 없는지)가 어떻게 작동하는지 이해하는 것은 필수입니다. 숫자 타입들은 `null` 이라는 값을 별도로 가지지 **않습니다**. 대신, 특수한 값이 사용됩니다. | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, `List` 는 별도의 `null` 표현을 사용합니다. 이런 동작을 통해서 성능이 향상되고, `null` 값을 처리하기 위한 마이그레이션이나 특별한 코드 없이도 필드의 nullability 를 자유롭게 변경할 수 있게 됩니다. :::warning `byte` 타입은 널 값을 지원하지 않습니다. ::: ## DateTime Isar 는 날짜의 표준 시간대 정보를 저장하지 않습니다. 대신 `DateTime`을 UTC로 변환해서 저장합니다. Isar 는 모든 날짜를 현지 시간으로 반환합니다. `DateTime`은 마이크로초 단위로 저장됩니다. 브라우저에서는 자바스크립트의 제한 때문에 밀리초 단위의 정밀도만 지원됩니다. ## Enum Isar는 다른 Isar 타입들 처럼 열거형을 저장하고 사용할 수 있습니다. 하지만 Isar가 디스크의 열거형을 어떻게 나타낼지 선택해야 합니다. Isar 는 4가지의 전략을 지원합니다. | EnumType | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | `ordinal` | 열거형의 인덱스는 `byte` 로 저장됩니다. 이것은 매우 효율적이지만 널이 가능한 열거형에서는 허용하지 않습니다 | | `ordinal32` | 열거형의 인덱스는 `short`(4바이트 정수)로 저장됩니다. | | `name` | 열거형 이름은 `String` 으로 저장됩니다. | | `value` | 사용자 지정 속성을 사용하여 열거형을 검색합니다 | :::warning `ordinal`과 `ordinal32` 는 열거값의 순서에 따라 달라집니다. 순서를 변경하는 경우, 기존 데이터베이스는 잘못된 값을 반환합니다. ::: 각각의 전략을 사용하는 예시를 확인하세요. ```dart @collection class EnumCollection { Id? id; @enumerated // EnumType.ordinal 과 같습니다. late TestEnum byteIndex; // 널이 될 수 없습니다. @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // 널이 될 수 없습니다. @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` 물론 열거형을 리스트에서 사용해도 됩니다. ## 임베드된 객체 컬렉션 모델에 중첩된 객체가 있는 것이 도움이 되는 경우가 많습니다. 객체를 중첩할 수 있는 깊이에는 제한이 없습니다. 그러나 깊이 중첩된 객체를 업데이트하려면 전체 객체 트리를 데이터베이스에 기록해야 합니다. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` 임베드된 객체는 null로 사용할 수 없으며 다른 객체를 확장할 수 있습니다. 유일한 요구 사항은 `@embedded` 어노테이션을 추가하고 필수 매개 변수가 없는 기본 생성자를 갖는 것입니다. ================================================ FILE: docs/docs/ko/transactions.md ================================================ --- title: 트랜잭션 --- # 트랜잭션 (Transactions) Isar 에서 트랜잭션은 단일 작업 단위 안에서 여러 데이터베이스 작업들을 합치게 됩니다. Isar 와의 대부분의 상호작용은 암묵적으로 트랜잭션을 사용합니다. Isar 의 읽기 및 쓰기 접근은 [ACID](http://en.wikipedia.org/wiki/ACID) 를 준수합니다. 오류가 발생하면 트랜잭션은 자동으로 롤백됩니다. ## 명시적 트랜잭션 명시적 트랜잭션에서는 데이터베이스의 일관된 스냅샷을 얻을 수 있습니다. 트랜잭션 시간을 최소화하려고 노력하세요. 트랜잭션 내부에서 네트워크 호출 및 기타 장기적인 작업들은 금지됩니다. 트랜잭션 (특히 쓰기 트랜잭션) 은 비용이 많이 들기 때문에 항상 연속적인 작업들을 단일 트랜잭션으로 그룹화해야 합니다. 트랜잭션은 동기식이나 비동기식일 수 있습니다. 동기적인 트랜잭션에서는 동기 연산만 수행할 수 있습니다. 비동기식 트랜잭션에서는 비동기 연산만 수행할 수 있습니다. | | 읽기 | 읽기 & 쓰기 | | ------ | ------------ | ----------------- | | 동기 | `.txnSync()` | `.writeTxnSync()` | | 비동기 | `.txn()` | `.writeTxn()` | ### 읽기 트랜잭션 명시적 읽기 트랜잭션은 선택 사항이지만, 원자적 읽기(atomic reads) 를 수행하고 트랜잭션 내부의 데이터베이스의 일관된 상태에 의존할 수 있게 해줍니다. 내부적으로 Isar 는 모든 읽기 작업에 항상 암시적 읽기 트랜잭션을 사용합니다. :::tip 비동기 읽기 트랜잭션은 다른 읽기 및 쓰기 트랜잭션과 병렬로 실행됩니다. 꽤 멋있지 않나요? ::: ### 쓰기 트랜잭션 읽기 연산과 달리, Isar 에서 쓰기 연산은 반드시 명시적 트랜잭션으로 감싸야 합니다. 쓰기 트랜잭션이 성공적으로 완료되면 자동으로 커밋되고 모든 변경사항이 디스크에 기록됩니다. 오류가 발생하면 트랜잭션이 중단되고 모든 변경 사항이 롤백됩니다. 트랜잭션은 "전부가 아니면 아무것도 없다(all or nothing)" 입니다. 트랜잭션 내의 모든 쓰기가 성공하거나, 아니면 데이터 일관성을 보장하기 위해서 모두 무효가 되거나 입니다. :::warning 데이터베이스 연산이 실패하면, 그 트랜잭션은 중단되므로 더 이상 사용할 수 없습니다. 다트에서 그 오류를 캐치(catch) 하더라도요. ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: 트랜잭션 안으로 for 루프를 이동시키세요. for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/ko/tutorials/quickstart.md ================================================ --- title: 빠른 시작 --- # 빠른 시작 세상에, 이제야 왔군요! 가장 멋진 플러터 데이터베이스를 사용해 보겠습니다... 이 빠른 시작에서는 말은 줄이고 바로 코드를 보겠습니다. ## 1. 의존성 추가하기 재미있는 부분을 보기 전에 `pubspec.yaml` 에 몇 개의 패키지를 추가해야 합니다. 우리는 펍을 이용해서 힘든 일을 쉽게 할 수 있습니다. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. 클래스에 주석 추가(어노테이션) 컬렉션 클래스에 `@collection` 으로 주석을 달고 `Id` 필드를 선택합니다. ```dart part 'email.g.dart'; @collection class User { Id id = Isar.autoIncrement; // id = null 을 사용해도 자동 증분할 수 있습니다. String? name; int? age; } ``` Id는 컬렉션에서 개체를 고유하게 식별하고 나중에 개체를 다시 찾을 수 있도록 합니다. ## 3. 코드 생성기를 실행하기 다음 명령을 실행하여 `build_runner` 를 시작합니다: ``` dart run build_runner build ``` 플러터를 사용하고 있다면, 다음 명령을 사용합니다. ``` flutter pub run build_runner build ``` ## 4. Isar 인스턴스 열기 새 Isar 인스턴스를 열고 모든 컬렉션 스키마를 전달합니다. 선택적으로 인스턴스 이름과 디렉토리를 지정할 수도 있습니다. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [EmailSchema], directory: dir.path, ); ``` ## 5. 읽기와 쓰기 한번 인스턴스를 열면, 콜렉션들을 사용할 수 있습니다. 모든 기본적인 CRUD 작업은 `IsarCollection` 을 통해서 이루어집니다. ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // 삽입 & 업데이트 }); final existingUser = await isar.users.get(newUser.id); // 가져오기 await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // 삭제 }); ``` ## 다른 자료들 혹시 영상으로 공부를 하는 것이 더 좋나요? 다음 영상으로 Isar를 시작해보세요:


================================================ FILE: docs/docs/ko/watchers.md ================================================ --- title: 감시자(Watchers) --- # 감시자 Isar 에서는 데이터 변경을 구독할 수 있습니다. 특정 객체, 전체 컬렉션 또는 쿼리의 변경 내용을 "감시" 할 수 있습니다. 감시자(Watchers)를 사용하면 데이터베이스의 변경사항에 효율적으로 대응할 수 있습니다. 연락처가 추가될 때 UI를 재구성하거나 문서가 업데이트될 때 네트워크 요청을 보내는 등의 작업을 수행할 수 있습니다. 트랜잭션이 성공적으로 커밋되고 대상이 실제로 변경된 후 감시자에게 통지됩니다. ## 객체 감시 특정 객체가 생성, 업데이트 또는 삭제될 때 알림을 받으려면 객체를 확인해야 합니다: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // 출력: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // 출력: User changed: Mark await isar.users.delete(5); // 출력: User changed: null ``` 위의 예시에서 볼 수 있듯이 객체가 아직 존재하지 않아도 됩니다. 객체가 생성되면 감시자가 알게 됩니다. `fireImmediately` 라는 추가 매개 변수가 있습니다. `true` 로 설정하면 Isar 는 즉시 스트림에 객체의 현재 값을 추가합니다. ### 게으른 감시 (Lazy watching) 새 값 말고 변경 사항에 대해서만 구독할 수 있습니다. 그러면 Isar 가 객체를 가져올 필요가 없어집니다. ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // 출력: User 5 changed ``` ## 컬렉션 감시 단일 객체를 감시하는 대신에, 전체 컬렉션을 보고 객체가 추가, 업데이트, 또는 삭제될 때 알아차릴 수 있습니다: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // 출력: A User changed ``` ## 쿼리 감시 전체 쿼리를 감시할 수 있습니다. Isar 는 쿼리 결과가 실제로 변경될 때만 알리려고 최선을 다합니다. 링크로 인해 쿼리가 변경되는 경우 알림이 표시되지 않습니다. 링크 변경에 대한 알림이 필요한 경우 컬렉션 감시를 이용합니다. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // 출력: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // 출력: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // 출력 없음 await isar.users.put(User()..name = 'Antonia'); // 출력: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::주의 오프셋과 제한 또는 별개의 쿼리를 사용하는 경우에, Isar 는 객체가 쿼리의 바깥에 있지만, 필터와 일치하는 경우에도 알림을 받습니다. ::: `watchObject()` 처럼 `watchLazy()` 를 사용하여 쿼리 결과가 변경될 때 알림을 받을 수 있지만 결과를 가져오지는 않습니다. :::위험 모든 변경 사항에 대해서 쿼리를 다시 실행하는 것은 매우 비효율적입니다. 대신 게으른 컬렉션 감시자를 사용하는 것이 가장 좋습니다. ::: ================================================ FILE: docs/docs/limitations.md ================================================ --- title: Limitations --- # Limitations As you know, Isar works on mobile devices and desktops running on the VM as well as Web. Both platforms are very different and have different limitations. ## VM Limitations - Only the first 1024 bytes of a string can be used for a prefix where-clause - Objects can only be 16MB in size ## Web Limitations Because Isar Web relies on IndexedDB, there are more limitations, but they are barely noticeable while using Isar. - Synchronous methods are unsupported - Currently, `Isar.splitWords()` and `.matches()` filters are not yet implemented - Schema changes are not as tightly checked as in the VM so be careful to comply with the rules - All number types are stored as double (the only js number type) so `@Size32` has no effect - Indexes are represented differently so hash indexes don't use less space (they still work the same) - `col.delete()` and `col.deleteAll()` work correctly but the return value is not correct - `col.clear()` do not reset the auto-increment value - `NaN` is not supported as a value ================================================ FILE: docs/docs/links.md ================================================ --- title: Links --- # Links Links allow you to express relationships between objects, such as a comment's author (User). You can model `1:1`, `1:n`, and `n:n` relationships with Isar links. Using links is less ergonomic than using embedded objects, and you should use embedded objects whenever possible. Think of the link as a separate table that contains the relation. It's similar to SQL relations but has a different feature set and API. ## IsarLink `IsarLink` can contain no or one related object, and it can be used to express a to-one relationship. `IsarLink` has a single property called `value` which holds the linked object. Links are lazy, so you need to tell the `IsarLink` to load or save the `value` explicitly. You can do this by calling `linkProperty.load()` and `linkProperty.save()`. :::tip The id property of the source and target collections of a link should be non-final. ::: For non-web targets, links get loaded automatically when you use them for the first time. Let's start by adding an IsarLink to a collection: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` We defined a link between teachers and students. Every student can have exactly one teacher in this example. First, we create the teacher and assign it to a student. We have to `.put()` the teacher and save the link manually. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` We can now use the link: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Let's try the same thing with synchronous code. We don't need to save the link manually because `.putSync()` automatically saves all links. It even creates the teacher for us. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks It would make more sense if the student from the previous example could have multiple teachers. Fortunately, Isar has `IsarLinks`, which can contain multiple related objects and express a to-many relationship. `IsarLinks` extends `Set` and exposes all the methods that are allowed for sets. `IsarLinks` behaves much like `IsarLink` and is also lazy. To load all linked object call `linkProperty.load()`. To persist the changes, call `linkProperty.save()`. Internally both `IsarLink` and `IsarLinks` are represented in the same way. We can upgrade the `IsarLink` from before to an `IsarLinks` to assign multiple teachers to a single student (without losing data). ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` This works because we did not change the name of the link (`teacher`), so Isar remembers it from before. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlinks I hear you ask, "What if we want to express reverse relationships?". Don't worry; we'll now introduce backlinks. Backlinks are links in the reverse direction. Each link always has an implicit backlink. You can make it available to your app by annotating an `IsarLink` or `IsarLinks` with `@Backlink()`. Backlinks do not require additional memory or resources; you can freely add, remove and rename them without losing data. We want to know which students a specific teacher has, so we define a backlink: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` We need to specify the link to which the backlink points. It is possible to have multiple different links between two objects. ## Initialize links `IsarLink` and `IsarLinks` have a zero-arg constructor, which should be used to assign the link property when the object is created. It is good practice to make link properties `final`. When you `put()` your object for the first time, the link gets initialized with source and target collection, and you can call methods like `load()` and `save()`. A link starts tracking changes immediately after its creation, so you can add and remove relations even before the link is initialized. :::danger It is illegal to move a link to another object. ::: ================================================ FILE: docs/docs/pt/README.md ================================================ --- home: true title: Home heroImage: /isar.svg actions: - text: Vamos Começar! link: /pt/tutorials/quickstart.html type: primary features: - title: 💙 Feito para Flutter details: Configuração mínima, fácil de usar, sem configuração, sem clichê. Basta adicionar algumas linhas de código para começar. - title: 🚀 Altamente escalável details: Armazene centenas de milhares de registros em um único banco de dados NoSQL e consulte-os de forma eficiente e assíncrona. - title: 🍭 Rico em recursos details: O Isar possui um rico conjunto de recursos para ajudá-lo a gerenciar seus dados. Índices compostos e de várias entradas, modificadores de consulta, suporte a JSON e muito mais. - title: 🔎 Pesquisa de texto completo details: Isar tem busca embutida de texto completo. Crie um índice de várias entradas e pesquise registros facilmente. - title: 🧪 Semântica ACID details: Isar é compatível com ACID e lida com transações automaticamente. Ele reverte as alterações se ocorrer um erro. - title: 💃 Tipagem estática details: As consultas de Isar são estaticamente tipadas e verificadas em tempo de compilação. Não há necessidade de se preocupar com erros de tempo de execução. - title: 📱 Multi plataforma details: iOS, Android, Desktop e SUPORTE COMPLETO DA WEB! - title: ⏱ Assíncrono details: Operações de consulta paralela e suporte multiisolado pronto para uso - title: 🦄 Código Aberto details: Tudo é de código aberto e gratuito para sempre! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/pt/crud.md ================================================ --- title: Criar, Ler, Atualizar, Apagar --- # Criar, Ler, Atualizar, Apagar Quando você tiver suas coleções definidas, aprenda a manipulá-las! ## Abrindo Isar Antes que você possa fazer qualquer coisa, precisamos de uma instância Isar. Cada instância requer um diretório com permissão de gravação onde o arquivo de banco de dados pode ser armazenado. Se você não especificar um diretório, o Isar encontrará um diretório padrão adequado para a plataforma atual. Forneça todos os esquemas que deseja usar com a instância Isar. Se você abrir várias instâncias, ainda precisará fornecer os mesmos esquemas para cada instância. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` Você pode usar a configuração padrão ou fornecer alguns dos seguintes parâmetros: | Config | Description | | -------| -------------| | `name` | Abra várias instâncias com nomes distintos. Por padrão, `"default"` em uso. | | `directory` | O local de armazenamento para esta instância. Por padrão, `NSDocumentDirectory` é usado para iOS e `getDataDirectory` para Android. Para web não é necessário. | | `relaxedDurability` | Diminua a garantia de durabilidade para aumentar o desempenho de gravação. Em caso de falha do sistema (não falha do aplicativo), é possível perder a última transação confirmada. A corrupção não é possível| | `compactOnLaunch` | Condições para verificar se o banco de dados deve ser compactado quando a instância for aberta. | | `inspector` |Inspetor habilitado para compilações de depuração. Para builds de perfil e versão, esta opção é ignorada. | Se uma instância já estiver aberta, chamar `Isar.open()` produzirá a instância existente independentemente dos parâmetros especificados. Isso é útil para usar Isar em um isolado. :::tip Considere usar o pacote [path_provider](https://pub.dev/packages/path_provider) para obter um caminho válido em todas as plataformas. ::: O local de armazenamento do arquivo de banco de dados é `directory/name.isar` ## Leitura do banco de dados Use instâncias `IsarCollection` para localizar, consultar e criar novos objetos de um determinado tipo em Isar. Para os exemplos abaixo, assumimos que temos uma coleção `Recipe` definida da seguinte forma: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### Obter coleção Todas as suas coleções residem na instância Isar. Você pode obter a coleção de receitas com: ```dart final recipes = isar.recipes; ``` Essa foi fácil! Se você não quiser usar acessadores de coleção, você também pode usar o método `collection()`: ```dart final recipes = isar.collection(); ``` ### Obter um objeto (por id) Ainda não temos dados na coleção, mas vamos fingir que temos para que possamos obter um objeto imaginário pelo id `123` ```dart final recipe = await isar.recipes.get(123); ``` `get()` retorna um `Future` com o objeto ou `null` se não existir. Todas as operações Isar são assíncronas por padrão e a maioria delas tem uma contrapartida síncrona: ```dart final recipe = isar.recipes.getSync(123); ``` :::warning Você deve usar como padrão a versão assíncrona dos métodos em seu isolamento de interface do usuário. Como o Isar é muito rápido, geralmente é aceitável usar a versão síncrona. ::: Se você quiser obter vários objetos de uma vez, use `getAll()` ou `getAllSync()`: ```dart final recipe = await isar.recipes.getAll([1, 2]); ``` ### Objetos de consulta Em vez de obter objetos por id, você também pode consultar uma lista de objetos que correspondem a certas condições usando `.where()` e `.filter()`: ```dart final allRecipes = await isar.recipes.where().findAll(); final favouires = await isar.recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ Saber mais: [Consultas](queries) ## Modificando o banco de dados Finalmente chegou a hora de modificar nossa coleção! Para criar, atualizar ou excluir objetos, use as respectivas operações envolvidas em uma transação de gravação: ```dart await isar.writeTxn(() async { final recipe = await isar.recipes.get(123) recipe.isFavorite = false; await isar.recipes.put(recipe); // realizar operações de atualização await isar.recipes.delete(123); // ou operações de apagar }); ``` ➡️ Saber mais: [Transações](transactions) ### Inserir objeto Para persistir um objeto em Isar, insira-o em uma coleção. O método `put()` de Isar irá inserir ou atualizar o objeto dependendo da sua existência na coleção. Se o campo id for `null` ou `Isar.autoIncrement`, Isar usará um id de incremento automático. ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await isar.recipes.put(pancakes); }) ``` Isar atribuirá automaticamente o id ao objeto se o campo `id` não for final. Inserir vários objetos de uma só vez é extremamente fácil: ```dart await isar.writeTxn(() async { await isar.recipes.putAll([pancakes, pizza]); }) ``` ### Atualizar objeto Tanto a criação quanto a atualização funcionam com `collection.put(object)`. Se o id for `null` (ou não existir), o objeto será inserido; caso contrário, ele é atualizado. Então, se quisermos desfavoritar nossas panquecas, podemos fazer o seguinte: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await isar.recipes.put(recipe); }); ``` ### Apagar objeto Quer se livrar de um objeto em Isar? Use `collection.delete(id)`. O método delete retorna se um objeto com o id especificado foi encontrado e excluído. Se você quiser excluir o objeto com id `123`, por exemplo, você pode fazer: ```dart await isar.writeTxn(() async { final success = await isar.recipes.delete(123); print('Receita apagada: $success'); }); ``` Da mesma forma para obter e colocar, também há uma operação de exclusão em massa que retorna o número de objetos excluídos: ```dart await isar.writeTxn(() async { final count = await isar.recipes.deleteAll([1, 2, 3]); print('Apagamos $count receitas'); }); ``` Se você não souber os ids dos objetos que deseja excluir, poderá usar uma consulta: ```dart await isar.writeTxn(() async { final count = await isar.recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('Apagamos $count receitas'); }); ``` ================================================ FILE: docs/docs/pt/faq.md ================================================ --- title: QF --- # Questões Frequentes Uma coleção aleatória de questões frequentes sobre bancos de dados Isar e Flutter. ### Por que preciso de um banco de dados? > Armazeno meus dados em um banco de dados backend, por que preciso do Isar?. Ainda hoje, é muito comum não ter conexão de dados se você estiver no metrô ou no avião ou se for visitar sua avó, que não tem WiFi e o sinal do celular é muito ruim. Você não deve deixar uma conexão ruim paralisar seu aplicativo! ### Isar vs Hive A resposta é fácil: o Isar foi [iniciado como substituto do Hive](https://github.com/hivedb/hive/issues/246) e agora está em um estado em que recomendo sempre usar o Isar sobre o Hive. ### Cláusulas Where?! > Por que **_I_** precisa escolher qual índice usar? Existem várias razões. Muitos bancos de dados usam heurísticas para escolher o melhor índice para uma determinada consulta. O banco de dados precisa coletar dados de uso adicionais (-> sobrecarga) e ainda pode escolher o índice errado. Também torna a criação de uma consulta mais lenta. Ninguém conhece seus dados melhor do que você, o desenvolvedor. Assim, você pode escolher o índice ideal e decidir, por exemplo, se deseja usar um índice para consulta ou classificação. ### Eu tenho que usar índices / cláusulas where? Não! Isar provavelmente é rápido o suficiente se você confiar apenas em filtros. ### Isar é rápido o suficiente? O Isar está entre os bancos de dados mais rápidos para dispositivos móveis, portanto, deve ser rápido o suficiente para a maioria dos casos de uso. Se você tiver problemas de desempenho, é provável que esteja fazendo algo errado. ### O Isar aumenta o tamanho do meu aplicativo? Um pouco, sim. O Isar aumentará o tamanho do download do seu aplicativo em cerca de 1 a 1,5 MB. Isar Web adiciona apenas alguns KB. ### Os documentos estão incorretos/há um erro de digitação. Ah não, desculpe. Por favor [abra um issue](https://github.com/isar-community/isar/issues/new/choose) ou, melhor ainda, um PR para corrigi-lo 💪. ================================================ FILE: docs/docs/pt/indexes.md ================================================ --- title: Índices --- # Índices Os índices são o recurso mais poderoso do Isar. Muitos bancos de dados incorporados oferecem índices "normais" (se houver), mas o Isar também possui índices compostos e de várias entradas. Compreender como os índices funcionam é essencial para otimizar o desempenho da consulta. Isar permite que você escolha qual índice você deseja usar e como deseja usá-lo. Começaremos com uma rápida introdução sobre o que são índices. ## O que são índices? Quando uma coleção não é indexada, a ordem das linhas provavelmente não será discernível pela consulta como otimizada de forma alguma e, portanto, sua consulta terá que pesquisar os objetos linearmente. Em outras palavras, a consulta terá que pesquisar em todos os objetos para encontrar aqueles que correspondam às condições. Como você pode imaginar, isso pode levar algum tempo. Olhar através de cada objeto não é muito eficiente. Por exemplo, esta coleção `Product` é totalmente desordenada. ```dart @collection class Product { Id? id; late String name; late int price; } ``` **Dados:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | Uma consulta que tenta encontrar todos os produtos que custam mais de € 30 deve pesquisar todas as nove linhas. Isso não é um problema para nove linhas, mas pode se tornar um problema para 100 mil linhas. ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` Para melhorar o desempenho desta consulta, indexamos a propriedade `price`. Um índice é como uma tabela de pesquisa classificada: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **Índice gerado:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | Agora, a consulta pode ser executada muito mais rápido. O executor pode pular diretamente para as últimas três linhas do índice e encontrar os objetos correspondentes por seu id. ### Ordenação Outra coisa legal: os índices podem fazer uma classificação super rápida. As consultas classificadas são caras porque o banco de dados precisa carregar todos os resultados na memória antes de classificá-los. Mesmo se você especificar um deslocamento ou limite, eles serão aplicados após a classificação. Vamos imaginar que queremos encontrar os quatro produtos mais baratos. Poderíamos usar a seguinte consulta: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` Neste exemplo, o banco de dados teria que carregar todos os objetos (!), classificá-los por preço e retornar os quatro produtos com o menor preço. Como você provavelmente pode imaginar, isso pode ser feito de forma muito mais eficiente com o índice anterior. O banco de dados pega as primeiras quatro linhas do índice e retorna os objetos correspondentes, pois eles já estão na ordem correta. Para usar o índice para classificação, escreveríamos a consulta assim: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` A cláusula where `.anyX()` diz ao Isar para usar um índice apenas para ordenar. Você também pode usar uma cláusula where como `.priceGreaterThan()` e obter resultados ordenados. ## Índices únicos Um índice único garante que o índice não contenha valores duplicados. Pode consistir em uma ou várias propriedades. Se um índice único tiver uma propriedade, os valores dessa propriedade serão exclusivos. Se o índice exclusivo tiver mais de uma propriedade, a combinação de valores nessas propriedades será única. ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` Qualquer tentativa de inserir ou atualizar dados no índice exclusivo que cause uma duplicata resultará em um erro: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // tente inserir usuário com o mesmo username await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## Substituir índices Às vezes, não é preferível lançar um erro se uma restrição exclusiva for violada. Em vez disso, você pode substituir o objeto existente pelo novo. Isso pode ser feito definindo a propriedade `replace` do índice como `true`. ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` Agora, quando tentamos inserir um usuário com um nome de usuário existente, Isar substituirá o usuário existente pelo novo. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` Os índices de substituição também geram métodos `putBy()` que permitem atualizar objetos em vez de substituí-los. O id existente é reutilizado e os links ainda são preenchidos. ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; //usuário não existe, então é o mesmo que o put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` Como você pode ver, o id do primeiro usuário inserido é reutilizado. ## Índices Case-insensitive Todos os índices nas propriedades `String` e `List` diferenciam maiúsculas de minúsculas por padrão. Se você deseja criar um índice que não diferencia maiúsculas de minúsculas, pode usar a opção `caseSensitive`: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## Tipo de índice Existem diferentes tipos de índices. Na maioria das vezes, você desejará usar um índice `IndexType.value`, mas os índices de hash são mais eficientes. ### Índice de valor Índices de valor são o tipo padrão e o único permitido para todas as propriedades que não contêm Strings ou Lists. Os valores de propriedade são usados para construir o índice. No caso de listas, os elementos da lista são usados. É o mais flexível, mas também consome espaço dos três tipos de índice. :::tip Use `IndexType.value` para primitivos, Strings onde você precisa de cláusulas `startsWith()` where e Lists se você quiser procurar por elementos individuais. ::: ### Índice Hash Strings e Lists podem ser hash para reduzir significativamente o armazenamento exigido pelo índice. A desvantagem dos índices de hash é que eles não podem ser usados para varreduras de prefixo (cláusulas `startsWith` where). :::tip Use `IndexType.hash` para Strings e Lists se você não precisar das cláusulas `startsWith` e `elementEqualTo` where. ::: ### Índice HashElements Lists de strings podem ser hash como um todo (usando `IndexType.hash`), ou os elementos da list podem ser hash separadamente (usando `IndexType.hashElements`), criando efetivamente um índice de várias entradas com elementos hash. :::tip Use `IndexType.hashElements` para `List` onde você precisa das cláusulas where `elementEqualTo`. ::: ## Índices compostos Um índice composto é um índice em várias propriedades. Isar permite criar índices compostos de até três propriedades. Índices compostos também são conhecidos como índices de várias colunas. Provavelmente é melhor começar com um exemplo. Criamos uma coleção de pessoas e definimos um índice composto nas propriedades age e name: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **Dados:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **Índice gerado:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | O índice composto gerado contém todas as pessoas classificadas por idade e nome. Índices compostos são ótimos se você deseja criar consultas eficientes classificadas por várias propriedades. Eles também permitem cláusulas where avançadas com várias propriedades: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` A última propriedade de um índice composto também suporta condições como `startsWith()` ou `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## Índices de várias entradas Se você indexar uma lista usando `IndexType.value`, o Isar criará automaticamente um índice de várias entradas e cada item da lista será indexado em relação ao objeto. Funciona para todos os tipos de listas. As aplicações práticas para índices de várias entradas incluem a indexação de uma lista de tags ou a criação de um índice de texto completo. ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` divide uma string em palavras de acordo com a especificação do [Unicode Annex #29](https://unicode.org/reports/tr29/), então funciona para quase todos os idiomas corretamente. **Dados:** | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | As entradas com palavras duplicadas aparecem apenas uma vez no índice. **Índice gerado:** | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | Este índice agora pode ser usado para prefixo (ou igualdade) onde cláusulas das palavras individuais da descrição. :::tip Em vez de armazenar as palavras diretamente, considere também usar o resultado de um [algoritmo fonético](https://en.wikipedia.org/wiki/Phonetic_algorithm) como [Soundex](https://en.wikipedia.org/wiki/ Soundex). ::: ================================================ FILE: docs/docs/pt/limitations.md ================================================ # Limitações Como você sabe, o Isar funciona em dispositivos móveis e desktops executados na VM e na Web. Ambas as plataformas são muito diferentes e têm limitações diferentes. ## Limitações da VM - Apenas os primeiros 1024 bytes de uma string podem ser usados ​​para um prefixo where-clause - Objetos podem ter apenas 16 MB de tamanho ## Limitações da Web Como o Isar Web depende do IndexedDB, há mais limitações, mas elas são quase imperceptíveis ao usar o Isar. - Métodos síncronos não são suportados - Atualmente, os filtros `Isar.splitWords()` e `.matches()` ainda não estão implementados - As alterações de esquema não são tão rigorosamente verificadas quanto na VM, portanto, tenha cuidado para cumprir as regras - Todos os tipos de números são armazenados como double (o único tipo de número js) para que `@Size32` não tenha efeito - Os índices são representados de forma diferente para que os índices de hash não usem menos espaço (eles ainda funcionam da mesma forma) - `col.delete()` e `col.deleteAll()` funcionam corretamente, mas o valor de retorno não está correto - `col.clear()` não redefine o valor de incremento automático - `NaN` não é suportado como valor ================================================ FILE: docs/docs/pt/links.md ================================================ --- title: Links --- # Links Os links permitem que você expresse relacionamentos entre objetos, como o autor de um comentário (Usuário). Você pode modelar relacionamentos `1:1`, `1:n` e `n:n` com links Isar. Usar links é menos ergonômico do que usar objetos incorporados e você deve usar objetos incorporados sempre que possível. Pense no link como uma tabela separada que contém a relação. É semelhante às relações SQL, mas possui um conjunto de recursos e API diferentes. ## IsarLink `IsarLink` pode conter nenhum ou um objeto relacionado e pode ser usado para expressar um relacionamento de um para um. `IsarLink` tem uma única propriedade chamada `value` que contém o objeto vinculado. Os links são preguiçosos, então você precisa dizer ao `IsarLink` para carregar ou salvar o `valor` explicitamente. Você pode fazer isso chamando `linkProperty.load()` e `linkProperty.save()`. :::tip A propriedade id das coleções de origem e destino de um link não deve ser final. ::: Para destinos não Web, os links são carregados automaticamente quando você os usa pela primeira vez. Vamos começar adicionando um IsarLink a uma coleção: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` Definimos um vínculo entre teachers e students. Cada student pode ter exatamente um teacher neste exemplo. Primeiro, criamos o teacher e o atribuímos a um student. Temos que fazer o `.put()` do teacher e salvar o link manualmente. ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` Agora podemos usar o link: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Vamos tentar a mesma coisa com código síncrono. Não precisamos salvar o link manualmente porque `.putSync()` salva automaticamente todos os links. Até cria o professor para nós. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks Faria mais sentido se o aluno do exemplo anterior pudesse ter vários professores. Felizmente, Isar tem `IsarLinks`, que pode conter vários objetos relacionados e expressar um relacionamento para muitos. `IsarLinks` estende `Set` e expõe todos os métodos permitidos para sets. `IsarLinks` se comporta muito como `IsarLink` e também é preguiçoso. Para carregar todos os objetos vinculados, chame `linkProperty.load()`. Para persistir as alterações, chame `linkProperty.save()`. Internamente, `IsarLink` e `IsarLinks` são representados da mesma maneira. Podemos atualizar o `IsarLink` de antes para um `IsarLinks` para atribuir vários professores a um único aluno (sem perder dados). ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` Isso funciona porque não mudamos o nome do link (`professor`), então Isar se lembra de antes. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## Backlinks Eu ouço você perguntar: "E se quisermos expressar relacionamentos reversos?". Não se preocupe; agora vamos apresentar backlinks. Backlinks são links na direção inversa. Cada link sempre tem um backlink implícito. Você pode disponibilizá-lo para seu aplicativo anotando um `IsarLink` ou `IsarLinks` com `@Backlink()`. Backlinks não requerem memória ou recursos adicionais; você pode adicionar, remover e renomeá-los livremente sem perder dados. Queremos saber quais students um teacher específico tem, então definimos um backlink: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` Precisamos especificar o link para o qual o backlink aponta. É possível ter vários links diferentes entre dois objetos. ## Inicializar links `IsarLink` e `IsarLinks` possuem um construtor sem argumentos, que deve ser usado para atribuir a propriedade link quando o objeto for criado. É uma boa prática tornar as propriedades do link `final`. Quando você faz o `put()` do seu objeto pela primeira vez, o link é inicializado com a coleção de origem e destino, e você pode chamar métodos como `load()` e `save()`. Um link começa a rastrear as alterações imediatamente após sua criação, para que você possa adicionar e remover relações antes mesmo de o link ser inicializado. :::danger É ilegal mover um link para outro objeto. ::: ================================================ FILE: docs/docs/pt/queries.md ================================================ --- title: Queries --- # Queries Querying is how you find records that match certain conditions, for example: - Find all starred contacts - Find distinct first names in contacts - Delete all contacts that don't have the last name defined Because queries are executed on the database and not in Dart, they're really fast. When you cleverly use indexes, you can improve the query performance even further. In the following, you'll learn how to write queries and how you can make them as fast as possible. There are two different methods of filtering your records: Filters and where clauses. We'll start by taking a look at how filters work. ## Filters Filters are easy to use and understand. Depending on the type of your properties, there are different filter operations available most of which have self-explanatory names. Filters work by evaluating an expression for every object in the collection being filtered. If the expression resolves to `true`, Isar includes the object in the results. Filters do not affect the ordering of the results. We'll use the following model for the examples below: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Query conditions Depending on the type of field, there are different conditions available. | Condition | Description | | ----------| ------------| | `.equalTo(value)` | Matches values that are equal to the specified `value`. | | `.between(lower, upper)` | Matches values that are between `lower` and `upper`. | | `.greaterThan(bound)` | Matches values that are greater than `bound`. | | `.lessThan(bound)` | Matches values that are less than `bound`. `null` values will be included by default because `null` is considered smaller than any other value. | | `.isNull()` | Matches values that are `null`.| | `.isNotNull()` | Matches values that are not `null`.| | `.length()` | List, String and link length queries filter objects based on the number of elements in a list or link. | Let's assume the database contains four shoes with sizes 39, 40, 46 and one with an un-set (`null`) size. Unless you perform sorting, the values will be returned sorted by id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Logical operators You can composite predicates using the following logical operators: | Operator | Description | | ---------- | ----------- | | `.and()` | Evaluates to `true` if both left-hand and right-hand expressions evaluate to `true`. | | `.or()` | Evaluates to `true` if either expression evaluates to `true`. | | `.xor()` | Evaluates to `true` if exactly one expression evaluates to `true`. | | `.not()` | Negates the result of the following expression. | | `.group()` | Group conditions and allow to specify order of evaluation. | If you want to find all shoes in size 46, you can use the following query: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` If you want to use more than one condition, you can combine multiple filters using logical **and** `.and()`, logical **or** `.or()` and logical **xor** `.xor()`. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filters are implicitly combined with logical and. .isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to: `size == 46 && isUnisex == true`. You can also group conditions using `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` This query is equivalent to `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. To negate a condition or group, use logical **not** `.not()`: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to `size != 46 && isUnisex != true`. ### String conditions In addition to the query conditions above, String values offer a few more conditions you can use. Regex-like wildcards, for example, allow more flexibility in search. | Condition | Description | | -------------------- | ----------------------------------------------------------------- | | `.startsWith(value)` | Matches string values that begins with provided `value`. | | `.contains(value)` | Matches string values that contain the provided `value`. | | `.endsWith(value)` | Matches string values that end with the provided `value`. | | `.matches(wildcard)` | Matches string values that match the provided `wildcard` pattern. | **Case sensitivity** All string operations have an optional `caseSensitive` parameter that defaults to `true`. **Wildcards:** A [wildcard string expression](https://en.wikipedia.org/wiki/Wildcard_character) is a string that uses normal characters with two special wildcard characters: - The `*` wildcard matches zero or more of any character - The `?` wildcard matches any character. For example, the wildcard string `"d?g"` matches `"dog"`, `"dig"`, and `"dug"`, but not `"ding"`, `"dg"`, or `"a dog"`. ### Query modifiers Sometimes it is necessary to build a query based on some conditions or for different values. Isar has a very powerful tool for building conditional queries: | Modifier | Description | | --------------------- | ---------------------------------------------------- | | `.optional(cond, qb)` | Extends the query only if the `condition` is `true`. This can be used almost anywhere in a query for example to conditionally sort or limit it. | | `.anyOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **or**. | | `.allOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **and**. | In this example, we build a method that can find shoes with an optional filter: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // only apply filter if sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` If you want to find all shoes that have one of multiple shoe sizes, you can either write a conventional query or use the `anyOf()` modifier: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` Query modifiers are especially useful when you want to build dynamic queries. ### Lists Even lists can be queried: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` You can query based on the list length: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` These are equivalent to the Dart code `tweets.where((t) => t.hashtags.isEmpty);` and `tweets.where((t) => t.hashtags.length > 5);`. You can also query based on list elements: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` This is equivalent to the Dart code `tweets.where((t) => t.hashtags.contains('flutter'));`. ### Embedded objects Embedded objects are one of Isar's most useful features. They can be queried very efficiently using the same conditions available for top-level objects. Let's assume we have the following model: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` We want to query all cars that have a brand with the name `"BMW"` and the country `"Germany"`. We can do this using the following query: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Always try to group nested queries. The above query is more efficient than the following one. Even though the result is the same: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Links If your model contains [links or backlinks](links) you can filter your query based on the linked objects or the number of linked objects. :::warning Keep in mind that link queries can be expensive because Isar needs to look up linked objects. Consider using embedded objects instead. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` We want to find all students that have a math or English teacher: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Link filters evaluate to `true` if at least one linked object matches the conditions. Let's search for all students that have no teachers: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` or alternatively: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where clauses Where clauses are a very powerful tool, but it can be a little challenging to get them right. In contrast to filters where clauses use the indexes you defined in the schema to check the query conditions. Querying an index is a lot faster than filtering each record individually. ➡️ Learn more: [Indexes](indexes) :::tip As a basic rule, you should always try to reduce the records as much as possible using where clauses and do the remaining filtering using filters. ::: You can only combine where clauses using logical **or**. In other words, you can sum multiple where clauses together, but you can't query the intersection of multiple where clauses. Let's add indexes to the shoe collection: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` There are two indexes. The index on `size` allows us to use where clauses like `.sizeEqualTo()`. The composite index on `isUnisex` allows where clauses like `isUnisexSizeEqualTo()`. But also `isUnisexEqualTo()` because you can always use any prefix of an index. We can now rewrite the query from before that finds unisex shoes in size 46 using the composite index. This query will be a lot faster than the previous one: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where clauses have two more superpowers: They give you "free" sorting and a super fast distinct operation. ### Combining where clauses and filters Remember the `shoes.filter()` queries? It's actually just a shortcut for `shoes.where().filter()`. You can (and should) combine where clauses and filters in the same query to use the benefits of both: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` The where clause is applied first to reduce the number of objetcs to be filtered. Then the filter is applied to the remaining objetcs. ## Sorting You can define how the results should be sorted when executing the query using the `.sortBy()`, `.sortByDesc()`, `.thenBy()` and `.thenByDesc()` methods. To find all shoes sorted by model name in ascending order and size in descending order without using an index: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Sorting many results can be expensive, especially since sorting happens before offset and limit. The sorting methods above never make use of indexes. Luckily, we can again use where clause sorting and make our query lightning-fast even if we need to sort a million objects. ### Where clause sorting If you use a **single** where clause in your query, the results are already sorted by the index. That's a big deal! Let's assume we have shoes in sizes `[43, 39, 48, 40, 42, 45]` and we want to find all shoes with a size greater than `42` and also have them sorted by size: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // also sorts the results by size .findAll(); // -> [43, 45, 48] ``` As you can see, the result is sorted by the `size` index. If you want to reverse the where clause sort order, you can set `sort` to `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` Sometimes you don't want to use a where clause but still benefit from the implicit sorting. You can use the `any` where clause: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` If you use a composite index, the results are sorted by all fields in the index. :::tip If you need the results to be sorted, consider using an index for that purpose. Especially if you work with `offset()` and `limit()`. ::: Sometimes it's not possible or useful to use an index for sorting. For such cases, you should use indexes to reduce the number of resulting entries as much as possible. ## Unique values To return only entries with unique values, use the distinct predicate. For example, to find out how many different shoe models you have in your Isar database: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` You can also chain multiple distinct conditions to find all shoes with distinct model-size combinations: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Only the first result of each distinct combination is returned. You can use where clauses and sort operations to control it. ### Where clause distinct If you have a non-unique index, you may want to get all of its distinct values. You could use the `distinctBy` operation from the previous section, but it's performed after sorting and filters, so there is some overhead. If you only use a single where clause, you can instead rely on the index to perform the distinct operation. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip In theory, you could even use multiple where clauses for sorting and distinct. The only restriction is that those where clauses are not overlapping and use the same index. For correct sorting, they also need to be applied in sort order. Be very careful if you rely on this! ::: ## Offset & Limit It's often a good idea to limit the number of results from a query for lazy list views. You can do so by setting a `limit()`: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` By setting an `offset()` you can also paginate the results of your query. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Since instantiating Dart objects is often the most expensive part of executing a query, it is a good idea only to load the objects you need. ## Execution order Isar executes queries always in the same order: 1. Traverse primary or secondary index to find objects (apply where clauses) 2. Filter objects 3. Sort results 4. Apply distinct operation 5. Offset & limit results 6. Return results ## Query operations In the previous examples, we used `.findAll()` to retrieve all matching objects. There are more operations available, however: | Operation | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | Retreive only the first matching object or `null` if none matches. | | `.findAll()` | Retreive all matching objects. | | `.count()` | Count how many objects match the query. | | `.deleteFirst()` | Delete the first matching object from the collection. | | `.deleteAll()` | Delete all matching objects from the collection. | | `.build()` | Compile the query to reuse it later. This saves the cost to build a query if you want to execute it multiple times. | ## Property queries If you are only interested in the values of a single property, you can use a property query. Just build a regular query and select a property: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` Using only a single property saves time during deserialization. Property queries also work for embedded objects and lists. ## Aggregation Isar supports aggregating the values of a property query. The following aggregation operations are available: | Operation | Description | | ------------ | -------------------------------------------------------------- | | `.min()` | Finds the minimum value or `null` if none matches. | | `.max()` | Finds the maximum value or `null` if none matches. | | `.sum()` | Sums all values. | | `.average()` | Calculates the average of all values or `NaN` if none matches. | Using aggregations is vastly faster than finding all matching objects and performing the aggregation manually. ## Dynamic queries :::danger This section is most likely not relevant to you. It is discouraged to use dynamic queries unless you absolutely need to (and you rarely do). ::: All the examples above used the QueryBuilder and the generated static extension methods. Maybe you want to create dynamic queries or a custom query language (like the Isar Inspector). In that case, you can use the `buildQuery()` method: | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------------- | | `whereClauses` | The where clauses of the query. | | `whereDistinct` | Whether where clauses should return distinct values (only useful for single where clauses). | | `whereSort` | The traverse order of the where clauses (only useful for single where clauses). | | `filter` | The filter to apply to the results. | | `sortBy` | A list of properties to sort by. | | `distinctBy` | A list of properties to distinct by. | | `offset` | The offset of the results. | | `limit` | The maximum number of results to return. | | `property` | If non-null, only the values of this property are returned. | Let's create a dynamic query: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` The following query is equivalent: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/pt/schema.md ================================================ --- title: Schema --- # Schema When you use Isar to store your app's data, you're dealing with collections. A collection is like a database table in the associated Isar database and can only contain a single type of Dart object. Each collection object represents a row of data in the corresponding collection. A collection definition is called "schema". The Isar Generator will do the heavy lifting for you and generate most of the code you need to use the collection. ## Anatomy of a collection You define each Isar collection by annotating a class with `@collection` or `@Collection()`. An Isar collection includes fields for each column in the corresponding table in the database, including one that comprises the primary key. The following code is an example of a simple collection that defines a `User` table with columns for ID, first name, and last name: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip To persist a field, Isar must have access to it. You can ensure Isar has access to a field by making it public or by providing getter and setter methods. ::: There are a few optional parameters to customize the collection: | Config | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `inheritance` | Control whether fields of parent classes and mixins will be stored in Isar. Enabled by default. | | `accessor` | Allows you to rename the default collection accessor (for example `isar.contacts` for the `Contact` collection). | | `ignore` | Allows ignoring certain properties. These are also respected for super classes. | ### Isar Id Each collection class has to define an id property with the type `Id` uniquely identifying an object. `Id` is just an alias for `int` that allows the Isar Generator to recognize the id property. Isar automatically indexes id fields, which allows you to get and modify objects based on their id efficiently. You can either set ids yourself or ask Isar to assign an auto-increment id. If the `id` field is `null` and not `final`, Isar will assign an auto-increment id. If you want a non-nullable auto-increment id, you can use `Isar.autoIncrement` instead of `null`. :::tip Auto increment ids are not reused when an object is deleted. The only way to reset auto-increment ids is to clear the database. ::: ### Renaming collections and fields By default, Isar uses the class name as the collection name. Similarly, Isar uses field names as column names in the database. If you want a collection or field to have a different name, add the `@Name` annotation. The following example demonstrates custom names for collection and fields: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Especially if you want to rename Dart fields or classes that are already stored in the database, you should consider using the `@Name` annotation. Otherwise, the database will delete and re-create the field or collection. ### Ignoring fields Isar persists all public fields of a collection class. By annotating a property or getter with `@ignore`, you can exclude it from persistence, as shown in the following code snippet: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` In cases where a collection inherits fields from a parent collection, it's usually easier to use the `ignore` property of the `@Collection` annotation: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` If a collection contains a field with a type that is not supported by Isar, you have to ignore the field. :::warning Keep in mind that it is not good practice to store information in Isar objects that are not persisted. ::: ## Supported types Isar supports the following data types: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` Additionally, embedded objects and enums are supported. We'll cover those below. ## byte, short, float For many use cases, you don't need the full range of a 64-bit integer or double. Isar supports additional types that allow you to save space and memory when storing smaller numbers. | Type | Size in bytes | Range | | ---------- | ------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | The additional number types are just aliases for the native Dart types, so using `short`, for example, works the same as using `int`. Here is an example collection containing all of the above types: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` All number types can also be used in lists. For storing bytes, you should use `List`. ## Nullable types Understanding how nullability works in Isar is essential: Number types do **NOT** have a dedicated `null` representation. Instead, a specific value is used: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, and `List` have a separate `null` representation. This behavior enables performance improvements, and it allows you to change the nullability of your fields freely without requiring migration or special code to handle `null` values. :::warning The `byte` type does not support null values. ::: ## DateTime Isar does not store timezone information of your dates. Instead, it converts `DateTime`s to UTC before storing them. Isar returns all dates in local time. `DateTime`s are stored with microsecond precision. In browsers, only millisecond precision is supported because of JavaScript limitations. ## Enum Isar allows storing and using enums like other Isar types. You have to choose, however, how Isar should represent the enum on the disk. Isar supports four different strategies: | EnumType | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | `ordinal` | The index of the enum is stored as `byte`. This is very efficient but does not allow nullable enums | | `ordinal32` | The index of the enum is stored as `short` (4-byte integer). | | `name` | The enum name is stored as `String`. | | `value` | A custom property is used to retrieve the enum value. | :::warning `ordinal` and `ordinal32` depend on the order of the enum values. If you change the order, existing databases will return incorrect values. ::: Let's check out an example for each strategy. ```dart @collection class EnumCollection { Id? id; @enumerated // same as EnumType.ordinal late TestEnum byteIndex; // cannot be nullable @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // cannot be nullable @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Of course, Enums can also be used in lists. ## Embedded objects It's often helpful to have nested objects in your collection model. There is no limit to how deep you can nest objects. Keep in mind, however, that updating a deeply nested object will require writing the whole object tree to the database. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Embedded objects can be nullable and extend other objects. The only requirement is that they are annotated with `@embedded` and have a default constructor without required parameters. ================================================ FILE: docs/docs/pt/transactions.md ================================================ --- title: Transações --- # Transações No Isar, as transações combinam várias operações de banco de dados em uma única unidade de trabalho. A maioria das interações com Isar usa transações implicitamente. O acesso de leitura e gravação no Isar é compatível com [ACID](http://en.wikipedia.org/wiki/ACID). As transações são revertidas automaticamente se ocorrer um erro. ## Transações explicitas Em uma transação explícita, você obtém um snapShot consistente do banco de dados. Tente minimizar a duração das transações. É proibido fazer chamadas de rede ou outras operações de longa duração em uma transação. As transações (especialmente transações de gravação) têm um custo e você deve sempre tentar agrupar operações sucessivas em uma única transação. As transações podem ser síncronas ou assíncronas. Em transações síncronas, você só pode usar operações síncronas. Em transações assíncronas, apenas operações assíncronas. | | Read | Read & Write | |--------------|--------------|--------------------| | Synchronous | `.txnSync()` | `.writeTxnSync()` | | Asynchronous | `.txn()` | `.writeTxn()` | ### Transações de leitura As transações de leitura explícita são opcionais, mas permitem que você faça leituras atômicas e dependa de um estado consistente do banco de dados dentro da transação. Internamente, o Isar sempre usa transações de leitura implícitas para todas as operações de leitura. :::tip As transações de leitura assíncrona são executadas em paralelo com outras transações de leitura e gravação. Bem fixe, certo? ::: ### Transações de escrita Ao contrário das operações de leitura, as operações de gravação em Isar devem ser agrupadas em uma transação explícita. Quando uma transação de gravação é concluída com êxito, ela é confirmada automaticamente e todas as alterações são gravadas no disco. Se ocorrer um erro, a transação será abortada e todas as alterações serão revertidas. As transações são “tudo ou nada”: ou todas as gravações em uma transação são bem-sucedidas ou nenhuma delas entra em vigor para garantir a consistência dos dados. :::warning Quando uma operação de banco de dados falha, a transação é abortada e não deve mais ser usada. Mesmo se você pegar o erro no Dart. ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: move loop inside transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/pt/tutorials/quickstart.md ================================================ --- title: Início rápido --- # Início rápido Caramba, você está aqui! Vamos começar a usar o banco de dados Flutter mais legal que existe... Seremos curtos em palavras e rápidos em código neste início rápido. ## 1. Adicionar dependências Antes que a diversão comece, precisamos adicionar alguns pacotes ao `pubspec.yaml`. Podemos usar o pub para fazer o trabalho complexo para nós. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Anotar classes Anote suas coleções de classes com `@collection` e escolha um campo 'Id'. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // você também pode attribuir id = null para incrementar automaticamente String? name; int? age; } ``` Os IDs identificam exclusivamente objetos em uma coleção e permitem que você os encontre novamente mais tarde. ## 3. Executar gerador de código Execute o seguinte comando para iniciar o `build_runner`: ``` dart run build_runner build ``` Se você estiver usando o Flutter, use o seguinte: ``` flutter pub run build_runner build ``` ## 4. Abrir instância Isar Abra uma nova instância Isar e passe todos os seus esquemas de coleção. Opcionalmente, você pode especificar um nome de instância e um diretório. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Escrever e ler Depois que sua instância estiver aberta, você poderá começar a usar as coleções. Todas as operações básicas de CRUD estão disponíveis via `IsarCollection`. ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // inserir & atualizar }); final existingUser = await isar.users.get(newUser.id); // get await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // delete }); ``` ## Outros recursos Você é um aprendiz visual? Confira estes vídeos para começar com Isar:


================================================ FILE: docs/docs/pt/watchers.md ================================================ --- title: Watchers --- # Watchers Isar allows you to subscribe to changes in the database. You can "watch" for changes in a specific object, an entire collection, or a query. Watchers enable you to react to changes in the database efficiently. You can for example rebuild your UI when a contact is added, send a network request when a document is updated, etc. A watcher is notified after a transaction commits successfully and the target actually changes. ## Watching Objects If you want to be notified when a specific object is created, updated or deleted, you should watch an object: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` As you can see in the example above, the object does not need to exist yet. The watcher will be notified when it is created. There is an additional parameter `fireImmediately`. If you set it to `true`, Isar will immediately add the object's current value to the stream. ### Lazy watching Maybe you don't need to receive the new value but only be notified about the change. That saves Isar from having to fetch the object: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## Watching Collections Instead of watching a single object, you can watch an entire collection and get notified when any object is added, updated, or deleted: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## Watching Queries It is even possible to watch entire queries. Isar does its best to only notify you when the query results actually change. You will not be notified if links cause the query to change. Use a collection watcher if you need to be notified about link changes. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning If you use offset & limit or distinct queries, Isar will also notify you when objects match the filter but outside the query, results change. ::: Just like `watchObject()`, you can use `watchLazy()` to get notified when the query results change but not fetch the results. :::danger Rerunning queries for every change is very inefficient. It would be best if you used a lazy collection watcher instead. ::: ================================================ FILE: docs/docs/queries.md ================================================ --- title: Queries --- # Queries Querying is how you find records that match certain conditions, for example: - Find all starred contacts - Find distinct first names in contacts - Delete all contacts that don't have the last name defined Because queries are executed on the database and not in Dart, they're really fast. When you cleverly use indexes, you can improve the query performance even further. In the following, you'll learn how to write queries and how you can make them as fast as possible. There are two different methods of filtering your records: Filters and where clauses. We'll start by taking a look at how filters work. ## Filters Filters are easy to use and understand. Depending on the type of your properties, there are different filter operations available most of which have self-explanatory names. Filters work by evaluating an expression for every object in the collection being filtered. If the expression resolves to `true`, Isar includes the object in the results. Filters do not affect the ordering of the results. We'll use the following model for the examples below: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### Query conditions Depending on the type of field, there are different conditions available. | Condition | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `.equalTo(value)` | Matches values that are equal to the specified `value`. | | `.between(lower, upper)` | Matches values that are between `lower` and `upper`. | | `.greaterThan(bound)` | Matches values that are greater than `bound`. | | `.lessThan(bound)` | Matches values that are less than `bound`. `null` values will be included by default because `null` is considered smaller than any other value. | | `.isNull()` | Matches values that are `null`. | | `.isNotNull()` | Matches values that are not `null`. | | `.length()` | List, String and link length queries filter objects based on the number of elements in a list or link. | Let's assume the database contains four shoes with sizes 39, 40, 46 and one with an un-set (`null`) size. Unless you perform sorting, the values will be returned sorted by id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### Logical operators You can composite predicates using the following logical operators: | Operator | Description | | ---------- | ------------------------------------------------------------------------------------ | | `.and()` | Evaluates to `true` if both left-hand and right-hand expressions evaluate to `true`. | | `.or()` | Evaluates to `true` if either expression evaluates to `true`. | | `.xor()` | Evaluates to `true` if exactly one expression evaluates to `true`. | | `.not()` | Negates the result of the following expression. | | `.group()` | Group conditions and allow to specify order of evaluation. | If you want to find all shoes in size 46, you can use the following query: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` If you want to use more than one condition, you can combine multiple filters using logical **and** `.and()`, logical **or** `.or()` and logical **xor** `.xor()`. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filters are implicitly combined with logical and. .isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to: `size == 46 && isUnisex == true`. You can also group conditions using `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` This query is equivalent to `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. To negate a condition or group, use logical **not** `.not()`: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to `size != 46 && isUnisex != true`. ### String conditions In addition to the query conditions above, String values offer a few more conditions you can use. Regex-like wildcards, for example, allow more flexibility in search. | Condition | Description | | -------------------- | ----------------------------------------------------------------- | | `.startsWith(value)` | Matches string values that begins with provided `value`. | | `.contains(value)` | Matches string values that contain the provided `value`. | | `.endsWith(value)` | Matches string values that end with the provided `value`. | | `.matches(wildcard)` | Matches string values that match the provided `wildcard` pattern. | **Case sensitivity** All string operations have an optional `caseSensitive` parameter that defaults to `true`. **Wildcards:** A [wildcard string expression](https://en.wikipedia.org/wiki/Wildcard_character) is a string that uses normal characters with two special wildcard characters: - The `*` wildcard matches zero or more of any character - The `?` wildcard matches any character. For example, the wildcard string `"d?g"` matches `"dog"`, `"dig"`, and `"dug"`, but not `"ding"`, `"dg"`, or `"a dog"`. ### Query modifiers Sometimes it is necessary to build a query based on some conditions or for different values. Isar has a very powerful tool for building conditional queries: | Modifier | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `.optional(cond, qb)` | Extends the query only if the `condition` is `true`. This can be used almost anywhere in a query for example to conditionally sort or limit it. | | `.anyOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **or**. | | `.allOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **and**. | In this example, we build a method that can find shoes with an optional filter: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // only apply filter if sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` If you want to find all shoes that have one of multiple shoe sizes, you can either write a conventional query or use the `anyOf()` modifier: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` Query modifiers are especially useful when you want to build dynamic queries. ### Lists Even lists can be queried: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` You can query based on the list length: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` These are equivalent to the Dart code `tweets.where((t) => t.hashtags.isEmpty);` and `tweets.where((t) => t.hashtags.length > 5);`. You can also query based on list elements: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` This is equivalent to the Dart code `tweets.where((t) => t.hashtags.contains('flutter'));`. ### Embedded objects Embedded objects are one of Isar's most useful features. They can be queried very efficiently using the same conditions available for top-level objects. Let's assume we have the following model: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` We want to query all cars that have a brand with the name `"BMW"` and the country `"Germany"`. We can do this using the following query: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` Always try to group nested queries. The above query is more efficient than the following one. Even though the result is the same: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### Links If your model contains [links or backlinks](links) you can filter your query based on the linked objects or the number of linked objects. :::warning Keep in mind that link queries can be expensive because Isar needs to look up linked objects. Consider using embedded objects instead. ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` We want to find all students that have a math or English teacher: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Link filters evaluate to `true` if at least one linked object matches the conditions. Let's search for all students that have no teachers: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` or alternatively: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where clauses Where clauses are a very powerful tool, but it can be a little challenging to get them right. In contrast to filters where clauses use the indexes you defined in the schema to check the query conditions. Querying an index is a lot faster than filtering each record individually. ➡️ Learn more: [Indexes](indexes) :::tip As a basic rule, you should always try to reduce the records as much as possible using where clauses and do the remaining filtering using filters. ::: You can only combine where clauses using logical **or**. In other words, you can sum multiple where clauses together, but you can't query the intersection of multiple where clauses. Let's add indexes to the shoe collection: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` There are two indexes. The index on `size` allows us to use where clauses like `.sizeEqualTo()`. The composite index on `isUnisex` allows where clauses like `.isUnisexSizeEqualTo()`. But also `.isUnisexEqualTo()` because you can always use any prefix of an index. We can now rewrite the query from before that finds unisex shoes in size 46 using the composite index. This query will be a lot faster than the previous one: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where clauses have two more superpowers: They give you "free" sorting and a super fast distinct operation. ### Combining where clauses and filters Remember the `shoes.filter()` queries? It's actually just a shortcut for `shoes.where().filter()`. You can (and should) combine where clauses and filters in the same query to use the benefits of both: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` The where clause is applied first to reduce the number of objects to be filtered. Then the filter is applied to the remaining objects. ## Sorting You can define how the results should be sorted when executing the query using the `.sortBy()`, `.sortByDesc()`, `.thenBy()` and `.thenByDesc()` methods. To find all shoes sorted by model name in ascending order and size in descending order without using an index: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` Sorting many results can be expensive, especially since sorting happens before offset and limit. The sorting methods above never make use of indexes. Luckily, we can again use where clause sorting and make our query lightning-fast even if we need to sort a million objects. ### Where clause sorting If you use a **single** where clause in your query, the results are already sorted by the index. That's a big deal! Let's assume we have shoes in sizes `[43, 39, 48, 40, 42, 45]` and we want to find all shoes with a size greater than `42` and also have them sorted by size: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // also sorts the results by size .findAll(); // -> [43, 45, 48] ``` As you can see, the result is sorted by the `size` index. If you want to reverse the where clause sort order, you can set `sort` to `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` Sometimes you don't want to use a where clause but still benefit from the implicit sorting. You can use the `any` where clause: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` If you use a composite index, the results are sorted by all fields in the index. :::tip If you need the results to be sorted, consider using an index for that purpose. Especially if you work with `offset()` and `limit()`. ::: Sometimes it's not possible or useful to use an index for sorting. For such cases, you should use indexes to reduce the number of resulting entries as much as possible. ## Unique values To return only entries with unique values, use the distinct predicate. For example, to find out how many different shoe models you have in your Isar database: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` You can also chain multiple distinct conditions to find all shoes with distinct model-size combinations: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` Only the first result of each distinct combination is returned. You can use where clauses and sort operations to control it. ### Where clause distinct If you have a non-unique index, you may want to get all of its distinct values. You could use the `distinctBy` operation from the previous section, but it's performed after sorting and filters, so there is some overhead. If you only use a single where clause, you can instead rely on the index to perform the distinct operation. ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip In theory, you could even use multiple where clauses for sorting and distinct. The only restriction is that those where clauses are not overlapping and use the same index. For correct sorting, they also need to be applied in sort order. Be very careful if you rely on this! ::: ## Offset & Limit It's often a good idea to limit the number of results from a query for lazy list views. You can do so by setting a `limit()`: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` By setting an `offset()` you can also paginate the results of your query. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` Since instantiating Dart objects is often the most expensive part of executing a query, it is a good idea only to load the objects you need. ## Execution order Isar executes queries always in the same order: 1. Traverse primary or secondary index to find objects (apply where clauses) 2. Filter objects 3. Sort results 4. Apply distinct operation 5. Offset & limit results 6. Return results ## Query operations In the previous examples, we used `.findAll()` to retrieve all matching objects. There are more operations available, however: | Operation | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | Retrieve only the first matching object or `null` if none matches. | | `.findAll()` | Retrieve all matching objects. | | `.count()` | Count how many objects match the query. | | `.deleteFirst()` | Delete the first matching object from the collection. | | `.deleteAll()` | Delete all matching objects from the collection. | | `.build()` | Compile the query to reuse it later. This saves the cost to build a query if you want to execute it multiple times. | ## Property queries If you are only interested in the values of a single property, you can use a property query. Just build a regular query and select a property: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` Using only a single property saves time during deserialization. Property queries also work for embedded objects and lists. ## Aggregation Isar supports aggregating the values of a property query. The following aggregation operations are available: | Operation | Description | | ------------ | -------------------------------------------------------------- | | `.min()` | Finds the minimum value or `null` if none matches. | | `.max()` | Finds the maximum value or `null` if none matches. | | `.sum()` | Sums all values. | | `.average()` | Calculates the average of all values or `NaN` if none matches. | Using aggregations is vastly faster than finding all matching objects and performing the aggregation manually. ## Dynamic queries :::danger This section is most likely not relevant to you. It is discouraged to use dynamic queries unless you absolutely need to (and you rarely do). ::: All the examples above used the QueryBuilder and the generated static extension methods. Maybe you want to create dynamic queries or a custom query language (like the Isar Inspector). In that case, you can use the `buildQuery()` method: | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------------- | | `whereClauses` | The where clauses of the query. | | `whereDistinct` | Whether where clauses should return distinct values (only useful for single where clauses). | | `whereSort` | The traverse order of the where clauses (only useful for single where clauses). | | `filter` | The filter to apply to the results. | | `sortBy` | A list of properties to sort by. | | `distinctBy` | A list of properties to distinct by. | | `offset` | The offset of the results. | | `limit` | The maximum number of results to return. | | `property` | If non-null, only the values of this property are returned. | Let's create a dynamic query: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` The following query is equivalent: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/recipes/data_migration.md ================================================ --- title: Data migration --- # Data Migration Isar automatically migrates your database schemas if you add or remove collections, fields, or indexes. Sometimes you might want to migrate your data as well. Isar does not offer a built-in solution because it would impose arbitrary migration restrictions. It is easy to implement migration logic that fits your needs. We want to use a single version for the entire database in this example. We use shared preferences to store the current version and compare it to the version we want to migrate to. If the versions do not match, we migrate the data and update the version. :::tip You could also give each collection its own version and migrate them individually. ::: Imagine we have a user collection with a birthday field. In version 2 of our app, we need an additional birth year field to query users based on age. Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` The problem is the existing user models will have an empty `birthYear` field because it did not exist in version 1. We need to migrate the data to set the `birthYear` field. ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // If the version is not set (new installation) or already 2, we do not need to migrate return; default: throw Exception('Unknown version: $currentVersion'); } // Update version await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // We paginate through the users to avoid loading all users into memory at once for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // We don't need to update anything since the birthYear getter is used await isar.users.putAll(users); }); } } ``` :::warning If you have to migrate a lot of data, consider using a background isolate to prevent strain on the UI thread. ::: ================================================ FILE: docs/docs/recipes/full_text_search.md ================================================ --- title: Full-text search --- # Full-text search Full-text search is a powerful way to search text in the database. You should already be familiar with how [indexes](/indexes) work, but let's go over the basics. An index works like a lookup table, allowing the query engine to find records with a given value quickly. For example, if you have a `title` field in your object, you can create an index on that field to make it faster to find objects with a given title. ## Why is full-text search useful? You can easily search text using filters. There are various string operations for example `.startsWith()`, `.contains()` and `.matches()`. The problem with filters is that their runtime is `O(n)` where `n` is the number of records in the collection. String operations like `.matches()` are especially expensive. :::tip Full-text search is much faster than filters, but indexes have some limitations. In this recipe, we will explore how to work around these limitations. ::: ## Basic example The idea is always the same: Instead of indexing the whole text, we index the words in the text so we can search for them individually. Let's create the most basic full-text index: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` We can now search for messages with specific words in the content: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` This query is super fast, but there are some problems: 1. We can only search for entire words 2. We do not consider punctuation 3. We do not support other whitespace characters ## Splitting text the right way Let's try to improve the previous example. We could try to develop a complicated regex to fix word splitting, but it will likely be slow and wrong for edge cases. The [Unicode Annex #29](https://unicode.org/reports/tr29/) defines how to split text into words correctly for almost all languages. It is quite complicated, but fortunately, Isar does the heavy lifting for us: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## I want more control Easy peasy! We can change our index also to support prefix matching and case-insensitive matching: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` By default, Isar will store the words as hashed values which is fast and space efficient. But hashes can't be used for prefix matching. Using `IndexType.value`, we can change the index to use the words directly instead. It gives us the `.titleWordsAnyStartsWith()` where clause: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## I also need `.endsWith()` Sure thing! We will use a trick to achieve `.endsWith()` matching: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` Don't forget reversing the ending you want to search for: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Stemming algorithms Unfortunately, indexes do not support `.contains()` matching (this is true for other databases as well). But there are a few alternatives that are worth exploring. The choice highly depends on your use. One example is indexing word stems instead of the whole word. A stemming algorithm is a process of linguistic normalization in which the variant forms of a word are reduced to a common form: ``` connection connections connective ---> connect connected connecting ``` Popular algorithms are the [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) and the [Snowball stemming algorithms](https://snowballstem.org/algorithms/). There are also more advanced forms like [lemmatization](https://en.wikipedia.org/wiki/Lemmatisation). ## Phonetic algorithms A [phonetic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) is an algorithm for indexing words by their pronunciation. In other words, it allows you to find words that sound similar to the ones you are looking for. :::warning Most phonetic algorithms only support a single language. ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex) is a phonetic algorithm for indexing names by sound, as pronounced in English. The goal is for homophones to be encoded to the same representation so they can be matched despite minor differences in spelling. It is a straightforward algorithm, and there are multiple improved versions. Using this algorithm, both `"Robert"` and `"Rupert"` return the string `"R163"` while `"Rubin"` yields `"R150"`. `"Ashcraft"` and `"Ashcroft"` both yield `"A261"`. ### Double Metaphone The [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) phonetic encoding algorithm is the second generation of this algorithm. It makes several fundamental design improvements over the original Metaphone algorithm. Double Metaphone accounts for various irregularities in English of Slavic, Germanic, Celtic, Greek, French, Italian, Spanish, Chinese, and other origins. ================================================ FILE: docs/docs/recipes/multi_isolate.md ================================================ --- title: Multi-Isolate usage --- # Multi-Isolate usage Instead of threads, all Dart code runs inside isolates. Each isolate has its own memory heap, ensuring that none of the state in an isolate is accessible from any other isolate. Isar can be accessed from multiple isolates at the same time, and even watchers work across isolates. In this recipe, we will check out how to use Isar in a multi-isolate environment. ## When to use multiple isolates Isar transactions are executed in parallel even if they run in the same isolate. In some cases, it is still beneficial to access Isar from multiple isolates. The reason is that Isar spends quite some time encoding and decoding data from and to Dart objects. You can think of it as encoding and decoding JSON (just more efficient). These operations run inside the isolate from which the data is accessed and naturally block other code in the isolate. In other words: Isar performs some of the work in your Dart isolate. If you only need to read or write a few hundred objects at once, doing it in the UI isolate is not a problem. But for huge transactions or if the UI thread is already busy, you should consider using a separate isolate. ## Example The first thing we need to do is to open Isar in the new isolate. Since the instance of Isar is already open in the main isolate, `Isar.open()` will return the same instance. :::warning Make sure to provide the same schemas as in the main isolate. Otherwise, you will get an error. ::: `compute()` starts a new isolate in Flutter and runs the given function in it. ```dart void main() { // Open Isar in the UI isolate final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // listen to changes in the database isar.messages.watchLazy(() { print('omg the messages changed!'); }); // start a new isolate and create 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // after some time: // > omg the messages changed! // > isolate finished } // function that will be executed in the new isolate Future createDummyMessages(int count) async { // we don't need the path here because the instance is already open final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // we use synchronous transactions in isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` There are a few interesting things to note in the example above: - `isar.messages.watchLazy()` is called in the UI isolate and is notified of changes from another isolate. - Instances are referenced by name. The default name is `default`, but in this example, we set it to `myInstance`. - We used a synchronous transaction to create the messages. Blocking our new isolate is no problem, and synchronous transactions are a little faster. ================================================ FILE: docs/docs/recipes/string_ids.md ================================================ --- title: String ids --- # String ids This is one of the most frequent requests I get, so here is a tutorial on using String ids. Isar does not natively support String ids, and there is a good reason for it: integer ids are much more efficient and faster. Especially for links, the overhead of a String id is too significant. I understand that sometimes you have to store external data that uses UUIDs or other non-integer ids. I recommend storing the String id as a property in your object and using a fast hash implementation to generate a 64-bit int that can be used as Id. ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` With this approach, you get the best of both worlds: Efficient integer ids for links and the ability to use String ids. ## Fast hash function Ideally, your hash function should have high quality (you don't want collisions) and be fast. I recommend using the following implementation: ```dart /// FNV-1a 64bit hash algorithm optimized for Dart Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` If you choose a different hash function, ensure it returns a 64-bit int and avoid using a cryptographic hash function because they are much slower. :::warning Avoid using `string.hashCode` because it is not guaranteed to be stable across different platforms and versions of Dart. ::: ================================================ FILE: docs/docs/schema.md ================================================ --- title: Schema --- # Schema When you use Isar to store your app's data, you're dealing with collections. A collection is like a database table in the associated Isar database and can only contain a single type of Dart object. Each collection object represents a row of data in the corresponding collection. A collection definition is called "schema". The Isar Generator will do the heavy lifting for you and generate most of the code you need to use the collection. ## Anatomy of a collection You define each Isar collection by annotating a class with `@collection` or `@Collection()`. An Isar collection includes fields for each column in the corresponding table in the database, including one that comprises the primary key. The following code is an example of a simple collection that defines a `User` table with columns for ID, first name, and last name: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip To persist a field, Isar must have access to it. You can ensure Isar has access to a field by making it public or by providing getter and setter methods. ::: There are a few optional parameters to customize the collection: | Config | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `inheritance` | Control whether fields of parent classes and mixins will be stored in Isar. Enabled by default. | | `accessor` | Allows you to rename the default collection accessor (for example `isar.contacts` for the `Contact` collection). | | `ignore` | Allows ignoring certain properties. These are also respected for super classes. | ### Isar Id Each collection class has to define an id property with the type `Id` uniquely identifying an object. `Id` is just an alias for `int` that allows the Isar Generator to recognize the id property. Isar automatically indexes id fields, which allows you to get and modify objects based on their id efficiently. You can either set ids yourself or ask Isar to assign an auto-increment id. If the `id` field is `null` and not `final`, Isar will assign an auto-increment id. If you want a non-nullable auto-increment id, you can use `Isar.autoIncrement` instead of `null`. :::tip Auto increment ids are not reused when an object is deleted. The only way to reset auto-increment ids is to clear the database. ::: ### Renaming collections and fields By default, Isar uses the class name as the collection name. Similarly, Isar uses field names as column names in the database. If you want a collection or field to have a different name, add the `@Name` annotation. The following example demonstrates custom names for collection and fields: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` Especially if you want to rename Dart fields or classes that are already stored in the database, you should consider using the `@Name` annotation. Otherwise, the database will delete and re-create the field or collection. ### Ignoring fields Isar persists all public fields of a collection class. By annotating a property or getter with `@ignore`, you can exclude it from persistence, as shown in the following code snippet: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` In cases where a collection inherits fields from a parent collection, it's usually easier to use the `ignore` property of the `@Collection` annotation: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` If a collection contains a field with a type that is not supported by Isar, you have to ignore the field. :::warning Keep in mind that it is not good practice to store information in Isar objects that are not persisted. ::: ## Supported types Isar supports the following data types: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` Additionally, embedded objects and enums are supported. We'll cover those below. ## byte, short, float For many use cases, you don't need the full range of a 64-bit integer or double. Isar supports additional types that allow you to save space and memory when storing smaller numbers. | Type | Size in bytes | Range | | ---------- | ------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | The additional number types are just aliases for the native Dart types, so using `short`, for example, works the same as using `int`. Here is an example collection containing all of the above types: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` All number types can also be used in lists. For storing bytes, you should use `List`. ## Nullable types Understanding how nullability works in Isar is essential: Number types do **NOT** have a dedicated `null` representation. Instead, a specific value is used: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, and `List` have a separate `null` representation. This behavior enables performance improvements, and it allows you to change the nullability of your fields freely without requiring migration or special code to handle `null` values. :::warning The `byte` type does not support null values. ::: ## DateTime Isar does not store timezone information of your dates. Instead, it converts `DateTime`s to UTC before storing them. Isar returns all dates in local time. `DateTime`s are stored with microsecond precision. In browsers, only millisecond precision is supported because of JavaScript limitations. ## Enum Isar allows storing and using enums like other Isar types. You have to choose, however, how Isar should represent the enum on the disk. Isar supports four different strategies: | EnumType | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | `ordinal` | The index of the enum is stored as `byte`. This is very efficient but does not allow nullable enums | | `ordinal32` | The index of the enum is stored as `short` (4-byte integer). | | `name` | The enum name is stored as `String`. | | `value` | A custom property is used to retrieve the enum value. | :::warning `ordinal` and `ordinal32` depend on the order of the enum values. If you change the order, existing databases will return incorrect values. ::: Let's check out an example for each strategy. ```dart @collection class EnumCollection { Id? id; @enumerated // same as EnumType.ordinal late TestEnum byteIndex; // cannot be nullable @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // cannot be nullable @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` Of course, Enums can also be used in lists. ## Embedded objects It's often helpful to have nested objects in your collection model. There is no limit to how deep you can nest objects. Keep in mind, however, that updating a deeply nested object will require writing the whole object tree to the database. ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` Embedded objects can be nullable and extend other objects. The only requirement is that they are annotated with `@embedded` and have a default constructor without required parameters. ================================================ FILE: docs/docs/transactions.md ================================================ --- title: Transactions --- # Transactions In Isar, transactions combine multiple database operations in a single unit of work. Most interactions with Isar implicitly use transactions. Read & write access in Isar is [ACID](http://en.wikipedia.org/wiki/ACID) compliant. Transactions are automatically rolled back if an error occurs. ## Explicit transactions In an explicit transaction, you get a consistent snapshot of the database. Try to minimize the duration of transactions. It is forbidden to do network calls or other long-running operations in a transaction. Transactions (especially write transactions) do have a cost, and you should always try to group successive operations into a single transaction. Transactions can either be synchronous or asynchronous. In synchronous transactions, you may only use synchronous operations. In asynchronous transactions, only async operations. | | Read | Read & Write | | ------------ | ------------ | ----------------- | | Synchronous | `.txnSync()` | `.writeTxnSync()` | | Asynchronous | `.txn()` | `.writeTxn()` | ### Read transactions Explicit read transactions are optional, but they allow you to do atomic reads and rely on a consistent state of the database inside the transaction. Internally Isar always uses implicit read transactions for all read operations. :::tip Async read transactions run in parallel to other read and write transactions. Pretty cool, right? ::: ### Write transactions Unlike read operations, write operations in Isar must be wrapped in an explicit transaction. When a write transaction finishes successfully, it is automatically committed, and all changes are written to disk. If an error occurs, the transaction is aborted, and all the changes are rolled back. Transactions are “all or nothing”: either all the writes within a transaction succeed, or none of them take effect to guarantee data consistency. :::warning When a database operation fails, the transaction is aborted and must no longer be used. Even if you catch the error in Dart. ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: move loop inside transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/tutorials/quickstart.md ================================================ --- title: Quickstart --- # Quickstart Holy smokes, you're here! Let's get started on using the coolest Flutter database out there... We're going to be short on words and quick on code in this quickstart. ## 1. Add dependencies Before the fun begins, we need to add a few packages to the `pubspec.yaml`. We can use pub to do the heavy lifting for us. ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. Annotate classes Annotate your collection classes with `@collection` and choose an `Id` field. ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // you can also use id = null to auto increment String? name; int? age; } ``` Ids uniquely identify objects in a collection and allow you to find them again later. ## 3. Run code generator Execute the following command to start the `build_runner`: ``` dart run build_runner build ``` If you are using Flutter, use the following: ``` flutter pub run build_runner build ``` ## 4. Open Isar instance Open a new Isar instance and pass all of your collection schemas. Optionally you can specify an instance name and directory. ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. Write and read Once your instance is open, you can start using the collections. All basic CRUD operations are available via the `IsarCollection`. ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // insert & update }); final existingUser = await isar.users.get(newUser.id); // get await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // delete }); ``` ## Other resources Are you a visual learner? Check out these videos to get started with Isar:


================================================ FILE: docs/docs/ur/README.md ================================================ --- home: true title: ہوم heroImage: /isar.svg actions: - text: آئیے شروع کریں۔ link: /tutorials/quickstart.html type: primary features: - title: کے لیے بنایا گیاہے💙 Flutter details: کم سے کم سیٹ اپ، استعمال میں آسان، کوئی ترتیب نہیں، کوئی بوائلر پلیٹ نہیں۔ شروع کرنے کے لیے بس کوڈ کی چند لائنیں شامل کریں۔ - title: 🚀 انتہائی قابل توسیع details: ایک ہی نو ایس کیو ایل ڈیٹا بیس میں سیکڑوں ہزاروں ریکارڈز کو ذخیرہ کریں اور ان سے موثر اور متضاد طور پر استفسار کریں۔ - title: 🍭 خصوصیت سے بھرپور details: آپ کے ڈیٹا کو منظم کرنے میں آپ کی مدد کرنے کے لیے ای زار کے پاس خصوصیات کا ایک بھرپور مجموعہ ہے۔ کمپوزٹ اور ملٹی انٹری انڈیکس، استفسار میں ترمیم کرنے والے، جےسن سپورٹ، اور بہت کچھ۔ - title: 🔎 مکمل متن کی تلاش details: ای زار کے پاس بنی بنائں مکمل متن تلاشی ہے۔ ملٹی انٹری انڈیکس بنائیں اور آسانی سے ریکارڈ تلاش کریں۔ - title: 🧪ایسڈ سیمنٹکس details: ای زار تیزاب کے مطابق ہے اور لین دین کو خود بخود ہینڈل کرتا ہے۔ اگر کوئی خرابی پیش آتی ہے تو یہ تبدیلیوں کو واپس لے لیتا ہے۔ - title: 💃 جامد ٹائپنگ details: ای زار کے سوالات کو جامد طور پر ٹائپ کیا جاتا ہے اور مرتب وقت کی جانچ پڑتال کی جاتی ہے۔ رن ٹائم غلطیوں کے بارے میں فکر کرنے کی ضرورت نہیں ہے۔ - title: 📱 ملٹی پلیٹ فارم details: iOS, Android, Desktop, اور مکمل WEB SUPPORT! - title: ⏱ غیر مطابقت پذیر details: متوازی استفسار کے آپریشنز اور ملٹی آئسولیٹ سپورٹ آؤٹ آف دی باکس - title: 🦄 اوپن سورس details: سب کچھ اوپن سورس اور ہمیشہ کے لیے مفت ہے! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/ur/crud.md ================================================ --- title: بنائیں، پڑھیں، اپ ڈیٹ کریں، حذف کریں --- # بنائیں، پڑھیں، اپ ڈیٹ کریں، حذف کریں جب آپ نے اپنے کلیکشنز کی وضاحت کی ہے، تو سیکھیں کہ انہیں کیسے جوڑنا ہے! ## ای زار کھولنا اس سے پہلے کہ آپ کچھ کر سکیں، ہمیں ای زار کی مثال درکار ہے۔ ہر مثال کے لیے لکھنے کی اجازت کے ساتھ ڈائرکٹری کی ضرورت ہوتی ہے جہاں ڈیٹا بیس فائل کو محفوظ کیا جا سکتا ہے۔ اگر آپ ڈائرکٹری کی وضاحت نہیں کرتے ہیں، تو ای زار موجودہ پلیٹ فارم کے لیے ایک مناسب ڈیفالٹ ڈائریکٹری تلاش کرے گا۔ وہ تمام اسکیمے فراہم کریں جو آپ ای زار مثال کے ساتھ استعمال کرنا چاہتے ہیں۔ اگر آپ متعدد مثالوں کو کھولتے ہیں، تو آپ کو اب بھی ہر ایک مثال کے لیے ایک ہی اسکیما فراہم کرنا ہوں گی۔ ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` آپ پہلے سے طے شدہ تشکیل استعمال کرسکتے ہیں یا درج ذیل میں سے کچھ پیرامیٹرز فراہم کرسکتے ہیں۔ ترتیب | تفصیل | | -------| -------------| | نام | الگ الگ ناموں کے ساتھ متعدد مثالیں کھولیں۔بذریعہ ڈیفالٹ، `"ڈیفالٹ"` استعمال ہوتا ہے۔ | |ڈائریکٹری | اس مثال کے لیے اسٹوریج کا مقام۔ بطور ڈیفالٹ، آئی او ایس کے لیے `این ایس ڈاکومینٹ ڈائریکٹری` اور اینڈرائڈ کے لیے `گٹ ڈیٹا ڈائریکٹری` استعمال کیا جاتا ہے۔ ویب کے لیے ضروری نہیں ہے۔ | |آرام پائیدار | تحریری کارکردگی کو بڑھانے کے لیے پائیداری کی ضمانت کو آرام دیتا ہے۔ سسٹم کریش ہونے کی صورت میں (ایپ کریش نہیں)، آخری کمٹڈ ٹرانزیکشن سے محروم ہونا ممکن ہے۔ کرپشن ممکن نہیں | | کمپیکٹ اون لانچ | یہ چیک کرنے کی شرائط کہ آیا مثال کے کھولنے پر ڈیٹا بیس کو کمپیکٹ کیا جانا چاہیے۔ | | انسپکٹر | ڈیبگ بلڈز کے لیے انسپکٹر کو فعال کیا۔ پروفائل اور ریلیز کے لیے اس اختیار کو نظر انداز کر دیا گیا ہے۔ | اگر کوئی مثال پہلے سے کھلی ہوئی ہے تو، 'ای زار.کھولیں()' کو کال کرنے سے مخصوص پیرامیٹرز سے قطع نظر موجودہ مثال حاصل ہو جائے گی۔ یہ ای زار کو الگ تھلگ میں استعمال کرنے کے لیے مفید ہے۔ :::ٹپ تمام پلیٹ فارمز پر درست راستہ حاصل کرنے کے لیے [path_provider](https://pub.dev/packages/path_provider) پیکیج استعمال کرنے پر غور کریں۔ ::: `directory/name.isar` ڈیٹا بیس فائل کا سٹوریج لوکیشن ہے۔ ## ڈیٹا بیس سے پڑھنا ای زار میں دی گئی قسم کی نئی اشیاء تلاش کرنے، استفسار کرنے اور تخلیق کرنے کے لیے `ای زار کلیکشن` مثالیں استعمال کریں۔ ذیل میں دی گئی مثالوں کے لیے، ہم فرض کرتے ہیں کہ ہمارے پاس ایک مجموعہ ہے 'رےسیپ' کی وضاحت اس طرح کی گئی ہے: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### ایک مجموعہ حاصل کریں۔ آپ کے تمام مجموعے ایزار مثال میں رہتے ہیں۔ آپ ترکیبوں کا مجموعہ اس کے ساتھ حاصل کرسکتے ہیں: ```dart final recipes = isar.recipes; ``` یہ آسان تھا! اگر آپ کلیکشن ایکسیسرز استعمال نہیں کرنا چاہتے تو آپ `کلیکشن()` طریقہ بھی استعمال کر سکتے ہیں: ```dart final recipes = isar.collection(); ``` ### کوئی چیز حاصل کریں (بذریعہ ID) ہمارے پاس ابھی تک ڈیٹا جمع نہیں ہے لیکن آئیے دکھاوا کرتے ہیں کہ ہم ایسا کرتے ہیں تاکہ ہم 123 آئی ڈی کے ذریعے ایک خیالی چیز حاصل کر سکیں۔ ```dart final recipe = await isar.recipes.get(123); ``` `گیٹ()` کسی بھی چیز کے ساتھ `فیوچر` لوٹاتا ہے یا `نل` اگر یہ موجود نہیں ہے۔ ایزار کے تمام آپریشنز بطور ڈیفالٹ غیر مطابقت پذیر ہوتے ہیں، اور ان میں سے اکثر کا ہم وقتی ہم منصب ہوتا ہے: ```dart final recipe = isar.recipes.getSync(123); ``` :::warning آپ کو اپنے یوآئی الگ تھلگ میں طریقوں کے غیر مطابقت پذیر ورژن پر ڈیفالٹ کرنا چاہئے۔ چونکہ ایزار بہت تیز ہے، یہ اکثر مطابقت پذیر ورژن استعمال کرنے کے لئے قابل قبول ہے. ::: اگر آپ ایک ساتھ ایک سے زیادہ اشیاء حاصل کرنا چاہتے ہیں تو `گٹ آل ()` یا `گٹ آل سنک ()` استعمال کریں: ```dart final recipe = await isar.recipes.getAll([1, 2]); ``` ### اشیاء سے استفسار کریں۔ آی ڈی کے ذریعے آبجیکٹ حاصل کرنے کے بجائے آپ `.ویئر()` اور `.فیلٹر()` کا استعمال کرتے ہوئے مخصوص شرائط سے مماثل اشیاء کی فہرست سے بھی استفسار کر سکتے ہیں: ```dart final allRecipes = await isar.recipes.where().findAll(); final favouires = await isar.recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ مزید جانیں: [Queries](queries) ## ڈیٹا بیس میں ترمیم کرنا آخر کار ہمارے کلیکشن میں ترمیم کرنے کا وقت آگیا ہے! اوبجیکٹس بنانے، اپ ڈیٹ کرنے یا حذف کرنے کے لیے، تحریری لین دین میں لپیٹے ہوئے متعلقہ آپریشنز کا استعمال کریں: ```dart await isar.writeTxn(() async { final recipe = await isar.recipes.get(123) recipe.isFavorite = false; await isar.recipes.put(recipe); // perform update operations await isar.recipes.delete(123); // or delete operations }); ``` ➡️ مزید جانیں: [Transactions](transactions) ### آبجیکٹ داخل کریں۔ ایزار میں کسی چیز کو برقرار رکھنے کے لیے، اسے ایک کلیکشن میں داخل کریں۔ ایزار کا `پٹ()` طریقہ یا تو آبجیکٹ کو داخل یا اپ ڈیٹ کرے گا اس پر منحصر ہے کہ آیا یہ پہلے سے مجموعہ میں موجود ہے۔ اگر آئی ڈی فیلڈ `نل` یا `ایزار.آٹوانکریمنٹ` ہے تو ایزار ایک خودکار اضافہ آئی ڈی استعمال کرے گا۔ ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await isar.recipes.put(pancakes); }) ``` ایزار خود بخود آئی ڈی آبجیکٹ کو تفویض کر دے گا اگر `آئی ڈی` فیلڈ غیر حتمی ہے۔ ایک ساتھ متعدد اشیاء کو داخل کرنا اتنا ہی آسان ہے: ```dart await isar.writeTxn(() async { await isar.recipes.putAll([pancakes, pizza]); }) ``` ### آبجیکٹ کو اپ ڈیٹ کریں۔ دونوں کو بنانا اور اپ ڈیٹ کرنا `کلیکشن.پت(ااوبجکٹ)` کے ساتھ کام کرتا ہے۔ اگر آئی ڈی نل ہے (یا موجود نہیں ہے) تو آبجیکٹ داخل کیا جاتا ہے۔ دوسری صورت میں، یہ اپ ڈیٹ کیا جاتا ہے. لہذا اگر ہم اپنے پان کیکس کو ناپسند کرنا چاہتے ہیں، تو ہم درج ذیل کام کر سکتے ہیں: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await isar.recipes.put(recipe); }); ``` ### آبجیکٹ کو حذف کریں۔ ایزار میں کسی چیز سے چھٹکارا حاصل کرنا چاہتے ہیں؟ `کلیکشن.ڈیلیٹ(آئی ڈی)` استعمال کریں۔ حذف کرنے کا طریقہ واپس کرتا ہے کہ آیا مخصوص آئی ڈی کے ساتھ کوئی آبجیکٹ ملا اور حذف کر دیا گیا تھا۔ اگر آپ آئی ڈی `123` کے ساتھ آبجیکٹ کو حذف کرنا چاہتے ہیں، مثال کے طور پر، آپ یہ کر سکتے ہیں: ```dart await isar.writeTxn(() async { final success = await isar.recipes.delete(123); print('Recipe deleted: $success'); }); ``` اسی طرح حاصل کرنے اور ڈالنے کے لئے، ایک بلک ڈیلیٹ آپریشن بھی ہے جو حذف شدہ اشیاء کی تعداد لوٹاتا ہے: ```dart await isar.writeTxn(() async { final count = await isar.recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` اگر آپ ان اشیاء کی آئی ڈی نہیں جانتے جنہیں آپ حذف کرنا چاہتے ہیں، تو آپ ایک استفسار استعمال کر سکتے ہیں: ```dart await isar.writeTxn(() async { final count = await isar.recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/ur/faq.md ================================================ --- title: اکثر پوچھے گئے سوالات --- # اکثر پوچھے گئے سوالات ای زار اور فلٹر ڈیٹا بیس کے بارے میں اکثر پوچھے جانے والے سوالات کا بے ترتیب مجموعہ۔ ### مجھے ڈیٹا بیس کی ضرورت کیوں ہے؟ > میں اپنا ڈیٹا بیک اینڈ ڈیٹا بیس میں محفوظ کرتا ہوں، مجھے اسار کی ضرورت کیوں ہے؟ آج بھی، اگر آپ سب وے یا ہوائی جہاز میں ہیں یا اگر آپ اپنی دادی سے ملنے جاتے ہیں، جن کے پاس وائی فائی نہیں ہے اور سیل سگنل بہت خراب ہے۔ آپ کو خراب کنکشن کو اپنی ایپ کو معذور نہیں ہونے دینا چاہیے! ### Isar vs Hive The answer is easy: Isar was [started as a replacement for Hive](https://github.com/hivedb/hive/issues/246) and is now at a state where I recommend always using Isar over Hive. ### کہاں کی شقیں؟! > **_I_** کو یہ کیوں منتخب کرنا ہوگا کہ کون سا انڈیکس استعمال کرنا ہے؟ متعدد وجوہات ہیں۔ بہت سے ڈیٹا بیس دی گئی استفسار کے لیے بہترین انڈیکس کا انتخاب کرنے کے لیے ہیورسٹکس کا استعمال کرتے ہیں۔ ڈیٹا بیس کو اضافی استعمال کا ڈیٹا جمع کرنے کی ضرورت ہے (-> اوور ہیڈ) اور پھر بھی غلط انڈیکس کا انتخاب کر سکتا ہے۔ یہ استفسار کی تخلیق کو بھی سست بناتا ہے۔ آپ کے ڈیٹا کو آپ سے بہتر کوئی نہیں جانتا، ڈویلپر۔ لہذا آپ بہترین انڈیکس کا انتخاب کر سکتے ہیں اور مثال کے طور پر فیصلہ کر سکتے ہیں کہ آیا آپ استفسار یا چھانٹنے کے لیے انڈیکس استعمال کرنا چاہتے ہیں۔ ### کیا مجھے اشاریہ جات / جہاں شقیں استعمال کرنی ہیں؟ نھیں کیا! اگر آپ صرف فلٹرز پر بھروسہ کرتے ہیں تو اسر کافی تیز ہے۔ ### کیا اسر کا روزہ کافی ہے؟ Isar موبائل کے لیے تیز ترین ڈیٹا بیس میں سے ایک ہے، اس لیے اسے زیادہ تر استعمال کے معاملات کے لیے کافی تیز ہونا چاہیے۔ اگر آپ کارکردگی کے مسائل کا شکار ہیں، تو امکان یہ ہے کہ آپ کچھ غلط کر رہے ہیں۔ ### کیا اسر میری ایپ کا سائز بڑھاتا ہے؟ A little bit, yes. Isar will increase the download size of your app by about 1 - 1.5 MB. Isar Web adds only a few KB. ### دستاویزات غلط ہیں / ٹائپنگ کی غلطی ہے۔ Oh no, sorry. Please [open an issue](https://github.com/isar-community/isar/issues/new/choose) or, even better, a PR to fix it 💪. ================================================ FILE: docs/docs/ur/indexes.md ================================================ --- title: انڈیکسز --- # اشاریہ جات اشاریہ جات اسار کی سب سے طاقتور خصوصیت ہیں۔ بہت سے ایمبیڈڈ ڈیٹا بیس "نارمل" اشاریہ جات پیش کرتے ہیں (اگر بالکل بھی ہیں)، لیکن اسر کے پاس جامع اور ملٹی انٹری انڈیکس بھی ہوتے ہیں۔ یہ سمجھنا کہ اشاریہ جات کیسے کام کرتے ہیں استفسار کی کارکردگی کو بہتر بنانے کے لیے ضروری ہے۔ ای زار آپ کو یہ منتخب کرنے دیتا ہے کہ آپ کون سا انڈیکس استعمال کرنا چاہتے ہیں اور آپ اسے کیسے استعمال کرنا چاہتے ہیں۔ ہم ایک فوری تعارف کے ساتھ شروع کریں گے کہ اشاریہ جات کیا ہیں۔ ## اشاریہ جات کیا ہیں؟ جب کسی مجموعے کو انڈیکس نہیں کیا جاتا ہے تو، قطاروں کی ترتیب کو کسی بھی طرح سے آپٹمائز کیے گئے استفسار کے ذریعے واضح نہیں کیا جا سکتا ہے، اور اس لیے آپ کے استفسار کو اشیاء کے ذریعے لکیری طور پر تلاش کرنا پڑے گا۔ دوسرے لفظوں میں، استفسار کو حالات سے مماثل چیزوں کو تلاش کرنے کے لیے ہر شے کے ذریعے تلاش کرنا ہوگی۔ جیسا کہ آپ تصور کر سکتے ہیں، اس میں کچھ وقت لگ سکتا ہے۔ ہر ایک چیز کو تلاش کرنا زیادہ کارآمد نہیں ہے۔ For example, this `Product` collection is entirely unordered. ```dart @collection class Product { Id? id; late String name; late int price; } ``` #### ڈیٹا: | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | ایک سوال جو تمام پروڈکٹس کو تلاش کرنے کی کوشش کرتا ہے جن کی قیمت €30 سے ​​زیادہ ہے تمام نو قطاروں میں تلاش کرنا پڑتی ہے۔ یہ نو قطاروں کے لیے کوئی مسئلہ نہیں ہے، لیکن یہ 100k قطاروں کے لیے ایک مسئلہ بن سکتا ہے۔ ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` اس استفسار کی کارکردگی کو بہتر بنانے کے لیے، ہم `قیمت` پراپرٹی کو انڈیکس کرتے ہیں۔ ایک انڈیکس ایک ترتیب شدہ تلاش کی میز کی طرح ہے: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` #### تیار کردہ انڈیکس: | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | اب، استفسار بہت تیزی سے عمل میں لایا جا سکتا ہے۔ ایگزیکیوٹر براہ راست آخری تین انڈیکس قطاروں میں جا سکتا ہے اور متعلقہ اشیاء کو ان کی آئی ڈی سے تلاش کر سکتا ہے۔ ### چھانٹنا ایک اور عمدہ چیز: اشاریہ جات انتہائی تیز چھانٹ سکتے ہیں۔ ترتیب شدہ سوالات مہنگے ہوتے ہیں کیونکہ ڈیٹا بیس کو تمام نتائج کو ترتیب دینے سے پہلے میموری میں لوڈ کرنا ہوتا ہے۔ یہاں تک کہ اگر آپ آفسیٹ یا حد کی وضاحت کرتے ہیں، تو وہ چھانٹنے کے بعد لاگو ہوتے ہیں۔ آئیے تصور کریں کہ ہم چار سب سے سستی مصنوعات تلاش کرنا چاہتے ہیں۔ ہم درج ذیل استفسار استعمال کر سکتے ہیں: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` اس مثال میں، ڈیٹا بیس کو تمام (!) اشیاء کو لوڈ کرنا ہوگا، قیمت کے لحاظ سے ترتیب دینا ہوگا، اور چار مصنوعات کو سب سے کم قیمت کے ساتھ واپس کرنا ہوگا۔ جیسا کہ آپ شاید تصور کر سکتے ہیں، یہ پچھلے انڈیکس کے ساتھ بہت زیادہ مؤثر طریقے سے کیا جا سکتا ہے. ڈیٹا بیس انڈیکس کی پہلی چار قطاریں لیتا ہے اور متعلقہ اشیاء کو واپس کرتا ہے کیونکہ وہ پہلے سے ہی درست ترتیب میں ہیں۔ انڈیکس کو ترتیب دینے کے لیے استعمال کرنے کے لیے، ہم استفسار کو اس طرح لکھیں گے: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` The `.anyX()` where clause tells Isar to use an index just for sorting. You can also use a where clause like `.priceGreaterThan()` and get sorted results. ## منفرد اشاریہ جات ایک منفرد انڈیکس اس بات کو یقینی بناتا ہے کہ انڈیکس میں کوئی ڈپلیکیٹ قدر شامل نہیں ہے۔ یہ ایک یا متعدد خصوصیات پر مشتمل ہوسکتا ہے۔ اگر ایک منفرد انڈیکس میں ایک خاصیت ہے، تو اس خاصیت کی قدریں منفرد ہوں گی۔ اگر منفرد انڈیکس میں ایک سے زیادہ خاصیتیں ہیں، تو ان خصوصیات میں اقدار کا مجموعہ منفرد ہے۔ ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` منفرد انڈیکس میں ڈیٹا داخل کرنے یا اپ ڈیٹ کرنے کی کوئی بھی کوشش جو ڈپلیکیٹ کا سبب بنتی ہے اس کے نتیجے میں ایک خرابی ہوگی: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> ok final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // try to insert user with same username await isar.users.put(user2); // -> error: unique constraint violated print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## اشاریہ جات کو تبدیل کریں۔ اگر کسی انوکھی رکاوٹ کی خلاف ورزی کی جاتی ہے تو بعض اوقات غلطی کرنا بہتر نہیں ہوتا۔ اس کے بجائے، آپ موجودہ آبجیکٹ کو نئے سے تبدیل کرنا چاہیں گے۔ یہ انڈیکس کی 'ری پلیس' پراپرٹی کو 'سچ' پر سیٹ کر کے حاصل کیا جا سکتا ہے۔ ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` اب جب ہم موجودہ صارف نام کے ساتھ کسی صارف کو داخل کرنے کی کوشش کرتے ہیں، تو ای زار موجودہ صارف کو نئے صارف کے ساتھ بدل دے گا۔ ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` انڈیکس کو تبدیل کرنے سے `پٹ بائی()` طریقے بھی تیار ہوتے ہیں جو آپ کو اشیاء کو تبدیل کرنے کے بجائے اپ ڈیٹ کرنے کی اجازت دیتے ہیں۔ موجودہ آئی ڈی کو دوبارہ استعمال کیا جاتا ہے، اور لنکس اب بھی آباد ہیں۔ ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user does not exist so this is the same as put() await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` جیسا کہ آپ دیکھ سکتے ہیں، پہلے داخل کردہ صارف کی شناخت دوبارہ استعمال کی جاتی ہے۔ ## کیس غیر حساس اشاریہ جات All indexes on `String` and `List` properties are case-sensitive by default. If you want to create a case-insensitive index, you can use the `caseSensitive` option: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## انڈیکس کی قسم اشاریہ جات کی مختلف اقسام ہیں۔ زیادہ تر وقت، آپ ایک `IndexType.value` انڈیکس استعمال کرنا چاہیں گے، لیکن ہیش انڈیکس زیادہ موثر ہوتے ہیں۔ ### ویلیو انڈیکس ویلیو انڈیکس ڈیفالٹ قسم ہیں اور ان تمام پراپرٹیز کے لیے صرف ایک کی اجازت ہے جس میں سٹرنگز یا فہرستیں نہیں ہیں۔ انڈیکس بنانے کے لیے پراپرٹی ویلیوز کا استعمال کیا جاتا ہے۔ فہرستوں کے معاملے میں، فہرست کے عناصر استعمال کیے جاتے ہیں۔ یہ تینوں انڈیکس اقسام میں سب سے زیادہ لچکدار ہے لیکن جگہ استعمال کرنے والا بھی ہے۔ :::tip Use `IndexType.value` for primitives, Strings where you need `startsWith()` where clauses, and Lists if you want to search for individual elements. ::: ### ہیش انڈیکس انڈیکس کے لیے درکار اسٹوریج کو نمایاں طور پر کم کرنے کے لیے سٹرنگز اور لسٹوں کو ہیش کیا جا سکتا ہے۔ ہیش اشاریہ جات کا نقصان یہ ہے کہ انہیں سابقہ ​​اسکین کے لیے استعمال نہیں کیا جا سکتا (`startsWith` جہاں شقیں ہیں)۔ :::tip Use `IndexType.hash` for Strings and Lists if you don't need `startsWith`, and `elementEqualTo` where clauses. ::: ### HashElements انڈیکس String lists can be hashed as a whole (using `IndexType.hash`), or the elements of the list can be hashed separately (using `IndexType.hashElements`), effectively creating a multi-entry index with hashed elements. :::tip Use `IndexType.hashElements` for `List` where you need `elementEqualTo` where clauses. ::: ## جامع اشاریہ جات ایک جامع اشاریہ متعدد خصوصیات پر مشتمل ایک اشاریہ ہے۔ ای زار آپ کو تین خصوصیات تک کے جامع اشاریہ جات بنانے کی اجازت دیتا ہے۔ جامع اشاریہ جات کو متعدد کالم اشاریہ جات کے نام سے بھی جانا جاتا ہے۔ شاید ایک مثال کے ساتھ شروع کرنا بہتر ہے۔ ہم ایک شخص کا مجموعہ بناتے ہیں اور عمر اور نام کی خصوصیات پر ایک جامع انڈیکس کی وضاحت کرتے ہیں: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` #### Data: | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | #### تیار کردہ انڈیکس | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | تیار کردہ کمپوزٹ انڈیکس میں تمام افراد کو ان کی عمر کے لحاظ سے ان کے نام سے ترتیب دیا گیا ہے۔ جامع اشاریہ جات بہت اچھے ہیں اگر آپ ایک سے زیادہ خصوصیات کے لحاظ سے ترتیب دی گئی موثر سوالات تخلیق کرنا چاہتے ہیں۔ وہ متعدد خصوصیات کے ساتھ اعلی درجے کی جہاں شقوں کو بھی فعال کرتے ہیں: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` جامع انڈیکس کی آخری خاصیت بھی اس طرح کی شرائط کی حمایت کرتی ہے۔ `startsWith()` or `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## ملٹی انٹری انڈیکس If you index a list using `IndexType.value`, Isar خود بخود ملٹی انٹری انڈیکس بنائے گا، اور فہرست میں موجود ہر آئٹم کو آبجیکٹ کی طرف انڈیکس کیا جاتا ہے۔ یہ تمام اقسام کی فہرستوں کے لیے کام کرتا ہے۔ ملٹی انٹری انڈیکس کے لیے عملی ایپلی کیشنز میں ٹیگز کی فہرست کو انڈیکس کرنا یا مکمل ٹیکسٹ انڈیکس بنانا شامل ہے۔ ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` splits a string into words according to the [Unicode Annex #29](https://unicode.org/reports/tr29/) specification, so it works for almost all languages correctly. #### ڈیٹا: | id | description | descriptionWords | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | Entries with duplicate words only appear once in the index. #### تیار کردہ انڈیکس | descriptionWords | id | | ---------------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | یہ انڈیکس اب سابقہ ​​(یا مساوات) کے لیے استعمال کیا جا سکتا ہے جہاں وضاحت کے انفرادی الفاظ کی شقیں ہیں۔ :::tip Instead of storing the words directly, also consider using the result of a [phonectic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) like [Soundex](https://en.wikipedia.org/wiki/Soundex). ::: ================================================ FILE: docs/docs/ur/limitations.md ================================================ # حدود جیسا کہ آپ جانتے ہیں، ای زار ورچوئل مشین کے ساتھ ساتھ ویب پر چلنے والے موبائل آلات اور ڈیسک ٹاپس پر کام کرتا ہے۔ دونوں پلیٹ فارم بہت مختلف ہیں اور مختلف حدود ہیں۔ ## وی ایم حدود - کسی سٹرنگ کے صرف پہلے 1024 بائٹس کو ایک سابقہ ​​جہاں-شق کے لیے استعمال کیا جا سکتا ہے۔ - اشیاء صرف 16MB سائز کی ہو سکتی ہیں۔ ## ویب کی حدود چونکہ ای زار ویب انڈیکس دیٹا بیس پر انحصار کرتا ہے، اس لیے مزید حدود ہیں لیکن ای زار استعمال کرتے وقت وہ بمشکل ہی قابل توجہ ہیں۔ - Synchronous methods are unsupported - Currently, `Isar.splitWords()` and `.matches()` filters are not yet implemented - Schema changes are not as tighly checked as in the VM so be careful to comply with the rules - All number types are stored as double (the only js number type) so `@Size32` has no effect - Indexes are represented differenlty so hash indexes don't use less space (they still work the same) - `col.delete()` and `col.deleteAll()` work correctly but the return value is not correct - `col.clear()` do not reset the auto-increment value - `NaN` is not supported as a value ================================================ FILE: docs/docs/ur/links.md ================================================ --- title: لنکس --- # لنکس روابط آپ کو اشیاء کے درمیان تعلقات کا اظہار کرنے کی اجازت دیتے ہیں، جیسے کہ تبصرہ کا مصنف (صارف)۔ آپ ای زار لنکس کے ساتھ `1:1`، `1:n`، اور `n:n` تعلقات کو ماڈل بنا سکتے ہیں۔ لنکس کا استعمال ایمبیڈڈ اشیاء کے استعمال سے کم ایرگونومک ہے اور جب بھی ممکن ہو آپ کو ایمبیڈڈ اشیاء کا استعمال کرنا چاہیے۔ لنک کو ایک علیحدہ جدول کے طور پر سوچیں جس میں رشتہ موجود ہو۔ یہ ایس کیو ایل ریلیشنز کی طرح ہے لیکن اس میں ایک مختلف فیچر سیٹ اور اےپی آئی ہے۔ ## IsarLink `IsarLink` can contain no or one related object, and it can be used to express a to-one relationship. `IsarLink` has a single property called `value` which holds the linked object. Links are lazy, so you need to tell the `IsarLink` to load or save the `value` explicitly. You can do this by calling `linkProperty.load()` and `linkProperty.save()`. :::tip کسی لنک کے سورس اور ٹارگٹ کلیکشن کی آئی ڈی پراپرٹی غیر حتمی ہونی چاہیے۔ ::: غیر ویب اہداف کے لیے، جب آپ انہیں پہلی بار استعمال کرتے ہیں تو لنکس خود بخود لوڈ ہو جاتے ہیں۔ آئیے ایک مجموعہ میں ایک IsarLink شامل کرکے شروع کریں: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` ہم نے اساتذہ اور طلباء کے درمیان ایک ربط کی وضاحت کی۔ اس مثال میں ہر طالب علم کو بالکل ایک استاد ہو سکتا ہے۔ سب سے پہلے، ہم استاد بناتے ہیں اور اسے ایک طالب علم کو تفویض کرتے ہیں۔ ہمیں استاد کو `.پٹ()` کرنا ہوگا اور لنک کو دستی طور پر محفوظ کرنا ہوگا۔ ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teachers.save(); }); ``` We can now use the link: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` Let's try the same thing with synchronous code. We don't need to save the link manually because `.putSync()` automatically saves all links. It even creates the teacher for us. ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks یہ زیادہ معنی خیز ہوگا اگر پچھلی مثال کے طالب علم کے متعدد اساتذہ ہوسکتے ہیں۔ Fortunately, Isar has `IsarLinks`, which can contain multiple related objects and express a to-many relationship. `IsarLinks` extends `Set` and exposes all the methods that are allowed for sets. `IsarLinks` behaves much like `IsarLink` and is also lazy. To load all linked object call `linkProperty.load()`. To persist the changes, call `linkProperty.save()`. Internally both `IsarLink` and `IsarLinks` are represented in the same way. We can upgrade the `IsarLink` from before to an `IsarLinks` to assign multiple teachers to a single student (without losing data). ```dart @collection class Student { Id? id; late String name; final teacher = IsarLinks(); } ``` This works because we did not change the name of the link (`teacher`), so Isar remembers it from before. ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## بیک لنکس میں نے آپ کو یہ پوچھتے ہوئے سنا ہے، "اگر ہم معکوس تعلقات کا اظہار کرنا چاہتے ہیں تو کیا ہوگا؟"۔ فکر مت کرو؛ اب ہم بیک لنکس متعارف کرائیں گے۔ Backlinks are links in the reverse direction. Each link always has an implicit backlink. You can make it available to your app by annotating an `IsarLink` or `IsarLinks` with `@Backlink()`. بیک لنکس کو اضافی میموری یا وسائل کی ضرورت نہیں ہوتی ہے۔ آپ ڈیٹا کو کھونے کے بغیر انہیں آزادانہ طور پر شامل، ہٹا سکتے اور ان کا نام تبدیل کر سکتے ہیں۔ ہم یہ جاننا چاہتے ہیں کہ ایک مخصوص استاد کون سے طلباء کے پاس ہے، اس لیے ہم ایک بیک لنک کی وضاحت کرتے ہیں: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` ہمیں اس لنک کی وضاحت کرنے کی ضرورت ہے جس کی طرف بیک لنک اشارہ کرتا ہے۔ دو اشیاء کے درمیان متعدد مختلف روابط کا ہونا ممکن ہے۔ ## لنکس شروع کریں۔ `IsarLink` and `IsarLinks` have a zero-arg constructor, which should be used to assign the link property when the object is created. It is good practice to make link properties `final`. جب آپ پہلی بار اپنے آبجیکٹ کو `پٹ()` کرتے ہیں، تو لنک سورس اور ٹارگٹ کلیکشن کے ساتھ شروع ہو جاتا ہے، اور آپ `لوڈ()` اور `سیو()` جیسے طریقوں کو کال کر سکتے ہیں۔ ایک لنک اپنی تخلیق کے فوراً بعد تبدیلیوں کو ٹریک کرنا شروع کر دیتا ہے، لہذا آپ لنک شروع ہونے سے پہلے ہی تعلقات کو شامل اور ہٹا سکتے ہیں۔ :::danger کسی لنک کو کسی دوسری چیز میں منتقل کرنا غیر قانونی ہے۔ ::: ================================================ FILE: docs/docs/ur/queries.md ================================================ --- title: سوالات --- # سوالات استفسار یہ ہے کہ آپ کو ایسے ریکارڈز کیسے ملتے ہیں جو کچھ شرائط سے میل کھاتے ہیں، مثال کے طور پر: - تمام ستارے والے رابطے تلاش کریں۔ - رابطوں میں الگ الگ نام تلاش کریں۔ - ان تمام رابطوں کو حذف کریں جن کے آخری نام کی وضاحت نہیں کی گئی ہے۔ چونکہ سوالات ڈیٹا بیس پر کیے جاتے ہیں نہ کہ ڈارٹ میں، وہ واقعی تیز ہیں۔ جب آپ ہوشیاری سے اشاریہ جات کا استعمال کرتے ہیں، تو آپ استفسار کی کارکردگی کو مزید بہتر بنا سکتے ہیں۔ مندرجہ ذیل میں، آپ سیکھیں گے کہ سوالات کیسے لکھتے ہیں اور آپ انہیں جتنی جلدی ممکن ہو سکے کیسے بنا سکتے ہیں۔ آپ کے ریکارڈ کو فلٹر کرنے کے دو مختلف طریقے ہیں: فلٹرز اور جہاں شقیں۔ ہم ایک نظر ڈال کر شروع کریں گے کہ فلٹرز کیسے کام کرتے ہیں۔ ## فلٹرز فلٹرز استعمال کرنے اور سمجھنے میں آسان ہیں۔ آپ کی خصوصیات کی قسم پر منحصر ہے، مختلف فلٹر آپریشنز دستیاب ہیں جن میں سے اکثر کے خود وضاحتی نام ہیں۔ فلٹر فلٹر کیے جانے والے مجموعہ میں موجود ہر شے کے اظہار کا جائزہ لے کر کام کرتے ہیں۔ اگر اظہار 'سچ' پر حل کرتا ہے، تو اسر نتائج میں اعتراض کو شامل کرتا ہے۔ فلٹرز نتائج کی ترتیب کو متاثر نہیں کرتے ہیں۔ ہم ذیل کی مثالوں کے لیے درج ذیل ماڈل استعمال کریں گے۔ ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### استفسار کی شرائط فیلڈ کی قسم پر منحصر ہے، مختلف شرائط دستیاب ہیں۔ | Condition | Description | | ----------| ------------| | `.equalTo(value)` | Matches values that are equal to the specified `value`. | | `.between(lower, upper)` | Matches values that are between `lower` and `upper`. | | `.greaterThan(bound)` | Matches values that are greater than `bound`. | | `.lessThan(bound)` | Matches values that are less than `bound`. `null` values will be included by default because `null` is considered smaller than any other value. | | `.isNull()` | Matches values that are `null`.| | `.isNotNull()` | Matches values that are not `null`.| | `.length()` | List, String and link length queries filter objects based on the number of elements in a list or link. | Let's assume the database contains four shoes with sizes 39, 40, 46 and one with an un-set (`null`) size. Unless you perform sorting, the values will be returned sorted by id. ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### منطقی آپریٹرز آپ مندرجہ ذیل منطقی آپریٹرز کا استعمال کرتے ہوئے جامع پیشن گوئی کر سکتے ہیں: | Operator | Description | | ---------- | ----------- | | `.and()` | Evaluates to `true` if both left-hand and right-hand expressions evaluate to `true`. | | `.or()` | Evaluates to `true` if either expression evaluates to `true`. | | `.xor()` | Evaluates to `true` if exactly one expression evaluates to `true`. | | `.not()` | Negates the result of the following expression. | | `.group()` | Group conditions and allow to specify order of evaluation. | اگر آپ 46 سائز میں تمام جوتے تلاش کرنا چاہتے ہیں، تو آپ درج ذیل استفسار کا استعمال کر سکتے ہیں: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` If you want to use more than one condition, you can combine multiple filters using logical **and** `.and()`, logical **or** `.or()` and logical **xor** `.xor()`. ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // Optional. Filters are implicitly combined with logical and. .isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to: `size == 46 && isUnisex == true`. You can also group conditions using `.group()`: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` This query is equivalent to `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`. To negate a condition or group, use logical **not** `.not()`: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` This query is equivalent to `size != 46 && isUnisex != true`. ### سٹرنگ کے حالات مندرجہ بالا استفسار کی شرائط کے علاوہ، سٹرنگ اقدار کچھ اور شرائط پیش کرتی ہیں جنہیں آپ استعمال کر سکتے ہیں۔ مثال کے طور پر ریجیکس جیسے وائلڈ کارڈز تلاش میں مزید لچک پیدا کرتے ہیں۔ | Condition | Description | | -------------------- | ----------------------------------------------------------------- | | `.startsWith(value)` | Matches string values that begins with provided `value`. | | `.contains(value)` | Matches string values that contain the provided `value`. | | `.endsWith(value)` | Matches string values that end with the provided `value`. | | `.matches(wildcard)` | Matches string values that match the provided `wildcard` pattern. | **کیس کی حساسیت** All string operations have an optional `caseSensitive` parameter that defaults to `true`. **Wildcards:** A [wildcard string expression](https://en.wikipedia.org/wiki/Wildcard_character) is a string that uses normal characters with two special wildcard characters: - The `*` wildcard matches zero or more of any character - The `?` wildcard matches any character. For example, the wildcard string `"d?g"` matches `"dog"`, `"dig"`, and `"dug"`, but not `"ding"`, `"dg"`, or `"a dog"`. ### سوال میں ترمیم کرنے والے بعض اوقات کچھ شرائط یا مختلف اقدار کی بنیاد پر استفسار کرنا ضروری ہوتا ہے۔ aای زار مشروط سوالات کی تعمیر کے لئے ایک بہت طاقتور ٹول ہے: | Modifier | Description | | --------------------- | ---------------------------------------------------- | | `.optional(cond, qb)` | Extends the query only if the `condition` is `true`. This can be used almost anywhere in a query for example to conditionally sort or limit it. | | `.anyOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **or**. | | `.allOf(list, qb)` | Extends the query for each value in `values` and combines the conditions using logical **and**. | In this example, we build a method that can find shoes with an optional filter: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // only apply filter if sizeFilter != null (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` If you want to find all shoes that have one of multiple shoe sizes, you can either write a conventional query or use the `anyOf()` modifier: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` استفسار میں ترمیم کرنے والے خاص طور پر اس وقت مفید ہوتے ہیں جب آپ متحرک سوالات بنانا چاہتے ہیں۔ ### فہرستیں یہاں تک کہ فہرستوں سے بھی استفسار کیا جا سکتا ہے: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` آپ فہرست کی لمبائی کی بنیاد پر استفسار کر سکتے ہیں: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` These are equivalent to the Dart code `tweets.where((t) => t.hashtags.isEmpty);` and `tweets.where((t) => t.hashtags.length > 5);`. You can also query based on list elements: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` This is equivalent to the Dart code `tweets.where((t) => t.hashtags.contains('flutter'));`. ### ایمبیڈڈ اشیاء ایمبیڈڈ اشیاء اسر کی سب سے مفید خصوصیات میں سے ایک ہیں۔ اعلیٰ سطحی اشیاء کے لیے دستیاب انہی شرائط کا استعمال کرتے ہوئے ان سے بہت مؤثر طریقے سے استفسار کیا جا سکتا ہے۔ آئیے فرض کریں کہ ہمارے پاس مندرجہ ذیل ماڈل ہے: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` We want to query all cars that have a brand with the name `"BMW"` and the country `"Germany"`. We can do this using the following query: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` ہمیشہ نیسٹڈ سوالات کو گروپ کرنے کی کوشش کریں۔ مندرجہ بالا استفسار درج ذیل سے زیادہ موثر ہے۔ اگرچہ نتیجہ ایک ہی ہے: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### لنکس اگر آپ کے ماڈل میں [لنک یا بیک لنکس](لنک) ہیں تو آپ لنک شدہ اشیاء یا منسلک اشیاء کی تعداد کی بنیاد پر اپنے استفسار کو فلٹر کر سکتے ہیں۔ :::warning ذہن میں رکھیں کہ لنک کے سوالات مہنگے ہوسکتے ہیں کیونکہ اسر کو منسلک اشیاء کو تلاش کرنے کی ضرورت ہے۔ اس کے بجائے سرایت شدہ اشیاء استعمال کرنے پر غور کریں۔ ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` ہم ان تمام طلباء کو تلاش کرنا چاہتے ہیں جن کے پاس ریاضی یا انگریزی کے استاد ہیں: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` Link filters evaluate to `true` if at least one linked object matches the conditions. Let's search for all students that have no teachers: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` or alternatively: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## جہاں کلازز جہاں شقیں ایک بہت طاقتور ٹول ہیں، لیکن انہیں درست کرنا تھوڑا مشکل ہو سکتا ہے۔ فلٹرز کے برعکس جہاں شقیں استفسار کے حالات کو چیک کرنے کے لیے اسکیما میں بیان کردہ اشاریہ جات کا استعمال کرتی ہیں۔ انڈیکس سے استفسار کرنا ہر ریکارڈ کو انفرادی طور پر فلٹر کرنے سے کہیں زیادہ تیز ہے۔ ➡️ Learn more: [Indexes](indexes) :::tip ایک بنیادی اصول کے طور پر، آپ کو ہمیشہ جہاں تک ممکن ہو وہاں کی شقوں کا استعمال کرتے ہوئے ریکارڈ کو کم کرنے کی کوشش کرنی چاہیے اور باقی فلٹرنگ فلٹرز کا استعمال کرتے ہوئے کرنی چاہیے۔ ::: آپ صرف منطقی **یا** کا استعمال کرتے ہوئے شقوں کو جوڑ سکتے ہیں۔ دوسرے الفاظ میں، آپ متعدد جہاں شقوں کو ایک ساتھ جمع کر سکتے ہیں، لیکن آپ متعدد جہاں شقوں کے تقاطع سے استفسار نہیں کر سکتے۔ آئیے جوتوں کے مجموعہ میں اشاریہ جات شامل کریں: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` There are two indexes. The index on `size` allows us to use where clauses like `.sizeEqualTo()`. The composite index on `isUnisex` allows where clauses like `isUnisexSizeEqualTo()`. But also `isUnisexEqualTo()` because you can always use any prefix of an index. اب ہم کمپوزٹ انڈیکس کا استعمال کرتے ہوئے 46 سائز میں یونیسیکس جوتے تلاش کرنے سے پہلے کے سوال کو دوبارہ لکھ سکتے ہیں۔ یہ استفسار پچھلی کی نسبت بہت تیز ہوگا: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where clauses have two more superpowers: They give you "free" sorting and a super fast distinct operation. ### جہاں شقوں اور فلٹرز کو ملانا Remember the `shoes.filter()` queries? It's actually just a shortcut for `shoes.where().filter()`. You can (and should) combine where clauses and filters in the same query to use the benefits of both: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` جہاں شق پہلے لاگو کی جاتی ہے تاکہ فلٹر کیے جانے والے آبجیکٹ کی تعداد کو کم کیا جا سکے۔ پھر فلٹر بقیہ آبجیکٹس پر لاگو ہوتا ہے۔ ## چھانٹنا You can define how the results should be sorted when executing the query using the `.sortBy()`, `.sortByDesc()`, `.thenBy()` and `.thenByDesc()` methods. انڈیکس کا استعمال کیے بغیر تمام جوتوں کو ماڈل کے نام کے مطابق صعودی ترتیب اور نزولی ترتیب میں سائز تلاش کرنے کے لیے: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` بہت سے نتائج کو ترتیب دینا مہنگا ہو سکتا ہے، خاص طور پر چونکہ چھانٹنا آفسیٹ اور حد سے پہلے ہوتا ہے۔ اوپر چھانٹنے کے طریقے کبھی بھی اشاریہ جات کا استعمال نہیں کرتے ہیں۔ خوش قسمتی سے، ہم دوبارہ استعمال کر سکتے ہیں جہاں شق چھانٹنا ہے اور اپنی استفسار کو تیز رفتار بنا سکتے ہیں چاہے ہمیں ایک ملین اشیاء کو ترتیب دینے کی ضرورت ہو۔ ### جہاں شق کی چھانٹی اگر آپ اپنی استفسار میں ایک **سنگل** جہاں شق استعمال کرتے ہیں، تو نتائج پہلے ہی انڈیکس کے مطابق ترتیب دیئے گئے ہیں۔ یہ ایک بڑی بات ہے! Let's assume we have shoes in sizes `[43, 39, 48, 40, 42, 45]` and we want to find all shoes with a size greater than `42` and also have them sorted by size: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // also sorts the results by size .findAll(); // -> [43, 45, 48] ``` As you can see, the result is sorted by the `size` index. If you want to reverse the where clause sort order, you can set `sort` to `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` بعض اوقات آپ جہاں کی شق استعمال نہیں کرنا چاہتے لیکن پھر بھی مضمر چھانٹی سے فائدہ اٹھاتے ہیں۔ آپ 'کوئی' استعمال کر سکتے ہیں جہاں شق: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` اگر آپ ایک جامع اشاریہ استعمال کرتے ہیں، تو نتائج کو اشاریہ کے تمام شعبوں کے حساب سے ترتیب دیا جاتا ہے۔ :::tip If you need the results to be sorted, consider using an index for that purpose. Especially if you work with `offset()` and `limit()`. ::: بعض اوقات چھانٹنے کے لیے اشاریہ استعمال کرنا ممکن یا مفید نہیں ہوتا ہے۔ اس طرح کے معاملات کے لیے، آپ کو انڈیکس کا استعمال کرنا چاہیے تاکہ نتیجے میں آنے والے اندراجات کی تعداد کو جتنا ممکن ہو کم کیا جا سکے۔ ## منفرد اقدار منفرد قدروں کے ساتھ صرف اندراجات واپس کرنے کے لیے، الگ پیش گوئی کا استعمال کریں۔ مثال کے طور پر، یہ معلوم کرنے کے لیے کہ آپ کے ای زار ڈیٹا بیس میں آپ کے جوتوں کے کتنے مختلف ماڈل ہیں: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` آپ الگ الگ ماڈل سائز کے امتزاج کے ساتھ تمام جوتوں کو تلاش کرنے کے لیے متعدد مختلف شرائط کو بھی جوڑ سکتے ہیں: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` ہر ایک الگ امتزاج کا صرف پہلا نتیجہ واپس آتا ہے۔ آپ اسے کنٹرول کرنے کے لیے جہاں کی شقیں اور چھانٹنے کی کارروائیاں استعمال کر سکتے ہیں۔ ### جہاں شق الگ ہے۔ اگر آپ کے پاس غیر منفرد انڈیکس ہے، تو آپ اس کی تمام الگ الگ اقدار حاصل کرنا چاہتے ہیں۔ آپ پچھلے حصے سے `distinctBy` آپریشن استعمال کر سکتے ہیں، لیکن یہ چھانٹنے اور فلٹر کرنے کے بعد انجام دیا جاتا ہے، اس لیے کچھ اوور ہیڈ ہے۔ اگر آپ صرف ایک جہاں کی شق استعمال کرتے ہیں، تو آپ اس کے بجائے الگ آپریشن کرنے کے لیے انڈیکس پر انحصار کر سکتے ہیں۔ ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip نظریہ میں، آپ یہاں تک کہ ایک سے زیادہ استعمال کر سکتے ہیں جہاں چھانٹنے اور الگ کرنے کے لیے شقیں ہیں۔ پابندی صرف یہ ہے کہ جہاں شقیں اوور لیپنگ نہ ہوں اور وہی انڈیکس استعمال کریں۔ درست چھانٹنے کے لیے، انہیں بھی ترتیب کے لحاظ سے لاگو کرنے کی ضرورت ہے۔ اگر آپ اس پر بھروسہ کرتے ہیں تو بہت محتاط رہیں! ::: ## آفسیٹ اور حد سست فہرست کے نظارے کے لیے استفسار کے نتائج کی تعداد کو محدود کرنا اکثر اچھا خیال ہوتا ہے۔ آپ ایک `لیمٹ()` سیٹ کر کے ایسا کر سکتے ہیں: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` By setting an `offset()` you can also paginate the results of your query. ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` چونکہ ڈارٹ آبجیکٹ کو انسٹیٹیوٹ کرنا اکثر استفسار پر عمل کرنے کا سب سے مہنگا حصہ ہوتا ہے، اس لیے یہ ایک اچھا خیال ہے کہ آپ کو مطلوبہ اشیاء کو لوڈ کیا جائے۔ ## ایگزیکیوشن آرڈر ای زار سوالات کو ہمیشہ اسی ترتیب میں انجام دیتا ہے: 1. اشیاء تلاش کرنے کے لیے پرائمری یا سیکنڈری انڈیکس کو عبور کریں (جہاں شقیں لگائیں) 2. اشیاء کو فلٹر کریں۔ 3. نتائج ترتیب دیں۔ 4. الگ آپریشن کا اطلاق کریں۔ 5. آف سیٹ اور نتائج کو محدود کریں۔ 6. نتائج واپس کریں۔ ## استفسار کے آپریشنز In the previous examples, we used `.findAll()` to retrieve all matching objects. There are more operations available, however: | Operation | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `.findFirst()` | Retreive only the first matching object or `null` if none matches. | | `.findAll()` | Retreive all matching objects. | | `.count()` | Count how many objects match the query. | | `.deleteFirst()` | Delete the first matching object from the collection. | | `.deleteAll()` | Delete all matching objects from the collection. | | `.build()` | Compile the query to reuse it later. This saves the cost to build a query if you want to execute it multiple times. | ## پراپرٹی کے سوالات اگر آپ صرف ایک پراپرٹی کی قدروں میں دلچسپی رکھتے ہیں، تو آپ پراپرٹی استفسار استعمال کرسکتے ہیں۔ بس ایک باقاعدہ استفسار بنائیں اور ایک پراپرٹی منتخب کریں: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` صرف ایک پراپرٹی کا استعمال ڈی سیریلائزیشن کے دوران وقت بچاتا ہے۔ پراپرٹی کے سوالات ایمبیڈڈ اشیاء اور فہرستوں کے لیے بھی کام کرتے ہیں۔ ## جمع کرنا اسار پراپرٹی کے سوال کی قدروں کو جمع کرنے کی حمایت کرتا ہے۔ مندرجہ ذیل جمع آپریشن دستیاب ہیں: | Operation | Description | | ------------ | -------------------------------------------------------------- | | `.min()` | Finds the minimum value or `null` if none matches. | | `.max()` | Finds the maximum value or `null` if none matches. | | `.sum()` | Sums all values. | | `.average()` | Calculates the average of all values or `NaN` if none matches. | جمع کا استعمال تمام مماثل اشیاء کو تلاش کرنے اور دستی طور پر جمع کرنے سے کہیں زیادہ تیز ہے۔ ## متحرک سوالات :::danger یہ سیکشن غالباً آپ سے متعلق نہیں ہے۔ متحرک سوالات استعمال کرنے کی حوصلہ شکنی کی جاتی ہے جب تک کہ آپ کو بالکل ضرورت نہ ہو (اور آپ شاذ و نادر ہی کرتے ہیں)۔ ::: All the examples above used the QueryBuilder and the generated static extension methods. Maybe you want to create dynamic queries or a custom query language (like the Isar Inspector). In that case, you can use the `buildQuery()` method: | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------------- | | `whereClauses` | The where clauses of the query. | | `whereDistinct` | Whether where clauses should return distinct values (only useful for single where clauses). | | `whereSort` | The traverse order of the where clauses (only useful for single where clauses). | | `filter` | The filter to apply to the results. | | `sortBy` | A list of properties to sort by. | | `distinctBy` | A list of properties to distinct by. | | `offset` | The offset of the results. | | `limit` | The maximum number of results to return. | | `property` | If non-null, only the values of this property are returned. | آئیے ایک متحرک استفسار بنائیں: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` درج ذیل استفسار مساوی ہے: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/ur/recipes/data_migration.md ================================================ --- title: ڈیٹا مائیگریشن --- # ڈیٹا مائیگریشن اگر آپ مجموعے، فیلڈز، یا اشاریہ جات کو شامل یا ہٹاتے ہیں تو ایزار خود بخود آپ کے ڈیٹا بیس اسکیموں کو منتقل کر دیتا ہے۔ بعض اوقات آپ اپنے ڈیٹا کو بھی منتقل کرنا چاہتے ہیں۔ ایزار ایک بلٹ ان حل پیش نہیں کرتا ہے کیونکہ یہ من مانی نقل مکانی پر پابندیاں عائد کرے گا۔ ہجرت کی منطق کو لاگو کرنا آسان ہے جو آپ کی ضروریات کے مطابق ہو۔ ہم اس مثال میں پورے ڈیٹا بیس کے لیے ایک ہی ورژن استعمال کرنا چاہتے ہیں۔ ہم موجودہ ورژن کو ذخیرہ کرنے کے لیے مشترکہ ترجیحات کا استعمال کرتے ہیں اور اس کا موازنہ اس ورژن سے کرتے ہیں جس میں ہم منتقل ہونا چاہتے ہیں۔ اگر ورژن مماثل نہیں ہیں، تو ہم ڈیٹا کو منتقل کرتے ہیں اور ورژن کو اپ ڈیٹ کرتے ہیں۔ ::: warning آپ ہر مجموعہ کو اس کا اپنا ورژن بھی دے سکتے ہیں اور انہیں انفرادی طور پر منتقل کر سکتے ہیں۔ ::: تصور کریں کہ ہمارے پاس سالگرہ والے فیلڈ کے ساتھ صارف کا مجموعہ ہے۔ ہماری ایپ کے ورژن 2 میں، ہمیں عمر کی بنیاد پر صارفین سے استفسار کرنے کے لیے ایک اضافی پیدائشی سال کی فیلڈ کی ضرورت ہے۔ Version 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` Version 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` مسئلہ یہ ہے کہ موجودہ صارف کے ماڈلز میں خالی `پیدائشی سال` فیلڈ ہوگی کیونکہ یہ ورژن 1 میں موجود نہیں تھا۔ ہمیں `پیدائشی سال` فیلڈ سیٹ کرنے کے لئے ڈیٹا کو منتقل کرنے کی ضرورت ہے۔ ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // If the version is not set (new installation) or already 2, we do not need to migrate return; default: throw Exception('Unknown version: $currentVersion'); } // Update version await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // We paginate through the users to avoid loading all users into memory at once for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // We don't need to update anything since the birthYear getter is used await isar.users.putAll(users); }); } } ``` :::warning اگر آپ کو بہت سارے ڈیٹا کو منتقل کرنا ہے تو، یوآئی تھریڈ پر دباؤ کو روکنے کے لیے بیک گراؤنڈ آئسولیٹ استعمال کرنے پر غور کریں۔ ::: ================================================ FILE: docs/docs/ur/recipes/full_text_search.md ================================================ --- title: مکمل متن کی تلاش --- # مکمل متن کی تلاش مکمل متن کی تلاش ڈیٹا بیس میں متن تلاش کرنے کا ایک طاقتور طریقہ ہے۔ آپ کو پہلے سے ہی واقف ہونا چاہئے کہ [اشاریہ جات](/اشاریہ جات) کیسے کام کرتے ہیں، لیکن آئیے بنیادی باتوں کو دیکھتے ہیں۔ ایک انڈیکس تلاش کی میز کی طرح کام کرتا ہے، جس سے استفسار کے انجن کو دی گئی قدر کے ساتھ تیزی سے ریکارڈ تلاش کرنے کی اجازت ملتی ہے۔ مثال کے طور پر، اگر آپ کے آبجیکٹ میں ایک `ٹائٹل` فیلڈ ہے، تو آپ اس فیلڈ پر ایک انڈیکس بنا سکتے ہیں تاکہ دیے گئے عنوان کے ساتھ اشیاء کو تلاش کرنا تیز تر بنایا جا سکے۔ ## مکمل متن کی تلاش کیوں مفید ہے؟ You can easily search text using filters. There are various string operations for example `.startsWith()`, `.contains()` and `.matches()`. The problem with filters is that their runtime is `O(n)` where `n` is the number of records in the collection. String operations like `.matches()` are especially expensive. :::tip مکمل متن کی تلاش فلٹرز سے کہیں زیادہ تیز ہے، لیکن اشاریہ جات کی کچھ حدود ہیں۔ اس نسخہ میں، ہم ان حدود کے ارد گرد کام کرنے کا طریقہ دریافت کریں گے۔ ::: ## بنیادی مثال خیال ہمیشہ ایک جیسا ہوتا ہے: پورے متن کو ترتیب دینے کے بجائے، ہم متن میں الفاظ کو ترتیب دیتے ہیں تاکہ ہم انفرادی طور پر ان کو تلاش کر سکیں۔ آئیے سب سے بنیادی فل ٹیکسٹ انڈیکس بنائیں: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` We can now search for messages with specific words in the content: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` یہ استفسار بہت تیز ہے، لیکن کچھ مسائل ہیں: 1. ہم صرف پورے الفاظ تلاش کر سکتے ہیں۔ 2. ہم اوقاف پر غور نہیں کرتے 3. ہم دوسرے وائٹ اسپیس حروف کی حمایت نہیں کرتے ہیں۔ ##متن کو صحیح طریقے سے تقسیم کرنا آئیے پچھلی مثال کو بہتر بنانے کی کوشش کرتے ہیں۔ ہم الفاظ کی تقسیم کو ٹھیک کرنے کے لیے ایک پیچیدہ ریجیکس تیار کرنے کی کوشش کر سکتے ہیں، لیکن یہ ممکنہ طور پر کنارے کے معاملات کے لیے سست اور غلط ہوگا۔ The [Unicode Annex #29](https://unicode.org/reports/tr29/) defines how to split text into words correctly for almost all languages. It is quite complicated, but fortunately, Isar does the heavy lifting for us: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## میں مزید کنٹرول چاہتا ہوں۔ بالکل آسان! ہم اپنے انڈیکس کو بھی تبدیل کر سکتے ہیں تاکہ سابقہ ​​مماثلت اور کیس غیر حساس مماثلت کو سپورٹ کیا جا سکے۔ ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` By default, Isar will store the words as hashed values which is fast and space efficient. But hashes can't be used for prefix matching. Using `IndexType.value`, we can change the index to use the words directly instead. It gives us the `.titleWordsAnyStartsWith()` where clause: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## I also need `.endsWith()` Sure thing! We will use a trick to achieve `.endsWith()` matching: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` Don't forget reversing the ending you want to search for: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## Stemming algorithms Unfortunately, indexes do not support `.contains()` matching (this is true for other databases as well). But there are a few alternatives that are worth exploring. The choice highly depends on your use. One example is indexing word stems instead of the whole word. A stemming algorithm is a process of linguistic normalization in which the variant forms of a word are reduced to a common form: ``` connection connections connective ---> connect connected connecting ``` Popular algorithms are the [Porter stemming algorithm](https://tartarus.org/martin/PorterStemmer/) and the [Snowball stemming algorithms](https://snowballstem.org/algorithms/). There are also more advanced forms like [lemmatization](https://en.wikipedia.org/wiki/Lemmatisation). ## Phonetic algorithms A [phonetic algorithm](https://en.wikipedia.org/wiki/Phonetic_algorithm) is an algorithm for indexing words by their pronunciation. In other words, it allows you to find words that sound similar to the ones you are looking for. :::warning Most phonetic algorithms only support a single language. ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex) is a phonetic algorithm for indexing names by sound, as pronounced in English. The goal is for homophones to be encoded to the same representation so they can be matched despite minor differences in spelling. It is a straightforward algorithm, and there are multiple improved versions. Using this algorithm, both `"Robert"` and `"Rupert"` return the string `"R163"` while `"Rubin"` yields `"R150"`. `"Ashcraft"` and `"Ashcroft"` both yield `"A261"`. ### Double Metaphone The [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) phonetic encoding algorithm is the second generation of this algorithm. It makes several fundamental design improvements over the original Metaphone algorithm. ڈبل میٹافون سلاو، جرمن، سیلٹک، یونانی، فرانسیسی، اطالوی، ہسپانوی، چینی، اور دیگر ماخذ کی انگریزی میں مختلف بے ضابطگیوں کے لیے اکاؤنٹس ہیں۔ ================================================ FILE: docs/docs/ur/recipes/multi_isolate.md ================================================ --- title: کثیر الگ تھلگ استعمال --- # کثیر الگ تھلگ استعمال دھاگوں کے بجائے، تمام ڈارٹ کوڈ الگ تھلگ کے اندر چلتا ہے۔ ہر الگ تھلگ کی اپنی یادداشت کا ڈھیر ہوتا ہے، اس بات کو یقینی بناتا ہے کہ الگ تھلگ ریاست میں سے کوئی بھی کسی دوسرے الگ تھلگ سے قابل رسائی نہیں ہے۔ ایزار تک ایک ہی وقت میں متعدد الگ تھلگ مقامات سے رسائی حاصل کی جاسکتی ہے، اور یہاں تک کہ دیکھنے والے بھی الگ تھلگ جگہوں پر کام کرتے ہیں۔ اس ترکیب میں، ہم دیکھیں گے کہ ایک کثیر الگ تھلگ ماحول میں اسار کو کیسے استعمال کیا جائے۔ ## ایک سے زیادہ الگ تھلگ کب استعمال کریں۔ اسر لین دین متوازی طور پر انجام پاتے ہیں چاہے وہ ایک ہی الگ تھلگ میں چلیں۔ بعض صورتوں میں، متعدد الگ تھلگ مقامات سے اسار تک رسائی حاصل کرنا اب بھی فائدہ مند ہے۔ Tاس کی وجہ یہ ہے کہ اسر ڈارٹ آبجیکٹ سے اور ڈیٹا کو انکوڈنگ اور ڈی کوڈ کرنے میں کافی وقت صرف کرتا ہے۔ آپ اسے انکوڈنگ اور ڈی کوڈنگ جیسن (صرف زیادہ موثر) کے طور پر سوچ سکتے ہیں۔ یہ آپریشن آئسولیٹ کے اندر چلتے ہیں جہاں سے ڈیٹا تک رسائی حاصل کی جاتی ہے اور قدرتی طور پر الگ تھلگ میں دوسرے کوڈ کو بلاک کر دیتے ہیں۔ دوسرے الفاظ میں: اسر آپ کے ڈارٹ آئسولیٹ میں کچھ کام انجام دیتا ہے۔ اگر آپ کو صرف چند سو اشیاء کو ایک ساتھ پڑھنے یا لکھنے کی ضرورت ہے، تو اسے یوآئی الگ تھلگ میں کرنا کوئی مسئلہ نہیں ہے۔ لیکن بڑی لین دین کے لیے یا اگر یوآئی تھریڈ پہلے سے مصروف ہے، تو آپ کو الگ الگ الگ استعمال کرنے پر غور کرنا چاہیے۔ ## مثال The first thing we need to do is to open Isar in the new isolate. Since the instance of Isar is already open in the main isolate, `Isar.open()` will return the same instance. :::warning Make sure to provide the same schemas as in the main isolate. Otherwise, you will get an error. ::: `compute()` starts a new isolate in Flutter and runs the given function in it. ```dart void main() { // Open Isar in the UI isolate final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // listen to changes in the database isar.messages.watchLazy(() { print('omg the messages changed!'); }); // start a new isolate and create 10000 messages compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // after some time: // > omg the messages changed! // > isolate finished } // function that will be executed in the new isolate Future createDummyMessages(int count) async { // we don't need the path here because the instance is already open final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // we use a synchronous transactions in isolates isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` There are a few interesting things to note in the example above: - `isar.messages.watchLazy()` is called in the UI isolate and is notified of changes from another isolate. - Instances are referenced by name. The default name is `default`, but in this example, we set it to `myInstance`. - We used a synchronous transaction to create the mesasges. Blocking our new isolate is no problem, and synchronous transactions are a little faster. ================================================ FILE: docs/docs/ur/recipes/string_ids.md ================================================ --- title: اسٹرنگ آئی ڈیز --- # اسٹرنگ آئی ڈیز یہ مجھے ملنے والی اکثر درخواستوں میں سے ایک ہے، اس لیے یہاں اسٹرنگ آئی ڈیز کے استعمال سے متعلق ایک سبق ہے۔ ایزار مقامی طور پر اسٹرنگ آئی ڈیز کی حمایت نہیں کرتا ہے، اور اس کی ایک اچھی وجہ ہے: انٹیجر آئی ڈیز بہت زیادہ موثر اور تیز ہیں۔ خاص طور پر لنکس کے لیے، اسٹرنگ آئی ڈی کا اوور ہیڈ بہت اہم ہے۔ میں سمجھتا ہوں کہ بعض اوقات آپ کو بیرونی ڈیٹا ذخیرہ کرنا پڑتا ہے جو UUIDs یا دیگر غیر عددی آئی ڈیز استعمال کرتا ہے۔ میں اسٹرنگ آئی ڈی کو آپ کے آبجیکٹ میں بطور پراپرٹی اسٹور کرنے اور 64 بٹ انٹ بنانے کے لیے تیز رفتار ہیش نفاذ کا استعمال کرنے کی تجویز کرتا ہوں جسے بطور آئی ڈی استعمال کیا جا سکتا ہے۔ ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` اس نقطہ نظر کے ساتھ، آپ کو دونوں جہانوں میں سے بہترین حاصل ہوتا ہے: لنکس کے لیے موثر عددی ڈیز اور اسٹرنگ آئی ڈیز استعمال کرنے کی اہلیت۔ ## فاسٹ ہیش فنکشن مثالی طور پر، آپ کے ہیش فنکشن میں اعلیٰ معیار ہونا چاہیے (آپ کو تصادم نہیں چاہیے) اور تیز ہونا چاہیے۔ میں مندرجہ ذیل نفاذ کو استعمال کرنے کی سفارش کرتا ہوں: ```dart /// FNV-1a 64bit hash algorithm optimized for Dart Strings int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` اگر آپ ایک مختلف ہیش فنکشن کا انتخاب کرتے ہیں، تو یقینی بنائیں کہ یہ 64 بٹ انٹ واپس کرتا ہے اور کرپٹوگرافک ہیش فنکشن استعمال کرنے سے گریز کریں کیونکہ وہ بہت سست ہیں۔ :::warning Avoid using `string.hashCode` because it is not guaranteed to be stable across different platforms and versions of Dart. ::: ================================================ FILE: docs/docs/ur/schema.md ================================================ --- title: اسکیما --- # اسکیما جب آپ اپنی ایپ کا ڈیٹا ذخیرہ کرنے کے لیے ایزار کا استعمال کرتے ہیں، تو آپ مجموعوں کے ساتھ کام کر رہے ہوتے ہیں۔ ایک مجموعہ متعلقہ ایزار ڈیٹا بیس میں ڈیٹا بیس کی میز کی طرح ہے اور اس میں صرف ایک قسم کی ڈارٹ آبجیکٹ ہو سکتی ہے۔ ہر مجموعہ آبجیکٹ متعلقہ مجموعہ میں ڈیٹا کی ایک قطار کی نمائندگی کرتا ہے۔ مجموعہ کی تعریف کو "اسکیما" کہا جاتا ہے۔ ایزار جنریٹر آپ کے لیے بھاری بھرکم سامان اٹھائے گا اور زیادہ تر کوڈ تیار کرے گا جس کی آپ کو کلیکشن استعمال کرنے کی ضرورت ہے۔ ## مجموعہ کی اناٹومی۔ آپ `کلیکشن` یا `کلیکشن` کے ساتھ کلاس کی تشریح کر کے ہر اسر مجموعہ کی وضاحت کرتے ہیں۔ ایزار مجموعہ میں ڈیٹا بیس میں متعلقہ جدول میں ہر کالم کے لیے فیلڈز شامل ہوتے ہیں، جس میں بنیادی کلید شامل ہوتی ہے۔ درج ذیل کوڈ ایک سادہ مجموعہ کی ایک مثال ہے جو ایزار، پہلا نام اور آخری نام کے کالموں کے ساتھ ایک `صارف` ٹیبل کی وضاحت کرتا ہے: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip کسی فیلڈ کو برقرار رکھنے کے لیے، اسر کو اس تک رسائی حاصل ہونی چاہیے۔ آپ اس بات کو یقینی بنا سکتے ہیں کہ ایسر کو کسی فیلڈ تک رسائی حاصل ہے اسے عوامی بنا کر یا گیٹر اور سیٹٹر کے طریقے فراہم کر کے۔ ::: مجموعہ کو حسب ضرورت بنانے کے لیے چند اختیاری پیرامیٹرز ہیں: | Config | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `inheritance` | کنٹرول کریں کہ آیا پیرنٹ کلاسز اور مکسین کے فیلڈز کو اسر میں محفوظ کیا جائے گا۔ بطور ڈیفالٹ فعال۔ | | `accessor` | آپ کو ڈیفالٹ کلیکشن ایکسیسر کا نام تبدیل کرنے کی اجازت دیتا ہے (مثال کے طور پر `رابطہ` مجموعہ کے لئے `ایزار.رابطہ`)۔ | | `ignore` | کچھ خصوصیات کو نظر انداز کرنے کی اجازت دیتا ہے۔ یہ سپر کلاسز کے لیے بھی قابل احترام ہیں۔ | ### ای زار آئی ڈی ہر کلیکشن کلاس کو کسی شے کی منفرد شناخت کرنے والی قسم `آئی ڈی` کے ساتھ ایک آئی ڈی پراپرٹی کی وضاحت کرنی ہوتی ہے۔ `آئی ڈی` `انٹ` کا صرف ایک عرف ہے جو ای زار جنریٹر کو آئی ڈی کی خاصیت کو پہچاننے کی اجازت دیتا ہے۔ ای زار خود بخود آئی ڈی فیلڈز کو انڈیکس کرتا ہے، جو آپ کو ان کی شناخت کی بنیاد پر اشیاء کو مؤثر طریقے سے حاصل کرنے اور ان میں ترمیم کرنے کی اجازت دیتا ہے۔ آپ یا تو خود ids سیٹ کر سکتے ہیں یا ای زار سے ایک آٹو انکریمنٹ آئی ڈی تفویض کرنے کو کہہ سکتے ہیں۔ اگر `آئی ڈی` فیلڈ `نل` ہے اور `حتمی` نہیں ہے تو ای زار ایک خودکار اضافہ آئی ڈی تفویض کرے گا۔ اگر آپ غیر منسوخ آٹو انکریمنٹ آئی ڈی چاہتے ہیں تو آپ `نل` کی بجائے `ای زار.آٹوانکریمنٹ` استعمال کر سکتے ہیں۔ :::tip جب کسی چیز کو حذف کیا جاتا ہے تو آٹو انکریمنٹ آئی ڈیز دوبارہ استعمال نہیں کی جاتی ہیں۔ آٹو انکریمنٹ آئی ڈی کو دوبارہ ترتیب دینے کا واحد طریقہ ڈیٹا بیس کو صاف کرنا ہے۔ ::: ### مجموعوں اور فیلڈز کا نام تبدیل کرنا پہلے سے طے شدہ طور پر، ای زار کلاس کا نام مجموعہ کے نام کے طور پر استعمال کرتا ہے۔ اسی طرح، ای زار ڈیٹا بیس میں فیلڈ کے ناموں کو کالم کے نام کے طور پر استعمال کرتا ہے۔ اگر آپ چاہتے ہیں کہ کسی مجموعہ یا فیلڈ کا نام مختلف ہو، تو `@نام` تشریح شامل کریں۔ درج ذیل مثال جمع کرنے اور فیلڈز کے لیے حسب ضرورت ناموں کو ظاہر کرتی ہے: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` خاص طور پر اگر آپ ڈارٹ فیلڈز یا کلاسز کا نام تبدیل کرنا چاہتے ہیں جو پہلے سے ڈیٹا بیس میں محفوظ ہیں، آپ کو `@نام` تشریح استعمال کرنے پر غور کرنا چاہیے۔ بصورت دیگر، ڈیٹا بیس فیلڈ یا مجموعہ کو حذف کر کے دوبارہ تخلیق کر دے گا۔ ### Ignoring fields Isar persists all public fields of a collection class. By annotating a property or getter with `@ignore`, you can exclude it from persistence, as shown in the following code snippet: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` In cases where a collection inherits fields from a parent collection, it's usually easier to use the `ignore` property of the `@Collection` annotation: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` اگر کسی مجموعے میں ایک ایسی فیلڈ ہے جس کی قسم ایسر کے ذریعہ تعاون یافتہ نہیں ہے، تو آپ کو فیلڈ کو نظر انداز کرنا ہوگا۔ :::warning اس بات کو ذہن میں رکھیں کہ ایسیر اشیاء میں معلومات کو ذخیرہ کرنا اچھا عمل نہیں ہے جو برقرار نہیں ہیں۔ ::: ## تائید شدہ اقسام ای زار درج ذیل ڈیٹا کی اقسام کی حمایت کرتا ہے: - `bool` - `int` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` Additionally, embedded objects and enums are supported. We'll cover those below. ##بائٹ، مختصر، فلوٹ بہت سے استعمال کے معاملات کے لیے، آپ کو 64 بٹ انٹیجر یا ڈبل ​​کی پوری رینج کی ضرورت نہیں ہے۔ ای ار اضافی اقسام کی حمایت کرتا ہے جو آپ کو چھوٹے نمبروں کو ذخیرہ کرتے وقت جگہ اور میموری کو بچانے کی اجازت دیتا ہے۔ | Type | Size in bytes | Range | | ---------- |-------------- | ------------------------------------------------------- | | **byte** | 1 | 0 to 255 | | **short** | 4 | -2,147,483,647 to 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 to 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 to 3.4e38 | | **double** | 8 | -1.7e308 to 1.7e308 | The additional number types are just aliases for the native Dart types, so using `short`, for example, works the same as using `int`. Here is an example collection containing all of the above types: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` All number types can also be used in lists. For storing bytes, you should use `List`. ## کالعدم اقسام Understanding how nullability works in Isar is essential: Number types do **NOT** have a dedicated `null` representation. Instead, a specific value is used: | Type | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`, `String`, and `List` have a separate `null` representation. یہ رویہ کارکردگی کو بہتر بنانے کے قابل بناتا ہے، اور یہ آپ کو نل اقدار کو ہینڈل کرنے کے لیے منتقلی یا خصوصی کوڈ کی ضرورت کے بغیر اپنے فیلڈز کی منسوخی کو آزادانہ طور پر تبدیل کرنے کی اجازت دیتا ہے۔ :::warning The `byte` type does not support null values. ::: ## تاریخ وقت Isar does not store timezone information of your dates. Instead, it converts `DateTime`s to UTC before storing them. Isar returns all dates in local time. `DateTime`s are stored with microsecond precision. In browsers, only millisecond precision is supported because of JavaScript limitations. ## اینوم ایزار دیگر ایزار اقسام کی طرح اینومز کو ذخیرہ کرنے اور استعمال کرنے کی اجازت دیتا ہے۔ تاہم، آپ کو انتخاب کرنا ہوگا کہ اسر ڈسک پر موجود اینوم کی نمائندگی کیسے کرے۔ ایزار چار مختلف حکمت عملیوں کی حمایت کرتا ہے: | EnumType | Description | ----------- | ----------- | `ordinal` | The index of the enum is stored as `byte`. This is very efficient but does not allow nullable enums | | `ordinal32` | The index of the enum is stored as `short` (4-byte integer). | | `name` | The enum name is stored as `String`. | | `value` | A custom property is used to retrieve the enum value. | :::warning `ordinal` and `ordinal32` depend on the order of the enum values. If you change the order, existing databases will return incorrect values. ::: آئیے ہر حکمت عملی کے لیے ایک مثال دیکھیں۔ ```dart @collection class EnumCollection { Id? id; @enumerated // same as EnumType.ordinal late TestEnum byteIndex; // cannot be nullable @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // cannot be nullable @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` یقینا، اینمز کو فہرستوں میں بھی استعمال کیا جا سکتا ہے۔ ## ایمبیڈڈ اشیاء آپ کے کلیکشن ماڈل میں گھریلو اشیاء کا ہونا اکثر مددگار ہوتا ہے۔ اس کی کوئی حد نہیں ہے کہ آپ اشیاء کو کتنی گہرائی میں گھونسلا سکتے ہیں۔ تاہم، ذہن میں رکھیں کہ گہرے اندر کی چیز کو اپ ڈیٹ کرنے کے لیے پورے آبجیکٹ ٹری کو ڈیٹا بیس میں لکھنے کی ضرورت ہوگی۔ ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` ایمبیڈڈ اشیاء کالعدم ہوسکتی ہیں اور دیگر اشیاء کو بڑھا سکتی ہیں۔ صرف ضرورت یہ ہے کہ وہ `@ایمبڈیڈ` کے ساتھ تشریح شدہ ہوں اور بغیر مطلوبہ پیرامیٹرز کے ڈیفالٹ کنسٹرکٹر ہوں۔ ================================================ FILE: docs/docs/ur/transactions.md ================================================ --- title: لین دین --- # لین دین ای زار میں، لین دین کام کی ایک اکائی میں متعدد ڈیٹا بیس آپریشنز کو یکجا کرتا ہے۔ اسر کے ساتھ زیادہ تر تعاملات لین دین کا استعمال کرتے ہیں۔ اسار میں پڑھنے اور لکھنے کی رسائی [ACID](http://en.wikipedia.org/wiki/ACID) کے مطابق ہے۔ اگر کوئی غلطی ہوتی ہے تو ٹرانزیکشنز خود بخود واپس ہو جاتی ہیں۔ ## واضح لین دین ایک واضح لین دین میں، آپ کو ڈیٹا بیس کا ایک مستقل سنیپ شاٹ ملتا ہے۔ لین دین کی مدت کو کم سے کم کرنے کی کوشش کریں۔ لین دین میں نیٹ ورک کالز یا دیگر طویل عرصے سے چلنے والے آپریشنز کرنا منع ہے۔ لین دین (خاص طور پر لین دین لکھیں) کی ایک قیمت ہوتی ہے، اور آپ کو ہمیشہ ایک ہی لین دین میں یکے بعد دیگرے آپریشنز کو گروپ کرنے کی کوشش کرنی چاہیے۔ لین دین یا تو مطابقت پذیر یا غیر مطابقت پذیر ہوسکتے ہیں۔ ہم وقت ساز لین دین میں، آپ صرف مطابقت پذیر کارروائیوں کا استعمال کر سکتے ہیں۔ غیر مطابقت پذیر لین دین میں، صرف اسینک آپریشنز۔ | | Read | Read & Write | |--------------|--------------|--------------------| | Synchronous | `.txnSync()` | `.writeTxnSync()` | | Asynchronous | `.txn()` | `.writeTxn()` | ### لین دین پڑھیں واضح پڑھنے والے لین دین اختیاری ہیں، لیکن وہ آپ کو ایٹم ریڈز کرنے اور لین دین کے اندر موجود ڈیٹا بیس کی مستقل حالت پر انحصار کرنے کی اجازت دیتے ہیں۔ داخلی طور پر ای ذار تمام پڑھنے والے آپریشنز کے لیے ہمیشہ مضمر پڑھنے والے لین دین کا استعمال کرتا ہے۔ :::tip اےسنک پڑھنے والے لین دین دوسرے پڑھنے اور لکھنے والے لین دین کے متوازی چلتے ہیں۔ بہت اچھا، ٹھیک ہے؟ ::: ### لین دین لکھیں۔ پڑھنے کی کارروائیوں کے برعکس، اسار میں تحریری کارروائیوں کو ایک واضح لین دین میں لپیٹنا ضروری ہے۔ جب تحریری لین دین کامیابی کے ساتھ ختم ہوجاتا ہے، تو یہ خود بخود کمٹڈ ہوجاتا ہے، اور تمام تبدیلیاں ڈسک پر لکھی جاتی ہیں۔ اگر کوئی خرابی پیش آتی ہے تو، لین دین کو روک دیا جاتا ہے، اور تمام تبدیلیاں واپس کر دی جاتی ہیں۔ ٹرانزیکشنز "سب یا کچھ نہیں" ہیں: یا تو ٹرانزیکشن کے اندر تمام تحریریں کامیاب ہوتی ہیں، یا ان میں سے کوئی بھی ڈیٹا کی مستقل مزاجی کی ضمانت کے لیے اثر انداز نہیں ہوتا ہے۔ :::warning جب ڈیٹا بیس آپریشن ناکام ہوجاتا ہے، تو لین دین ختم ہوجاتا ہے اور اسے مزید استعمال نہیں کیا جانا چاہیے۔ یہاں تک کہ اگر آپ ڈارٹ میں غلطی کو پکڑتے ہیں۔ ::: ```dart @collection class Contact { Id? id; String? name; } // GOOD await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // BAD: move loop inside transaction for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/ur/tutorials/quickstart.md ================================================ --- title: فورا شروع کریں --- # فورا شروع کریں خوشی کی بات ہے،آپ یہاں ہیں! آئیے وہاں موجود بہترین فلٹر ڈیٹابیس کا استعمال شروع کریں۔ ہم اس فورا شروع کرتے ہیں میں الفاظ میں مختصر اور کوڈ پر تیز ہونے جا رہے ہیں۔ ## 1. انحصار شامل کریں۔ تفریح کا آغاز کرنےسے پہلےہمیں "پب سپیک۔یمل" میں چند پیکجز شامل کرنے کی ضرورت ہے۔ہم اپنے لیے بھاری سامان اٹھانے کے لیے پب کا استعمال کر سکتے ہیں۔ ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. کلاسوں کی تشریح کریں۔ اپنی کلیکشن کلاسز کو "کلیکشن@" کے ساتھ تشریح کریں اورایک "آئی ڈی@" فیلڈ کا انتخاب کریں۔ ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // you can also use id = null to auto increment String? name; int? age; } ``` آئی ڈیز مجموعہ میں اشیاء کی منفرد شناخت کرتی ہیں اور آپ کو بعد میں انہیں دوبارہ تلاش کرنے کی اجازت دیتی ہیں۔ ## 3. کوڈ جنریٹر چلائیں۔ شروع کرنے کے لیے درج ذیل کمانڈ پر عمل کریں "بیلڈ_رنر"؛ ``` dart run build_runner build ``` اگر آپ فلٹر استعمال کر رہے ہیں تو درج ذیل استعمال کریں؛ ``` flutter pub run build_runner build ``` ## 4. ای زار مثال کھولیں۔ ایک نیا ای زار مثال کھولیں اور اپنے تمام کلیکشن اسکیموں کو پاس کریں۔ اختیاری طور پر آپ مثال کا نام اور ڈائریکٹری بتا سکتے ہیں۔ ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. لکھیں اور پڑھیں ایک بار آپ کا مثال کھلنے کے بعد، آپ مجموعے کا استعمال شروع کر سکتے ہیں۔ تمام بنیادی کرڈ آپریشنز "ای زار کلیکشن" کے ذریعے دستیاب ہیں۔ ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); داخل کریں اور تروتازہ کریں۔// }); final existingUser = await isar.users.get(newUser.id); حاصل کریں۔// await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); حذف کریں// }); ``` ## دیگر وسائل کیا آپ بصری سیکھنے والے ہیں؟ ای زار کے ساتھ شروع کرنے کے لیے یہ ویڈیوز دیکھیں:


================================================ FILE: docs/docs/ur/watchers.md ================================================ --- title: نگران --- # نگران ای زار آپ کو ڈیٹا بیس میں ہونے والی تبدیلیوں کو سبسکرائب کرنے کی اجازت دیتا ہے۔ آپ کسی مخصوص شے، پورے مجموعہ، یا کسی سوال میں تبدیلیوں کے لیے "دیکھ" سکتے ہیں۔ نگہبان آپ کو ڈیٹا بیس میں ہونے والی تبدیلیوں پر موثر انداز میں رد عمل ظاہر کرنے کے قابل بناتے ہیں۔ مثال کے طور پر جب کوئی رابطہ شامل کیا جاتا ہے تو آپ اپنا یوآئی دوبارہ بنا سکتے ہیں، جب کوئی دستاویز اپ ڈیٹ ہو جائے تو نیٹ ورک کی درخواست بھیج سکتے ہیں، وغیرہ۔ لین دین کے کامیابی سے انجام پانے اور ہدف میں تبدیلی کے بعد دیکھنے والے کو مطلع کیا جاتا ہے۔ ##آبجیکٹ دیکھنا اگر آپ چاہتے ہیں کہ کسی مخصوص چیز کے بننے، اپ ڈیٹ یا حذف ہونے پر آپ کو مطلع کیا جائے، تو آپ کو کسی چیز کو دیکھنا چاہیے: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` جیسا کہ آپ اوپر کی مثال میں دیکھ سکتے ہیں، آبجیکٹ کو ابھی موجود ہونے کی ضرورت نہیں ہے۔ دیکھنے والے کو اس کے بننے پر مطلع کیا جائے گا۔ There is an additional parameter `fireImmediately`. If you set it to `true`, Isar will immediately add the object's current value to the stream. ### سست دیکھ رہا ہے۔ ہوسکتا ہے کہ آپ کو نئی قیمت وصول کرنے کی ضرورت نہ ہو لیکن صرف تبدیلی کے بارے میں مطلع کیا جائے۔ یہ ای زار کو آبجیکٹ لانے سے بچاتا ہے: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## کلیکشن دیکھ رہے ہیں۔ کسی ایک شے کو دیکھنے کے بجائے، آپ ایک پورا مجموعہ دیکھ سکتے ہیں اور کسی بھی چیز کو شامل، اپ ڈیٹ یا حذف کیے جانے پر مطلع کر سکتے ہیں: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## سوالات دیکھ رہے ہیں۔ پورے سوالات کو دیکھنا بھی ممکن ہے۔ ای زار صرف آپ کو مطلع کرنے کی پوری کوشش کرتا ہے جب سوال کے نتائج حقیقت میں تبدیل ہوں۔ آپ کو مطلع نہیں کیا جائے گا اگر لنکس استفسار کو تبدیل کرنے کا سبب بنتے ہیں۔ اگر آپ کو لنک کی تبدیلیوں کے بارے میں مطلع کرنے کی ضرورت ہو تو کلیکشن واچر کا استعمال کریں۔ ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning اگر آپ آفسیٹ اور لمیٹ یا الگ الگ سوالات استعمال کرتے ہیں، تو آئسر آپ کو اس وقت بھی مطلع کرے گا جب اشیاء فلٹر سے مماثل ہوں لیکن استفسار سے باہر، نتائج تبدیل ہوتے ہیں۔ ::: Just like `watchObject()`, you can use `watchLazy()` to get notified when the query results change but not fetch the results. :::danger ہر تبدیلی کے لیے استفسارات کو دوبارہ چلانا بہت ناکارہ ہے۔ اس کے بجائے اگر آپ سست کلیکشن واچر کا استعمال کریں تو بہتر ہوگا۔ ::: ================================================ FILE: docs/docs/watchers.md ================================================ --- title: Watchers --- # Watchers Isar allows you to subscribe to changes in the database. You can "watch" for changes in a specific object, an entire collection, or a query. Watchers enable you to react to changes in the database efficiently. You can for example rebuild your UI when a contact is added, send a network request when a document is updated, etc. A watcher is notified after a transaction commits successfully and the target actually changes. ## Watching Objects If you want to be notified when a specific object is created, updated or deleted, you should watch an object: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // prints: User changed: Mark await isar.users.delete(5); // prints: User changed: null ``` As you can see in the example above, the object does not need to exist yet. The watcher will be notified when it is created. There is an additional parameter `fireImmediately`. If you set it to `true`, Isar will immediately add the object's current value to the stream. ### Lazy watching Maybe you don't need to receive the new value but only be notified about the change. That saves Isar from having to fetch the object: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // prints: User 5 changed ``` ## Watching Collections Instead of watching a single object, you can watch an entire collection and get notified when any object is added, updated, or deleted: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // prints: A User changed ``` ## Watching Queries It is even possible to watch entire queries. Isar does its best to only notify you when the query results actually change. You will not be notified if links cause the query to change. Use a collection watcher if you need to be notified about link changes. ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // prints: Users with A are: [] await isar.users.put(User()..name = 'Albert'); // prints: Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // no print await isar.users.put(User()..name = 'Antonia'); // prints: Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning If you use offset & limit or distinct queries, Isar will also notify you when objects match the filter but outside the query, results change. ::: Just like `watchObject()`, you can use `watchLazy()` to get notified when the query results change but not fetch the results. :::danger Rerunning queries for every change is very inefficient. It would be best if you used a lazy collection watcher instead. ::: ================================================ FILE: docs/docs/zh/README.md ================================================ --- home: true title: 主页 heroImage: /isar.svg actions: - text: 让我们开始吧 link: /zh/tutorials/quickstart.html type: primary features: - title: 💙 专门为 Flutter 打造 details: 简化设置,易于使用,几行代码即可开始使用,无样板代码。 - title: 🚀 高可扩展性 details: 单个 NoSQL 数据库实例即能支持存入数十万的数据并能保证高速的异步查询。 - title: 🍭 多功能性 details: Isar 集成了很多现有功能来帮助你管理数据:包括但不限于复合索引和多条目索引、查询修改器、支持 JSON 等。 - title: 🔎 全文检索 details: Isar 内部支持全文检索。创建一个多条目索引然后进行查询将变得十分简单。 - title: 🧪 ACID 语义 details: Isar 兼容 ACID 语义并能自动处理事务。倘若遇到错误会自动回滚。 - title: 💃 类型静态 details: Isar 的查询都是静态的,即在编译时就已确定变量。完全无需担心运行时错误。 - title: 📱 支持多平台 details: iOS、 Android、桌面端以及 Web 端! - title: ⏱ 异步多线程 details: 并行查询 & 多线程支持开箱即用 - title: 🦄 开源 details: 所有一切都是开源并永久免费! footer: Apache Licensed | Copyright © 2022 Simon Leier --- ================================================ FILE: docs/docs/zh/crud.md ================================================ --- title: 增删改查 --- # 增删改查(CRUD) 当你已经定义了 Collection,现在来学习如何对其操作。 ## 创建一个 Isar 实例 首先我们必须创建一个 Isar 实例。每一个实例需要一个可写的路径来保存数据库文件。倘若你未指定路径,Isar 会根据当前设备所属平台来自动选择合适的默认路径。 将你想要使用的所有 Collection 的 Schema 作为参数传入到创建实例的方法中。如果你有多个实例,你仍然需要给每个实例配置相同的 Schema(即各个实例的 Schema 必须一致)。 ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [RecipeSchema], directory: dir.path, ); ``` 你可以使用默认配置,也可以根据下表修改参数: | 参数配置 | 描述 | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `name` | 以不同名称创建多个实例。默认情况下,`"default"` 会被用作实例名称。 | | `directory` | 该实例数据库文件的存储路径。默认情况下,iOS 是 `NSDocumentDirectory`,而 Android 则用 `getDataDirectory` 返回的结果,Web 端可选。 | | `relaxedDurability` | 放宽可靠性来提高写入性能。倘若应用遇到系统崩溃(不是 App 的崩溃),允许丢弃最后一次提交的事务操作结果。数据库文件损毁是不可能的。 | | `compactOnLaunch` | 是否以数据库压缩的形式来启用实例。 | | `inspector` | 在开发调试阶段启用检查器 Inspector。 对于 profile 和 release 版本,该参数会被忽略。 | 倘若一个实例已经被创建,调用 `Isar.open()` 会无视传入的参数,直接返回该实例。这使得在单一 isolate 内使用 Isar 会很有用。 :::tip 考虑使用 [path_provider](https://pub.dev/packages/path_provider) 来获取所有平台的有效路径。 ::: 数据库文件的路径在 `directory/name.isar`。 ## 从数据库中读取数据 对于给定类型,通过调用 `IsarCollection` 来查找、查询以及创建新的对象。 在下方的例子中,我们假定有一个 `Recipe` Collection,其定义如下: ```dart @collection class Recipe { Id? id; String? name; DateTime? lastCooked; bool? isFavorite; } ``` ### 获取 Collection 你声明的所有 Collection 都存在于 Isar 实例中(只要它们的 Schema 在创建实例的时候被传入了)。你可以通过下面代码来读取菜单数据: ```dart final recipes = isar.recipes; ``` 就这么简单!如果你不想用 Collection 的访问名(这里即 recipes),也可以调用 `collection()` 方法: ```dart final recipes = isar.collection(); ``` ### 通过 Id 来获取数据对象 我们的 Collection 中还没有数据。但是假设已有数据,我们可以通过以下代码来访问 Id 为 `123` 的菜单。 ```dart final recipe = await isar.recipes.get(123); ``` `get()` 返回一个包含对象的 `Future`,如果对象不存在,则返回 `null`。 默认情况下 Isar 所有的操作均为异步,而大部分操作也有其对应的同步处理方法,如: ```dart final recipe = isar.recipes.getSync(123); ``` :::tip 因为 Isar 已经足够快了,所以你应该在 UI isolate 中尽可能使用默认的异步方法。当然使用对应的同步方法也是可接受的。 ::: 如果你想要同时获取多个对象数据, 使用 `getAll()` 或 `getAllSync()`: ```dart final recipe = await isar.recipes.getAll([1, 2]); ``` ### 查询对象 除了通过 Id 来获取对象数据,你也可以通过 `.where()` 和 `.filter()` 来查询匹配指定条件的多个对象,其返回的是数组 List: ```dart final allRecipes = await isar.recipes.where().findAll(); final favouires = await isar.recipes.filter() .isFavoriteEqualTo(true) .findAll(); ``` ➡️ 学习更多:[查询](queries) ## 修改数据库 终于到了修改数据的时候了! 在一个写入事务(Write Transaction)中使用对应的操作序列来创建、修改和删除对象: ```dart await isar.writeTxn(() async { final recipe = await isar.recipes.get(123) recipe.isFavorite = false; await isar.recipes.put(recipe); // 修改数据 await isar.recipes.delete(123); // 或者删除数据 }); ``` ➡️ 学习更多:[事务](transactions) ### 插入对象 通过插入对象到 Collection,即可保存其数据到 Isar 数据库中。 Isar 的`put()` 方法会创建或者覆盖对象数据,取决于该对象是否已经存在于数据库里。 如果一个字段是 `null` 或 `Isar.autoIncrement`,Isar 则会分配一个自增 Id 来表示。 ```dart final pancakes = Recipe() ..name = 'Pancakes' ..lastCooked = DateTime.now() ..isFavorite = true; await isar.writeTxn(() async { await isar.recipes.put(pancakes); }) ``` 如果 `id` 不为 final, 那么 Isar 会自动将这个 Id 分配给该对象。 同时插入多个对象也很简单: ```dart await isar.writeTxn(() async { await isar.recipes.putAll([pancakes, pizza]); }) ``` ### 修改对象 `collection.put(object)` 方法兼有创建和修改的功能。如果一个对象的 Id 是 `null` (或者不存在),它就会被创建;否则,它就会被修改。 所以如果我们想要取消喜欢煎饼的话,可以做以下操作: ```dart await isar.writeTxn(() async { pancakes.isFavorite = false; await isar.recipes.put(recipe); }); ``` ### 删除对象 想要从 Isar 数据库中删除一个对象?用 `collection.delete(id)` 方法。这个方法会返回指定对象是否被删除(即返回布尔值)。如果你想通过 Id 来删除指定菜单,比如其 Id 为 `123`,你可以用下方代码: ```dart await isar.writeTxn(() async { final success = await isar.recipes.delete(123); print('Recipe deleted: $success'); }); ``` 相似地,也有对应的批量删除方法,其返回结果是被删除对象的数量: ```dart await isar.writeTxn(() async { final count = await isar.recipes.deleteAll([1, 2, 3]); print('We deleted $count recipes'); }); ``` 如果你不知道你想删除对象的 Id,你可以先通过指定条件来查询: ```dart await isar.writeTxn(() async { final count = await isar.recipes.filter() .isFavoriteEqualTo(false) .deleteAll(); print('We deleted $count recipes'); }); ``` ================================================ FILE: docs/docs/zh/faq.md ================================================ --- title: 常见疑问 --- # 常见疑问 关于 Isar 和 Flutter 数据库的常见问题集合。 ### 为什么我需要一个数据库? > 我把数据存在后台服务器的数据库,为什么还需要 Isar? 即便在今天,你可能也会遇到没有网络连接的时候,比如在地铁里、飞机上或者拜访老一辈的时候(你懂的)。网络不好也就意味着无法连接到后台数据库,所以你应该用离线数据库来增强你 App 的使用体验! ### Isar 对比 Hive 答案很简单:Isar [就是为了代替 Hive 而生的](https://github.com/hivedb/hive/issues/246),现阶段我都会毫不犹豫地推荐 Isar 而不是 Hive。 ### Where 子句?! > 为什么 **_我_** 必须选择使用何种索引? 有多方面的原因。许多数据库会针对给定查询启发式选择最佳索引。数据库需要收集额外的使用信息(-> 额外的性能开销),而且可能仍然会选到错误的索引,从而导致查询变得更慢。 没有人比你,作为开发者,更了解你的数据。所以你大可根据实际情况选择最优索引,甚至可以决定是否需要用索引来做查询或排序。 ### 我是否必须用索引或 where 子句? 不必!如果你只用 Filter,Isar 也已经足够快了。 ### Isar 真的足够快? 在针对移动端的数据库中,Isar 在性能方面名列前茅,所以对于大多数应用场景,它本身已经足够快了。如果你遇到了性能问题,很大可能是哪里做得不对。 ### Isar 会增加我 App 的打包大小吗? 是的,但是很小。 Isar 会给你的 App 增加 1 - 1.5 MB 的下载大小,Web 端则仅增加几 KB。 ### 文档有疏漏或错误。 好吧,对不起。 请[在此开一个 Issue](https://github.com/isar-community/isar/issues/new/choose) 或者更好的话,提交一个 PR 来帮助修复 💪。 ================================================ FILE: docs/docs/zh/indexes.md ================================================ --- title: 索引 --- # 索引(Index) 索引是 Isar 最重要的功能。所有嵌入式数据库都提供了“普通”索引功能(如果有的话),但是 Isar 支持组合搜索引和多条目索引。理解索引的工作原理是优化查询性能的基本前提。你可以选择使用哪种索引以及如何使用它们。我们先从索引的简介开始。 ## 什么是索引? 当一个 Collection 未被索引时,数据行的顺序很大可能无法被查询所识别,也就无从优化查询性能。因此查询不得不线性地搜索所有对象。也就是说,必须对每个对象进行查询,看它是否符合查询条件。你可以想象,这会耗费不少时间。对每个对象进行查询不是很高效。 举例来说,这个 `Product` Collection 是完全无序的。 ```dart @collection class Product { Id? id; late String name; late int price; } ``` **数据:** | id | name | price | | --- | --------- | ----- | | 1 | Book | 15 | | 2 | Table | 55 | | 3 | Chair | 25 | | 4 | Pencil | 3 | | 5 | Lightbulb | 12 | | 6 | Carpet | 60 | | 7 | Pillow | 30 | | 8 | Computer | 650 | | 9 | Soap | 2 | 如果要找出价格超过 30 欧元的商品时,就需要查询 9 行数据。虽然 9 行数据不多问题不大,但是如果需要查询十万行那就是很大的问题了。 ```dart final expensiveProducts = await isar.products.filter() .priceGreaterThan(30) .findAll(); ``` 为了改善查询性能,我们对 `price` 属性进行了索引。一个索引就像是一张有序的查询表: ```dart @collection class Product { Id? id; late String name; @Index() late int price; } ``` **生成的索引表:** | price | id | | -------------------- | ------------------ | | 2 | 9 | | 3 | 4 | | 12 | 5 | | 15 | 1 | | 25 | 3 | | 30 | 7 | | **55** | **2** | | **60** | **6** | | **650** | **8** | 现在查询就会快多了。Isar 会直接从底下三行通过 Id 找出它们对应的对象。 ### 排序 另一个比较酷的是:索引支持超快的排序。对查询结果排序往往很耗性能,因为数据库必须加载所有的数据,将它们暂时放在内存,然后对它们排序。即使你指定了偏移量或限制,但它俩是在排序完成后才会被执行的。 假定我们想要找出四个最便宜的商品。我们可以使用下方查询代码: ```dart final cheapest = await isar.products.filter() .sortByPrice() .limit(4) .findAll(); ``` 在这个例子中,数据库必须加载所有(!)商品数据,按照价格给它们排序,然后返回四个价格最低的商品。 你或许会想到,用之前的索引来做应该会更高效。数据库直接读取索引表的前四行,然后返回它们所对应的商品数据,因为索引表默认是已经按照索引属性的大小顺序排好了的。 我们通过下方代码来实现: ```dart final cheapestFast = await isar.products.where() .anyPrice() .limit(4) .findAll(); ``` 这个 `.anyX()` Where 子句告诉 Isar 索引只是用来排序。你同样也可以使用如 `.priceGreaterThan()` 这样的 Where 子句来获取相同结果。 ## 唯一索引 唯一索引能保证索引不含重复的值。它可以由一个或多个属性构成。如果一个唯一索引仅包含一个属性,那么其对应的属性值就是唯一的。如果唯一索引由多个属性构成,那这些属性值的组合是唯一的。 ```dart @collection class User { Id? id; @Index(unique: true) late String username; late int age; } ``` 所有对唯一索引会造成数据重复的写入操作都会造成错误: ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); // -> 没问题 final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; // 试着写入一个和上面相同用户名的用户数据 await isar.users.put(user2); // -> 错误:违反了唯一性约束 print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] ``` ## 替换索引 有时候你可能不愿抛出唯一性约束错误,而是想要用新数据覆盖掉原有数据。那么你可以将对应属性的索引设置为 `replace: true` 来实现。 ```dart @collection class User { Id? id; @Index(unique: true, replace: true) late String username; } ``` 现在如果我们试着插入一个同用户名的用户数据,Isar 会直接使用新数据覆盖原有数据(这里的原有数据 user1 被新数据 user2 覆盖了,因为属性 username 必须是唯一的)。 ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; await isar.users.put(user1); print(await isar.user.where().findAll()); // > [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); print(await isar.user.where().findAll()); // > [{id: 2, username: 'user1' age: 30}] ``` 替换索引也提供了 `putBy()` 方法,允许你只更新对象数据,而不是直接覆盖它们。那么现有的 Id 将会被复用,所有关联也会被保留。 ```dart final user1 = User() ..id = 1 ..username = 'user1' ..age = 25; // user1 是第一次被写入数据库,因此这里效果等同于 put() 方法 await isar.users.putByUsername(user1); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1', age: 25}] final user2 = User() ..id = 2; ..username = 'user1' ..age = 30; await isar.users.put(user2); await isar.user.where().findAll(); // -> [{id: 1, username: 'user1' age: 30}] ``` 你可以看到,此处 Id 被复用了,对象始终是同一个,只是更改了 age 属性。 ## 大小写不敏感的索引 所有针对 `String` 和 `List` 属性的索引默认情况下对大小写敏感。如果你想创建一个对大小写不敏感的索引,你可以设置 `caseSensitive` 选项为 `false`: ```dart @collection class Person { Id? id; @Index(caseSensitive: false) late String name; @Index(caseSensitive: false) late List tags; } ``` ## 索引类型 索引有三种不同类型。大多数情况下,你会使用 `IndexType.value` 的值索引,但是哈希索引会更高效。 ### 值索引 值索引是默认的索引类型,是唯一可被用于非字符串或非数组类型属性的索引。属性的值将会被用于创建索引。对于数组 List,其包含的元素会被用来创建索引。值索引是三种类型中最灵活但同时也是最占存储空间的索引。 :::tip 对于原生数据类型(如 Int)的属性,或字符串类型的属性,但需要用到 `startsWith()` Where 子句,亦或是数组类型的属性,需要对其单一元素查询,那么你可以使用 `IndexType.value`。 ::: ### 哈希索引 字符串和数组可以通过散列化索引来大幅度减小存储空间。哈希索引的缺点是它无法通过前缀匹配来搜寻(如使用 `startsWith` 的 Where 子句)。 :::tip 对于类型为数组或字符串的属性,如果你不会用到 `startsWith` 和 `elementEqualTo` 的 Where 子句,可以使用 `IndexType.hash`。 ::: ### 哈希元素索引 我们可以使用 `IndexType.hash` 来对整个数组或字符串散列化处理,也可以使用 `IndexType.hashElements` 分别对数组中单个元素做散列化,来高效地创建多条目的索引。 :::tip 对于 `List` 类型的属性,如果你需要用到 `elementEqualTo`的 Where 子句,可以使用 `IndexType.hashElements`。 ::: ## 组合索引 组合索引是指包含多个属性的索引。Isar 允许你创建最多三个属性的组合索引。 组合索引也就是所谓的多列索引。 让我们从示例学习组合索引。我们先创建了一个 Person Collection,然后基于 age 和 name 属性定义了一个组合索引: ```dart @collection class Person { Id? id; late String name; @Index(composite: [CompositeIndex('name')]) late int age; late String hometown; } ``` **数据:** | id | name | age | hometown | | --- | ------ | --- | --------- | | 1 | Daniel | 20 | Berlin | | 2 | Anne | 20 | Paris | | 3 | Carl | 24 | San Diego | | 4 | Simon | 24 | Munich | | 5 | David | 20 | New York | | 6 | Carl | 24 | London | | 7 | Audrey | 30 | Prague | | 8 | Anne | 24 | Paris | **生成的索引表:** | age | name | id | | --- | ------ | --- | | 20 | Anne | 2 | | 20 | Daniel | 1 | | 20 | David | 5 | | 24 | Anne | 8 | | 24 | Carl | 3 | | 24 | Carl | 6 | | 24 | Simon | 4 | | 30 | Audrey | 7 | 生成的组合索引表包含了所有人的信息,并默认按照他们的年龄和姓名排序。 如果你想要高效地使用多个属性来进行排序并查询,组合索引能帮你轻松实现。它也提供了可同时查询多个属性的进阶版 Where 子句: ```dart final result = await isar.where() .ageNameEqualTo(24, 'Carl') .hometownProperty() .findAll() // -> ['San Diego', 'London'] ``` 组合索引中的最后一个属性也支持查询条件语句如 `startsWith()` 或 `lessThan()`: ```dart final result = await isar.where() .ageEqualToNameStartsWith(20, 'Da') .findAll() // -> [Daniel, David] ``` ## 多条目索引(全文检索) 如果你用 `IndexType.value` 对一个数组进行索引,Isar 会自动创建多条目索引,数组中每一个元素都会被索引。这适用于所有类型的数组。 多条目索引的实际应用包括对标签数组的索引或者创建全文检索的索引。 ```dart @collection class Product { Id? id; late String description; @Index(type: IndexType.value, caseSensitive: false) List get descriptionWords => Isar.splitWords(description); } ``` `Isar.splitWords()` 能将字符串按照 [Unicode Annex #29](https://unicode.org/reports/tr29/) 分解成一个个单词,所以它几乎适用于所有人类语言。 **数据:** | id | 字符串 | 分解结果 | | --- | ---------------------------- | ---------------------------- | | 1 | comfortable blue t-shirt | [comfortable, blue, t-shirt] | | 2 | comfortable, red pullover!!! | [comfortable, red, pullover] | | 3 | plain red t-shirt | [plain, red, t-shirt] | | 4 | red necktie (super red) | [red, necktie, super, red] | 相同的字符只会在索引中出现一次。 **生成的索引表:** | 单词 | id | | ----------- | --------- | | comfortable | [1, 2] | | blue | 1 | | necktie | 4 | | plain | 3 | | pullover | 2 | | red | [2, 3, 4] | | super | 4 | | t-shirt | [1, 3] | 现在这个索引可以使用每个单词的前缀匹配(或等同比较)的 Where 子句了。 :::tip 你应该也要考虑使用[语音算法](https://en.wikipedia.org/wiki/Phonetic_algorithm)如 [Soundex](https://en.wikipedia.org/wiki/Soundex) 返回的结果,而不是直接存储单词。 ::: ================================================ FILE: docs/docs/zh/limitations.md ================================================ # 局限性 你知道 Isar 是跨平台支持移动端、桌面端和 Web 端的,在移动和桌面端它是通过虚拟机来运行的,而 Web 端则不是。两者差异较大且有不同的局限性。 ## 虚拟机的局限 - 一个字符串只能用其前 1024 字节来进行 Where 子句的前缀匹配查询 - 对象的大小只能为 16MB ## Web 端的局限 因为 Isar 在 Web 端依赖于 IndexedDB,所以遇到的限制会更多,但即便如此,在使用 Isar 的过程中基本可以忽略不计。 - 不支持同步操作方法 - 目前,`Isar.splitWords()` 和 `.matches()` 还不支持 Web 端 - 不会像在虚拟机中那样严格检查 Schema 的改变,所以必须谨慎对待 - 所有数字类型将按照双浮点数(JS 中唯一的数字类型)做排序,所以 `@Size32` 不会起作用 - 索引的原理不同,所以哈希索引无法节省更多存储空间(但用法依然一致) - `col.delete()` 和 `col.deleteAll()` 能运行但是返回值不正确 - `col.clear()` 无法重置自增值 - `NaN` 尚不支持 ================================================ FILE: docs/docs/zh/links.md ================================================ --- title: 关联 --- # 关联(Link) 关联允许你表达对象之间的关系,比如评论的作者(即用户)。你可以使用 Isar 的关联来实现 `1:1`、`1:n` 和 `n:n` 的关系。使用关联比使用嵌套对象更不符人类工程学,因此你应该尽可能使用嵌套对象来代替关联。 你可以将关联理解为包含关系的一张数据表。它和 SQL 的关系很接近,但有一些不同的功能设定和 API。 ## IsarLink `IsarLink` 可以包含最多一个被关联对象,它经常被用于表达对一的关系。 `IsarLink` 有一个叫做 `value` 的属性,它负责存放被关联对象。 关联是懒加载的,因此你需要显式告诉 `IsarLink` 加载并保存 `value` 的值。你可以分别调用 `linkProperty.load()` 和 `linkProperty.save()` 方法。 :::tip 关联和被关联 Collection 的 Id 不应该为 final。 ::: 对于 Web 端,当你第一次使用一个 Collection 时,它所含的关联会被自动加载。让我们先从添加一个 IsarLink 开始学习: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teacher = IsarLink(); } ``` 我们在教师和学生之间定义了一个关联。在例子中,每一个学生对应每一个教师。 首先,我们创建一位数学教师,然后将 TA 分配给一个叫 Linda 的学生。我们需要使用 `.put()` 方法并手动保存关联。 ```dart final mathTeacher = Teacher()..subject = 'Math'; final linda = Student() ..name = 'Linda' ..teacher.value = mathTeacher; await isar.writeTxn(() async { await isar.students.put(linda); await isar.teachers.put(mathTeacher); await linda.teacher.save(); }); ``` 现在我们可以使用关联: ```dart final linda = await isar.students.where().nameEqualTo('Linda').findFirst(); final teacher = linda.teacher.value; // > Teacher(subject: 'Math') ``` 让我们用同步方法复现一次。我们不需要手动保存关联,因为 `.putSync()` 方法自动会存储所有关联,它甚至帮我们写入了被关联教师的数据。 ```dart final englishTeacher = Teacher()..subject = 'English'; final david = Student() ..name = 'David' ..teacher.value = englishTeacher; isar.writeTxnSync(() { isar.students.putSync(david); }); ``` ## IsarLinks 在上述例子中,一个学生对应多个教师才更符合实际情况。幸运的是,Isar 也有 `IsarLinks` 来实现对多的关系。 `IsarLinks` 自 `Set` 扩展而来,因此也可使用其相关的方法。 `IsarLinks` 和 `IsarLink` 一样也是懒加载。你可以通过调用 `linkProperty.load()` 来加载所有相关联对象,调用`linkProperty.save()` 来保存。 `IsarLink` 和 `IsarLinks` 的内部实现逻辑是一样的。我们可以将上述例子中的 `IsarLink` 改为 `IsarLinks`,将多个教师数据分配给单个学生(数据不会丢失)。 ```dart @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` 可以这么做的原因是我们没有修改关联的名称(`teacher`),所以 Isar 直接使用了之前的数据。 ```dart final biologyTeacher = Teacher()..subject = 'Biology'; final linda = isar.students.where() .filter() .nameEqualTo('Linda') .findFirst(); print(linda.teachers); // {Teacher('Math')} linda.teachers.add(biologyTeacher); await isar.writeTxn(() async { await linda.teachers.save(); }); print(linda.teachers); // {Teacher('Math'), Teacher('Biology')} ``` ## 反向关联 我知道你可能会问要表达反向关系该怎么做。无需担心,现在我们来介绍反向关联。 反向关联字面意义上很好理解,就是相反方向的关联。每个关联总是对应一个隐式的反向关联。为了使用它们,你可以给 `IsarLink` 或 `IsarLinks` 添加 `@Backlink()` 的注解。 反向关联不需要额外的内存或计算资源;所以你可以自由地添加、删除或者给它们改名,而无需担心数据丢失。 如果我们想要知道某一位教师所教的是哪些学生,就可以这么定义反向关联: ```dart @collection class Teacher { Id id; late String subject; @Backlink(to: 'teacher') final student = IsarLinks(); } ``` 我们需要给反向关联指定指向的关联。两个对象之间可以拥有多个不同关联。 ## 初始化关联 `IsarLink` 和 `IsarLinks` 都有一个无参构造器,用于在对象被创建时分配关联属性。将关联属性声明为 `final` 是正确的做法。 当你第一次使用 `put()` 方法来创建对象时,关联就会被初始化,然后你可以调用 `load()` 和 `save()` 方法。关联在被创建之后就会立即开始记录它所关联属性的数据变化,所以你甚至可以在创建它之前就可以添加或删除对象之间的关系。 :::danger 将关联移到另一个对象是不符合规范的。 ::: ================================================ FILE: docs/docs/zh/queries.md ================================================ --- title: 查询 --- # 查询 查询是指你如何查找匹配指定条件的数据。例如: - 查找所有被收藏的联系人 - 查找联系人列表中名(不是姓)不同的人 - 删除那些没有写明姓氏的联系人 因为查询是在数据库中而不是在 Dart 中执行的,所以它们非常快。当你巧妙地运用索引,性能将会更大幅度地被提高。下面你将学习如何来查询数据,以及如何提升查询性能。 有两种方法来过滤数据:过滤器 Filter 和 Where 子句。我们先来看 Filter 的用法。 ## Filter Filter 很好理解也很容易使用。Isar Generator 会根据 Collection 中字段的类型来生成多种 Filter,其中大部分 Filter 的名称也解释了它们的用途。 Filter 通过特定条件表达式来匹配 Collection 中每一个待查询对象。如果该表达式返回 `true`,那么 Isar 就会将该对象纳入查询结果中。Filter 不会影响查询结果的排列顺序。 我们通过下方 Collection 作为例子来说明: ```dart @collection class Shoe { Id? id; int? size; late String model; late bool isUnisex; } ``` ### 查询条件 根据上述 Collection 的字段类型,我们会有以下几种条件表达式可选择: | 条件 | 描述 | | ------------------------ | ---------------------------------------------------------------------------------------- | | `.equalTo(value)` | 匹配等于给定 `value` 的值. | | `.between(lower, upper)` | 匹配介于 `lower` 和 `upper` 之间的值 | | `.greaterThan(bound)` | 匹配大于 `bound` 的值. | | `.lessThan(bound)` | 匹配小于 `bound` 的值。 默认情况下 `null` 也会被纳入其中,因为 `null` 被认为小于任何值。 | | `.isNull()` | 匹配为 `null` 的值 | | `.isNotNull()` | 匹配不为 `null` 的值 | | `.length()` | 对于数组 List、字符串 String 和关联 Link 的长度查询是基于数组或关联中对象的数量的。 | 假设数据库包含四双鞋的数据,分别为尺码 39、40、46 和一双未知尺码(`null`)。除非你对它们进行排序,不然返回的结果是按照 Id 来排列的。 ```dart isar.shoes.filter() .sizeLessThan(40) .findAll() // -> [39, null] isar.shoes.filter() .sizeLessThan(40, include: true) .findAll() // -> [39, null, 40] isar.shoes.filter() .sizeBetween(39, 46, includeLower: false) .findAll() // -> [40, 46] ``` ### 逻辑运算符 你可以自行组合下方逻辑运算符来进行查询: | 运算符 | 描述 | | ---------- | --------------------------------------------------- | | `.and()` | 如果左右两边的表达式同时为 `true` 则返回 `true`。 | | `.or()` | 如果两侧表达式至少有一个为 `true` 则返回 `true`。 | | `.xor()` | 如果两侧表达式有且只有一个为 `true` 则返回 `true`。 | | `.not()` | 否定随后紧跟表达式的结果。 | | `.group()` | 给条件分组,允许指定运算顺序。 | 如果你想要查找所有尺码为 46 的鞋子,你可以使用以下代码: ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .findAll(); ``` 如果你想要使用多个条件,你可以用逻辑**与** `.and()`、逻辑**或** `.or()` 和逻辑**异或** `.xor()` 来组合多个 Filter。 ```dart final result = await isar.shoes.filter() .sizeEqualTo(46) .and() // 可选的。 因为 Filter 之间已经隐式使用了逻辑与。 .isUnisexEqualTo(true) .findAll(); ``` 上述查询条件等同于: `size == 46 && isUnisex == true`。 你也可以通过 `.group()` 对其进行分组: ```dart final result = await isar.shoes.filter() .sizeBetween(43, 46) .and() .group((q) => q .modelNameContains('Nike') .or() .isUnisexEqualTo(false) ) .findAll() ``` 上述查询条件等同于 `size >= 43 && size <= 46 && (modelName.contains('Nike') || isUnisex == false)`。 使用逻辑**非** `.not()` 来否定一个条件或一个条件组: ```dart final result = await isar.shoes.filter() .not().sizeEqualTo(46) .and() .not().isUnisexEqualTo(true) .findAll(); ``` 上述查询条件等同于 `size != 46 && isUnisex != true`。 ### 字符串条件 除了上述查询条件,还有下表若干个针对字符串查询的条件表达式可供使用。 例如,类似正则的通配符在搜索时提供了更多灵活性。 | 条件 | 描述 | | -------------------- | ---------------------------------- | | `.startsWith(value)` | 匹配以 `value` 开头的字符串。 | | `.contains(value)` | 匹配包含 `value` 的字符串。 | | `.endsWith(value)` | 匹配以 `value` 结尾的字符串。 | | `.matches(wildcard)` | 匹配符合 `wildcard` 正则的字符串。 | **大小写敏感** 所有字符串操作都有一个可选的参数 `caseSensitive`,默认情况下为 `true`。 **通配符:** 一个[通配符字符串表达式](https://en.wikipedia.org/wiki/Wildcard_character)是指一段使用了两个特殊通配符的普通字符串: - `*` 通配符匹配零个或多个任意字符。 - `?` 通配符匹配任意一个字符。 例如,通配符字符串 `"d?g"` 匹配 `"dog"`、`"dig"`、和 `"dug"`,但不匹配 `"ding"`、`"dg"` 或`"a dog"`。 ### 查询修改器 有时候,基于某些特定条件的查询或针对不同值的查询是有必要的。Isar 通过内置强大的修改器功能来实现这些条件查询: | 修改器 | 描述 | | --------------------- | --------------------------------------------------------------------------------------------------------------------- | | `.optional(cond, qb)` | 当且仅当 `condition` 为 `true` 时扩充查询条件。该修改器可被用于查询表达式的任意位置,比如有条件地排序或限制查询个数。 | | `.anyOf(list, qb)` | 为 `values` 中每个值扩充查询条件,然后将它们作逻辑**或**运算。 | | `.allOf(list, qb)` | 为 `values` 中的每个值扩充查询条件,然后将它们作逻辑**与**运算。 | 在下方例子中,我们创建了一个函数,该函数通过一个可选的 Filter 来查找鞋子: ```dart Future> findShoes(Id? sizeFilter) { return isar.shoes.filter() .optional( sizeFilter != null, // 当且仅当 sizeFilter != null 时,才会执行 q.sizeEqualTo(sizeFilter!) (q) => q.sizeEqualTo(sizeFilter!), ).findAll(); } ``` 如果你想要搜寻某些尺码的鞋子时,如 38、40 或 42 码的鞋子,你要么可以使用传统的方式,要么可以使用修改器,代码如下: ```dart final shoes1 = await isar.shoes.filter() .sizeEqualTo(38) .or() .sizeEqualTo(40) .or() .sizeEqualTo(42) .findAll(); final shoes2 = await isar.shoes.filter() .anyOf( [38, 40, 42], (q, int size) => q.sizeEqualTo(size) ).findAll(); // shoes1 == shoes2 ``` 当你想要动态查询时,修改器特别有用。 ### 数组 List 甚至也可以查询数组 List: ```dart class Tweet { Id? id; String? text; List hashtags = []; } ``` 你可以根据数组的长度来查询: ```dart final tweetsWithoutHashtags = await isar.tweets.filter() .hashtagsIsEmpty() .findAll(); final tweetsWithManyHashtags = await isar.tweets.filter() .hashtagsLengthGreaterThan(5) .findAll(); ``` 这分别等同于 `tweets.where((t) => t.hashtags.isEmpty);` 和 `tweets.where((t) => t.hashtags.length > 5);`。 你亦可基于其包含的元素来查询: ```dart final flutterTweets = await isar.tweets.filter() .hashtagsElementEqualTo('flutter') .findAll(); ``` 这等同于 `tweets.where((t) => t.hashtags.contains('flutter'));`。 ### 嵌套对象 嵌套对象是 Isar 中最有用的功能之一。可以使用同样适用于顶层对象的查询条件来高效查询它们。假定我们有以下数据模型: ```dart @collection class Car { Id? id; Brand? brand; } @embedded class Brand { String? name; String? country; } ``` 我们想要查询品牌名为 `"BMW"` 且品牌国家为 `"Germany"` 的所有车辆。我们可以执行以下代码: ```dart final germanCars = await isar.cars.filter() .brand((q) => q .nameEqualTo('BMW') .and() .countryEqualTo('Germany') ).findAll(); ``` 永远试着给嵌套查询分组。上述查询比下方的例子性能更好。尽管查询结果是相同的: ```dart final germanCars = await isar.cars.filter() .brand((q) => q.nameEqualTo('BMW')) .and() .brand((q) => q.countryEqualTo('Germany')) .findAll(); ``` ### 关联(Link) 如果你的数据模型含有[关联或反向关联](links),你可以根据被关联的对象或被关联对象的数量来进行查询。 :::tip 记住,关联查询的效率相对更低。因为 Isar 需要查询相关联的对象。考虑尽量使用嵌套对象来代替关联。 ::: ```dart @collection class Teacher { Id? id; late String subject; } @collection class Student { Id? id; late String name; final teachers = IsarLinks(); } ``` 我们想要找到所有修数学或英语的学生: ```dart final result = await isar.students.filter() .teachers((q) { return q.subjectEqualTo('Math') .or() .subjectEqualTo('English'); }).findAll(); ``` 只要至少一个相关联对象符合条件,查询条件就会为 `true` 。 让我们搜索所有没有老师的学生: ```dart final result = await isar.students.filter().teachersLengthEqualTo(0).findAll(); ``` 或者: ```dart final result = await isar.students.filter().teachersIsEmpty().findAll(); ``` ## Where 子句 Where 子句很强大,但是用对可能有点困难。 相对于 Filter, Where 子句利用你在 Schema 中定义的索引来作为查询条件。对索引进行查询比对单条数据查询可快多了。 ➡️ 学习更多:[索引](indexes) :::tip 一条基本的规则是你应该永远尽可能多地使用 Where 子句来进行索引查询,然后用 Filter 对未被索引的数据进行查询。 ::: 你只能用逻辑**与**来对多个 Where 子句做逻辑运算。换句话说,你可以叠加多个 Where 子句,但不能查询多个 Where 子句的交集。 让我们给下面 Collection 添加索引: ```dart @collection class Shoe with IsarObject { Id? id; @Index() Id? size; late String model; @Index(composite: [CompositeIndex('size')]) late bool isUnisex; } ``` 这里有俩个索引。 `size` 上的索引允许我们使用像 `.sizeEqualTo()` 的 Where 子句,`isUnisex` 上的组合索引则允许我们可以使用像 `isUnisexSizeEqualTo()` 这样的 Where 子句,当然也可以使用 `isUnisexEqualTo()`,因为永远可以使用索引的任何前缀查询语句。 我们可以用组合索引重写之前的查询尺码 46 鞋子的代码。这次查询会比之前快很多: ```dart final result = isar.shoes.where() .isUnisexSizeEqualTo(true, 46) .findAll(); ``` Where 子句还有两个强大特性:它允许你“自由”排序和超快去重操作。 ### 将 Where 子句和 Filter 相结合 还记得 `shoes.filter()` 查询吗?实际上它是 `shoes.where().filter()` 的简写。你可以(也应该)在同一查询中同时运用 Where 子句和 Filter 来最大限度地提升查询性能: ```dart final result = isar.shoes.where() .isUnisexEqualTo(true) .filter() .modelContains('Nike') .findAll(); ``` 先用 Where 子句来过滤出部分对象,减少了查询对象数量。然后用 Filter 来查询剩下的对象。 ## 排序 你可以在查询中使用 `.sortBy()`、`.sortByDesc()`、 `.thenBy()` 和 `.thenByDesc()` 等方法来给待查询数据进行排序。 下方代码演示了不用索引来查询鞋子,查询结果以鞋款名正序和鞋码倒序来排列: ```dart final sortedShoes = isar.shoes.filter() .sortByModel() .thenBySizeDesc() .findAll(); ``` 对诸多结果进行排序可是非常消耗性能的,尤其是因为排序发生在偏移量(Offset)和限制(Limit)之前。上述排序的方法也从未利用到索引。幸运的是,我们可以再次使用 Where 子句来进行排序以提升性能,这样即使对上百万的结果进行排序也毫无问题。 ### 使用 Where 子句来排序 如果你在查询中使用**单个** Where 子句, 那么查询结果就已经通过索引被排列好了。这很重要! 假设我们有鞋码分别为 `[43, 39, 48, 40, 42, 45]` 的鞋子。我们想查询所有鞋码大于 42 的鞋子,然后将它们按鞋码大小排序: ```dart final bigShoes = isar.shoes.where() .sizeGreaterThan(42) // 也将结果按鞋码大小排序 .findAll(); // -> [43, 45, 48] ``` 如你所见,此处结果是按照索引 `size` 来排序的。如果你想要倒序排列,可以将 `sort` 设置为 `Sort.desc`: ```dart final bigShoesDesc = await isar.shoes.where(sort: Sort.desc) .sizeGreaterThan(42) .findAll(); // -> [48, 45, 43] ``` 有些时候你不想过滤数据,只是想对全部数据排序,但是也可以受益于这种隐式排序。你可以使用 `any` Where 子句: ```dart final shoes = await isar.shoes.where() .anySize() .findAll(); // -> [39, 40, 42, 43, 45, 48] ``` 如果你使用组合索引,查询结果会根据索引内所有字段进行排序。 :::tip 如果你需要对结果进行排序,考虑使用索引。尤其是如果你需要用到 `offset()` 和 `limit()`。 ::: 然而有时候使用索引来排序变得不太方便或不容易实现。对于这种情况,你应该尽可能通过索引来减少待查询结果的数量。 ## 唯一值 使用 distinct 断言来返回含有唯一值的对象数据。 例如,在 Isar 数据库中找出有多少种不同鞋款: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .findAll(); ``` 你也可以链式地调用多个 distinct 条件来找出所有不同鞋码且不同鞋款的鞋子: ```dart final shoes = await isar.shoes.filter() .distinctByModel() .distinctBySize() .findAll(); ``` 只有每种不同条件组合的第一个对象会被返回。 你可以用 Where 子句和排序操作来控制它。 ### Where 子句去重化 如果你有一个索引,它对应的字段可能出现相同值,你可能希望对该字段进行去重化。你可以使用前面部分提到的 `distinctBy` 方法,但它在排序和 Filter 之后执行,所以有些许额外的性能开销。 而如果你只用到一个 Where 子句,你可以只依赖索引来实现去重化。 ```dart final shoes = await isar.shoes.where(distinct: true) .anySize() .findAll(); ``` :::tip 理论上,你甚至可以使用多个 Where 子句来排序和去重。唯一的限制是那些 Where 子句不能彼此有重叠(即上面提到的交集)且不能使用相同的索引。它们需要按照顺序来使用,以便正确排序。因此如果依赖于这种用法,你必须要细心谨慎。 ::: ## 偏移量(Offset)和限制(Limit) 对于一个懒加载列表组件来说,限制显示的个数通常是很好的办法。你可以使用 `limit()` 对查询结果的数量进行限制: ```dart final firstTenShoes = await isar.shoes.where() .limit(10) .findAll(); ``` 而借用 `offset()` 你也可以对查询结果进行分页。 ```dart final firstTenShoes = await isar.shoes.where() .offset(20) .limit(10) .findAll(); ``` 因为初始化 Dart 对象往往是执行查询过程中最消耗性能的部分,因此只加载你所需要的对象是一个不错的做法。 ## 执行顺序 Isar 总是按照下面顺序执行查询: 1. 遍历索引来查询对象(即使用 Where 子句) 2. 对对象进行过滤 3. 对结果进行排序 4. 去重化(若有) 5. 偏移量和限制(若有) 6. 返回查询结果 ## 查询操作方法 在之前的例子中,我们使用方法 `.findAll()` 来获取所有匹配对象。然而,还有其他几种查询操作方法: | 方法 | 描述 | | ---------------- | ---------------------------------------------------------------------------------------------------- | | `.findFirst()` | 返回第一个匹配条件的对象,若无匹配,则返回 `null`。 | | `.findAll()` | 返回所有匹配条件的对象。 | | `.count()` | 返回匹配条件的对象数量。 | | `.deleteFirst()` | 从 Collection 中删除第一个匹配条件的对象。 | | `.deleteAll()` | 从 Collection 中删除所有匹配条件的对象。 | | `.build()` | 将查询条件语句编译,以便重复使用。倘若你想要多次用到同一查询条件,你可以使用这个方法来避免重复代码。 | ## 查询属性 如果你只对单条属性的值感兴趣,你可以使用属性查询。创建一个查询然后选择想要的属性即可: ```dart List models = await isar.shoes.where() .modelProperty() .findAll(); List sizes = await isar.shoes.where() .sizeProperty() .findAll(); ``` 使用单个属性在反序列化中节省了很多时间。属性查询同样适用于嵌套对象和数组。 ## 聚合查询(Aggregation) Isar 支持对单个属性的聚合查询,为此提供了下表几种聚合查询操作方法: | 操作 | 描述 | | ------------ | -------------------------------------------- | | `.min()` | 返回最小值,若无匹配,则返回 `null`。 | | `.max()` | 返回最大值,若无匹配,则返回 `null`。 | | `.sum()` | 返回所有值的总和。 | | `.average()` | 返回所有值的平均值,若无匹配,则返回 `NaN`。 | 直接使用聚合查询比先找出对象,再做聚合运算快多了。 ## 动态查询 :::danger 这部分很大可能与你无关。不鼓励使用动态查询,除非你绝对需要(往往你很少需要)。 ::: 所有上述例子都使用了查询构造器 QueryBuilder 和由 Isar Generator 自动生成的静态扩充方法。你可能想要创建一个动态查询,或自定义的查询语言(就像 Isar Inspector 做的那样)。在这种情况下,你可以使用方法 `buildQuery()`: | 参数 | 描述 | | --------------- | ------------------------------------------------------------------------ | | `whereClauses` | 查询语句所需的 Where 子句 | | `whereDistinct` | 是否设置 Where 子句对返回结果去重化(只有在使用单个 Where 子句时有用)。 | | `whereSort` | Where 子句的遍历顺序(只有在使用单个 Where 子句时有用)。 | | `filter` | 用来过滤查询结果的 Filter。 | | `sortBy` | 需要用来排序的属性列表。 | | `distinctBy` | 需要用来去重化的属性列表。 | | `offset` | 查询结果的偏移量。 | | `limit` | 返回查询结果的最大数量。 | | `property` | 若非空,则只返回该属性的值。 | 让我们创建一个动态查询: ```dart final shoes = await isar.shoes.buildQuery( whereClauses: [ WhereClause( indexName: 'size', lower: [42], includeLower: true, upper: [46], includeUpper: true, ) ], filter: FilterGroup.and([ FilterCondition( type: ConditionType.contains, property: 'model', value: 'nike', caseSensitive: false, ), FilterGroup.not( FilterCondition( type: ConditionType.contains, property: 'model', value: 'adidas', caseSensitive: false, ), ), ]), sortBy: [ SortProperty( property: 'model', sort: Sort.desc, ) ], offset: 10, limit: 10, ).findAll(); ``` 上述代码等价于以下代码: ```dart final shoes = await isar.shoes.where() .sizeBetween(42, 46) .filter() .modelContains('nike', caseSensitive: false) .not() .modelContains('adidas', caseSensitive: false) .sortByModelDesc() .offset(10).limit(10) .findAll(); ``` ================================================ FILE: docs/docs/zh/recipes/data_migration.md ================================================ --- title: 数据迁移 --- # 数据迁移 当你添加或删除 Collection、或其字段或索引时,Isar 会自动为你的数据库 Schema 做数据迁移。有时候你可能想要自行迁移。Isar 没有提供相关函数,因为这么做会给数据迁移强行加了限制。根据自己的需求来自由实现迁移功能其实很简单。 在下方的例子中,我们希望使用整个数据库的单一版本。我们用 shared_preferences 这个库来保存当下的版本,然后跟我们要迁移的版本做比较。如果两个版本不匹配,那么就迁移数据,更新版本。 :::tip 你也可以给每个 Collection 分配单独的版本,然后单独为它们各自做数据迁移。 ::: 假设我们有一个用户 Collection,它包含一个出生日 birthday 字段。在我们第二版 App 中,我们需要根据用户年龄来查询用户,就必须添加额外的出生年份字段。 版本 1: ```dart @collection class User { Id? id; late String name; late DateTime birthday; } ``` 版本 2: ```dart @collection class User { Id? id; late String name; late DateTime birthday; short get birthYear => birthday.year; } ``` 可问题是现有的用户数据中不会有 `birthYear` 这个字段的信息,因为它在版本 1 中不存在。我们需要借用数据迁移来为 `birthYear` 字段赋值。 ```dart import 'package:isar/isar.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); await performMigrationIfNeeded(isar); runApp(MyApp(isar: isar)); } Future performMigrationIfNeeded(Isar isar) async { final prefs = await SharedPreferences.getInstance(); final currentVersion = prefs.getInt('version') ?? 2; switch(currentVersion) { case 1: await migrateV1ToV2(isar); break; case 2: // 如果版本未设置(新建的时候)或已经是版本 2,我们就不做处理 return; default: throw Exception('Unknown version: $currentVersion'); } // 更新版本 await prefs.setInt('version', 2); } Future migrateV1ToV2(Isar isar) async { final userCount = await isar.users.count(); // 我们对用户数据进行分页读写,避免同时将所有数据存放到内存 for (var i = 0; i < userCount; i += 50) { final users = await isar.users.where().offset(i).limit(50).findAll(); await isar.writeTxn((isar) async { // 我们不需要更新任何信息,因为 birthYear 的 getter 已经被使用 await isar.users.putAll(users); }); } } ``` :::warning 如果你需要迁移大量数据,考虑在后台使用另一个 isolate 来处理,以防止对 UI 进程造成阻塞。 ::: ================================================ FILE: docs/docs/zh/recipes/full_text_search.md ================================================ --- title: 全文检索 --- # 全文检索 全文检索是一种从数据库中搜索文本的强大功能。你现在应该已经熟悉[索引](../indexes.md)的工作原理了,但还是让我们先了解一些基本知识。 索引就像一张查询表,允许快速地根据给定值查找数据。例如,如果你的对象含有一个 `title` 字段,你可以以该字段创建一张索引表,以此根据给定的标题来快速查询。 ## 为什么全文检索很有用? 你本可以轻松通过 Filter 来搜索文本。Isar 为你提供了许多字符串查询方法,例如 `.startsWith()`、`.contains()` 和 `.matches()`。但问题在于 Filter 的复杂度是 `O(n)`,其中 `n` 是 Collection 中对象的个数,像 `.matches()` 这样的字符串操作就格外消耗性能。 :::tip 全文检索比 Filter 快多了,但是索引也有局限的地方。在本专题中,我们将探寻如何解决这些局限性。 ::: ## 基本示例 想法依然不变:我们对文本中的单词进行索引,而不是对整个文本索引,这样我们可以对单个单词进行搜索。 让我们先创建一个基本的全文检索索引: ```dart class Message { Id? id; late String content; @Index() List get contentWords => content.split(' '); } ``` 现在我们可以通过内容中某些指定词汇来搜索讯息: ```dart final posts = await isar.messages .where() .contentWordsAnyEqualTo('hello') .findAll(); ``` 这条查询非常快,但是有几个问题: 1. 我们只能搜索整个词汇 2. 我们没考虑标点符号 3. 我们不支持其他空白字符 ## 正确分割文本 让我们完善上述例子。我们可以用一个复杂的正则来正确分割文本,但是在某些少数情况下它很可能会出错且导致查询变得很慢。 [Unicode Annex #29](https://unicode.org/reports/tr29/) 为几乎所有人类语言定义了如何正确分割文本。它很复杂,但是幸运的是,Isar 内部已经帮我们实现了: ```dart Isar.splitWords('hello world'); // -> ['hello', 'world'] Isar.splitWords('The quick (“brown”) fox can’t jump 32.3 feet, right?'); // -> ['The', 'quick', 'brown', 'fox', 'can’t', 'jump', '32.3', 'feet', 'right'] ``` ## 我想要更多控制 很简单!我们可以修改索引配置,让它支持前缀匹配和大小写匹配: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get titleWords => title.split(' '); } ``` 默认情况下,Isar 会将单词散列化,这么做性能很快且节省存储空间。但是这样就无法使用前缀匹配查询。我们改变了索引类型,使用 `IndexType.value` 而不是 `IndexType.hash`,来直接使用那些单词。借此我们就可以使用 `.titleWordsAnyStartsWith()` 的 Where 子句: ```dart final posts = await isar.posts .where() .titleWordsAnyStartsWith('hel') .or() .titleWordsAnyStartsWith('welco') .or() .titleWordsAnyStartsWith('howd') .findAll(); ``` ## 我也需要 `.endsWith()` 方法 没问题!我们会用一个小技巧来实现 `.endsWith()` 匹配: ```dart class Post { Id? id; late String title; @Index(type: IndexType.value, caseSensitive: false) List get revTitleWords { return Isar.splitWords(title).map( (word) => word.reversed).toList() ); } } ``` 不要忘记倒序排列查询的结果: ```dart final posts = await isar.posts .where() .revTitleWordsAnyStartsWith('lcome'.reversed) .findAll(); ``` ## 词干提取算法 不幸的是,索引不支持 `.contains()` 匹配(其他数据库也如此)。但是还有几个备选方案值得我们研究一番。选择何种方式完全取决于你的使用场景。举个例子,你可以对词干进行索引,而不是对整个单词索引。 词干提取算法指的是自然语言处理领域里去除词缀得到词根的过程,即得到单词最一般的写法: ``` connection connections connective ---> connect connected connecting ``` 常见的算法有 [Porter 词干提取算法](https://tartarus.org/martin/PorterStemmer/) 和 [Snowball 词干提取算法](https://snowballstem.org/algorithms/)。 还有将单词复杂形态转变成最基础形态的[词形还原](https://en.wikipedia.org/wiki/Lemmatisation)。 ## 语音算法 [语音算法](https://en.wikipedia.org/wiki/Phonetic_algorithm) 是指根据发音来检索单词的算法。也就是说,它可以根据发音接近程度来帮你查询结果。 :::warning 大部分语音算法通常只支持单一语言,一般是英语。 ::: ### Soundex [Soundex](https://en.wikipedia.org/wiki/Soundex) 是一种语音算法,它通过英文发音来检索名字。它的目的是将同音词用同一编码表示,虽然发音略有差异,但可达到模糊匹配的效果。这是个非常直接明了的算法,也有若干改进版本。 若是用这个算法,那么单词 `"Robert"` 和 `"Rupert"` 都会返回编码 `"R163"`,而单词 `"Rubin"` 则返回 `"R150"`。 同音词 `"Ashcraft"` 和 `"Ashcroft"` 则都会返回 `"A261"`。 ### Double Metaphone [Double Metaphone](https://en.wikipedia.org/wiki/Metaphone) 也是一种语音算法,是 Metaphone 的二代版本。它在前代基础上改进了不少基本设计。 Double Metaphone 加入了对大量来自外来语如斯拉夫语、德语、凯尔特语、希腊语、法语、意大利语、西班牙语、中文等的不规则英文单词发音的支持。 ================================================ FILE: docs/docs/zh/recipes/multi_isolate.md ================================================ --- title: Multi-Isolate 用法 --- # Multi-Isolate 用法 所有的 Dart 代码都是在 isolate 而不是线程中运行的。每个 isolate 都有它自己的内存,确保彼此之间互相隔离。 Isar 可同时用于多个 isolate,甚至观察者也支持跨多个 isolate。本专题将会探讨如何在多 isolate 环境下使用 Isar。 ## 什么时候使用多个 isolate Isar 事务即使在单 isolate 环境中也是并行运行的。但有些情况下,从多 isolate 环境访问 Isar 可能依然利大于弊。 因为 Isar 花费不少时间去对 Dart 对象进行编码和解码。你姑且可以将之类比成对 JSON 编码和解码(只不过实际性能更快)。这些操作都在读取数据的 isolate 中进行,当然会阻碍该 isolate 中其他代码的运行。也就是说:Isar 的确需要在你的 isolate 中做不少工作。 如果你同时需要读取或写入几百个对象数据,在 UI isolate 中这么做没有问题。但是如果 UI 已经很忙碌或事务操作量很大,你就应该考虑使用多个 isolate。 ## 例子 首先我们在新的 isolate 中创建 Isar 实例。因为在主 isolate 中我们已经有一个 Isar 实例,所以调用方法 `Isar.open()` 会直接返回该实例。 :::warning 确保在主 isolate 中给实例传入相同的 Schema,否则会发生错误。 ::: `compute()` 创建一个新的 isolate,并运行传给它的函数。 ```dart void main() { // 在 UI isolate 中创建 Isar 实例 final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [MessageSchema], directory: dir.path, name: 'myInstance', ); // 订阅数据库中消息表的变化 isar.messages.watchLazy(() { print('omg the messages changed!'); }); // 创建一个新的 isolate,写入 10000 条讯息到数据库 compute(createDummyMessages, 10000).then(() { print('isolate finished'); }); // 一段时间后,打印出: // > omg the messages changed! // > isolate finished } // 函数将会在新的 isolate 中被执行 Future createDummyMessages(int count) async { // 我们没必要在此指定路径,因为它已经被创建好了 final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [PostSchema], directory: dir.path, name: 'myInstance', ); final messages = List.generate(count, (i) => Message()..content = 'Message $i'); // 我们在 isolate 中使用了同步事务 isar.writeTxnSync(() { isar.messages.insertAllSync(messages); }); } ``` 上述例子中需要注意的几个点: - `isar.messages.watchLazy()` 在 UI isolate 中被调用,接收来自另一个 isolate 中数据变化的通知。 - 实例之间用名称来辨别。默认实例名称为 `default`,但是在上面的示例中,我们将它命名为 `myInstance`。 - 我们在一个新 isolate 中使用了同步事务来写入讯息。阻塞这个 isolate 没什么问题,因为它不会影响到 UI isolate,而且同步事务相比异步更快。 ================================================ FILE: docs/docs/zh/recipes/string_ids.md ================================================ --- title: 字符串 Id --- # 字符串 Id 这是我遇到的最常见的请求之一,所以就有了这篇教程。 Isar 原生不支持字符串 Id,这是经过深思熟虑的:原因是整型 Id 比字符串 Id 性能更好。尤其是在处理关联上,使用字符串 Id 会显著增加额外的性能开销。 我理解,有时候你需要存储一些使用 UUID 或非整型 Id 的数据。我建议将这些字符串 Id 作为对象的属性,并用其快速散列化后的 64 位整型作为 Isar 对象的 Id。 ```dart @collection class User { String? id; Id get isarId => fastHash(id!); String? name; int? age; } ``` 通过这个办法,你既可以高效地使用整型 Id 来处理关联,又保留了原有数据中的字符串 Id。 ## 快速散列函数 理想情况下,你的散列函数应该兼具高可用性(没人希望崩溃或意外发生)和高性能。我推荐使用下方代码实现: ```dart /// 针对 Dart 字符串优化的 64 位哈希算法 FNV-1a int fastHash(String string) { var hash = 0xcbf29ce484222325; var i = 0; while (i < string.length) { final codeUnit = string.codeUnitAt(i++); hash ^= codeUnit >> 8; hash *= 0x100000001b3; hash ^= codeUnit & 0xFF; hash *= 0x100000001b3; } return hash; } ``` 如果你选择其他散列函数,确保它返回 64 位整型,避免使用加密散列函数,因为它们非常慢。 :::warning 避免使用 `string.hashCode`,因为无法保证它能够适用于各个平台,或适配各个版本的 Dart。 ::: ================================================ FILE: docs/docs/zh/schema.md ================================================ --- title: Schema --- # Schema 当你使用 Isar 来存储数据时,你需要对 Collection 进行操作。Collection 可理解为 Isar 数据库中的表,其包含的数据只能为同一类 Dart 对象。每个对象则代表了对应数据表中的一行数据。 对 Collection 的定义就被称为 “Schema”。 Isar Generator 会根据 Schema 自动生成大部分代码,然后你可以通过这些代码来对 Collection 进行相关操作。 ## Collection 的构造 你可以通过给每一个类添加 `@collection` 或 `@Collection()` 的注解来定义一个 Collection。 一个 Collection 所包含的字段对应数据库中的每一列,其中包括一个主 key。 如下代码所示,`User` Collection 表示一张用户数据表,其列名分别为 id、firsName 以及 lastName: ```dart @collection class User { Id? id; String? firstName; String? lastName; } ``` :::tip 为了保存一个字段,Isar 必须能够读取到它。你可以声明其为公开字段,或者为其提供 getter 或 setter 方法,来确保 Isar 能够读取到它。 ::: 在定义 Collection 的时候,若干配置参数可供选择: | 参数 | 描述 | | ------------- | ---------------------------------------------------------------------------------------------------------------------- | | `inheritance` | 确定 Isar 是否保存父类的字段或 mixin。默认情况为启用。 | | `accessor` | 允许你更改默认的 Collection 的访问名。例如,默认设置下自动生成的代码会用 `isar.contacts` 来访问 `Contact` Collection。 | | `ignore` | 允许忽视特定字段。 这同样也适用于超类。 | ### Isar Id 每一个 Collection 类都必须定义一个 `Id` 类型的 Id 属性,以便唯一指代一个对象。 实际上,`Id` 类型在这里只是 `int` 类型的别名,只不过是为了让 Isar Generator 能够识别该属性。 Isar 将会自动索引 Id 属性,这能够让你轻松高效地通过对象的 Id 来查询或修改它。 你可以选择要么自己设置 Id,要么让 Isar 自行分配一个自增的 Id。但是如果你设置的 `id` 字段为 `null` 或者不是 `final`,Isar 也会自动覆盖成自增的 Id。倘若你想要一个非空自增的 Id,那么你可以给它赋值为 `Isar.autoIncrement`,而不是 `null`。 :::tip 当对象被删除后,其自增的 Id 也不会被重新使用。唯一重置 Id 的方法是删除整个数据库。 ::: ### 给 Collection 和其字段改名 默认情况下,Isar 会使用类的名称作为 Collection 的名称。相似地,Isar 也会用字段名称来作为数据表的列名。倘若你想要修改 Collection 或字段的名称,在对应位置添加 `@Name` 注解。可参考下方例子: ```dart @collection @Name("User") class MyUserClass1 { @Name("id") Id myObjectId; @Name("firstName") String theFirstName; @Name("lastName") String familyNameOrWhatever; } ``` 尤其是当你想要修改已经存入数据库中对象的字段名称时,你可以考虑使用 `@Name`(例如,现有数据字段命名带有下划线,而在 Dart 中定义时则为小写驼峰式,如此情况下可以通过修改名称来匹配)。否则的话, 因为名称不匹配,导致原有的数据未被保存,而不同名的字段则额外被添加到数据库里。 ### 忽略字段 Isar 会保存 Collection 类中所有的公开属性。如下例子所示,给一个属性或 getter 添加 `@ignore` 注解,就可以防止其被 Isar 保存: ```dart @collection class User { Id? id; String? firstName; String? lastName; @ignore String? password; } ``` 当 Collection 类从父类继承了一些你不想保存的字段时,通常直接在 `@Collection` 注解里设置 `ignore` 更为便利: ```dart @collection class User { Image? profilePicture; } @Collection(ignore: {'profilePicture'}) class Member extends User { Id? id; String? firstName; String? lastName; } ``` 如果一个 Collection 包含 Isar 不支持的数据类型的字段时,你必须忽略掉对应字段。 :::warning 记住,在那些未被 Isar 保存的字段里存储信息不是正确的做法。 ::: ## Isar 支持的数据类型 Isar 支持以下数据类型: - `bool` - `byte` - `short` - `int` - `float` - `double` - `DateTime` - `String` - `List` - `List` - `List` - `List` - `List` - `List` - `List` - `List` 还有,嵌套的对象和枚举也是支持的。我们等会会讲到它们。 ## byte,short,float 对于大多数应用场景,你不会需要整个 64 位范围的整数或双精度浮点数。Isar 支持以下额外的数据类型,它们可以用于较小范围的数字,这样也帮助你节省了存储空间和内存使用。 | 类型 | 字节大小 | 数字范围 | | ---------- | -------- | ------------------------------------------------------- | | **byte** | 1 | 0 到 255 | | **short** | 4 | -2,147,483,647 到 2,147,483,647 | | **int** | 8 | -9,223,372,036,854,775,807 到 9,223,372,036,854,775,807 | | **float** | 4 | -3.4e38 到 3.4e38 | | **double** | 8 | -1.7e308 到 1.7e308 | 表中的数字类型只是原生 Dart 数据类型的别名。所以例如 `short` 实际上和 `int` 用法一样,只不过限定了它的数字范围。 参考下方例子: ```dart @collection class TestCollection { Id? id; late byte byteValue; short? shortValue; int? intValue; float? floatValue; double? doubleValue; } ``` 所有的数字类型也可以用于数组 List。比如你可以用 `List` 来保存 byte。 ## 可空类型 理解 Isar 中的可空性原理是最基本的:数字类型**并没有**对于 `null` 的专门表达。相反,Isar 用特定的值来表示空: | 类型 | VM | | ---------- | ------------- | | **short** | `-2147483648` | | **int** |  `int.MIN` | | **float** | `double.NaN` | | **double** |  `double.NaN` | `bool`、`String` 和 `List` 则有对 `null` 的表达。 这样的处理方式能够带来性能上的提高,能够让你自由更改字段的可空性,而不需要额外的数据迁移或多余代码来处理空值。 :::warning `byte` 类型不支持空值。 ::: ## 日期(DateTime) Isar 不会保存日期类型数据中的时区信息。相反,它会在存储之前将 `DateTime` 数据转成 UTC 格式。 Isar 返回的日期数据都为当地时间。 `DateTime` 以微秒精度被存储,而在浏览器中,由于 JavaScript 的局限性,最高只能以毫秒精度被存储。 ## 枚举(Enum) 就像其他 Isar 所支持的数据类型一样,Isar 也允许存储和使用枚举类型。然而,你可以选择 Isar 如何来表示枚举。 Isar 支持以下四种不同策略: | 策略类型 | 描述 | | ----------- | ------------------------------------------------------------ | | `ordinal` | 枚举的索引以 `byte` 类型被保存。性能很高但不支持可空的枚举。 | | `ordinal32` | 枚举的索引以 `short` (4 字节整型) 被保存。 | | `name` | 枚举的名称以 `String` 被保存。 | | `value` | 用一个自定义属性来读取枚举值。 | :::warning `ordinal` 和 `ordinal32` 依赖于枚举值的属性。如果你改变了枚举内值的顺序,现有数据库将会返回错误的结果。 ::: 让我们通过例子来了解每种策略。 ```dart @collection class EnumCollection { Id? id; @enumerated // 等价于 EnumType.ordinal late TestEnum byteIndex; // 不能为空值 @Enumerated(EnumType.ordinal) late TestEnum byteIndex2; // 不能为空值 @Enumerated(EnumType.ordinal32) TestEnum? shortIndex; @Enumerated(EnumType.name) TestEnum? name; @Enumerated(EnumType.value, 'myValue') TestEnum? myValue; } enum TestEnum { first(10), second(100), third(1000); const TestEnum(this.myValue); final short myValue; } ``` 当然,枚举类型也可以被用于数组 List。 ## 嵌套对象 在 Collection 中使用嵌套对象通常很有用。对象可以嵌套到任何深度。但是请记住,对一个嵌套对象进行修改需要将整个对象树写入数据库。 ```dart @collection class Email { Id? id; String? title; Recepient? recipient; } @embedded class Recepient { String? name; String? address; } ``` 嵌套对象可为空且可以扩展自其他对象,唯一的要求是需要添加 `@embedded` 注解,而且它们的构造器不能有参数。 ================================================ FILE: docs/docs/zh/transactions.md ================================================ --- title: 事务 --- # 事务(Transaction) 在 Isar 中,事务将多条数据库操作序列合并成单个逻辑单位。大多数与 Isar 的交互都隐式用到了事务。Isar 的读写操作是兼容 [ACID](http://en.wikipedia.org/wiki/ACID) 特性的。倘若错误发生,事务会自动回滚。 ## 显式事务 在显式事务中,你会得到连续的数据库快照。尝试缩短事务的持续时长。禁止在事务中访问网络或做其他需长时间运行的操作。 事务(特别是写入事务)的确有性能损耗,你应该尽可能将连续的操作序列并入到单一事务。 事务要么是同步的,要么是异步的。在同步事务中,你只能使用同步操作。类似地,在异步事务中只能使用异步操作。 | | 读取 | 读写 | | ---- | ------------ | ----------------- | | 同步 | `.txnSync()` | `.writeTxnSync()` | | 异步 | `.txn()` | `.writeTxn()` | ### 读取事务 显式的读取事务是可选的,但是它们可以让你进行原子化读取并且依赖于事务执行过程中数据库的一致性。对于所有的读取操作,Isar 内部总是使用隐式的读取事务。 :::tip 异步的读取事务和其他读写事务是并行运行的。很酷,对吧? ::: ### 写入事务 不同于读取事务,在 Isar 中必须显式使用写入事务。 当一个写入事务成功完成后,它会自动提交,将所有修改写入到磁盘。如果有错误发生,事务就会被终止,所有修改会被回滚。事务就是“应用所有修改或什么都不修改”:在一个成功执行的事务里执行所有修改,或什么都不修改来保证数据的一致性。 :::warning 当数据操作失败时,该事务会被终止。即使你在 Dart 中捕获到了错误,也不要再次使用该事务。 ::: ```dart @collection class Contact { Id? id; String? name; } // 良好 await isar.writeTxn(() async { for (var contact in getContacts()) { await isar.contacts.put(contact); } }); // 不好:要将循环放到事务里面 for (var contact in getContacts()) { await isar.writeTxn(() async { await isar.contacts.put(contact); }); } ``` ================================================ FILE: docs/docs/zh/tutorials/quickstart.md ================================================ --- title: 快速开始 --- # 快速开始 嗨,你可终于来啦!让我们开始使用 Flutter 生态中最酷的数据库吧... 废话不多说,让我们来看代码。 ## 1. 添加依赖 在开始之前,我们需要在 `pubspec.yaml` 文件中添加若干依赖,可以运行以下命令帮助我们完成: ```bash dart pub add isar:^0.0.0-placeholder isar_flutter_libs:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev dart pub add dev:isar_generator:^0.0.0-placeholder --hosted-url=https://pub.isar-community.dev ``` ## 2. 给类添加注解 用 `@collection` 给你的 Collection 类添加注解,并指定一个 `Id` 字段。 ```dart part 'user.g.dart'; @collection class User { Id id = Isar.autoIncrement; // 你也可以用 id = null 来表示 id 是自增的 String? name; int? age; } ``` Id 唯一指向了 Collection 中的对象,之后我们可通过 Id 来查询这些对象。 ## 3. 运行代码生成器 对于纯 Dart 项目,通过以下命令来执行 `build_runner`: ``` dart run build_runner build ``` 倘若你的项目用到了 Flutter,可用下方命令来代替: ``` flutter pub run build_runner build ``` ## 4. 创建一个 Isar 实例 创建一个新的 Isar 实例,并将你想保存到 Isar 的所有 collection 的 schema(它在上一步由 Isar Generator 根据你定义的 collection 自动生成) 作为参数传入。你还可以指定实例的名称以及它所存储数据的文件路径。 ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [UserSchema], directory: dir.path, ); ``` ## 5. 读写操作 当实例被创建后,我们就可以使用它了。 可以通过 `IsarCollection` 来调用所有 CRUD 方法。 ```dart final newUser = User()..name = 'Jane Doe'..age = 36; await isar.writeTxn(() async { await isar.users.put(newUser); // 将新用户数据写入到 Isar }); final existingUser = await isar.users.get(newUser.id); // 通过 Id 读取用户数据 await isar.writeTxn(() async { await isar.users.delete(existingUser.id!); // 通过 Id 删除指定用户 }); ``` ## 其他资源 你倾向于通过视频来学习?不妨查看下方一些资源吧:


================================================ FILE: docs/docs/zh/watchers.md ================================================ --- title: 观察者 --- # 观察者(Watcher) Isar 允许你订阅数据库中的变化。你可以“观察”单个对象、整个 Collection 或单个查询的变化。 观察者可以让你针对数据库中的变化高效地做出回应。例如,当一个联系人被添加之后,你可以重建 UI;或当一个文件被修改后,发送一个网络请求等等。 当事务成功被执行后,目标被修改,然后就会通知观察者。 ## 观察对象 如果你想在某个对象被创建、修改或删除时收到通知,你可以通过下面代码观察该对象: ```dart Stream userChanged = isar.users.watchObject(5); userChanged.listen((newUser) { print('User changed: ${newUser?.name}'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // 打印出:User changed: David final user2 = User(id: 5)..name = 'Mark'; await isar.users.put(user); // 打印出:User changed: Mark await isar.users.delete(5); // 打印出:User changed: null ``` 如你所见,声明观察者的时候,Id 为 5 的对象还未被创建。一旦被创建,观察者就会收到通知。 还有一个参数 `fireImmediately`,如果你设置为 `true`,Isar 会立刻将该对象的当前值添加到 Stream 中。 ### 懒观察 也许你不需要知道一个对象的最新值,只需了解其是否被修改过,那么可以用懒观察,这样 Isar 无需去读取该对象的值: ```dart Stream userChanged = isar.users.watchObjectLazy(5); userChanged.listen(() { print('User 5 changed'); }); final user = User(id: 5)..name = 'David'; await isar.users.put(user); // 打印出:User 5 changed ``` ## 观察 Collection 除了观察单个对象,你也可以观察整个 Collection 中是否有对象被添加、修改或删除: ```dart Stream userChanged = isar.users.watchLazy(); userChanged.listen(() { print('A User changed'); }); final user = User()..name = 'David'; await isar.users.put(user); // 打印出: A User changed ``` ## 观察查询 甚至你也可以观察整个查询的结果是否发生变化。Isar 尽力只在查询结果真正发生变化时通知你。但是当由关联造成查询结果发生变化时,你不会收到任何通知。针对关联变化,你可以观察 Collection。 ```dart Query usersWithA = isar.users.filter() .nameStartsWith('A') .build(); Stream> queryChanged = usersWithA.watch(fireImmediately: true); queryChanged.listen((users) { print('Users with A are: $users'); }); // 打印出:Users with A are: [] await isar.users.put(User()..name = 'Albert'); // 打印出:Users with A are: [User(name: Albert)] await isar.users.put(User()..name = 'Monika'); // 无任何打印输出 await isar.users.put(User()..name = 'Antonia'); // 打印出:Users with A are: [User(name: Albert), User(name: Antonia)] ``` :::warning 如果你的查询使用了偏移量和限制,或者进行了去重化,即使是在查询结果范围之外的对象,若符合该查询条件,Isar 也会通知你查询结果发生了变化。 ::: 就像观察对象的懒观察一样,你也可以使用 `watchLazy()` 来懒观察一条查询结果是否有变化,而无需去读取查询的结果。 :::danger 观察查询时返回每一个变动是十分低效的。尽量使用懒观察 Collection 来代替。 ::: ================================================ FILE: docs/package.json ================================================ { "name": "docs", "version": "1.0.0", "devDependencies": { "@vuepress/plugin-shiki": "^2.0.0-beta.53", "vuepress": "^2.0.0-beta.53" }, "scripts": { "dev": "vuepress dev docs", "build": "vuepress build docs" } } ================================================ FILE: examples/pub/.gitignore ================================================ *.g.dart android/ ios/ windows/ linux/ macos/ web/ ================================================ FILE: examples/pub/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: android create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: ios create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: linux create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: macos create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: web create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - platform: windows create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/pub/README.md ================================================ # pub Sample showcasing the use of Isar to build a fully offline-first pub.dev client. ================================================ FILE: examples/pub/analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml analyzer: errors: public_member_api_docs: ignore ================================================ FILE: examples/pub/lib/asset_loader.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:isar/isar.dart'; import 'package:pub_app/models/asset.dart'; import 'package:pub_app/models/package.dart'; import 'package:pub_app/repository.dart'; import 'package:tar/tar.dart'; class PackageAndVersion { PackageAndVersion(this.package, this.version); final String package; final String version; } Future loadAssets(PackageAndVersion p) async { final isar = Isar.openSync( [PackageSchema, AssetSchema], inspector: false, ); Asset? readme; Asset? changelog; final targz = await Repository(Dio()).downloadPackage(p.package, p.version); final tar = gzip.decode(targz); final reader = TarReader(Stream.value(tar)); while (await reader.moveNext()) { final entry = reader.current; if (entry.type == TypeFlag.reg) { if (readme == null && entry.name.toLowerCase() == 'readme.md') { final content = await entry.contents.transform(utf8.decoder).join(); readme = Asset( package: p.package, version: p.version, kind: AssetKind.readme, content: content, ); } else if (changelog == null && entry.name.toLowerCase() == 'changelog.md') { final content = await entry.contents.transform(utf8.decoder).join(); changelog = Asset( package: p.package, version: p.version, kind: AssetKind.changelog, content: content, ); } } if (readme != null && changelog != null) { break; } } if (readme != null || changelog != null) { isar.writeTxnSync(() { isar.assets.putAllSync([ if (readme != null) readme, if (changelog != null) changelog, ]); }); } } ================================================ FILE: examples/pub/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/ui/detail_page.dart'; import 'package:pub_app/ui/home_page.dart'; import 'package:pub_app/ui/search_page.dart'; void main() { runApp(ProviderScope(child: PubApp())); } final darkModePod = StateProvider((ref) => false); class PubApp extends ConsumerWidget { PubApp({super.key}); final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomePage(), ), GoRoute( path: '/packages/:package', builder: (context, state) => DetailPage( name: state.params['package']!, ), ), GoRoute( path: '/packages/:package/versions/:version', builder: (context, state) => DetailPage( name: state.params['package']!, version: state.params['version'], ), ), GoRoute( path: '/search/:query', builder: (context, state) => SearchPage( query: state.params['query']!, ), ), ], ); @override Widget build(BuildContext context, WidgetRef ref) { final darkMode = ref.watch(darkModePod); return MaterialApp.router( routeInformationProvider: _router.routeInformationProvider, routeInformationParser: _router.routeInformationParser, routerDelegate: _router.routerDelegate, title: 'Pub', theme: ThemeData.from( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF1c2834), brightness: darkMode ? Brightness.dark : Brightness.light, ), useMaterial3: true, ), ); } } ================================================ FILE: examples/pub/lib/models/api/metrics.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'metrics.g.dart'; @JsonSerializable(createToJson: false) class ApiPackageMetrics { ApiPackageMetrics({ required this.grantedPoints, required this.maxPoints, required this.likeCount, required this.popularityScore, required this.tags, }); factory ApiPackageMetrics.fromJson(Map json) => _$ApiPackageMetricsFromJson(json); final int grantedPoints; final int maxPoints; final int likeCount; final double popularityScore; final List tags; } ================================================ FILE: examples/pub/lib/models/api/package.dart ================================================ import 'package:json_annotation/json_annotation.dart'; import 'package:pubspec/pubspec.dart'; part 'package.g.dart'; @JsonSerializable(fieldRename: FieldRename.snake, createToJson: false) class ApiPackage { ApiPackage({ required this.name, required this.latest, this.versions = const [], }); factory ApiPackage.fromJson(Map json) => _$ApiPackageFromJson(json); final String name; final ApiPackageVersion latest; final List versions; } @JsonSerializable(fieldRename: FieldRename.snake, createToJson: false) class ApiPackageVersion { ApiPackageVersion({ required this.version, required this.pubspec, required this.published, }); factory ApiPackageVersion.fromJson(Map json) => _$ApiPackageVersionFromJson(json); final String version; final PubSpec pubspec; final DateTime published; } ================================================ FILE: examples/pub/lib/models/asset.dart ================================================ import 'package:isar/isar.dart'; part 'asset.g.dart'; @collection class Asset { Asset({ required this.package, required this.version, required this.kind, required this.content, }) : id = Isar.autoIncrement; Id id; @Index( unique: true, replace: true, composite: [ CompositeIndex('version'), CompositeIndex('kind'), ], ) final String package; final String version; @enumerated final AssetKind kind; final String content; } enum AssetKind { readme, changelog } ================================================ FILE: examples/pub/lib/models/package.dart ================================================ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:isar/isar.dart'; import 'package:pub_app/models/api/metrics.dart'; import 'package:pub_app/models/api/package.dart'; import 'package:pubspec/pubspec.dart'; part 'package.g.dart'; @CopyWith() @collection class Package { Package({ required this.name, required this.version, required this.isLatest, this.homepage, this.documentation, this.description, required this.dependencies, required this.devDependencies, required this.published, this.points, this.likes, this.popularity, this.publisher, this.dart, this.flutter, this.flutterFavorite, this.license, this.osiLicense, this.platforms, }) : id = Isar.autoIncrement; final Id id; @Index(unique: true, replace: true, composite: [CompositeIndex('version')]) final String name; final String version; final bool isLatest; final String? description; final String? homepage; final String? documentation; final List dependencies; final List devDependencies; final DateTime published; final short? points; final short? likes; final float? popularity; final String? publisher; final bool? dart; final bool? flutter; final bool? flutterFavorite; final String? license; final bool? osiLicense; @enumerated final List? platforms; static List fromApiPackage(ApiPackage package) { final latestVersion = package.latest.version; final versions = []; for (final p in package.versions) { versions.add( Package( name: package.name, version: p.version, isLatest: p.version == latestVersion, homepage: p.pubspec.homepage, documentation: p.pubspec.documentation, description: p.pubspec.description, dependencies: Dependency.fromDependencies(p.pubspec.dependencies), devDependencies: Dependency.fromDependencies(p.pubspec.devDependencies), published: p.published, ), ); } return versions; } Package copyWithMetrics(ApiPackageMetrics metrics) { final publishers = metrics.tags.where((t) => t.startsWith('publisher:')).toList(); final publisher = publishers.isNotEmpty ? publishers.first.substring(10) : null; return copyWith( points: metrics.grantedPoints, likes: metrics.likeCount, popularity: metrics.popularityScore, publisher: publisher, dart: metrics.tags.contains('sdk:dart'), flutter: metrics.tags.contains('sdk:flutter'), flutterFavorite: metrics.tags.contains('is:flutter-favorite'), license: metrics.tags .firstWhere( (e) => e.startsWith('license:') && e != 'license:osi-approved' && e != 'license:fsf-libre', orElse: () => 'license:unknown', ) .substring(8) .toUpperCase(), osiLicense: metrics.tags.contains('license:osi-approved'), platforms: [ if (metrics.tags.contains('platform:web')) SupportedPlatform.web, if (metrics.tags.contains('platform:android')) SupportedPlatform.android, if (metrics.tags.contains('platform:ios')) SupportedPlatform.ios, if (metrics.tags.contains('platform:linux')) SupportedPlatform.linux, if (metrics.tags.contains('platform:macos')) SupportedPlatform.macos, if (metrics.tags.contains('platform:windows')) SupportedPlatform.windows, ], ); } } @embedded class Dependency { Dependency({this.name = 'unknown', this.constraint = 'any'}); final String name; final String constraint; static List fromDependencies( Map dependenciesMap, ) { final dependencies = []; for (final package in dependenciesMap.keys) { final dep = dependenciesMap[package]!; final constraint = dep is HostedReference ? dep.versionConstraint.toString() : 'unknown'; dependencies.add( Dependency( name: package, constraint: constraint, ), ); } return dependencies; } } enum SupportedPlatform { android('Android'), ios('iOS'), linux('Linux'), windows('Windows'), macos('macOS'), web('Web'); const SupportedPlatform(this.name); final String name; } ================================================ FILE: examples/pub/lib/package_manager.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:pub_app/asset_loader.dart'; import 'package:pub_app/models/asset.dart'; import 'package:pub_app/models/package.dart'; import 'package:pub_app/repository.dart'; class PackageManager { const PackageManager(this.isar, this.repository); final Isar isar; final Repository repository; Stream watchPackage(String name, {String? version}) async* { final query = isar.packages .where() .nameEqualToAnyVersion(name) .filter() .optional(version == null, (q) => q.isLatestEqualTo(true)) .optional(version != null, (q) => q.versionEqualTo(version!)) .build(); await for (final results in query.watch(fireImmediately: true)) { if (results.isNotEmpty) { yield results.first; } } } Stream> watchPackageVersions(String name) async* { final query = isar.packages .where() .nameEqualToAnyVersion(name) .sortByPublishedDesc() .build(); await for (final results in query.watch(fireImmediately: true)) { if (results.isNotEmpty) { yield results; } } } Stream watchLatestVersion(String name) async* { final query = isar.packages .where() .nameEqualToAnyVersion(name) .filter() .isLatestEqualTo(true) .versionProperty() .build(); await for (final results in query.watch(fireImmediately: true)) { if (results.isNotEmpty) { yield results.first; } } } Stream watchPreReleaseVersion(String name) async* { await for (final _ in isar.packages.watchLazy(fireImmediately: true)) { final latestDate = await isar.packages .where() .nameEqualToAnyVersion(name) .filter() .isLatestEqualTo(true) .publishedProperty() .findFirst(); if (latestDate != null) { yield await isar.packages .where() .nameEqualToAnyVersion(name) .filter() .publishedGreaterThan(latestDate) .sortByPublishedDesc() .versionProperty() .findFirst(); } } } Future loadPackage( String name, { bool loadMetrics = false, String? version, }) async { final newPackageVersions = await repository.getPackageVersions(name); final latestExistingDate = await isar.packages .where() .nameEqualToAnyVersion(name) .publishedProperty() .max(); final versionsToAdd = newPackageVersions .where( (e) => e.published.millisecondsSinceEpoch > (latestExistingDate?.millisecondsSinceEpoch ?? 0), ) .toList(); final currentLatest = await isar.packages .where() .nameEqualToAnyVersion(name) .filter() .isLatestEqualTo(true) .findFirst(); final newLatestVersion = newPackageVersions.firstWhere((e) => e.isLatest).version; if (currentLatest != null && currentLatest.version != newLatestVersion) { versionsToAdd.add(currentLatest.copyWith(isLatest: false)); } if (loadMetrics) { version ??= newLatestVersion; final metrics = await repository.getPackageMetrics(name, version); final package = newPackageVersions .firstWhere((e) => e.version == version) .copyWithMetrics(metrics); versionsToAdd.add(package); } await isar.writeTxn(() async { await isar.packages.putAll(versionsToAdd); }); } Stream> watchPackageAssets( String name, String version, ) async* { final query = isar.assets.where().packageVersionEqualToAnyKind(name, version).build(); final existing = await query.findAll(); if (existing.isNotEmpty) { yield { for (final asset in existing) asset.kind: asset.content, }; } else { final existingAnyVersion = await isar.assets .where() .packageEqualToAnyVersionKind(name) .sortByVersionDesc() .findAll(); if (existingAnyVersion.isNotEmpty) { final assets = {}; for (final asset in existingAnyVersion) { if (!assets.containsKey(asset.kind)) { assets[asset.kind] = asset.content; } } yield assets; } } await for (final results in query.watch()) { if (results.isNotEmpty) { yield { for (final asset in results) asset.kind: asset.content, }; } } } Future loadPackageAssets(String name, String version) { return compute(loadAssets, PackageAndVersion(name, version)); } Future> search(String query, int page, {bool online = true}) { if (online) { return repository.search(query, page + 1); } else { return isar.packages .filter() .nameContains(query, caseSensitive: false) .or() .descriptionContains(query, caseSensitive: false) .sortByLikesDesc() .distinctByName() .offset(page * 10) .limit(10) .nameProperty() .findAll(); } } Future bulkLoad(String query) async { var page = 0; while (true) { final packageNames = await search(query, page); if (packageNames.isEmpty) { break; } await Future.wait( packageNames.map((e) => loadPackage(e, loadMetrics: true)), ); page++; } } Stream> watchFavoriteNames() { return isar.packages .filter() .flutterFavoriteEqualTo(true) .distinctByName() .nameProperty() .watch(fireImmediately: true); } } ================================================ FILE: examples/pub/lib/provider.dart ================================================ // ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes import 'dart:async'; import 'package:dio/dio.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pub_app/models/asset.dart'; import 'package:pub_app/models/package.dart'; import 'package:pub_app/package_manager.dart'; import 'package:pub_app/repository.dart'; import 'package:riverpod/riverpod.dart'; final isarPod = FutureProvider((ref) async { final dir = await getApplicationDocumentsDirectory(); return Isar.open([PackageSchema, AssetSchema], directory: dir.path); }); final repositoryPod = Provider((ref) { return Repository(Dio()); }); final packageManagerPod = FutureProvider((ref) async { final isar = await ref.watch(isarPod.future); final repository = ref.watch(repositoryPod); return PackageManager(isar, repository); }); final freshPackagePod = StreamProvider.family((ref, package) async* { final manager = await ref.watch(packageManagerPod.future); unawaited( manager.loadPackage( package.name, version: package.version, loadMetrics: true, ), ); yield* manager.watchPackage(package.name, version: package.version); }); final packagePod = StreamProvider.family((ref, package) async* { final manager = await ref.watch(packageManagerPod.future); yield* manager.watchPackage(package.name, version: package.version); }); final packageVersionsPod = StreamProvider.family, String>((ref, package) async* { final manager = await ref.watch(packageManagerPod.future); yield* manager.watchPackageVersions(package); }); final latestVersionPod = StreamProvider.family((ref, name) async* { final manager = await ref.watch(packageManagerPod.future); yield* manager.watchLatestVersion(name); }); final preReleaseVersionPod = StreamProvider.family((ref, name) async* { final manager = await ref.watch(packageManagerPod.future); yield* manager.watchPreReleaseVersion(name); }); final assetsPod = StreamProvider.family, PackageNameVersion>( (ref, package) async* { final manager = await ref.watch(packageManagerPod.future); unawaited(manager.loadPackageAssets(package.name, package.version!)); yield* manager.watchPackageAssets(package.name, package.version!); }); class PackageNameVersion { const PackageNameVersion(this.name, [this.version]); final String name; final String? version; @override int get hashCode => Object.hash(name, version); @override bool operator ==(Object other) => other is PackageNameVersion && name == other.name && version == other.version; } class QueryPage { const QueryPage(this.query, this.page); final String query; final int page; @override int get hashCode => Object.hash(query, page); @override bool operator ==(Object other) => other is QueryPage && query == other.query && page == other.page; } ================================================ FILE: examples/pub/lib/repository.dart ================================================ // ignore_for_file: avoid_dynamic_calls import 'package:dio/dio.dart'; import 'package:pub_app/models/api/metrics.dart'; import 'package:pub_app/models/api/package.dart'; import 'package:pub_app/models/package.dart'; const _api = 'https://pub.dev/api'; class Repository { Repository(this.dio); final Dio dio; Future> getPackageVersions(String name) async { final response = await dio.get>('$_api/packages/$name'); final package = ApiPackage.fromJson(response.data!); return Package.fromApiPackage(package); } Future getPackageMetrics( String name, String version, ) async { final response = await dio.get>( '$_api/packages/$name/versions/$version/score', ); return ApiPackageMetrics.fromJson(response.data!); } Future> downloadPackage(String name, String version) async { final response = await dio.get>( '$_api/packages/$name/versions/$version/archive.tar.gz', options: Options(responseType: ResponseType.bytes), ); return response.data!; } Future> search(String query, int page) async { final response = await dio.get>( '$_api/search', queryParameters: { 'q': query, 'page': page, }, ); return (response.data!['packages'] as List) .map((e) => e['package'] as String) .toList(); } } ================================================ FILE: examples/pub/lib/ui/app_bar.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/main.dart'; import 'package:url_launcher/url_launcher.dart'; class PubAppBar extends ConsumerWidget implements PreferredSizeWidget { const PubAppBar({super.key, this.favorite = false}); final bool favorite; @override Widget build(BuildContext context, WidgetRef ref) { final darkMode = ref.watch(darkModePod); return AppBar( automaticallyImplyLeading: false, title: GestureDetector( onTap: () { context.go('/'); }, child: AnimatedCrossFade( firstChild: SvgPicture.asset('assets/pub_logo_dark.svg', width: 150), secondChild: SvgPicture.asset('assets/pub_logo.svg', width: 150), crossFadeState: darkMode ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: kThemeChangeDuration, ), ), centerTitle: false, actions: [ if (favorite) Center( child: ElevatedButton.icon( onPressed: () { launchUrl( Uri.parse( 'https://docs.flutter.dev/development/packages-and-plugins/favorites', ), ); }, icon: const FlutterLogo(), label: const Text('Flutter Favorite'), ), ), IconButton( icon: Icon( darkMode ? Icons.light_mode_rounded : Icons.dark_mode_rounded, ), onPressed: () { ref.read(darkModePod.notifier).state = !darkMode; }, ), ], ); } @override Size get preferredSize => AppBar().preferredSize; } ================================================ FILE: examples/pub/lib/ui/detail_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pub_app/models/asset.dart'; import 'package:pub_app/models/package.dart'; import 'package:pub_app/provider.dart'; import 'package:pub_app/ui/app_bar.dart'; import 'package:pub_app/ui/markdown_viewer.dart'; import 'package:pub_app/ui/package_metadata.dart'; import 'package:pub_app/ui/package_versions.dart'; class DetailPage extends ConsumerWidget { const DetailPage({ super.key, required this.name, this.version, }); final String name; final String? version; @override Widget build(BuildContext context, WidgetRef ref) { final package = ref.watch(freshPackagePod(PackageNameVersion(name, version))); return Scaffold( appBar: PubAppBar( favorite: package.valueOrNull?.flutterFavorite ?? false, ), body: SingleChildScrollView( child: package.map( data: (data) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: PackageHeader(package: data.value), ), const SizedBox(height: 20), PackageBody(package: data.value), ], ); }, error: (err) { return Center(child: Text('Error: $err')); }, loading: (loading) => const Center( child: CircularProgressIndicator(), ), ), ), ); } } class PackageBody extends StatefulWidget { const PackageBody({super.key, required this.package}); final Package package; @override State createState() => _PackageBodyState(); } class _PackageBodyState extends State { _BodyPage page = _BodyPage.readme; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ MediaQuery.removePadding( removeBottom: true, context: context, child: NavigationBar( height: 60, labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, selectedIndex: page.index, destinations: const [ NavigationDestination( icon: Icon(Icons.description_rounded), label: 'Readme', ), NavigationDestination( icon: Icon(Icons.change_history_rounded), label: 'Changelog', ), /*NavigationDestination( icon: Icon(Icons.adjust), label: 'Example', ),*/ NavigationDestination( icon: Icon(Icons.list_alt_rounded), label: 'Versions', ), ], onDestinationSelected: (value) { setState(() { page = _BodyPage.values[value]; }); }, ), ), const SizedBox(height: 20), if (page == _BodyPage.readme) Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: PackageAsset( name: widget.package.name, version: widget.package.version, kind: AssetKind.readme, ), ) else if (page == _BodyPage.changelog) Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: PackageAsset( name: widget.package.name, version: widget.package.version, kind: AssetKind.changelog, ), ) else if (page == _BodyPage.versions) Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: PackageVersions(name: widget.package.name), ), ], ); } } enum _BodyPage { readme, changelog, //example, versions; } class PackageAsset extends ConsumerWidget { const PackageAsset({ super.key, required this.name, required this.version, required this.kind, }); final String name; final String version; final AssetKind kind; @override Widget build(BuildContext context, WidgetRef ref) { final assets = ref.watch(assetsPod(PackageNameVersion(name, version))); return assets.map( data: (data) { final md = data.value[kind]; if (md != null) { return MarkdownViewer(markdown: md); } else { return const Center( child: Text('This file is empty.'), ); } }, error: (err) => Center(child: Text('Error: $err')), loading: (loading) => const Center(child: CircularProgressIndicator()), ); } } ================================================ FILE: examples/pub/lib/ui/home_page.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/provider.dart'; import 'package:pub_app/ui/publisher.dart'; import 'package:pub_app/ui/search.dart'; class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @override ConsumerState createState() => _HomePageState(); } class _HomePageState extends ConsumerState { static bool refreshed = false; @override void initState() { super.initState(); if (!refreshed) { ref .read(packageManagerPod.future) .then((pm) => pm.bulkLoad('is:flutter-favorite')); refreshed = true; } } @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( physics: const ClampingScrollPhysics(), child: SafeArea( top: false, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 230, child: Search( onSearch: (query) { context.go('/search/$query'); }, ), ), const Favorites(), ], ), ), ), ); } } final randomFavoriteNamesPod = StreamProvider((ref) async* { final manager = await ref.watch(packageManagerPod.future); Set? previousNames; await for (final packageNames in manager.watchFavoriteNames()) { if (packageNames.isNotEmpty && (previousNames == null || !setEquals(packageNames.toSet(), previousNames))) { previousNames = packageNames.toSet(); packageNames.shuffle(); yield packageNames.sublist(0, 10); } } }); class Favorites extends ConsumerWidget { const Favorites({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final favoriteNames = ref.watch(randomFavoriteNamesPod).valueOrNull; return Padding( padding: const EdgeInsets.only(left: 20, right: 20, top: 25), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Flutter Favorites', style: theme.textTheme.headline3!.copyWith( color: theme.colorScheme.primary, ), ), Text( 'Some of the packages that demonstrate the highest levels ' 'of quality, selected by the Flutter Ecosystem Committee', style: theme.textTheme.subtitle1, ), if (favoriteNames != null) ...[ const SizedBox(height: 15), for (final name in favoriteNames) Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: PackageCard(name: name), ), ], ], ), ); } } class PackageCard extends ConsumerWidget { const PackageCard({super.key, required this.name}); final String name; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final package = ref.watch(packagePod(PackageNameVersion(name))).valueOrNull; return Card( margin: EdgeInsets.zero, clipBehavior: Clip.antiAlias, child: Material( color: Colors.transparent, child: InkWell( onTap: () { context.push('/packages/$name'); }, child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: theme.textTheme.headline5!.copyWith( color: theme.colorScheme.onPrimaryContainer, ), ), if (package?.description != null) ...[ const SizedBox(height: 5), Text( package!.description!.trim(), style: theme.textTheme.bodyText2, ), ], if (package?.publisher != null) ...[ const SizedBox(height: 5), Publisher(package!.publisher!), ], ], ), ), ), ), ); } } ================================================ FILE: examples/pub/lib/ui/markdown_viewer.dart ================================================ import 'package:clickup_fading_scroll/clickup_fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html_svg/flutter_html_svg.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:markdown/markdown.dart' as md; import 'package:url_launcher/url_launcher_string.dart'; final _markdownHtmlPod = Provider.family((ref, source) { return md.markdownToHtml( source, extensionSet: md.ExtensionSet.gitHubWeb, ); }); class MarkdownViewer extends ConsumerWidget { const MarkdownViewer({super.key, required this.markdown}); final String markdown; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final html = ref.read(_markdownHtmlPod(markdown)); return Html( data: html, onLinkTap: (url, context, attributes, element) { if (url != null) { launchUrlString(url); } }, customRenders: { svgTagMatcher(): svgTagRender(), svgDataUriMatcher(): svgDataImageRender(), svgAssetUriMatcher(): svgAssetImageRender(), svgNetworkSourceMatcher(): svgNetworkImageRender(), tagMatcher('code'): CustomRender.widget( widget: (context, children) { final code = context.tree.element!.text; final codeBgColor = theme.colorScheme.secondaryContainer.withOpacity(0.25); if (code.contains('\n')) { return FadingScroll( builder: (context, controller) { return SingleChildScrollView( scrollDirection: Axis.horizontal, controller: controller, child: Container( padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: codeBgColor, borderRadius: BorderRadius.circular(8), ), child: SelectableText( code.trim(), style: GoogleFonts.jetBrainsMono( color: theme.colorScheme.onSecondaryContainer, ), ), ), ); }, ); } else { return SelectableText( code.trim(), style: GoogleFonts.jetBrainsMono( backgroundColor: codeBgColor, color: theme.colorScheme.onSecondaryContainer, ), ); } }, ), tagMatcher('h1'): CustomRender.widget( widget: (context, children) { return Container( width: double.infinity, padding: const EdgeInsets.only(bottom: 5), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: theme.dividerColor), ), ), child: Text( context.tree.element!.text, style: context.tree.style.generateTextStyle(), ), ); }, ), tagMatcher('h2'): CustomRender.widget( widget: (context, children) { return Container( width: double.infinity, padding: const EdgeInsets.only(bottom: 5), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: theme.dividerColor), ), ), child: Text( context.tree.element!.text, style: context.tree.style.generateTextStyle(), ), ); }, ), }, ); } } ================================================ FILE: examples/pub/lib/ui/package_metadata.dart ================================================ import 'package:clickup_fading_scroll/clickup_fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/models/package.dart'; import 'package:pub_app/provider.dart'; import 'package:pub_app/ui/publisher.dart'; import 'package:timeago/timeago.dart' as timeago; class PackageHeader extends ConsumerWidget { const PackageHeader({super.key, required this.package}); final Package package; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 20), Text( '${package.name} ${package.version}', style: theme.textTheme.headline5, ), const SizedBox(height: 3), Wrap( children: [ Text( timeago.format(package.published), style: theme.textTheme.subtitle2, ), if (package.publisher != null) ...[ Text( ' • ', style: theme.textTheme.subtitle2, ), Publisher(package.publisher!), ], ], ), const SizedBox(height: 15), Scores(package: package), const SizedBox(height: 15), Platforms(package: package), if (package.description != null) ...[ const SizedBox(height: 15), Text( package.description!.trim(), style: theme.textTheme.bodyText2!.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.9), ), ), ] ], ); } } class Platforms extends StatelessWidget { const Platforms({super.key, required this.package, this.compact = false}); final Package package; final bool compact; @override Widget build(BuildContext context) { final theme = Theme.of(context); final platforms = package.platforms?.map((e) => e.name).toList()?..sort(); final sdks = [ if (package.dart == true) 'DART', if (package.flutter == true) 'FLUTTER' ]; return Wrap( spacing: 5, runSpacing: 5, children: [ if (sdks.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(7), ), child: Text( sdks.join(' • '), style: theme.textTheme.subtitle2!.copyWith( fontSize: compact ? 9 : 11, color: theme.colorScheme.onPrimaryContainer, ), ), ), if (platforms?.isEmpty == false) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(7), ), child: Text( platforms!.join(' • ').toUpperCase(), style: theme.textTheme.subtitle2!.copyWith( fontSize: compact ? 9 : 11, color: theme.colorScheme.onPrimaryContainer, ), ), ), ], ); } } class Scores extends ConsumerWidget { const Scores({ super.key, required this.package, this.alwaysShowLatest = false, }); final Package package; final bool alwaysShowLatest; @override Widget build(BuildContext context, WidgetRef ref) { final latestVersion = ref.watch(latestVersionPod(package.name)).valueOrNull; final preReleaseVersion = ref.watch(preReleaseVersionPod(package.name)).valueOrNull; final widgets = [ if (package.likes != null) ScoreItem( stat: package.likes.toString(), title: 'LIKES', ), if (package.points != null) ScoreItem( stat: package.points.toString(), title: 'PUB POINTS', ), if (package.popularity != null) ScoreItem( stat: '${(package.popularity! * 100).round()}%', title: 'POPULARITY', ), if (latestVersion != null && (latestVersion != package.version || alwaysShowLatest)) ScoreItem( stat: latestVersion, title: 'LATEST', onTap: () { context.push('/packages/${package.name}'); }, ), if (preReleaseVersion != null) ScoreItem( stat: preReleaseVersion, title: 'PRERELEASE', onTap: () { context .push('/packages/${package.name}/versions/$preReleaseVersion'); }, ), ]; return FadingScroll( builder: (context, controller) { return SingleChildScrollView( scrollDirection: Axis.horizontal, controller: controller, child: IntrinsicHeight( child: Row( //crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < widgets.length; i++) ...[ if (i != 0) const VerticalDivider(thickness: 1, width: 0), widgets[i], ] ], ), ), ); }, ); } } class ScoreItem extends StatelessWidget { const ScoreItem({ super.key, required this.stat, required this.title, this.onTap, }); final String stat; final String title; final VoidCallback? onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( stat, style: theme.textTheme.titleMedium!.copyWith( color: theme.colorScheme.primary, ), ), Text( title, style: theme.textTheme.labelSmall!.copyWith( fontSize: 9, color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ), ); } } ================================================ FILE: examples/pub/lib/ui/package_versions.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/provider.dart'; import 'package:timeago/timeago.dart' as timeago; class PackageVersions extends ConsumerWidget { const PackageVersions({super.key, required this.name}); final String name; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final versions = ref.watch(packageVersionsPod(name)).valueOrNull ?? []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (final version in versions) ...[ InkWell( onTap: () { context.push( '/packages/$name/versions/${Uri.encodeComponent(version.version)}', ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Text( version.version, style: theme.textTheme.headline5!.copyWith( color: theme.colorScheme.primary, ), ), const Spacer(), Text(timeago.format(version.published)) ], ), ), ), ), const Divider(), ] ], ); } } ================================================ FILE: examples/pub/lib/ui/publisher.dart ================================================ import 'package:flutter/material.dart'; class Publisher extends StatelessWidget { const Publisher(this.publisher, {super.key}); final String publisher; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.verified_outlined, size: 16, ), const SizedBox(width: 2), Text( publisher, style: theme.textTheme.subtitle2! .copyWith(color: theme.colorScheme.primary), ), ], ); } } ================================================ FILE: examples/pub/lib/ui/search.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; class Search extends StatefulWidget { const Search({super.key, required this.onSearch, this.query}); final String? query; final void Function(String query) onSearch; @override State createState() => _SearchState(); } class _SearchState extends State { late final textController = TextEditingController(text: widget.query); @override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( child: SvgPicture.asset( 'assets/search_bg.svg', fit: BoxFit.cover, ), ), Positioned.fill( child: SafeArea( bottom: false, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { context.go('/'); }, child: SvgPicture.asset( 'assets/pub_logo_dark.svg', width: 250, ), ), const SizedBox(height: 20), Container( margin: const EdgeInsets.symmetric( vertical: 15, horizontal: 40, ), decoration: const BoxDecoration( color: Color(0xff35404d), borderRadius: BorderRadius.all(Radius.circular(50)), ), child: Padding( padding: const EdgeInsets.only(left: 10), child: Row( children: [ const Icon(Icons.search, color: Colors.grey), const SizedBox(width: 10), Expanded( child: Center( child: TextField( controller: textController, style: const TextStyle(color: Colors.white), decoration: InputDecoration( border: InputBorder.none, hintText: 'Search packages', hintStyle: TextStyle( color: Colors.grey.shade400, ), ), onSubmitted: (query) { if (query.isNotEmpty) { widget.onSearch(query); } }, onEditingComplete: () { FocusManager.instance.primaryFocus?.unfocus(); }, ), ), ), ], ), ), ), ], ), ), ), ) ], ); } } ================================================ FILE: examples/pub/lib/ui/search_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:pub_app/provider.dart'; import 'package:pub_app/ui/package_metadata.dart'; import 'package:pub_app/ui/search.dart'; class SearchPage extends ConsumerStatefulWidget { const SearchPage({super.key, required this.query}); final String query; @override ConsumerState createState() => _SearchPageState(); } class _SearchPageState extends ConsumerState { final controller = ScrollController(); final List packages = []; late String query = widget.query; bool loading = false; bool online = true; @override void initState() { super.initState(); controller.addListener(() { if (controller.position.extentAfter < 500 && !loading) { _loadMore(); } }); _loadMore(); } @override Widget build(BuildContext context) { return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 230, child: Search( query: widget.query, onSearch: (q) { setState(() { query = q; packages.clear(); }); _loadMore(); }, ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Center( child: ChoiceChip( label: Text(online ? 'Online' : 'Offline'), selected: online, onSelected: (value) { setState(() { online = value; packages.clear(); }); _loadMore(); }, ), ), ), if (packages.isNotEmpty) Expanded( child: ListView.builder( controller: controller, itemBuilder: (context, index) { return SearchResult(name: packages[index]); }, itemCount: packages.length, ), ) else if (loading) const Expanded( child: Center(child: CircularProgressIndicator()), ) else const Expanded(child: Center(child: Text('No Results'))), ], ), ); } Future _loadMore() async { try { loading = true; final manager = await ref.read(packageManagerPod.future); final newPackages = await manager.search(query, packages.length ~/ 10, online: online); if (mounted) { setState(() { packages.addAll(newPackages); }); } } finally { loading = false; } } } class SearchResult extends ConsumerWidget { const SearchResult({super.key, required this.name}); final String name; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final package = ref.watch(freshPackagePod(PackageNameVersion(name))).valueOrNull; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ InkWell( onTap: () { context.push('/packages/$name'); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: theme.textTheme.headline6), if (package?.description != null) ...[ const SizedBox(height: 4), Text( package!.description!, style: theme.textTheme.bodyText2!.copyWith(fontSize: 13), maxLines: 4, ), ], if (package != null) ...[ const SizedBox(height: 12), Scores( package: package, alwaysShowLatest: true, ), ], if (package?.platforms?.isEmpty == false || package?.flutter == true || package?.dart == true) ...[ const SizedBox(height: 12), Platforms(package: package!, compact: true), ] ], ), ), ), const Divider(), ], ); } } ================================================ FILE: examples/pub/pubspec.yaml ================================================ name: pub_app description: A new Flutter project. publish_to: "none" version: 1.0.0+1 environment: sdk: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" isar_version: &isar_version 3.0.2 # define the version to be used dependencies: auto_size_text: ^3.0.0 clickup_fading_scroll: git: url: https://github.com/clickup/clickup_fading_scroll.git copy_with_extension: ^4.0.3 dio: ^4.0.0 flutter: sdk: flutter flutter_riverpod: ^2.0.0 google_fonts: ^3.0.1 isar: *isar_version isar_flutter_libs: *isar_version json_annotation: ^4.6.0 markdown: ^6.0.0 pub_semver: ^2.1.1 pubspec: ^2.3.0 pubspec_parse: ^1.2.1 riverpod: ^2.0.0 shimmer: ^2.0.0 tar: ^0.5.6 timeago: ^3.3.0 url_launcher: ^6.1.5 go_router: ^5.0.0 flutter_svg: ^1.1.5 flutter_html: ^3.0.0-alpha.6 flutter_html_svg: ^3.0.0-alpha.4 dev_dependencies: build_runner: ^2.0.0 copy_with_extension_gen: ^4.0.3 isar_generator: *isar_version json_serializable: ^6.3.0 flutter_lints: ^2.0.1 flutter: uses-material-design: true assets: - assets/ff_banner.png - assets/pub_logo.svg - assets/pub_logo_dark.svg - assets/search_bg.svg ================================================ FILE: packages/isar/.gitignore ================================================ *.g.dart ================================================ FILE: packages/isar/CHANGELOG.md ================================================ ## 3.1.8 ### Fixes Fix Android release build on Flutter 3.24.0 ## 3.1.7 ### Fixes Add apple privacy manifest ## 3.1.6 ### Breaking Sorry, but we had to shift package hosting to https://pub.isar-community.dev ## 3.1.4 - Fix inspector URL ## 3.1.3 - Maintenance release, mainly adapts build dependencies ## 3.1.0+1 ### Fixes - Fixed error building MacOS library ## 3.1.0 ### Breaking Sorry for this breaking change. Unfortunately, it was necessary to fix stability issues on Android. - `directory` is now required for `Isar.open()` and `Isar.openSync()` ### Fixes - Fixed a crash that occasionally occurred when opening Isar - Fixed a schema migration issue - Fixed an issue where embedded class renaming didn't work correctly ### Enhancements - Many internal improvements - Performance improvements ## 3.0.6 ### Fixes - Add check to verify transactions are used for correct instance - Add check to verify that async transactions are still active - Fix upstream issue with opening databases ## 3.0.5 ### Enhancements - Improved performance for all operations - Added `maxSizeMiB` option to `Isar.open()` to specify the maximum size of the database file - Significantly reduced native library size - With the help of the community, the docs have been translated into a range of languages - Improved API docs - Added integration tests for more platforms to ensure high-quality releases - Support for unicode paths on Windows ### Fixes - Fixed crash while opening Isar - Fixed crash on older Android devices - Fixed a native port that was not closed correctly in some cases - Added swift version to podspec - Fixed crash on Windows - Fixed "IndexNotFound" error ## 3.0.4 REDACTED. ## 3.0.3 REDACTED. ## 3.0.2 ### Enhancements - The Inspector now supports creating objects and importing JSON - Added Inspector check to make sure Chrome is used ### Fixes - Added support for the latest analyzer - Fixed native ports that were not closed correctly in some cases - Added support for Ubuntu 18.04 and older - Fixed issue with aborting transactions - Fixed crash when invalid JSON was provided to `importJsonRaw()` - Added missing `exportJsonSync()` and `exportJsonRawSync()` - Fixed issue where secondary instance could not be selected in the Inspector ## 3.0.1 ### Enhancements - Support for arm64 iOS Simulators ### Fixes - Fixed issue where `.anyOf()`, `.allOf()`, and `.oneOf()` could not be negated - Fixed too low min-iOS version. The minimum supported is 11.0 - Fixed error during macOS App Store build ## 3.0.0 This release has been a lot of work! Thanks to everyone who contributed and joined the countless discussions. You are really awesome! Special thanks to [@Jtplouffe](https://github.com/Jtplouffe) and [@Peyman](https://github.com/Viper-Bit) for their incredible work. ### Web support This version does not support the web target yet. It will be back in the next version. Please continue using 2.5.0 if you need web support. ### Enhancements - Completely new Isar inspector that does not need to be installed anymore - Extreme performance improvements for almost all operations (up to 50%) - Support for embedded objects using `@embedded` - Support for enums using `@enumerated` - Vastly improved Isar binary format space efficiency resulting in about 20% smaller databases - Added `id`, `byte`, `short` and `float` typedefs - `IsarLinks` now support all `Set` methods based on the Isar `Id` of objects - Added `download` option to `Isar.initializeIsarCore()` to download binaries automatically - Added `replace` option for indexes - Added verification for correct Isar binary version - Added `collection.getSize()` and `collection.getSizeSync()` - Added `query.anyOf()` and `query.allOf()` query modifiers - Support for much more complex composite index queries - Support for logical XOR and the `.oneOf()` query modifier - Made providing a path optional - The default Isar name is now `default` and stored in `dir/name.isar` and `dir/name.isar.lock` - On non-web platforms, `IsarLink` and `IsarLinks` will load automatically - `.putSync()`, `.putAllSync()` etc. will now save links recursively by default - Added `isar.getSize()` and `isar.getSizeSync()` - Added `linksLengthEqualTo()`, `linksIsEmpty()`, `linksIsNotEmpty()`, `linksLengthGreaterThan()`, `linksLengthLessThan()`, `linksLengthBetween()` and `linkIsNull()` filters - Added `listLengthEqualTo()`, `listIsEmpty()`, `listIsNotEmpty()`, `listLengthGreaterThan()`, `listLengthLessThan()`, `listLengthBetween()` filters - Added `isNotNull()` filters - Added `compactOnLaunch` conditions to `Isar.open()` for automatic database compaction - Added `isar.copyToFile()` which copies a compacted version of the database to a path - Added check to verify that linked collections schemas are provided for opening an instance - Apply default values from constructor during deserialization - Added `isar.verify()` and `col.verify()` methods for checking database integrity in unit tests - Added missing float and double queries and an `epsilon` parameter ### Breaking changes - Removed `TypeConverter` support in favor of `@embedded` and `@enumerated` - Removed `@Id()` and `@Size32()` annotations in favor of the `Id` and `short` types - Changed the `schemas` parameter from named to positional - The maximum size of objects is now 16MB - Removed `replaceOnConflict` and `saveLinks` parameter from `collection.put()` and `collection.putAll()` - Removed `isar` parameter from `Isar.txn()`, `Isar.writeTxn()`, `Isar.txnSync()` and `Isar.writeTxnSync()` - Removed `query.repeat()` - Removed `query.sortById()` and `query.distinctById()` - Fixed `.or()` instead of `.and()` being used implicitly when combining filters - Renamed multi-entry where clauses from `.yourListAnyEqualTo()` to `.yourListElementEqualTo()` to avoid confusion - Isar will no longer create the provided directory. Make sure it exists before opening an Isar Instance. - Changed the default index type for all `List`s to `IndexType.hash` - Renamed `isar.getCollection()` to `isar.collection()` - It is no longer allowed to extend or implement another collection - Unsupported properties will no longer be ignored by default - Renamed the `initialReturn` parameter to `fireImmediately` - Renamed `Isar.initializeLibraries()` to `Isar.initializeIsarCore()` ### Fixes There are too many fixes to list them all. - A lot of link fixes and a slight behavior change to make them super reliable - Fixed missing symbols on older Android phones - Fixed composite queries - Fixed various generator issues - Fixed error retrieving the id property in a query - Fixed missing symbols on 32-bit Android 5 & 6 devices - Fixed inconsistent `null` handling in json export - Fixed default directory issue on Android - Fixed different where clauses returning duplicate results - Fixed hash index issue where multiple list values resulted in the same hash - Fixed edge case where creating a new index failed ## 2.5.0 ### Enhancements - Support for Android x86 (32 bit emulator) and macOS arm64 (Apple Silicon) - Greatly improved test coverage for sync methods - `col.clear()` now resets the auto increment counter to `0` - Significantly reduced Isar Core binary size (about 1.4MB -> 800KB) ### Minor Breaking - Changed `initializeLibraries(Map libraries)` to `initializeLibraries(Map libraries)` - Changed min Dart SDK to `2.16.0` ### Fixes - Fixed issue with `IsarLink.saveSync()` - Fixed `id` queries - Fixed error thrown by `BroadcastChannel` in Firefox - Fixed Isar Inspector connection issue ## 2.4.0 ### Enhancements - Support for querying links - Support for filtering and sorting links - Added methods to update and count links without loading them - Added `isLoaded` property to links - Added methods to count the number of objects in a collection - Big internal improvements ### Minor Breaking - There are now different kinds of where clauses for dynamic queries - `isar.getCollection()` no longer requires the name of the collection - `Isar.instanceNames` now returns a `Set` instead of a `List` ### Fixes - Fixed iOS crash that frequently happened on older devices - Fixed 32bit issue on Android - Fixed link issues - Fixed missing `BroadcastChannel` API for older Safari versions ## 2.2.1 ### Enhancements - Reduced Isar web code size by 50% - Made `directory` parameter of `Isar.open()` optional for web - Made `name` parameter of `Isar.getInstance()` optional - Added `Isar.defaultName` constant - Enabled `TypeConverter`s with supertypes - Added message if `TypeConverter` nullability doesn't match - Added more tests ### Fixes - Fixed issue with date queries - Fixed `FilterGroup.not` constructor (thanks for the PR @jtzell) ## 2.2.0 Isar now has full web support 🎉. No changes to your code required, just run it. _Web passes all unit tests but is still considered beta for now._ ### Minor Breaking - Added `saveLinks` parameter to `.put()` and `.putAll()` which defaults to `false` - Changed default `overrideChanges` parameter of `links.load()` to `true` to avoid unintended behavior ### Enhancements - Full web support! - Improved write performance - Added `deleteFromDisk` option to `isar.close()` - Added `.reset()` and `.resetSync()` methods to `IsarLink` and `IsarLinks` - Improved `links.save()` performance - Added many tests ### Fixed - Fixed value of `null` dates to be `DateTime.fromMillisecondsSinceEpoch(0)` - Fixed problem with migration - Fixed incorrect list values for new properties (`[]` instead of `null`) - Improved handling of link edge-cases ## 2.1.4 - Removed `path` dependency - Fixed incorrect return value of `deleteByIndex()` - Fixed wrong auto increment ids in some cases (thanks @robban112) - Fixed an issue with `Isar.close()` (thanks @msxenon) - Fixed `$` escaping in generated code (thanks @jtzell) - Fixed broken link in pub.dev example page ## 2.1.0 `isar_connect` is now integrated into `isar` ### Enhancements - Added check for outdated generated files - Added check for changed schema across isolates - Added `Isar.openSync()` - Added `col.importJsonRawSync()`, `col.importJsonSync()`, `query.exportJsonRawSync()`, `query.exportJsonSync()` - Improved performance for queries - Improved handling of ffi memory - More tests ### Fixed - Fixed issue where imported json required existing ids - Fixed issue with transaction handling (thanks @Peng-Qian for the awesome help) - Fixed issue with `@Ignore` annotation not always working - Fixed issue with `getByIndex()` not returning correct object id (thanks @jtzell) ## 2.0.0 ### Breaking - The id for non-final objects is now assigned automatically after `.put()` and `.putSync()` - `double` and `List` indexes can no longer be at the beginning of a composite index - `List` indexes can no longer be hashed - `.greaterThan()`, `.lessThan()` and `.between()` filters and are now excluding for `double` values (`>=` -> `>`) - Changed the default index type for lists to `IndexType.value` - `IsarLink` and `IsarLinks` will no longer be initialized by Isar and must not be `nullable` or `late`. - Dart `2.14` or higher is required ### Enhancements - Added API docs for all public methods - Added `isar.clear()`, `isar.clearSync()`, `col.clear()` and `col.clearSync()` - Added `col.filter()` as shortcut for `col.where().filter()` - Added `include` parameter to `.greaterThan()` and `.lessThan()` filters and where clauses - Added `includeLower` and `includeUpper` parameters to `.between()` filters and where clauses - Added `Isar.autoIncrement` to allow non-nullable auto-incrementing ids - `Isar.close()` now returns whether the last instance was closed - List values in composite indexes are now of type `IndexType.hash` automatically - Allowed multiple indexes on the same property - Removed exported packages from API docs - Improved generated code - Improved Isar Core error messages - Minor performance improvements - Automatic XCode configuration - Updated analyzer to `3.0.0` - More tests ### Fixed - `IsarLink` and `IsarLinks` can now be final - Fixed multi-entry index queries returning items multiple times in some cases - Fixed `.anyLessThan()` and `.anyGreaterThan()` issues - Fixed issues with backlinks - Fixed issue where query only returned the first `99999` results - Fixed issue with id where clauses - Fixed default index type for lists and bytes - Fixed issue where renaming indexes was not possible - Fixed issue where wrong index name was used for `.getByX()` and `.deleteByX()` - Fixed issue where composite indexes did not allow non-hashed Strings as last value - Fixed issue where `@Ignore()` fields were not ignored ## 1.0.5 ### Enhancements - Updated dependencies ### Fixes: - Included desktop binaries - Fixed "Cannot allocate memory" error on older iOS devices - Fixed stripped binaries for iOS release builds - Fixed IsarInspector issues (thanks to [RubenBez](https://github.com/RubenBez) and [rizzi37](https://github.com/rizzi37)) ## 1.0.0+1 Added missing binaries ## 1.0.0 Switched from liblmdb to libmdbx for better performance, more stability and many internal improvements. ### Breaking The internal database format has been changed to improve performance. Old databases do not work anymore! ### Fixes - Fix issue with links being removed after object update - Fix String index problems ### Enhancements - Support `greaterThan`, `lessThan` and `between` queries for String values - Support for inheritance (enabled by default) - Support for `final` properties and getters - Support for `freezed` and other code generators - Support getting / deleting objects by a key `col.deleteByName('Anne')` - Support for list indexes (hash an element based) - Generator now creates individual files instead of one big file - Allow specifying the collection accessor name - Unsupported properties are now ignored automatically - Returns the assigned ids after `.put()` operations (objects are no longer mutated) - Introduces `replaceOnConflict` option for `.put()` (instead of specifying it for index) - many more... ### Internal - Improve generated code - Many new unit tests ## 0.4.0 ### Breaking - Remove `.where...In()` and `...In()` extension methods - Split `.watch(lazy: bool)` into `.watch()` and `.watchLazy()` - Remove `include` option for filters ### Fixes - Generate id for JSON imports that don't have an id - Enable `sortBy` and `thenBy` generation ### Enhancements - Add `.optional()` and `.repeat()` query modifiers - Support property queries - Support query aggregation - Support dynamic queries (for custom query languages) - Support multi package configuration with `@ExternalCollection()` - Add `caseSensitive` option to `.distinctBy()` ### Internal - Change iOS linking - Improve generated code - Set up integration tests and improve unit tests - Use CORE/0.4.0 ## 0.2.0 - Link support - Many improvements and fixes ## 0.1.0 - Support for links and backlinks ## 0.0.4 - Bugfixes and many improvements ## 0.0.2 Fix dependency issue ## 0.0.1 Initial release ================================================ FILE: packages/isar/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2022 Simon Leier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: packages/isar/README.md ================================================ > 🚨 Please use the [renamed repository](https://github.com/isar-community/isar-community) (`isar-community/isar-community`) 🚨 > > This repository is no longer maintained and will not receive any further updates or fixes. > > The new repository will continue active development and is also published on [pub.dev/packages/isar_community](https://pub.dev/packages/isar_community) under the new package name. > > We strongly recommend migrating to the new repository for the latest improvements, bug fixes, and community support. > ⚠️ This repository is a fork of the [original project](https://github.com/isar/isar), focusing primarily on bug fixes and small updates for version 3. Our objective is to enhance the stability and reliability of the codebase while implementing minor improvements to refine the user experience. See details below on how to use this community fork.

Isar Database

QuickstartDocumentationSample AppsSupport & IdeasPub.dev

> #### Isar [ee-zahr]: > > 1. River in Bavaria, Germany. > 2. [Crazy fast](#benchmarks) NoSQL database that is a joy to use. ## Features - 💙 **Made for Flutter**. Easy to use, no config, no boilerplate - 🚀 **Highly scalable** The sky is the limit (pun intended) - 🍭 **Feature rich**. Composite & multi-entry indexes, query modifiers, JSON support etc. - ⏱ **Asynchronous**. Parallel query operations & multi-isolate support by default - 🦄 **Open source**. Everything is open source and free forever! Isar database can do much more (and we are just getting started) - 🕵️ **Full-text search**. Make searching fast and fun - 📱 **Multiplatform**. iOS, Android, Desktop - 🧪 **ACID semantics**. Rely on database consistency - 💃 **Static typing**. Compile-time checked and autocompleted queries - ✨ **Beautiful documentation**. Readable, easy to understand and ever-improving Join the [Telegram group](https://t.me/isardb) for discussion and sneak peeks of new versions of the DB. If you want to say thank you, star us on GitHub and like us on pub.dev 🙌💙 ## Quickstart Holy smokes you're here! Let's get started on using the coolest Flutter database out there... ### 1. Add to pubspec.yaml ```yaml isar_version: &isar_version 3.1.8 # define the version to be used dependencies: isar: version: *isar_version hosted: https://pub.isar-community.dev/ isar_flutter_libs: # contains Isar Core version: *isar_version hosted: https://pub.isar-community.dev/ dev_dependencies: isar_generator: version: *isar_version hosted: https://pub.isar-community.dev/ build_runner: any ``` ### 2. Annotate a Collection ```dart part 'email.g.dart'; @collection class Email { Id id = Isar.autoIncrement; // you can also use id = null to auto increment @Index(type: IndexType.value) String? title; List? recipients; @enumerated Status status = Status.pending; } @embedded class Recipient { String? name; String? address; } enum Status { draft, pending, sent, } ``` ### 3. Open a database instance ```dart final dir = await getApplicationDocumentsDirectory(); final isar = await Isar.open( [EmailSchema], directory: dir.path, ); ``` ### 4. Query the database ```dart final emails = await isar.emails.filter() .titleContains('awesome', caseSensitive: false) .sortByStatusDesc() .limit(10) .findAll(); ``` ## Isar Database Inspector The Isar Inspector allows you to inspect the Isar instances & collections of your app in real-time. You can execute queries, edit properties, switch between instances and sort the data. To launch the inspector, just run your Isar app in debug mode and open the Inspector link in the logs. ## CRUD operations All basic crud operations are available via the `IsarCollection`. ```dart final newEmail = Email()..title = 'Amazing new database'; await isar.writeTxn(() { await isar.emails.put(newEmail); // insert & update }); final existingEmail = await isar.emails.get(newEmail.id!); // get await isar.writeTxn(() { await isar.emails.delete(existingEmail.id!); // delete }); ``` ## Database Queries Isar database has a powerful query language that allows you to make use of your indexes, filter distinct objects, use complex `and()`, `or()` and `.xor()` groups, query links and sort the results. ```dart final importantEmails = isar.emails .where() .titleStartsWith('Important') // use index .limit(10) .findAll() final specificEmails = isar.emails .filter() .recipient((q) => q.nameEqualTo('David')) // query embedded objects .or() .titleMatches('*university*', caseSensitive: false) // title containing 'university' (case insensitive) .findAll() ``` ## Database Watchers With Isar database, you can watch collections, objects, or queries. A watcher is notified after a transaction commits successfully and the target actually changes. Watchers can be lazy and not reload the data or they can be non-lazy and fetch new results in the background. ```dart Stream collectionStream = isar.emails.watchLazy(); Stream> queryStream = importantEmails.watch(); queryStream.listen((newResult) { // do UI updates }) ``` ## Benchmarks Benchmarks only give a rough idea of the performance of a database but as you can see, Isar NoSQL database is quite fast 😇 | | | | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | | | If you are interested in more benchmarks or want to check how Isar performs on your device you can run the [benchmarks](https://github.com/isar-community/isar_benchmark) yourself. ## Unit tests If you want to use Isar database in unit tests or Dart code, call `await Isar.initializeIsarCore(download: true)` before using Isar in your tests. Isar NoSQL database will automatically download the correct binary for your platform. You can also pass a `libraries` map to adjust the download location for each platform. Make sure to use `flutter test -j 1` to avoid tests running in parallel. This would break the automatic download. ## Contributors ✨ Big thanks go to these wonderful people:

Alexis

Burak

Carlo Loguercio

Frostedfox

Hafeez Rana

Hamed H.

JT

Jack Rivers

Joachim Nohl

Johnson

LaLucid

Lety

Michael

Moseco

Nelson Mutane

Peyman

Simon Leier

Ura

blendthink

mnkeis

nobkd
### License ``` Copyright 2022 Simon Leier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: packages/isar/analysis_options.yaml ================================================ include: package:very_good_analysis/analysis_options.yaml analyzer: exclude: - "lib/src/native/bindings.dart" errors: cascade_invocations: ignore avoid_positional_boolean_parameters: ignore parameter_assignments: ignore prefer_asserts_with_message: ignore ================================================ FILE: packages/isar/example/README.md ================================================ ## The fastest way to get started is by following the [Quickstart Guide](https://isar.dev/tutorials/quickstart.html)! Have fun using Isar! ================================================ FILE: packages/isar/lib/isar.dart ================================================ library isar; import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:typed_data'; import 'package:isar/src/isar_connect_api.dart'; import 'package:isar/src/native/isar_core.dart' if (dart.library.html) 'package:isar/src/web/isar_web.dart'; import 'package:isar/src/native/isar_link_impl.dart' if (dart.library.html) 'package:isar/src/web/isar_link_impl.dart'; import 'package:isar/src/native/open.dart' if (dart.library.html) 'package:isar/src/web/open.dart'; import 'package:isar/src/native/split_words.dart' if (dart.library.html) 'package:isar/src/web/split_words.dart'; import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; part 'src/annotations/backlink.dart'; part 'src/annotations/collection.dart'; part 'src/annotations/embedded.dart'; part 'src/annotations/enumerated.dart'; part 'src/annotations/ignore.dart'; part 'src/annotations/index.dart'; part 'src/annotations/name.dart'; part 'src/annotations/type.dart'; part 'src/isar.dart'; part 'src/isar_collection.dart'; part 'src/isar_connect.dart'; part 'src/isar_error.dart'; part 'src/isar_link.dart'; part 'src/isar_reader.dart'; part 'src/isar_writer.dart'; part 'src/query.dart'; part 'src/query_builder.dart'; part 'src/query_builder_extensions.dart'; part 'src/query_components.dart'; part 'src/schema/collection_schema.dart'; part 'src/schema/index_schema.dart'; part 'src/schema/link_schema.dart'; part 'src/schema/property_schema.dart'; part 'src/schema/schema.dart'; /// @nodoc @protected typedef IsarUint8List = Uint8List; const bool _kIsWeb = identical(0, 0.0); ================================================ FILE: packages/isar/lib/src/annotations/backlink.dart ================================================ part of isar; /// Annotation to create a backlink to an existing link. @Target({TargetKind.field}) class Backlink { /// Annotation to create a backlink to an existing link. const Backlink({required this.to}); /// The Dart name of the target link. final String to; } ================================================ FILE: packages/isar/lib/src/annotations/collection.dart ================================================ part of isar; /// Annotation to create an Isar collection. const collection = Collection(); /// Annotation to create an Isar collection. @Target({TargetKind.classType}) class Collection { /// Annotation to create an Isar collection. const Collection({ this.inheritance = true, this.accessor, this.ignore = const {}, }); /// Should properties and accessors of parent classes and mixins be included? final bool inheritance; /// Allows you to override the default collection accessor. /// /// Example: /// ```dart /// @Collection(accessor: 'col') /// class MyCol { /// Id? id; /// } /// /// // access collection using: isar.col /// ``` final String? accessor; /// A list of properties or getter names that Isar should ignore. final Set ignore; } ================================================ FILE: packages/isar/lib/src/annotations/embedded.dart ================================================ part of isar; /// Annotation to nest objects of this type in collections. const embedded = Embedded(); /// Annotation to nest objects of this type in collections. @Target({TargetKind.classType}) class Embedded { /// Annotation to nest objects of this type in collections. const Embedded({this.inheritance = true, this.ignore = const {}}); /// Should properties and accessors of parent classes and mixins be included? final bool inheritance; /// A list of properties or getter names that Isar should ignore. final Set ignore; } ================================================ FILE: packages/isar/lib/src/annotations/enumerated.dart ================================================ part of isar; /// Annotation to specify how an enum property should be serialized. const enumerated = Enumerated(EnumType.ordinal); /// Annotation to specify how an enum property should be serialized. @Target({TargetKind.field, TargetKind.getter}) class Enumerated { /// Annotation to specify how an enum property should be serialized. const Enumerated(this.type, [this.property]); /// How the enum property should be serialized. final EnumType type; /// The property to use for the enum values. final String? property; } /// Enum type for enum values. enum EnumType { /// Stores the index of the enum as a byte value. ordinal, /// Stores the index of the enum as a 4-byte value. Use this type if your enum /// has more than 256 values or needs to be nullable. ordinal32, /// Uses the name of the enum value. name, /// Uses a custom enum value. value } ================================================ FILE: packages/isar/lib/src/annotations/ignore.dart ================================================ part of isar; /// Annotate a property or accessor in an Isar collection to ignore it. const ignore = Ignore(); /// Annotate a property or accessor in an Isar collection to ignore it. @Target({TargetKind.field, TargetKind.getter}) class Ignore { /// Annotate a property or accessor in an Isar collection to ignore it. const Ignore(); } ================================================ FILE: packages/isar/lib/src/annotations/index.dart ================================================ part of isar; /// Specifies how an index is stored in Isar. enum IndexType { /// Stores the value as-is in the index. value, /// Strings or Lists can be hashed to reduce the storage required by the /// index. The disadvantage of hash indexes is that they can't be used for /// prefix scans (`startsWith()` where clauses). String and list indexes are /// hashed by default. hash, /// `List` can hash its elements. hashElements, } /// Annotate properties to build an index. @Target({TargetKind.field, TargetKind.getter}) class Index { /// Annotate properties to build an index. const Index({ this.name, this.composite = const [], this.unique = false, this.replace = false, this.type, this.caseSensitive, }); /// Name of the index. By default, the names of the properties are /// concatenated using "_" final String? name; /// Specify up to two other properties to build a composite index. final List composite; /// A unique index ensures the index does not contain any duplicate values. /// Any attempt to insert or update data into the unique index that causes a /// duplicate will result in an error. final bool unique; /// If set to `true`, inserting a duplicate unique value will replace the /// existing object instead of throwing an error. final bool replace; /// Specifies how an index is stored in Isar. /// /// Defaults to: /// - `IndexType.hash` for `String`s and `List`s /// - `IndexType.value` for all other types final IndexType? type; /// String or `List` indexes can be case sensitive (default) or case /// insensitive. final bool? caseSensitive; } /// Another property that is part of the composite index. class CompositeIndex { /// Another property that is part of the composite index. const CompositeIndex( this.property, { this.type, this.caseSensitive, }); /// Dart name of the property. final String property; /// See [Index.type]. final IndexType? type; /// See [Index.caseSensitive]. final bool? caseSensitive; } ================================================ FILE: packages/isar/lib/src/annotations/name.dart ================================================ part of isar; /// Annotate Isar collections or properties to change their name. /// /// Can be used to change the name in Dart independently of Isar. @Target({TargetKind.classType, TargetKind.field, TargetKind.getter}) class Name { /// Annotate Isar collections or properties to change their name. const Name(this.name); /// The name this entity should have in the database. final String name; } ================================================ FILE: packages/isar/lib/src/annotations/type.dart ================================================ // ignore_for_file: camel_case_types part of isar; /// Type to specify the id property of a collection. typedef Id = int; /// Type to mark an [int] property or List as 8-bit sized. /// /// You may only store values between 0 and 255 in such a property. typedef byte = int; /// Type to mark an [int] property or List as 32-bit sized. /// /// You may only store values between -2147483648 and 2147483647 in such a /// property. typedef short = int; /// Type to mark a [double] property or List to have 32-bit precision. typedef float = double; ================================================ FILE: packages/isar/lib/src/common/isar_common.dart ================================================ // ignore_for_file: invalid_use_of_protected_member import 'dart:async'; import 'package:isar/isar.dart'; const Symbol _zoneTxn = #zoneTxn; /// @nodoc abstract class IsarCommon extends Isar { /// @nodoc IsarCommon(super.name); final List> _activeAsyncTxns = []; var _asyncWriteTxnsActive = 0; Transaction? _currentTxnSync; void _requireNotInTxn() { if (_currentTxnSync != null || Zone.current[_zoneTxn] != null) { throw IsarError( 'Cannot perform this operation from within an active transaction. ' 'Isar does not support nesting transactions.', ); } } /// @nodoc Future beginTxn(bool write, bool silent); Future _beginTxn( bool write, bool silent, Future Function() callback, ) async { requireOpen(); _requireNotInTxn(); final completer = Completer(); _activeAsyncTxns.add(completer.future); try { if (write) { _asyncWriteTxnsActive++; } final txn = await beginTxn(write, silent); final zone = Zone.current.fork( zoneValues: {_zoneTxn: txn}, ); T result; try { result = await zone.run(callback); await txn.commit(); } catch (e) { await txn.abort(); rethrow; } finally { txn.free(); } return result; } finally { completer.complete(); _activeAsyncTxns.remove(completer.future); if (write) { _asyncWriteTxnsActive--; } } } @override Future txn(Future Function() callback) { return _beginTxn(false, false, callback); } @override Future writeTxn(Future Function() callback, {bool silent = false}) { return _beginTxn(true, silent, callback); } /// @nodoc Future getTxn( bool write, Future Function(T txn) callback, ) { final currentTxn = Zone.current[_zoneTxn] as T?; if (currentTxn != null) { if (!currentTxn.active) { throw IsarError('Transaction is not active anymore. Make sure to await ' 'all your asynchronous code within transactions to prevent it from ' 'being closed prematurely.'); } else if (write && !currentTxn.write) { throw IsarError('Operation cannot be performed within a read ' 'transaction. Use isar.writeTxn() instead.'); } else if (currentTxn.isar != this) { throw IsarError('Transaction does not match Isar instance. ' 'Make sure to use transactions from the same Isar instance.'); } return callback(currentTxn); } else if (!write) { return _beginTxn(false, false, () { return callback(Zone.current[_zoneTxn] as T); }); } else { throw IsarError('Write operations require an explicit transaction. ' 'Wrap your code in isar.writeTxn()'); } } /// @nodoc Transaction beginTxnSync(bool write, bool silent); T _beginTxnSync(bool write, bool silent, T Function() callback) { requireOpen(); _requireNotInTxn(); if (write && _asyncWriteTxnsActive > 0) { throw IsarError( 'An async write transaction is already in progress in this isolate. ' 'You cannot begin a sync write transaction until it is finished. ' 'Use asynchroneous transactions if you want to queue multiple write ' 'transactions.', ); } final txn = beginTxnSync(write, silent); _currentTxnSync = txn; T result; try { result = callback(); txn.commitSync(); } catch (e) { txn.abortSync(); rethrow; } finally { _currentTxnSync = null; txn.free(); } return result; } @override T txnSync(T Function() callback) { return _beginTxnSync(false, false, callback); } @override T writeTxnSync(T Function() callback, {bool silent = false}) { return _beginTxnSync(true, silent, callback); } /// @nodoc R getTxnSync( bool write, R Function(T txn) callback, ) { if (_currentTxnSync != null) { if (write && !_currentTxnSync!.write) { throw IsarError( 'Operation cannot be performed within a read transaction. ' 'Use isar.writeTxnSync() instead.', ); } return callback(_currentTxnSync! as T); } else if (!write) { return _beginTxnSync(false, false, () => callback(_currentTxnSync! as T)); } else { throw IsarError('Write operations require an explicit transaction. ' 'Wrap your code in isar.writeTxnSync()'); } } @override Future close({bool deleteFromDisk = false}) async { requireOpen(); _requireNotInTxn(); await Future.wait(_activeAsyncTxns); await super.close(); return performClose(deleteFromDisk); } /// @nodoc bool performClose(bool deleteFromDisk); } /// @nodoc abstract class Transaction { /// @nodoc Transaction(this.isar, this.sync, this.write); /// @nodoc final Isar isar; /// @nodoc final bool sync; /// @nodoc final bool write; /// @nodoc bool get active; /// @nodoc Future commit(); /// @nodoc void commitSync(); /// @nodoc Future abort(); /// @nodoc void abortSync(); /// @nodoc void free() {} } ================================================ FILE: packages/isar/lib/src/common/isar_link_base_impl.dart ================================================ import 'package:isar/isar.dart'; /// @nodoc abstract class IsarLinkBaseImpl implements IsarLinkBase { var _initialized = false; Id? _objectId; /// The isar name of the link late final String linkName; /// The origin collection of the link. For backlinks it is actually the target /// collection. late final IsarCollection sourceCollection; /// The target collection of the link. For backlinks it is actually the origin /// collection. late final IsarCollection targetCollection; @override bool get isAttached => _objectId != null; @override void attach( IsarCollection sourceCollection, IsarCollection targetCollection, String linkName, Id? objectId, ) { if (_initialized) { if (linkName != this.linkName || !identical(sourceCollection, this.sourceCollection) || !identical(targetCollection, this.targetCollection)) { throw IsarError( 'Link has been moved! It is not allowed to move ' 'a link to a different collection.', ); } } else { _initialized = true; this.sourceCollection = sourceCollection; this.targetCollection = targetCollection; this.linkName = linkName; } _objectId = objectId; } /// Returns the containing object's id or throws an exception if this link has /// not been attached to an object yet. Id requireAttached() { if (_objectId == null) { throw IsarError( 'Containing object needs to be managed by Isar to use this method. ' 'Use collection.put(yourObject) to add it to the database.', ); } else { return _objectId!; } } /// Returns the id of a linked object. Id Function(OBJ obj) get getId; /// Returns the id of a linked object or throws an exception if the id is /// `null` or set to `Isar.autoIncrement`. Id requireGetId(OBJ object) { final id = getId(object); if (id != Isar.autoIncrement) { return id; } else { throw IsarError( 'Object "$object" has no id and can therefore not be linked. ' 'Make sure to .put() objects before you use them in links.', ); } } /// See [IsarLinks.filter]. QueryBuilder filter() { final containingId = requireAttached(); final qb = QueryBuilderInternal( collection: targetCollection, whereClauses: [ LinkWhereClause( linkCollection: sourceCollection.name, linkName: linkName, id: containingId, ), ], ); return QueryBuilder(qb); } /// See [IsarLinks.update]. Future update({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }); /// See [IsarLinks.updateSync]. void updateSync({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }); } ================================================ FILE: packages/isar/lib/src/common/isar_link_common.dart ================================================ import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; const bool _kIsWeb = identical(0, 0.0); /// @nodoc abstract class IsarLinkCommon extends IsarLinkBaseImpl with IsarLink { OBJ? _value; @override bool isChanged = false; @override bool isLoaded = false; @override OBJ? get value { if (isAttached && !isLoaded && !isChanged && !_kIsWeb) { loadSync(); } return _value; } @override set value(OBJ? value) { isChanged |= !identical(_value, value); _value = value; isLoaded = true; } @override Future load() async { _value = await filter().findFirst(); isChanged = false; isLoaded = true; } @override void loadSync() { _value = filter().findFirstSync(); isChanged = false; isLoaded = true; } @override Future save() async { if (!isChanged) { return; } final object = value; await update(link: [if (object != null) object], reset: true); isChanged = false; isLoaded = true; } @override void saveSync() { if (!isChanged) { return; } final object = _value; updateSync(link: [if (object != null) object], reset: true); isChanged = false; isLoaded = true; } @override Future reset() async { await update(reset: true); _value = null; isChanged = false; isLoaded = true; } @override void resetSync() { updateSync(reset: true); _value = null; isChanged = false; isLoaded = true; } @override String toString() { return 'IsarLink($_value)'; } } ================================================ FILE: packages/isar/lib/src/common/isar_links_common.dart ================================================ import 'dart:collection'; import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; const bool _kIsWeb = identical(0, 0.0); /// @nodoc abstract class IsarLinksCommon extends IsarLinkBaseImpl with IsarLinks, SetMixin { final _objects = {}; /// @nodoc final addedObjects = HashSet.identity(); /// @nodoc final removedObjects = HashSet.identity(); @override bool isLoaded = false; @override bool get isChanged => addedObjects.isNotEmpty || removedObjects.isNotEmpty; Map get _loadedObjects { if (isAttached && !isLoaded && !_kIsWeb) { loadSync(); } return _objects; } @override void attach( IsarCollection sourceCollection, IsarCollection targetCollection, String linkName, Id? objectId, ) { super.attach(sourceCollection, targetCollection, linkName, objectId); _applyAddedRemoved(); } @override Future load({bool overrideChanges = false}) async { final objects = await filter().findAll(); _applyLoaded(objects, overrideChanges); } @override void loadSync({bool overrideChanges = false}) { final objects = filter().findAllSync(); _applyLoaded(objects, overrideChanges); } void _applyLoaded(List objects, bool overrideChanges) { _objects.clear(); for (final object in objects) { final id = getId(object); if (id != Isar.autoIncrement) { _objects[id] = object; } } if (overrideChanges) { addedObjects.clear(); removedObjects.clear(); } else { _applyAddedRemoved(); } isLoaded = true; } void _applyAddedRemoved() { for (final object in addedObjects) { final id = getId(object); if (id != Isar.autoIncrement) { _objects[id] = object; } } for (final object in removedObjects) { final id = getId(object); if (id != Isar.autoIncrement) { _objects.remove(id); } } } @override Future save() async { if (!isChanged) { return; } await update(link: addedObjects, unlink: removedObjects); addedObjects.clear(); removedObjects.clear(); isLoaded = true; } @override void saveSync() { if (!isChanged) { return; } updateSync(link: addedObjects, unlink: removedObjects); addedObjects.clear(); removedObjects.clear(); isLoaded = true; } @override Future reset() async { await update(reset: true); clear(); isLoaded = true; } @override void resetSync() { updateSync(reset: true); clear(); isLoaded = true; } @override bool add(OBJ value) { if (isAttached) { final id = getId(value); if (id != Isar.autoIncrement) { if (_objects.containsKey(id)) { return false; } _objects[id] = value; } } removedObjects.remove(value); return addedObjects.add(value); } @override bool contains(Object? element) { requireAttached(); if (element is OBJ) { final id = getId(element); if (id != Isar.autoIncrement) { return _loadedObjects.containsKey(id); } } return false; } @override Iterator get iterator => _loadedObjects.values.iterator; @override int get length => _loadedObjects.length; @override OBJ? lookup(Object? element) { requireAttached(); if (element is OBJ) { final id = getId(element); if (id != Isar.autoIncrement) { return _loadedObjects[id]; } } return null; } @override bool remove(Object? value) { if (value is! OBJ) { return false; } if (isAttached) { final id = getId(value); if (id != Isar.autoIncrement) { if (isLoaded && !_objects.containsKey(id)) { return false; } _objects.remove(id); } } addedObjects.remove(value); return removedObjects.add(value); } @override Set toSet() { requireAttached(); return HashSet( equals: (o1, o2) => getId(o1) == getId(o2), // ignore: noop_primitive_operations hashCode: (o) => getId(o).toInt(), isValidKey: (o) => o is OBJ && getId(o) != Isar.autoIncrement, )..addAll(_loadedObjects.values); } @override void clear() { _objects.clear(); addedObjects.clear(); removedObjects.clear(); } @override String toString() { final content = IterableBase.iterableToFullString(_objects.values, '{', '}'); return 'IsarLinks($content)'; } } ================================================ FILE: packages/isar/lib/src/common/schemas.dart ================================================ import 'package:isar/isar.dart'; /// @nodoc List> getSchemas( List> collectionSchemas, ) { final schemas = >{}; for (final collectionSchema in collectionSchemas) { schemas.add(collectionSchema); schemas.addAll(collectionSchema.embeddedSchemas.values); } return schemas.toList(); } ================================================ FILE: packages/isar/lib/src/isar.dart ================================================ part of isar; /// Callback for a newly opened Isar instance. typedef IsarOpenCallback = void Function(Isar isar); /// Callback for a release Isar instance. typedef IsarCloseCallback = void Function(String isarName); /// An instance of the Isar Database. abstract class Isar { /// @nodoc @protected Isar(this.name) { _instances[name] = this; for (final callback in _openCallbacks) { callback(this); } } /// The version of the Isar library. static const version = '3.1.8'; /// Smallest valid id. static const Id minId = isarMinId; /// Largest valid id. static const Id maxId = isarMaxId; /// The default Isar instance name. static const String defaultName = 'default'; /// The default max Isar size. static const int defaultMaxSizeMiB = 1024; /// Placeholder for an auto-increment id. static const Id autoIncrement = isarAutoIncrementId; static final Map _instances = {}; static final Set _openCallbacks = {}; static final Set _closeCallbacks = {}; /// Name of the instance. final String name; /// The directory containing the database file or `null` on the web. String? get directory; /// The full path of the database file is `directory/name.isar` and the lock /// file `directory/name.isar.lock`. String? get path => directory != null ? '$directory/$name.isar' : null; late final Map> _collections; late final Map> _collectionsByName; bool _isOpen = true; static void _checkOpen(String name, List> schemas) { if (name.isEmpty || name.startsWith('_')) { throw IsarError('Instance names must not be empty or start with "_".'); } if (_instances.containsKey(name)) { throw IsarError('Instance has already been opened.'); } if (schemas.isEmpty) { throw IsarError('At least one collection needs to be opened.'); } final schemaNames = {}; for (final schema in schemas) { if (!schemaNames.add(schema.name)) { throw IsarError('Duplicate collection ${schema.name}.'); } } for (final schema in schemas) { final dependencies = schema.links.values.map((e) => e.target); for (final dependency in dependencies) { if (!schemaNames.contains(dependency)) { throw IsarError( "Collection ${schema.name} depends on $dependency but it's schema " 'was not provided.', ); } } } } /// Open a new Isar instance. static Future open( List> schemas, { required String directory, String name = defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, CompactCondition? compactOnLaunch, bool inspector = true, }) { _checkOpen(name, schemas); /// Tree shake the inspector for profile and release builds. assert(() { if (!_kIsWeb && inspector) { _IsarConnect.initialize(schemas); } return true; }()); return openIsar( schemas: schemas, directory: directory, name: name, maxSizeMiB: maxSizeMiB, relaxedDurability: relaxedDurability, compactOnLaunch: compactOnLaunch, ); } /// Open a new Isar instance. static Isar openSync( List> schemas, { required String directory, String name = defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, CompactCondition? compactOnLaunch, bool inspector = true, }) { _checkOpen(name, schemas); /// Tree shake the inspector for profile and release builds. assert(() { if (!_kIsWeb && inspector) { _IsarConnect.initialize(schemas); } return true; }()); return openIsarSync( schemas: schemas, directory: directory, name: name, maxSizeMiB: maxSizeMiB, relaxedDurability: relaxedDurability, compactOnLaunch: compactOnLaunch, ); } /// Is the instance open? bool get isOpen => _isOpen; /// @nodoc @protected void requireOpen() { if (!isOpen) { throw IsarError('Isar instance has already been closed'); } } /// Executes an asynchronous read-only transaction. Future txn(Future Function() callback); /// Executes an asynchronous read-write transaction. /// /// If [silent] is `true`, watchers are not notified about changes in this /// transaction. Future writeTxn(Future Function() callback, {bool silent = false}); /// Executes a synchronous read-only transaction. T txnSync(T Function() callback); /// Executes a synchronous read-write transaction. /// /// If [silent] is `true`, watchers are not notified about changes in this /// transaction. T writeTxnSync(T Function() callback, {bool silent = false}); /// @nodoc @protected void attachCollections(Map> collections) { _collections = collections; _collectionsByName = { for (IsarCollection col in collections.values) col.name: col, }; } /// Get a collection by its type. /// /// You should use the generated extension methods instead. IsarCollection collection() { requireOpen(); final collection = _collections[T]; if (collection == null) { throw IsarError('Missing ${T.runtimeType}Schema in Isar.open'); } return collection as IsarCollection; } /// @nodoc @protected IsarCollection? getCollectionByNameInternal(String name) { return _collectionsByName[name]; } /// Remove all data in this instance and reset the auto increment values. Future clear() async { for (final col in _collections.values) { await col.clear(); } } /// Remove all data in this instance and reset the auto increment values. void clearSync() { for (final col in _collections.values) { col.clearSync(); } } /// Returns the size of all the collections in bytes. Not supported on web. /// /// This method is extremely fast and independent of the number of objects in /// the instance. Future getSize({bool includeIndexes = false, bool includeLinks = false}); /// Returns the size of all collections in bytes. Not supported on web. /// /// This method is extremely fast and independent of the number of objects in /// the instance. int getSizeSync({bool includeIndexes = false, bool includeLinks = false}); /// Copy a compacted version of the database to the specified file. /// /// If you want to backup your database, you should always use a compacted /// version. Compacted does not mean compressed. /// /// Do not run this method while other transactions are active to avoid /// unnecessary growth of the database. Future copyToFile(String targetPath); /// Releases an Isar instance. /// /// If this is the only isolate that holds a reference to this instance, the /// Isar instance will be closed. [deleteFromDisk] additionally removes all /// database files if enabled. /// /// Returns whether the instance was actually closed. Future close({bool deleteFromDisk = false}) { requireOpen(); _isOpen = false; if (identical(_instances[name], this)) { _instances.remove(name); } for (final callback in _closeCallbacks) { callback(name); } return Future.value(false); } /// Verifies the integrity of the database file. /// /// Do not use this method in production apps. @visibleForTesting @experimental Future verify(); /// A list of all Isar instances opened in the current isolate. static Set get instanceNames => _instances.keys.toSet(); /// Returns an Isar instance opened in the current isolate by its name. If /// no name is provided, the default instance is returned. static Isar? getInstance([String name = defaultName]) { return _instances[name]; } /// Registers a listener that is called whenever an Isar instance is opened. static void addOpenListener(IsarOpenCallback callback) { _openCallbacks.add(callback); } /// Removes a previously registered `IsarOpenCallback`. static void removeOpenListener(IsarOpenCallback callback) { _openCallbacks.remove(callback); } /// Registers a listener that is called whenever an Isar instance is /// released. static void addCloseListener(IsarCloseCallback callback) { _closeCallbacks.add(callback); } /// Removes a previously registered `IsarOpenCallback`. static void removeCloseListener(IsarCloseCallback callback) { _closeCallbacks.remove(callback); } /// Initialize Isar Core manually. You need to provide Isar Core libraries /// for every platform your app will run on. /// /// If [download] is `true`, Isar will attempt to download the correct /// library and place it in the specified path or the script directory. /// /// Be careful if multiple unit tests try to download the library at the /// same time. Always use `flutter test -j 1` when you rely on auto /// downloading to ensure that only one test is running at a time. /// /// Only use this method for non-Flutter code or unit tests. static Future initializeIsarCore({ Map libraries = const {}, bool download = false, }) async { await initializeCoreBinary( libraries: libraries, download: download, ); } /// Split a String into words according to Unicode Annex #29. Only words /// containing at least one alphanumeric character will be included. static List splitWords(String input) => isarSplitWords(input); } /// Isar databases can contain unused space that will be reused for later /// operations. You can specify conditions to trigger manual compaction where /// the entire database is copied and unused space freed. /// /// This operation can only be performed while a database is being opened and /// should only be used if absolutely necessary. class CompactCondition { /// Compaction will happen if all of the specified conditions are true. const CompactCondition({ this.minFileSize, this.minBytes, this.minRatio, }) : assert( minFileSize != null || minBytes != null || minRatio != null, 'At least one condition needs to be specified.', ); /// The minimum size in bytes of the database file to trigger compaction. It /// is highly discouraged to trigger compaction solely on this condition. final int? minFileSize; /// The minimum number of bytes that can be freed with compaction. final int? minBytes; /// The minimum compaction ration. For example `2.0` would trigger compaction /// as soon as the file size can be halved. final double? minRatio; } ================================================ FILE: packages/isar/lib/src/isar_collection.dart ================================================ part of isar; /// Normal keys consist of a single object, composite keys multiple. typedef IndexKey = List; /// Use `IsarCollection` instances to find, query, and create new objects of a /// given type in Isar. /// /// You can get an instance of `IsarCollection` by calling `isar.get()` or /// by using the generated `isar.yourCollections` getter. abstract class IsarCollection { /// The corresponding Isar instance. Isar get isar; /// Get the schema of the collection. CollectionSchema get schema; /// The name of the collection. String get name => schema.name; /// {@template col_get} /// Get a single object by its [id] or `null` if the object does not exist. /// {@endtemplate} Future get(Id id) { return getAll([id]).then((List objects) => objects[0]); } /// {@macro col_get} OBJ? getSync(Id id) { return getAllSync([id])[0]; } /// {@template col_get_all} /// Get a list of objects by their [ids] or `null` if an object does not /// exist. /// {@endtemplate} Future> getAll(List ids); /// {@macro col_get_all} List getAllSync(List ids); /// {@template col_get_by_index} /// Get a single object by the unique index [indexName] and [key]. /// /// Returns `null` if the object does not exist. /// /// If possible, you should use the generated type-safe methods instead. /// {@endtemplate} @experimental Future getByIndex(String indexName, IndexKey key) { return getAllByIndex(indexName, [key]) .then((List objects) => objects[0]); } /// {@macro col_get_by_index} @experimental OBJ? getByIndexSync(String indexName, IndexKey key) { return getAllByIndexSync(indexName, [key])[0]; } /// {@template col_get_all_by_index} /// Get a list of objects by the unique index [indexName] and [keys]. /// /// Returns `null` if the object does not exist. /// /// If possible, you should use the generated type-safe methods instead. /// {@endtemplate} @experimental Future> getAllByIndex(String indexName, List keys); /// {@macro col_get_all_by_index}' @experimental List getAllByIndexSync(String indexName, List keys); /// {@template col_put} /// Insert or update an [object]. Returns the id of the new or updated object. /// /// If the object has an non-final id property, it will be set to the assigned /// id. Otherwise you should use the returned id to update the object. /// {@endtemplate} Future put(OBJ object) { return putAll([object]).then((List ids) => ids[0]); } /// {@macro col_put} Id putSync(OBJ object, {bool saveLinks = true}) { return putAllSync([object], saveLinks: saveLinks)[0]; } /// {@template col_put_all} /// Insert or update a list of [objects]. Returns the list of ids of the new /// or updated objects. /// /// If the objects have an non-final id property, it will be set to the /// assigned id. Otherwise you should use the returned ids to update the /// objects. /// {@endtemplate} Future> putAll(List objects); /// {@macro col_put_all} List putAllSync(List objects, {bool saveLinks = true}); /// {@template col_put_by_index} /// Insert or update the [object] by the unique index [indexName]. Returns the /// id of the new or updated object. /// /// If there is already an object with the same index key, it will be /// updated and all links will be preserved. Otherwise a new object will be /// inserted. /// /// If the object has an non-final id property, it will be set to the assigned /// id. Otherwise you should use the returned id to update the object. /// /// If possible, you should use the generated type-safe methods instead. /// {@endtemplate} @experimental Future putByIndex(String indexName, OBJ object) { return putAllByIndex(indexName, [object]).then((List ids) => ids[0]); } /// {@macro col_put_by_index} @experimental Id putByIndexSync(String indexName, OBJ object, {bool saveLinks = true}) { return putAllByIndexSync(indexName, [object])[0]; } /// {@template col_put_all_by_index} /// Insert or update a list of [objects] by the unique index [indexName]. /// Returns the list of ids of the new or updated objects. /// /// If there is already an object with the same index key, it will be /// updated and all links will be preserved. Otherwise a new object will be /// inserted. /// /// If the objects have an non-final id property, it will be set to the /// assigned id. Otherwise you should use the returned ids to update the /// objects. /// /// If possible, you should use the generated type-safe methods instead. /// {@endtemplate} @experimental Future> putAllByIndex(String indexName, List objects); /// {@macro col_put_all_by_index} @experimental List putAllByIndexSync( String indexName, List objects, { bool saveLinks = true, }); /// {@template col_delete} /// Delete a single object by its [id]. /// /// Returns whether the object has been deleted. Isar web always returns /// `true`. /// {@endtemplate} Future delete(Id id) { return deleteAll([id]).then((int count) => count == 1); } /// {@macro col_delete} bool deleteSync(Id id) { return deleteAllSync([id]) == 1; } /// {@template col_delete_all} /// Delete a list of objects by their [ids]. /// /// Returns the number of objects that have been deleted. Isar web always /// returns `ids.length`. /// {@endtemplate} Future deleteAll(List ids); /// {@macro col_delete_all} int deleteAllSync(List ids); /// {@template col_delete_by_index} /// Delete a single object by the unique index [indexName] and [key]. /// /// Returns whether the object has been deleted. Isar web always returns /// `true`. /// {@endtemplate} @experimental Future deleteByIndex(String indexName, IndexKey key) { return deleteAllByIndex(indexName, [key]).then((int count) => count == 1); } /// {@macro col_delete_by_index} @experimental bool deleteByIndexSync(String indexName, IndexKey key) { return deleteAllByIndexSync(indexName, [key]) == 1; } /// {@template col_delete_all_by_index} /// Delete a list of objects by the unique index [indexName] and [keys]. /// /// Returns the number of objects that have been deleted. Isar web always /// returns `keys.length`. /// {@endtemplate} @experimental Future deleteAllByIndex(String indexName, List keys); /// {@macro col_delete_all_by_index} @experimental int deleteAllByIndexSync(String indexName, List keys); /// {@template col_clear} /// Remove all data in this collection and reset the auto increment value. /// {@endtemplate} Future clear(); /// {@macro col_clear} void clearSync(); /// {@template col_import_json_raw} /// Import a list of json objects encoded as a byte array. /// /// The json objects must have the same structure as the objects in this /// collection. Otherwise an exception will be thrown. /// {@endtemplate} Future importJsonRaw(Uint8List jsonBytes); /// {@macro col_import_json_raw} void importJsonRawSync(Uint8List jsonBytes); /// {@template col_import_json} /// Import a list of json objects. /// /// The json objects must have the same structure as the objects in this /// collection. Otherwise an exception will be thrown. /// {@endtemplate} Future importJson(List> json); /// {@macro col_import_json} void importJsonSync(List> json); /// Start building a query using the [QueryBuilder]. /// /// You can use where clauses to only return [distinct] results. If you want /// to reverse the order, set [sort] to [Sort.desc]. QueryBuilder where({ bool distinct = false, Sort sort = Sort.asc, }) { final qb = QueryBuilderInternal( collection: this, whereDistinct: distinct, whereSort: sort, ); return QueryBuilder(qb); } /// Start building a query using the [QueryBuilder]. /// /// Shortcut if you don't want to use where clauses. QueryBuilder filter() => where().filter(); /// Build a query dynamically for example to build a custom query language. /// /// It is highly discouraged to use this method. Only in very special cases /// should it be used. If you open an issue please always mention that you /// used this method. /// /// The type argument [R] needs to be equal to [OBJ] if no [property] is /// specified. Otherwise it should be the type of the property. @experimental Query buildQuery({ List whereClauses = const [], bool whereDistinct = false, Sort whereSort = Sort.asc, FilterOperation? filter, List sortBy = const [], List distinctBy = const [], int? offset, int? limit, String? property, }); /// {@template col_count} /// Returns the total number of objects in this collection. /// /// For non-web apps, this method is extremely fast and independent of the /// number of objects in the collection. /// {@endtemplate} Future count(); /// {@macro col_count} int countSync(); /// {@template col_size} /// Returns the size of the collection in bytes. Not supported on web. /// /// For non-web apps, this method is extremely fast and independent of the /// number of objects in the collection. /// {@endtemplate} Future getSize({bool includeIndexes = false, bool includeLinks = false}); /// {@macro col_size} int getSizeSync({bool includeIndexes = false, bool includeLinks = false}); /// Watch the collection for changes. /// /// If [fireImmediately] is `true`, an event will be fired immediately. Stream watchLazy({bool fireImmediately = false}); /// Watch the object with [id] for changes. If a change occurs, the new object /// will be returned in the stream. /// /// Objects that don't exist (yet) can also be watched. If [fireImmediately] /// is `true`, the object will be sent to the consumer immediately. Stream watchObject(Id id, {bool fireImmediately = false}); /// Watch the object with [id] for changes. /// /// If [fireImmediately] is `true`, an event will be fired immediately. Stream watchObjectLazy(Id id, {bool fireImmediately = false}); /// Verifies the integrity of the collection and its indexes. /// /// Throws an exception if the collection does not contain exactly the /// provided [objects]. /// /// Do not use this method in production apps. @visibleForTesting @experimental Future verify(List objects); /// Verifies the integrity of a link. /// /// Throws an exception if not exactly [sourceIds] as linked to the /// [targetIds]. /// /// Do not use this method in production apps. @visibleForTesting @experimental Future verifyLink( String linkName, List sourceIds, List targetIds, ); } ================================================ FILE: packages/isar/lib/src/isar_connect.dart ================================================ // coverage:ignore-file // ignore_for_file: avoid_print part of isar; abstract class _IsarConnect { static const Map Function(Map _)> _handlers = { ConnectAction.getSchema: _getSchema, ConnectAction.listInstances: _listInstances, ConnectAction.watchInstance: _watchInstance, ConnectAction.executeQuery: _executeQuery, ConnectAction.removeQuery: _removeQuery, ConnectAction.importJson: _importJson, ConnectAction.exportJson: _exportJson, ConnectAction.editProperty: _editProperty, }; static List>? _schemas; // ignore: cancel_subscriptions static final _querySubscription = >[]; static final List> _collectionSubscriptions = >[]; static void initialize(List> schemas) { if (_schemas != null) { return; } _schemas = schemas; Isar.addOpenListener((_) { postEvent(ConnectEvent.instancesChanged.event, {}); }); Isar.addCloseListener((_) { postEvent(ConnectEvent.instancesChanged.event, {}); }); for (final handler in _handlers.entries) { registerExtension(handler.key.method, (String method, Map parameters) async { try { final args = parameters.containsKey('args') ? jsonDecode(parameters['args']!) as Map : {}; final result = {'result': await handler.value(args)}; return ServiceExtensionResponse.result(jsonEncode(result)); } catch (e) { return ServiceExtensionResponse.error( ServiceExtensionResponse.extensionError, e.toString(), ); } }); } _printConnection(); } static void _printConnection() { Service.getInfo().then((ServiceProtocolInfo info) { final serviceUri = info.serverUri; if (serviceUri == null) { return; } final port = serviceUri.port; var path = serviceUri.path; if (path.endsWith('/')) { path = path.substring(0, path.length - 1); } if (path.endsWith('=')) { path = path.substring(0, path.length - 1); } final url = ' https://inspect.isar-community.dev/${Isar.version}/#/$port$path '; String line(String text, String fill) { final fillCount = url.length - text.length; final left = List.filled(fillCount ~/ 2, fill); final right = List.filled(fillCount - left.length, fill); return left.join() + text + right.join(); } print('╔${line('', '═')}╗'); print('║${line('ISAR CONNECT STARTED', ' ')}║'); print('╟${line('', '─')}╢'); print('║${line('Open the link to connect to the Isar', ' ')}║'); print('║${line('Inspector while this build is running.', ' ')}║'); print('╟${line('', '─')}╢'); print('║$url║'); print('╚${line('', '═')}╝'); }); } static Future _getSchema(Map _) async { return _schemas!.map((e) => e.toJson()).toList(); } static Future _listInstances(Map _) async { return Isar.instanceNames.toList(); } static Future _watchInstance(Map params) async { for (final sub in _collectionSubscriptions) { unawaited(sub.cancel()); } _collectionSubscriptions.clear(); if (params.isEmpty) { return true; } final instanceName = params['instance'] as String; final instance = Isar.getInstance(instanceName)!; for (final collection in instance._collections.values) { final sub = collection.watchLazy(fireImmediately: true).listen((_) { _sendCollectionInfo(collection); }); _collectionSubscriptions.add(sub); } return true; } static void _sendCollectionInfo(IsarCollection collection) { final count = collection.countSync(); final size = collection.getSizeSync( includeIndexes: true, includeLinks: true, ); final collectionInfo = ConnectCollectionInfo( instance: collection.isar.name, collection: collection.name, size: size, count: count, ); postEvent( ConnectEvent.collectionInfoChanged.event, collectionInfo.toJson(), ); } static Future> _executeQuery( Map params, ) async { for (final sub in _querySubscription) { unawaited(sub.cancel()); } _querySubscription.clear(); final cQuery = ConnectQuery.fromJson(params); final instance = Isar.getInstance(cQuery.instance)!; final links = _schemas!.firstWhere((e) => e.name == cQuery.collection).links.values; final query = cQuery.toQuery(); params.remove('limit'); params.remove('offset'); final countQuery = ConnectQuery.fromJson(params).toQuery(); _querySubscription.add( query.watchLazy().listen((_) { postEvent(ConnectEvent.queryChanged.event, {}); }), ); final subscribed = {cQuery.collection}; for (final link in links) { if (subscribed.add(link.target)) { final target = instance.getCollectionByNameInternal(link.target)!; _querySubscription.add( target.watchLazy().listen((_) { postEvent(ConnectEvent.queryChanged.event, {}); }), ); } } final objects = await query.exportJson(); if (links.isNotEmpty) { final source = instance.getCollectionByNameInternal(cQuery.collection)!; for (final object in objects) { for (final link in links) { final target = instance.getCollectionByNameInternal(link.target)!; final links = await target.buildQuery( whereClauses: [ LinkWhereClause( linkCollection: source.name, linkName: link.name, id: object[source.schema.idName] as int, ), ], limit: link.single ? 1 : null, ).exportJson(); if (link.single) { object[link.name] = links.isEmpty ? null : links.first; } else { object[link.name] = links; } } } } return { 'objects': objects, 'count': await countQuery.count(), }; } static Future _removeQuery(Map params) async { final query = ConnectQuery.fromJson(params).toQuery(); await query.isar.writeTxn(query.deleteAll); return true; } static Future _importJson(Map params) async { final instance = Isar.getInstance(params['instance'] as String)!; final collection = instance.getCollectionByNameInternal(params['collection'] as String)!; final objects = (params['objects'] as List).cast>(); await instance.writeTxn(() async { await collection.importJson(objects); }); } static Future> _exportJson(Map params) async { final query = ConnectQuery.fromJson(params).toQuery(); return query.exportJson(); } static Future _editProperty(Map params) async { final cEdit = ConnectEdit.fromJson(params); final isar = Isar.getInstance(cEdit.instance)!; final collection = isar.getCollectionByNameInternal(cEdit.collection)!; final keys = cEdit.path.split('.'); final query = collection.buildQuery( whereClauses: [IdWhereClause.equalTo(value: cEdit.id)], ); final objects = await query.exportJson(); if (objects.isNotEmpty) { dynamic object = objects.first; for (var i = 0; i < keys.length; i++) { if (i == keys.length - 1 && object is Map) { object[keys[i]] = cEdit.value; } else if (object is Map) { object = object[keys[i]]; } else if (object is List) { object = object[int.parse(keys[i])]; } } try { await isar.writeTxn(() async { await collection.importJson(objects); }); } catch (e) { print(e); } } } } ================================================ FILE: packages/isar/lib/src/isar_connect_api.dart ================================================ // coverage:ignore-file // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; enum ConnectAction { getSchema('ext.isar.getSchema'), listInstances('ext.isar.listInstances'), watchInstance('ext.isar.watchInstance'), executeQuery('ext.isar.executeQuery'), removeQuery('ext.isar.removeQuery'), importJson('ext.isar.importJson'), exportJson('ext.isar.exportJson'), editProperty('ext.isar.editProperty'); const ConnectAction(this.method); final String method; } enum ConnectEvent { instancesChanged('isar.instancesChanged'), queryChanged('isar.queryChanged'), collectionInfoChanged('isar.collectionInfoChanged'); const ConnectEvent(this.event); final String event; } class ConnectQuery { ConnectQuery({ required this.instance, required this.collection, this.filter, this.offset, this.limit, this.sortProperty, this.sortAsc, }); factory ConnectQuery.fromJson(Map json) { return ConnectQuery( instance: json['instance'] as String, collection: json['collection'] as String, filter: _filterFromJson(json['filter'] as Map?), offset: json['offset'] as int?, limit: json['limit'] as int?, sortProperty: json['sortProperty'] as String?, sortAsc: json['sortAsc'] as bool?, ); } final String instance; final String collection; final FilterOperation? filter; final int? offset; final int? limit; final String? sortProperty; final bool? sortAsc; Map toJson() { return { 'instance': instance, 'collection': collection, if (filter != null) 'filter': _filterToJson(filter!), if (offset != null) 'offset': offset, if (limit != null) 'limit': limit, if (sortProperty != null) 'sortProperty': sortProperty, if (sortAsc != null) 'sortAsc': sortAsc, }; } static FilterOperation? _filterFromJson(Map? json) { if (json == null) { return null; } if (json.containsKey('filters')) { final filters = (json['filters'] as List) .map((e) => _filterFromJson(e as Map?)!) .toList(); return FilterGroup( type: FilterGroupType.values[json['type'] as int], filters: filters, ); } else { return FilterCondition( type: FilterConditionType.values[json['type'] as int], property: json['property'] as String, value1: json['value1'], value2: json['value2'], include1: json['include1'] as bool, include2: json['include2'] as bool, caseSensitive: json['caseSensitive'] as bool, ); } } static Map _filterToJson(FilterOperation filter) { if (filter is FilterCondition) { return { 'type': filter.type.index, 'property': filter.property, 'value1': filter.value1, 'value2': filter.value2, 'include1': filter.include1, 'include2': filter.include2, 'caseSensitive': filter.caseSensitive, }; } else if (filter is FilterGroup) { return { 'type': filter.type.index, 'filters': filter.filters.map(_filterToJson).toList(), }; } else { throw UnimplementedError(); } } Query toQuery() { final isar = Isar.getInstance(instance)!; // ignore: invalid_use_of_protected_member final collection = isar.getCollectionByNameInternal(this.collection)!; WhereClause? whereClause; var whereSort = Sort.asc; SortProperty? sortProperty; if (this.sortProperty != null) { if (this.sortProperty == collection.schema.idName) { whereClause = const IdWhereClause.any(); whereSort = sortAsc == true ? Sort.asc : Sort.desc; } else { sortProperty = SortProperty( property: this.sortProperty!, sort: sortAsc == true ? Sort.asc : Sort.desc, ); } } return collection.buildQuery( whereClauses: [if (whereClause != null) whereClause], whereSort: whereSort, filter: filter, offset: offset, limit: limit, sortBy: [if (sortProperty != null) sortProperty], ); } } class ConnectEdit { ConnectEdit({ required this.instance, required this.collection, required this.id, required this.path, required this.value, }); factory ConnectEdit.fromJson(Map json) { return ConnectEdit( instance: json['instance'] as String, collection: json['collection'] as String, id: json['id'] as Id, path: json['path'] as String, value: json['value'], ); } final String instance; final String collection; final Id id; final String path; final dynamic value; Map toJson() { return { 'instance': instance, 'collection': collection, 'id': id, 'path': path, 'value': value, }; } } class ConnectCollectionInfo { ConnectCollectionInfo({ required this.instance, required this.collection, required this.size, required this.count, }); factory ConnectCollectionInfo.fromJson(Map json) { return ConnectCollectionInfo( instance: json['instance'] as String, collection: json['collection'] as String, size: json['size'] as int, count: json['count'] as int, ); } final String instance; final String collection; final int size; final int count; Map toJson() { return { 'instance': instance, 'collection': collection, 'size': size, 'count': count, }; } } ================================================ FILE: packages/isar/lib/src/isar_error.dart ================================================ part of isar; /// An error raised by Isar. class IsarError extends Error { /// @nodoc @protected IsarError(this.message); /// The message final String message; @override String toString() { return 'IsarError: $message'; } } /// This error is returned when a unique index constraint is violated. class IsarUniqueViolationError extends IsarError { /// @nodoc @protected IsarUniqueViolationError() : super('Unique index violated'); } ================================================ FILE: packages/isar/lib/src/isar_link.dart ================================================ part of isar; /// @nodoc @sealed abstract class IsarLinkBase { /// Is the containing object managed by Isar? bool get isAttached; /// Have the contents been changed? If not, `.save()` is a no-op. bool get isChanged; /// Has this link been loaded? bool get isLoaded; /// {@template link_load} /// Loads the linked object(s) from the database /// {@endtemplate} Future load(); /// {@macro link_load} void loadSync(); /// {@template link_save} /// Saves the linked object(s) to the database if there are changes. /// /// Also puts new objects into the database that have id set to `null` or /// `Isar.autoIncrement`. /// {@endtemplate} Future save(); /// {@macro link_save} void saveSync(); /// {@template link_reset} /// Unlinks all linked object(s). /// /// You can even call this method on links that have not been loaded yet. /// {@endtemplate} Future reset(); /// {@macro link_reset} void resetSync(); /// @nodoc @protected void attach( IsarCollection sourceCollection, IsarCollection targetCollection, String linkName, Id? objectId, ); } /// Establishes a 1:1 relationship with the same or another collection. The /// target collection is specified by the generic type argument. abstract class IsarLink implements IsarLinkBase { /// Create an empty, unattached link. Make sure to provide the correct /// generic argument. factory IsarLink() => IsarLinkImpl(); /// The linked object or `null` if no object is linked. OBJ? get value; /// The linked object or `null` if no object is linked. set value(OBJ? obj); } /// Establishes a 1:n relationship with the same or another collection. The /// target collection is specified by the generic type argument. abstract class IsarLinks implements IsarLinkBase, Set { /// Create an empty, unattached link. Make sure to provide the correct /// generic argument. factory IsarLinks() => IsarLinksImpl(); @override Future load({bool overrideChanges = true}); @override void loadSync({bool overrideChanges = true}); /// {@template links_update} /// Creates and removes the specified links in the database. /// /// This operation does not alter the state of the local copy of this link /// and it can even be used without loading the link. /// {@endtemplate} Future update({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }); /// {@macro links_update} void updateSync({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }); /// Starts a query for linked objects. QueryBuilder filter(); /// {@template links_count} /// Counts the linked objects in the database. /// /// It does not take the local state into account and can even be used /// without loading the link. /// {@endtemplate} Future count() => filter().count(); /// {@macro links_count} int countSync() => filter().countSync(); } ================================================ FILE: packages/isar/lib/src/isar_reader.dart ================================================ // ignore_for_file: public_member_api_docs part of isar; /// @nodoc @protected abstract class IsarReader { bool readBool(int offset); bool? readBoolOrNull(int offset); int readByte(int offset); int? readByteOrNull(int offset); int readInt(int offset); int? readIntOrNull(int offset); double readFloat(int offset); double? readFloatOrNull(int offset); int readLong(int offset); int? readLongOrNull(int offset); double readDouble(int offset); double? readDoubleOrNull(int offset); DateTime readDateTime(int offset); DateTime? readDateTimeOrNull(int offset); String readString(int offset); String? readStringOrNull(int offset); T? readObjectOrNull( int offset, Deserialize deserialize, Map> allOffsets, ); List? readBoolList(int offset); List? readBoolOrNullList(int offset); List? readByteList(int offset); List? readIntList(int offset); List? readIntOrNullList(int offset); List? readFloatList(int offset); List? readFloatOrNullList(int offset); List? readLongList(int offset); List? readLongOrNullList(int offset); List? readDoubleList(int offset); List? readDoubleOrNullList(int offset); List? readDateTimeList(int offset); List? readDateTimeOrNullList(int offset); List? readStringList(int offset); List? readStringOrNullList(int offset); List? readObjectList( int offset, Deserialize deserialize, Map> allOffsets, T defaultValue, ); List? readObjectOrNullList( int offset, Deserialize deserialize, Map> allOffsets, ); } ================================================ FILE: packages/isar/lib/src/isar_writer.dart ================================================ // ignore_for_file: public_member_api_docs part of isar; /// @nodoc @protected abstract class IsarWriter { void writeBool(int offset, bool? value); void writeByte(int offset, int value); void writeInt(int offset, int? value); void writeFloat(int offset, double? value); void writeLong(int offset, int? value); void writeDouble(int offset, double? value); void writeDateTime(int offset, DateTime? value); void writeString(int offset, String? value); void writeObject( int offset, Map> allOffsets, Serialize serialize, T? value, ); void writeByteList(int offset, List? values); void writeBoolList(int offset, List? values); void writeIntList(int offset, List? values); void writeFloatList(int offset, List? values); void writeLongList(int offset, List? values); void writeDoubleList(int offset, List? values); void writeDateTimeList(int offset, List? values); void writeStringList(int offset, List? values); void writeObjectList( int offset, Map> allOffsets, Serialize serialize, List? values, ); } ================================================ FILE: packages/isar/lib/src/native/bindings.dart ================================================ // ignore_for_file: camel_case_types, non_constant_identifier_names // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. import 'dart:ffi' as ffi; class IsarCoreBindings { /// Holds the symbol lookup function. final ffi.Pointer Function(String symbolName) _lookup; /// The symbols are looked up in [dynamicLibrary]. IsarCoreBindings(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; /// The symbols are looked up with [lookup]. IsarCoreBindings.fromLookup( ffi.Pointer Function(String symbolName) lookup) : _lookup = lookup; ffi.Pointer isar_find_word_boundaries( ffi.Pointer input_bytes, int length, ffi.Pointer number_words, ) { return _isar_find_word_boundaries( input_bytes, length, number_words, ); } late final _isar_find_word_boundariesPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function(ffi.Pointer, ffi.Uint32, ffi.Pointer)>>('isar_find_word_boundaries'); late final _isar_find_word_boundaries = _isar_find_word_boundariesPtr.asFunction< ffi.Pointer Function( ffi.Pointer, int, ffi.Pointer)>(); void isar_free_word_boundaries( ffi.Pointer boundaries, int word_count, ) { return _isar_free_word_boundaries( boundaries, word_count, ); } late final _isar_free_word_boundariesPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Uint32)>>('isar_free_word_boundaries'); late final _isar_free_word_boundaries = _isar_free_word_boundariesPtr .asFunction, int)>(); void isar_free_string( ffi.Pointer string, ) { return _isar_free_string( string, ); } late final _isar_free_stringPtr = _lookup)>>( 'isar_free_string'); late final _isar_free_string = _isar_free_stringPtr.asFunction)>(); ffi.Pointer isar_get_error( int err_code, ) { return _isar_get_error( err_code, ); } late final _isar_get_errorPtr = _lookup Function(ffi.Int64)>>( 'isar_get_error'); late final _isar_get_error = _isar_get_errorPtr.asFunction Function(int)>(); void isar_free_c_object_set( ffi.Pointer ros, ) { return _isar_free_c_object_set( ros, ); } late final _isar_free_c_object_setPtr = _lookup)>>( 'isar_free_c_object_set'); late final _isar_free_c_object_set = _isar_free_c_object_setPtr .asFunction)>(); int isar_get( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer object, ) { return _isar_get( collection, txn, object, ); } late final _isar_getPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_get'); late final _isar_get = _isar_getPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); int isar_get_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer key, ffi.Pointer object, ) { return _isar_get_by_index( collection, txn, index_id, key, object, ); } late final _isar_get_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer, ffi.Pointer)>>('isar_get_by_index'); late final _isar_get_by_index = _isar_get_by_indexPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer, ffi.Pointer)>(); int isar_get_all( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer objects, ) { return _isar_get_all( collection, txn, objects, ); } late final _isar_get_allPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_get_all'); late final _isar_get_all = _isar_get_allPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); int isar_get_all_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer> keys, ffi.Pointer objects, ) { return _isar_get_all_by_index( collection, txn, index_id, keys, objects, ); } late final _isar_get_all_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer>, ffi.Pointer)>>('isar_get_all_by_index'); late final _isar_get_all_by_index = _isar_get_all_by_indexPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer>, ffi.Pointer)>(); int isar_put( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer object, ) { return _isar_put( collection, txn, object, ); } late final _isar_putPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_put'); late final _isar_put = _isar_putPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); int isar_put_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer object, ) { return _isar_put_by_index( collection, txn, index_id, object, ); } late final _isar_put_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer)>>('isar_put_by_index'); late final _isar_put_by_index = _isar_put_by_indexPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer)>(); int isar_put_all( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer objects, ) { return _isar_put_all( collection, txn, objects, ); } late final _isar_put_allPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_put_all'); late final _isar_put_all = _isar_put_allPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); int isar_put_all_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer objects, ) { return _isar_put_all_by_index( collection, txn, index_id, objects, ); } late final _isar_put_all_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer)>>('isar_put_all_by_index'); late final _isar_put_all_by_index = _isar_put_all_by_indexPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer)>(); int isar_delete( ffi.Pointer collection, ffi.Pointer txn, int id, ffi.Pointer deleted, ) { return _isar_delete( collection, txn, id, deleted, ); } late final _isar_deletePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Int64, ffi.Pointer)>>('isar_delete'); late final _isar_delete = _isar_deletePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer)>(); int isar_delete_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer key, ffi.Pointer deleted, ) { return _isar_delete_by_index( collection, txn, index_id, key, deleted, ); } late final _isar_delete_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer, ffi.Pointer)>>('isar_delete_by_index'); late final _isar_delete_by_index = _isar_delete_by_indexPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer, ffi.Pointer)>(); int isar_delete_all( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer ids, int ids_length, ffi.Pointer count, ) { return _isar_delete_all( collection, txn, ids, ids_length, count, ); } late final _isar_delete_allPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Uint32, ffi.Pointer)>>('isar_delete_all'); late final _isar_delete_all = _isar_delete_allPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, int, ffi.Pointer)>(); int isar_delete_all_by_index( ffi.Pointer collection, ffi.Pointer txn, int index_id, ffi.Pointer> keys, int keys_length, ffi.Pointer count, ) { return _isar_delete_all_by_index( collection, txn, index_id, keys, keys_length, count, ); } late final _isar_delete_all_by_indexPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer>, ffi.Uint32, ffi.Pointer)>>('isar_delete_all_by_index'); late final _isar_delete_all_by_index = _isar_delete_all_by_indexPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer, int, ffi.Pointer>, int, ffi.Pointer)>(); int isar_clear( ffi.Pointer collection, ffi.Pointer txn, ) { return _isar_clear( collection, txn, ); } late final _isar_clearPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer)>>('isar_clear'); late final _isar_clear = _isar_clearPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer)>(); int isar_json_import( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer id_name, ffi.Pointer json_bytes, int json_length, ) { return _isar_json_import( collection, txn, id_name, json_bytes, json_length, ); } late final _isar_json_importPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Uint32)>>('isar_json_import'); late final _isar_json_import = _isar_json_importPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer, int)>(); int isar_count( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer count, ) { return _isar_count( collection, txn, count, ); } late final _isar_countPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_count'); late final _isar_count = _isar_countPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); int isar_get_size( ffi.Pointer collection, ffi.Pointer txn, bool include_indexes, bool include_links, ffi.Pointer size, ) { return _isar_get_size( collection, txn, include_indexes, include_links, size, ); } late final _isar_get_sizePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Bool, ffi.Bool, ffi.Pointer)>>('isar_get_size'); late final _isar_get_size = _isar_get_sizePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, bool, bool, ffi.Pointer)>(); int isar_verify( ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer objects, ) { return _isar_verify( collection, txn, objects, ); } late final _isar_verifyPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('isar_verify'); late final _isar_verify = _isar_verifyPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); void isar_connect_dart_api( DartPostCObjectFnType ptr, ) { return _isar_connect_dart_api( ptr, ); } late final _isar_connect_dart_apiPtr = _lookup>( 'isar_connect_dart_api'); late final _isar_connect_dart_api = _isar_connect_dart_apiPtr .asFunction(); void isar_filter_static( ffi.Pointer> filter, bool value, ) { return _isar_filter_static( filter, value, ); } late final _isar_filter_staticPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer>, ffi.Bool)>>('isar_filter_static'); late final _isar_filter_static = _isar_filter_staticPtr .asFunction>, bool)>(); void isar_filter_and_or_xor( ffi.Pointer> filter, bool and, bool exclusive, ffi.Pointer> conditions, int length, ) { return _isar_filter_and_or_xor( filter, and, exclusive, conditions, length, ); } late final _isar_filter_and_or_xorPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer>, ffi.Bool, ffi.Bool, ffi.Pointer>, ffi.Uint32)>>('isar_filter_and_or_xor'); late final _isar_filter_and_or_xor = _isar_filter_and_or_xorPtr.asFunction< void Function(ffi.Pointer>, bool, bool, ffi.Pointer>, int)>(); void isar_filter_not( ffi.Pointer> filter, ffi.Pointer condition, ) { return _isar_filter_not( filter, condition, ); } late final _isar_filter_notPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer>, ffi.Pointer)>>('isar_filter_not'); late final _isar_filter_not = _isar_filter_notPtr.asFunction< void Function(ffi.Pointer>, ffi.Pointer)>(); int isar_filter_object( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer condition, int embedded_col_id, int property_id, ) { return _isar_filter_object( collection, filter, condition, embedded_col_id, property_id, ); } late final _isar_filter_objectPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Uint64, ffi.Uint64)>>('isar_filter_object'); late final _isar_filter_object = _isar_filter_objectPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, ffi.Pointer, int, int)>(); int isar_filter_link( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer condition, int link_id, ) { return _isar_filter_link( collection, filter, condition, link_id, ); } late final _isar_filter_linkPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Uint64)>>('isar_filter_link'); late final _isar_filter_link = _isar_filter_linkPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, ffi.Pointer, int)>(); int isar_filter_link_length( ffi.Pointer collection, ffi.Pointer> filter, int lower, int upper, int link_id, ) { return _isar_filter_link_length( collection, filter, lower, upper, link_id, ); } late final _isar_filter_link_lengthPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Uint32, ffi.Uint32, ffi.Uint64)>>('isar_filter_link_length'); late final _isar_filter_link_length = _isar_filter_link_lengthPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, int, int, int)>(); int isar_filter_list_length( ffi.Pointer collection, ffi.Pointer> filter, int lower, int upper, int embedded_col_id, int property_id, ) { return _isar_filter_list_length( collection, filter, lower, upper, embedded_col_id, property_id, ); } late final _isar_filter_list_lengthPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Uint32, ffi.Uint32, ffi.Uint64, ffi.Uint64)>>('isar_filter_list_length'); late final _isar_filter_list_length = _isar_filter_list_lengthPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, int, int, int, int)>(); int isar_filter_null( ffi.Pointer collection, ffi.Pointer> filter, int embedded_col_id, int property_id, ) { return _isar_filter_null( collection, filter, embedded_col_id, property_id, ); } late final _isar_filter_nullPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Uint64, ffi.Uint64)>>('isar_filter_null'); late final _isar_filter_null = _isar_filter_nullPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, int, int)>(); void isar_filter_id( ffi.Pointer> filter, int lower, bool include_lower, int upper, bool include_upper, ) { return _isar_filter_id( filter, lower, include_lower, upper, include_upper, ); } late final _isar_filter_idPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer>, ffi.Int64, ffi.Bool, ffi.Int64, ffi.Bool)>>('isar_filter_id'); late final _isar_filter_id = _isar_filter_idPtr.asFunction< void Function(ffi.Pointer>, int, bool, int, bool)>(); int isar_filter_long( ffi.Pointer collection, ffi.Pointer> filter, int lower, bool include_lower, int upper, bool include_upper, int embedded_col_id, int property_id, ) { return _isar_filter_long( collection, filter, lower, include_lower, upper, include_upper, embedded_col_id, property_id, ); } late final _isar_filter_longPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Int64, ffi.Bool, ffi.Int64, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_long'); late final _isar_filter_long = _isar_filter_longPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, int, bool, int, bool, int, int)>(); int isar_filter_double( ffi.Pointer collection, ffi.Pointer> filter, double lower, double upper, int embedded_col_id, int property_id, ) { return _isar_filter_double( collection, filter, lower, upper, embedded_col_id, property_id, ); } late final _isar_filter_doublePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Double, ffi.Double, ffi.Uint64, ffi.Uint64)>>('isar_filter_double'); late final _isar_filter_double = _isar_filter_doublePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, double, double, int, int)>(); int isar_filter_string( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer lower, bool include_lower, ffi.Pointer upper, bool include_upper, bool case_sensitive, int embedded_col_id, int property_id, ) { return _isar_filter_string( collection, filter, lower, include_lower, upper, include_upper, case_sensitive, embedded_col_id, property_id, ); } late final _isar_filter_stringPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Bool, ffi.Pointer, ffi.Bool, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_string'); late final _isar_filter_string = _isar_filter_stringPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, bool, ffi.Pointer, bool, bool, int, int)>(); int isar_filter_string_starts_with( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer value, bool case_sensitive, int embedded_col_id, int property_id, ) { return _isar_filter_string_starts_with( collection, filter, value, case_sensitive, embedded_col_id, property_id, ); } late final _isar_filter_string_starts_withPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_string_starts_with'); late final _isar_filter_string_starts_with = _isar_filter_string_starts_withPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, bool, int, int)>(); int isar_filter_string_ends_with( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer value, bool case_sensitive, int embedded_col_id, int property_id, ) { return _isar_filter_string_ends_with( collection, filter, value, case_sensitive, embedded_col_id, property_id, ); } late final _isar_filter_string_ends_withPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_string_ends_with'); late final _isar_filter_string_ends_with = _isar_filter_string_ends_withPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, bool, int, int)>(); int isar_filter_string_contains( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer value, bool case_sensitive, int embedded_col_id, int property_id, ) { return _isar_filter_string_contains( collection, filter, value, case_sensitive, embedded_col_id, property_id, ); } late final _isar_filter_string_containsPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_string_contains'); late final _isar_filter_string_contains = _isar_filter_string_containsPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, bool, int, int)>(); int isar_filter_string_matches( ffi.Pointer collection, ffi.Pointer> filter, ffi.Pointer value, bool case_sensitive, int embedded_col_id, int property_id, ) { return _isar_filter_string_matches( collection, filter, value, case_sensitive, embedded_col_id, property_id, ); } late final _isar_filter_string_matchesPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, ffi.Bool, ffi.Uint64, ffi.Uint64)>>('isar_filter_string_matches'); late final _isar_filter_string_matches = _isar_filter_string_matchesPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer>, ffi.Pointer, bool, int, int)>(); void isar_key_create( ffi.Pointer> key, ) { return _isar_key_create( key, ); } late final _isar_key_createPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer>)>>('isar_key_create'); late final _isar_key_create = _isar_key_createPtr .asFunction>)>(); bool isar_key_increase( ffi.Pointer key, ) { return _isar_key_increase( key, ); } late final _isar_key_increasePtr = _lookup)>>( 'isar_key_increase'); late final _isar_key_increase = _isar_key_increasePtr.asFunction)>(); bool isar_key_decrease( ffi.Pointer key, ) { return _isar_key_decrease( key, ); } late final _isar_key_decreasePtr = _lookup)>>( 'isar_key_decrease'); late final _isar_key_decrease = _isar_key_decreasePtr.asFunction)>(); void isar_key_add_byte( ffi.Pointer key, int value, ) { return _isar_key_add_byte( key, value, ); } late final _isar_key_add_bytePtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Uint8)>>('isar_key_add_byte'); late final _isar_key_add_byte = _isar_key_add_bytePtr .asFunction, int)>(); void isar_key_add_int( ffi.Pointer key, int value, ) { return _isar_key_add_int( key, value, ); } late final _isar_key_add_intPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Int32)>>('isar_key_add_int'); late final _isar_key_add_int = _isar_key_add_intPtr .asFunction, int)>(); void isar_key_add_long( ffi.Pointer key, int value, ) { return _isar_key_add_long( key, value, ); } late final _isar_key_add_longPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Int64)>>('isar_key_add_long'); late final _isar_key_add_long = _isar_key_add_longPtr .asFunction, int)>(); void isar_key_add_float( ffi.Pointer key, double value, ) { return _isar_key_add_float( key, value, ); } late final _isar_key_add_floatPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Double)>>('isar_key_add_float'); late final _isar_key_add_float = _isar_key_add_floatPtr .asFunction, double)>(); void isar_key_add_double( ffi.Pointer key, double value, ) { return _isar_key_add_double( key, value, ); } late final _isar_key_add_doublePtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Double)>>('isar_key_add_double'); late final _isar_key_add_double = _isar_key_add_doublePtr .asFunction, double)>(); void isar_key_add_string( ffi.Pointer key, ffi.Pointer value, bool case_sensitive, ) { return _isar_key_add_string( key, value, case_sensitive, ); } late final _isar_key_add_stringPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Bool)>>('isar_key_add_string'); late final _isar_key_add_string = _isar_key_add_stringPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, bool)>(); void isar_key_add_string_hash( ffi.Pointer key, ffi.Pointer value, bool case_sensitive, ) { return _isar_key_add_string_hash( key, value, case_sensitive, ); } late final _isar_key_add_string_hashPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Bool)>>('isar_key_add_string_hash'); late final _isar_key_add_string_hash = _isar_key_add_string_hashPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, bool)>(); void isar_key_add_string_list_hash( ffi.Pointer key, ffi.Pointer> value, int length, bool case_sensitive, ) { return _isar_key_add_string_list_hash( key, value, length, case_sensitive, ); } late final _isar_key_add_string_list_hashPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Pointer>, ffi.Uint32, ffi.Bool)>>('isar_key_add_string_list_hash'); late final _isar_key_add_string_list_hash = _isar_key_add_string_list_hashPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer>, int, bool)>(); void isar_key_add_byte_list_hash( ffi.Pointer key, ffi.Pointer value, int length, ) { return _isar_key_add_byte_list_hash( key, value, length, ); } late final _isar_key_add_byte_list_hashPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Uint32)>>('isar_key_add_byte_list_hash'); late final _isar_key_add_byte_list_hash = _isar_key_add_byte_list_hashPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, int)>(); void isar_key_add_int_list_hash( ffi.Pointer key, ffi.Pointer value, int length, ) { return _isar_key_add_int_list_hash( key, value, length, ); } late final _isar_key_add_int_list_hashPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Uint32)>>('isar_key_add_int_list_hash'); late final _isar_key_add_int_list_hash = _isar_key_add_int_list_hashPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, int)>(); void isar_key_add_long_list_hash( ffi.Pointer key, ffi.Pointer value, int length, ) { return _isar_key_add_long_list_hash( key, value, length, ); } late final _isar_key_add_long_list_hashPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, ffi.Uint32)>>('isar_key_add_long_list_hash'); late final _isar_key_add_long_list_hash = _isar_key_add_long_list_hashPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, int)>(); ffi.Pointer isar_version() { return _isar_version(); } late final _isar_versionPtr = _lookup Function()>>( 'isar_version'); late final _isar_version = _isar_versionPtr.asFunction Function()>(); int isar_instance_create( ffi.Pointer> isar, ffi.Pointer name, ffi.Pointer path, ffi.Pointer schema_json, int max_size_mib, bool relaxed_durability, int compact_min_file_size, int compact_min_bytes, double compact_min_ratio, ) { return _isar_instance_create( isar, name, path, schema_json, max_size_mib, relaxed_durability, compact_min_file_size, compact_min_bytes, compact_min_ratio, ); } late final _isar_instance_createPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer>, ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Int64, ffi.Bool, ffi.Uint32, ffi.Uint32, ffi.Double)>>('isar_instance_create'); late final _isar_instance_create = _isar_instance_createPtr.asFunction< int Function( ffi.Pointer>, ffi.Pointer, ffi.Pointer, ffi.Pointer, int, bool, int, int, double)>(); void isar_instance_create_async( ffi.Pointer> isar, ffi.Pointer name, ffi.Pointer path, ffi.Pointer schema_json, int max_size_mib, bool relaxed_durability, int compact_min_file_size, int compact_min_bytes, double compact_min_ratio, int port, ) { return _isar_instance_create_async( isar, name, path, schema_json, max_size_mib, relaxed_durability, compact_min_file_size, compact_min_bytes, compact_min_ratio, port, ); } late final _isar_instance_create_asyncPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer>, ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Int64, ffi.Bool, ffi.Uint32, ffi.Uint32, ffi.Double, DartPort)>>('isar_instance_create_async'); late final _isar_instance_create_async = _isar_instance_create_asyncPtr.asFunction< void Function( ffi.Pointer>, ffi.Pointer, ffi.Pointer, ffi.Pointer, int, bool, int, int, double, int)>(); bool isar_instance_close( ffi.Pointer isar, ) { return _isar_instance_close( isar, ); } late final _isar_instance_closePtr = _lookup< ffi.NativeFunction)>>( 'isar_instance_close'); late final _isar_instance_close = _isar_instance_closePtr .asFunction)>(); bool isar_instance_close_and_delete( ffi.Pointer isar, ) { return _isar_instance_close_and_delete( isar, ); } late final _isar_instance_close_and_deletePtr = _lookup< ffi.NativeFunction)>>( 'isar_instance_close_and_delete'); late final _isar_instance_close_and_delete = _isar_instance_close_and_deletePtr .asFunction)>(); ffi.Pointer isar_instance_get_path( ffi.Pointer isar, ) { return _isar_instance_get_path( isar, ); } late final _isar_instance_get_pathPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer)>>('isar_instance_get_path'); late final _isar_instance_get_path = _isar_instance_get_pathPtr .asFunction Function(ffi.Pointer)>(); int isar_instance_get_collection( ffi.Pointer isar, ffi.Pointer> collection, int collection_id, ) { return _isar_instance_get_collection( isar, collection, collection_id, ); } late final _isar_instance_get_collectionPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Uint64)>>('isar_instance_get_collection'); late final _isar_instance_get_collection = _isar_instance_get_collectionPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, int)>(); int isar_instance_get_size( ffi.Pointer instance, ffi.Pointer txn, bool include_indexes, bool include_links, ffi.Pointer size, ) { return _isar_instance_get_size( instance, txn, include_indexes, include_links, size, ); } late final _isar_instance_get_sizePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Bool, ffi.Bool, ffi.Pointer)>>('isar_instance_get_size'); late final _isar_instance_get_size = _isar_instance_get_sizePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, bool, bool, ffi.Pointer)>(); void isar_instance_copy_to_file( ffi.Pointer instance, ffi.Pointer path, int port, ) { return _isar_instance_copy_to_file( instance, path, port, ); } late final _isar_instance_copy_to_filePtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, DartPort)>>('isar_instance_copy_to_file'); late final _isar_instance_copy_to_file = _isar_instance_copy_to_filePtr.asFunction< void Function( ffi.Pointer, ffi.Pointer, int)>(); int isar_instance_verify( ffi.Pointer instance, ffi.Pointer txn, ) { return _isar_instance_verify( instance, txn, ); } late final _isar_instance_verifyPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer)>>('isar_instance_verify'); late final _isar_instance_verify = _isar_instance_verifyPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer)>(); int isar_get_offsets( ffi.Pointer collection, int embedded_col_id, ffi.Pointer offsets, ) { return _isar_get_offsets( collection, embedded_col_id, offsets, ); } late final _isar_get_offsetsPtr = _lookup< ffi.NativeFunction< ffi.Uint32 Function(ffi.Pointer, ffi.Uint64, ffi.Pointer)>>('isar_get_offsets'); late final _isar_get_offsets = _isar_get_offsetsPtr.asFunction< int Function( ffi.Pointer, int, ffi.Pointer)>(); int isar_link( ffi.Pointer collection, ffi.Pointer txn, int link_id, int id, int target_id, ) { return _isar_link( collection, txn, link_id, id, target_id, ); } late final _isar_linkPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Int64, ffi.Int64)>>('isar_link'); late final _isar_link = _isar_linkPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, int, int)>(); int isar_link_unlink( ffi.Pointer collection, ffi.Pointer txn, int link_id, int id, int target_id, ) { return _isar_link_unlink( collection, txn, link_id, id, target_id, ); } late final _isar_link_unlinkPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Int64, ffi.Int64)>>('isar_link_unlink'); late final _isar_link_unlink = _isar_link_unlinkPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, int, int)>(); int isar_link_unlink_all( ffi.Pointer collection, ffi.Pointer txn, int link_id, int id, ) { return _isar_link_unlink_all( collection, txn, link_id, id, ); } late final _isar_link_unlink_allPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Int64)>>('isar_link_unlink_all'); late final _isar_link_unlink_all = _isar_link_unlink_allPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer, int, int)>(); int isar_link_update_all( ffi.Pointer collection, ffi.Pointer txn, int link_id, int id, ffi.Pointer ids, int link_count, int unlink_count, bool replace, ) { return _isar_link_update_all( collection, txn, link_id, id, ids, link_count, unlink_count, replace, ); } late final _isar_link_update_allPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Int64, ffi.Pointer, ffi.Uint32, ffi.Uint32, ffi.Bool)>>('isar_link_update_all'); late final _isar_link_update_all = _isar_link_update_allPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, int, ffi.Pointer, int, int, bool)>(); int isar_link_verify( ffi.Pointer collection, ffi.Pointer txn, int link_id, ffi.Pointer ids, int ids_count, ) { return _isar_link_verify( collection, txn, link_id, ids, ids_count, ); } late final _isar_link_verifyPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Pointer, ffi.Uint32)>>('isar_link_verify'); late final _isar_link_verify = _isar_link_verifyPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, ffi.Pointer, int)>(); ffi.Pointer isar_qb_create( ffi.Pointer collection, ) { return _isar_qb_create( collection, ); } late final _isar_qb_createPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer)>>('isar_qb_create'); late final _isar_qb_create = _isar_qb_createPtr.asFunction< ffi.Pointer Function(ffi.Pointer)>(); int isar_qb_add_id_where_clause( ffi.Pointer builder, int start_id, int end_id, ) { return _isar_qb_add_id_where_clause( builder, start_id, end_id, ); } late final _isar_qb_add_id_where_clausePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Int64, ffi.Int64)>>('isar_qb_add_id_where_clause'); late final _isar_qb_add_id_where_clause = _isar_qb_add_id_where_clausePtr .asFunction, int, int)>(); int isar_qb_add_index_where_clause( ffi.Pointer builder, int index_id, ffi.Pointer lower_key, ffi.Pointer upper_key, bool sort_asc, bool skip_duplicates, ) { return _isar_qb_add_index_where_clause( builder, index_id, lower_key, upper_key, sort_asc, skip_duplicates, ); } late final _isar_qb_add_index_where_clausePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Uint64, ffi.Pointer, ffi.Pointer, ffi.Bool, ffi.Bool)>>('isar_qb_add_index_where_clause'); late final _isar_qb_add_index_where_clause = _isar_qb_add_index_where_clausePtr.asFunction< int Function(ffi.Pointer, int, ffi.Pointer, ffi.Pointer, bool, bool)>(); int isar_qb_add_link_where_clause( ffi.Pointer builder, ffi.Pointer source_collection, int link_id, int id, ) { return _isar_qb_add_link_where_clause( builder, source_collection, link_id, id, ); } late final _isar_qb_add_link_where_clausePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Uint64, ffi.Int64)>>('isar_qb_add_link_where_clause'); late final _isar_qb_add_link_where_clause = _isar_qb_add_link_where_clausePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, int, int)>(); void isar_qb_set_filter( ffi.Pointer builder, ffi.Pointer filter, ) { return _isar_qb_set_filter( builder, filter, ); } late final _isar_qb_set_filterPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer)>>('isar_qb_set_filter'); late final _isar_qb_set_filter = _isar_qb_set_filterPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer)>(); int isar_qb_add_sort_by( ffi.Pointer builder, int property_id, bool asc, ) { return _isar_qb_add_sort_by( builder, property_id, asc, ); } late final _isar_qb_add_sort_byPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Uint64, ffi.Bool)>>('isar_qb_add_sort_by'); late final _isar_qb_add_sort_by = _isar_qb_add_sort_byPtr .asFunction, int, bool)>(); int isar_qb_add_distinct_by( ffi.Pointer builder, int property_id, bool case_sensitive, ) { return _isar_qb_add_distinct_by( builder, property_id, case_sensitive, ); } late final _isar_qb_add_distinct_byPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Uint64, ffi.Bool)>>('isar_qb_add_distinct_by'); late final _isar_qb_add_distinct_by = _isar_qb_add_distinct_byPtr .asFunction, int, bool)>(); void isar_qb_set_offset_limit( ffi.Pointer builder, int offset, int limit, ) { return _isar_qb_set_offset_limit( builder, offset, limit, ); } late final _isar_qb_set_offset_limitPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Int64, ffi.Int64)>>('isar_qb_set_offset_limit'); late final _isar_qb_set_offset_limit = _isar_qb_set_offset_limitPtr .asFunction, int, int)>(); ffi.Pointer isar_qb_build( ffi.Pointer builder, ) { return _isar_qb_build( builder, ); } late final _isar_qb_buildPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer)>>('isar_qb_build'); late final _isar_qb_build = _isar_qb_buildPtr .asFunction Function(ffi.Pointer)>(); void isar_q_free( ffi.Pointer query, ) { return _isar_q_free( query, ); } late final _isar_q_freePtr = _lookup)>>( 'isar_q_free'); late final _isar_q_free = _isar_q_freePtr.asFunction)>(); int isar_q_find( ffi.Pointer query, ffi.Pointer txn, ffi.Pointer result, int limit, ) { return _isar_q_find( query, txn, result, limit, ); } late final _isar_q_findPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Uint32)>>('isar_q_find'); late final _isar_q_find = _isar_q_findPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, int)>(); int isar_q_delete( ffi.Pointer query, ffi.Pointer collection, ffi.Pointer txn, int limit, ffi.Pointer count, ) { return _isar_q_delete( query, collection, txn, limit, count, ); } late final _isar_q_deletePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Uint32, ffi.Pointer)>>('isar_q_delete'); late final _isar_q_delete = _isar_q_deletePtr.asFunction< int Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, int, ffi.Pointer)>(); int isar_q_export_json( ffi.Pointer query, ffi.Pointer collection, ffi.Pointer txn, ffi.Pointer id_name, ffi.Pointer> json_bytes, ffi.Pointer json_length, ) { return _isar_q_export_json( query, collection, txn, id_name, json_bytes, json_length, ); } late final _isar_q_export_jsonPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer>, ffi.Pointer)>>('isar_q_export_json'); late final _isar_q_export_json = _isar_q_export_jsonPtr.asFunction< int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Pointer>, ffi.Pointer)>(); void isar_free_json( ffi.Pointer json_bytes, int json_length, ) { return _isar_free_json( json_bytes, json_length, ); } late final _isar_free_jsonPtr = _lookup< ffi.NativeFunction< ffi.Void Function( ffi.Pointer, ffi.Uint32)>>('isar_free_json'); late final _isar_free_json = _isar_free_jsonPtr .asFunction, int)>(); int isar_q_aggregate( ffi.Pointer collection, ffi.Pointer query, ffi.Pointer txn, int operation, int property_id, ffi.Pointer> result, ) { return _isar_q_aggregate( collection, query, txn, operation, property_id, result, ); } late final _isar_q_aggregatePtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, ffi.Uint8, ffi.Uint64, ffi.Pointer>)>>( 'isar_q_aggregate'); late final _isar_q_aggregate = _isar_q_aggregatePtr.asFunction< int Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, int, int, ffi.Pointer>)>(); int isar_q_aggregate_long_result( ffi.Pointer result, ) { return _isar_q_aggregate_long_result( result, ); } late final _isar_q_aggregate_long_resultPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function(ffi.Pointer)>>( 'isar_q_aggregate_long_result'); late final _isar_q_aggregate_long_result = _isar_q_aggregate_long_resultPtr .asFunction)>(); double isar_q_aggregate_double_result( ffi.Pointer result, ) { return _isar_q_aggregate_double_result( result, ); } late final _isar_q_aggregate_double_resultPtr = _lookup< ffi.NativeFunction< ffi.Double Function(ffi.Pointer)>>( 'isar_q_aggregate_double_result'); late final _isar_q_aggregate_double_result = _isar_q_aggregate_double_resultPtr .asFunction)>(); int isar_txn_begin( ffi.Pointer isar, ffi.Pointer> txn, bool sync1, bool write, bool silent, int port, ) { return _isar_txn_begin( isar, txn, sync1, write, silent, port, ); } late final _isar_txn_beginPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Pointer>, ffi.Bool, ffi.Bool, ffi.Bool, DartPort)>>('isar_txn_begin'); late final _isar_txn_begin = _isar_txn_beginPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer>, bool, bool, bool, int)>(); int isar_txn_finish( ffi.Pointer txn, bool commit, ) { return _isar_txn_finish( txn, commit, ); } late final _isar_txn_finishPtr = _lookup< ffi.NativeFunction< ffi.Int64 Function( ffi.Pointer, ffi.Bool)>>('isar_txn_finish'); late final _isar_txn_finish = _isar_txn_finishPtr .asFunction, bool)>(); ffi.Pointer isar_watch_collection( ffi.Pointer isar, ffi.Pointer collection, int port, ) { return _isar_watch_collection( isar, collection, port, ); } late final _isar_watch_collectionPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, DartPort)>>('isar_watch_collection'); late final _isar_watch_collection = _isar_watch_collectionPtr.asFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, int)>(); ffi.Pointer isar_watch_object( ffi.Pointer isar, ffi.Pointer collection, int id, int port, ) { return _isar_watch_object( isar, collection, id, port, ); } late final _isar_watch_objectPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ffi.Int64, DartPort)>>('isar_watch_object'); late final _isar_watch_object = _isar_watch_objectPtr.asFunction< ffi.Pointer Function(ffi.Pointer, ffi.Pointer, int, int)>(); ffi.Pointer isar_watch_query( ffi.Pointer isar, ffi.Pointer collection, ffi.Pointer query, int port, ) { return _isar_watch_query( isar, collection, query, port, ); } late final _isar_watch_queryPtr = _lookup< ffi.NativeFunction< ffi.Pointer Function( ffi.Pointer, ffi.Pointer, ffi.Pointer, DartPort)>>('isar_watch_query'); late final _isar_watch_query = _isar_watch_queryPtr.asFunction< ffi.Pointer Function(ffi.Pointer, ffi.Pointer, ffi.Pointer, int)>(); void isar_stop_watching( ffi.Pointer handle, ) { return _isar_stop_watching( handle, ); } late final _isar_stop_watchingPtr = _lookup)>>( 'isar_stop_watching'); late final _isar_stop_watching = _isar_stop_watchingPtr .asFunction)>(); } class CObject extends ffi.Struct { @ffi.Int64() external int id; external ffi.Pointer buffer; @ffi.Uint32() external int buffer_length; } class CObjectSet extends ffi.Struct { external ffi.Pointer objects; @ffi.Uint32() external int length; } class CIsarCollection extends ffi.Opaque {} class CIsarTxn extends ffi.Opaque {} class CIndexKey extends ffi.Opaque {} typedef DartPostCObjectFnType = ffi.Pointer< ffi.NativeFunction)>>; typedef DartPort = ffi.Int64; class CDartCObject extends ffi.Opaque {} class CFilter extends ffi.Opaque {} class CIsarInstance extends ffi.Opaque {} class CQueryBuilder extends ffi.Opaque {} class CQuery extends ffi.Opaque {} class CAggregationResult extends ffi.Opaque {} class CWatchHandle extends ffi.Opaque {} const int IsarIndex_MAX_STRING_INDEX_SIZE = 1024; const int IsarObject_NULL_BYTE = 0; const int IsarObject_NULL_BOOL = 0; const int IsarObject_FALSE_BOOL = 1; const int IsarObject_TRUE_BOOL = 2; const int IsarObject_NULL_INT = -2147483648; const int IsarObject_NULL_LONG = -9223372036854775808; const int IsarObject_MAX_SIZE = 33554432; const int SchemaManager_ISAR_FILE_VERSION = 2; ================================================ FILE: packages/isar/lib/src/native/encode_string.dart ================================================ import 'dart:ffi'; import 'dart:typed_data'; const int _oneByteLimit = 0x7f; // 7 bits const int _twoByteLimit = 0x7ff; // 11 bits const int _surrogateTagMask = 0xFC00; const int _surrogateValueMask = 0x3FF; const int _leadSurrogateMin = 0xD800; /// Encodes a Dart String to UTF8, writes it at [offset] into [buffer] and /// returns the number of written bytes. /// /// The buffer needs to have a capacity of at least `offset + str.length * 3`. int encodeString(String str, Uint8List buffer, int offset) { final startOffset = offset; for (var stringIndex = 0; stringIndex < str.length; stringIndex++) { final codeUnit = str.codeUnitAt(stringIndex); // ASCII has the same representation in UTF-8 and UTF-16. if (codeUnit <= _oneByteLimit) { buffer[offset++] = codeUnit; } else if ((codeUnit & _surrogateTagMask) == _leadSurrogateMin) { // combine surrogate pair final nextCodeUnit = str.codeUnitAt(++stringIndex); final rune = 0x10000 + ((codeUnit & _surrogateValueMask) << 10) | (nextCodeUnit & _surrogateValueMask); // If the rune is encoded with 2 code-units then it must be encoded // with 4 bytes in UTF-8. buffer[offset++] = 0xF0 | (rune >> 18); buffer[offset++] = 0x80 | ((rune >> 12) & 0x3f); buffer[offset++] = 0x80 | ((rune >> 6) & 0x3f); buffer[offset++] = 0x80 | (rune & 0x3f); } else if (codeUnit <= _twoByteLimit) { buffer[offset++] = 0xC0 | (codeUnit >> 6); buffer[offset++] = 0x80 | (codeUnit & 0x3f); } else { buffer[offset++] = 0xE0 | (codeUnit >> 12); buffer[offset++] = 0x80 | ((codeUnit >> 6) & 0x3f); buffer[offset++] = 0x80 | (codeUnit & 0x3f); } } return offset - startOffset; } /// @nodoc extension CString on String { /// Create a zero terminated C-String from a Dart String Pointer toCString(Allocator alloc) { final bufferPtr = alloc(length * 3 + 1); final buffer = bufferPtr.asTypedList(length * 3 + 1); final size = encodeString(this, buffer, 0); buffer[size] = 0; return bufferPtr.cast(); } } ================================================ FILE: packages/isar/lib/src/native/index_key.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_writer_impl.dart'; final _keyPtrPtr = malloc>(); Pointer buildIndexKey( CollectionSchema schema, IndexSchema index, IndexKey key, ) { if (key.length > index.properties.length) { throw IsarError('Invalid number of values for index ${index.name}.'); } IC.isar_key_create(_keyPtrPtr); final keyPtr = _keyPtrPtr.value; for (var i = 0; i < key.length; i++) { final indexProperty = index.properties[i]; _addKeyValue( keyPtr, key[i], schema.property(indexProperty.name), indexProperty.type, indexProperty.caseSensitive, ); } return keyPtr; } Pointer buildLowerUnboundedIndexKey() { IC.isar_key_create(_keyPtrPtr); return _keyPtrPtr.value; } Pointer buildUpperUnboundedIndexKey() { IC.isar_key_create(_keyPtrPtr); final keyPtr = _keyPtrPtr.value; IC.isar_key_add_long(keyPtr, maxLong); return keyPtr; } void _addKeyValue( Pointer keyPtr, Object? value, PropertySchema property, IndexType type, bool caseSensitive, ) { if (property.enumMap != null) { if (value is Enum) { value = property.enumMap![value.name]; } else if (value is List) { value = value.map((e) { if (e is Enum) { return property.enumMap![e.name]; } else { return e; } }).toList(); } } final isarType = type != IndexType.hash ? property.type.scalarType : property.type; switch (isarType) { case IsarType.bool: IC.isar_key_add_byte(keyPtr, (value as bool?).byteValue); break; case IsarType.byte: IC.isar_key_add_byte(keyPtr, (value ?? 0) as int); break; case IsarType.int: IC.isar_key_add_int(keyPtr, (value as int?) ?? nullInt); break; case IsarType.float: IC.isar_key_add_float(keyPtr, (value as double?) ?? nullFloat); break; case IsarType.long: IC.isar_key_add_long(keyPtr, (value as int?) ?? nullLong); break; case IsarType.double: IC.isar_key_add_double(keyPtr, (value as double?) ?? nullDouble); break; case IsarType.dateTime: IC.isar_key_add_long(keyPtr, (value as DateTime?).longValue); break; case IsarType.string: final strPtr = _strToNative(value as String?); if (type == IndexType.value) { IC.isar_key_add_string(keyPtr, strPtr, caseSensitive); } else { IC.isar_key_add_string_hash(keyPtr, strPtr, caseSensitive); } _freeStr(strPtr); break; case IsarType.boolList: if (value == null) { IC.isar_key_add_byte_list_hash(keyPtr, nullptr, 0); } else { value as List; final boolListPtr = malloc(value.length); boolListPtr .asTypedList(value.length) .setAll(0, value.map((e) => e.byteValue)); IC.isar_key_add_byte_list_hash(keyPtr, boolListPtr, value.length); malloc.free(boolListPtr); } break; case IsarType.byteList: if (value == null) { IC.isar_key_add_byte_list_hash(keyPtr, nullptr, 0); } else { value as List; final bytesPtr = malloc(value.length); bytesPtr.asTypedList(value.length).setAll(0, value); IC.isar_key_add_byte_list_hash(keyPtr, bytesPtr, value.length); malloc.free(bytesPtr); } break; case IsarType.intList: if (value == null) { IC.isar_key_add_int_list_hash(keyPtr, nullptr, 0); } else { value as List; final intListPtr = malloc(value.length); intListPtr .asTypedList(value.length) .setAll(0, value.map((e) => e ?? nullInt)); IC.isar_key_add_int_list_hash(keyPtr, intListPtr, value.length); malloc.free(intListPtr); } break; case IsarType.longList: if (value == null) { IC.isar_key_add_long_list_hash(keyPtr, nullptr, 0); } else { value as List; final longListPtr = malloc(value.length); longListPtr .asTypedList(value.length) .setAll(0, value.map((e) => e ?? nullLong)); IC.isar_key_add_long_list_hash(keyPtr, longListPtr, value.length); malloc.free(longListPtr); } break; case IsarType.dateTimeList: if (value == null) { IC.isar_key_add_long_list_hash(keyPtr, nullptr, 0); } else { value as List; final longListPtr = malloc(value.length); for (var i = 0; i < value.length; i++) { longListPtr[i] = value[i].longValue; } IC.isar_key_add_long_list_hash(keyPtr, longListPtr, value.length); } break; case IsarType.stringList: if (value == null) { IC.isar_key_add_string_list_hash(keyPtr, nullptr, 0, false); } else { value as List; final stringListPtr = malloc>(value.length); for (var i = 0; i < value.length; i++) { stringListPtr[i] = _strToNative(value[i]); } IC.isar_key_add_string_list_hash( keyPtr, stringListPtr, value.length, caseSensitive, ); for (var i = 0; i < value.length; i++) { _freeStr(stringListPtr[i]); } } break; case IsarType.object: case IsarType.floatList: case IsarType.doubleList: case IsarType.objectList: throw IsarError('Unsupported property type.'); } } Pointer _strToNative(String? str) { if (str == null) { return Pointer.fromAddress(0); } else { return str.toCString(malloc); } } void _freeStr(Pointer strPtr) { if (!strPtr.isNull) { malloc.free(strPtr); } } double? adjustFloatBound({ required double? value, required bool lowerBound, required bool include, required double epsilon, }) { value ??= double.nan; if (lowerBound) { if (include) { if (value.isFinite) { return value - epsilon; } } else { if (value.isNaN) { return double.negativeInfinity; } else if (value == double.negativeInfinity) { return -double.maxFinite; } else if (value == double.maxFinite) { return double.infinity; } else if (value == double.infinity) { return null; } else { return value + epsilon; } } } else { if (include) { if (value.isFinite) { return value + epsilon; } } else { if (value.isNaN) { return null; } else if (value == double.negativeInfinity) { return double.nan; } else if (value == -double.maxFinite) { return double.negativeInfinity; } else if (value == double.infinity) { return double.maxFinite; } else { return value - epsilon; } } } return value; } ================================================ FILE: packages/isar/lib/src/native/isar_collection_impl.dart ================================================ // ignore_for_file: public_member_api_docs, invalid_use_of_protected_member import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/index_key.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_impl.dart'; import 'package:isar/src/native/isar_reader_impl.dart'; import 'package:isar/src/native/isar_writer_impl.dart'; import 'package:isar/src/native/query_build.dart'; import 'package:isar/src/native/txn.dart'; class IsarCollectionImpl extends IsarCollection { IsarCollectionImpl({ required this.isar, required this.ptr, required this.schema, }); @override final IsarImpl isar; final Pointer ptr; @override final CollectionSchema schema; late final _offsets = isar.offsets[OBJ]!; late final _staticSize = _offsets.last; @pragma('vm:prefer-inline') OBJ deserializeObject(CObject cObj) { final buffer = cObj.buffer.asTypedList(cObj.buffer_length); final reader = IsarReaderImpl(buffer); final object = schema.deserialize( cObj.id, reader, _offsets, isar.offsets, ); schema.attach(this, cObj.id, object); return object; } @pragma('vm:prefer-inline') OBJ? deserializeObjectOrNull(CObject cObj) { if (!cObj.buffer.isNull) { return deserializeObject(cObj); } else { return null; } } @pragma('vm:prefer-inline') List deserializeObjects(CObjectSet objectSet) { final objects = []; for (var i = 0; i < objectSet.length; i++) { final cObjPtr = objectSet.objects.elementAt(i); final object = deserializeObject(cObjPtr.ref); objects.add(object); } return objects; } @pragma('vm:prefer-inline') List deserializeObjectsOrNull(CObjectSet objectSet) { final objects = List.filled(objectSet.length, null); for (var i = 0; i < objectSet.length; i++) { final cObj = objectSet.objects.elementAt(i).ref; if (!cObj.buffer.isNull) { objects[i] = deserializeObject(cObj); } } return objects; } @pragma('vm:prefer-inline') Pointer> _getKeysPtr( String indexName, List keys, Allocator alloc, ) { final keysPtrPtr = alloc>(keys.length); for (var i = 0; i < keys.length; i++) { keysPtrPtr[i] = buildIndexKey(schema, schema.index(indexName), keys[i]); } return keysPtrPtr; } List deserializeProperty(CObjectSet objectSet, int? propertyId) { final values = []; if (propertyId != null) { final propertyOffset = _offsets[propertyId]; for (var i = 0; i < objectSet.length; i++) { final cObj = objectSet.objects.elementAt(i).ref; final buffer = cObj.buffer.asTypedList(cObj.buffer_length); values.add( schema.deserializeProp( IsarReaderImpl(buffer), propertyId, propertyOffset, isar.offsets, ) as T, ); } } else { for (var i = 0; i < objectSet.length; i++) { final cObj = objectSet.objects.elementAt(i).ref; values.add(cObj.id as T); } } return values; } void serializeObjects( Txn txn, Pointer objectsPtr, List objects, ) { var maxBufferSize = 0; for (var i = 0; i < objects.length; i++) { final object = objects[i]; maxBufferSize += schema.estimateSize(object, _offsets, isar.offsets); } final bufferPtr = txn.alloc(maxBufferSize); final buffer = bufferPtr.asTypedList(maxBufferSize).buffer; var writtenBytes = 0; for (var i = 0; i < objects.length; i++) { final objBuffer = buffer.asUint8List(writtenBytes); final binaryWriter = IsarWriterImpl(objBuffer, _staticSize); final object = objects[i]; schema.serialize( object, binaryWriter, _offsets, isar.offsets, ); final size = binaryWriter.usedBytes; final cObj = objectsPtr.elementAt(i).ref; cObj.id = schema.getId(object); cObj.buffer = bufferPtr.elementAt(writtenBytes); cObj.buffer_length = size; writtenBytes += size; } } @override Future> getAll(List ids) { return isar.getTxn(false, (Txn txn) async { final cObjSetPtr = txn.newCObjectSet(ids.length); final objectsPtr = cObjSetPtr.ref.objects; for (var i = 0; i < ids.length; i++) { objectsPtr.elementAt(i).ref.id = ids[i]; } IC.isar_get_all(ptr, txn.ptr, cObjSetPtr); await txn.wait(); return deserializeObjectsOrNull(cObjSetPtr.ref); }); } @override List getAllSync(List ids) { return isar.getTxnSync(false, (Txn txn) { final cObjPtr = txn.getCObject(); final cObj = cObjPtr.ref; final objects = List.filled(ids.length, null); for (var i = 0; i < ids.length; i++) { cObj.id = ids[i]; nCall(IC.isar_get(ptr, txn.ptr, cObjPtr)); objects[i] = deserializeObjectOrNull(cObj); } return objects; }); } @override Future> getAllByIndex(String indexName, List keys) { return isar.getTxn(false, (Txn txn) async { final cObjSetPtr = txn.newCObjectSet(keys.length); final keysPtrPtr = _getKeysPtr(indexName, keys, txn.alloc); IC.isar_get_all_by_index( ptr, txn.ptr, schema.index(indexName).id, keysPtrPtr, cObjSetPtr, ); await txn.wait(); return deserializeObjectsOrNull(cObjSetPtr.ref); }); } @override List getAllByIndexSync(String indexName, List keys) { final index = schema.index(indexName); return isar.getTxnSync(false, (Txn txn) { final cObjPtr = txn.getCObject(); final cObj = cObjPtr.ref; final objects = List.filled(keys.length, null); for (var i = 0; i < keys.length; i++) { final keyPtr = buildIndexKey(schema, index, keys[i]); nCall(IC.isar_get_by_index(ptr, txn.ptr, index.id, keyPtr, cObjPtr)); objects[i] = deserializeObjectOrNull(cObj); } return objects; }); } @override int putSync(OBJ object, {bool saveLinks = true}) { return isar.getTxnSync(true, (Txn txn) { return putByIndexSyncInternal( txn: txn, object: object, saveLinks: saveLinks, ); }); } @override int putByIndexSync(String indexName, OBJ object, {bool saveLinks = true}) { return isar.getTxnSync(true, (Txn txn) { return putByIndexSyncInternal( txn: txn, object: object, indexId: schema.index(indexName).id, saveLinks: saveLinks, ); }); } int putByIndexSyncInternal({ required Txn txn, int? indexId, required OBJ object, bool saveLinks = true, }) { final cObjPtr = txn.getCObject(); final cObj = cObjPtr.ref; final estimatedSize = schema.estimateSize(object, _offsets, isar.offsets); cObj.buffer = txn.getBuffer(estimatedSize); final buffer = cObj.buffer.asTypedList(estimatedSize); final writer = IsarWriterImpl(buffer, _staticSize); schema.serialize( object, writer, _offsets, isar.offsets, ); cObj.buffer_length = writer.usedBytes; cObj.id = schema.getId(object); if (indexId != null) { nCall(IC.isar_put_by_index(ptr, txn.ptr, indexId, cObjPtr)); } else { nCall(IC.isar_put(ptr, txn.ptr, cObjPtr)); } final id = cObj.id; schema.attach(this, id, object); if (saveLinks) { for (final link in schema.getLinks(object)) { link.saveSync(); } } return id; } @override Future> putAll(List objects) { return putAllByIndex(null, objects); } @override List putAllSync(List objects, {bool saveLinks = true}) { return putAllByIndexSync(null, objects, saveLinks: saveLinks); } @override Future> putAllByIndex(String? indexName, List objects) { final indexId = indexName != null ? schema.index(indexName).id : null; return isar.getTxn(true, (Txn txn) async { final cObjSetPtr = txn.newCObjectSet(objects.length); serializeObjects(txn, cObjSetPtr.ref.objects, objects); if (indexId != null) { IC.isar_put_all_by_index(ptr, txn.ptr, indexId, cObjSetPtr); } else { IC.isar_put_all(ptr, txn.ptr, cObjSetPtr); } await txn.wait(); final cObjectSet = cObjSetPtr.ref; final ids = List.filled(objects.length, 0); for (var i = 0; i < objects.length; i++) { final cObjPtr = cObjectSet.objects.elementAt(i); final id = cObjPtr.ref.id; ids[i] = id; final object = objects[i]; schema.attach(this, id, object); } return ids; }); } @override List putAllByIndexSync( String? indexName, List objects, { bool saveLinks = true, }) { final indexId = indexName != null ? schema.index(indexName).id : null; final ids = List.filled(objects.length, 0); isar.getTxnSync(true, (Txn txn) { for (var i = 0; i < objects.length; i++) { ids[i] = putByIndexSyncInternal( txn: txn, object: objects[i], indexId: indexId, saveLinks: saveLinks, ); } }); return ids; } @override Future deleteAll(List ids) { return isar.getTxn(true, (Txn txn) async { final countPtr = txn.alloc(); final idsPtr = txn.alloc(ids.length); idsPtr.asTypedList(ids.length).setAll(0, ids); IC.isar_delete_all(ptr, txn.ptr, idsPtr, ids.length, countPtr); await txn.wait(); return countPtr.value; }); } @override int deleteAllSync(List ids) { return isar.getTxnSync(true, (Txn txn) { final deletedPtr = txn.alloc(); var counter = 0; for (var i = 0; i < ids.length; i++) { nCall(IC.isar_delete(ptr, txn.ptr, ids[i], deletedPtr)); if (deletedPtr.value) { counter++; } } return counter; }); } @override Future deleteAllByIndex(String indexName, List keys) { return isar.getTxn(true, (Txn txn) async { final countPtr = txn.alloc(); final keysPtrPtr = _getKeysPtr(indexName, keys, txn.alloc); IC.isar_delete_all_by_index( ptr, txn.ptr, schema.index(indexName).id, keysPtrPtr, keys.length, countPtr, ); await txn.wait(); return countPtr.value; }); } @override int deleteAllByIndexSync(String indexName, List keys) { return isar.getTxnSync(true, (Txn txn) { final countPtr = txn.alloc(); final keysPtrPtr = _getKeysPtr(indexName, keys, txn.alloc); nCall( IC.isar_delete_all_by_index( ptr, txn.ptr, schema.index(indexName).id, keysPtrPtr, keys.length, countPtr, ), ); return countPtr.value; }); } @override Future clear() { return isar.getTxn(true, (Txn txn) async { IC.isar_clear(ptr, txn.ptr); await txn.wait(); }); } @override void clearSync() { isar.getTxnSync(true, (Txn txn) { nCall(IC.isar_clear(ptr, txn.ptr)); }); } @override Future importJson(List> json) { final bytes = const Utf8Encoder().convert(jsonEncode(json)); return importJsonRaw(bytes); } @override Future importJsonRaw(Uint8List jsonBytes) { return isar.getTxn(true, (Txn txn) async { final bytesPtr = txn.alloc(jsonBytes.length); bytesPtr.asTypedList(jsonBytes.length).setAll(0, jsonBytes); final idNamePtr = schema.idName.toCString(txn.alloc); IC.isar_json_import( ptr, txn.ptr, idNamePtr, bytesPtr, jsonBytes.length, ); await txn.wait(); }); } @override void importJsonSync(List> json) { final bytes = const Utf8Encoder().convert(jsonEncode(json)); importJsonRawSync(bytes); } @override void importJsonRawSync(Uint8List jsonBytes) { return isar.getTxnSync(true, (Txn txn) async { final bytesPtr = txn.getBuffer(jsonBytes.length); bytesPtr.asTypedList(jsonBytes.length).setAll(0, jsonBytes); final idNamePtr = schema.idName.toCString(txn.alloc); nCall( IC.isar_json_import( ptr, txn.ptr, idNamePtr, bytesPtr, jsonBytes.length, ), ); }); } @override Future count() { return isar.getTxn(false, (Txn txn) async { final countPtr = txn.alloc(); IC.isar_count(ptr, txn.ptr, countPtr); await txn.wait(); return countPtr.value; }); } @override int countSync() { return isar.getTxnSync(false, (Txn txn) { final countPtr = txn.alloc(); nCall(IC.isar_count(ptr, txn.ptr, countPtr)); return countPtr.value; }); } @override Future getSize({ bool includeIndexes = false, bool includeLinks = false, }) { return isar.getTxn(false, (Txn txn) async { final sizePtr = txn.alloc(); IC.isar_get_size(ptr, txn.ptr, includeIndexes, includeLinks, sizePtr); await txn.wait(); return sizePtr.value; }); } @override int getSizeSync({bool includeIndexes = false, bool includeLinks = false}) { return isar.getTxnSync(false, (Txn txn) { final sizePtr = txn.alloc(); nCall( IC.isar_get_size( ptr, txn.ptr, includeIndexes, includeLinks, sizePtr, ), ); return sizePtr.value; }); } @override Stream watchLazy({bool fireImmediately = false}) { isar.requireOpen(); final port = ReceivePort(); final handle = IC.isar_watch_collection(isar.ptr, ptr, port.sendPort.nativePort); final controller = StreamController( onCancel: () { IC.isar_stop_watching(handle); port.close(); }, ); if (fireImmediately) { controller.add(null); } controller.addStream(port); return controller.stream; } @override Stream watchObject(Id id, {bool fireImmediately = false}) { return watchObjectLazy(id, fireImmediately: fireImmediately) .asyncMap((event) => get(id)); } @override Stream watchObjectLazy(Id id, {bool fireImmediately = false}) { isar.requireOpen(); final cObjPtr = malloc(); final port = ReceivePort(); final handle = IC.isar_watch_object(isar.ptr, ptr, id, port.sendPort.nativePort); malloc.free(cObjPtr); final controller = StreamController( onCancel: () { IC.isar_stop_watching(handle); port.close(); }, ); if (fireImmediately) { controller.add(null); } controller.addStream(port); return controller.stream; } @override Query buildQuery({ List whereClauses = const [], bool whereDistinct = false, Sort whereSort = Sort.asc, FilterOperation? filter, List sortBy = const [], List distinctBy = const [], int? offset, int? limit, String? property, }) { isar.requireOpen(); return buildNativeQuery( this, whereClauses, whereDistinct, whereSort, filter, sortBy, distinctBy, offset, limit, property, ); } @override Future verify(List objects) async { await isar.verify(); return isar.getTxn(false, (Txn txn) async { final cObjSetPtr = txn.newCObjectSet(objects.length); serializeObjects(txn, cObjSetPtr.ref.objects, objects); IC.isar_verify(ptr, txn.ptr, cObjSetPtr); await txn.wait(); }); } @override Future verifyLink( String linkName, List sourceIds, List targetIds, ) async { final link = schema.link(linkName); return isar.getTxn(false, (Txn txn) async { final idsPtr = txn.alloc(sourceIds.length + targetIds.length); for (var i = 0; i < sourceIds.length; i++) { idsPtr[i * 2] = sourceIds[i]; idsPtr[i * 2 + 1] = targetIds[i]; } IC.isar_link_verify( ptr, txn.ptr, link.id, idsPtr, sourceIds.length + targetIds.length, ); await txn.wait(); }); } } ================================================ FILE: packages/isar/lib/src/native/isar_core.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/native/bindings.dart'; const Id isarMinId = -9223372036854775807; const Id isarMaxId = 9223372036854775807; const Id isarAutoIncrementId = -9223372036854775808; typedef IsarAbi = Abi; const int minByte = 0; const int maxByte = 255; const int minInt = -2147483648; const int maxInt = 2147483647; const int minLong = -9223372036854775808; const int maxLong = 9223372036854775807; const double minDouble = double.nan; const double maxDouble = double.infinity; const nullByte = IsarObject_NULL_BYTE; const nullInt = IsarObject_NULL_INT; const nullLong = IsarObject_NULL_LONG; const nullFloat = double.nan; const nullDouble = double.nan; final nullDate = DateTime.fromMillisecondsSinceEpoch(0); const nullBool = IsarObject_NULL_BOOL; const falseBool = IsarObject_FALSE_BOOL; const trueBool = IsarObject_TRUE_BOOL; const String binariesUrl = 'https://binaries.isar-community.dev'; bool _isarInitialized = false; // ignore: non_constant_identifier_names late final IsarCoreBindings IC; typedef FinalizerFunction = void Function(Pointer token); late final Pointer isarClose; late final Pointer isarQueryFree; FutureOr initializeCoreBinary({ Map libraries = const {}, bool download = false, }) { if (_isarInitialized) { return null; } String? libraryPath; if (!Platform.isIOS) { libraryPath = libraries[Abi.current()] ?? Abi.current().localName; } try { _initializePath(libraryPath); } catch (e) { if (!Platform.isAndroid && !Platform.isIOS) { final downloadPath = _getLibraryDownloadPath(libraries); if (download) { return _downloadIsarCore(downloadPath).then((value) { _initializePath(downloadPath); }); } else { // try to use the binary at the download path anyway _initializePath(downloadPath); } } else { throw IsarError( 'Could not initialize IsarCore library for processor architecture ' '"${Abi.current()}". If you create a Flutter app, make sure to add ' 'isar_flutter_libs to your dependencies.\n$e', ); } } } void _initializePath(String? libraryPath) { late DynamicLibrary dylib; if (Platform.isIOS) { dylib = DynamicLibrary.process(); } else { dylib = DynamicLibrary.open(libraryPath!); } final bindings = IsarCoreBindings(dylib); final coreVersion = bindings.isar_version().cast().toDartString(); if (coreVersion != Isar.version && coreVersion != 'debug') { throw IsarError( 'Incorrect Isar Core version: Required ${Isar.version} found ' '$coreVersion. Make sure to use the latest isar_flutter_libs. If you ' 'have a Dart only project, make sure that old Isar Core binaries are ' 'deleted.', ); } IC = bindings; isarClose = dylib.lookup('isar_instance_close'); isarQueryFree = dylib.lookup('isar_q_free'); _isarInitialized = true; } String _getLibraryDownloadPath(Map libraries) { final providedPath = libraries[Abi.current()]; if (providedPath != null) { return providedPath; } else { final name = Abi.current().localName; if (Platform.script.path.isEmpty) { return name; } var dir = Platform.script.pathSegments .sublist(0, Platform.script.pathSegments.length - 1) .join(Platform.pathSeparator); if (!Platform.isWindows) { // Not on windows, add leading platform path separator dir = '${Platform.pathSeparator}$dir'; } return '$dir${Platform.pathSeparator}$name'; } } Future _downloadIsarCore(String libraryPath) async { final libraryFile = File(libraryPath); // ignore: avoid_slow_async_io if (await libraryFile.exists()) { return; } final remoteName = Abi.current().remoteName; final uri = Uri.parse('$binariesUrl/${Isar.version}/$remoteName'); final request = await HttpClient().getUrl(uri); final response = await request.close(); if (response.statusCode != 200) { throw IsarError( 'Could not download IsarCore library: ${response.reasonPhrase}', ); } await response.pipe(libraryFile.openWrite()); } IsarError? isarErrorFromResult(int result) { if (result != 0) { final error = IC.isar_get_error(result); if (error.address == 0) { throw IsarError( 'There was an error but it could not be loaded from IsarCore.', ); } try { final message = error.cast().toDartString(); return IsarError(message); } finally { IC.isar_free_string(error); } } else { return null; } } @pragma('vm:prefer-inline') void nCall(int result) { final error = isarErrorFromResult(result); if (error != null) { throw error; } } Stream wrapIsarPort(ReceivePort port) { final portStreamController = StreamController(onCancel: port.close); port.listen((event) { if (event == 0) { portStreamController.add(null); } else { final error = isarErrorFromResult(event as int); portStreamController.addError(error!); } }); return portStreamController.stream; } extension PointerX on Pointer { @pragma('vm:prefer-inline') bool get isNull => address == 0; } extension on Abi { String get localName { switch (Abi.current()) { case Abi.androidArm: case Abi.androidArm64: case Abi.androidIA32: case Abi.androidX64: return 'libisar.so'; case Abi.macosArm64: case Abi.macosX64: return 'libisar.dylib'; case Abi.linuxX64: return 'libisar.so'; case Abi.windowsArm64: case Abi.windowsX64: return 'isar.dll'; default: throw IsarError( 'Unsupported processor architecture "${Abi.current()}". ' 'Please open an issue on GitHub to request it.', ); } } String get remoteName { switch (Abi.current()) { case Abi.macosArm64: case Abi.macosX64: return 'libisar_macos.dylib'; case Abi.linuxX64: return 'libisar_linux_x64.so'; case Abi.windowsArm64: return 'isar_windows_arm64.dll'; case Abi.windowsX64: return 'isar_windows_x64.dll'; } throw UnimplementedError(); } } ================================================ FILE: packages/isar/lib/src/native/isar_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; import 'package:isar/src/common/isar_common.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/txn.dart'; class IsarImpl extends IsarCommon implements Finalizable { IsarImpl(super.name, this.ptr) { _finalizer = NativeFinalizer(isarClose); _finalizer.attach(this, ptr.cast(), detach: this); } final Pointer ptr; late final NativeFinalizer _finalizer; final offsets = >{}; final Pointer> _syncTxnPtrPtr = malloc>(); String? _directory; @override String get directory { requireOpen(); if (_directory == null) { final dirPtr = IC.isar_instance_get_path(ptr); try { _directory = dirPtr.cast().toDartString(); } finally { IC.isar_free_string(dirPtr); } } return _directory!; } @override Future beginTxn(bool write, bool silent) async { final port = ReceivePort(); final portStream = wrapIsarPort(port); final txnPtrPtr = malloc>(); IC.isar_txn_begin( ptr, txnPtrPtr, false, write, silent, port.sendPort.nativePort, ); final txn = Txn.async(this, txnPtrPtr.value, write, portStream); await txn.wait(); return txn; } @override Transaction beginTxnSync(bool write, bool silent) { nCall(IC.isar_txn_begin(ptr, _syncTxnPtrPtr, true, write, silent, 0)); return Txn.sync(this, _syncTxnPtrPtr.value, write); } @override bool performClose(bool deleteFromDisk) { _finalizer.detach(this); if (deleteFromDisk) { return IC.isar_instance_close_and_delete(ptr); } else { return IC.isar_instance_close(ptr); } } @override Future getSize({ bool includeIndexes = false, bool includeLinks = false, }) { return getTxn(false, (Txn txn) async { final sizePtr = txn.alloc(); IC.isar_instance_get_size( ptr, txn.ptr, includeIndexes, includeLinks, sizePtr, ); await txn.wait(); return sizePtr.value; }); } @override int getSizeSync({bool includeIndexes = false, bool includeLinks = false}) { return getTxnSync(false, (Txn txn) { final sizePtr = txn.alloc(); nCall( IC.isar_instance_get_size( ptr, txn.ptr, includeIndexes, includeLinks, sizePtr, ), ); return sizePtr.value; }); } @override Future copyToFile(String targetPath) async { final pathPtr = targetPath.toCString(malloc); final receivePort = ReceivePort(); final nativePort = receivePort.sendPort.nativePort; try { final stream = wrapIsarPort(receivePort); IC.isar_instance_copy_to_file(ptr, pathPtr, nativePort); await stream.first; } finally { malloc.free(pathPtr); } } @override Future verify() async { return getTxn(false, (Txn txn) async { IC.isar_instance_verify(ptr, txn.ptr); await txn.wait(); }); } } ================================================ FILE: packages/isar/lib/src/native/isar_link_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:ffi'; import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; import 'package:isar/src/common/isar_link_common.dart'; import 'package:isar/src/common/isar_links_common.dart'; import 'package:isar/src/native/isar_collection_impl.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/txn.dart'; mixin IsarLinkBaseMixin on IsarLinkBaseImpl { @override IsarCollectionImpl get sourceCollection => super.sourceCollection as IsarCollectionImpl; @override IsarCollectionImpl get targetCollection => super.targetCollection as IsarCollectionImpl; late final int linkId = sourceCollection.schema.link(linkName).id; @override late final getId = targetCollection.schema.getId; @override Future update({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }) { final linkList = link.toList(); final unlinkList = unlink.toList(); final containingId = requireAttached(); return targetCollection.isar.getTxn(true, (Txn txn) { final count = linkList.length + unlinkList.length; final idsPtr = txn.alloc(count); final ids = idsPtr.asTypedList(count); for (var i = 0; i < linkList.length; i++) { ids[i] = requireGetId(linkList[i]); } for (var i = 0; i < unlinkList.length; i++) { ids[linkList.length + i] = requireGetId(unlinkList[i]); } IC.isar_link_update_all( sourceCollection.ptr, txn.ptr, linkId, containingId, idsPtr, linkList.length, unlinkList.length, reset, ); return txn.wait(); }); } @override void updateSync({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }) { final containingId = requireAttached(); targetCollection.isar.getTxnSync(true, (Txn txn) { if (reset) { nCall( IC.isar_link_unlink_all( sourceCollection.ptr, txn.ptr, linkId, containingId, ), ); } for (final object in link) { var id = getId(object); if (id == Isar.autoIncrement) { id = targetCollection.putByIndexSyncInternal( txn: txn, object: object, ); } nCall( IC.isar_link( sourceCollection.ptr, txn.ptr, linkId, containingId, id, ), ); } for (final object in unlink) { final unlinkId = requireGetId(object); nCall( IC.isar_link_unlink( sourceCollection.ptr, txn.ptr, linkId, containingId, unlinkId, ), ); } }); } } class IsarLinkImpl extends IsarLinkCommon with IsarLinkBaseMixin {} class IsarLinksImpl extends IsarLinksCommon with IsarLinkBaseMixin {} ================================================ FILE: packages/isar/lib/src/native/isar_reader_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:convert'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:meta/meta.dart'; /// @nodoc @protected class IsarReaderImpl implements IsarReader { IsarReaderImpl(this._buffer) : _byteData = ByteData.view(_buffer.buffer, _buffer.offsetInBytes) { _staticSize = _byteData.getUint16(0, Endian.little); } static const Utf8Decoder utf8Decoder = Utf8Decoder(); final Uint8List _buffer; final ByteData _byteData; late int _staticSize; @pragma('vm:prefer-inline') bool _readBool(int offset) { final value = _buffer[offset]; if (value == trueBool) { return true; } else { return false; } } @pragma('vm:prefer-inline') @override bool readBool(int offset) { if (offset >= _staticSize) { return false; } return _readBool(offset); } @pragma('vm:prefer-inline') bool? _readBoolOrNull(int offset) { final value = _buffer[offset]; if (value == trueBool) { return true; } else if (value == falseBool) { return false; } else { return null; } } @pragma('vm:prefer-inline') @override bool? readBoolOrNull(int offset) { if (offset >= _staticSize) { return null; } return _readBoolOrNull(offset); } @pragma('vm:prefer-inline') @override int readByte(int offset) { if (offset >= _staticSize) { return 0; } return _buffer[offset]; } @pragma('vm:prefer-inline') @override int? readByteOrNull(int offset) { if (offset >= _staticSize) { return null; } return _buffer[offset]; } @pragma('vm:prefer-inline') @override int readInt(int offset) { if (offset >= _staticSize) { return nullInt; } return _byteData.getInt32(offset, Endian.little); } @pragma('vm:prefer-inline') int? _readIntOrNull(int offset) { final value = _byteData.getInt32(offset, Endian.little); if (value != nullInt) { return value; } else { return null; } } @pragma('vm:prefer-inline') @override int? readIntOrNull(int offset) { if (offset >= _staticSize) { return null; } return _readIntOrNull(offset); } @pragma('vm:prefer-inline') @override double readFloat(int offset) { if (offset >= _staticSize) { return nullDouble; } return _byteData.getFloat32(offset, Endian.little); } @pragma('vm:prefer-inline') double? _readFloatOrNull(int offset) { final value = _byteData.getFloat32(offset, Endian.little); if (!value.isNaN) { return value; } else { return null; } } @pragma('vm:prefer-inline') @override double? readFloatOrNull(int offset) { if (offset >= _staticSize) { return null; } return _readFloatOrNull(offset); } @pragma('vm:prefer-inline') @override int readLong(int offset) { if (offset >= _staticSize) { return nullLong; } return _byteData.getInt64(offset, Endian.little); } @pragma('vm:prefer-inline') int? _readLongOrNull(int offset) { final value = _byteData.getInt64(offset, Endian.little); if (value != nullLong) { return value; } else { return null; } } @pragma('vm:prefer-inline') @override int? readLongOrNull(int offset) { if (offset >= _staticSize) { return null; } return _readLongOrNull(offset); } @pragma('vm:prefer-inline') @override double readDouble(int offset) { if (offset >= _staticSize) { return nullDouble; } return _byteData.getFloat64(offset, Endian.little); } @pragma('vm:prefer-inline') double? _readDoubleOrNull(int offset) { final value = _byteData.getFloat64(offset, Endian.little); if (!value.isNaN) { return value; } else { return null; } } @pragma('vm:prefer-inline') @override double? readDoubleOrNull(int offset) { if (offset >= _staticSize) { return null; } return _readDoubleOrNull(offset); } @pragma('vm:prefer-inline') @override DateTime readDateTime(int offset) { final time = readLongOrNull(offset); return time != null ? DateTime.fromMicrosecondsSinceEpoch(time, isUtc: true).toLocal() : nullDate; } @pragma('vm:prefer-inline') @override DateTime? readDateTimeOrNull(int offset) { final time = readLongOrNull(offset); if (time != null) { return DateTime.fromMicrosecondsSinceEpoch(time, isUtc: true).toLocal(); } else { return null; } } @pragma('vm:prefer-inline') int _readUint24(int offset) { return _buffer[offset] | _buffer[offset + 1] << 8 | _buffer[offset + 2] << 16; } @pragma('vm:prefer-inline') @override String readString(int offset) { return readStringOrNull(offset) ?? ''; } @pragma('vm:prefer-inline') @override String? readStringOrNull(int offset) { if (offset >= _staticSize) { return null; } var bytesOffset = _readUint24(offset); if (bytesOffset == 0) { return null; } final length = _readUint24(bytesOffset); bytesOffset += 3; return utf8Decoder.convert(_buffer, bytesOffset, bytesOffset + length); } @pragma('vm:prefer-inline') @override T? readObjectOrNull( int offset, Deserialize deserialize, Map> allOffsets, ) { if (offset >= _staticSize) { return null; } var bytesOffset = _readUint24(offset); if (bytesOffset == 0) { return null; } final length = _readUint24(bytesOffset); bytesOffset += 3; final buffer = Uint8List.sublistView(_buffer, bytesOffset, bytesOffset + length); final reader = IsarReaderImpl(buffer); final offsets = allOffsets[T]!; return deserialize(0, reader, offsets, allOffsets); } @override List? readBoolList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, false); for (var i = 0; i < length; i++) { list[i] = _readBool(listOffset + i); } return list; } @override List? readBoolOrNullList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, null); for (var i = 0; i < length; i++) { list[i] = _readBoolOrNull(listOffset + i); } return list; } @override List? readByteList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; return _buffer.sublist(listOffset, listOffset + length); } @override List? readIntList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = Int32List(length); for (var i = 0; i < length; i++) { list[i] = _byteData.getInt32(listOffset + i * 4, Endian.little); } return list; } @override List? readIntOrNullList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, null); for (var i = 0; i < length; i++) { list[i] = _readIntOrNull(listOffset + i * 4); } return list; } @override List? readFloatList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = Float32List(length); for (var i = 0; i < length; i++) { list[i] = _byteData.getFloat32(listOffset + i * 4, Endian.little); } return list; } @override List? readFloatOrNullList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, null); for (var i = 0; i < length; i++) { list[i] = _readFloatOrNull(listOffset + i * 4); } return list; } @override List? readLongList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = Int64List(length); for (var i = 0; i < length; i++) { list[i] = _byteData.getInt64(listOffset + i * 8, Endian.little); } return list; } @override List? readLongOrNullList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, null); for (var i = 0; i < length; i++) { list[i] = _readLongOrNull(listOffset + i * 8); } return list; } @override List? readDoubleList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = Float64List(length); for (var i = 0; i < length; i++) { list[i] = _byteData.getFloat64(listOffset + i * 8, Endian.little); } return list; } @override List? readDoubleOrNullList(int offset) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, null); for (var i = 0; i < length; i++) { list[i] = _readDoubleOrNull(listOffset + i * 8); } return list; } @override List? readDateTimeList(int offset) { return readLongOrNullList(offset)?.map((e) { if (e != null) { return DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true).toLocal(); } else { return nullDate; } }).toList(); } @override List? readDateTimeOrNullList(int offset) { return readLongOrNullList(offset)?.map((e) { if (e != null) { return DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true).toLocal(); } }).toList(); } List? readDynamicList( int offset, T nullValue, T Function(int startOffset, int endOffset) transform, ) { if (offset >= _staticSize) { return null; } var listOffset = _readUint24(offset); if (listOffset == 0) { return null; } final length = _readUint24(listOffset); listOffset += 3; final list = List.filled(length, nullValue); var contentOffset = listOffset + length * 3; for (var i = 0; i < length; i++) { final itemSize = _readUint24(listOffset + i * 3); if (itemSize != 0) { list[i] = transform(contentOffset, contentOffset + itemSize - 1); contentOffset += itemSize - 1; } } return list; } @override List? readStringList(int offset) { return readDynamicList(offset, '', (startOffset, endOffset) { return utf8Decoder.convert(_buffer, startOffset, endOffset); }); } @override List? readStringOrNullList(int offset) { return readDynamicList(offset, null, (startOffset, endOffset) { return utf8Decoder.convert(_buffer, startOffset, endOffset); }); } @override List? readObjectList( int offset, Deserialize deserialize, Map> allOffsets, T defaultValue, ) { final offsets = allOffsets[T]!; return readDynamicList(offset, defaultValue, (startOffset, endOffset) { final buffer = Uint8List.sublistView(_buffer, startOffset, endOffset); final reader = IsarReaderImpl(buffer); return deserialize(0, reader, offsets, allOffsets); }); } @override List? readObjectOrNullList( int offset, Deserialize deserialize, Map> allOffsets, ) { final offsets = allOffsets[T]!; return readDynamicList(offset, null, (startOffset, endOffset) { final buffer = Uint8List.sublistView(_buffer, startOffset, endOffset); final reader = IsarReaderImpl(buffer); return deserialize(0, reader, offsets, allOffsets); }); } } ================================================ FILE: packages/isar/lib/src/native/isar_writer_impl.dart ================================================ // ignore_for_file: public_member_api_docs, prefer_asserts_with_message, // avoid_positional_boolean_parameters import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:meta/meta.dart'; /// @nodoc @protected class IsarWriterImpl implements IsarWriter { IsarWriterImpl(Uint8List buffer, int staticSize) : _dynamicOffset = staticSize, _buffer = buffer, _byteData = ByteData.view(buffer.buffer, buffer.offsetInBytes) { _byteData.setUint16(0, staticSize, Endian.little); // Required because we don't want to persist uninitialized memory. for (var i = 2; i < staticSize; i++) { _buffer[i] = 0; } } final Uint8List _buffer; final ByteData _byteData; int _dynamicOffset; int get usedBytes => _dynamicOffset; @override @pragma('vm:prefer-inline') void writeBool(int offset, bool? value) { _buffer[offset] = value.byteValue; } @override @pragma('vm:prefer-inline') void writeByte(int offset, int value) { assert(value >= minByte && value <= maxByte); _buffer[offset] = value; } @override @pragma('vm:prefer-inline') void writeInt(int offset, int? value) { value ??= nullInt; assert(value >= minInt && value <= maxInt); _byteData.setInt32(offset, value, Endian.little); } @override @pragma('vm:prefer-inline') void writeFloat(int offset, double? value) { _byteData.setFloat32(offset, value ?? double.nan, Endian.little); } @override @pragma('vm:prefer-inline') void writeLong(int offset, int? value) { _byteData.setInt64(offset, value ?? nullLong, Endian.little); } @override @pragma('vm:prefer-inline') void writeDouble(int offset, double? value) { _byteData.setFloat64(offset, value ?? double.nan, Endian.little); } @override @pragma('vm:prefer-inline') void writeDateTime(int offset, DateTime? value) { writeLong(offset, value?.toUtc().microsecondsSinceEpoch); } @pragma('vm:prefer-inline') void _writeUint24(int offset, int value) { _buffer[offset] = value; _buffer[offset + 1] = value >> 8; _buffer[offset + 2] = value >> 16; } @override @pragma('vm:prefer-inline') void writeString(int offset, String? value) { if (value != null) { final byteCount = encodeString(value, _buffer, _dynamicOffset + 3); _writeUint24(offset, _dynamicOffset); _writeUint24(_dynamicOffset, byteCount); _dynamicOffset += byteCount + 3; } else { _writeUint24(offset, 0); } } @override @pragma('vm:prefer-inline') void writeObject( int offset, Map> allOffsets, Serialize serialize, T? value, ) { if (value != null) { final buffer = Uint8List.sublistView(_buffer, _dynamicOffset + 3); final offsets = allOffsets[T]!; final binaryWriter = IsarWriterImpl(buffer, offsets.last); serialize(value, binaryWriter, offsets, allOffsets); final byteCount = binaryWriter.usedBytes; _writeUint24(offset, _dynamicOffset); _writeUint24(_dynamicOffset, byteCount); _dynamicOffset += byteCount + 3; } else { _writeUint24(offset, 0); } } @pragma('vm:prefer-inline') void _writeListOffset(int offset, int? length) { if (length == null) { _writeUint24(offset, 0); } else { _writeUint24(offset, _dynamicOffset); _writeUint24(_dynamicOffset, length); _dynamicOffset += 3; } } @override @pragma('vm:prefer-inline') void writeByteList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var i = 0; i < values.length; i++) { _buffer[_dynamicOffset++] = values[i]; } } } @override void writeBoolList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var i = 0; i < values.length; i++) { _buffer[_dynamicOffset++] = values[i].byteValue; } } } @override void writeIntList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var value in values) { value ??= nullInt; assert(value >= minInt && value <= maxInt); _byteData.setUint32(_dynamicOffset, value, Endian.little); _dynamicOffset += 4; } } } @override void writeFloatList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var i = 0; i < values.length; i++) { _byteData.setFloat32( _dynamicOffset, values[i] ?? nullFloat, Endian.little, ); _dynamicOffset += 4; } } } @override void writeLongList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var i = 0; i < values.length; i++) { _byteData.setInt64( _dynamicOffset, values[i] ?? nullLong, Endian.little, ); _dynamicOffset += 8; } } } @override void writeDoubleList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { for (var i = 0; i < values.length; i++) { _byteData.setFloat64( _dynamicOffset, values[i] ?? nullDouble, Endian.little, ); _dynamicOffset += 8; } } } @override void writeDateTimeList(int offset, List? values) { final longList = values?.map((e) => e.longValue).toList(); writeLongList(offset, longList); } @override void writeStringList(int offset, List? values) { _writeListOffset(offset, values?.length); if (values != null) { final offsetListOffset = _dynamicOffset; _dynamicOffset += values.length * 3; for (var i = 0; i < values.length; i++) { final value = values[i]; if (value != null) { final byteCount = encodeString(value, _buffer, _dynamicOffset); _writeUint24(offsetListOffset + i * 3, byteCount + 1); _dynamicOffset += byteCount; } else { _writeUint24(offsetListOffset + i * 3, 0); } } } } @override void writeObjectList( int offset, Map> allOffsets, Serialize serialize, List? values, ) { _writeListOffset(offset, values?.length); if (values != null) { final offsetListOffset = _dynamicOffset; _dynamicOffset += values.length * 3; final offsets = allOffsets[T]!; final staticSize = offsets.last; for (var i = 0; i < values.length; i++) { final value = values[i]; if (value != null) { final buffer = Uint8List.sublistView(_buffer, _dynamicOffset); final binaryWriter = IsarWriterImpl(buffer, staticSize); serialize(value, binaryWriter, offsets, allOffsets); final byteCount = binaryWriter.usedBytes; _writeUint24(offsetListOffset + i * 3, byteCount + 1); _dynamicOffset += byteCount; } else { _writeUint24(offsetListOffset + i * 3, 0); } } } } } extension IsarBoolValue on bool? { @pragma('vm:prefer-inline') int get byteValue => this == null ? nullBool : (this == true ? trueBool : falseBool); } extension IsarDateTimeValue on DateTime? { @pragma('vm:prefer-inline') int get longValue => this?.toUtc().microsecondsSinceEpoch ?? nullLong; } ================================================ FILE: packages/isar/lib/src/native/open.dart ================================================ // ignore_for_file: public_member_api_docs, invalid_use_of_protected_member import 'dart:convert'; import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/common/schemas.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_collection_impl.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_impl.dart'; final Pointer> _isarPtrPtr = malloc>(); List _getOffsets( Pointer colPtr, int propertiesCount, int embeddedColId, ) { final offsetsPtr = malloc(propertiesCount); final staticSize = IC.isar_get_offsets(colPtr, embeddedColId, offsetsPtr); final offsets = offsetsPtr.asTypedList(propertiesCount).toList(); offsets.add(staticSize); malloc.free(offsetsPtr); return offsets; } void _initializeInstance( IsarImpl isar, List> schemas, ) { final colPtrPtr = malloc>(); final cols = >{}; for (final schema in schemas) { nCall(IC.isar_instance_get_collection(isar.ptr, colPtrPtr, schema.id)); final offsets = _getOffsets(colPtrPtr.value, schema.properties.length, 0); for (final embeddedSchema in schema.embeddedSchemas.values) { final embeddedType = embeddedSchema.type; if (!isar.offsets.containsKey(embeddedType)) { final offsets = _getOffsets( colPtrPtr.value, embeddedSchema.properties.length, embeddedSchema.id, ); isar.offsets[embeddedType] = offsets; } } schema.toCollection(() { isar.offsets[OBJ] = offsets; schema as CollectionSchema; cols[OBJ] = IsarCollectionImpl( isar: isar, ptr: colPtrPtr.value, schema: schema, ); }); } malloc.free(colPtrPtr); isar.attachCollections(cols); } Future openIsar({ required List> schemas, required String directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) async { initializeCoreBinary(); IC.isar_connect_dart_api(NativeApi.postCObject.cast()); return using((Arena alloc) async { final namePtr = name.toCString(alloc); final dirPtr = directory.toCString(alloc); final schemasJson = getSchemas(schemas).map((e) => e.toJson()); final schemaStrPtr = jsonEncode(schemasJson.toList()).toCString(alloc); final compactMinFileSize = compactOnLaunch?.minFileSize; final compactMinBytes = compactOnLaunch?.minBytes; final compactMinRatio = compactOnLaunch == null ? double.nan : compactOnLaunch.minRatio; final receivePort = ReceivePort(); final nativePort = receivePort.sendPort.nativePort; final stream = wrapIsarPort(receivePort); IC.isar_instance_create_async( _isarPtrPtr, namePtr, dirPtr, schemaStrPtr, maxSizeMiB, relaxedDurability, compactMinFileSize ?? 0, compactMinBytes ?? 0, compactMinRatio ?? 0, nativePort, ); await stream.first; final isar = IsarImpl(name, _isarPtrPtr.value); _initializeInstance(isar, schemas); return isar; }); } Isar openIsarSync({ required List> schemas, required String directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) { initializeCoreBinary(); IC.isar_connect_dart_api(NativeApi.postCObject.cast()); return using((Arena alloc) { final namePtr = name.toCString(alloc); final dirPtr = directory.toCString(alloc); final schemasJson = getSchemas(schemas).map((e) => e.toJson()); final schemaStrPtr = jsonEncode(schemasJson.toList()).toCString(alloc); final compactMinFileSize = compactOnLaunch?.minFileSize; final compactMinBytes = compactOnLaunch?.minBytes; final compactMinRatio = compactOnLaunch == null ? double.nan : compactOnLaunch.minRatio; nCall( IC.isar_instance_create( _isarPtrPtr, namePtr, dirPtr, schemaStrPtr, maxSizeMiB, relaxedDurability, compactMinFileSize ?? 0, compactMinBytes ?? 0, compactMinRatio ?? 0, ), ); final isar = IsarImpl(name, _isarPtrPtr.value); _initializeInstance(isar, schemas); return isar; }); } ================================================ FILE: packages/isar/lib/src/native/query_build.dart ================================================ // ignore_for_file: invalid_use_of_protected_member, public_member_api_docs import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/index_key.dart'; import 'package:isar/src/native/isar_collection_impl.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_writer_impl.dart'; import 'package:isar/src/native/query_impl.dart'; final Pointer maxStr = '\u{FFFFF}'.toNativeUtf8().cast(); Query buildNativeQuery( IsarCollectionImpl col, List whereClauses, bool whereDistinct, Sort whereSort, FilterOperation? filter, List sortBy, List distinctBy, int? offset, int? limit, String? property, ) { final qbPtr = IC.isar_qb_create(col.ptr); for (final wc in whereClauses) { if (wc is IdWhereClause) { _addIdWhereClause(qbPtr, wc, whereSort); } else if (wc is IndexWhereClause) { _addIndexWhereClause( col.schema, qbPtr, wc, whereDistinct, whereSort, ); } else { _addLinkWhereClause(col.isar, qbPtr, wc as LinkWhereClause); } } if (filter != null) { final alloc = Arena(malloc); try { final filterPtr = _buildFilter(col, null, filter, alloc); if (filterPtr != null) { IC.isar_qb_set_filter(qbPtr, filterPtr); } } finally { alloc.releaseAll(); } } for (final sortProperty in sortBy) { final property = col.schema.property(sortProperty.property); nCall( IC.isar_qb_add_sort_by( qbPtr, property.id, sortProperty.sort == Sort.asc, ), ); } if (offset != null || limit != null) { IC.isar_qb_set_offset_limit(qbPtr, offset ?? -1, limit ?? -1); } for (final distinctByProperty in distinctBy) { final property = col.schema.property(distinctByProperty.property); nCall( IC.isar_qb_add_distinct_by( qbPtr, property.id, distinctByProperty.caseSensitive ?? true, ), ); } QueryDeserialize deserialize; int? propertyId; if (property == null) { deserialize = (col as IsarCollectionImpl).deserializeObjects; } else { propertyId = property != col.schema.idName ? col.schema.property(property).id : null; deserialize = (CObjectSet cObjSet) => col.deserializeProperty(cObjSet, propertyId); } final queryPtr = IC.isar_qb_build(qbPtr); return QueryImpl(col, queryPtr, deserialize, propertyId); } void _addIdWhereClause( Pointer qbPtr, IdWhereClause wc, Sort sort, ) { final lower = (wc.lower ?? minLong) + (wc.includeLower ? 0 : 1); final upper = (wc.upper ?? maxLong) - (wc.includeUpper ? 0 : 1); nCall( IC.isar_qb_add_id_where_clause( qbPtr, sort == Sort.asc ? lower : upper, sort == Sort.asc ? upper : lower, ), ); } Pointer? _buildLowerIndexBound( CollectionSchema schema, IndexSchema index, IndexWhereClause wc, ) { if (wc.lower == null) { return buildLowerUnboundedIndexKey(); } final firstVal = wc.lower!.length == 1 ? wc.lower!.first : null; if (firstVal is double) { final adjusted = adjustFloatBound( value: firstVal, lowerBound: true, include: wc.includeLower, epsilon: wc.epsilon, ); if (adjusted == null) { return null; } return buildIndexKey(schema, index, [adjusted]); } else { final lowerPtr = buildIndexKey(schema, index, wc.lower!); if (!wc.includeLower) { if (!IC.isar_key_increase(lowerPtr)) { return null; } } return lowerPtr; } } Pointer? _buildUpperIndexBound( CollectionSchema schema, IndexSchema index, IndexWhereClause wc, ) { if (wc.upper == null) { return buildUpperUnboundedIndexKey(); } final firstVal = wc.upper!.length == 1 ? wc.upper!.first : null; if (firstVal is double) { final adjusted = adjustFloatBound( value: firstVal, lowerBound: false, include: wc.includeUpper, epsilon: wc.epsilon, ); if (adjusted == null) { return null; } else { return buildIndexKey(schema, index, [adjusted]); } } else { final upperPtr = buildIndexKey(schema, index, wc.upper!); if (!wc.includeUpper) { if (!IC.isar_key_decrease(upperPtr)) { return null; } } // Also include composite indexes for upper keys if (index.properties.length > wc.upper!.length) { IC.isar_key_add_long(upperPtr, maxLong); } return upperPtr; } } void _addIndexWhereClause( CollectionSchema schema, Pointer qbPtr, IndexWhereClause wc, bool distinct, Sort sort, ) { final index = schema.index(wc.indexName); final lowerPtr = _buildLowerIndexBound(schema, index, wc); final upperPtr = _buildUpperIndexBound(schema, index, wc); if (lowerPtr != null && upperPtr != null) { nCall( IC.isar_qb_add_index_where_clause( qbPtr, schema.index(wc.indexName).id, lowerPtr, upperPtr, sort == Sort.asc, distinct, ), ); } else { // this where clause does not match any objects nCall( IC.isar_qb_add_id_where_clause( qbPtr, Isar.autoIncrement, Isar.autoIncrement, ), ); } } void _addLinkWhereClause( Isar isar, Pointer qbPtr, LinkWhereClause wc, ) { final linkCol = isar.getCollectionByNameInternal(wc.linkCollection)!; linkCol as IsarCollectionImpl; final linkId = linkCol.schema.link(wc.linkName).id; nCall(IC.isar_qb_add_link_where_clause(qbPtr, linkCol.ptr, linkId, wc.id)); } Pointer? _buildFilter( IsarCollectionImpl col, Schema? embeddedCol, FilterOperation filter, Allocator alloc, ) { if (filter is FilterGroup) { return _buildFilterGroup(col, embeddedCol, filter, alloc); } else if (filter is LinkFilter) { return _buildLink(col, filter, alloc); } else if (filter is ObjectFilter) { return _buildObject(col, embeddedCol, filter, alloc); } else if (filter is FilterCondition) { return _buildCondition(col, embeddedCol, filter, alloc); } else { return null; } } Pointer? _buildFilterGroup( IsarCollectionImpl col, Schema? embeddedCol, FilterGroup group, Allocator alloc, ) { final builtConditions = group.filters .map((FilterOperation op) => _buildFilter(col, embeddedCol, op, alloc)) .where((Pointer? it) => it != null) .toList(); if (builtConditions.isEmpty) { return null; } final filterPtrPtr = alloc>(); if (group.type == FilterGroupType.not) { IC.isar_filter_not( filterPtrPtr, builtConditions.first!, ); } else if (builtConditions.length == 1) { return builtConditions[0]; } else { final conditionsPtrPtr = alloc>(builtConditions.length); for (var i = 0; i < builtConditions.length; i++) { conditionsPtrPtr[i] = builtConditions[i]!; } IC.isar_filter_and_or_xor( filterPtrPtr, group.type == FilterGroupType.and, group.type == FilterGroupType.xor, conditionsPtrPtr, builtConditions.length, ); } return filterPtrPtr.value; } Pointer? _buildLink( IsarCollectionImpl col, LinkFilter link, Allocator alloc, ) { final linkSchema = col.schema.link(link.linkName); final linkTargetCol = col.isar.getCollectionByNameInternal(linkSchema.target)!; final linkId = col.schema.link(link.linkName).id; final filterPtrPtr = alloc>(); if (link.filter != null) { final condition = _buildFilter( linkTargetCol as IsarCollectionImpl, null, link.filter!, alloc, ); if (condition == null) { return null; } nCall( IC.isar_filter_link( col.ptr, filterPtrPtr, condition, linkId, ), ); } else { nCall( IC.isar_filter_link_length( col.ptr, filterPtrPtr, link.lower!, link.upper!, linkId, ), ); } return filterPtrPtr.value; } Pointer? _buildObject( IsarCollectionImpl col, Schema? embeddedCol, ObjectFilter objectFilter, Allocator alloc, ) { final property = (embeddedCol ?? col.schema).property(objectFilter.property); final condition = _buildFilter( col, col.schema.embeddedSchemas[property.target], objectFilter.filter, alloc, ); if (condition == null) { return null; } final filterPtrPtr = alloc>(); nCall( IC.isar_filter_object( col.ptr, filterPtrPtr, condition, embeddedCol?.id ?? 0, property.id, ), ); return filterPtrPtr.value; } Object _prepareValue( Object? value, Allocator alloc, IsarType type, Map? enumMap, ) { if (value is bool) { return value.byteValue; } else if (value is DateTime) { return value.longValue; } else if (value is Enum) { return _prepareValue(enumMap![value.name], alloc, type, null); } else if (value is String) { return value.toCString(alloc); } else if (value == null) { switch (type) { case IsarType.bool: case IsarType.byte: case IsarType.boolList: case IsarType.byteList: return minByte; case IsarType.int: case IsarType.intList: return minInt; case IsarType.long: case IsarType.longList: case IsarType.dateTime: case IsarType.dateTimeList: return minLong; case IsarType.float: case IsarType.double: case IsarType.floatList: case IsarType.doubleList: return minDouble; case IsarType.string: case IsarType.stringList: case IsarType.object: case IsarType.objectList: return nullptr; } } else { return value; } } Pointer _buildCondition( IsarCollectionImpl col, Schema? embeddedCol, FilterCondition condition, Allocator alloc, ) { final property = condition.property != col.schema.idName ? (embeddedCol ?? col.schema).property(condition.property) : null; final value1 = _prepareValue( condition.value1, alloc, property?.type ?? IsarType.long, property?.enumMap, ); final value2 = _prepareValue( condition.value2, alloc, property?.type ?? IsarType.long, property?.enumMap, ); final filterPtr = alloc>(); switch (condition.type) { case FilterConditionType.equalTo: _buildConditionEqual( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, val: value1, caseSensitive: condition.caseSensitive, epsilon: condition.epsilon, ); break; case FilterConditionType.between: _buildConditionBetween( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, lower: value1, includeLower: condition.include1, upper: value2, includeUpper: condition.include2, caseSensitive: condition.caseSensitive, epsilon: condition.epsilon, ); break; case FilterConditionType.lessThan: _buildConditionLessThan( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, val: value1, include: condition.include1, caseSensitive: condition.caseSensitive, epsilon: condition.epsilon, ); break; case FilterConditionType.greaterThan: _buildConditionGreaterThan( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, val: value1, include: condition.include1, caseSensitive: condition.caseSensitive, epsilon: condition.epsilon, ); break; case FilterConditionType.startsWith: case FilterConditionType.endsWith: case FilterConditionType.contains: case FilterConditionType.matches: _buildConditionStringOp( colPtr: col.ptr, filterPtr: filterPtr, conditionType: condition.type, embeddedColId: embeddedCol?.id, propertyId: property?.id, val: value1, include: condition.include1, caseSensitive: condition.caseSensitive, ); break; case FilterConditionType.isNull: _buildConditionIsNull( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, ); break; case FilterConditionType.isNotNull: _buildConditionIsNotNull( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, alloc: alloc, ); break; case FilterConditionType.elementIsNull: _buildConditionElementIsNull( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, isObjectList: property?.type == IsarType.objectList, nullValue: value1, ); break; case FilterConditionType.elementIsNotNull: _buildConditionElementIsNotNull( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, isObjectList: property?.type == IsarType.objectList, nullValue: value1, alloc: alloc, ); break; case FilterConditionType.listLength: _buildListLength( colPtr: col.ptr, filterPtr: filterPtr, embeddedColId: embeddedCol?.id, propertyId: property?.id, lower: value1, upper: value2, ); break; } return filterPtr.value; } void _buildConditionIsNull({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, }) { if (propertyId != null) { nCall( IC.isar_filter_null( colPtr, filterPtr, embeddedColId ?? 0, propertyId, ), ); } else { IC.isar_filter_static(filterPtr, false); } } void _buildConditionIsNotNull({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Allocator alloc, }) { if (propertyId != null) { final conditionPtr = alloc>(); nCall( IC.isar_filter_null( colPtr, conditionPtr, embeddedColId ?? 0, propertyId, ), ); IC.isar_filter_not(filterPtr, conditionPtr.value); } else { IC.isar_filter_static(filterPtr, true); } } void _buildConditionElementIsNull({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required bool isObjectList, required Object nullValue, }) { if (isObjectList) { IC.isar_filter_object( colPtr, filterPtr, nullptr, embeddedColId ?? 0, propertyId ?? 0, ); } else { _buildConditionEqual( colPtr: colPtr, filterPtr: filterPtr, embeddedColId: embeddedColId, propertyId: propertyId, val: nullValue, epsilon: 0, caseSensitive: true, ); } } void _buildConditionElementIsNotNull({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required bool isObjectList, required Object nullValue, required Allocator alloc, }) { if (isObjectList) { final objFilterPtrPtr = alloc>(); IC.isar_filter_static(objFilterPtrPtr, true); IC.isar_filter_object( colPtr, filterPtr, objFilterPtrPtr.value, embeddedColId ?? 0, propertyId ?? 0, ); } else { _buildConditionGreaterThan( colPtr: colPtr, filterPtr: filterPtr, embeddedColId: embeddedColId, propertyId: propertyId, val: nullValue, include: false, epsilon: 0, caseSensitive: true, ); } } void _buildConditionEqual({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Object val, required bool caseSensitive, required double epsilon, }) { if (val is int) { if (propertyId == null) { IC.isar_filter_id(filterPtr, val, true, val, true); } else { nCall( IC.isar_filter_long( colPtr, filterPtr, val, true, val, true, embeddedColId ?? 0, propertyId, ), ); } } else if (val is double) { final lower = adjustFloatBound( value: val, lowerBound: true, include: true, epsilon: epsilon, ); final upper = adjustFloatBound( value: val, lowerBound: false, include: true, epsilon: epsilon, ); if (lower == null || upper == null) { IC.isar_filter_static(filterPtr, false); } else { nCall( IC.isar_filter_double( colPtr, filterPtr, lower, upper, embeddedColId ?? 0, propertyId!, ), ); } } else if (val is Pointer) { nCall( IC.isar_filter_string( colPtr, filterPtr, val, true, val, true, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); } else { throw IsarError('Unsupported type for condition'); } } void _buildConditionBetween({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Object lower, required bool includeLower, required Object upper, required bool includeUpper, required bool caseSensitive, required double epsilon, }) { if (lower is int && upper is int) { if (propertyId == null) { IC.isar_filter_id(filterPtr, lower, includeLower, upper, includeUpper); } else { nCall( IC.isar_filter_long( colPtr, filterPtr, lower, includeLower, upper, includeUpper, embeddedColId ?? 0, propertyId, ), ); } } else if (lower is double && upper is double) { final adjustedLower = adjustFloatBound( value: lower, lowerBound: true, include: includeLower, epsilon: epsilon, ); final adjustedUpper = adjustFloatBound( value: upper, lowerBound: false, include: includeUpper, epsilon: epsilon, ); if (adjustedLower == null || adjustedUpper == null) { IC.isar_filter_static(filterPtr, false); } else { nCall( IC.isar_filter_double( colPtr, filterPtr, adjustedLower, adjustedUpper, embeddedColId ?? 0, propertyId!, ), ); } } else if (lower is Pointer && upper is Pointer) { nCall( IC.isar_filter_string( colPtr, filterPtr, lower, includeLower, upper, includeUpper, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); } else { throw IsarError('Unsupported type for condition'); } } void _buildConditionLessThan({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Object val, required bool include, required bool caseSensitive, required double epsilon, }) { if (val is int) { if (propertyId == null) { IC.isar_filter_id(filterPtr, minLong, true, val, include); } else { nCall( IC.isar_filter_long( colPtr, filterPtr, minLong, true, val, include, embeddedColId ?? 0, propertyId, ), ); } } else if (val is double) { final upper = adjustFloatBound( value: val, lowerBound: false, include: include, epsilon: epsilon, ); if (upper == null) { IC.isar_filter_static(filterPtr, false); } else { nCall( IC.isar_filter_double( colPtr, filterPtr, minDouble, upper, embeddedColId ?? 0, propertyId!, ), ); } } else if (val is Pointer) { nCall( IC.isar_filter_string( colPtr, filterPtr, nullptr, true, val, include, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); } else { throw IsarError('Unsupported type for condition'); } } void _buildConditionGreaterThan({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Object val, required bool include, required bool caseSensitive, required double epsilon, }) { if (val is int) { if (propertyId == null) { IC.isar_filter_id(filterPtr, val, include, maxLong, true); } else { nCall( IC.isar_filter_long( colPtr, filterPtr, val, include, maxLong, true, embeddedColId ?? 0, propertyId, ), ); } } else if (val is double) { final lower = adjustFloatBound( value: val, lowerBound: true, include: include, epsilon: epsilon, ); if (lower == null) { IC.isar_filter_static(filterPtr, false); } else { nCall( IC.isar_filter_double( colPtr, filterPtr, lower, maxDouble, embeddedColId ?? 0, propertyId!, ), ); } } else if (val is Pointer) { nCall( IC.isar_filter_string( colPtr, filterPtr, val, include, maxStr, true, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); } else { throw IsarError('Unsupported type for condition'); } } void _buildConditionStringOp({ required Pointer colPtr, required Pointer> filterPtr, required FilterConditionType conditionType, required int? embeddedColId, required int? propertyId, required Object val, required bool include, required bool caseSensitive, }) { if (val is Pointer) { if (val.isNull) { throw IsarError('String operation value must not be null'); } // ignore: missing_enum_constant_in_switch switch (conditionType) { case FilterConditionType.startsWith: nCall( IC.isar_filter_string_starts_with( colPtr, filterPtr, val, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); break; case FilterConditionType.endsWith: nCall( IC.isar_filter_string_ends_with( colPtr, filterPtr, val, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); break; case FilterConditionType.contains: nCall( IC.isar_filter_string_contains( colPtr, filterPtr, val, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); break; case FilterConditionType.matches: nCall( IC.isar_filter_string_matches( colPtr, filterPtr, val, caseSensitive, embeddedColId ?? 0, propertyId!, ), ); break; } } else { throw IsarError('Unsupported type for condition'); } } void _buildListLength({ required Pointer colPtr, required Pointer> filterPtr, required int? embeddedColId, required int? propertyId, required Object? lower, required Object? upper, }) { if (lower is int && upper is int) { nCall( IC.isar_filter_list_length( colPtr, filterPtr, lower, upper, embeddedColId ?? 0, propertyId!, ), ); } else { throw IsarError('Unsupported type for condition'); } } ================================================ FILE: packages/isar/lib/src/native/query_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_collection_impl.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/txn.dart'; typedef QueryDeserialize = List Function(CObjectSet); class QueryImpl extends Query implements Finalizable { QueryImpl(this.col, this.queryPtr, this.deserialize, this.propertyId) { NativeFinalizer(isarQueryFree).attach(this, queryPtr.cast()); } static const int maxLimit = 4294967295; final IsarCollectionImpl col; final Pointer queryPtr; final QueryDeserialize deserialize; final int? propertyId; @override Isar get isar => col.isar; @override Future findFirst() { return findInternal(maxLimit).then((List result) { if (result.isNotEmpty) { return result[0]; } else { return null; } }); } @override Future> findAll() => findInternal(maxLimit); Future> findInternal(int limit) { return col.isar.getTxn(false, (Txn txn) async { final resultsPtr = txn.alloc(); try { IC.isar_q_find(queryPtr, txn.ptr, resultsPtr, limit); await txn.wait(); return deserialize(resultsPtr.ref).cast(); } finally { IC.isar_free_c_object_set(resultsPtr); } }); } @override T? findFirstSync() { final results = findSyncInternal(1); if (results.isNotEmpty) { return results[0]; } else { return null; } } @override List findAllSync() => findSyncInternal(maxLimit); List findSyncInternal(int limit) { return col.isar.getTxnSync(false, (Txn txn) { final resultsPtr = txn.getCObjectsSet(); try { nCall(IC.isar_q_find(queryPtr, txn.ptr, resultsPtr, limit)); return deserialize(resultsPtr.ref).cast(); } finally { IC.isar_free_c_object_set(resultsPtr); } }); } @override Future deleteFirst() => deleteInternal(1).then((int count) => count == 1); @override Future deleteAll() => deleteInternal(maxLimit); Future deleteInternal(int limit) { return col.isar.getTxn(false, (Txn txn) async { final countPtr = txn.alloc(); IC.isar_q_delete(queryPtr, col.ptr, txn.ptr, limit, countPtr); await txn.wait(); return countPtr.value; }); } @override bool deleteFirstSync() => deleteSyncInternal(1) == 1; @override int deleteAllSync() => deleteSyncInternal(maxLimit); int deleteSyncInternal(int limit) { return col.isar.getTxnSync(false, (Txn txn) { final countPtr = txn.alloc(); nCall(IC.isar_q_delete(queryPtr, col.ptr, txn.ptr, limit, countPtr)); return countPtr.value; }); } @override Stream> watch({bool fireImmediately = false}) { return watchLazy(fireImmediately: fireImmediately) .asyncMap((event) => findAll()); } @override Stream watchLazy({bool fireImmediately = false}) { final port = ReceivePort(); final handle = IC.isar_watch_query( col.isar.ptr, col.ptr, queryPtr, port.sendPort.nativePort, ); final controller = StreamController( onCancel: () { IC.isar_stop_watching(handle); port.close(); }, ); if (fireImmediately) { controller.add(null); } controller.addStream(port); return controller.stream; } @override Future exportJsonRaw(R Function(Uint8List) callback) { return col.isar.getTxn(false, (Txn txn) async { final bytesPtrPtr = txn.alloc>(); final lengthPtr = txn.alloc(); final idNamePtr = col.schema.idName.toCString(txn.alloc); nCall( IC.isar_q_export_json( queryPtr, col.ptr, txn.ptr, idNamePtr, bytesPtrPtr, lengthPtr, ), ); try { await txn.wait(); final bytes = bytesPtrPtr.value.asTypedList(lengthPtr.value); return callback(bytes); } finally { IC.isar_free_json(bytesPtrPtr.value, lengthPtr.value); } }); } @override R exportJsonRawSync(R Function(Uint8List) callback) { return col.isar.getTxnSync(false, (Txn txn) { final bytesPtrPtr = txn.alloc>(); final lengthPtr = txn.alloc(); final idNamePtr = col.schema.idName.toCString(txn.alloc); try { nCall( IC.isar_q_export_json( queryPtr, col.ptr, txn.ptr, idNamePtr, bytesPtrPtr, lengthPtr, ), ); final bytes = bytesPtrPtr.value.asTypedList(lengthPtr.value); return callback(bytes); } finally { IC.isar_free_json(bytesPtrPtr.value, lengthPtr.value); } }); } @override Future aggregate(AggregationOp op) async { return col.isar.getTxn(false, (Txn txn) async { final resultPtrPtr = txn.alloc>(); IC.isar_q_aggregate( col.ptr, queryPtr, txn.ptr, op.index, propertyId ?? 0, resultPtrPtr, ); await txn.wait(); return _convertAggregatedResult(resultPtrPtr.value, op); }); } @override R? aggregateSync(AggregationOp op) { return col.isar.getTxnSync(false, (Txn txn) { final resultPtrPtr = txn.alloc>(); nCall( IC.isar_q_aggregate( col.ptr, queryPtr, txn.ptr, op.index, propertyId ?? 0, resultPtrPtr, ), ); return _convertAggregatedResult(resultPtrPtr.value, op); }); } R? _convertAggregatedResult( Pointer resultPtr, AggregationOp op, ) { final nullable = op == AggregationOp.min || op == AggregationOp.max; if (R == int || R == DateTime) { final value = IC.isar_q_aggregate_long_result(resultPtr); if (nullable && value == nullLong) { return null; } if (R == int) { return value as R; } else { return DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true).toLocal() as R; } } else { final value = IC.isar_q_aggregate_double_result(resultPtr); if (nullable && value.isNaN) { return null; } else { return value as R; } } } } ================================================ FILE: packages/isar/lib/src/native/split_words.dart ================================================ import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_reader_impl.dart'; // ignore: public_member_api_docs List isarSplitWords(String input) { initializeCoreBinary(); final bytesPtr = malloc(input.length * 3); final bytes = bytesPtr.asTypedList(input.length * 3); final byteCount = encodeString(input, bytes, 0); final wordCountPtr = malloc(); final boundariesPtr = IC.isar_find_word_boundaries(bytesPtr.cast(), byteCount, wordCountPtr); final wordCount = wordCountPtr.value; final boundaries = boundariesPtr.asTypedList(wordCount * 2); final words = []; for (var i = 0; i < wordCount * 2; i++) { final wordBytes = bytes.sublist(boundaries[i++], boundaries[i]); words.add(IsarReaderImpl.utf8Decoder.convert(wordBytes)); } IC.isar_free_word_boundaries(boundariesPtr, wordCount); malloc.free(bytesPtr); malloc.free(wordCountPtr); return words; } ================================================ FILE: packages/isar/lib/src/native/txn.dart ================================================ import 'dart:async'; import 'dart:collection'; import 'dart:ffi'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_common.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/isar_core.dart'; /// @nodoc class Txn extends Transaction { /// @nodoc Txn.sync(Isar isar, this.ptr, bool write) : super(isar, true, write); /// @nodoc Txn.async(Isar isar, this.ptr, bool write, Stream stream) : super(isar, false, write) { _completers = Queue(); _portSubscription = stream.listen( (_) => _completers.removeFirst().complete(), onError: (Object e) => _completers.removeFirst().completeError(e), ); } @override bool active = true; /// An arena allocator that has the same lifetime as this transaction. final alloc = Arena(malloc); /// The pointer to the native transaction. final Pointer ptr; Pointer? _cObjPtr; Pointer? _cObjSetPtr; late Pointer _buffer; int _bufferLen = -1; late final Queue> _completers; late final StreamSubscription? _portSubscription; /// Get a shared CObject pointer Pointer getCObject() { _cObjPtr ??= alloc(); return _cObjPtr!; } /// Get a shared CObjectSet pointer Pointer getCObjectsSet() { _cObjSetPtr ??= alloc(); return _cObjSetPtr!; } /// Allocate a new CObjectSet with the given capacity. Pointer newCObjectSet(int length) { final cObjSetPtr = alloc(); cObjSetPtr.ref ..objects = alloc(length) ..length = length; return cObjSetPtr; } /// Get a shared buffer with at least the specified size. Pointer getBuffer(int size) { if (_bufferLen < size) { final allocSize = (size * 1.3).toInt(); _buffer = alloc(allocSize); _bufferLen = allocSize; } return _buffer; } /// Wait for the latest async operation to complete. Future wait() { final completer = Completer(); _completers.add(completer); return completer.future; } @override Future commit() async { active = false; IC.isar_txn_finish(ptr, true); await wait(); unawaited(_portSubscription!.cancel()); } @override void commitSync() { active = false; nCall(IC.isar_txn_finish(ptr, true)); } @override Future abort() async { active = false; IC.isar_txn_finish(ptr, false); await wait(); unawaited(_portSubscription!.cancel()); } @override void abortSync() { active = false; nCall(IC.isar_txn_finish(ptr, false)); } @override void free() { alloc.releaseAll(); } } ================================================ FILE: packages/isar/lib/src/query.dart ================================================ part of isar; /// Querying is how you find records that match certain conditions. abstract class Query { /// The default precision for floating point number queries. static const double epsilon = 0.00001; /// The corresponding Isar instance. Isar get isar; /// {@template query_find_first} /// Find the first object that matches this query or `null` if no object /// matches. /// {@endtemplate} Future findFirst(); /// {@macro query_find_first} T? findFirstSync(); /// {@template query_find_all} /// Find all objects that match this query. /// {@endtemplate} Future> findAll(); /// {@macro query_find_all} List findAllSync(); /// @nodoc @protected Future aggregate(AggregationOp op); /// @nodoc @protected R? aggregateSync(AggregationOp op); /// {@template query_count} /// Count how many objects match this query. /// /// This operation is much faster than using `findAll().length`. /// {@endtemplate} Future count() => aggregate(AggregationOp.count).then((int? value) => value!); /// {@macro query_count} int countSync() => aggregateSync(AggregationOp.count)!; /// {@template query_is_empty} /// Returns `true` if there are no objects that match this query. /// /// This operation is faster than using `count() == 0`. /// {@endtemplate} Future isEmpty() => aggregate(AggregationOp.isEmpty).then((value) => value == 1); /// {@macro query_is_empty} bool isEmptySync() => aggregateSync(AggregationOp.isEmpty) == 1; /// {@template query_is_not_empty} /// Returns `true` if there are objects that match this query. /// /// This operation is faster than using `count() > 0`. /// {@endtemplate} Future isNotEmpty() => aggregate(AggregationOp.isEmpty).then((value) => value == 0); /// {@macro query_is_not_empty} bool isNotEmptySync() => aggregateSync(AggregationOp.isEmpty) == 0; /// {@template query_delete_first} /// Delete the first object that matches this query. Returns whether a object /// has been deleted. /// {@endtemplate} Future deleteFirst(); /// {@macro query_delete_first} bool deleteFirstSync(); /// {@template query_delete_all} /// Delete all objects that match this query. Returns the number of deleted /// objects. /// {@endtemplate} Future deleteAll(); /// {@macro query_delete_all} int deleteAllSync(); /// {@template query_watch} /// Create a watcher that yields the results of this query whenever its /// results have (potentially) changed. /// /// If you don't always use the results, consider using `watchLazy` and rerun /// the query manually. If [fireImmediately] is `true`, the results will be /// sent to the consumer immediately. /// {@endtemplate} Stream> watch({bool fireImmediately = false}); /// {@template query_watch_lazy} /// Watch the query for changes. If [fireImmediately] is `true`, an event will /// be fired immediately. /// {@endtemplate} Stream watchLazy({bool fireImmediately = false}); /// {@template query_export_json_raw} /// Export the results of this query as json bytes. /// /// **IMPORTANT:** Do not leak the bytes outside the callback. If you need to /// use the bytes outside, create a copy of the `Uint8List`. /// {@endtemplate} Future exportJsonRaw(R Function(Uint8List) callback); /// {@macro query_export_json_raw} R exportJsonRawSync(R Function(Uint8List) callback); /// {@template query_export_json} /// Export the results of this query as json. /// {@endtemplate} Future>> exportJson() { return exportJsonRaw((Uint8List bytes) { final json = jsonDecode(const Utf8Decoder().convert(bytes)) as List; return json.cast>(); }); } /// {@macro query_export_json} List> exportJsonSync() { return exportJsonRawSync((Uint8List bytes) { final json = jsonDecode(const Utf8Decoder().convert(bytes)) as List; return json.cast>(); }); } } /// @nodoc @protected enum AggregationOp { /// Finds the smallest value. min, /// Finds the largest value. max, /// Calculates the sum of all values. sum, /// Calculates the average of all values. average, /// Counts all values. count, /// Returns `true` if the query has no results. isEmpty, } /// Extension for Queries extension QueryAggregation on Query { /// {@template aggregation_min} /// Returns the minimum value of this query. /// {@endtemplate} Future min() => aggregate(AggregationOp.min); /// {@macro aggregation_min} T? minSync() => aggregateSync(AggregationOp.min); /// {@template aggregation_max} /// Returns the maximum value of this query. /// {@endtemplate} Future max() => aggregate(AggregationOp.max); /// {@macro aggregation_max} T? maxSync() => aggregateSync(AggregationOp.max); /// {@template aggregation_average} /// Returns the average value of this query. /// {@endtemplate} Future average() => aggregate(AggregationOp.average).then((double? value) => value!); /// {@macro aggregation_average} double averageSync() => aggregateSync(AggregationOp.average)!; /// {@template aggregation_sum} /// Returns the sum of all values of this query. /// {@endtemplate} Future sum() => aggregate(AggregationOp.sum).then((value) => value!); /// {@macro aggregation_sum} T sumSync() => aggregateSync(AggregationOp.sum)!; } /// Extension for Queries extension QueryDateAggregation on Query { /// {@macro aggregation_min} Future min() => aggregate(AggregationOp.min); /// {@macro aggregation_min} DateTime? minSync() => aggregateSync(AggregationOp.min); /// {@macro aggregation_max} Future max() => aggregate(AggregationOp.max); /// {@macro aggregation_max} DateTime? maxSync() => aggregateSync(AggregationOp.max); } ================================================ FILE: packages/isar/lib/src/query_builder.dart ================================================ part of isar; /// @nodoc @protected typedef FilterQuery = QueryBuilder Function(QueryBuilder q); /// Query builders are used to create queries in a safe way. /// /// Acquire a `QueryBuilder` instance using `collection.where()` or /// `collection.filter()`. class QueryBuilder { /// @nodoc @protected const QueryBuilder(this._query); final QueryBuilderInternal _query; /// @nodoc @protected static QueryBuilder apply( QueryBuilder qb, QueryBuilderInternal Function(QueryBuilderInternal query) transform, ) { return QueryBuilder(transform(qb._query)); } } /// @nodoc @protected class QueryBuilderInternal { /// @nodoc const QueryBuilderInternal({ this.collection, this.whereClauses = const [], this.whereDistinct = false, this.whereSort = Sort.asc, this.filter = const FilterGroup.and([]), this.filterGroupType = FilterGroupType.and, this.filterNot = false, this.distinctByProperties = const [], this.sortByProperties = const [], this.offset, this.limit, this.propertyName, }); /// @nodoc final IsarCollection? collection; /// @nodoc final List whereClauses; /// @nodoc final bool whereDistinct; /// @nodoc final Sort whereSort; /// @nodoc final FilterGroup filter; /// @nodoc final FilterGroupType filterGroupType; /// @nodoc final bool filterNot; /// @nodoc final List distinctByProperties; /// @nodoc final List sortByProperties; /// @nodoc final int? offset; /// @nodoc final int? limit; /// @nodoc final String? propertyName; /// @nodoc QueryBuilderInternal addFilterCondition(FilterOperation cond) { if (filterNot) { cond = FilterGroup.not(cond); } late FilterGroup filterGroup; if (filter.type == filterGroupType || filter.filters.length <= 1) { filterGroup = FilterGroup( type: filterGroupType, filters: [...filter.filters, cond], ); } else if (filterGroupType == FilterGroupType.and) { filterGroup = FilterGroup( type: filter.type, filters: [ ...filter.filters.sublist(0, filter.filters.length - 1), FilterGroup( type: filterGroupType, filters: [filter.filters.last, cond], ), ], ); } else { filterGroup = FilterGroup( type: filterGroupType, filters: [filter, cond], ); } return copyWith( filter: filterGroup, filterGroupType: FilterGroupType.and, filterNot: false, ); } /// @nodoc QueryBuilderInternal addWhereClause(WhereClause where) { return copyWith(whereClauses: [...whereClauses, where]); } /// @nodoc QueryBuilderInternal group(FilterQuery q) { // ignore: prefer_const_constructors final qb = q(QueryBuilder(QueryBuilderInternal())); return addFilterCondition(qb._query.filter); } /// @nodoc QueryBuilderInternal listLength( String property, int lower, bool includeLower, int upper, bool includeUpper, ) { if (!includeLower) { lower += 1; } if (!includeUpper) { if (upper == 0) { lower = 1; } else { upper -= 1; } } return addFilterCondition( FilterCondition.listLength( property: property, lower: lower, upper: upper, ), ); } /// @nodoc QueryBuilderInternal object( FilterQuery q, String property, ) { // ignore: prefer_const_constructors final qb = q(QueryBuilder(QueryBuilderInternal())); return addFilterCondition( ObjectFilter(filter: qb._query.filter, property: property), ); } /// @nodoc QueryBuilderInternal link( FilterQuery q, String linkName, ) { // ignore: prefer_const_constructors final qb = q(QueryBuilder(QueryBuilderInternal())); return addFilterCondition( LinkFilter(filter: qb._query.filter, linkName: linkName), ); } /// @nodoc QueryBuilderInternal linkLength( String linkName, int lower, bool includeLower, int upper, bool includeUpper, ) { if (!includeLower) { lower += 1; } if (!includeUpper) { if (upper == 0) { lower = 1; } else { upper -= 1; } } return addFilterCondition( LinkFilter.length( lower: lower, upper: upper, linkName: linkName, ), ); } /// @nodoc QueryBuilderInternal addSortBy(String propertyName, Sort sort) { return copyWith( sortByProperties: [ ...sortByProperties, SortProperty(property: propertyName, sort: sort), ], ); } /// @nodoc QueryBuilderInternal addDistinctBy( String propertyName, { bool? caseSensitive, }) { return copyWith( distinctByProperties: [ ...distinctByProperties, DistinctProperty( property: propertyName, caseSensitive: caseSensitive, ), ], ); } /// @nodoc QueryBuilderInternal addPropertyName(String propertyName) { return copyWith(propertyName: propertyName); } /// @nodoc QueryBuilderInternal copyWith({ List? whereClauses, FilterGroup? filter, bool? filterIsGrouped, FilterGroupType? filterGroupType, bool? filterNot, List? parentFilters, List? distinctByProperties, List? sortByProperties, int? offset, int? limit, String? propertyName, }) { assert(offset == null || offset >= 0, 'Invalid offset'); assert(limit == null || limit >= 0, 'Invalid limit'); return QueryBuilderInternal( collection: collection, whereClauses: whereClauses ?? List.unmodifiable(this.whereClauses), whereDistinct: whereDistinct, whereSort: whereSort, filter: filter ?? this.filter, filterGroupType: filterGroupType ?? this.filterGroupType, filterNot: filterNot ?? this.filterNot, distinctByProperties: distinctByProperties ?? List.unmodifiable(this.distinctByProperties), sortByProperties: sortByProperties ?? List.unmodifiable(this.sortByProperties), offset: offset ?? this.offset, limit: limit ?? this.limit, propertyName: propertyName ?? this.propertyName, ); } /// @nodoc @protected Query build() { return collection!.buildQuery( whereDistinct: whereDistinct, whereSort: whereSort, whereClauses: whereClauses, filter: filter, sortBy: sortByProperties, distinctBy: distinctByProperties, offset: offset, limit: limit, property: propertyName, ); } } /// @nodoc /// /// Right after query starts @protected class QWhere implements QWhereClause, QFilter, QSortBy, QDistinct, QOffset, QLimit, QQueryProperty {} /// @nodoc /// /// No more where conditions are allowed @protected class QAfterWhere implements QFilter, QSortBy, QDistinct, QOffset, QLimit, QQueryProperty {} /// @nodoc @protected class QWhereClause {} /// @nodoc @protected class QAfterWhereClause implements QWhereOr, QFilter, QSortBy, QDistinct, QOffset, QLimit, QQueryProperty {} /// @nodoc @protected class QWhereOr {} /// @nodoc @protected class QFilter {} /// @nodoc @protected class QFilterCondition {} /// @nodoc @protected class QAfterFilterCondition implements QFilterCondition, QFilterOperator, QSortBy, QDistinct, QOffset, QLimit, QQueryProperty {} /// @nodoc @protected class QFilterOperator {} /// @nodoc @protected class QAfterFilterOperator implements QFilterCondition {} /// @nodoc @protected class QSortBy {} /// @nodoc @protected class QAfterSortBy implements QSortThenBy, QDistinct, QOffset, QLimit, QQueryProperty {} /// @nodoc @protected class QSortThenBy {} /// @nodoc @protected class QDistinct implements QOffset, QLimit, QQueryProperty {} /// @nodoc @protected class QOffset {} /// @nodoc @protected class QAfterOffset implements QLimit, QQueryProperty {} /// @nodoc @protected class QLimit {} /// @nodoc @protected class QAfterLimit implements QQueryProperty {} /// @nodoc @protected class QQueryProperty implements QQueryOperations {} /// @nodoc @protected class QQueryOperations {} ================================================ FILE: packages/isar/lib/src/query_builder_extensions.dart ================================================ part of isar; /// Extension for QueryBuilders. extension QueryWhereOr on QueryBuilder { /// Union of two where clauses. QueryBuilder or() { return QueryBuilder(_query); } } /// @nodoc @protected typedef WhereRepeatModifier = QueryBuilder Function(QueryBuilder q, E element); /// Extension for QueryBuilders. extension QueryWhere on QueryBuilder { /// Joins the results of the [modifier] for each item in [items] using logical /// OR. So an object will be included if it matches at least one of the /// resulting where clauses. /// /// If [items] is empty, this is a no-op. QueryBuilder anyOf( Iterable items, WhereRepeatModifier modifier, ) { QueryBuilder? q; for (final e in items) { q = modifier(q?.or() ?? QueryBuilder(_query), e); } return q ?? QueryBuilder(_query); } } /// Extension for QueryBuilders. extension QueryFilters on QueryBuilder { /// Start using filter conditions. QueryBuilder filter() { return QueryBuilder(_query); } } /// @nodoc @protected typedef FilterRepeatModifier = QueryBuilder Function( QueryBuilder q, E element, ); /// Extension for QueryBuilders. extension QueryFilterAndOr on QueryBuilder { /// Intersection of two filter conditions. QueryBuilder and() { return QueryBuilder.apply( this, (q) => q.copyWith(filterGroupType: FilterGroupType.and), ); } /// Union of two filter conditions. QueryBuilder or() { return QueryBuilder.apply( this, (q) => q.copyWith(filterGroupType: FilterGroupType.or), ); } /// Logical XOR of two filter conditions. QueryBuilder xor() { return QueryBuilder.apply( this, (q) => q.copyWith(filterGroupType: FilterGroupType.xor), ); } } /// Extension for QueryBuilders. extension QueryFilterNot on QueryBuilder { /// Complement the next filter condition or group. QueryBuilder not() { return QueryBuilder.apply( this, (q) => q.copyWith(filterNot: !q.filterNot), ); } /// Joins the results of the [modifier] for each item in [items] using logical /// OR. So an object will be included if it matches at least one of the /// resulting filters. /// /// If [items] is empty, this is a no-op. QueryBuilder anyOf( Iterable items, FilterRepeatModifier modifier, ) { return QueryBuilder.apply(this, (query) { return query.group((q) { var q2 = QueryBuilder(q._query); for (final e in items) { q2 = modifier(q2.or(), e); } return q2; }); }); } /// Joins the results of the [modifier] for each item in [items] using logical /// AND. So an object will be included if it matches all of the resulting /// filters. /// /// If [items] is empty, this is a no-op. QueryBuilder allOf( Iterable items, FilterRepeatModifier modifier, ) { return QueryBuilder.apply(this, (query) { return query.group((q) { var q2 = QueryBuilder(q._query); for (final e in items) { q2 = modifier(q2.and(), e); } return q2; }); }); } /// Joins the results of the [modifier] for each item in [items] using logical /// XOR. So an object will be included if it matches exactly one of the /// resulting filters. /// /// If [items] is empty, this is a no-op. QueryBuilder oneOf( Iterable items, FilterRepeatModifier modifier, ) { QueryBuilder? q; for (final e in items) { q = modifier(q?.xor() ?? QueryBuilder(_query), e); } return q ?? QueryBuilder(_query); } } /// Extension for QueryBuilders. extension QueryFilterNoGroups on QueryBuilder { /// Group filter conditions. QueryBuilder group(FilterQuery q) { return QueryBuilder.apply(this, (query) => query.group(q)); } } /// Extension for QueryBuilders. extension QueryOffset on QueryBuilder { /// Offset the query results by a static number. QueryBuilder offset(int offset) { return QueryBuilder.apply(this, (q) => q.copyWith(offset: offset)); } } /// Extension for QueryBuilders. extension QueryLimit on QueryBuilder { /// Limit the maximum number of query results. QueryBuilder limit(int limit) { return QueryBuilder.apply(this, (q) => q.copyWith(limit: limit)); } } /// @nodoc @protected typedef QueryOption = QueryBuilder Function( QueryBuilder q, ); /// Extension for QueryBuilders. extension QueryModifier on QueryBuilder { /// Only apply a part of the query if `enabled` is true. QueryBuilder optional( bool enabled, QueryOption option, ) { if (enabled) { return option(this); } else { return QueryBuilder(_query); } } } /// Extension for QueryBuilders extension QueryExecute on QueryBuilder { /// Create a query from this query builder. Query build() => _query.build(); /// {@macro query_find_first} Future findFirst() => build().findFirst(); /// {@macro query_find_first} R? findFirstSync() => build().findFirstSync(); /// {@macro query_find_all} Future> findAll() => build().findAll(); /// {@macro query_find_all} List findAllSync() => build().findAllSync(); /// {@macro query_count} Future count() => build().count(); /// {@macro query_count} int countSync() => build().countSync(); /// {@macro query_is_empty} Future isEmpty() => build().isEmpty(); /// {@macro query_is_empty} bool isEmptySync() => build().isEmptySync(); /// {@macro query_is_not_empty} Future isNotEmpty() => build().isNotEmpty(); /// {@macro query_is_not_empty} bool isNotEmptySync() => build().isNotEmptySync(); /// {@macro query_delete_first} Future deleteFirst() => build().deleteFirst(); /// {@macro query_delete_first} bool deleteFirstSync() => build().deleteFirstSync(); /// {@macro query_delete_all} Future deleteAll() => build().deleteAll(); /// {@macro query_delete_all} int deleteAllSync() => build().deleteAllSync(); /// {@macro query_watch} Stream> watch({bool fireImmediately = false}) => build().watch(fireImmediately: fireImmediately); /// {@macro query_watch_lazy} Stream watchLazy({bool fireImmediately = false}) => build().watchLazy(fireImmediately: fireImmediately); /// {@macro query_export_json_raw} Future exportJsonRaw(T Function(Uint8List) callback) => build().exportJsonRaw(callback); /// {@macro query_export_json_raw} T exportJsonRawSync(T Function(Uint8List) callback) => build().exportJsonRawSync(callback); /// {@macro query_export_json} Future>> exportJson() => build().exportJson(); /// {@macro query_export_json} List> exportJsonSync() => build().exportJsonSync(); } /// Extension for QueryBuilders extension QueryExecuteAggregation on QueryBuilder { /// {@macro aggregation_min} Future min() => build().min(); /// {@macro aggregation_min} T? minSync() => build().minSync(); /// {@macro aggregation_max} Future max() => build().max(); /// {@macro aggregation_max} T? maxSync() => build().maxSync(); /// {@macro aggregation_average} Future average() => build().average(); /// {@macro aggregation_average} double averageSync() => build().averageSync(); /// {@macro aggregation_sum} Future sum() => build().sum(); /// {@macro aggregation_sum} T sumSync() => build().sumSync(); } /// Extension for QueryBuilders extension QueryExecuteDateAggregation on QueryBuilder { /// {@macro aggregation_min} Future min() => build().min(); /// {@macro aggregation_min} DateTime? minSync() => build().minSync(); /// {@macro aggregation_max} Future max() => build().max(); /// {@macro aggregation_max} DateTime? maxSync() => build().maxSync(); } ================================================ FILE: packages/isar/lib/src/query_components.dart ================================================ part of isar; /// A where clause to traverse an Isar index. abstract class WhereClause { const WhereClause._(); } /// A where clause traversing the primary index (ids). class IdWhereClause extends WhereClause { /// Where clause that matches all ids. Useful to get sorted results. const IdWhereClause.any() : lower = null, upper = null, includeLower = true, includeUpper = true, super._(); /// Where clause that matches all id values greater than the given [lower] /// bound. const IdWhereClause.greaterThan({ required Id this.lower, this.includeLower = true, }) : upper = null, includeUpper = true, super._(); /// Where clause that matches all id values less than the given [upper] /// bound. const IdWhereClause.lessThan({ required Id this.upper, this.includeUpper = true, }) : lower = null, includeLower = true, super._(); /// Where clause that matches the id value equal to the given [value]. const IdWhereClause.equalTo({ required Id value, }) : lower = value, upper = value, includeLower = true, includeUpper = true, super._(); /// Where clause that matches all id values between the given [lower] and /// [upper] bounds. const IdWhereClause.between({ this.lower, this.includeLower = true, this.upper, this.includeUpper = true, }) : super._(); /// The lower bound id or `null` for unbounded. final Id? lower; /// Whether the lower bound should be included in the results. final bool includeLower; /// The upper bound id or `null` for unbounded. final Id? upper; /// Whether the upper bound should be included in the results. final bool includeUpper; } /// A where clause traversing an index. class IndexWhereClause extends WhereClause { /// Where clause that matches all index values. Useful to get sorted results. const IndexWhereClause.any({required this.indexName}) : lower = null, upper = null, includeLower = true, includeUpper = true, epsilon = Query.epsilon, super._(); /// Where clause that matches all index values greater than the given [lower] /// bound. /// /// For composite indexes, the first elements of the [lower] list are checked /// for equality. const IndexWhereClause.greaterThan({ required this.indexName, required IndexKey this.lower, this.includeLower = true, this.epsilon = Query.epsilon, }) : upper = null, includeUpper = true, super._(); /// Where clause that matches all index values less than the given [upper] /// bound. /// /// For composite indexes, the first elements of the [upper] list are checked /// for equality. const IndexWhereClause.lessThan({ required this.indexName, required IndexKey this.upper, this.includeUpper = true, this.epsilon = Query.epsilon, }) : lower = null, includeLower = true, super._(); /// Where clause that matches all index values equal to the given [value]. const IndexWhereClause.equalTo({ required this.indexName, required IndexKey value, this.epsilon = Query.epsilon, }) : lower = value, upper = value, includeLower = true, includeUpper = true, super._(); /// Where clause that matches all index values between the given [lower] and /// [upper] bounds. /// /// For composite indexes, the first elements of the [lower] and [upper] lists /// are checked for equality. const IndexWhereClause.between({ required this.indexName, required IndexKey this.lower, this.includeLower = true, required IndexKey this.upper, this.includeUpper = true, this.epsilon = Query.epsilon, }) : super._(); /// The Isar name of the index to be used. final String indexName; /// The lower bound of the where clause. final IndexKey? lower; /// Whether the lower bound should be included in the results. Double values /// are never included. final bool includeLower; /// The upper bound of the where clause. final IndexKey? upper; /// Whether the upper bound should be included in the results. Double values /// are never included. final bool includeUpper; /// The precision to use for floating point values. final double epsilon; } /// A where clause traversing objects linked to the specified object. class LinkWhereClause extends WhereClause { /// Create a where clause for the specified link. const LinkWhereClause({ required this.linkCollection, required this.linkName, required this.id, }) : super._(); /// The name of the collection the link originates from. final String linkCollection; /// The isar name of the link to be used. final String linkName; /// The id of the source object. final Id id; } /// @nodoc @protected abstract class FilterOperation { const FilterOperation._(); } /// The type of dynamic filter conditions. enum FilterConditionType { /// Filter checking for equality. equalTo, /// Filter matching values greater than the bound. greaterThan, /// Filter matching values smaller than the bound. lessThan, /// Filter matching values between the bounds. between, /// Filter matching String values starting with the prefix. startsWith, /// Filter matching String values ending with the suffix. endsWith, /// Filter matching String values containing the String. contains, /// Filter matching String values matching the wildcard. matches, /// Filter matching values that are `null`. isNull, /// Filter matching values that are not `null`. isNotNull, /// Filter matching lists that contain `null`. elementIsNull, /// Filter matching lists that contain an element that is not `null`. elementIsNotNull, /// Filter matching the length of a list. listLength, } /// Create a filter condition dynamically. class FilterCondition extends FilterOperation { /// @nodoc @protected const FilterCondition({ required this.type, required this.property, this.value1, this.value2, required this.include1, required this.include2, required this.caseSensitive, this.epsilon = Query.epsilon, }) : super._(); /// Filters the results to only include objects where the property equals /// [value]. /// /// For lists, at least one of the values in the list has to match. const FilterCondition.equalTo({ required this.property, required Object? value, this.caseSensitive = true, this.epsilon = Query.epsilon, }) : type = FilterConditionType.equalTo, value1 = value, include1 = true, value2 = null, include2 = false, super._(); /// Filters the results to only include objects where the property is greater /// than [value]. /// /// For lists, at least one of the values in the list has to match. const FilterCondition.greaterThan({ required this.property, required Object? value, bool include = false, this.caseSensitive = true, this.epsilon = Query.epsilon, }) : type = FilterConditionType.greaterThan, value1 = value, include1 = include, value2 = null, include2 = false, super._(); /// Filters the results to only include objects where the property is less /// than [value]. /// /// For lists, at least one of the values in the list has to match. const FilterCondition.lessThan({ required this.property, required Object? value, bool include = false, this.caseSensitive = true, this.epsilon = Query.epsilon, }) : type = FilterConditionType.lessThan, value1 = value, include1 = include, value2 = null, include2 = false, super._(); /// Filters the results to only include objects where the property is /// between [lower] and [upper]. /// /// For lists, at least one of the values in the list has to match. const FilterCondition.between({ required this.property, Object? lower, bool includeLower = true, Object? upper, bool includeUpper = true, this.caseSensitive = true, this.epsilon = Query.epsilon, }) : value1 = lower, include1 = includeLower, value2 = upper, include2 = includeUpper, type = FilterConditionType.between, super._(); /// Filters the results to only include objects where the property starts /// with [value]. /// /// For String lists, at least one of the values in the list has to match. const FilterCondition.startsWith({ required this.property, required String value, this.caseSensitive = true, }) : type = FilterConditionType.startsWith, value1 = value, include1 = true, value2 = null, include2 = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the property ends with /// [value]. /// /// For String lists, at least one of the values in the list has to match. const FilterCondition.endsWith({ required this.property, required String value, this.caseSensitive = true, }) : type = FilterConditionType.endsWith, value1 = value, include1 = true, value2 = null, include2 = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the String property /// contains [value]. /// /// For String lists, at least one of the values in the list has to match. const FilterCondition.contains({ required this.property, required String value, this.caseSensitive = true, }) : type = FilterConditionType.contains, value1 = value, include1 = true, value2 = null, include2 = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the property matches /// the [wildcard]. /// /// For String lists, at least one of the values in the list has to match. const FilterCondition.matches({ required this.property, required String wildcard, this.caseSensitive = true, }) : type = FilterConditionType.matches, value1 = wildcard, include1 = true, value2 = null, include2 = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the property is null. const FilterCondition.isNull({ required this.property, }) : type = FilterConditionType.isNull, value1 = null, include1 = false, value2 = null, include2 = false, caseSensitive = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the property is not /// null. const FilterCondition.isNotNull({ required this.property, }) : type = FilterConditionType.isNotNull, value1 = null, include1 = false, value2 = null, include2 = false, caseSensitive = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include lists that contain `null`. const FilterCondition.elementIsNull({ required this.property, }) : type = FilterConditionType.elementIsNull, value1 = null, include1 = false, value2 = null, include2 = false, caseSensitive = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include lists that do not contain `null`. const FilterCondition.elementIsNotNull({ required this.property, }) : type = FilterConditionType.elementIsNotNull, value1 = null, include1 = false, value2 = null, include2 = false, caseSensitive = false, epsilon = Query.epsilon, super._(); /// Filters the results to only include objects where the length of /// [property] is between [lower] (included) and [upper] (included). /// /// Only list properties are supported. const FilterCondition.listLength({ required this.property, required int lower, required int upper, }) : type = FilterConditionType.listLength, value1 = lower, include1 = true, value2 = upper, include2 = true, caseSensitive = false, epsilon = Query.epsilon, assert(lower >= 0 && upper >= 0, 'List length must be positive.'), super._(); /// Type of the filter condition. final FilterConditionType type; /// Property used for comparisons. final String property; /// Value used for comparisons. Lower bound for `ConditionType.between`. final Object? value1; /// Should `value1` be part of the results. final bool include1; /// Upper bound for `ConditionType.between`. final Object? value2; /// Should `value1` be part of the results. final bool include2; /// Are string operations case sensitive. final bool caseSensitive; /// The precision to use for floating point values. final double epsilon; } /// The type of filter groups. enum FilterGroupType { /// Logical AND. and, /// Logical OR. or, /// Logical XOR. xor, /// Logical NOT. not, } /// Group one or more filter conditions. class FilterGroup extends FilterOperation { /// @nodoc @protected FilterGroup({ required this.type, required this.filters, }) : super._(); /// Create a logical AND filter group. /// /// Matches when all [filters] match. const FilterGroup.and(this.filters) : type = FilterGroupType.and, super._(); /// Create a logical OR filter group. /// /// Matches when any of the [filters] matches. const FilterGroup.or(this.filters) : type = FilterGroupType.or, super._(); /// Create a logical XOR filter group. /// /// Matches when exactly one of the [filters] matches. const FilterGroup.xor(this.filters) : type = FilterGroupType.xor, super._(); /// Negate a filter. /// /// Matches when any of the [filter] doesn't matches. FilterGroup.not(FilterOperation filter) : filters = [filter], type = FilterGroupType.not, super._(); /// Type of this group. final FilterGroupType type; /// The filter(s) to be grouped. final List filters; } /// Sort order enum Sort { /// Ascending sort order. asc, /// Descending sort order. desc, } /// Property used to sort query results. class SortProperty { /// Create a sort property. const SortProperty({required this.property, required this.sort}); /// Isar name of the property used for sorting. final String property; /// Sort order. final Sort sort; } /// Property used to filter duplicate values. class DistinctProperty { /// Create a distinct property. const DistinctProperty({required this.property, this.caseSensitive}); /// Isar name of the property used for sorting. final String property; /// Should Strings be case sensitive? final bool? caseSensitive; } /// Filter condition based on an embedded object. class ObjectFilter extends FilterOperation { /// Create a filter condition based on an embedded object. const ObjectFilter({ required this.property, required this.filter, }) : super._(); /// Property containing the embedded object(s). final String property; /// Filter condition that should be applied final FilterOperation filter; } /// Filter condition based on a link. class LinkFilter extends FilterOperation { /// Create a filter condition based on a link. const LinkFilter({ required this.linkName, required FilterOperation this.filter, }) : lower = null, upper = null, super._(); /// Create a filter condition based on the number of linked objects. const LinkFilter.length({ required this.linkName, required int this.lower, required int this.upper, }) : filter = null, assert(lower >= 0 && upper >= 0, 'Link length must be positive.'), super._(); /// Isar name of the link. final String linkName; /// Filter condition that should be applied final FilterOperation? filter; /// The minumum number of linked objects final int? lower; /// The maximum number of linked objects final int? upper; } ================================================ FILE: packages/isar/lib/src/schema/collection_schema.dart ================================================ part of isar; /// This schema represents a collection. class CollectionSchema extends Schema { /// @nodoc @protected const CollectionSchema({ required super.id, required super.name, required super.properties, required super.estimateSize, required super.serialize, required super.deserialize, required super.deserializeProp, required this.idName, required this.indexes, required this.links, required this.embeddedSchemas, required this.getId, required this.getLinks, required this.attach, required this.version, }) : assert( Isar.version == version, 'Outdated generated code. Please re-run code ' 'generation using the latest generator.', ); /// @nodoc @protected factory CollectionSchema.fromJson(Map json) { final collection = Schema.fromJson(json); return CollectionSchema( id: collection.id, name: collection.name, properties: collection.properties, idName: json['idName'] as String, indexes: { for (final index in json['indexes'] as List) (index as Map)['name'] as String: IndexSchema.fromJson(index), }, links: { for (final link in json['links'] as List) (link as Map)['name'] as String: LinkSchema.fromJson(link), }, embeddedSchemas: { for (final schema in json['embeddedSchemas'] as List) (schema as Map)['name'] as String: Schema.fromJson(schema), }, estimateSize: (_, __, ___) => throw UnimplementedError(), serialize: (_, __, ___, ____) => throw UnimplementedError(), deserialize: (_, __, ___, ____) => throw UnimplementedError(), deserializeProp: (_, __, ___, ____) => throw UnimplementedError(), getId: (_) => throw UnimplementedError(), getLinks: (_) => throw UnimplementedError(), attach: (_, __, ___) => throw UnimplementedError(), version: Isar.version, ); } /// Name of the id property final String idName; @override bool get embedded => false; /// A map of name -> index pairs final Map indexes; /// A map of name -> link pairs final Map links; /// A map of name -> embedded schema pairs final Map> embeddedSchemas; /// @nodoc final GetId getId; /// @nodoc final GetLinks getLinks; /// @nodoc final Attach attach; /// @nodoc final String version; /// @nodoc void toCollection(void Function() callback) => callback(); /// @nodoc @pragma('vm:prefer-inline') IndexSchema index(String indexName) { final index = indexes[indexName]; if (index != null) { return index; } else { throw IsarError('Unknown index "$indexName"'); } } /// @nodoc @pragma('vm:prefer-inline') LinkSchema link(String linkName) { final link = links[linkName]; if (link != null) { return link; } else { throw IsarError('Unknown link "$linkName"'); } } /// @nodoc @protected @override Map toJson() { final json = { ...super.toJson(), 'idName': idName, 'indexes': [ for (final index in indexes.values) index.toJson(), ], 'links': [ for (final link in links.values) link.toJson(), ], }; assert(() { json['embeddedSchemas'] = [ for (final schema in embeddedSchemas.values) schema.toJson(), ]; return true; }()); return json; } } /// @nodoc @protected typedef GetId = Id Function(T object); /// @nodoc @protected typedef GetLinks = List> Function(T object); /// @nodoc @protected typedef Attach = void Function(IsarCollection col, Id id, T object); ================================================ FILE: packages/isar/lib/src/schema/index_schema.dart ================================================ part of isar; /// This schema represents an index. class IndexSchema { /// @nodoc @protected const IndexSchema({ required this.id, required this.name, required this.unique, required this.replace, required this.properties, }); /// @nodoc @protected factory IndexSchema.fromJson(Map json) { return IndexSchema( id: -1, name: json['name'] as String, unique: json['unique'] as bool, replace: json['replace'] as bool, properties: (json['properties'] as List) .map((e) => IndexPropertySchema.fromJson(e as Map)) .toList(), ); } /// Internal id of this index. final int id; /// Name of this index. final String name; /// Whether duplicates are disallowed in this index. final bool unique; /// Whether duplocates will be replaced or throw an error. final bool replace; /// Composite properties. final List properties; /// @nodoc @protected Map toJson() { final json = { 'name': name, 'unique': unique, 'replace': replace, 'properties': [ for (final property in properties) property.toJson(), ], }; return json; } } /// This schema represents a composite index property. class IndexPropertySchema { /// @nodoc @protected const IndexPropertySchema({ required this.name, required this.type, required this.caseSensitive, }); /// @nodoc @protected factory IndexPropertySchema.fromJson(Map json) { return IndexPropertySchema( name: json['name'] as String, type: IndexType.values.firstWhere((e) => _typeName[e] == json['type']), caseSensitive: json['caseSensitive'] as bool, ); } /// Isar name of the property. final String name; /// Type of index. final IndexType type; /// Whether String properties should be stored with casing. final bool caseSensitive; /// @nodoc @protected Map toJson() { return { 'name': name, 'type': _typeName[type], 'caseSensitive': caseSensitive, }; } static const _typeName = { IndexType.value: 'Value', IndexType.hash: 'Hash', IndexType.hashElements: 'HashElements', }; } ================================================ FILE: packages/isar/lib/src/schema/link_schema.dart ================================================ part of isar; /// This schema represents a link to the same or another collection. class LinkSchema { /// @nodoc @protected const LinkSchema({ required this.id, required this.name, required this.target, required this.single, this.linkName, }); /// @nodoc @protected factory LinkSchema.fromJson(Map json) { return LinkSchema( id: -1, name: json['name'] as String, target: json['target'] as String, single: json['single'] as bool, linkName: json['linkName'] as String?, ); } /// Internal id of this link. final int id; /// Name of this link. final String name; /// Isar name of the target collection. final String target; /// Whether this is link can only hold a single target object. final bool single; /// If this is a backlink, [linkName] is the name of the source link in the /// [target] collection. final String? linkName; /// Whether this link is a backlink. bool get isBacklink => linkName != null; /// @nodoc @protected Map toJson() { final json = { 'name': name, 'target': target, 'single': single, }; assert(() { if (linkName != null) { json['linkName'] = linkName; } return true; }()); return json; } } ================================================ FILE: packages/isar/lib/src/schema/property_schema.dart ================================================ part of isar; /// A single propery of a collection or embedded object. class PropertySchema { /// @nodoc @protected const PropertySchema({ required this.id, required this.name, required this.type, this.enumMap, this.target, }); /// @nodoc @protected factory PropertySchema.fromJson(Map json) { return PropertySchema( id: -1, name: json['name'] as String, type: IsarType.values.firstWhere((e) => e.schemaName == json['type']), enumMap: json['enumMap'] as Map?, target: json['target'] as String?, ); } /// Internal id of this property. final int id; /// Name of the property final String name; /// Isar type of the property final IsarType type; /// Maps enum names to database values final Map? enumMap; /// For embedded objects: Name of the target schema final String? target; /// @nodoc @protected Map toJson() { final json = { 'name': name, 'type': type.schemaName, if (target != null) 'target': target, }; assert(() { if (enumMap != null) { json['enumMap'] = enumMap; } return true; }()); return json; } } /// Supported Isar types enum IsarType { /// Boolean bool('Bool'), /// 8-bit unsigned integer byte('Byte'), /// 32-bit singed integer int('Int'), /// 32-bit float float('Float'), /// 64-bit singed integer long('Long'), /// 64-bit float double('Double'), /// DateTime dateTime('DateTime'), /// String string('String'), /// Embedded object object('Object'), /// Boolean list boolList('BoolList'), /// 8-bit unsigned integer list byteList('ByteList'), /// 32-bit singed integer list intList('IntList'), /// 32-bit float list floatList('FloatList'), /// 64-bit singed integer list longList('LongList'), /// 64-bit float list doubleList('DoubleList'), /// DateTime list dateTimeList('DateTimeList'), /// String list stringList('StringList'), /// Embedded object list objectList('ObjectList'); /// @nodoc const IsarType(this.schemaName); /// @nodoc final String schemaName; } /// @nodoc extension IsarTypeX on IsarType { /// Whether this type represents a list bool get isList => index >= IsarType.boolList.index; /// @nodoc IsarType get scalarType { switch (this) { case IsarType.boolList: return IsarType.bool; case IsarType.byteList: return IsarType.byte; case IsarType.intList: return IsarType.int; case IsarType.floatList: return IsarType.float; case IsarType.longList: return IsarType.long; case IsarType.doubleList: return IsarType.double; case IsarType.dateTimeList: return IsarType.dateTime; case IsarType.stringList: return IsarType.string; case IsarType.objectList: return IsarType.object; // ignore: no_default_cases default: return this; } } /// @nodoc IsarType get listType { switch (this) { case IsarType.bool: return IsarType.boolList; case IsarType.byte: return IsarType.byteList; case IsarType.int: return IsarType.intList; case IsarType.float: return IsarType.floatList; case IsarType.long: return IsarType.longList; case IsarType.double: return IsarType.doubleList; case IsarType.dateTime: return IsarType.dateTimeList; case IsarType.string: return IsarType.stringList; case IsarType.object: return IsarType.objectList; // ignore: no_default_cases default: return this; } } } ================================================ FILE: packages/isar/lib/src/schema/schema.dart ================================================ part of isar; /// This schema either represents a collection or embedded object. class Schema { /// @nodoc @protected const Schema({ required this.id, required this.name, required this.properties, required this.estimateSize, required this.serialize, required this.deserialize, required this.deserializeProp, }); /// @nodoc @protected factory Schema.fromJson(Map json) { return Schema( id: -1, name: json['name'] as String, properties: { for (final property in json['properties'] as List) (property as Map)['name'] as String: PropertySchema.fromJson(property), }, estimateSize: (_, __, ___) => throw UnimplementedError(), serialize: (_, __, ___, ____) => throw UnimplementedError(), deserialize: (_, __, ___, ____) => throw UnimplementedError(), deserializeProp: (_, __, ___, ____) => throw UnimplementedError(), ); } /// Internal id of this collection or embedded object. final int id; /// Name of the collection or embedded object final String name; /// Whether this is an embedded object bool get embedded => true; /// A map of name -> property pairs final Map properties; /// @nodoc @protected final EstimateSize estimateSize; /// @nodoc @protected final Serialize serialize; /// @nodoc @protected final Deserialize deserialize; /// @nodoc @protected final DeserializeProp deserializeProp; /// Returns a property by its name or throws an error. @pragma('vm:prefer-inline') PropertySchema property(String propertyName) { final property = properties[propertyName]; if (property != null) { return property; } else { throw IsarError('Unknown property "$propertyName"'); } } /// @nodoc @protected Map toJson() { final json = { 'name': name, 'embedded': embedded, 'properties': [ for (final property in properties.values) property.toJson(), ], }; return json; } /// @nodoc @protected Type get type => OBJ; } /// @nodoc @protected typedef EstimateSize = int Function( T object, List offsets, Map> allOffsets, ); /// @nodoc @protected typedef Serialize = void Function( T object, IsarWriter writer, List offsets, Map> allOffsets, ); /// @nodoc @protected typedef Deserialize = T Function( Id id, IsarReader reader, List offsets, Map> allOffsets, ); /// @nodoc @protected typedef DeserializeProp = dynamic Function( IsarReader reader, int propertyId, int offset, Map> allOffsets, ); ================================================ FILE: packages/isar/lib/src/web/bindings.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:indexed_db'; import 'dart:js'; import 'package:isar/isar.dart'; import 'package:js/js.dart'; import 'package:js/js_util.dart'; @JS('JSON.stringify') external String stringify(dynamic value); @JS('indexedDB.cmp') external int idbCmp(dynamic value1, dynamic value2); @JS('Object.keys') external List objectKeys(dynamic obj); Map jsMapToDart(Object obj) { final keys = objectKeys(obj); final map = {}; for (final key in keys) { map[key] = getProperty(obj, key); } return map; } @JS('Promise') class Promise {} extension PromiseX on Promise { Future wait() => promiseToFuture(this); } @JS('openIsar') external Promise openIsarJs( String name, List schemas, bool relaxedDurability, ); @JS('IsarTxn') class IsarTxnJs { external Promise commit(); external void abort(); external bool get write; } @JS('IsarInstance') class IsarInstanceJs { external IsarTxnJs beginTxn(bool write); external IsarCollectionJs getCollection(String name); external Promise close(bool deleteFromDisk); } typedef ChangeCallbackJs = void Function(); typedef ObjectChangeCallbackJs = void Function(Object? object); typedef QueryChangeCallbackJs = void Function(List results); typedef StopWatchingJs = JsFunction; @JS('IsarCollection') class IsarCollectionJs { external IsarLinkJs getLink(String name); external Promise getAll(IsarTxnJs txn, List ids); external Promise getAllByIndex( IsarTxnJs txn, String indexName, List> values, ); external Promise putAll(IsarTxnJs txn, List objects); external Promise deleteAll(IsarTxnJs txn, List ids); external Promise deleteAllByIndex( IsarTxnJs txn, String indexName, List keys, ); external Promise clear(IsarTxnJs txn); external StopWatchingJs watchLazy(ChangeCallbackJs callback); external StopWatchingJs watchObject(Id id, ObjectChangeCallbackJs callback); external StopWatchingJs watchQuery( QueryJs query, QueryChangeCallbackJs callback, ); external StopWatchingJs watchQueryLazy( QueryJs query, ChangeCallbackJs callback, ); } @JS('IsarLink') class IsarLinkJs { external Promise update( IsarTxnJs txn, bool backlink, Id id, List addedTargets, List deletedTargets, ); external Promise clear(IsarTxnJs txn, Id id, bool backlink); } @JS('IdWhereClause') @anonymous class IdWhereClauseJs { external KeyRange? range; } @JS('IndexWhereClause') @anonymous class IndexWhereClauseJs { external String indexName; external KeyRange? range; } @JS('LinkWhereClause') @anonymous class LinkWhereClauseJs { external String linkCollection; external String linkName; external bool backlink; external Id id; } @JS('Function') class FilterJs { external FilterJs(String id, String obj, String method); } @JS('Function') class SortCmpJs { external SortCmpJs(String a, String b, String method); } @JS('Function') class DistinctValueJs { external DistinctValueJs(String obj, String method); } @JS('IsarQuery') class QueryJs { external QueryJs( IsarCollectionJs collection, List whereClauses, bool whereDistinct, bool whereAscending, FilterJs? filter, SortCmpJs? sortCmp, DistinctValueJs? distinctValue, int? offset, int? limit, ); external Promise findFirst(IsarTxnJs txn); external Promise findAll(IsarTxnJs txn); external Promise deleteFirst(IsarTxnJs txn); external Promise deleteAll(IsarTxnJs txn); external Promise min(IsarTxnJs txn, String propertyName); external Promise max(IsarTxnJs txn, String propertyName); external Promise sum(IsarTxnJs txn, String propertyName); external Promise average(IsarTxnJs txn, String propertyName); external Promise count(IsarTxnJs txn); } ================================================ FILE: packages/isar/lib/src/web/isar_collection_impl.dart ================================================ // ignore_for_file: public_member_api_docs, invalid_use_of_protected_member import 'dart:async'; import 'dart:convert'; import 'dart:js'; import 'dart:js_util'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_impl.dart'; import 'package:isar/src/web/isar_reader_impl.dart'; import 'package:isar/src/web/isar_web.dart'; import 'package:isar/src/web/isar_writer_impl.dart'; import 'package:isar/src/web/query_build.dart'; import 'package:meta/dart2js.dart'; class IsarCollectionImpl extends IsarCollection { IsarCollectionImpl({ required this.isar, required this.native, required this.schema, }); @override final IsarImpl isar; final IsarCollectionJs native; @override final CollectionSchema schema; @override String get name => schema.name; late final _offsets = isar.offsets[OBJ]!; @tryInline OBJ deserializeObject(Object object) { final id = getProperty(object, idName); final reader = IsarReaderImpl(object); return schema.deserialize(id, reader, _offsets, isar.offsets); } @tryInline List deserializeObjects(dynamic objects) { final list = objects as List; final results = []; for (final object in list) { results.add(object is Object ? deserializeObject(object) : null); } return results; } @override Future> getAll(List ids) { return isar.getTxn(false, (IsarTxnJs txn) async { final objects = await native.getAll(txn, ids).wait>(); return deserializeObjects(objects); }); } @override Future> getAllByIndex(String indexName, List keys) { return isar.getTxn(false, (IsarTxnJs txn) async { final objects = await native .getAllByIndex(txn, indexName, keys) .wait>(); return deserializeObjects(objects); }); } @override List getAllSync(List ids) => unsupportedOnWeb(); @override List getAllByIndexSync(String indexName, List keys) => unsupportedOnWeb(); @override Future> putAll(List objects) { return putAllByIndex(null, objects); } @override List putAllSync(List objects, {bool saveLinks = true}) => unsupportedOnWeb(); @override Future> putAllByIndex(String? indexName, List objects) { return isar.getTxn(true, (IsarTxnJs txn) async { final serialized = []; for (final object in objects) { final jsObj = newObject(); final writer = IsarWriterImpl(jsObj); schema.serialize(object, writer, _offsets, isar.offsets); setProperty(jsObj, idName, schema.getId(object)); serialized.add(jsObj); } final ids = await native.putAll(txn, serialized).wait>(); for (var i = 0; i < objects.length; i++) { final object = objects[i]; final id = ids[i] as Id; schema.attach(this, id, object); } return ids.cast().toList(); }); } @override List putAllByIndexSync( String indexName, List objects, { bool saveLinks = true, }) => unsupportedOnWeb(); @override Future deleteAll(List ids) async { await isar.getTxn(true, (IsarTxnJs txn) { return native.deleteAll(txn, ids).wait(); }); return ids.length; } @override Future deleteAllByIndex(String indexName, List keys) { return isar.getTxn(true, (IsarTxnJs txn) { return native.deleteAllByIndex(txn, indexName, keys).wait(); }); } @override int deleteAllSync(List ids) => unsupportedOnWeb(); @override int deleteAllByIndexSync(String indexName, List keys) => unsupportedOnWeb(); @override Future clear() { return isar.getTxn(true, (IsarTxnJs txn) { return native.clear(txn).wait(); }); } @override void clearSync() => unsupportedOnWeb(); @override Future importJson(List> json) { return isar.getTxn(true, (IsarTxnJs txn) async { await native.putAll(txn, json.map(jsify).toList()).wait(); }); } @override Future importJsonRaw(Uint8List jsonBytes) { final json = jsonDecode(const Utf8Decoder().convert(jsonBytes)) as List; return importJson(json.cast()); } @override void importJsonSync(List> json) => unsupportedOnWeb(); @override void importJsonRawSync(Uint8List jsonBytes) => unsupportedOnWeb(); @override Future count() => where().count(); @override int countSync() => unsupportedOnWeb(); @override Future getSize({ bool includeIndexes = false, bool includeLinks = false, }) => unsupportedOnWeb(); @override int getSizeSync({ bool includeIndexes = false, bool includeLinks = false, }) => unsupportedOnWeb(); @override Stream watchLazy({bool fireImmediately = false}) { JsFunction? stop; final controller = StreamController( onCancel: () { stop?.apply([]); }, ); final void Function() callback = allowInterop(() => controller.add(null)); stop = native.watchLazy(callback); return controller.stream; } @override Stream watchObject( Id id, { bool fireImmediately = false, bool deserialize = true, }) { JsFunction? stop; final controller = StreamController( onCancel: () { stop?.apply([]); }, ); final Null Function(Object? obj) callback = allowInterop((Object? obj) { final object = deserialize && obj != null ? deserializeObject(obj) : null; controller.add(object); }); stop = native.watchObject(id, callback); return controller.stream; } @override Stream watchObjectLazy(Id id, {bool fireImmediately = false}) => watchObject(id, deserialize: false); @override Query buildQuery({ List whereClauses = const [], bool whereDistinct = false, Sort whereSort = Sort.asc, FilterOperation? filter, List sortBy = const [], List distinctBy = const [], int? offset, int? limit, String? property, }) { return buildWebQuery( this, whereClauses, whereDistinct, whereSort, filter, sortBy, distinctBy, offset, limit, property, ); } @override Future verify(List objects) => unsupportedOnWeb(); @override Future verifyLink( String linkName, List sourceIds, List targetIds, ) => unsupportedOnWeb(); } ================================================ FILE: packages/isar/lib/src/web/isar_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:html'; import 'package:isar/isar.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_web.dart'; const Symbol _zoneTxn = #zoneTxn; class IsarImpl extends Isar { IsarImpl(super.name, this.instance); final IsarInstanceJs instance; final offsets = >{}; final List> _activeAsyncTxns = []; @override final String? directory = null; void requireNotInTxn() { if (Zone.current[_zoneTxn] != null) { throw IsarError( 'Cannot perform this operation from within an active transaction.', ); } } Future _txn( bool write, bool silent, Future Function() callback, ) async { requireOpen(); requireNotInTxn(); final completer = Completer(); _activeAsyncTxns.add(completer.future); final txn = instance.beginTxn(write); final zone = Zone.current.fork( zoneValues: {_zoneTxn: txn}, ); T result; try { result = await zone.run(callback); await txn.commit().wait(); } catch (e) { txn.abort(); if (e is DomException) { if (e.name == DomException.CONSTRAINT) { throw IsarUniqueViolationError(); } else { throw IsarError('${e.name}: ${e.message}'); } } else { rethrow; } } finally { completer.complete(); _activeAsyncTxns.remove(completer.future); } return result; } @override Future txn(Future Function() callback) { return _txn(false, false, callback); } @override Future writeTxn(Future Function() callback, {bool silent = false}) { return _txn(true, silent, callback); } @override T txnSync(T Function() callback) => unsupportedOnWeb(); @override T writeTxnSync(T Function() callback, {bool silent = false}) => unsupportedOnWeb(); Future getTxn(bool write, Future Function(IsarTxnJs txn) callback) { final currentTxn = Zone.current[_zoneTxn] as IsarTxnJs?; if (currentTxn != null) { if (write && !currentTxn.write) { throw IsarError( 'Operation cannot be performed within a read transaction.', ); } return callback(currentTxn); } else if (!write) { return _txn(false, false, () { return callback(Zone.current[_zoneTxn] as IsarTxnJs); }); } else { throw IsarError('Write operations require an explicit transaction.'); } } @override Future getSize({ bool includeIndexes = false, bool includeLinks = false, }) => unsupportedOnWeb(); @override int getSizeSync({ bool includeIndexes = false, bool includeLinks = false, }) => unsupportedOnWeb(); @override Future copyToFile(String targetPath) => unsupportedOnWeb(); @override Future close({bool deleteFromDisk = false}) async { requireOpen(); requireNotInTxn(); await Future.wait(_activeAsyncTxns); await super.close(); await instance.close(deleteFromDisk).wait(); return true; } @override Future verify() => unsupportedOnWeb(); } ================================================ FILE: packages/isar/lib/src/web/isar_link_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; import 'package:isar/src/common/isar_link_common.dart'; import 'package:isar/src/common/isar_links_common.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_collection_impl.dart'; import 'package:isar/src/web/isar_web.dart'; mixin IsarLinkBaseMixin on IsarLinkBaseImpl { @override IsarCollectionImpl get sourceCollection => super.sourceCollection as IsarCollectionImpl; @override IsarCollectionImpl get targetCollection => super.targetCollection as IsarCollectionImpl; @override late final Id Function(OBJ) getId = targetCollection.schema.getId; late final String? backlinkLinkName = sourceCollection.schema.link(linkName).linkName; late final IsarLinkJs jsLink = backlinkLinkName != null ? targetCollection.native.getLink(backlinkLinkName!) : sourceCollection.native.getLink(linkName); @override Future update({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }) { final linkList = link.toList(); final unlinkList = unlink.toList(); final containingId = requireAttached(); final backlink = backlinkLinkName != null; final linkIds = List.filled(linkList.length, 0); for (var i = 0; i < linkList.length; i++) { linkIds[i] = requireGetId(linkList[i]); } final unlinkIds = List.filled(unlinkList.length, 0); for (var i = 0; i < unlinkList.length; i++) { unlinkIds[i] = requireGetId(unlinkList[i]); } return targetCollection.isar.getTxn(true, (IsarTxnJs txn) async { if (reset) { await jsLink.clear(txn, containingId, backlink).wait(); } return jsLink .update(txn, backlink, containingId, linkIds, unlinkIds) .wait(); }); } @override void updateSync({ Iterable link = const [], Iterable unlink = const [], bool reset = false, }) => unsupportedOnWeb(); } class IsarLinkImpl extends IsarLinkCommon with IsarLinkBaseMixin {} class IsarLinksImpl extends IsarLinksCommon with IsarLinkBaseMixin {} ================================================ FILE: packages/isar/lib/src/web/isar_reader_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; import 'package:js/js_util.dart'; import 'package:meta/dart2js.dart'; const nullNumber = double.negativeInfinity; const idName = '_id'; final nullDate = DateTime.fromMillisecondsSinceEpoch(0); class IsarReaderImpl implements IsarReader { IsarReaderImpl(this.object); final Object object; @tryInline @override bool readBool(int offset) { final value = getProperty(object, offset); return value == 1; } @tryInline @override bool? readBoolOrNull(int offset) { final value = getProperty(object, offset); return value == 0 ? false : value == 1 ? true : null; } @tryInline @override int readByte(int offset) { final value = getProperty(object, offset); return value is int ? value : nullNumber as int; } @tryInline @override int? readByteOrNull(int offset) { final value = getProperty(object, offset); return value is int && value != nullNumber ? value : null; } @tryInline @override int readInt(int offset) { final value = getProperty(object, offset); return value is int ? value : nullNumber as int; } @tryInline @override int? readIntOrNull(int offset) { final value = getProperty(object, offset); return value is int && value != nullNumber ? value : null; } @tryInline @override double readFloat(int offset) { final value = getProperty(object, offset); return value is double ? value : nullNumber; } @tryInline @override double? readFloatOrNull(int offset) { final value = getProperty(object, offset); return value is double && value != nullNumber ? value : null; } @tryInline @override int readLong(int offset) { final value = getProperty(object, offset); return value is int ? value : nullNumber as int; } @tryInline @override int? readLongOrNull(int offset) { final value = getProperty(object, offset); return value is int && value != nullNumber ? value : null; } @tryInline @override double readDouble(int offset) { final value = getProperty(object, offset); return value is double && value != nullNumber ? value : nullNumber; } @tryInline @override double? readDoubleOrNull(int offset) { final value = getProperty(object, offset); return value is double && value != nullNumber ? value : null; } @tryInline @override DateTime readDateTime(int offset) { final value = getProperty(object, offset); return value is int && value != nullNumber ? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal() : nullDate; } @tryInline @override DateTime? readDateTimeOrNull(int offset) { final value = getProperty(object, offset); return value is int && value != nullNumber ? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal() : null; } @tryInline @override String readString(int offset) { final value = getProperty(object, offset); return value is String ? value : ''; } @tryInline @override String? readStringOrNull(int offset) { final value = getProperty(object, offset); return value is String ? value : null; } @tryInline @override T? readObjectOrNull( int offset, Deserialize deserialize, Map> allOffsets, ) { final value = getProperty(object, offset); if (value is Object) { final reader = IsarReaderImpl(value); return deserialize(0, reader, allOffsets[T]!, allOffsets); } else { return null; } } @tryInline @override List? readBoolList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e == 1).toList() : null; } @tryInline @override List? readBoolOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value .map( (e) => e == 0 ? false : e == 1 ? true : null, ) .toList() : null; } @tryInline @override List? readByteList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is int ? e : nullNumber as int).toList() : null; } @tryInline @override List? readIntList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is int ? e : nullNumber as int).toList() : null; } @tryInline @override List? readIntOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is int && e != nullNumber ? e : null).toList() : null; } @tryInline @override List? readFloatList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is double ? e : nullNumber).toList() : null; } @tryInline @override List? readFloatOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is double && e != nullNumber ? e : null).toList() : null; } @tryInline @override List? readLongList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is int ? e : nullNumber as int).toList() : null; } @tryInline @override List? readLongOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is int && e != nullNumber ? e : null).toList() : null; } @tryInline @override List? readDoubleList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is double ? e : nullNumber).toList() : null; } @tryInline @override List? readDoubleOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is double && e != nullNumber ? e : null).toList() : null; } @tryInline @override List? readDateTimeList(int offset) { final value = getProperty(object, offset); return value is List ? value .map( (e) => e is int && e != nullNumber ? DateTime.fromMillisecondsSinceEpoch(e, isUtc: true) .toLocal() : nullDate, ) .toList() : null; } @tryInline @override List? readDateTimeOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value .map( (e) => e is int && e != nullNumber ? DateTime.fromMillisecondsSinceEpoch(e, isUtc: true) .toLocal() : null, ) .toList() : null; } @tryInline @override List? readStringList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is String ? e : '').toList() : null; } @tryInline @override List? readStringOrNullList(int offset) { final value = getProperty(object, offset); return value is List ? value.map((e) => e is String ? e : null).toList() : null; } @tryInline @override List? readObjectList( int offset, Deserialize deserialize, Map> allOffsets, T defaultValue, ) { final value = getProperty(object, offset); return value is List ? value.map((e) { if (e is Object) { final reader = IsarReaderImpl(e); return deserialize(0, reader, allOffsets[T]!, allOffsets); } else { return defaultValue; } }).toList() : null; } @tryInline @override List? readObjectOrNullList( int offset, Deserialize deserialize, Map> allOffsets, ) { final value = getProperty(object, offset); return value is List ? value.map((e) { if (e is Object) { final reader = IsarReaderImpl(e); return deserialize(0, reader, allOffsets[T]!, allOffsets); } else { return null; } }).toList() : null; } } ================================================ FILE: packages/isar/lib/src/web/isar_web.dart ================================================ // ignore_for_file: unused_field, public_member_api_docs import 'dart:async'; import 'package:isar/isar.dart'; import 'package:meta/meta.dart'; /// @nodoc @protected const Id isarMinId = -9007199254740990; /// @nodoc @protected const Id isarMaxId = 9007199254740991; /// @nodoc @protected const Id isarAutoIncrementId = -9007199254740991; /// @nodoc Never unsupportedOnWeb() { throw UnsupportedError('This operation is not supported for Isar web'); } class _WebAbi { static const androidArm = null as dynamic; static const androidArm64 = null as dynamic; static const androidIA32 = null as dynamic; static const androidX64 = null as dynamic; static const iosArm64 = null as dynamic; static const iosX64 = null as dynamic; static const linuxArm64 = null as dynamic; static const linuxX64 = null as dynamic; static const macosArm64 = null as dynamic; static const macosX64 = null as dynamic; static const windowsArm64 = null as dynamic; static const windowsX64 = null as dynamic; } /// @nodoc @protected typedef IsarAbi = _WebAbi; FutureOr initializeCoreBinary({ Map libraries = const {}, bool download = false, }) => unsupportedOnWeb(); ================================================ FILE: packages/isar/lib/src/web/isar_writer_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; import 'package:isar/src/web/isar_reader_impl.dart'; import 'package:js/js_util.dart'; import 'package:meta/dart2js.dart'; class IsarWriterImpl implements IsarWriter { IsarWriterImpl(this.object); final Object object; @tryInline @override void writeBool(int offset, bool? value) { final number = value == true ? 1 : value == false ? 0 : nullNumber; setProperty(object, offset, number); } @tryInline @override void writeByte(int offset, int value) { setProperty(object, offset, value); } @tryInline @override void writeInt(int offset, int? value) { setProperty(object, offset, value ?? nullNumber); } @tryInline @override void writeFloat(int offset, double? value) { setProperty(object, offset, value ?? nullNumber); } @tryInline @override void writeLong(int offset, int? value) { setProperty(object, offset, value ?? nullNumber); } @tryInline @override void writeDouble(int offset, double? value) { setProperty(object, offset, value ?? nullNumber); } @tryInline @override void writeDateTime(int offset, DateTime? value) { setProperty( object, offset, value?.toUtc().millisecondsSinceEpoch ?? nullNumber, ); } @tryInline @override void writeString(int offset, String? value) { setProperty(object, offset, value ?? nullNumber); } @tryInline @override void writeObject( int offset, Map> allOffsets, Serialize serialize, T? value, ) { if (value != null) { final object = newObject(); final writer = IsarWriterImpl(object); serialize(value, writer, allOffsets[T]!, allOffsets); setProperty(this.object, offset, object); } } @tryInline @override void writeByteList(int offset, List? values) { setProperty(object, offset, values ?? nullNumber); } @tryInline @override void writeBoolList(int offset, List? values) { final list = values ?.map( (e) => e == false ? 0 : e == true ? 1 : nullNumber, ) .toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeIntList(int offset, List? values) { final list = values?.map((e) => e ?? nullNumber).toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeFloatList(int offset, List? values) { final list = values?.map((e) => e ?? nullNumber).toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeLongList(int offset, List? values) { final list = values?.map((e) => e ?? nullNumber).toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeDoubleList(int offset, List? values) { final list = values?.map((e) => e ?? nullNumber).toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeDateTimeList(int offset, List? values) { final list = values ?.map((e) => e?.toUtc().millisecondsSinceEpoch ?? nullNumber) .toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeStringList(int offset, List? values) { final list = values?.map((e) => e ?? nullNumber).toList(); setProperty(object, offset, list ?? nullNumber); } @tryInline @override void writeObjectList( int offset, Map> allOffsets, Serialize serialize, List? values, ) { if (values != null) { final list = values.map((e) { if (e != null) { final object = newObject(); final writer = IsarWriterImpl(object); serialize(e, writer, allOffsets[T]!, allOffsets); return object; } }).toList(); setProperty(object, offset, list); } } } ================================================ FILE: packages/isar/lib/src/web/open.dart ================================================ // ignore_for_file: public_member_api_docs, invalid_use_of_protected_member import 'dart:html'; //import 'dart:js_util'; import 'package:isar/isar.dart'; /*import 'package:isar/src/common/schemas.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_collection_impl.dart'; import 'package:isar/src/web/isar_impl.dart';*/ import 'package:isar/src/web/isar_web.dart'; import 'package:meta/meta.dart'; bool _loaded = false; Future initializeIsarWeb([String? jsUrl]) async { if (_loaded) { return; } _loaded = true; final script = ScriptElement(); script.type = 'text/javascript'; // ignore: unsafe_html script.src = 'https://unpkg.com/isar@${Isar.version}/dist/index.js'; script.async = true; document.head!.append(script); await script.onLoad.first.timeout( const Duration(seconds: 30), onTimeout: () { throw IsarError('Failed to load Isar'); }, ); } @visibleForTesting void doNotInitializeIsarWeb() { _loaded = true; } Future openIsar({ required List> schemas, String? directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) async { throw IsarError('Please use Isar 2.5.0 if you need web support. ' 'A 3.x version with web support will be released soon.'); /*await initializeIsarWeb(); final schemasJson = getSchemas(schemas).map((e) => e.toJson()); final schemasJs = jsify(schemasJson.toList()) as List; final instance = await openIsarJs(name, schemasJs, relaxedDurability) .wait(); final isar = IsarImpl(name, instance); final cols = >{}; for (final schema in schemas) { final col = instance.getCollection(schema.name); schema.toCollection(() { schema as CollectionSchema; cols[OBJ] = IsarCollectionImpl( isar: isar, native: col, schema: schema, ); }); } isar.attachCollections(cols); return isar;*/ } Isar openIsarSync({ required List> schemas, String? directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) => unsupportedOnWeb(); ================================================ FILE: packages/isar/lib/src/web/query_build.dart ================================================ // ignore_for_file: public_member_api_docs, invalid_use_of_protected_member import 'dart:indexed_db'; import 'package:isar/isar.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_collection_impl.dart'; import 'package:isar/src/web/isar_web.dart'; import 'package:isar/src/web/query_impl.dart'; Query buildWebQuery( IsarCollectionImpl col, List whereClauses, bool whereDistinct, Sort whereSort, FilterOperation? filter, List sortBy, List distinctBy, int? offset, int? limit, String? property, ) { final whereClausesJs = whereClauses.map((wc) { if (wc is IdWhereClause) { return _buildIdWhereClause(wc); } else if (wc is IndexWhereClause) { return _buildIndexWhereClause(col.schema, wc); } else { return _buildLinkWhereClause(col, wc as LinkWhereClause); } }).toList(); final filterJs = filter != null ? _buildFilter(col.schema, filter) : null; final sortJs = sortBy.isNotEmpty ? _buildSort(sortBy) : null; final distinctJs = distinctBy.isNotEmpty ? _buildDistinct(distinctBy) : null; final queryJs = QueryJs( col.native, whereClausesJs, whereDistinct, whereSort == Sort.asc, filterJs, sortJs, distinctJs, offset, limit, ); QueryDeserialize deserialize; //if (property == null) { deserialize = col.deserializeObject as T Function(Object); /*} else { deserialize = (jsObj) => col.schema.deserializeProp(jsObj, property) as T; }*/ return QueryImpl(col, queryJs, deserialize, property); } dynamic _valueToJs(dynamic value) { if (value == null) { return double.negativeInfinity; } else if (value == true) { return 1; } else if (value == false) { return 0; } else if (value is DateTime) { return value.toUtc().millisecondsSinceEpoch; } else if (value is List) { return value.map(_valueToJs).toList(); } else { return value; } } IdWhereClauseJs _buildIdWhereClause(IdWhereClause wc) { return IdWhereClauseJs() ..range = _buildKeyRange( wc.lower, wc.upper, wc.includeLower, wc.includeUpper, ); } IndexWhereClauseJs _buildIndexWhereClause( CollectionSchema schema, IndexWhereClause wc, ) { final index = schema.index(wc.indexName); final lower = wc.lower?.toList(); final upper = wc.upper?.toList(); if (upper != null) { while (index.properties.length > upper.length) { upper.add([]); } } dynamic lowerUnwrapped = wc.lower; if (index.properties.length == 1 && lower != null) { lowerUnwrapped = lower.isNotEmpty ? lower[0] : null; } dynamic upperUnwrapped = upper; if (index.properties.length == 1 && upper != null) { upperUnwrapped = upper.isNotEmpty ? upper[0] : double.infinity; } return IndexWhereClauseJs() ..indexName = wc.indexName ..range = _buildKeyRange( wc.lower != null ? _valueToJs(lowerUnwrapped) : null, wc.upper != null ? _valueToJs(upperUnwrapped) : null, wc.includeLower, wc.includeUpper, ); } LinkWhereClauseJs _buildLinkWhereClause( IsarCollectionImpl col, LinkWhereClause wc, ) { // ignore: unused_local_variable final linkCol = col.isar.getCollectionByNameInternal(wc.linkCollection)! as IsarCollectionImpl; //final backlinkLinkName = linkCol.schema.backlinkLinkNames[wc.linkName]; return LinkWhereClauseJs() ..linkCollection = wc.linkCollection //..linkName = backlinkLinkName ?? wc.linkName //..backlink = backlinkLinkName != null ..id = wc.id; } KeyRange? _buildKeyRange( dynamic lower, dynamic upper, bool includeLower, bool includeUpper, ) { if (lower != null) { if (upper != null) { final boundsEqual = idbCmp(lower, upper) == 0; if (boundsEqual) { if (includeLower && includeUpper) { return KeyRange.only(lower); } else { // empty range return KeyRange.upperBound(double.negativeInfinity, true); } } return KeyRange.bound( lower, upper, !includeLower, !includeUpper, ); } else { return KeyRange.lowerBound(lower, !includeLower); } } else if (upper != null) { return KeyRange.upperBound(upper, !includeUpper); } return null; } FilterJs? _buildFilter( CollectionSchema schema, FilterOperation filter, ) { final filterStr = _buildFilterOperation(schema, filter); if (filterStr != null) { return FilterJs('id', 'obj', 'return $filterStr'); } else { return null; } } String? _buildFilterOperation( CollectionSchema schema, FilterOperation filter, ) { if (filter is FilterGroup) { return _buildFilterGroup(schema, filter); } else if (filter is LinkFilter) { unsupportedOnWeb(); } else if (filter is FilterCondition) { return _buildCondition(schema, filter); } else { return null; } } String? _buildFilterGroup(CollectionSchema schema, FilterGroup group) { final builtConditions = group.filters .map((op) => _buildFilterOperation(schema, op)) .where((e) => e != null) .toList(); if (builtConditions.isEmpty) { return null; } if (group.type == FilterGroupType.not) { return '!(${builtConditions[0]})'; } else if (builtConditions.length == 1) { return builtConditions[0]; } else if (group.type == FilterGroupType.xor) { final conditions = builtConditions.join(','); return 'IsarQuery.xor($conditions)'; } else { final op = group.type == FilterGroupType.or ? '||' : '&&'; final condition = builtConditions.join(op); return '($condition)'; } } String _buildCondition( CollectionSchema schema, FilterCondition condition, ) { dynamic _prepareFilterValue(dynamic value) { if (value == null) { return null; } else if (value is String) { return stringify(value); } else { return _valueToJs(value); } } final isListOp = condition.type != FilterConditionType.isNull && condition.type != FilterConditionType.listLength && schema.property(condition.property).type.isList; final accessor = condition.property == schema.idName ? 'id' : 'obj.${condition.property}'; final variable = isListOp ? 'e' : accessor; final cond = _buildConditionInternal( conditionType: condition.type, variable: variable, val1: _prepareFilterValue(condition.value1), include1: condition.include1, val2: _prepareFilterValue(condition.value2), include2: condition.include2, caseSensitive: condition.caseSensitive, ); if (isListOp) { return '(Array.isArray($accessor) && $accessor.some(e => $cond))'; } else { return cond; } } String _buildConditionInternal({ required FilterConditionType conditionType, required String variable, required Object? val1, required bool include1, required Object? val2, required bool include2, required bool caseSensitive, }) { final isNull = '($variable == null || $variable === -Infinity)'; switch (conditionType) { case FilterConditionType.equalTo: if (val1 == null) { return isNull; } else if (val1 is String && !caseSensitive) { return '$variable?.toLowerCase() === ${val1.toLowerCase()}'; } else { return '$variable === $val1'; } case FilterConditionType.between: final val = val1 ?? val2; final lowerOp = include1 ? '>=' : '>'; final upperOp = include2 ? '<=' : '<'; if (val == null) { return isNull; } else if ((val1 is String?) && (val2 is String?) && !caseSensitive) { final lower = val1?.toLowerCase() ?? '-Infinity'; final upper = val2?.toLowerCase() ?? '-Infinity'; final variableLc = '$variable?.toLowerCase() ?? -Infinity'; final lowerCond = 'indexedDB.cmp($variableLc, $lower) $lowerOp 0'; final upperCond = 'indexedDB.cmp($variableLc, $upper) $upperOp 0'; return '($lowerCond && $upperCond)'; } else { final lowerCond = 'indexedDB.cmp($variable, ${val1 ?? '-Infinity'}) $lowerOp 0'; final upperCond = 'indexedDB.cmp($variable, ${val2 ?? '-Infinity'}) $upperOp 0'; return '($lowerCond && $upperCond)'; } case FilterConditionType.lessThan: if (val1 == null) { if (include1) { return isNull; } else { return 'false'; } } else { final op = include1 ? '<=' : '<'; if (val1 is String && !caseSensitive) { return 'indexedDB.cmp($variable?.toLowerCase() ?? ' '-Infinity, ${val1.toLowerCase()}) $op 0'; } else { return 'indexedDB.cmp($variable, $val1) $op 0'; } } case FilterConditionType.greaterThan: if (val1 == null) { if (include1) { return 'true'; } else { return '!$isNull'; } } else { final op = include1 ? '>=' : '>'; if (val1 is String && !caseSensitive) { return 'indexedDB.cmp($variable?.toLowerCase() ?? ' '-Infinity, ${val1.toLowerCase()}) $op 0'; } else { return 'indexedDB.cmp($variable, $val1) $op 0'; } } case FilterConditionType.startsWith: case FilterConditionType.endsWith: case FilterConditionType.contains: final op = conditionType == FilterConditionType.startsWith ? 'startsWith' : conditionType == FilterConditionType.endsWith ? 'endsWith' : 'includes'; if (val1 is String) { final isString = 'typeof $variable == "string"'; if (!caseSensitive) { return '($isString && $variable.toLowerCase() ' '.$op(${val1.toLowerCase()}))'; } else { return '($isString && $variable.$op($val1))'; } } else { throw IsarError('Unsupported type for condition'); } case FilterConditionType.matches: throw UnimplementedError(); case FilterConditionType.isNull: return isNull; // ignore: no_default_cases default: throw UnimplementedError(); } } SortCmpJs _buildSort(List properties) { final sort = properties.map((e) { final op = e.sort == Sort.asc ? '' : '-'; return '${op}indexedDB.cmp(a.${e.property} ?? "-Infinity", b.${e.property} ' '?? "-Infinity")'; }).join('||'); return SortCmpJs('a', 'b', 'return $sort'); } DistinctValueJs _buildDistinct(List properties) { final distinct = properties.map((e) { if (e.caseSensitive == false) { return 'obj.${e.property}?.toLowerCase() ?? "-Infinity"'; } else { return 'obj.${e.property}?.toString() ?? "-Infinity"'; } }).join('+'); return DistinctValueJs('obj', 'return $distinct'); } ================================================ FILE: packages/isar/lib/src/web/query_impl.dart ================================================ // ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:convert'; import 'dart:js'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/web/bindings.dart'; import 'package:isar/src/web/isar_collection_impl.dart'; import 'package:isar/src/web/isar_web.dart'; typedef QueryDeserialize = T Function(Object); class QueryImpl extends Query { QueryImpl(this.col, this.queryJs, this.deserialize, this.propertyName); final IsarCollectionImpl col; final QueryJs queryJs; final QueryDeserialize deserialize; final String? propertyName; @override Isar get isar => col.isar; @override Future findFirst() { return col.isar.getTxn(false, (IsarTxnJs txn) async { final result = await queryJs.findFirst(txn).wait(); if (result == null) { return null; } return deserialize(result); }); } @override T? findFirstSync() => unsupportedOnWeb(); @override Future> findAll() { return col.isar.getTxn(false, (IsarTxnJs txn) async { final result = await queryJs.findAll(txn).wait>(); return result.map((e) => deserialize(e as Object)).toList(); }); } @override List findAllSync() => unsupportedOnWeb(); @override Future aggregate(AggregationOp op) { return col.isar.getTxn(false, (IsarTxnJs txn) async { final property = propertyName ?? col.schema.idName; num? result; switch (op) { case AggregationOp.min: result = await queryJs.min(txn, property).wait(); break; case AggregationOp.max: result = await queryJs.max(txn, property).wait(); break; case AggregationOp.sum: result = await queryJs.sum(txn, property).wait(); break; case AggregationOp.average: result = await queryJs.average(txn, property).wait(); break; case AggregationOp.count: result = await queryJs.count(txn).wait(); break; // ignore: no_default_cases default: throw UnimplementedError(); } if (result == null) { return null; } if (R == DateTime) { return DateTime.fromMillisecondsSinceEpoch(result.toInt()).toLocal() as R; } else if (R == int) { return result.toInt() as R; } else if (R == double) { return result.toDouble() as R; } else { return null; } }); } @override R? aggregateSync(AggregationOp op) => unsupportedOnWeb(); @override Future deleteFirst() { return col.isar.getTxn(true, (IsarTxnJs txn) { return queryJs.deleteFirst(txn).wait(); }); } @override bool deleteFirstSync() => unsupportedOnWeb(); @override Future deleteAll() { return col.isar.getTxn(true, (IsarTxnJs txn) { return queryJs.deleteAll(txn).wait(); }); } @override int deleteAllSync() => unsupportedOnWeb(); @override Stream> watch({bool fireImmediately = false}) { JsFunction? stop; final controller = StreamController>( onCancel: () { stop?.apply([]); }, ); if (fireImmediately) { findAll().then(controller.add); } final Null Function(List results) callback = allowInterop((List results) { controller.add(results.map((e) => deserialize(e as Object)).toList()); }); stop = col.native.watchQuery(queryJs, callback); return controller.stream; } @override Stream watchLazy({bool fireImmediately = false}) { JsFunction? stop; final controller = StreamController( onCancel: () { stop?.apply([]); }, ); final Null Function() callback = allowInterop(() { controller.add(null); }); stop = col.native.watchQueryLazy(queryJs, callback); return controller.stream; } @override Future exportJsonRaw(R Function(Uint8List) callback) async { return col.isar.getTxn(false, (IsarTxnJs txn) async { final result = await queryJs.findAll(txn).wait(); final jsonStr = stringify(result); return callback(const Utf8Encoder().convert(jsonStr)); }); } @override Future>> exportJson() { return col.isar.getTxn(false, (IsarTxnJs txn) async { final result = await queryJs.findAll(txn).wait>(); return result.map((e) => jsMapToDart(e as Object)).toList(); }); } @override R exportJsonRawSync(R Function(Uint8List) callback) => unsupportedOnWeb(); @override List> exportJsonSync({bool primitiveNull = true}) => unsupportedOnWeb(); } ================================================ FILE: packages/isar/lib/src/web/split_words.dart ================================================ // ignore_for_file: public_member_api_docs import 'package:isar/src/web/isar_web.dart'; List isarSplitWords(String input) => unsupportedOnWeb(); ================================================ FILE: packages/isar/pubspec.yaml ================================================ name: isar description: Extremely fast, easy to use, and fully async NoSQL database for Flutter. version: 3.1.8 repository: https://github.com/isar-community/isar/tree/main/packages/isar homepage: https://github.com/isar-community/isar issue_tracker: https://github.com/isar-community/isar/issues documentation: https://isar.dev publish_to: https://pub.isar-community.dev/ funding: - https://github.com/sponsors/leisim/ environment: sdk: ">=2.17.0 <3.0.0" dependencies: ffi: ">=2.0.0 <3.0.0" # isar_test: # path: ../isar_test js: any meta: ^1.7.0 dev_dependencies: ffigen: "^7.0.0" test: ^1.21.1 very_good_analysis: ^3.0.1 ================================================ FILE: packages/isar/test/isar_reader_writer_test.dart ================================================ @TestOn('vm') // ignore_for_file: constant_identifier_names import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar/src/native/isar_core.dart'; import 'package:isar/src/native/isar_reader_impl.dart'; import 'package:isar/src/native/isar_writer_impl.dart'; import 'package:test/test.dart'; void main() { group('Golden Binary', () { late final json = File('../isar_core/tests/binary_golden.json').readAsStringSync(); late final tests = (jsonDecode(json) as List) .map((e) => BinaryTest.fromJson(e as Map)) .toList(); test('IsarReader', () { var t = 0; for (final test in tests) { final reader = IsarReaderImpl(Uint8List.fromList(test.bytes)); var offset = 2; for (var i = 0; i < test.types.length; i++) { final type = test.types[i]; final nullableValue = type.read(reader, offset, true); expect(nullableValue, test.values[i], reason: '${test.types} $t'); final nonNullableValue = type.read(reader, offset, false); _expectIgnoreNull(nonNullableValue, test.values[i], type); offset += type.size; } t++; } }); test('IsarWriter', () { for (final test in tests) { final buffer = Uint8List(10000); final size = test.types.fold(0, (sum, type) => sum + type.size) + 2; final bufferView = buffer.buffer.asUint8List(0, test.bytes.length); final writer = IsarWriterImpl(bufferView, size); var offset = 2; for (var i = 0; i < test.types.length; i++) { final type = test.types[i]; final value = test.values[i]; type.write(writer, offset, value); offset += type.size; } expect(buffer.sublist(0, test.bytes.length), test.bytes); } }); }); } enum Type { Bool(1, false, _readBool, _writeBool), Byte(1, 0, _readByte, _writeByte), Int(4, nullInt, _readInt, _writeInt), Float(4, nullFloat, _readFloat, _writeFloat), Long(8, nullLong, _readLong, _writeLong), Double(8, nullDouble, _readDouble, _writeDouble), String(3, '', _readString, _writeString), BoolList(3, false, _readBoolList, _writeBoolList), ByteList(3, 0, _readByteList, _writeByteList), IntList(3, nullInt, _readIntList, _writeIntList), FloatList(3, nullFloat, _readFloatList, _writeFloatList), LongList(3, nullLong, _readLongList, _writeLongList), DoubleList(3, nullDouble, _readDoubleList, _writeDoubleList), StringList(3, '', _readStringList, _writeStringList); const Type(this.size, this.nullValue, this.read, this.write); final int size; final dynamic nullValue; final dynamic Function(IsarReader reader, int offset, bool nullable) read; final void Function(IsarWriter reader, int offset, dynamic value) write; } class BinaryTest { const BinaryTest(this.types, this.values, this.bytes); factory BinaryTest.fromJson(Map json) { return BinaryTest( (json['types'] as List) .map((type) => Type.values.firstWhere((t) => t.name == type)) .toList(), json['values'] as List, (json['bytes'] as List).cast(), ); } final List types; final List values; final List bytes; } void _expectIgnoreNull( dynamic left, dynamic right, Type type, { bool inList = false, }) { if (right == null && (type.index < Type.BoolList.index || inList)) { if (left is double) { expect(left, isNaN); } else { expect(left, type.nullValue); } } else if (right is List) { left as List; for (var i = 0; i < right.length; i++) { _expectIgnoreNull(left[i], right[i], type, inList: true); } } else { expect(left, right); } } bool? _readBool(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readBoolOrNull(offset); } else { return reader.readBool(offset); } } void _writeBool(IsarWriter writer, int offset, dynamic value) { writer.writeBool(offset, value as bool?); } int? _readByte(IsarReader reader, int offset, bool nullable) { return reader.readByte(offset); } void _writeByte(IsarWriter writer, int offset, dynamic value) { writer.writeByte(offset, value as int); } int? _readInt(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readIntOrNull(offset); } else { return reader.readInt(offset); } } void _writeInt(IsarWriter writer, int offset, dynamic value) { writer.writeInt(offset, value as int?); } double? _readFloat(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readFloatOrNull(offset); } else { return reader.readFloat(offset); } } void _writeFloat(IsarWriter writer, int offset, dynamic value) { writer.writeFloat(offset, value as double?); } int? _readLong(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readLongOrNull(offset); } else { return reader.readLong(offset); } } void _writeLong(IsarWriter writer, int offset, dynamic value) { writer.writeLong(offset, value as int?); } double? _readDouble(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readDoubleOrNull(offset); } else { return reader.readDouble(offset); } } void _writeDouble(IsarWriter writer, int offset, dynamic value) { writer.writeDouble(offset, value as double?); } String? _readString(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readStringOrNull(offset); } else { return reader.readString(offset); } } void _writeString(IsarWriter writer, int offset, dynamic value) { final bytes = value is String ? utf8.encode(value) as Uint8List : null; writer.writeByteList(offset, bytes); } List? _readBoolList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readBoolOrNullList(offset); } else { return reader.readBoolList(offset); } } void _writeBoolList(IsarWriter writer, int offset, dynamic value) { writer.writeBoolList(offset, (value as List?)?.cast()); } List? _readByteList(IsarReader reader, int offset, bool nullable) { return reader.readByteList(offset); } void _writeByteList(IsarWriter writer, int offset, dynamic value) { final bytes = value is List ? Uint8List.fromList(value.cast()) : null; writer.writeByteList(offset, bytes); } List? _readIntList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readIntOrNullList(offset); } else { return reader.readIntList(offset); } } void _writeIntList(IsarWriter writer, int offset, dynamic value) { writer.writeIntList(offset, (value as List?)?.cast()); } List? _readFloatList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readFloatOrNullList(offset); } else { return reader.readFloatList(offset); } } void _writeFloatList(IsarWriter writer, int offset, dynamic value) { writer.writeFloatList(offset, (value as List?)?.cast()); } List? _readLongList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readLongOrNullList(offset); } else { return reader.readLongList(offset); } } void _writeLongList(IsarWriter writer, int offset, dynamic value) { writer.writeLongList(offset, (value as List?)?.cast()); } List? _readDoubleList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readDoubleOrNullList(offset); } else { return reader.readDoubleList(offset); } } void _writeDoubleList(IsarWriter writer, int offset, dynamic value) { writer.writeDoubleList(offset, (value as List?)?.cast()); } List? _readStringList(IsarReader reader, int offset, bool nullable) { if (nullable) { return reader.readStringOrNullList(offset); } else { return reader.readStringList(offset); } } void _writeStringList(IsarWriter writer, int offset, dynamic value) { writer.writeStringList(offset, (value as List?)?.cast()); } ================================================ FILE: packages/isar/tool/get_version.dart ================================================ import 'package:isar/isar.dart'; void main() { // ignore: avoid_print print(Isar.version); } ================================================ FILE: packages/isar/tool/verify_release_version.dart ================================================ import 'package:isar/isar.dart'; void main(List args) { if (Isar.version != args[0]) { throw StateError( 'Invalid Isar version for release: ${Isar.version} != ${args[0]}', ); } } ================================================ FILE: packages/isar_core/Cargo.toml ================================================ [package] name = "isar-core" version = "0.0.0" authors = ["Simon Leier "] edition = "2021" [dependencies] itertools = "0.10.3" enum_dispatch = "0.3.8" ffi = { package = "mdbx-sys", path = "../mdbx_sys" } libc = "0.2" xxhash-rust = { version = "0.8.5", features = ["xxh3"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" once_cell = "1.10.0" crossbeam-channel = "0.5.4" byteorder = "1" paste = "1.0" intmap = "2.0.0" snafu = "0.7.0" [target.'cfg(target_os = "windows")'.dependencies] widestring = "1.0" [dev-dependencies] rand = "0.8.5" cfg-if = "1" float_next_after = "0.1" [dev-dependencies.serde_json] version = "*" features = ["float_roundtrip"] ================================================ FILE: packages/isar_core/README.md ================================================ # isar-core The Rust core of the Isar database. ================================================ FILE: packages/isar_core/src/collection.rs ================================================ use crate::cursor::IsarCursors; use crate::error::{illegal_arg, IsarError, Result}; use crate::index::index_key::IndexKey; use crate::index::index_key_builder::IndexKeyBuilder; use crate::index::IsarIndex; use crate::link::IsarLink; use crate::mdbx::db::Db; use crate::object::id::BytesToId; use crate::object::isar_object::IsarObject; use crate::object::json_encode_decode::JsonEncodeDecode; use crate::object::object_builder::ObjectBuilder; use crate::object::property::Property; use crate::query::query_builder::QueryBuilder; use crate::txn::IsarTxn; use crate::watch::change_set::ChangeSet; use intmap::IntMap; use itertools::Itertools; use serde_json::Value; use std::cell::Cell; use std::ops::Deref; use xxhash_rust::xxh3::xxh3_64; pub struct IsarCollection { pub name: String, pub id: u64, pub properties: Vec, pub embedded_properties: IntMap>, pub(crate) instance_id: u64, pub(crate) db: Db, pub(crate) indexes: Vec, pub(crate) links: Vec, // links from this collection backlinks: Vec, // links to this collection auto_increment: Cell, } unsafe impl Send for IsarCollection {} unsafe impl Sync for IsarCollection {} impl IsarCollection { #[allow(clippy::too_many_arguments)] pub(crate) fn new( db: Db, instance_id: u64, name: &str, properties: Vec, embedded_properties: IntMap>, indexes: Vec, links: Vec, backlinks: Vec, ) -> Self { let id = xxh3_64(name.as_bytes()); IsarCollection { name: name.to_string(), id, properties, embedded_properties, instance_id, db, indexes, links, backlinks, auto_increment: Cell::new(0), } } pub fn new_object_builder(&self, buffer: Option>) -> ObjectBuilder { ObjectBuilder::new(&self.properties, buffer) } pub fn new_query_builder(&self) -> QueryBuilder { QueryBuilder::new(self) } pub(crate) fn init_auto_increment(&self, cursors: &IsarCursors) -> Result<()> { let mut cursor = cursors.get_cursor(self.db)?; if let Some((key, _)) = cursor.move_to_last()? { let id = key.deref().to_id(); self.update_auto_increment(id); } Ok(()) } pub(crate) fn update_auto_increment(&self, id: i64) { if id > self.auto_increment.get() { self.auto_increment.set(id); } } pub fn auto_increment(&self, _: &mut IsarTxn) -> Result { self.auto_increment_internal() } pub(crate) fn auto_increment_internal(&self) -> Result { let last = self.auto_increment.get(); if last < i64::MAX { self.auto_increment.set(last + 1); Ok(last + 1) } else { Err(IsarError::AutoIncrementOverflow {}) } } pub fn get<'txn>(&self, txn: &'txn mut IsarTxn, id: i64) -> Result>> { txn.read(self.instance_id, |cursors| { let mut cursor = cursors.get_cursor(self.db)?; let object = cursor .move_to(&id)? .map(|(_, v)| IsarObject::from_bytes(&v)); Ok(object) }) } pub(crate) fn get_index_by_id(&self, index_id: u64) -> Result<&IsarIndex> { self.indexes .iter() .find(|i| i.id == index_id) .ok_or(IsarError::UnknownIndex {}) } pub fn get_by_index<'txn>( &self, txn: &'txn mut IsarTxn, index_id: u64, key: &IndexKey, ) -> Result)>> { let index = self.get_index_by_id(index_id)?; txn.read(self.instance_id, |cursors| { if let Some(id) = index.get_id(cursors, key)? { let mut cursor = cursors.get_cursor(self.db)?; let (_, bytes) = cursor.move_to(&id)?.ok_or(IsarError::DbCorrupted { message: "Invalid index entry".to_string(), })?; let result = (id, IsarObject::from_bytes(&bytes)); Ok(Some(result)) } else { Ok(None) } }) } pub fn put(&self, txn: &mut IsarTxn, id: Option, object: IsarObject) -> Result { txn.write(self.instance_id, |cursors, change_set| { self.put_internal(cursors, change_set, id, object) }) } pub fn put_by_index( &self, txn: &mut IsarTxn, index_id: u64, object: IsarObject, ) -> Result { let index = self.get_index_by_id(index_id)?; if index.multi_entry { illegal_arg("Cannot put by a multi-entry index")?; } let key_builder = IndexKeyBuilder::new(&index.properties); txn.write(self.instance_id, |cursors, change_set| { let key = key_builder.create_primitive_key(object); let id = index.get_id(cursors, &key)?; let new_id = self.put_internal(cursors, change_set, id, object)?; Ok(new_id) }) } fn put_internal( &self, cursors: &IsarCursors, mut change_set: Option<&mut ChangeSet>, id: Option, object: IsarObject, ) -> Result { if object.len() > IsarObject::MAX_SIZE as usize { illegal_arg("Object is bigger than 16MB")?; } let id = if let Some(id) = id { self.delete_internal(cursors, false, change_set.as_deref_mut(), id)?; self.update_auto_increment(id); id } else { self.auto_increment_internal()? }; for index in &self.indexes { index.create_for_object(cursors, id, object, |id| { self.delete_internal(cursors, true, change_set.as_deref_mut(), id)?; Ok(()) })?; } let mut cursor = cursors.get_cursor(self.db)?; cursor.put(&id, object.as_bytes())?; if let Some(change_set) = change_set { change_set.register_change(self.id, id, object); } Ok(id) } pub fn delete(&self, txn: &mut IsarTxn, id: i64) -> Result { txn.write(self.instance_id, |cursors, change_set| { self.delete_internal(cursors, true, change_set, id) }) } pub fn delete_by_index( &self, txn: &mut IsarTxn, index_id: u64, key: &IndexKey, ) -> Result { let index = self.get_index_by_id(index_id)?; txn.write(self.instance_id, |cursors, change_set| { if let Some(id) = index.get_id(cursors, key)? { self.delete_internal(cursors, true, change_set, id)?; Ok(true) } else { Ok(false) } }) } fn delete_internal( &self, cursors: &IsarCursors, delete_links: bool, change_set: Option<&mut ChangeSet>, id: i64, ) -> Result { let mut cursor = cursors.get_cursor(self.db)?; if let Some((_, object)) = cursor.move_to(&id)? { let object = IsarObject::from_bytes(&object); for index in &self.indexes { index.delete_for_object(cursors, id, object)?; } if delete_links { for link in &self.links { link.delete_all_for_object(cursors, id)?; } for link in &self.backlinks { link.delete_all_for_object(cursors, id)?; } } if let Some(change_set) = change_set { change_set.register_change(self.id, id, object); } cursor.delete_current()?; Ok(true) } else { Ok(false) } } pub(crate) fn get_link_backlink(&self, link_id: u64) -> Result<&IsarLink> { if let Some(link) = self.links.iter().find(|l| l.id == link_id) { Ok(link) } else if let Some(link) = self.backlinks.iter().find(|l| l.id == link_id) { Ok(link) } else { illegal_arg("IsarLink does not exist") } } pub fn link(&self, txn: &mut IsarTxn, link_id: u64, id: i64, target_id: i64) -> Result { let link = self.get_link_backlink(link_id)?; txn.write(self.instance_id, |cursors, _| { link.create(cursors, id, target_id) }) } pub fn unlink(&self, txn: &mut IsarTxn, link_id: u64, id: i64, target_id: i64) -> Result { let link = self.get_link_backlink(link_id)?; txn.write(self.instance_id, |cursors, _| { link.delete(cursors, id, target_id) }) } pub fn unlink_all(&self, txn: &mut IsarTxn, link_id: u64, id: i64) -> Result<()> { let link = self.get_link_backlink(link_id)?; txn.write(self.instance_id, |cursors, _| { link.delete_all_for_object(cursors, id) }) } pub fn clear(&self, txn: &mut IsarTxn) -> Result<()> { txn.write(self.instance_id, |cursors, change_set| { for index in &self.indexes { index.clear(cursors)?; } for link in &self.links { link.clear(cursors)?; } for link in &self.backlinks { link.clear(cursors)?; } cursors.clear_db(self.db)?; self.auto_increment.set(0); if let Some(change_set) = change_set { change_set.register_all(self.id); } Ok(()) }) } pub fn count(&self, txn: &mut IsarTxn) -> Result { txn.read(self.instance_id, |cursors| Ok(cursors.db_stat(self.db)?.0)) } pub fn get_size( &self, txn: &mut IsarTxn, include_indexes: bool, include_links: bool, ) -> Result { txn.read(self.instance_id, |cursors| { let mut size = cursors.db_stat(self.db)?.1; if include_indexes { for index in &self.indexes { size += index.get_size(cursors)?; } } if include_links { for link in &self.links { size += link.get_size(cursors)?; } } Ok(size) }) } pub fn import_json(&self, txn: &mut IsarTxn, id_name: Option<&str>, json: Value) -> Result<()> { txn.write(self.instance_id, |cursors, mut change_set| { let array = json.as_array().ok_or(IsarError::InvalidJson {})?; let mut ob_result_cache = None; for value in array { let id = if let Some(id_name) = id_name { if let Some(id) = value.get(id_name) { let id = id.as_i64().ok_or(IsarError::InvalidJson {})?; Some(id) } else { None } } else { None }; let mut ob = ObjectBuilder::new(&self.properties, ob_result_cache); JsonEncodeDecode::decode( &self.properties, &self.embedded_properties, &mut ob, value, )?; let object = ob.finish(); self.put_internal(cursors, change_set.as_deref_mut(), id, object)?; ob_result_cache = Some(ob.recycle()); } Ok(()) }) } pub(crate) fn fill_indexes(&self, index_ids: &[u64], cursors: &IsarCursors) -> Result<()> { let indexes = index_ids .iter() .map(|id| self.get_index_by_id(*id).unwrap()) .collect_vec(); let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_all(false, true, |cursor, id_bytes, object| { let id = id_bytes.to_id(); // The object might become invalid if another one is deleted by an index. TODO: Find a better solution let bytes = object.to_vec(); let object = IsarObject::from_bytes(&bytes); for index in &indexes { index.create_for_object(cursors, id, object, |id| { let deleted = self.delete_internal(cursors, true, None, id)?; if deleted { cursor.move_to_next()?; } Ok(()) })?; } Ok(true) })?; Ok(()) } pub fn verify(&self, txn: &mut IsarTxn, objects: &IntMap) -> Result<()> { txn.read(self.instance_id, |cursors| { let mut counter = 0; let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_all(false, true, |_, id_bytes, bytes| { let id = id_bytes.to_id(); let db_object = IsarObject::from_bytes(bytes); let db_json = JsonEncodeDecode::encode( &self.properties, &self.embedded_properties, db_object, false, ); if let Some(object) = objects.get(id as u64) { let json = JsonEncodeDecode::encode( &self.properties, &self.embedded_properties, *object, false, ); if json == db_json { counter += 1; return Ok(true); } } Err(IsarError::DbCorrupted { message: "Unknown object in database.".to_string(), }) })?; if counter != objects.len() { return Err(IsarError::DbCorrupted { message: "Object missing in database.".to_string(), }); } for index in &self.indexes { index.verify(cursors, objects)?; } Ok(()) }) } pub fn verify_link(&self, txn: &mut IsarTxn, link_id: u64, links: &[(i64, i64)]) -> Result<()> { let link = self.get_link_backlink(link_id)?; txn.read(self.instance_id, |cursors| link.verify(cursors, links)) } } ================================================ FILE: packages/isar_core/src/cursor.rs ================================================ use crate::error::Result; use crate::mdbx::cursor::{Cursor, UnboundCursor}; use crate::mdbx::db::Db; use crate::mdbx::txn::Txn; use intmap::IntMap; use std::cell::RefCell; use std::ops::{Deref, DerefMut}; pub(crate) struct IsarCursors<'txn, 'env> { txn: &'txn Txn<'env>, unbound_cursors: RefCell>, cursors: RefCell>>, } impl<'txn, 'env> IsarCursors<'txn, 'env> { pub fn new( txn: &'txn Txn<'env>, unbound_cursors: Vec, ) -> IsarCursors<'txn, 'env> { IsarCursors { txn, unbound_cursors: RefCell::new(unbound_cursors), cursors: RefCell::new(IntMap::new()), } } pub fn get_cursor<'a>(&'a self, db: Db) -> Result> { let cursor = if let Some(cursor) = self.cursors.borrow_mut().remove(db.runtime_id()) { cursor } else { let unbound = self .unbound_cursors .borrow_mut() .pop() .unwrap_or_else(UnboundCursor::new); unbound.bind(self.txn, db)? }; Ok(IsarCursor { cursors: self, cursor: Some(cursor), db_id: db.runtime_id(), }) } pub fn db_stat(&self, db: Db) -> Result<(u64, u64)> { db.stat(&self.txn) } pub fn clear_db(&self, db: Db) -> Result<()> { db.clear(&self.txn) } pub fn close(self) -> Vec { let mut unbound_cursors = self.unbound_cursors.take(); for (_, cursor) in self.cursors.borrow_mut().drain() { unbound_cursors.push(cursor.unbind()) } unbound_cursors } } pub(crate) struct IsarCursor<'a, 'txn, 'env> { cursors: &'a IsarCursors<'txn, 'env>, cursor: Option>, db_id: u64, } impl<'a, 'txn, 'env> Deref for IsarCursor<'a, 'txn, 'env> { type Target = Cursor<'txn>; fn deref(&self) -> &Self::Target { self.cursor.as_ref().unwrap() } } impl<'a, 'txn, 'env> DerefMut for IsarCursor<'a, 'txn, 'env> { fn deref_mut(&mut self) -> &mut Self::Target { self.cursor.as_mut().unwrap() } } impl<'a, 'txn, 'env> Drop for IsarCursor<'a, 'txn, 'env> { fn drop(&mut self) { let cursor = self.cursor.take().unwrap(); let cursors = &self.cursors.cursors; if !cursors.borrow().contains_key(self.db_id) { cursors.borrow_mut().insert(self.db_id, cursor); } else if self.cursors.unbound_cursors.borrow().len() < 3 { self.cursors .unbound_cursors .borrow_mut() .push(cursor.unbind()); } } } ================================================ FILE: packages/isar_core/src/error.rs ================================================ use snafu::Snafu; pub type Result = std::result::Result; #[derive(Debug, Snafu, Eq, PartialEq)] pub enum IsarError { #[snafu(display("Isar version of the file is too new or too old to be used."))] VersionError {}, #[snafu(display( "No such file or directory. Please make sure that the provided path is valid." ))] PathError {}, #[snafu(display("Cannot open Environment: {}", error))] EnvError { error: Box }, #[snafu(display("The database is full."))] DbFull {}, #[snafu(display("Unique index violated."))] UniqueViolated {}, #[snafu(display("Write transaction required."))] WriteTxnRequired {}, #[snafu(display("Auto increment id cannot be generated because the limit is reached."))] AutoIncrementOverflow {}, #[snafu(display("The provided ObjectId does not match the collection."))] InvalidObjectId {}, #[snafu(display("The provided object is invalid."))] InvalidObject {}, #[snafu(display("Transaction closed."))] TransactionClosed {}, #[snafu(display("IllegalArg: {}.", message))] IllegalArg { message: String }, #[snafu(display("Index could not be found."))] UnknownIndex {}, #[snafu(display("Invalid JSON."))] InvalidJson {}, #[snafu(display("DbCorrupted: {}", message))] DbCorrupted { message: String }, #[snafu(display("SchemaError: {}", message))] SchemaError { message: String }, #[snafu(display("SchemaMismatch: The schema of the existing instance does not match."))] SchemaMismatch {}, #[snafu(display("InstanceMismatch: The transaction is from a different instance."))] InstanceMismatch {}, #[snafu(display("MdbxError ({}): {}", code, message))] MdbxError { code: i32, message: String }, } pub fn illegal_arg(msg: &str) -> Result { Err(IsarError::IllegalArg { message: msg.to_string(), }) } pub fn schema_error(msg: &str) -> Result { Err(IsarError::SchemaError { message: msg.to_string(), }) } ================================================ FILE: packages/isar_core/src/index/index_key.rs ================================================ use crate::index::IsarIndex; use crate::mdbx::Key; use std::borrow::Cow; use std::cmp; use std::cmp::Ordering; use xxhash_rust::xxh3::xxh3_64; #[derive(Clone, Eq, PartialEq)] pub struct IndexKey { bytes: Vec, } impl IndexKey { pub fn new() -> Self { IndexKey { bytes: vec![] } } pub fn from_bytes(bytes: Vec) -> Self { IndexKey { bytes } } pub fn add_byte(&mut self, value: u8) { self.bytes.push(value); } pub fn add_int(&mut self, value: i32) { let unsigned = value as u32; let bytes: [u8; 4] = (unsigned ^ 1 << 31).to_be_bytes(); self.bytes.extend_from_slice(&bytes); } pub fn add_long(&mut self, value: i64) { let unsigned = value as u64; let bytes = (unsigned ^ 1 << 63).to_be_bytes().to_vec(); self.bytes.extend_from_slice(&bytes); } pub fn add_float(&mut self, value: f32) { let bytes: [u8; 4] = if !value.is_nan() { let bits = if value.is_sign_positive() { value.to_bits() + 2u32.pow(31) } else { !(-value).to_bits() - 2u32.pow(31) }; bits.to_be_bytes() } else { [0; 4] }; self.bytes.extend_from_slice(&bytes); } pub fn add_double(&mut self, value: f64) { let bytes: [u8; 8] = if !value.is_nan() { let bits = if value.is_sign_positive() { value.to_bits() + 2u64.pow(63) } else { !(-value).to_bits() - 2u64.pow(63) }; bits.to_be_bytes() } else { [0; 8] }; self.bytes.extend_from_slice(&bytes); } pub fn add_string(&mut self, value: Option<&str>, case_sensitive: bool) { if let Some(value) = value { let value = if case_sensitive { value.to_string() } else { value.to_lowercase() }; let bytes = value.as_bytes(); if bytes.len() >= IsarIndex::MAX_STRING_INDEX_SIZE { let index_bytes = &bytes[0..IsarIndex::MAX_STRING_INDEX_SIZE]; self.bytes.extend_from_slice(index_bytes); let hash = xxh3_64(bytes); self.bytes.extend_from_slice(&u64::to_le_bytes(hash)); } else if bytes.is_empty() { self.bytes.push(1); } else { self.bytes.extend_from_slice(bytes); } } else { self.bytes.push(0); } } pub fn add_hash(&mut self, value: u64) { let bytes: [u8; 8] = value.to_be_bytes(); self.bytes.extend_from_slice(&bytes); } #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { self.bytes.len() } pub fn truncate(&mut self, len: usize) { self.bytes.truncate(len); } pub fn increase(&mut self) -> bool { let mut increased = false; for i in (0..self.bytes.len()).rev() { if let Some(added) = self.bytes[i].checked_add(1) { self.bytes[i] = added; increased = true; for i2 in (i + 1)..self.bytes.len() { self.bytes[i2] = 0; } break; } } increased } pub fn decrease(&mut self) -> bool { let mut decreased = false; for i in (0..self.bytes.len()).rev() { if let Some(subtracted) = self.bytes[i].checked_sub(1) { self.bytes[i] = subtracted; decreased = true; for i2 in (i + 1)..self.bytes.len() { self.bytes[i2] = 255; } break; } } decreased } } impl Key for IndexKey { fn as_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(&self.bytes) } fn cmp_bytes(&self, other: &[u8]) -> Ordering { let len = cmp::min(self.bytes.len(), other.len()); let cmp = (&self.bytes[0..len]).cmp(&other[0..len]); if cmp == Ordering::Equal { self.bytes.len().cmp(&other.len()) } else { cmp } } } impl PartialOrd for IndexKey { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for IndexKey { fn cmp(&self, other: &Self) -> Ordering { self.cmp_bytes(&other.bytes) } } #[cfg(test)] mod tests { use crate::object::isar_object::IsarObject; use super::*; use float_next_after::NextAfter; #[test] fn test_add_byte() { let pairs = vec![ (IsarObject::NULL_BYTE, vec![123, 0]), (123, vec![123, 123]), (255, vec![123, 255]), ]; for (val, bytes) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_byte(val); assert_eq!(&index_key.bytes, &bytes); } } #[test] fn test_add_int() { let pairs = vec![ (i32::MIN, vec![123, 0, 0, 0, 0]), (i32::MIN + 1, vec![123, 0, 0, 0, 1]), (-1, vec![123, 127, 255, 255, 255]), (0, vec![123, 128, 0, 0, 0]), (1, vec![123, 128, 0, 0, 1]), (i32::MAX - 1, vec![123, 255, 255, 255, 254]), (i32::MAX, vec![123, 255, 255, 255, 255]), ]; for (val, bytes) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_int(val); assert_eq!(&index_key.bytes, &bytes); } } #[test] fn test_add_long() { let pairs = vec![ (i64::MIN, vec![123, 0, 0, 0, 0, 0, 0, 0, 0]), (i64::MIN + 1, vec![123, 0, 0, 0, 0, 0, 0, 0, 1]), (-1, vec![123, 127, 255, 255, 255, 255, 255, 255, 255]), (0, vec![123, 128, 0, 0, 0, 0, 0, 0, 0]), (1, vec![123, 128, 0, 0, 0, 0, 0, 0, 1]), ( i64::MAX - 1, vec![123, 255, 255, 255, 255, 255, 255, 255, 254], ), (i64::MAX, vec![123, 255, 255, 255, 255, 255, 255, 255, 255]), ]; for (val, bytes) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_long(val); assert_eq!(&index_key.bytes, &bytes); } } #[test] fn test_add_float() { let pairs = vec![ (f32::NAN, vec![123, 0, 0, 0, 0]), (f32::NEG_INFINITY, vec![123, 0, 127, 255, 255]), (f32::MIN, vec![123, 0, 128, 0, 0]), (f32::MIN.next_after(f32::MAX), vec![123, 0, 128, 0, 1]), ((-0.0).next_after(f32::MIN), vec![123, 127, 255, 255, 254]), (-0.0, vec![123, 127, 255, 255, 255]), (0.0, vec![123, 128, 0, 0, 0]), (0.0.next_after(f32::MAX), vec![123, 128, 0, 0, 1]), (f32::MAX.next_after(f32::MIN), vec![123, 255, 127, 255, 254]), (f32::MAX, vec![123, 255, 127, 255, 255]), (f32::INFINITY, vec![123, 255, 128, 0, 0]), ]; for (val, bytes) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_float(val); assert_eq!(&index_key.bytes, &bytes); } } #[test] fn test_add_double() { let pairs = vec![ (f64::NAN, vec![123, 0, 0, 0, 0, 0, 0, 0, 0]), ( f64::NEG_INFINITY, vec![123, 0, 15, 255, 255, 255, 255, 255, 255], ), (f64::MIN, vec![123, 0, 16, 0, 0, 0, 0, 0, 0]), ( f64::MIN.next_after(f64::MAX), vec![123, 0, 16, 0, 0, 0, 0, 0, 1], ), ( (-0.0).next_after(f64::MIN), vec![123, 127, 255, 255, 255, 255, 255, 255, 254], ), (-0.0, vec![123, 127, 255, 255, 255, 255, 255, 255, 255]), (0.0, vec![123, 128, 0, 0, 0, 0, 0, 0, 0]), ( 0.0.next_after(f64::MAX), vec![123, 128, 0, 0, 0, 0, 0, 0, 1], ), ( f64::MAX.next_after(f64::MIN), vec![123, 255, 239, 255, 255, 255, 255, 255, 254], ), (f64::MAX, vec![123, 255, 239, 255, 255, 255, 255, 255, 255]), (f64::INFINITY, vec![123, 255, 240, 0, 0, 0, 0, 0, 0]), ]; for (val, bytes) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_double(val); assert_eq!(&index_key.bytes, &bytes); } } #[test] fn test_add_string() { let long_str = (0..850).map(|_| "aB").collect::(); let long_str_lc = long_str.to_lowercase(); let mut long_str_bytes = vec![123]; long_str_bytes.extend_from_slice(long_str.as_bytes()); let mut long_str_lc_bytes = vec![123]; long_str_lc_bytes.extend_from_slice(long_str_lc.as_bytes()); let mut hello_bytes = vec![123]; hello_bytes.extend_from_slice(b"hELLO"); let mut hello_bytes_lc = vec![123]; hello_bytes_lc.extend_from_slice(b"hello"); let pairs: Vec<(Option<&str>, Vec, Vec)> = vec![ (None, vec![123, 0], vec![123, 0]), (Some(""), vec![123, 1], vec![123, 1]), ( Some("hello"), hello_bytes_lc.clone(), hello_bytes_lc.clone(), ), (Some("hELLO"), hello_bytes.clone(), hello_bytes_lc.clone()), //(Some(&long_str), long_str_bytes, long_str_lc_bytes), ]; for (str, bytes, bytes_lc) in pairs { let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_string(str, true); assert_eq!(index_key.bytes, bytes); let mut index_key = IndexKey::new(); index_key.add_byte(123); index_key.add_string(str, false); assert_eq!(index_key.bytes, bytes_lc); } } } ================================================ FILE: packages/isar_core/src/index/index_key_builder.rs ================================================ use crate::error::Result; use crate::index::index_key::IndexKey; use crate::index::IndexProperty; use crate::object::data_type::DataType; use crate::object::isar_object::IsarObject; use crate::schema::index_schema::IndexType; pub(crate) struct IndexKeyBuilder<'a> { properties: &'a [IndexProperty], } impl<'a> IndexKeyBuilder<'a> { pub fn new(properties: &'a [IndexProperty]) -> Self { Self { properties } } pub fn create_keys( &self, object: IsarObject, mut callback: impl FnMut(&IndexKey) -> Result, ) -> Result { let first = self.properties.first().unwrap(); if !first.is_multi_entry() { let key = self.create_primitive_key(object); callback(&key)?; Ok(true) } else { assert_eq!(self.properties.len(), 1); Self::create_list_keys(first, object, &mut callback) } } pub fn create_primitive_key(&self, object: IsarObject) -> IndexKey { let mut key = IndexKey::new(); for index_property in self.properties { let property = &index_property.property; if index_property.index_type == IndexType::Hash { let hash = object.hash_property( property.offset, property.data_type, index_property.case_sensitive, 0, ); key.add_hash(hash); } else { match property.data_type { DataType::Bool | DataType::Byte => { assert_eq!(IsarObject::NULL_BOOL, IsarObject::NULL_BYTE); key.add_byte(object.read_byte(property.offset)) } DataType::Int => key.add_int(object.read_int(property.offset)), DataType::Float => key.add_float(object.read_float(property.offset)), DataType::Long => key.add_long(object.read_long(property.offset)), DataType::Double => key.add_double(object.read_double(property.offset)), DataType::String => key.add_string( object.read_string(property.offset), index_property.case_sensitive, ), _ => unreachable!(), } } } key } fn create_list_keys( index_property: &IndexProperty, object: IsarObject, mut callback: impl FnMut(&IndexKey) -> Result, ) -> Result { let mut key = IndexKey::new(); let property = &index_property.property; if object.is_null(property.offset, property.data_type) { return Ok(true); } match property.data_type { DataType::BoolList | DataType::ByteList => { for value in object.read_byte_list(property.offset).unwrap() { key.truncate(0); key.add_byte(*value); if !callback(&key)? { return Ok(false); } } } DataType::IntList => { for value in object.read_int_list(property.offset).unwrap() { key.truncate(0); key.add_int(value); if !callback(&key)? { return Ok(false); } } } DataType::LongList => { for value in object.read_long_list(property.offset).unwrap() { key.truncate(0); key.add_long(value); if !callback(&key)? { return Ok(false); } } } DataType::FloatList => { for value in object.read_float_list(property.offset).unwrap() { key.truncate(0); key.add_float(value); if !callback(&key)? { return Ok(false); } } } DataType::DoubleList => { for value in object.read_double_list(property.offset).unwrap() { key.truncate(0); key.add_double(value); if !callback(&key)? { return Ok(false); } } } DataType::StringList => { for value in object.read_string_list(property.offset).unwrap() { key.truncate(0); if index_property.index_type == IndexType::HashElements { let hash = IsarObject::hash_string(value, index_property.case_sensitive, 0); key.add_hash(hash); } else { key.add_string(value, index_property.case_sensitive); } if !callback(&key)? { return Ok(false); } } } _ => unreachable!(), } Ok(true) } } #[cfg(test)] mod tests {} ================================================ FILE: packages/isar_core/src/index/mod.rs ================================================ use crate::cursor::IsarCursors; use crate::error::{IsarError, Result}; use crate::index::index_key::IndexKey; use crate::index::index_key_builder::IndexKeyBuilder; use crate::mdbx::db::Db; use crate::object::id::{BytesToId, IdToBytes}; use crate::object::isar_object::IsarObject; use crate::object::property::Property; use crate::schema::index_schema::IndexType; use intmap::IntMap; use xxhash_rust::xxh3::xxh3_64; pub mod index_key; pub(crate) mod index_key_builder; #[derive(Clone, Eq, PartialEq)] pub struct IndexProperty { pub property: Property, pub index_type: IndexType, pub case_sensitive: bool, } impl IndexProperty { pub(crate) fn new(property: Property, index_type: IndexType, case_sensitive: bool) -> Self { IndexProperty { property, index_type, case_sensitive, } } pub fn get_string_with_case(&self, object: IsarObject) -> Option { object.read_string(self.property.offset).map(|str| { if self.case_sensitive { str.to_string() } else { str.to_lowercase() } }) } fn is_multi_entry(&self) -> bool { self.property.data_type.get_element_type().is_some() && self.index_type != IndexType::Hash } } #[derive(Clone, Eq, PartialEq)] pub(crate) struct IsarIndex { pub name: String, pub id: u64, pub properties: Vec, pub unique: bool, pub replace: bool, pub multi_entry: bool, db: Db, } impl IsarIndex { pub const MAX_STRING_INDEX_SIZE: usize = 1024; pub fn new( name: &str, db: Db, properties: Vec, unique: bool, replace: bool, ) -> Self { let id = xxh3_64(name.as_bytes()); let multi_entry = properties.first().unwrap().is_multi_entry(); IsarIndex { name: name.to_string(), id, properties, unique, replace, multi_entry, db, } } pub fn create_for_object( &self, cursors: &IsarCursors, id: i64, object: IsarObject, mut delete: F, ) -> Result<()> where F: FnMut(i64) -> Result<()>, { let mut cursor = cursors.get_cursor(self.db)?; let key_builder = IndexKeyBuilder::new(&self.properties); key_builder.create_keys(object, |key| { if self.unique { let existing = cursor.move_to(key)?; if let Some((_, existing_id_bytes)) = existing { let existing_id = existing_id_bytes.to_id(); if self.replace && existing_id != id { delete(existing_id)?; } else { return Err(IsarError::UniqueViolated {}); } } } cursor.put(key, &id.to_id_bytes())?; Ok(true) })?; Ok(()) } pub fn delete_for_object( &self, cursors: &IsarCursors, id: i64, object: IsarObject, ) -> Result<()> { let mut cursor = cursors.get_cursor(self.db)?; let key_builder = IndexKeyBuilder::new(&self.properties); key_builder.create_keys(object, |key| { let entry = if self.unique { cursor.move_to(key)? } else { cursor.move_to_key_val(key, &id.to_id_bytes())? }; if entry.is_some() { cursor.delete_current()?; } Ok(true) })?; Ok(()) } pub fn iter_between<'txn, 'env>( &self, cursors: &IsarCursors<'txn, 'env>, lower_key: &IndexKey, upper_key: &IndexKey, skip_duplicates: bool, ascending: bool, mut callback: impl FnMut(i64) -> Result, ) -> Result { let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_between( lower_key, upper_key, !self.unique, skip_duplicates, ascending, |_, _, id_bytes| callback(id_bytes.to_id()), ) } pub fn get_id<'txn, 'env>( &self, cursors: &IsarCursors<'txn, 'env>, key: &IndexKey, ) -> Result> { let mut result = None; self.iter_between(cursors, key, key, false, true, |id| { result = Some(id); Ok(false) })?; Ok(result) } pub fn get_size(&self, cursors: &IsarCursors) -> Result { Ok(cursors.db_stat(self.db)?.1) } pub fn clear(&self, cursors: &IsarCursors) -> Result<()> { cursors.clear_db(self.db) } pub fn verify(&self, cursors: &IsarCursors, objects: &IntMap) -> Result<()> { let mut count = 0; let mut cursor = cursors.get_cursor(self.db)?; for id in objects.keys() { let id = *id; let object = *objects.get(id).unwrap(); let key_builder = IndexKeyBuilder::new(&self.properties); key_builder.create_keys(object, |key| { count += 1; let result = cursor.move_to_key_val(key, &(id as i64).to_id_bytes())?; if result.is_some() { Ok(true) } else { Err(IsarError::DbCorrupted { message: "Missing index entry.".to_string(), }) } })?; } if cursors.db_stat(self.db)?.0 != count { Err(IsarError::DbCorrupted { message: "Obsolete index entry.".to_string(), }) } else { Ok(()) } } } ================================================ FILE: packages/isar_core/src/instance.rs ================================================ use crate::collection::IsarCollection; use crate::error::*; use crate::mdbx::env::Env; use crate::query::Query; use crate::schema::schema_manager::SchemaManager; use crate::schema::Schema; use crate::txn::IsarTxn; use crate::watch::change_set::ChangeSet; use crate::watch::isar_watchers::{IsarWatchers, WatcherModifier}; use crate::watch::watcher::WatcherCallback; use crate::watch::WatchHandle; use crossbeam_channel::{unbounded, Sender}; use intmap::IntMap; use once_cell::sync::Lazy; use std::fs::remove_file; use std::fs::{self, metadata}; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use xxhash_rust::xxh3::xxh3_64; static INSTANCES: Lazy>>> = Lazy::new(|| RwLock::new(IntMap::new())); static WATCHER_ID: AtomicU64 = AtomicU64::new(0); pub struct CompactCondition { pub min_file_size: u64, pub min_bytes: u64, pub min_ratio: f64, } pub struct IsarInstance { pub name: String, pub dir: String, pub collections: Vec, pub(crate) instance_id: u64, env: Env, watchers: Mutex, watcher_modifier_sender: Sender, } impl IsarInstance { pub fn open( name: &str, dir: Option<&str>, schema: Schema, max_size_mib: usize, relaxed_durability: bool, compact_condition: Option, ) -> Result> { let mut lock = INSTANCES.write().unwrap(); let instance_id = xxh3_64(name.as_bytes()); if let Some(instance) = lock.get(instance_id) { Ok(instance.clone()) } else { if let Some(dir) = dir { let new_instance = Self::open_internal( name, dir, instance_id, schema, max_size_mib, relaxed_durability, compact_condition, )?; let new_instance = Arc::new(new_instance); lock.insert(instance_id, new_instance.clone()); Ok(new_instance) } else { Err(IsarError::IllegalArg { message: "Please provide a valid directory.".to_string(), }) } } } fn get_isar_path(name: &str, dir: &str) -> String { let mut file_name = name.to_string(); file_name.push_str(".isar"); let mut path_buf = PathBuf::from(dir); path_buf.push(file_name); path_buf.as_path().to_str().unwrap().to_string() } fn move_old_database(name: &str, dir: &str, new_path: &str) { let mut old_path_buf = PathBuf::from(dir); old_path_buf.push(name); old_path_buf.push("mdbx.dat"); let old_path = old_path_buf.as_path(); let result = fs::rename(old_path, new_path); // Also try to migrate the previous default isar name if name == "default" && result.is_err() { Self::move_old_database("isar", dir, new_path) } } fn open_internal( name: &str, dir: &str, instance_id: u64, mut schema: Schema, max_size_mib: usize, relaxed_durability: bool, compact_condition: Option, ) -> Result { let isar_file = Self::get_isar_path(name, dir); Self::move_old_database(name, dir, &isar_file); let db_count = schema.count_dbs() as u64 + 3; let env = Env::create( &isar_file, db_count, max_size_mib.max(1), relaxed_durability, ) .map_err(|e| IsarError::EnvError { error: Box::new(e) })?; let txn = env.txn(true)?; let mut manager = SchemaManager::create(instance_id, &txn)?; txn.commit()?; let txn = env.txn(true)?; let added_indexes = manager.migrate_schema(&txn, &mut schema)?; txn.commit()?; let mut collections = vec![]; for col_schema in &schema.collections { let txn = env.txn(true)?; let col_id = xxh3_64(col_schema.name.as_bytes()); let added_indexes = added_indexes .get(col_id) .map(|v| v.as_slice()) .unwrap_or_default(); let col = manager.open_collection(&txn, col_schema, &schema, added_indexes)?; collections.push(col); txn.commit()?; } if !manager.schemas.is_empty() { let txn = env.txn(true)?; manager.delete_unopened_collections(&txn)?; txn.commit()?; } let (tx, rx) = unbounded(); let instance = IsarInstance { env, name: name.to_string(), dir: dir.to_string(), collections, instance_id, watchers: Mutex::new(IsarWatchers::new(rx)), watcher_modifier_sender: tx, }; if let Some(compact_condition) = compact_condition { let instance = instance.compact(compact_condition)?; if let Some(instance) = instance { Ok(instance) } else { Self::open_internal( name, dir, instance_id, schema, max_size_mib, relaxed_durability, None, ) } } else { Ok(instance) } } fn compact(self, compact_condition: CompactCondition) -> Result> { let mut txn = self.begin_txn(false, true)?; let instance_size = self.get_size(&mut txn, true, true)?; txn.abort(); let isar_file = Self::get_isar_path(&self.name, &self.dir); let file_size = metadata(&isar_file) .map_err(|_| IsarError::PathError {})? .len(); let compact_bytes = file_size.saturating_sub(instance_size); let compact_ratio = if instance_size == 0 { f64::INFINITY } else { (file_size as f64) / (instance_size as f64) }; let should_compact = file_size >= compact_condition.min_file_size && compact_bytes >= compact_condition.min_bytes && compact_ratio >= compact_condition.min_ratio; if should_compact { let compact_file = format!("{}.compact", &isar_file); self.copy_to_file(&compact_file)?; drop(self); let _ = fs::rename(&compact_file, &isar_file); Ok(None) } else { Ok(Some(self)) } } pub fn get_instance(name: &str) -> Option> { let instance_id = xxh3_64(name.as_bytes()); INSTANCES.read().unwrap().get(instance_id).cloned() } pub fn begin_txn(&self, write: bool, silent: bool) -> Result { let change_set = if write && !silent { let mut watchers_lock = self.watchers.lock().unwrap(); watchers_lock.sync(); let change_set = ChangeSet::new(watchers_lock); Some(change_set) } else { None }; let txn = self.env.txn(write)?; IsarTxn::new(self.instance_id, txn, write, change_set) } pub fn get_size( &self, txn: &mut IsarTxn, include_indexes: bool, include_links: bool, ) -> Result { let mut size = 0; for col in &self.collections { size += col.get_size(txn, include_indexes, include_links)?; } Ok(size) } pub fn copy_to_file(&self, path: &str) -> Result<()> { self.env.copy(path) } fn new_watcher(&self, start: WatcherModifier, stop: WatcherModifier) -> WatchHandle { self.watcher_modifier_sender.try_send(start).unwrap(); let sender = self.watcher_modifier_sender.clone(); WatchHandle::new(Box::new(move || { let _ = sender.try_send(stop); })) } pub fn watch_collection( &self, collection: &IsarCollection, callback: WatcherCallback, ) -> WatchHandle { let watcher_id = WATCHER_ID.fetch_add(1, Ordering::SeqCst); let col_id = collection.id; self.new_watcher( Box::new(move |iw| { iw.get_col_watchers(col_id) .add_watcher(watcher_id, callback); }), Box::new(move |iw| { iw.get_col_watchers(col_id).remove_watcher(watcher_id); }), ) } pub fn watch_object( &self, collection: &IsarCollection, oid: i64, callback: WatcherCallback, ) -> WatchHandle { let watcher_id = WATCHER_ID.fetch_add(1, Ordering::SeqCst); let col_id = collection.id; self.new_watcher( Box::new(move |iw| { iw.get_col_watchers(col_id) .add_object_watcher(watcher_id, oid, callback); }), Box::new(move |iw| { iw.get_col_watchers(col_id) .remove_object_watcher(oid, watcher_id); }), ) } pub fn watch_query( &self, collection: &IsarCollection, query: Query, callback: WatcherCallback, ) -> WatchHandle { let watcher_id = WATCHER_ID.fetch_add(1, Ordering::SeqCst); let col_id = collection.id; self.new_watcher( Box::new(move |iw| { iw.get_col_watchers(col_id) .add_query_watcher(watcher_id, query, callback); }), Box::new(move |iw| { iw.get_col_watchers(col_id).remove_query_watcher(watcher_id); }), ) } fn close_internal(self: Arc, delete_from_disk: bool) -> bool { // Check whether all other references are gone if Arc::strong_count(&self) == 2 { let mut lock = INSTANCES.write().unwrap(); // Check again to make sure there are no new references if Arc::strong_count(&self) == 2 { lock.remove(self.instance_id); if delete_from_disk { let mut path = Self::get_isar_path(&self.name, &self.dir); drop(self); let _ = remove_file(&path); path.push_str(".lock"); let _ = remove_file(&path); } return true; } } false } pub fn close(self: Arc) -> bool { self.close_internal(false) } pub fn close_and_delete(self: Arc) -> bool { self.close_internal(true) } pub fn verify(&self, txn: &mut IsarTxn) -> Result<()> { let mut db_names = vec![]; db_names.push("_info".to_string()); for col in &self.collections { db_names.push(col.name.clone()); for index in &col.indexes { db_names.push(format!("_i_{}_{}", col.name, index.name)); } for link in &col.links { db_names.push(format!("_l_{}_{}", col.name, link.name)); db_names.push(format!("_b_{}_{}", col.name, link.name)); } } let mut actual_db_names = txn.db_names()?; db_names.sort(); actual_db_names.sort(); if db_names != actual_db_names { Err(IsarError::DbCorrupted { message: "Incorrect databases".to_string(), }) } else { Ok(()) } } } ================================================ FILE: packages/isar_core/src/legacy/isar_object_v1.rs ================================================ use crate::object::{data_type::DataType, isar_object::IsarObject}; use byteorder::{ByteOrder, LittleEndian}; #[derive(Copy, Clone, Eq, PartialEq)] pub struct LegacyProperty { pub data_type: DataType, pub offset: usize, } impl LegacyProperty { pub const fn new(data_type: DataType, offset: usize) -> Self { LegacyProperty { data_type, offset } } } #[derive(Copy, Clone, Eq, PartialEq)] pub struct LegacyIsarObject<'a> { bytes: &'a [u8], static_size: usize, } impl<'a> LegacyIsarObject<'a> { pub fn from_bytes(bytes: &'a [u8]) -> Self { let static_size = LittleEndian::read_u16(bytes) as usize; LegacyIsarObject { bytes, static_size } } #[inline] pub(crate) fn contains_offset(&self, offset: usize) -> bool { self.static_size > offset } #[inline] pub fn contains_property(&self, property: LegacyProperty) -> bool { self.contains_offset(property.offset) } pub fn is_null(&self, property: LegacyProperty) -> bool { match property.data_type { DataType::Byte => self.read_byte(property) == IsarObject::NULL_BYTE, DataType::Int => self.read_int(property) == IsarObject::NULL_INT, DataType::Long => self.read_long(property) == IsarObject::NULL_LONG, DataType::Float => self.read_float(property).is_nan(), DataType::Double => self.read_double(property).is_nan(), _ => self.get_offset_length(property.offset, false).is_none(), } } pub fn read_byte(&self, property: LegacyProperty) -> u8 { if self.contains_property(property) { self.bytes[property.offset] } else { IsarObject::NULL_BYTE } } pub fn read_bool(&self, property: LegacyProperty) -> bool { self.read_byte(property) == IsarObject::TRUE_BOOL } pub fn read_int(&self, property: LegacyProperty) -> i32 { if self.contains_property(property) { LittleEndian::read_i32(&self.bytes[property.offset..]) } else { IsarObject::NULL_INT } } pub fn read_float(&self, property: LegacyProperty) -> f32 { if self.contains_property(property) { LittleEndian::read_f32(&self.bytes[property.offset..]) } else { IsarObject::NULL_FLOAT } } pub fn read_long(&self, property: LegacyProperty) -> i64 { if self.contains_property(property) { LittleEndian::read_i64(&self.bytes[property.offset..]) } else { IsarObject::NULL_LONG } } pub fn read_double(&self, property: LegacyProperty) -> f64 { if self.contains_property(property) { LittleEndian::read_f64(&self.bytes[property.offset..]) } else { IsarObject::NULL_DOUBLE } } fn get_offset_length(&self, offset: usize, dynamic_offset: bool) -> Option<(usize, usize)> { if dynamic_offset || self.contains_offset(offset) { let list_offset = LittleEndian::read_u32(&self.bytes[offset..]) as usize; let length = LittleEndian::read_u32(&self.bytes[offset + 4..]); if list_offset != 0 { return Some((list_offset as usize, length as usize)); } } None } fn read_string_at(&self, offset: usize, dynamic_offset: bool) -> Option<&'a str> { let (offset, length) = self.get_offset_length(offset, dynamic_offset)?; let str = unsafe { std::str::from_utf8_unchecked(&self.bytes[offset..offset + length]) }; Some(str) } pub fn read_string(&'a self, property: LegacyProperty) -> Option<&'a str> { self.read_string_at(property.offset, false) } pub fn read_byte_list(&self, property: LegacyProperty) -> Option<&'a [u8]> { let (offset, length) = self.get_offset_length(property.offset, false)?; Some(&self.bytes[offset..offset + length]) } pub fn read_int_list(&self, property: LegacyProperty) -> Option> { let (offset, length) = self.get_offset_length(property.offset, false)?; let list = (offset..offset + length * 4) .step_by(4) .into_iter() .map(|offset| LittleEndian::read_i32(&self.bytes[offset..])) .collect(); Some(list) } pub fn read_float_list(&self, property: LegacyProperty) -> Option> { let (offset, length) = self.get_offset_length(property.offset, false)?; let list = (offset..offset + length * 4) .step_by(4) .into_iter() .map(|offset| LittleEndian::read_f32(&self.bytes[offset..])) .collect(); Some(list) } pub fn read_long_list(&self, property: LegacyProperty) -> Option> { let (offset, length) = self.get_offset_length(property.offset, false)?; let list = (offset..offset + length * 8) .step_by(8) .into_iter() .map(|offset| LittleEndian::read_i64(&self.bytes[offset..])) .collect(); Some(list) } pub fn read_double_list(&self, property: LegacyProperty) -> Option> { let (offset, length) = self.get_offset_length(property.offset, false)?; let list = (offset..offset + length * 8) .step_by(8) .into_iter() .map(|offset| LittleEndian::read_f64(&self.bytes[offset..])) .collect(); Some(list) } pub fn read_string_list(&self, property: LegacyProperty) -> Option>> { let (offset, length) = self.get_offset_length(property.offset, false)?; let list = (offset..offset + length * 8) .step_by(8) .into_iter() .map(|offset| self.read_string_at(offset, true)) .collect(); Some(list) } } ================================================ FILE: packages/isar_core/src/legacy/mod.rs ================================================ pub(crate) mod isar_object_v1; ================================================ FILE: packages/isar_core/src/lib.rs ================================================ #![allow(clippy::new_without_default)] #[cfg(not(target_endian = "little"))] compile_error!("Only little endian systems are supported."); pub mod collection; mod cursor; pub mod error; pub mod index; pub mod instance; mod legacy; mod link; mod mdbx; pub mod object; pub mod query; pub mod schema; pub mod txn; pub mod watch; ================================================ FILE: packages/isar_core/src/link.rs ================================================ use crate::cursor::IsarCursors; use crate::error::{IsarError, Result}; use crate::mdbx::cursor::Cursor; use crate::mdbx::db::Db; use crate::object::id::{BytesToId, IdToBytes}; use crate::object::isar_object::IsarObject; use std::ops::Deref; use xxhash_rust::xxh3::xxh3_64_with_seed; #[derive(Clone)] pub(crate) struct IsarLink { pub name: String, pub id: u64, db: Db, bl_db: Db, source_db: Db, target_db: Db, } impl IsarLink { pub fn new( collection: &str, name: &str, backlink: bool, db: Db, bl_db: Db, source_db: Db, target_db: Db, ) -> IsarLink { let seed = if backlink { 1 } else { 0 }; let seed = xxh3_64_with_seed(collection.as_bytes(), seed); let id = xxh3_64_with_seed(name.as_bytes(), seed); IsarLink { name: name.to_string(), id, db, bl_db, source_db, target_db, } } pub fn iter_ids(&self, cursors: &IsarCursors, id: i64, mut callback: F) -> Result where F: FnMut(&mut Cursor, i64) -> Result, { let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_dups(&id, |cursor, link_target_key| { callback(cursor, link_target_key.to_id()) }) } pub fn iter<'txn, 'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, id: i64, mut callback: F, ) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { let mut target_cursor = cursors.get_cursor(self.target_db)?; self.iter_ids(cursors, id, |_, link_target_key| { if let Some((id_bytes, object)) = target_cursor.move_to(&link_target_key)? { callback(id_bytes.deref().to_id(), IsarObject::from_bytes(&object)) } else { Err(IsarError::DbCorrupted { message: "Target object does not exist".to_string(), }) } }) } pub fn create(&self, cursors: &IsarCursors, source_id: i64, target_id: i64) -> Result { let mut source_cursor = cursors.get_cursor(self.source_db)?; let mut target_cursor = cursors.get_cursor(self.target_db)?; let exists_source = source_cursor.move_to(&source_id)?.is_some(); let exists_target = target_cursor.move_to(&target_id)?.is_some(); if !exists_source || !exists_target { return Ok(false); } let mut link_cursor = cursors.get_cursor(self.db)?; link_cursor.put(&source_id, &target_id.to_id_bytes())?; let mut backlink_cursor = cursors.get_cursor(self.bl_db)?; backlink_cursor.put(&target_id, &source_id.to_id_bytes())?; Ok(true) } pub fn delete(&self, cursors: &IsarCursors, source_id: i64, target_id: i64) -> Result { let mut link_cursor = cursors.get_cursor(self.db)?; let exists = link_cursor .move_to_key_val(&source_id, &target_id.to_id_bytes())? .is_some(); if exists { let mut backlink_cursor = cursors.get_cursor(self.bl_db)?; let backlink_exists = backlink_cursor .move_to_key_val(&target_id, &source_id.to_id_bytes())? .is_some(); if backlink_exists { link_cursor.delete_current()?; backlink_cursor.delete_current()?; Ok(true) } else { Err(IsarError::DbCorrupted { message: "Backlink does not exist".to_string(), }) } } else { Ok(false) } } pub fn delete_all_for_object(&self, cursors: &IsarCursors, id: i64) -> Result<()> { let id_bytes = id.to_id_bytes(); let mut backlink_cursor = cursors.get_cursor(self.bl_db)?; self.iter_ids(cursors, id, |cursor, link_target_key| { let exists = backlink_cursor .move_to_key_val(&link_target_key, &id_bytes)? .is_some(); if exists { cursor.delete_current()?; backlink_cursor.delete_current()?; Ok(true) } else { Err(IsarError::DbCorrupted { message: "Backlink does not exist".to_string(), }) } })?; Ok(()) } pub fn get_size(&self, cursors: &IsarCursors) -> Result { Ok(cursors.db_stat(self.db)?.1) } pub fn clear(&self, cursors: &IsarCursors) -> Result<()> { cursors.clear_db(self.db)?; cursors.clear_db(self.bl_db) } pub fn verify(&self, cursors: &IsarCursors, links: &[(i64, i64)]) -> Result<()> { let link_count = cursors.db_stat(self.db)?.0 as usize; let backlink_count = cursors.db_stat(self.db)?.0 as usize; if link_count != links.len() || backlink_count != links.len() { return Err(IsarError::DbCorrupted { message: "Link or Backlink count mismatch.".to_string(), }); } let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_all(false, true, |_, id_bytes, target_id_bytes| { let id = id_bytes.to_id(); let target_id = target_id_bytes.to_id(); if links.contains(&(id, target_id)) { Ok(true) } else { Err(IsarError::DbCorrupted { message: "Unknown link in database.".to_string(), }) } })?; let mut cursor = cursors.get_cursor(self.bl_db)?; cursor.iter_all(false, true, |_, target_id_bytes, id_bytes| { let id = id_bytes.to_id(); let target_id = target_id_bytes.to_id(); if links.contains(&(id, target_id)) { Ok(true) } else { Err(IsarError::DbCorrupted { message: "Unknown link in database.".to_string(), }) } })?; Ok(()) } } ================================================ FILE: packages/isar_core/src/mdbx/cursor.rs ================================================ use crate::error::Result; use crate::mdbx::db::Db; use crate::mdbx::txn::Txn; use crate::mdbx::{from_mdb_val, mdbx_result, to_mdb_val, Key, KeyVal, EMPTY_KEY, EMPTY_VAL}; use core::ptr; use std::cmp::Ordering; use std::marker::PhantomData; pub struct UnboundCursor { cursor: *mut ffi::MDBX_cursor, } impl UnboundCursor { pub(crate) fn new() -> Self { let cursor = unsafe { ffi::mdbx_cursor_create(ptr::null_mut()) }; UnboundCursor { cursor } } pub fn bind<'txn>(self, txn: &'txn Txn, db: Db) -> Result> { unsafe { mdbx_result(ffi::mdbx_cursor_bind(txn.txn, self.cursor, db.dbi))?; } Ok(Cursor { cursor: self, _marker: PhantomData::default(), }) } } impl Drop for UnboundCursor { fn drop(&mut self) { unsafe { ffi::mdbx_cursor_close(self.cursor) } } } pub struct Cursor<'txn> { cursor: UnboundCursor, _marker: PhantomData<&'txn ()>, } impl<'txn> Cursor<'txn> { pub fn unbind(self) -> UnboundCursor { self.cursor } #[allow(clippy::try_err)] fn op_get( &mut self, op: ffi::MDBX_cursor_op, key: Option<&[u8]>, val: Option<&[u8]>, ) -> Result>> { let mut key = key.map_or(EMPTY_KEY, |key| unsafe { to_mdb_val(key) }); let mut data = val.map_or(EMPTY_VAL, |val| unsafe { to_mdb_val(val) }); let result = unsafe { ffi::mdbx_cursor_get(self.cursor.cursor, &mut key, &mut data, op) }; match result { ffi::MDBX_SUCCESS | ffi::MDBX_RESULT_TRUE => { let key = unsafe { from_mdb_val(&key) }; let data = unsafe { from_mdb_val(&data) }; Ok(Some((key, data))) } ffi::MDBX_NOTFOUND | ffi::MDBX_ENODATA => Ok(None), e => { mdbx_result(e)?; unreachable!(); } } } pub fn move_to(&mut self, key: &K) -> Result>> { self.op_get( ffi::MDBX_cursor_op::MDBX_SET_KEY, Some(&key.as_bytes()), None, ) } pub fn move_to_key_val(&mut self, key: &K, val: &[u8]) -> Result>> { self.op_get( ffi::MDBX_cursor_op::MDBX_GET_BOTH, Some(&key.as_bytes()), Some(val), ) } fn move_to_gte(&mut self, key: &K) -> Result>> { self.op_get( ffi::MDBX_cursor_op::MDBX_SET_RANGE, Some(&key.as_bytes()), None, ) } fn move_to_next_dup(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_NEXT_DUP, None, None) } fn move_to_last_dup(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_LAST_DUP, None, None) } fn move_to_prev_no_dup(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_PREV_NODUP, None, None) } pub fn move_to_next(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_NEXT, None, None) } pub fn move_to_first(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_FIRST, None, None) } pub fn move_to_last(&mut self) -> Result>> { self.op_get(ffi::MDBX_cursor_op::MDBX_LAST, None, None) } pub fn put(&mut self, key: &K, data: &[u8]) -> Result<()> { unsafe { // make sure that bytes are not dropped before the call to mdbx_cursor_put let bytes = &key.as_bytes(); let key = to_mdb_val(bytes); let mut data = to_mdb_val(data); mdbx_result(ffi::mdbx_cursor_put(self.cursor.cursor, &key, &mut data, 0))?; } Ok(()) } /// Requires the cursor to have a valid position pub fn delete_current(&mut self) -> Result<()> { unsafe { mdbx_result(ffi::mdbx_cursor_del(self.cursor.cursor, 0))? }; Ok(()) } fn iter( &mut self, skip_duplicates: bool, ascending: bool, mut callback: impl FnMut(&mut Self, &'txn [u8], &'txn [u8]) -> Result, ) -> Result { let next = match (ascending, skip_duplicates) { (true, true) => ffi::MDBX_cursor_op::MDBX_NEXT_NODUP, (true, false) => ffi::MDBX_cursor_op::MDBX_NEXT, (false, true) => ffi::MDBX_cursor_op::MDBX_PREV_NODUP, (false, false) => ffi::MDBX_cursor_op::MDBX_PREV, }; loop { if let Some((key, val)) = self.op_get(next, None, None)? { if !callback(self, key, val)? { return Ok(false); } } else { return Ok(true); } } } pub fn iter_all( &mut self, skip_duplicates: bool, ascending: bool, mut callback: impl FnMut(&mut Self, &'txn [u8], &'txn [u8]) -> Result, ) -> Result { let first = if ascending { self.move_to_first()? } else { self.move_to_last()? }; if let Some((key, val)) = first { if !callback(self, key, val)? { return Ok(false); } } else { return Ok(true); } self.iter(skip_duplicates, ascending, callback) } fn iter_between_first( &mut self, lower_key: &K, upper_key: &K, ascending: bool, duplicates: bool, ) -> Result>> { let first_entry = if !ascending { if let Some(first_entry) = self.move_to_gte(upper_key)? { if duplicates { self.move_to_last_dup()?.or(Some(first_entry)) } else { Some(first_entry) } } else if let Some(last) = self.move_to_last()? { // If some key between upper_key and lower_key happens to be the last key in the db if lower_key.cmp_bytes(&last.0) != Ordering::Greater { Some(last) } else { None } } else { None } } else { self.move_to_gte(lower_key)? }; if let Some(first_entry) = first_entry { if upper_key.cmp_bytes(&first_entry.0) == Ordering::Less { if !ascending { if let Some(prev) = self.move_to_prev_no_dup()? { if lower_key.cmp_bytes(&prev.0) != Ordering::Greater { return Ok(Some(prev)); } } } Ok(None) } else { Ok(Some(first_entry)) } } else { Ok(None) } } pub fn iter_between( &mut self, lower_key: &K, upper_key: &K, duplicates: bool, skip_duplicates: bool, ascending: bool, mut callback: impl FnMut(&mut Self, &'txn [u8], &'txn [u8]) -> Result, ) -> Result { if upper_key.cmp_bytes(&lower_key.as_bytes()) == Ordering::Less { return Ok(true); } if let Some((key, val)) = self.iter_between_first(lower_key, upper_key, ascending, duplicates)? { if !callback(self, key, val)? { return Ok(false); } } else { return Ok(true); } self.iter(skip_duplicates, ascending, |cursor, key, val| { let abort = if ascending { upper_key.cmp_bytes(&key) == Ordering::Less } else { lower_key.cmp_bytes(&key) == Ordering::Greater }; if abort { Ok(true) } else { callback(cursor, key, val) } }) } pub fn iter_dups( &mut self, key: &K, mut callback: impl FnMut(&mut Self, &'txn [u8]) -> Result, ) -> Result { if let Some((_, val)) = self.move_to(key)? { if !callback(self, &val)? { return Ok(false); } } else { return Ok(true); } loop { if let Some((_, val)) = self.move_to_next_dup()? { if !callback(self, &val)? { return Ok(false); } } else { return Ok(true); } } } } #[cfg(test)] mod tests { /*use crate::mdbx::db::Db; use crate::mdbx::env::tests::get_env; use crate::mdbx::env::Env; use itertools::Itertools; use std::sync::{Arc, Mutex}; fn get_filled_db() -> (Env, Db) { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", false, false).unwrap(); db.put(&txn, b"key1", b"val1").unwrap(); db.put(&txn, b"key2", b"val2").unwrap(); db.put(&txn, b"key3", b"val3").unwrap(); db.put(&txn, b"key4", b"val4").unwrap(); txn.commit().unwrap(); (env, db) } fn get_filled_db_dup() -> (Env, Db) { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", true, false).unwrap(); db.put(&txn, b"key1", b"val1").unwrap(); db.put(&txn, b"key1", b"val1b").unwrap(); db.put(&txn, b"key1", b"val1c").unwrap(); db.put(&txn, b"key2", b"val2").unwrap(); db.put(&txn, b"key2", b"val2b").unwrap(); db.put(&txn, b"key2", b"val2c").unwrap(); txn.commit().unwrap(); (env, db) } fn get_empty_db() -> (Env, Db) { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", true, false).unwrap(); txn.commit().unwrap(); (env, db) } #[test] fn test_get() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); let entry = cur.get().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1"[..]))); cur.move_to_next().unwrap(); let entry = cur.get().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_get_dup() { let (env, db) = get_filled_db_dup(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); let entry = cur.get().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1"[..]))); cur.move_to_next().unwrap(); let entry = cur.get().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1b"[..]))); } #[test] fn test_move_to_first() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let first = cur.move_to_first().unwrap(); assert_eq!(first, Some((&b"key1"[..], &b"val1"[..]))); let next = cur.move_to_next().unwrap(); assert_eq!(next, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_move_to_first_empty() { let (env, db) = get_empty_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let first = cur.move_to_first().unwrap(); assert_eq!(first, None); let next = cur.move_to_next().unwrap(); assert_eq!(next, None); } #[test] fn test_move_to_last() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let last = cur.move_to_last().unwrap(); assert_eq!(last, Some((&b"key4"[..], &b"val4"[..]))); let next = cur.move_to_next().unwrap(); assert_eq!(next, None); } #[test] fn test_move_to_last_dup() { let (env, db) = get_filled_db_dup(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let last = cur.move_to_last().unwrap(); assert_eq!(last, Some((&b"key2"[..], &b"val2c"[..]))); } #[test] fn test_move_to_last_empty() { let (env, db) = get_empty_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to_last().unwrap(); assert!(entry.is_none()); let entry = cur.move_to_next().unwrap(); assert!(entry.is_none()); } #[test] fn test_move_to() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to(b"key2").unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); let entry = cur.move_to(b"key1").unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1"[..]))); let next = cur.move_to_next().unwrap(); assert_eq!(next, Some((&b"key2"[..], &b"val2"[..]))); let entry = cur.move_to(b"key5").unwrap(); assert_eq!(entry, None); } #[test] fn test_move_to_empty() { let (env, db) = get_empty_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to(b"key1").unwrap(); assert!(entry.is_none()); let entry = cur.move_to_next().unwrap(); assert!(entry.is_none()); } #[test] fn test_move_to_gte() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to_gte(b"key2").unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); let entry = cur.move_to_gte(b"k").unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1"[..]))); let next = cur.move_to_next().unwrap(); assert_eq!(next, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn move_to_gte_empty() { let (env, db) = get_empty_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to_gte(b"key1").unwrap(); assert!(entry.is_none()); let entry = cur.move_to_next().unwrap(); assert!(entry.is_none()); } #[test] fn test_move_to_next() { let (env, db) = get_filled_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1"[..]))); let entry = cur.move_to_next().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_move_to_next_dup() { let (env, db) = get_filled_db_dup(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); let entry = cur.move_to_next().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1b"[..]))); let entry = cur.move_to_next().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1c"[..]))); let entry = cur.move_to_next().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_move_to_next_empty() { let (env, db) = get_empty_db(); let txn = env.txn(false).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entry = cur.move_to_next().unwrap(); assert!(entry.is_none()); let entry = cur.move_to_next().unwrap(); assert!(entry.is_none()); } #[test] fn test_delete_current() { let (env, db) = get_filled_db(); let txn = env.txn(true).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); cur.delete_current(false).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_delete_current_dup() { let (env, db) = get_filled_db_dup(); let txn = env.txn(true).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); cur.delete_current(false).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1b"[..]))); cur.delete_current(true).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_delete_while() { let (env, db) = get_filled_db(); let txn = env.txn(true).unwrap(); let mut cur = db.cursor(&txn).unwrap(); let entries = Arc::new(Mutex::new(vec![(b"key1", b"val1"), (b"key2", b"val2")])); cur.move_to_first().unwrap(); cur.delete_while( |k, v| { let mut entries = entries.lock().unwrap(); if entries.is_empty() { return false; } let (rk, rv) = entries.remove(0); assert_eq!((&rk[..], &rv[..]), (k, v)); true }, false, ) .unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key3"[..], &b"val3"[..]))); } #[test] fn test_delete_while_dup() { let (env, db) = get_filled_db_dup(); let txn = env.txn(true).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); cur.delete_current(false).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key1"[..], &b"val1b"[..]))); cur.delete_current(true).unwrap(); let entry = cur.move_to_first().unwrap(); assert_eq!(entry, Some((&b"key2"[..], &b"val2"[..]))); } #[test] fn test_iter() { let (env, db) = get_filled_db(); let txn = env.txn(true).unwrap(); let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); cur.move_to_next().unwrap(); let keys = cur .iter() .map(|r| { let (k, _) = r.unwrap(); k }) .collect_vec(); assert_eq!(vec![b"key2", b"key3", b"key4"], keys); } #[test] fn test_get_put_delete() { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", false, false).unwrap(); db.put(&txn, b"key1", b"val1").unwrap(); db.put(&txn, b"key2", b"val2").unwrap(); db.put(&txn, b"key3", b"val3").unwrap(); db.put(&txn, b"key2", b"val4").unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); assert_eq!(b"val1", db.get(&txn, b"key1").unwrap().unwrap()); assert_eq!(b"val4", db.get(&txn, b"key2").unwrap().unwrap()); assert_eq!(b"val3", db.get(&txn, b"key3").unwrap().unwrap()); assert_eq!(db.get(&txn, b"key").unwrap(), None); db.delete(&txn, b"key1", None).unwrap(); assert_eq!(db.get(&txn, b"key1").unwrap(), None); txn.abort(); } #[test] fn test_put_get_del_multi() { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", true, false).unwrap(); db.put(&txn, b"key1", b"val1").unwrap(); db.put(&txn, b"key1", b"val2").unwrap(); db.put(&txn, b"key1", b"val3").unwrap(); db.put(&txn, b"key2", b"val4").unwrap(); db.put(&txn, b"key2", b"val5").unwrap(); db.put(&txn, b"key2", b"val6").unwrap(); db.put(&txn, b"key3", b"val7").unwrap(); db.put(&txn, b"key3", b"val8").unwrap(); db.put(&txn, b"key3", b"val9").unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); { //let mut cur = db.cursor(&txn).unwrap(); //assert_eq!(cur.set(b"key2").unwrap(), true); //let iter = cur.iter_dup(); //let vals = iter.map(|x| x.1).collect_vec(); //assert!(iter.error.is_none()); //assert_eq!(vals, vec![b"val4", b"val5", b"val6"]); } txn.commit().unwrap(); let txn = env.txn(true).unwrap(); db.delete(&txn, b"key1", Some(b"val2")).unwrap(); db.delete(&txn, b"key2", None).unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); { let mut cur = db.cursor(&txn).unwrap(); cur.move_to_first().unwrap(); let iter = cur.iter(); let vals: Result> = iter.map_ok(|x| x.1).collect(); assert_eq!( vals.unwrap(), vec![b"val1", b"val3", b"val7", b"val8", b"val9"] ); } txn.commit().unwrap(); } #[test] fn test_put_no_override() { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", false, false).unwrap(); db.put(&txn, b"key", b"val").unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); assert_eq!(db.put_no_override(&txn, b"key", b"err").unwrap(), false); assert_eq!(db.put_no_override(&txn, b"key2", b"val2").unwrap(), true); assert_eq!(db.get(&txn, b"key").unwrap(), Some(&b"val"[..])); assert_eq!(db.get(&txn, b"key2").unwrap(), Some(&b"val2"[..])); txn.abort(); } #[test] fn test_put_no_dup_data() { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", true, false).unwrap(); db.put(&txn, b"key", b"val").unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); assert_eq!(db.put_no_dup_data(&txn, b"key", b"val").unwrap(), false); assert_eq!(db.put_no_dup_data(&txn, b"key2", b"val2").unwrap(), true); assert_eq!(db.get(&txn, b"key2").unwrap(), Some(&b"val2"[..])); txn.abort(); } #[test] fn test_clear_db() { let env = get_env(); let txn = env.txn(true).unwrap(); let db = Db::open(&txn, "test", false, false).unwrap(); db.put(&txn, b"key1", b"val1").unwrap(); db.put(&txn, b"key2", b"val2").unwrap(); db.put(&txn, b"key3", b"val3").unwrap(); txn.commit().unwrap(); let txn = env.txn(true).unwrap(); db.clear(&txn).unwrap(); txn.commit().unwrap(); let txn = env.txn(false).unwrap(); { let mut cursor = db.cursor(&txn).unwrap(); assert!(cursor.move_to_first().unwrap().is_none()); } txn.abort(); }*/ } ================================================ FILE: packages/isar_core/src/mdbx/db.rs ================================================ use crate::error::Result; use crate::mdbx::mdbx_result; use crate::mdbx::txn::Txn; use std::ffi::CString; use std::mem::size_of; use std::ptr; #[derive(Copy, Clone, Eq, PartialEq)] pub struct Db { pub(crate) dbi: ffi::MDBX_dbi, pub dup: bool, } impl Db { pub fn runtime_id(&self) -> u64 { self.dbi as u64 } pub fn open( txn: &Txn, name: Option<&str>, int_key: bool, dup: bool, int_dup: bool, ) -> Result { let mut flags = ffi::MDBX_CREATE; if int_key { flags |= ffi::MDBX_INTEGERKEY; } if dup { flags |= ffi::MDBX_DUPSORT; if int_dup { flags |= ffi::MDBX_INTEGERDUP | ffi::MDBX_DUPFIXED; } } let mut dbi: ffi::MDBX_dbi = 0; if let Some(name) = name { let name = CString::new(name.as_bytes()).unwrap(); unsafe { mdbx_result(ffi::mdbx_dbi_open(txn.txn, name.as_ptr(), flags, &mut dbi))?; } } else { unsafe { mdbx_result(ffi::mdbx_dbi_open(txn.txn, ptr::null(), 0, &mut dbi))?; } } Ok(Self { dbi, dup }) } pub fn stat(&self, txn: &Txn) -> Result<(u64, u64)> { let mut stat = ffi::MDBX_stat { ms_psize: 0, ms_depth: 0, ms_branch_pages: 0, ms_leaf_pages: 0, ms_overflow_pages: 0, ms_entries: 0, ms_mod_txnid: 0, }; let stat_ptr = &mut stat as *mut ffi::MDBX_stat; unsafe { ffi::mdbx_dbi_stat( txn.txn, self.dbi, stat_ptr, size_of::() as ffi::size_t, ); } let size = (stat.ms_branch_pages + stat.ms_leaf_pages + stat.ms_overflow_pages) * stat.ms_psize as u64; Ok((stat.ms_entries, size)) } pub fn clear(&self, txn: &Txn) -> Result<()> { unsafe { mdbx_result(ffi::mdbx_drop(txn.txn, self.dbi, false)) }?; Ok(()) } pub fn drop(self, txn: &Txn) -> Result<()> { unsafe { mdbx_result(ffi::mdbx_drop(txn.txn, self.dbi, true)) }?; Ok(()) } } #[cfg(test)] mod tests { /*#[test] fn test_open() { let env = get_env(); let read_txn = env.txn(false).unwrap(); assert!(Db::open(&read_txn, "test", false, false).is_err()); read_txn.abort(); let flags = vec![ (false, false, 0), (false, true, 0), (true, false, ffi::MDB_DUPSORT), (true, true, ffi::MDB_DUPSORT | ffi::MDB_DUPFIXED), ]; for (i, (dup, fixed_vals, flags)) in flags.iter().enumerate() { let txn = env.txn(true).unwrap(); let db = Db::open(&txn, format!("test{}", i).as_str(), *dup, *fixed_vals).unwrap(); txn.commit().unwrap(); let mut actual_flags: u32 = 0; let txn = env.txn(false).unwrap(); unsafe { ffi::mdb_dbi_flags(txn.txn, db.dbi, &mut actual_flags); } txn.abort(); assert_eq!(*flags, actual_flags); } }*/ } ================================================ FILE: packages/isar_core/src/mdbx/env.rs ================================================ use super::osal::*; use crate::error::{IsarError, Result}; use crate::mdbx::mdbx_result; use crate::mdbx::txn::Txn; use core::ptr; pub struct Env { env: *mut ffi::MDBX_env, } unsafe impl Sync for Env {} unsafe impl Send for Env {} const MIB: isize = 1 << 20; impl Env { pub fn create( path: &str, max_dbs: u64, max_size_mib: usize, relaxed_durability: bool, ) -> Result { let path = str_to_os(path)?; let mut env: *mut ffi::MDBX_env = ptr::null_mut(); unsafe { mdbx_result(ffi::mdbx_env_create(&mut env))?; mdbx_result(ffi::mdbx_env_set_option( env, ffi::MDBX_option_t::MDBX_opt_max_db, max_dbs, ))?; let mut flags = ffi::MDBX_NOTLS | ffi::MDBX_COALESCE | ffi::MDBX_NOSUBDIR; if relaxed_durability { flags |= ffi::MDBX_NOMETASYNC; } let max_size = (max_size_mib as isize).saturating_mul(MIB); let mut err_code = 0; for i in 0..9 { let max_size_i = (max_size - i * (max_size / 10)).clamp(10 * MIB, isize::MAX); mdbx_result(ffi::mdbx_env_set_geometry( env, MIB, 0, max_size_i, 5 * MIB, 20 * MIB, -1, ))?; err_code = ENV_OPEN(env, path.as_ptr(), flags, 0o600); if err_code == ffi::MDBX_SUCCESS { break; } } match err_code { ffi::MDBX_SUCCESS => Ok(Env { env }), ffi::MDBX_EPERM | ffi::MDBX_ENOFILE => Err(IsarError::PathError {}), e => { mdbx_result(e)?; unreachable!() } } } } pub fn txn(&self, write: bool) -> Result { let flags = if write { 0 } else { ffi::MDBX_TXN_RDONLY }; let mut txn: *mut ffi::MDBX_txn = ptr::null_mut(); unsafe { mdbx_result(ffi::mdbx_txn_begin_ex( self.env, ptr::null_mut(), flags, &mut txn, ptr::null_mut(), ))?; } Ok(Txn::new(txn, write)) } pub fn copy(&self, path: &str) -> Result<()> { let path = str_to_os(path)?; unsafe { mdbx_result(ENV_COPY(self.env, path.as_ptr(), ffi::MDBX_CP_COMPACT)) } } } impl Drop for Env { fn drop(&mut self) { if !self.env.is_null() { unsafe { ffi::mdbx_env_close_ex(self.env, false); } self.env = ptr::null_mut(); } } } ================================================ FILE: packages/isar_core/src/mdbx/mod.rs ================================================ #![allow(clippy::missing_safety_doc)] use crate::error::{IsarError, Result}; use core::slice; use libc::c_int; use std::borrow::Cow; use std::cmp::Ordering; use std::ffi::{c_void, CStr}; pub mod cursor; pub mod db; pub mod env; pub mod txn; pub type KeyVal<'txn> = (&'txn [u8], &'txn [u8]); pub const EMPTY_KEY: ffi::MDBX_val = ffi::MDBX_val { iov_len: 0, iov_base: 0 as *mut c_void, }; pub const EMPTY_VAL: ffi::MDBX_val = ffi::MDBX_val { iov_len: 0, iov_base: 0 as *mut c_void, }; #[inline] pub unsafe fn from_mdb_val<'a>(val: &ffi::MDBX_val) -> &'a [u8] { slice::from_raw_parts(val.iov_base as *const u8, val.iov_len as usize) } #[inline] pub unsafe fn to_mdb_val(value: &[u8]) -> ffi::MDBX_val { ffi::MDBX_val { iov_len: value.len() as ffi::size_t, iov_base: value.as_ptr() as *mut libc::c_void, } } #[inline] pub fn mdbx_result(err_code: c_int) -> Result<()> { match err_code { ffi::MDBX_SUCCESS | ffi::MDBX_RESULT_TRUE => Ok(()), ffi::MDBX_MAP_FULL => Err(IsarError::DbFull {}), other => unsafe { let err_raw = ffi::mdbx_strerror(other); let err = CStr::from_ptr(err_raw); Err(IsarError::MdbxError { code: other, message: err .to_str() .unwrap_or("Cannot decode error message") .to_string(), }) }, } } pub trait Key { fn as_bytes(&self) -> Cow<[u8]>; fn cmp_bytes(&self, other: &[u8]) -> Ordering; } #[cfg(target_os = "windows")] pub(crate) mod osal { use super::*; use widestring::U16CString; pub fn str_to_os(str: &str) -> Result { U16CString::from_str(str).map_err(|_| IsarError::IllegalArg { message: "Invalid String provided".to_string(), }) } pub const ENV_OPEN: unsafe extern "C" fn( *mut ffi::MDBX_env, *const u16, ffi::MDBX_env_flags_t, ffi::mdbx_mode_t, ) -> i32 = ffi::mdbx_env_openW; pub const ENV_COPY: unsafe extern "C" fn( *mut ffi::MDBX_env, *const u16, ffi::MDBX_copy_flags_t, ) -> i32 = ffi::mdbx_env_copyW; } #[cfg(not(target_os = "windows"))] pub(crate) mod osal { use super::*; use std::ffi::CString; pub fn str_to_os(str: &str) -> Result { CString::new(str.as_bytes()).map_err(|_| IsarError::IllegalArg { message: "Invalid String provided".to_string(), }) } pub const ENV_OPEN: unsafe extern "C" fn( *mut ffi::MDBX_env, *const libc::c_char, ffi::MDBX_env_flags_t, ffi::mdbx_mode_t, ) -> i32 = ffi::mdbx_env_open; pub const ENV_COPY: unsafe extern "C" fn( *mut ffi::MDBX_env, *const libc::c_char, ffi::MDBX_copy_flags_t, ) -> i32 = ffi::mdbx_env_copy; } ================================================ FILE: packages/isar_core/src/mdbx/txn.rs ================================================ use crate::error::Result; use crate::mdbx::mdbx_result; use core::ptr; use std::marker::PhantomData; pub struct Txn<'env> { pub(crate) txn: *mut ffi::MDBX_txn, pub write: bool, _marker: PhantomData<&'env ()>, } impl<'env> Txn<'env> { pub(crate) fn new(txn: *mut ffi::MDBX_txn, write: bool) -> Self { Txn { txn, write, _marker: PhantomData::default(), } } pub fn commit(mut self) -> Result<()> { let result = unsafe { mdbx_result(ffi::mdbx_txn_commit_ex(self.txn, ptr::null_mut())) }; self.txn = ptr::null_mut(); result?; Ok(()) } pub fn abort(self) {} } impl<'a> Drop for Txn<'a> { fn drop(&mut self) { if !self.txn.is_null() { unsafe { ffi::mdbx_txn_abort(self.txn); } self.txn = ptr::null_mut(); } } } ================================================ FILE: packages/isar_core/src/object/data_type.rs ================================================ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash)] pub enum DataType { Bool, Byte, Int, Float, #[serde(alias = "DateTime")] Long, Double, String, Object, BoolList, ByteList, IntList, FloatList, #[serde(alias = "DateTimeList")] LongList, DoubleList, StringList, ObjectList, } impl DataType { pub fn is_static(&self) -> bool { matches!( &self, DataType::Bool | DataType::Byte | DataType::Int | DataType::Long | DataType::Float | DataType::Double ) } pub fn is_dynamic(&self) -> bool { !self.is_static() } pub fn get_static_size(&self) -> usize { match *self { DataType::Bool | DataType::Byte => 1, DataType::Int | DataType::Float => 4, DataType::Long | DataType::Double => 8, _ => 3, } } pub fn is_scalar(&self) -> bool { self.get_element_type().is_none() } pub fn get_element_type(&self) -> Option { match self { DataType::BoolList => Some(DataType::Bool), DataType::ByteList => Some(DataType::Byte), DataType::IntList => Some(DataType::Int), DataType::FloatList => Some(DataType::Float), DataType::LongList => Some(DataType::Long), DataType::DoubleList => Some(DataType::Double), DataType::StringList => Some(DataType::String), DataType::ObjectList => Some(DataType::Object), _ => None, } } } ================================================ FILE: packages/isar_core/src/object/id.rs ================================================ use std::{borrow::Cow, cmp::Ordering}; use crate::mdbx::Key; pub trait BytesToId { fn to_id(&self) -> i64; } impl BytesToId for &[u8] { fn to_id(&self) -> i64 { let unsigned = u64::from_le_bytes((**self).try_into().unwrap()); let signed: i64 = unsigned as i64; signed ^ 1 << 63 } } pub trait IdToBytes { fn to_id_bytes(&self) -> [u8; 8]; } impl IdToBytes for i64 { fn to_id_bytes(&self) -> [u8; 8] { let unsigned = *self as u64; (unsigned ^ 1 << 63).to_le_bytes() } } impl Key for i64 { fn as_bytes(&self) -> Cow<[u8]> { let bytes = self.to_id_bytes(); Cow::Owned(bytes.to_vec()) } fn cmp_bytes(&self, other: &[u8]) -> Ordering { let other_id = other.to_id(); self.cmp(&other_id) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_to_id_bytes() { assert_eq!(i64::MIN.to_id_bytes(), [0, 0, 0, 0, 0, 0, 0, 0]); assert_eq!((i64::MIN + 1).to_id_bytes(), [1, 0, 0, 0, 0, 0, 0, 0]); assert_eq!( i64::MAX.to_id_bytes(), [255, 255, 255, 255, 255, 255, 255, 255] ); assert_eq!( (i64::MAX - 1).to_id_bytes(), [254, 255, 255, 255, 255, 255, 255, 255] ); } #[test] fn test_to_id() { assert_eq!([0, 0, 0, 0, 0, 0, 0, 0].as_ref().to_id(), i64::MIN); assert_eq!([1, 0, 0, 0, 0, 0, 0, 0].as_ref().to_id(), i64::MIN + 1); assert_eq!( [254, 255, 255, 255, 255, 255, 255, 255].as_ref().to_id(), i64::MAX - 1 ); assert_eq!( [255, 255, 255, 255, 255, 255, 255, 255].as_ref().to_id(), i64::MAX ); } } ================================================ FILE: packages/isar_core/src/object/isar_object.rs ================================================ use crate::object::data_type::DataType; use crate::object::object_builder::ObjectBuilder; use byteorder::{ByteOrder, LittleEndian}; use std::{cmp::Ordering, str::from_utf8_unchecked}; use xxhash_rust::xxh3::xxh3_64_with_seed; #[derive(Copy, Clone, Eq, PartialEq)] pub struct IsarObject<'a> { bytes: &'a [u8], static_size: usize, } impl<'a> IsarObject<'a> { pub const NULL_BYTE: u8 = 0; pub const NULL_BOOL: u8 = 0; pub const FALSE_BOOL: u8 = 1; pub const TRUE_BOOL: u8 = 2; pub const NULL_INT: i32 = i32::MIN; pub const NULL_LONG: i64 = i64::MIN; pub const NULL_FLOAT: f32 = f32::NAN; pub const NULL_DOUBLE: f64 = f64::NAN; pub const MAX_SIZE: u32 = 2 << 24; pub fn from_bytes(bytes: &'a [u8]) -> Self { let static_size = LittleEndian::read_u16(bytes) as usize; IsarObject { bytes, static_size } } pub fn as_bytes(&self) -> &'a [u8] { self.bytes } pub fn len(&self) -> usize { self.bytes.len() } #[inline] pub(crate) fn contains_offset(&self, offset: usize) -> bool { self.static_size > offset } pub fn is_null(&self, offset: usize, data_type: DataType) -> bool { match data_type { DataType::Byte => false, DataType::Bool => self.read_bool(offset).is_none(), DataType::Int => self.read_int(offset) == Self::NULL_INT, DataType::Long => self.read_long(offset) == Self::NULL_LONG, DataType::Float => self.read_float(offset).is_nan(), DataType::Double => self.read_double(offset).is_nan(), _ => self.get_offset_length(offset).is_none(), } } #[inline] pub fn byte_to_bool(value: u8) -> Option { if value == Self::NULL_BOOL { None } else { Some(value == Self::TRUE_BOOL) } } pub fn read_byte(&self, offset: usize) -> u8 { if self.contains_offset(offset) { self.bytes[offset] } else { Self::NULL_BYTE } } pub fn read_bool(&self, offset: usize) -> Option { let value = if self.contains_offset(offset) { self.bytes[offset] } else { Self::NULL_BOOL }; Self::byte_to_bool(value) } pub fn read_int(&self, offset: usize) -> i32 { if self.contains_offset(offset) { LittleEndian::read_i32(&self.bytes[offset..]) } else { Self::NULL_INT } } pub fn read_float(&self, offset: usize) -> f32 { if self.contains_offset(offset) { LittleEndian::read_f32(&self.bytes[offset..]) } else { Self::NULL_FLOAT } } pub fn read_long(&self, offset: usize) -> i64 { if self.contains_offset(offset) { LittleEndian::read_i64(&self.bytes[offset..]) } else { Self::NULL_LONG } } pub fn read_double(&self, offset: usize) -> f64 { if self.contains_offset(offset) { LittleEndian::read_f64(&self.bytes[offset..]) } else { Self::NULL_DOUBLE } } fn read_u24(&self, offset: usize) -> usize { LittleEndian::read_u24(&self.bytes[offset..]) as usize } fn get_offset_length(&self, offset: usize) -> Option<(usize, usize)> { if self.contains_offset(offset) { let length_offset = self.read_u24(offset); if length_offset != 0 { let length = self.read_u24(length_offset); return Some((length_offset + 3, length)); } } None } pub fn read_length(&self, offset: usize) -> Option { let (_, length) = self.get_offset_length(offset)?; Some(length) } pub fn read_byte_list(&self, offset: usize) -> Option<&'a [u8]> { let (offset, length) = self.get_offset_length(offset)?; Some(&self.bytes[offset..offset + length]) } pub fn read_string(&'a self, offset: usize) -> Option<&'a str> { let bytes = self.read_byte_list(offset)?; let str = unsafe { from_utf8_unchecked(bytes) }; Some(str) } pub fn read_object(&'a self, offset: usize) -> Option { let bytes = self.read_byte_list(offset)?; Some(IsarObject::from_bytes(bytes)) } pub fn read_bool_list(&self, offset: usize) -> Option>> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![None; length]; for i in 0..length { list[i] = Self::byte_to_bool(self.bytes[offset + i]); } Some(list) } pub fn read_int_list(&self, offset: usize) -> Option> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![0; length]; for i in 0..length { list[i] = LittleEndian::read_i32(&self.bytes[offset + i * 4..]); } Some(list) } pub fn read_int_or_null_list(&self, offset: usize) -> Option>> { self.read_int_list(offset).map(|list| { list.into_iter() .map(|value| { if value != Self::NULL_INT { Some(value) } else { None } }) .collect() }) } pub fn read_float_list(&self, offset: usize) -> Option> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![0.0; length]; for i in 0..length { list[i] = LittleEndian::read_f32(&self.bytes[offset + i * 4..]); } Some(list) } pub fn read_float_or_null_list(&self, offset: usize) -> Option>> { self.read_float_list(offset).map(|list| { list.into_iter() .map(|value| if !value.is_nan() { Some(value) } else { None }) .collect() }) } pub fn read_long_list(&self, offset: usize) -> Option> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![0; length]; for i in 0..length { list[i] = LittleEndian::read_i64(&self.bytes[offset + i * 8..]); } Some(list) } pub fn read_long_or_null_list(&self, offset: usize) -> Option>> { self.read_long_list(offset).map(|list| { list.into_iter() .map(|value| { if value != Self::NULL_LONG { Some(value) } else { None } }) .collect() }) } pub fn read_double_list(&self, offset: usize) -> Option> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![0.0; length]; for i in 0..length { list[i] = LittleEndian::read_f64(&self.bytes[offset + i * 8..]); } Some(list) } pub fn read_double_or_null_list(&self, offset: usize) -> Option>> { self.read_double_list(offset).map(|list| { list.into_iter() .map(|value| if !value.is_nan() { Some(value) } else { None }) .collect() }) } pub fn read_string_list(&self, offset: usize) -> Option>> { self.read_dynamic_list(offset, |bytes| unsafe { from_utf8_unchecked(bytes) }) } pub fn read_object_list(&self, offset: usize) -> Option>>> { self.read_dynamic_list(offset, |bytes| IsarObject::from_bytes(bytes)) } fn read_dynamic_list( &self, offset: usize, transform: impl Fn(&'a [u8]) -> T, ) -> Option>> { let (offset, length) = self.get_offset_length(offset)?; let mut list = vec![None; length]; let mut content_offset = offset + length * 3; for i in 0..length { let item_size = self.read_u24(offset + i * 3); if item_size != 0 { let item_size = item_size - 1; let bytes = &self.bytes[content_offset..content_offset + item_size]; let value = transform(bytes); list[i] = Some(value); content_offset += item_size; } } Some(list) } pub fn hash_property( &self, offset: usize, data_type: DataType, case_sensitive: bool, seed: u64, ) -> u64 { match data_type { DataType::Bool | DataType::Byte => xxh3_64_with_seed(&[self.read_byte(offset)], seed), DataType::Int => xxh3_64_with_seed(&self.read_int(offset).to_le_bytes(), seed), DataType::Float => xxh3_64_with_seed(&self.read_float(offset).to_le_bytes(), seed), DataType::Long => xxh3_64_with_seed(&self.read_long(offset).to_le_bytes(), seed), DataType::Double => xxh3_64_with_seed(&self.read_double(offset).to_le_bytes(), seed), DataType::String => Self::hash_string(self.read_string(offset), case_sensitive, seed), _ => match data_type { DataType::StringList => { Self::hash_string_list(self.read_string_list(offset), case_sensitive, seed) } _ => { if let Some((offset, length)) = self.get_offset_length(offset) { let element_size = data_type.get_element_type().unwrap().get_static_size(); xxh3_64_with_seed(&self.bytes[offset..offset + length * element_size], seed) } else { seed } } }, } } pub fn hash_string(value: Option<&str>, case_sensitive: bool, seed: u64) -> u64 { if let Some(str) = value { if case_sensitive { xxh3_64_with_seed(str.as_bytes(), seed) } else { xxh3_64_with_seed(str.to_lowercase().as_bytes(), seed) } } else { seed } } pub fn hash_list(value: Option<&[T]>, seed: u64) -> u64 { if let Some(list) = value { let bytes = ObjectBuilder::get_list_bytes(list); xxh3_64_with_seed(bytes, seed) } else { seed } } pub fn hash_string_list( value: Option>>, case_sensitive: bool, seed: u64, ) -> u64 { if let Some(str) = value { let mut hash = seed; for value in str { hash = Self::hash_string(value, case_sensitive, hash); } hash } else { seed } } pub fn compare_property( &self, other: &IsarObject, offset: usize, data_type: DataType, ) -> Ordering { match data_type { DataType::Bool | DataType::Byte => self.read_byte(offset).cmp(&other.read_byte(offset)), DataType::Int => self.read_int(offset).cmp(&other.read_int(offset)), DataType::Float => self.read_float(offset).total_cmp(&other.read_float(offset)), DataType::Long => self.read_long(offset).cmp(&other.read_long(offset)), DataType::Double => self .read_double(offset) .total_cmp(&other.read_double(offset)), DataType::String => self.read_string(offset).cmp(&other.read_string(offset)), _ => Ordering::Equal, } } } #[cfg(test)] mod tests { use itertools::Itertools; use crate::object::data_type::DataType::*; use crate::object::isar_object::IsarObject; use crate::object::object_builder::ObjectBuilder; use crate::object::property::Property; macro_rules! builder { ($builder:ident, $prop:ident, $type:ident) => { let $prop = Property::debug($type, 2); let props = vec![$prop.clone()]; let mut $builder = ObjectBuilder::new(&props, None); }; } #[test] fn test_read_non_contained_property() { let data_types = vec![ Bool, Byte, Int, Float, Long, Double, String, BoolList, ByteList, IntList, FloatList, LongList, DoubleList, StringList, ]; for data_type in data_types { builder!(_b, p, data_type); let empty = vec![0, 0]; let object = IsarObject::from_bytes(&empty); let should_be_null = data_type != Byte; assert_eq!(object.is_null(p.offset, p.data_type), should_be_null); } } #[test] fn test_read_bool() { builder!(b, p, Bool); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_bool(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Bool); b.write_bool(p.offset, Some(true)); assert_eq!(b.finish().read_bool(p.offset), Some(true)); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Bool); b.write_bool(p.offset, Some(false)); assert_eq!(b.finish().read_bool(p.offset), Some(false)); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_byte() { builder!(b, p, Byte); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_byte(p.offset), IsarObject::NULL_BYTE); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Byte); b.write_byte(p.offset, 123); assert_eq!(b.finish().read_byte(p.offset), 123); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_int() { builder!(b, p, Int); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_int(p.offset), IsarObject::NULL_INT); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Int); b.write_int(p.offset, 123); assert_eq!(b.finish().read_int(p.offset), 123); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_float() { builder!(b, p, Float); b.write_null(p.offset, p.data_type); assert!(b.finish().read_float(p.offset).is_nan()); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Float); b.write_float(p.offset, 123.123); assert!((b.finish().read_float(p.offset) - 123.123).abs() < 0.000001); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_long() { builder!(b, p, Long); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_long(p.offset), IsarObject::NULL_LONG); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Long); b.write_long(p.offset, 123123123123123123); assert_eq!(b.finish().read_long(p.offset), 123123123123123123); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_double() { builder!(b, p, Double); b.write_null(p.offset, p.data_type); assert!(b.finish().read_double(p.offset).is_nan()); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, Double); b.write_double(p.offset, 123123.123123123); assert!((b.finish().read_double(p.offset) - 123123.123123123).abs() < 0.00000001); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_string() { builder!(b, p, String); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_string(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, String); b.write_string(p.offset, Some("hello")); assert_eq!(b.finish().read_string(p.offset), Some("hello")); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, String); b.write_string(p.offset, Some("")); assert_eq!(b.finish().read_string(p.offset), Some("")); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_byte_list() { builder!(b, p, ByteList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_byte_list(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, ByteList); b.write_byte_list(p.offset, Some(&[1, 2, 3])); assert_eq!(b.finish().read_byte_list(p.offset), Some(&[1, 2, 3][..])); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, ByteList); b.write_byte_list(p.offset, Some(&[])); assert_eq!(b.finish().read_byte_list(p.offset), Some(&[][..])); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_int_list() { builder!(b, p, IntList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_int_list(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, IntList); b.write_int_list(p.offset, Some(&[1, 2, 3])); assert_eq!(b.finish().read_int_list(p.offset), Some(vec![1, 2, 3])); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, IntList); b.write_int_list(p.offset, Some(&[])); assert_eq!(b.finish().read_int_list(p.offset), Some(vec![])); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_float_list() { builder!(b, p, FloatList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_float_list(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, FloatList); b.write_float_list(p.offset, Some(&[1.1, 2.2, 3.3])); assert_eq!( b.finish().read_float_list(p.offset), Some(vec![1.1, 2.2, 3.3]) ); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, FloatList); b.write_float_list(p.offset, Some(&[])); assert_eq!(b.finish().read_float_list(p.offset), Some(vec![])); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_long_list() { builder!(b, p, LongList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_long_list(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, LongList); b.write_long_list(p.offset, Some(&[1, 2, 3])); assert_eq!(b.finish().read_long_list(p.offset), Some(vec![1, 2, 3])); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, LongList); b.write_long_list(p.offset, Some(&[])); assert_eq!(b.finish().read_long_list(p.offset), Some(vec![])); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_double_list() { builder!(b, p, DoubleList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_double_list(p.offset), None); assert!(b.finish().is_null(p.offset, p.data_type)); builder!(b, p, DoubleList); b.write_double_list(p.offset, Some(&[1.1, 2.2, 3.3])); assert_eq!( b.finish().read_double_list(p.offset), Some(vec![1.1, 2.2, 3.3]) ); assert!(!b.finish().is_null(p.offset, p.data_type)); builder!(b, p, DoubleList); b.write_double_list(p.offset, Some(&[])); assert_eq!(b.finish().read_double_list(p.offset), Some(vec![])); assert!(!b.finish().is_null(p.offset, p.data_type)); } #[test] fn test_read_string_list() { builder!(b, p, StringList); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().read_string_list(p.offset), None); let cases = vec![ vec![], vec![None], vec![None, None], vec![None, None, None], vec![Some("")], vec![Some(""), Some("")], vec![Some(""), Some(""), Some("")], vec![Some(""), None], vec![None, Some("")], vec![Some(""), None, None], vec![None, Some(""), None], vec![None, None, Some("")], vec![None, Some(""), Some("")], vec![Some(""), None, Some("")], vec![Some(""), Some(""), None], vec![Some("a")], vec![Some("a"), Some("ab")], vec![Some("a"), Some("ab"), Some("abc")], vec![None, Some("a")], vec![Some("a"), None], vec![None, Some("a")], vec![Some("a"), None, None], vec![None, Some("a"), None], vec![None, None, Some("a")], vec![None, Some("a"), Some("bbb")], vec![Some("a"), None, Some("bbb")], vec![Some("a"), Some("bbb"), None], ]; for case1 in &cases { for case2 in &cases { for case3 in &cases { let case = case1 .iter() .chain(case2) .chain(case3) .cloned() .collect_vec(); builder!(b, p, StringList); b.write_string_list(p.offset, Some(&case)); assert_eq!(b.finish().read_string_list(p.offset), Some(case)); } } } } } ================================================ FILE: packages/isar_core/src/object/json_encode_decode.rs ================================================ use crate::error::{IsarError, Result}; use crate::object::data_type::DataType; use crate::object::isar_object::IsarObject; use crate::object::object_builder::ObjectBuilder; use intmap::IntMap; use itertools::Itertools; use serde_json::{json, Map, Value}; use super::property::Property; pub struct JsonEncodeDecode {} impl<'a> JsonEncodeDecode { #[inline(never)] pub fn encode( properties: &[Property], embedded_properties: &IntMap>, object: IsarObject, primitive_null: bool, ) -> Map { let mut object_map = Map::new(); for property in properties { let value = if primitive_null && object.is_null(property.offset, property.data_type) { Value::Null } else { match property.data_type { DataType::Bool => { json!(object.read_bool(property.offset)) } DataType::Byte => { json!(object.read_byte(property.offset)) } DataType::Int => json!(object.read_int(property.offset)), DataType::Float => json!(object.read_float(property.offset)), DataType::Long => json!(object.read_long(property.offset)), DataType::Double => json!(object.read_double(property.offset)), DataType::String => json!(object.read_string(property.offset)), DataType::Object => { let properties = embedded_properties .get(property.target_id.unwrap()) .unwrap(); Self::object_to_value( properties, embedded_properties, object.read_object(property.offset), primitive_null, ) } DataType::BoolList => json!(object.read_bool_list(property.offset).unwrap()), DataType::ByteList => json!(object.read_byte_list(property.offset).unwrap()), DataType::IntList => { if primitive_null { json!(object.read_int_or_null_list(property.offset)) } else { json!(object.read_int_list(property.offset)) } } DataType::FloatList => { if primitive_null { json!(object.read_float_or_null_list(property.offset)) } else { json!(object.read_float_list(property.offset)) } } DataType::LongList => { if primitive_null { json!(object.read_long_or_null_list(property.offset)) } else { json!(object.read_long_list(property.offset)) } } DataType::DoubleList => { if primitive_null { json!(object.read_double_or_null_list(property.offset)) } else { json!(object.read_double_list(property.offset)) } } DataType::StringList => json!(object.read_string_list(property.offset)), DataType::ObjectList => { let properties = embedded_properties .get(property.target_id.unwrap()) .unwrap(); if let Some(objects) = object.read_object_list(property.offset) { let encoded = objects .into_iter() .map(|object| { Self::object_to_value( properties, embedded_properties, object, primitive_null, ) }) .collect_vec(); json!(encoded) } else { Value::Null } } } }; object_map.insert(property.name.clone(), value); } object_map } fn object_to_value( properties: &[Property], embedded_properties: &IntMap>, object: Option, primitive_null: bool, ) -> Value { if let Some(object) = object { let encoded = JsonEncodeDecode::encode(properties, embedded_properties, object, primitive_null); json!(encoded) } else { Value::Null } } #[inline(never)] pub fn decode( properties: &[Property], embedded_properties: &IntMap>, ob: &mut ObjectBuilder, json: &Value, ) -> Result<()> { let object = json.as_object().ok_or(IsarError::InvalidJson {})?; for property in properties { if let Some(value) = object.get(&property.name) { match property.data_type { DataType::Bool => ob.write_bool(property.offset, Self::value_to_bool(value)?), DataType::Byte => ob.write_byte(property.offset, Self::value_to_byte(value)?), DataType::Int => ob.write_int(property.offset, Self::value_to_int(value)?), DataType::Float => { ob.write_float(property.offset, Self::value_to_float(value)?) } DataType::Long => ob.write_long(property.offset, Self::value_to_long(value)?), DataType::Double => { ob.write_double(property.offset, Self::value_to_double(value)?) } DataType::String => { ob.write_string(property.offset, Self::value_to_string(value)?) } DataType::Object => { let builder = Self::value_to_object( value, embedded_properties, property.target_id.unwrap(), )?; ob.write_object(property.offset, builder.as_ref().map(|b| b.finish())); } DataType::BoolList => { let list = Self::value_to_array(value, Self::value_to_bool)?; ob.write_bool_list(property.offset, list.as_deref()); } DataType::ByteList => { let list = Self::value_to_array(value, Self::value_to_byte)?; ob.write_byte_list(property.offset, list.as_deref()); } DataType::IntList => { let list = Self::value_to_array(value, Self::value_to_int)?; ob.write_int_list(property.offset, list.as_deref()); } DataType::FloatList => { let list = Self::value_to_array(value, Self::value_to_float)?; ob.write_float_list(property.offset, list.as_deref()); } DataType::LongList => { let list = Self::value_to_array(value, Self::value_to_long)?; ob.write_long_list(property.offset, list.as_deref()); } DataType::DoubleList => { let list = Self::value_to_array(value, Self::value_to_double)?; ob.write_double_list(property.offset, list.as_deref()); } DataType::StringList => { if value.is_null() { ob.write_string_list(property.offset, None); } else if let Some(list) = value.as_array() { let list: Result>> = list.iter().map(Self::value_to_string).collect(); ob.write_string_list(property.offset, Some(list?.as_slice())); } else { return Err(IsarError::InvalidJson {}); } } DataType::ObjectList => { if value.is_null() { ob.write_object_list(property.offset, None); } else if let Some(list) = value.as_array() { let list: Result>> = list .iter() .map(|value| { Self::value_to_object( value, embedded_properties, property.target_id.unwrap(), ) }) .collect(); let list = list?; let objects = list .iter() .map(|o| o.as_ref().map(|o| o.finish())) .collect_vec(); ob.write_object_list(property.offset, Some(objects.as_slice())); } else { return Err(IsarError::InvalidJson {}); } } } } else { ob.write_null(property.offset, property.data_type); } } Ok(()) } fn value_to_bool(value: &Value) -> Result> { if value.is_null() { return Ok(None); } else if let Some(value) = value.as_bool() { return Ok(Some(value)); }; Err(IsarError::InvalidJson {}) } fn value_to_byte(value: &Value) -> Result { if value.is_null() { return Ok(IsarObject::NULL_BYTE); } else if let Some(value) = value.as_i64() { if value >= 0 && value <= u8::MAX as i64 { return Ok(value as u8); } } Err(IsarError::InvalidJson {}) } fn value_to_int(value: &Value) -> Result { if value.is_null() { return Ok(IsarObject::NULL_INT); } else if let Some(value) = value.as_i64() { if value >= i32::MIN as i64 && value <= i32::MAX as i64 { return Ok(value as i32); } } Err(IsarError::InvalidJson {}) } fn value_to_float(value: &Value) -> Result { if value.is_null() { return Ok(IsarObject::NULL_FLOAT); } else if let Some(value) = value.as_f64() { if value >= f32::MIN as f64 && value <= f32::MAX as f64 { return Ok(value as f32); } } Err(IsarError::InvalidJson {}) } fn value_to_long(value: &Value) -> Result { if value.is_null() { Ok(IsarObject::NULL_LONG) } else if let Some(value) = value.as_i64() { Ok(value) } else { Err(IsarError::InvalidJson {}) } } fn value_to_double(value: &Value) -> Result { if value.is_null() { Ok(IsarObject::NULL_DOUBLE) } else if let Some(value) = value.as_f64() { Ok(value) } else { Err(IsarError::InvalidJson {}) } } fn value_to_string(value: &Value) -> Result> { if value.is_null() { Ok(None) } else if let Some(value) = value.as_str() { Ok(Some(value)) } else { Err(IsarError::InvalidJson {}) } } fn value_to_object( value: &Value, embedded_properties: &IntMap>, target_id: u64, ) -> Result> { if value.is_null() { Ok(None) } else { let properties = embedded_properties.get(target_id).unwrap(); let mut embedded_ob = ObjectBuilder::new(properties, None); Self::decode(properties, embedded_properties, &mut embedded_ob, value)?; Ok(Some(embedded_ob)) } } fn value_to_array(value: &Value, convert: F) -> Result>> where F: Fn(&Value) -> Result, { if value.is_null() { Ok(None) } else if let Some(value) = value.as_array() { let array: Result> = value.iter().map(convert).collect(); Ok(Some(array?)) } else { Err(IsarError::InvalidJson {}) } } } ================================================ FILE: packages/isar_core/src/object/mod.rs ================================================ pub mod data_type; pub mod id; pub mod isar_object; pub mod json_encode_decode; pub mod object_builder; pub mod property; ================================================ FILE: packages/isar_core/src/object/object_builder.rs ================================================ use byteorder::{ByteOrder, LittleEndian}; use itertools::Itertools; use crate::object::data_type::DataType; use crate::object::isar_object::IsarObject; use std::slice::from_raw_parts; use super::property::Property; /* u16 static properties size --- Static properties --- i64 field1 u24 field2_offset or 0 f32 field3 u24 field4_offset or 0 --- Dynamic data --- field2_offset: u24 field2_length ... field2_data ... field4_offset: u24 field4_length u24 field4_item1_length + 1 or 0 u24 field4_item2_length + 1 or 0 ... field4_item1 ... ... field4_item2 ... */ pub struct ObjectBuilder { buffer: Vec, dynamic_offset: usize, } impl ObjectBuilder { pub fn new(properties: &[Property], buffer: Option>) -> ObjectBuilder { let static_size = properties .iter() .max_by_key(|p| p.offset) .map_or(0, |p| p.offset + p.data_type.get_static_size()); let mut buffer = buffer.unwrap_or_else(|| Vec::with_capacity(2 + static_size * 2)); buffer.clear(); let mut ob = ObjectBuilder { buffer, dynamic_offset: static_size, }; ob.write_at(0, &(static_size as u16).to_le_bytes()); ob } #[inline] fn write_at(&mut self, offset: usize, bytes: &[u8]) { if offset + bytes.len() > self.buffer.len() { self.buffer.resize(offset + bytes.len(), 0); } self.buffer[offset..offset + bytes.len()].copy_from_slice(bytes); } #[inline] fn write_u24(&mut self, offset: usize, value: usize) { if offset + 3 > self.buffer.len() { self.buffer.resize(offset + 3, 0); } LittleEndian::write_u24(&mut self.buffer[offset..], value as u32); } pub fn write_null(&mut self, offset: usize, data_type: DataType) { match data_type { DataType::Bool => self.write_bool(offset, None), DataType::Byte => self.write_byte(offset, IsarObject::NULL_BYTE), DataType::Int => self.write_int(offset, IsarObject::NULL_INT), DataType::Float => self.write_float(offset, IsarObject::NULL_FLOAT), DataType::Long => self.write_long(offset, IsarObject::NULL_LONG), DataType::Double => self.write_double(offset, IsarObject::NULL_DOUBLE), DataType::String => self.write_string(offset, None), DataType::Object => self.write_object(offset, None), DataType::BoolList => self.write_bool_list(offset, None), DataType::ByteList => self.write_byte_list(offset, None), DataType::IntList => self.write_int_list(offset, None), DataType::FloatList => self.write_float_list(offset, None), DataType::LongList => self.write_long_list(offset, None), DataType::DoubleList => self.write_double_list(offset, None), DataType::StringList => self.write_string_list(offset, None), DataType::ObjectList => self.write_object_list(offset, None), } } pub fn bool_to_byte(value: Option) -> u8 { if let Some(value) = value { if value { IsarObject::TRUE_BOOL } else { IsarObject::FALSE_BOOL } } else { IsarObject::NULL_BOOL } } pub fn write_byte(&mut self, offset: usize, value: u8) { self.write_at(offset, &[value]); } pub fn write_bool(&mut self, offset: usize, value: Option) { let value = Self::bool_to_byte(value); self.write_at(offset, &[value]); } pub fn write_int(&mut self, offset: usize, value: i32) { self.write_at(offset, &value.to_le_bytes()); } pub fn write_float(&mut self, offset: usize, value: f32) { self.write_at(offset, &value.to_le_bytes()); } pub fn write_long(&mut self, offset: usize, value: i64) { self.write_at(offset, &value.to_le_bytes()); } pub fn write_double(&mut self, offset: usize, value: f64) { self.write_at(offset, &value.to_le_bytes()); } pub fn write_string(&mut self, offset: usize, value: Option<&str>) { let bytes = value.map(|s| s.as_ref()); self.write_list(offset, bytes); } pub fn write_object(&mut self, offset: usize, value: Option) { self.write_list(offset, value.as_ref().map(|o| o.as_bytes())); } pub fn write_bool_list(&mut self, offset: usize, value: Option<&[Option]>) { let list = value.map(|list| list.iter().map(|b| Self::bool_to_byte(*b)).collect_vec()); self.write_list(offset, list.as_deref()); } pub fn write_byte_list(&mut self, offset: usize, value: Option<&[u8]>) { self.write_list(offset, value); } pub fn write_int_list(&mut self, offset: usize, value: Option<&[i32]>) { self.write_list(offset, value); } pub fn write_float_list(&mut self, offset: usize, value: Option<&[f32]>) { self.write_list(offset, value); } pub fn write_long_list(&mut self, offset: usize, value: Option<&[i64]>) { self.write_list(offset, value); } pub fn write_double_list(&mut self, offset: usize, value: Option<&[f64]>) { self.write_list(offset, value); } pub fn write_string_list(&mut self, offset: usize, value: Option<&[Option<&str>]>) { self.write_list_list(offset, value, |v| v.as_bytes()) } pub fn write_object_list(&mut self, offset: usize, value: Option<&[Option]>) { self.write_list_list(offset, value, |v| v.as_bytes()) } fn write_list(&mut self, offset: usize, list: Option<&[T]>) { if let Some(list) = list { let bytes = Self::get_list_bytes(list); self.write_u24(offset, self.dynamic_offset); self.write_u24(self.dynamic_offset, list.len()); self.write_at(self.dynamic_offset + 3, bytes); self.dynamic_offset += bytes.len() + 3; } else { self.write_u24(offset, 0); } } fn write_list_list( &mut self, offset: usize, value: Option<&[Option]>, to_bytes: impl Fn(&T) -> &[u8], ) { if let Some(value) = value { self.write_u24(offset, self.dynamic_offset); self.write_u24(self.dynamic_offset, value.len()); let mut offset_list_offset = self.dynamic_offset + 3; self.dynamic_offset += 3 + value.len() * 3; for v in value { if let Some(bytes) = v.as_ref().map(|v| to_bytes(v)) { self.write_u24(offset_list_offset, bytes.len() + 1); self.write_at(self.dynamic_offset, bytes); self.dynamic_offset += bytes.len(); } else { self.write_u24(offset_list_offset, 0); } offset_list_offset += 3; } } else { self.write_u24(offset, 0); } } #[inline] pub(crate) fn get_list_bytes(list: &[T]) -> &[u8] { let type_size = std::mem::size_of::(); let ptr = list.as_ptr() as *const T; unsafe { from_raw_parts::(ptr as *const u8, list.len() * type_size) } } pub fn finish(&self) -> IsarObject { IsarObject::from_bytes(&self.buffer) } pub fn recycle(self) -> Vec { let mut buffer = self.buffer; buffer.clear(); buffer } } #[cfg(test)] mod tests { use super::ObjectBuilder; use crate::object::data_type::DataType::{self, *}; use crate::object::isar_object::IsarObject; use crate::object::property::Property; macro_rules! builder { ($var:ident, $prop:ident, $type:ident) => { let $prop = Property::debug($type, 3); let props = vec![Property::debug(Byte, 2), $prop.clone()]; let mut $var = ObjectBuilder::new(&props, None); $var.write_byte(2, 255); }; } fn offset_size(value: usize) -> [u8; 3] { let mut bytes = [0; 3]; bytes[2] = (value >> 16) as u8; bytes[1] = (value >> 8) as u8; bytes[0] = value as u8; bytes } #[test] pub fn test_write_null() { builder!(b, p, Bool); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, 0]); builder!(b, p, Byte); b.write_null(p.offset, p.data_type); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, 0]); builder!(b, p, Int); b.write_null(p.offset, p.data_type); let mut bytes = vec![7, 0, 255]; bytes.extend_from_slice(&IsarObject::NULL_INT.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, Float); b.write_null(p.offset, p.data_type); let mut bytes = vec![7, 0, 255]; bytes.extend_from_slice(&IsarObject::NULL_FLOAT.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, Long); b.write_null(p.offset, p.data_type); let mut bytes = vec![11, 0, 255]; bytes.extend_from_slice(&IsarObject::NULL_LONG.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, Double); b.write_null(p.offset, p.data_type); let mut bytes = vec![11, 0, 255]; bytes.extend_from_slice(&IsarObject::NULL_DOUBLE.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); let list_types = vec![ String, Object, ByteList, IntList, FloatList, LongList, DoubleList, StringList, ObjectList, ]; for list_type in list_types { builder!(b, p, list_type); b.write_null(p.offset, p.data_type); let bytes = vec![6, 0, 255, 0, 0, 0]; assert_eq!(b.finish().as_bytes(), &bytes); } } #[test] pub fn test_write_bool() { builder!(b, p, Bool); b.write_bool(p.offset, Some(true)); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, IsarObject::TRUE_BOOL]); builder!(b, p, Bool); b.write_bool(p.offset, Some(false)); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, IsarObject::FALSE_BOOL]); builder!(b, p, Bool); b.write_bool(p.offset, None); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, IsarObject::NULL_BOOL]); } #[test] pub fn test_write_byte() { builder!(b, p, Byte); b.write_byte(p.offset, 0); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, 0]); builder!(b, p, Byte); b.write_byte(p.offset, 123); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, 123]); builder!(b, p, Byte); b.write_byte(p.offset, 255); assert_eq!(b.finish().as_bytes(), &[4, 0, 255, 255]); } #[test] pub fn test_write_int() { builder!(b, p, Int); b.write_int(p.offset, 123); assert_eq!(b.finish().as_bytes(), &[7, 0, 255, 123, 0, 0, 0]) } #[test] pub fn test_write_float() { builder!(b, p, Float); b.write_float(p.offset, 123.123); let mut bytes = vec![7, 0, 255]; bytes.extend_from_slice(&123.123f32.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, Float); b.write_float(p.offset, f32::NAN); let mut bytes = vec![7, 0, 255]; bytes.extend_from_slice(&f32::NAN.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_long() { builder!(b, p, Long); b.write_long(p.offset, 123123); let mut bytes = vec![11, 0, 255]; bytes.extend_from_slice(&123123i64.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes) } #[test] pub fn test_write_double() { builder!(b, p, Double); b.write_double(p.offset, 123.123); let mut bytes = vec![11, 0, 255]; bytes.extend_from_slice(&123.123f64.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, Double); b.write_double(p.offset, f64::NAN); let mut bytes = vec![11, 0, 255]; bytes.extend_from_slice(&f64::NAN.to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_string() { builder!(b, p, String); b.write_string(p.offset, Some("hello")); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(5)); bytes.extend_from_slice(b"hello"); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, String); b.write_string(p.offset, Some("")); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, String); b.write_string(p.offset, None); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_object() { builder!(b, p, Object); let object = IsarObject::from_bytes(&[3, 0, 111]); b.write_object(p.offset, Some(object)); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(3)); bytes.extend_from_slice(&[3, 0, 111]); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_multiple_static_types() { let props = vec![ Property::debug(DataType::Long, 2), Property::debug(DataType::Byte, 10), Property::debug(DataType::Int, 11), Property::debug(DataType::Float, 15), Property::debug(DataType::Long, 19), Property::debug(DataType::Double, 27), ]; let mut b = ObjectBuilder::new(&props, None); b.write_long(props.get(0).unwrap().offset, 1); b.write_byte(props.get(1).unwrap().offset, u8::MAX); b.write_int(props.get(2).unwrap().offset, i32::MAX); b.write_float(props.get(3).unwrap().offset, std::f32::consts::E); b.write_long(props.get(4).unwrap().offset, i64::MIN); b.write_double(props.get(5).unwrap().offset, std::f64::consts::PI); let mut bytes = vec![35, 0, 1, 0, 0, 0, 0, 0, 0, 0]; bytes.push(u8::MAX); bytes.extend_from_slice(&i32::MAX.to_le_bytes()); bytes.extend_from_slice(&std::f32::consts::E.to_le_bytes()); bytes.extend_from_slice(&i64::MIN.to_le_bytes()); bytes.extend_from_slice(&std::f64::consts::PI.to_le_bytes()); assert_eq!(b.finish().as_bytes(), bytes); } #[test] pub fn test_write_byte_list() { builder!(b, p, ByteList); b.write_byte_list(p.offset, Some(&[1, 2, 3])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(3)); bytes.extend_from_slice(&[1, 2, 3]); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, ByteList); b.write_byte_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_int_list() { builder!(b, p, IntList); b.write_int_list(p.offset, Some(&[1, -10])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(2)); bytes.extend_from_slice(&1i32.to_le_bytes()); bytes.extend_from_slice(&(-10i32).to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, IntList); b.write_int_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_float_list() { builder!(b, p, FloatList); b.write_float_list(p.offset, Some(&[1.1, -10.10])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(2)); bytes.extend_from_slice(&1.1f32.to_le_bytes()); bytes.extend_from_slice(&(-10.10f32).to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, FloatList); b.write_float_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_long_list() { builder!(b, p, LongList); b.write_long_list(p.offset, Some(&[1, -10])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(2)); bytes.extend_from_slice(&1i64.to_le_bytes()); bytes.extend_from_slice(&(-10i64).to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, LongList); b.write_long_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_double_list() { builder!(b, p, DoubleList); b.write_double_list(p.offset, Some(&[1.1, -10.10])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(2)); bytes.extend_from_slice(&1.1f64.to_le_bytes()); bytes.extend_from_slice(&(-10.10f64).to_le_bytes()); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, DoubleList); b.write_double_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_string_list() { builder!(b, p, StringList); b.write_string_list(p.offset, Some(&[Some("abc"), None, Some(""), Some("de")])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(4)); bytes.extend_from_slice(&offset_size(4)); bytes.extend_from_slice(&offset_size(0)); bytes.extend_from_slice(&offset_size(1)); bytes.extend_from_slice(&offset_size(3)); bytes.extend_from_slice(b"abcde"); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, StringList); b.write_string_list(p.offset, Some(&[None])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(1)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, StringList); b.write_string_list(p.offset, Some(&[Some("")])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(1)); bytes.extend_from_slice(&offset_size(1)); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, StringList); b.write_string_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } #[test] pub fn test_write_object_list() { builder!(b, p, ObjectList); let object1 = IsarObject::from_bytes(&[2, 0]); let object2 = IsarObject::from_bytes(&[3, 0, 123]); b.write_object_list(p.offset, Some(&[Some(object1), None, Some(object2)])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(3)); bytes.extend_from_slice(&offset_size(3)); bytes.extend_from_slice(&offset_size(0)); bytes.extend_from_slice(&offset_size(4)); bytes.extend_from_slice(&[2, 0, 3, 0, 123]); assert_eq!(b.finish().as_bytes(), &bytes); builder!(b, p, ObjectList); b.write_object_list(p.offset, Some(&[])); let mut bytes = vec![6, 0, 255]; bytes.extend_from_slice(&offset_size(6)); bytes.extend_from_slice(&offset_size(0)); assert_eq!(b.finish().as_bytes(), &bytes); } } ================================================ FILE: packages/isar_core/src/object/property.rs ================================================ use xxhash_rust::xxh3::xxh3_64; use super::data_type::DataType; #[derive(Clone, Eq, PartialEq)] pub struct Property { pub name: String, pub data_type: DataType, pub offset: usize, pub target_id: Option, } impl Property { pub fn new(name: &str, data_type: DataType, offset: usize, target_id: Option<&str>) -> Self { let target_id = target_id.map(|col| xxh3_64(col.as_bytes())); Property { name: name.to_string(), data_type, offset, target_id, } } pub const fn debug(data_type: DataType, offset: usize) -> Self { Property { name: String::new(), data_type, offset, target_id: None, } } } ================================================ FILE: packages/isar_core/src/query/fast_wild_match.rs ================================================ const ASTERISK: u8 = 42; const QUESTION_MARK: u8 = 63; pub(crate) fn fast_wild_match(tame: &str, wild: &str) -> bool { let wild = wild.as_bytes(); let tame = tame.as_bytes(); let mut i_wild = 0; let mut i_tame = 0; let mut i_last = 0; let mut i_star = 0; while tame.get(i_tame).is_some() { match wild.get(i_wild) { Some(&QUESTION_MARK) => { i_tame += 1; i_wild += 1; continue; } Some(&ASTERISK) => { loop { i_wild += 1; if wild.get(i_wild) != Some(&ASTERISK) { break; } } if wild.get(i_wild).is_none() { return true; } i_star = i_wild; } _ => { if tame.get(i_tame) == wild.get(i_wild) { i_tame += 1; i_wild += 1; continue; } if i_star == 0 { return false; } i_wild = i_star; i_tame = i_last + 1; } } while tame.get(i_tame) != wild.get(i_wild) && wild.get(i_wild) != Some(&QUESTION_MARK) { i_tame += 1; if tame.get(i_tame).is_none() { return false; } } i_last = i_tame; i_tame += 1; i_wild += 1; } while wild.get(i_wild) == Some(&ASTERISK) { i_wild += 1; } wild.get(i_wild).is_none() } #[cfg(test)] mod tests { use crate::query::fast_wild_match::fast_wild_match; #[test] fn test_wild() { let wild_cases = vec![ ("Hi", "Hi*", true), ("abc", "ab*d", false), ("abcccd", "*ccd", true), ("mississipissippi", "*issip*ss*", true), ("xxxx*zzzzzzzzy*f", "xxxx*zzy*fffff", false), ("xxxx*zzzzzzzzy*f", "xxx*zzy*f", true), ("xxxxzzzzzzzzyf", "xxxx*zzy*fffff", false), ("xxxxzzzzzzzzyf", "xxxx*zzy*f", true), ("xyxyxyzyxyz", "xy*z*xyz", true), ("mississippi", "*sip*", true), ("xyxyxyxyz", "xy*xyz", true), ("mississippi", "mi*sip*", true), ("ababac", "*abac*", true), ("ababac", "*abac*", true), ("aaazz", "a*zz*", true), ("a12b12", "*12*23", false), ("a12b12", "a12b", false), ("a12b12", "*12*12*", true), ("caaab", "*a?b", true), ("*", "*", true), ("a*abab", "a*b", true), ("a*r", "a*", true), ("a*ar", "a*aar", false), ("XYXYXYZYXYz", "XY*Z*XYz", true), ("missisSIPpi", "*SIP*", true), ("mississipPI", "*issip*PI", true), ("xyxyxyxyz", "xy*xyz", true), ("miSsissippi", "mi*sip*", true), ("miSsissippi", "mi*Sip*", false), ("abAbac", "*Abac*", true), ("abAbac", "*Abac*", true), ("aAazz", "a*zz*", true), ("A12b12", "*12*23", false), ("a12B12", "*12*12*", true), ("oWn", "*oWn*", true), ("bLah", "bLah", true), ("bLah", "bLaH", false), ("a", "*?", true), ("ab", "*?", true), ("abc", "*?", true), ("a", "??", false), ("ab", "?*?", true), ("ab", "*?*?*", true), ("abc", "?**?*?", true), ("abc", "?**?*&?", false), ("abcd", "?b*??", true), ("abcd", "?a*??", false), ("abcd", "?**?c?", true), ("abcd", "?**?d?", false), ("abcde", "?*b*?*d*?", true), ("bLah", "bL?h", true), ("bLaaa", "bLa?", false), ("bLah", "bLa?", true), ("bLaH", "?Lah", false), ("bLaH", "?LaH", true), ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab", "a*a*a*a*a*a*aa*aaa*a*a*b", true), ("abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "*a*b*ba*ca*a*aa*aaa*fa*ga*b*", true), ("abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "*a*b*ba*ca*a*x*aaa*fa*ga*b*", false), ("abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "*a*b*ba*ca*aaaa*fa*ga*gggg*b*", false), ("abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "*a*b*ba*ca*aaaa*fa*ga*ggg*b*", true), ("aaabbaabbaab", "*aabbaa*a*", true), ("a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", "a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", true), ("aaaaaaaaaaaaaaaaa", "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", true), ("aaaaaaaaaaaaaaaa", "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*", false), ("abc*abcd*abcde*abcdef*abcdefg*abcdefgh*abcdefghi*abcdefghij*abcdefghijk*abcdefghijkl*abcdefghijklm*abcdefghijklmn", "abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*", false), ("abc*abcd*abcde*abcdef*abcdefg*abcdefgh*abcdefghi*abcdefghij*abcdefghijk*abcdefghijkl*abcdefghijklm*abcdefghijklmn", "abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*", true), ("abc*abcd*abcd*abc*abcd", "abc*abc*abc*abc*abc", false), ("abc*abcd*abcd*abc*abcd*abcd*abc*abcd*abc*abc*abcd", "abc*abc*abc*abc*abc*abc*abc*abc*abc*abc*abcd", true), ("abc", "********a********b********c********", true), ("********a********b********c********", "abc", false), ("abc", "********a********b********b********", false), ("*abc*", "***a*b*c***", true), ("", "?", false), ("", "*?", false), ("", "", true), ("", "*", true), ("a", "", false), ]; for (tame, wild, result) in wild_cases { assert_eq!(fast_wild_match(tame, wild), result); } } #[test] fn test_tame() { let tame_cases = vec![ ("abc", "abd", false), ("abcccd", "abcccd", true), ("mississipissippi", "mississipissippi", true), ("xxxxzzzzzzzzyf", "xxxxzzzzzzzzyfffff", false), ("xxxxzzzzzzzzyf", "xxxxzzzzzzzzyf", true), ("xxxxzzzzzzzzyf", "xxxxzzy.fffff", false), ("xxxxzzzzzzzzyf", "xxxxzzzzzzzzyf", true), ("xyxyxyzyxyz", "xyxyxyzyxyz", true), ("mississippi", "mississippi", true), ("xyxyxyxyz", "xyxyxyxyz", true), ("m ississippi", "m ississippi", true), ("ababac", "ababac?", false), ("dababac", "ababac", false), ("aaazz", "aaazz", true), ("a12b12", "1212", false), ("a12b12", "a12b", false), ("a12b12", "a12b12", true), ("n", "n", true), ("aabab", "aabab", true), ("ar", "ar", true), ("aar", "aaar", false), ("XYXYXYZYXYz", "XYXYXYZYXYz", true), ("missisSIPpi", "missisSIPpi", true), ("mississipPI", "mississipPI", true), ("xyxyxyxyz", "xyxyxyxyz", true), ("miSsissippi", "miSsissippi", true), ("miSsissippi", "miSsisSippi", false), ("abAbac", "abAbac", true), ("abAbac", "abAbac", true), ("aAazz", "aAazz", true), ("A12b12", "A12b123", false), ("a12B12", "a12B12", true), ("oWn", "oWn", true), ("bLah", "bLah", true), ("bLah", "bLaH", false), ("a", "a", true), ("ab", "a?", true), ("abc", "ab?", true), ("a", "??", false), ("ab", "??", true), ("abc", "???", true), ("abcd", "????", true), ("abc", "????", false), ("abcd", "?b??", true), ("abcd", "?a??", false), ("abcd", "??c?", true), ("abcd", "??d?", false), ("abcde", "?b?d*?", true), ( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab", true, ), ( "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", true, ), ( "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "abababababababababababababababababababaacacacacacacacadaeafagahaiajaxalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", false, ), ( "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaggggagaaaaaaaab", false, ), ( "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", "abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab", true, ), ("aaabbaabbaab", "aaabbaabbaab", true), ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true, ), ("aaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaa", true), ("aaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaa", false), ( "abcabcdabcdeabcdefabcdefgabcdefghabcdefghiabcdefghijabcdefghijkabcdefghijklabcdefghijklmabcdefghijklmn", "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc", false, ), ( "abcabcdabcdeabcdefabcdefgabcdefghabcdefghiabcdefghijabcdefghijkabcdefghijklabcdefghijklmabcdefghijklmn", "abcabcdabcdeabcdefabcdefgabcdefghabcdefghiabcdefghijabcdefghijkabcdefghijklabcdefghijklmabcdefghijklmn", true, ), ("abcabcdabcdabcabcd", "abcabc?abcabcabc", false), ( "abcabcdabcdabcabcdabcdabcabcdabcabcabcd", "abcabc?abc?abcabc?abc?abc?bc?abc?bc?bcd", true, ), ("?abc?", "?abc?", true), ]; for (tame, wild, result) in tame_cases { assert_eq!(fast_wild_match(tame, wild), result); } } #[test] fn test_empty() { let empty_cases = vec![ ("", "abd", false), ("", "abcccd", false), ("", "mississipissippi", false), ("", "xxxxzzzzzzzzyfffff", false), ("", "xxxxzzzzzzzzyf", false), ("", "xxxxzzy.fffff", false), ("", "xxxxzzzzzzzzyf", false), ("", "xyxyxyzyxyz", false), ("", "mississippi", false), ("", "xyxyxyxyz", false), ("", "m ississippi", false), ("", "ababac*", false), ("", "ababac", false), ("", "aaazz", false), ("", "1212", false), ("", "a12b", false), ("", "a12b12", false), // A mix of cases ("", "n", false), ("", "aabab", false), ("", "ar", false), ("", "aaar", false), ("", "XYXYXYZYXYz", false), ("", "missisSIPpi", false), ("", "mississipPI", false), ("", "xyxyxyxyz", false), ("", "miSsissippi", false), ("", "miSsisSippi", false), ("", "abAbac", false), ("", "abAbac", false), ("", "aAazz", false), ("", "A12b123", false), ("", "a12B12", false), ("", "oWn", false), ("", "bLah", false), ("", "bLaH", false), ("", "", true), ("abc", "", false), ("abcccd", "", false), ("mississipissippi", "", false), ("xxxxzzzzzzzzyf", "", false), ("xxxxzzzzzzzzyf", "", false), ("xxxxzzzzzzzzyf", "", false), ("xxxxzzzzzzzzyf", "", false), ("xyxyxyzyxyz", "", false), ("mississippi", "", false), ("xyxyxyxyz", "", false), ("m ississippi", "", false), ("ababac", "", false), ("dababac", "", false), ("aaazz", "", false), ("a12b12", "", false), ("a12b12", "", false), ("a12b12", "", false), ("n", "", false), ("aabab", "", false), ("ar", "", false), ("aar", "", false), ("XYXYXYZYXYz", "", false), ("missisSIPpi", "", false), ("mississipPI", "", false), ("xyxyxyxyz", "", false), ("miSsissippi", "", false), ("miSsissippi", "", false), ("abAbac", "", false), ("abAbac", "", false), ("aAazz", "", false), ("A12b12", "", false), ("a12B12", "", false), ("oWn", "", false), ("bLah", "", false), ("bLah", "", false), ]; for (tame, wild, result) in empty_cases { assert_eq!(fast_wild_match(tame, wild), result); } } } ================================================ FILE: packages/isar_core/src/query/filter.rs ================================================ use crate::collection::IsarCollection; use crate::cursor::IsarCursors; use crate::error::{illegal_arg, Result}; use crate::link::IsarLink; use crate::object::data_type::DataType; use crate::object::isar_object::IsarObject; use crate::object::property::Property; use crate::query::fast_wild_match::fast_wild_match; use enum_dispatch::enum_dispatch; use itertools::Itertools; use paste::paste; #[macro_export] macro_rules! primitive_create { ($data_type:ident, $property:expr, $lower:expr, $upper:expr) => { paste! { if $property.data_type == DataType::$data_type || ($property.data_type == DataType::Bool && DataType::$data_type == DataType::Byte) { Ok(Filter( FilterCond::[<$data_type Between>]([<$data_type BetweenCond>] { offset: $property.offset, $lower, $upper, }) )) } else if $property.data_type == DataType::[<$data_type List>] || ($property.data_type == DataType::BoolList && DataType::[<$data_type List>] == DataType::ByteList) { Ok(Filter( FilterCond::[]([] { offset: $property.offset, $lower, $upper, }) )) } else { illegal_arg("Property does not support this filter.") } } }; } #[macro_export] macro_rules! string_filter_create { ($name:ident, $property:expr, $value:expr, $case_sensitive:expr) => { paste! { { let value = if $case_sensitive { $value.to_string() } else { $value.to_lowercase() }; let filter_cond = if $property.data_type == DataType::String { Ok(FilterCond::[]([] { offset: $property.offset, value, $case_sensitive, })) } else if $property.data_type == DataType::StringList { Ok(FilterCond::[]([] { offset: $property.offset, value, $case_sensitive, })) } else { illegal_arg("Property does not support this filter.") }?; Ok(Filter(filter_cond)) } } }; } #[derive(Clone)] pub struct Filter(FilterCond); impl Filter { pub fn id(lower: i64, upper: i64) -> Filter { let filter_cond = FilterCond::IdBetween(IdBetweenCond { lower, upper }); Filter(filter_cond) } pub fn byte(property: &Property, lower: u8, upper: u8) -> Result { primitive_create!(Byte, property, lower, upper) } pub fn int(property: &Property, lower: i32, upper: i32) -> Result { primitive_create!(Int, property, lower, upper) } pub fn long(property: &Property, lower: i64, upper: i64) -> Result { primitive_create!(Long, property, lower, upper) } pub fn float(property: &Property, lower: f32, upper: f32) -> Result { primitive_create!(Float, property, lower, upper) } pub fn double(property: &Property, lower: f64, upper: f64) -> Result { primitive_create!(Double, property, lower, upper) } pub fn string_to_bytes(str: Option<&str>, case_sensitive: bool) -> Option> { if case_sensitive { str.map(|s| s.as_bytes().to_vec()) } else { str.map(|s| s.to_lowercase().as_bytes().to_vec()) } } pub fn string( property: &Property, lower: Option<&str>, upper: Option<&str>, case_sensitive: bool, ) -> Result { Self::byte_string( property, Self::string_to_bytes(lower, case_sensitive), Self::string_to_bytes(upper, case_sensitive), case_sensitive, ) } pub fn byte_string( property: &Property, lower: Option>, upper: Option>, case_sensitive: bool, ) -> Result { let filter_cond = if property.data_type == DataType::String { Ok(FilterCond::StringBetween(StringBetweenCond { offset: property.offset, lower, upper, case_sensitive, })) } else if property.data_type == DataType::StringList { Ok(FilterCond::AnyStringBetween(AnyStringBetweenCond { offset: property.offset, lower, upper, case_sensitive, })) } else { illegal_arg("Property does not support this filter.") }?; Ok(Filter(filter_cond)) } pub fn string_starts_with( property: &Property, value: &str, case_sensitive: bool, ) -> Result { string_filter_create!(StartsWith, property, value, case_sensitive) } pub fn string_ends_with( property: &Property, value: &str, case_sensitive: bool, ) -> Result { string_filter_create!(EndsWith, property, value, case_sensitive) } pub fn string_contains( property: &Property, value: &str, case_sensitive: bool, ) -> Result { string_filter_create!(Contains, property, value, case_sensitive) } pub fn string_matches( property: &Property, value: &str, case_sensitive: bool, ) -> Result { string_filter_create!(Matches, property, value, case_sensitive) } pub fn list_length(property: &Property, lower: usize, upper: usize) -> Result { let filter_cond = if property.data_type.get_element_type().is_some() { Ok(FilterCond::ListLength(ListLengthCond { offset: property.offset, lower, upper, })) } else { illegal_arg("Property does not support this filter.") }?; Ok(Filter(filter_cond)) } pub fn null(property: &Property) -> Filter { let filter_cond = FilterCond::Null(NullCond { offset: property.offset, data_type: property.data_type, }); Filter(filter_cond) } pub fn and(filters: Vec) -> Filter { let filters = filters.into_iter().map(|f| f.0).collect_vec(); let filter_cond = FilterCond::And(AndCond { filters }); Filter(filter_cond) } pub fn or(filters: Vec) -> Filter { let filters = filters.into_iter().map(|f| f.0).collect_vec(); let filter_cond = FilterCond::Or(OrCond { filters }); Filter(filter_cond) } pub fn xor(filters: Vec) -> Filter { let filters = filters.into_iter().map(|f| f.0).collect_vec(); let filter_cond = FilterCond::Xor(XorCond { filters }); Filter(filter_cond) } pub fn not(filter: Filter) -> Filter { let filter_cond = FilterCond::Not(NotCond { filter: Box::new(filter.0), }); Filter(filter_cond) } pub fn stat(value: bool) -> Filter { let filter_cond = FilterCond::Static(StaticCond { value }); Filter(filter_cond) } pub fn object(property: &Property, filter: Option) -> Result { let filter_cond = if property.data_type == DataType::Object { if let Some(filter) = filter { Ok(FilterCond::Object(ObjectCond { offset: property.offset, filter: Box::new(filter.0), })) } else { Ok(FilterCond::Null(NullCond { offset: property.offset, data_type: DataType::Object, })) } } else if property.data_type == DataType::ObjectList { Ok(FilterCond::AnyObject(AnyObjectCond { offset: property.offset, filter: filter.map(|f| Box::new(f.0)), })) } else { illegal_arg("Property does not support this filter.") }?; Ok(Filter(filter_cond)) } pub fn link(collection: &IsarCollection, link_id: u64, filter: Filter) -> Result { let link = collection.get_link_backlink(link_id)?.clone(); let filter_cond = FilterCond::AnyLink(AnyLinkCond { link, filter: Box::new(filter.0), }); Ok(Filter(filter_cond)) } pub fn link_length( collection: &IsarCollection, link_id: u64, lower: usize, upper: usize, ) -> Result { let link = collection.get_link_backlink(link_id)?.clone(); let filter_cond = FilterCond::LinkLength(LinkLengthCond { link, lower, upper }); Ok(Filter(filter_cond)) } pub(crate) fn evaluate( &self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>, ) -> Result { self.0.evaluate(id, object, cursors) } } #[enum_dispatch] #[derive(Clone)] enum FilterCond { IdBetween(IdBetweenCond), ByteBetween(ByteBetweenCond), IntBetween(IntBetweenCond), LongBetween(LongBetweenCond), FloatBetween(FloatBetweenCond), DoubleBetween(DoubleBetweenCond), StringBetween(StringBetweenCond), StringStartsWith(StringStartsWithCond), StringEndsWith(StringEndsWithCond), StringContains(StringContainsCond), StringMatches(StringMatchesCond), AnyByteBetween(AnyByteBetweenCond), AnyIntBetween(AnyIntBetweenCond), AnyLongBetween(AnyLongBetweenCond), AnyFloatBetween(AnyFloatBetweenCond), AnyDoubleBetween(AnyDoubleBetweenCond), AnyStringBetween(AnyStringBetweenCond), AnyStringStartsWith(AnyStringStartsWithCond), AnyStringEndsWith(AnyStringEndsWithCond), AnyStringContains(AnyStringContainsCond), AnyStringMatches(AnyStringMatchesCond), ListLength(ListLengthCond), Null(NullCond), And(AndCond), Or(OrCond), Xor(XorCond), Not(NotCond), Static(StaticCond), Object(ObjectCond), AnyObject(AnyObjectCond), AnyLink(AnyLinkCond), LinkLength(LinkLengthCond), } #[enum_dispatch(FilterCond)] trait Condition { fn evaluate(&self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>) -> Result; } #[derive(Clone)] struct IdBetweenCond { lower: i64, upper: i64, } impl Condition for IdBetweenCond { fn evaluate(&self, id: i64, _object: IsarObject, _: Option<&IsarCursors>) -> Result { Ok(self.lower <= id && self.upper >= id) } } #[macro_export] macro_rules! filter_between_struct { ($name:ident, $data_type:ident, $type:ty) => { #[derive(Clone)] struct $name { upper: $type, lower: $type, offset: usize, } }; } #[macro_export] macro_rules! primitive_filter_between { ($name:ident, $prop_accessor:ident) => { impl Condition for $name { fn evaluate( &self, _id: i64, object: IsarObject, _: Option<&IsarCursors>, ) -> Result { let val = object.$prop_accessor(self.offset); Ok(self.lower <= val && self.upper >= val) } } }; } filter_between_struct!(ByteBetweenCond, Byte, u8); primitive_filter_between!(ByteBetweenCond, read_byte); filter_between_struct!(IntBetweenCond, Int, i32); primitive_filter_between!(IntBetweenCond, read_int); filter_between_struct!(LongBetweenCond, Long, i64); primitive_filter_between!(LongBetweenCond, read_long); #[macro_export] macro_rules! primitive_filter_between_list { ($name:ident, $prop_accessor:ident) => { impl Condition for $name { fn evaluate( &self, _id: i64, object: IsarObject, _: Option<&IsarCursors>, ) -> Result { let vals = object.$prop_accessor(self.offset); if let Some(vals) = vals { for val in vals { if self.lower <= val && self.upper >= val { return Ok(true); } } } Ok(false) } } }; } filter_between_struct!(AnyByteBetweenCond, Byte, u8); impl Condition for AnyByteBetweenCond { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let vals = object.read_byte_list(self.offset); if let Some(vals) = vals { for val in vals { if self.lower <= *val && self.upper >= *val { return Ok(true); } } } Ok(false) } } filter_between_struct!(AnyIntBetweenCond, Int, i32); primitive_filter_between_list!(AnyIntBetweenCond, read_int_list); filter_between_struct!(AnyLongBetweenCond, Long, i64); primitive_filter_between_list!(AnyLongBetweenCond, read_long_list); #[macro_export] macro_rules! float_filter_between { ($name:ident, $prop_accessor:ident) => { impl Condition for $name { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let val = object.$prop_accessor(self.offset); Ok(float_filter_between!(eval val, self.lower, self.upper)) } } }; (eval $val:expr, $lower:expr, $upper:expr) => {{ ($lower <= $val || $lower.is_nan()) && ($upper >= $val || $val.is_nan() || ($upper.is_infinite() && $upper.is_sign_positive())) }}; } filter_between_struct!(FloatBetweenCond, Float, f32); float_filter_between!(FloatBetweenCond, read_float); filter_between_struct!(DoubleBetweenCond, Double, f64); float_filter_between!(DoubleBetweenCond, read_double); #[macro_export] macro_rules! float_filter_between_list { ($name:ident, $prop_accessor:ident) => { impl Condition for $name { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let vals = object.$prop_accessor(self.offset); if let Some(vals) = vals { for val in vals { if float_filter_between!(eval val, self.lower, self.upper) { return Ok(true); } } } Ok(false) } } }; } filter_between_struct!(AnyFloatBetweenCond, Float, f32); float_filter_between_list!(AnyFloatBetweenCond, read_float_list); filter_between_struct!(AnyDoubleBetweenCond, Double, f64); float_filter_between_list!(AnyDoubleBetweenCond, read_double_list); #[derive(Clone)] struct StringBetweenCond { offset: usize, lower: Option>, upper: Option>, case_sensitive: bool, } #[derive(Clone)] struct AnyStringBetweenCond { offset: usize, lower: Option>, upper: Option>, case_sensitive: bool, } fn string_between( value: Option<&str>, lower: Option<&[u8]>, upper: Option<&[u8]>, case_sensitive: bool, ) -> bool { if let Some(obj_str) = value { let mut matches = true; if case_sensitive { if let Some(lower) = lower { matches = lower <= obj_str.as_bytes(); } matches &= if let Some(upper) = upper { upper >= obj_str.as_bytes() } else { false }; } else { let obj_str = obj_str.to_lowercase(); if let Some(lower) = lower { matches = lower <= obj_str.as_bytes(); } matches &= if let Some(upper) = upper { upper >= obj_str.as_bytes() } else { false }; } matches } else { lower.is_none() } } impl Condition for StringBetweenCond { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let value = object.read_string(self.offset); let result = string_between( value, self.lower.as_deref(), self.upper.as_deref(), self.case_sensitive, ); Ok(result) } } impl Condition for AnyStringBetweenCond { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let list = object.read_string_list(self.offset); if let Some(list) = list { for value in list { let result = string_between( value, self.lower.as_deref(), self.upper.as_deref(), self.case_sensitive, ); if result { return Ok(true); } } } Ok(false) } } #[macro_export] macro_rules! string_filter_struct { ($name:ident) => { paste! { #[derive(Clone)] struct [<$name Cond>] { offset: usize, value: String, case_sensitive: bool, } } }; } #[macro_export] macro_rules! string_filter { ($name:ident) => { paste! { string_filter_struct!($name); impl Condition for [<$name Cond>] { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let other_str = object.read_string(self.offset); let result = string_filter!(eval $name, self, other_str); Ok(result) } } string_filter_struct!([]); impl Condition for [] { fn evaluate(&self, _id: i64, object: IsarObject, _: Option<&IsarCursors>) -> Result { let list = object.read_string_list(self.offset); if let Some(list) = list { for value in list { if string_filter!(eval $name, self, value) { return Ok(true); } } } Ok(false) } } } }; (eval $name:tt, $filter:expr, $value:expr) => { if let Some(other_str) = $value { if $filter.case_sensitive { string_filter!($name &$filter.value, other_str) } else { let lowercase_string = other_str.to_lowercase(); let lowercase_str = &lowercase_string; string_filter!($name &$filter.value, lowercase_str) } } else { false } }; (StringStartsWith $filter_str:expr, $other_str:ident) => { $other_str.starts_with($filter_str) }; (StringEndsWith $filter_str:expr, $other_str:ident) => { $other_str.ends_with($filter_str) }; (StringContains $filter_str:expr, $other_str:ident) => { $other_str.contains($filter_str) }; (StringMatches $filter_str:expr, $other_str:ident) => { fast_wild_match($other_str, $filter_str) }; } string_filter!(StringStartsWith); string_filter!(StringEndsWith); string_filter!(StringContains); string_filter!(StringMatches); #[derive(Clone)] struct ListLengthCond { offset: usize, lower: usize, upper: usize, } impl Condition for ListLengthCond { fn evaluate( &self, _id: i64, object: IsarObject, _cursors: Option<&IsarCursors>, ) -> Result { if let Some(len) = object.read_length(self.offset) { Ok(self.lower <= len && self.upper >= len) } else { Ok(false) } } } #[derive(Clone)] struct NullCond { offset: usize, data_type: DataType, } impl Condition for NullCond { fn evaluate( &self, _id: i64, object: IsarObject, _cursors: Option<&IsarCursors>, ) -> Result { Ok(object.is_null(self.offset, self.data_type)) } } #[derive(Clone)] struct AndCond { filters: Vec, } impl Condition for AndCond { fn evaluate(&self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>) -> Result { for filter in &self.filters { if !filter.evaluate(id, object, cursors)? { return Ok(false); } } Ok(true) } } #[derive(Clone)] struct OrCond { filters: Vec, } impl Condition for OrCond { fn evaluate(&self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>) -> Result { for filter in &self.filters { if filter.evaluate(id, object, cursors)? { return Ok(true); } } Ok(false) } } #[derive(Clone)] struct XorCond { filters: Vec, } impl Condition for XorCond { fn evaluate(&self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>) -> Result { let mut any = false; for filter in &self.filters { if filter.evaluate(id, object, cursors)? { if any { return Ok(false); } else { any = true; } } } Ok(any) } } #[derive(Clone)] struct NotCond { filter: Box, } impl Condition for NotCond { fn evaluate(&self, id: i64, object: IsarObject, cursors: Option<&IsarCursors>) -> Result { Ok(!self.filter.evaluate(id, object, cursors)?) } } #[derive(Clone)] struct StaticCond { value: bool, } impl Condition for StaticCond { fn evaluate(&self, _id: i64, _: IsarObject, _: Option<&IsarCursors>) -> Result { Ok(self.value) } } #[derive(Clone)] struct ObjectCond { offset: usize, filter: Box, } impl Condition for ObjectCond { fn evaluate( &self, _id: i64, object: IsarObject, _cursors: Option<&IsarCursors>, ) -> Result { if let Some(object) = object.read_object(self.offset) { self.filter.evaluate(i64::MIN, object, None) } else { Ok(false) } } } #[derive(Clone)] struct AnyObjectCond { offset: usize, filter: Option>, } impl Condition for AnyObjectCond { fn evaluate( &self, _id: i64, object: IsarObject, _cursors: Option<&IsarCursors>, ) -> Result { if let Some(list) = object.read_object_list(self.offset) { if let Some(filter) = &self.filter { for object in list { if let Some(object) = object { let result = filter.evaluate(0, object, None)?; if result { return Ok(true); } } } } else { for object in list { if object.is_none() { return Ok(true); } } } } Ok(false) } } #[derive(Clone)] struct AnyLinkCond { link: IsarLink, filter: Box, } impl Condition for AnyLinkCond { fn evaluate( &self, id: i64, _object: IsarObject, cursors: Option<&IsarCursors>, ) -> Result { if let Some(cursors) = cursors { self.link .iter(cursors, id, |id, object| { self.filter .evaluate(id, object, Some(cursors)) .map(|matches| !matches) }) .map(|none_matches| !none_matches) } else { Ok(true) } } } #[derive(Clone)] struct LinkLengthCond { link: IsarLink, lower: usize, upper: usize, } impl Condition for LinkLengthCond { fn evaluate( &self, id: i64, _object: IsarObject, cursors: Option<&IsarCursors>, ) -> Result { if let Some(cursors) = cursors { let mut length = 0; self.link.iter_ids(cursors, id, |_, _| { length += 1; Ok(true) })?; Ok(self.lower <= length && self.upper >= length) } else { Ok(true) } } } ================================================ FILE: packages/isar_core/src/query/id_where_clause.rs ================================================ use crate::cursor::IsarCursors; use crate::error::Result; use crate::mdbx::db::Db; use crate::object::id::BytesToId; use crate::object::isar_object::IsarObject; use crate::query::Sort; use intmap::IntMap; #[derive(Clone)] pub(crate) struct IdWhereClause { db: Db, lower: i64, upper: i64, sort: Sort, } impl IdWhereClause { pub(crate) fn new(db: Db, lower: i64, upper: i64, sort: Sort) -> Self { IdWhereClause { db, lower, upper, sort, } } pub fn is_empty(&self) -> bool { self.upper < self.lower } pub(crate) fn id_matches(&self, id: i64) -> bool { self.lower <= id && self.upper >= id } pub(crate) fn iter<'txn, 'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, mut result_ids: Option<&mut IntMap<()>>, mut callback: F, ) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { let mut cursor = cursors.get_cursor(self.db)?; cursor.iter_between( &self.lower, &self.upper, false, false, self.sort == Sort::Ascending, |_, id_bytes, object| { let id = id_bytes.to_id(); if let Some(result_ids) = result_ids.as_deref_mut() { if !result_ids.insert_checked(id as u64, ()) { return Ok(true); } } let object = IsarObject::from_bytes(object); callback(id, object) }, ) } pub(crate) fn is_overlapping(&self, other: &Self) -> bool { (self.lower <= other.lower && self.upper >= other.upper) || (other.lower <= self.lower && other.upper >= self.upper) } } ================================================ FILE: packages/isar_core/src/query/index_where_clause.rs ================================================ use crate::cursor::IsarCursors; use crate::error::{IsarError, Result}; use crate::index::index_key::IndexKey; use crate::index::index_key_builder::IndexKeyBuilder; use crate::index::IsarIndex; use crate::mdbx::db::Db; use crate::object::isar_object::IsarObject; use crate::query::Sort; use intmap::IntMap; #[derive(Clone)] pub(crate) struct IndexWhereClause { db: Db, index: IsarIndex, lower_key: IndexKey, upper_key: IndexKey, skip_duplicates: bool, sort: Sort, } impl IndexWhereClause { pub fn new( db: Db, index: IsarIndex, lower_key: IndexKey, upper_key: IndexKey, skip_duplicates: bool, sort: Sort, ) -> Result { Ok(IndexWhereClause { db, index, lower_key, upper_key, skip_duplicates, sort, }) } pub fn object_matches(&self, object: IsarObject) -> bool { let mut key_matches = false; let key_builder = IndexKeyBuilder::new(&self.index.properties); key_builder .create_keys(object, |key| { key_matches = key >= &self.lower_key && key <= &self.upper_key; Ok(!key_matches) }) .unwrap(); key_matches } pub fn iter_ids<'txn, 'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, callback: F, ) -> Result where F: FnMut(i64) -> Result, { self.index.iter_between( cursors, &self.lower_key, &self.upper_key, self.skip_duplicates, self.sort == Sort::Ascending, callback, ) } pub fn iter<'txn, 'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, mut result_ids: Option<&mut IntMap<()>>, mut callback: F, ) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { let mut data_cursor = cursors.get_cursor(self.db)?; self.iter_ids(cursors, |id| { if let Some(result_ids) = result_ids.as_deref_mut() { if !result_ids.insert_checked(id as u64, ()) { return Ok(true); } } let entry = data_cursor.move_to(&id)?; let (_, object) = entry.ok_or(IsarError::DbCorrupted { message: "Could not find object specified in index.".to_string(), })?; let object = IsarObject::from_bytes(&object); callback(id, object) }) } pub fn is_overlapping(&self, other: &Self) -> bool { self.index != other.index || ((self.lower_key <= other.lower_key && self.upper_key >= other.upper_key) || (other.lower_key <= self.lower_key && other.upper_key >= self.upper_key)) } pub fn has_duplicates(&self) -> bool { self.index.multi_entry } } /*#[cfg(test)] mod tests { //use super::*; //use itertools::Itertools; #[macro_export] macro_rules! exec_wc ( ($txn:ident, $col:ident, $wc:ident, $res:ident) => { let mut cursor = $col.debug_get_index(0).debug_get_db().cursor(&$txn).unwrap(); let $res = $wc.iter(&mut cursor) .unwrap() .map(Result::unwrap) .map(|(_, v)| v) .collect_vec(); }; ); /*fn get_str_obj(col: &IsarCollection, str: &str) -> Vec { let mut ob = col.new_object_builder(); ob.write_string(Some(str)); ob.finish() }*/ #[test] fn test_iter() { /*isar!(isar, col => col!(field => String; ind!(field))); let txn = isar.begin_txn(true, false).unwrap(); let oid1 = col.put(&txn, None, &get_str_obj(&col, "aaaa")).unwrap(); let oid2 = col.put(&txn, None, &get_str_obj(&col, "aabb")).unwrap(); let oid3 = col.put(&txn, None, &get_str_obj(&col, "bbaa")).unwrap(); let oid4 = col.put(&txn, None, &get_str_obj(&col, "bbbb")).unwrap(); let all_oids = &[ oid1.as_ref(), oid2.as_ref(), oid3.as_ref(), oid4.as_ref(), ]; let mut wc = col.new_where_clause(Some(0)).unwrap(); exec_wc!(txn, col, wc, oids); assert_eq!(&oids, all_oids); wc.add_lower_string_value(Some("aa"), true); exec_wc!(txn, col, wc, oids); assert_eq!(&oids, all_oids); let mut wc = col.new_where_clause(Some(0)).unwrap(); wc.add_lower_string_value(Some("aa"), false); exec_wc!(txn, col, wc, oids); assert_eq!(&oids, &[oid3.as_ref(), oid4.as_ref()]); wc.add_upper_string_value(Some("bba"), true); exec_wc!(txn, col, wc, oids); assert_eq!(&oids, &[oid3.as_ref()]); let mut wc = col.new_where_clause(Some(0)).unwrap(); wc.add_lower_string_value(Some("x"), false); exec_wc!(txn, col, wc, oids); assert_eq!(&oids, &[] as &[&[u8]]);*/ } #[test] fn test_add_upper_oid() {} } */ ================================================ FILE: packages/isar_core/src/query/link_where_clause.rs ================================================ use crate::cursor::IsarCursors; use crate::error::Result; use crate::link::IsarLink; use crate::object::isar_object::IsarObject; use intmap::IntMap; #[derive(Clone)] pub(crate) struct LinkWhereClause { link: IsarLink, id: i64, } impl LinkWhereClause { pub fn new(link: IsarLink, id: i64) -> Result { Ok(LinkWhereClause { link, id }) } pub fn iter<'txn, 'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, mut result_ids: Option<&mut IntMap<()>>, mut callback: F, ) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { self.link.iter(cursors, self.id, |id, object| { if let Some(result_ids) = result_ids.as_deref_mut() { if !result_ids.insert_checked(id as u64, ()) { return Ok(true); } } callback(id, object) }) } } ================================================ FILE: packages/isar_core/src/query/mod.rs ================================================ use intmap::IntMap; use serde_json::{json, Value}; use std::cmp::Ordering; use crate::collection::IsarCollection; use crate::cursor::IsarCursors; use crate::error::Result; use crate::object::isar_object::IsarObject; use crate::object::json_encode_decode::JsonEncodeDecode; use crate::object::property::Property; use crate::query::filter::Filter; use crate::query::where_clause::WhereClause; use crate::txn::IsarTxn; mod fast_wild_match; pub mod filter; mod id_where_clause; mod index_where_clause; mod link_where_clause; pub mod query_builder; mod where_clause; #[derive(Copy, Clone, Eq, PartialEq)] pub enum Sort { Ascending, Descending, } pub enum Case { Sensitive, Insensitive, } #[derive(Clone)] pub struct Query { instance_id: u64, where_clauses: Vec, where_clauses_dup: bool, filter: Option, sort: Vec<(Property, Sort)>, distinct: Vec<(Property, bool)>, offset: usize, limit: usize, } impl<'txn> Query { #[allow(clippy::too_many_arguments)] pub(crate) fn new( instance_id: u64, where_clauses: Vec, filter: Option, sort: Vec<(Property, Sort)>, distinct: Vec<(Property, bool)>, offset: usize, limit: usize, ) -> Self { let where_clauses_dup = Self::check_where_clauses_duplicates(&where_clauses); Query { instance_id, where_clauses, where_clauses_dup, filter, sort, distinct, offset, limit, } } fn check_where_clauses_duplicates(where_clauses: &[WhereClause]) -> bool { for (i, wc1) in where_clauses.iter().enumerate() { if wc1.has_duplicates() { return true; } for wc2 in where_clauses.iter().skip(i + 1) { if wc1.is_overlapping(wc2) { return true; } } } false } pub(crate) fn execute_raw<'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, mut callback: F, ) -> Result<()> where F: FnMut(i64, IsarObject<'txn>) -> Result, { let mut result_ids = if self.where_clauses_dup { Some(IntMap::new()) } else { None }; let static_filter = Filter::stat(true); let filter = self.filter.as_ref().unwrap_or(&static_filter); for where_clause in &self.where_clauses { let result = where_clause.iter(cursors, result_ids.as_mut(), |id, object| { if filter.evaluate(id, object, Some(cursors))? { callback(id, object) } else { Ok(true) } })?; if !result { return Ok(()); } } Ok(()) } fn execute_unsorted<'env, F>( &self, cursors: &IsarCursors<'txn, 'env>, callback: F, ) -> Result<()> where F: FnMut(i64, IsarObject<'txn>) -> Result, { if !self.distinct.is_empty() { let callback = self.add_distinct_unsorted(callback); let callback = self.add_offset_limit_unsorted(callback); self.execute_raw(cursors, callback) } else { let callback = self.add_offset_limit_unsorted(callback); self.execute_raw(cursors, callback) } } fn hash_properties(object: IsarObject, properties: &[(Property, bool)]) -> u64 { let mut hash = 0; for (p, case_sensitive) in properties { hash = object.hash_property(p.offset, p.data_type, *case_sensitive, hash); } hash } fn add_distinct_unsorted( &self, mut callback: F, ) -> impl FnMut(i64, IsarObject<'txn>) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { let properties = self.distinct.clone(); let mut hashes = IntMap::new(); move |id, object| { let hash = Self::hash_properties(object, &properties); if hashes.insert_checked(hash, ()) { callback(id, object) } else { Ok(true) } } } fn add_offset_limit_unsorted( &self, mut callback: F, ) -> impl FnMut(i64, IsarObject<'txn>) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { let offset = self.offset; let max_count = self.limit.saturating_add(offset); let mut count = 0; move |id, value| { count += 1; if count > max_count || (count > offset && !callback(id, value)?) { Ok(false) } else { Ok(true) } } } fn execute_sorted<'env>( &self, cursors: &IsarCursors<'txn, 'env>, ) -> Result)>> { let mut results = vec![]; self.execute_raw(cursors, |id, object| { results.push((id, object)); Ok(true) })?; results.sort_unstable_by(|(_, o1), (_, o2)| { for (p, sort) in &self.sort { let ord = o1.compare_property(o2, p.offset, p.data_type); if ord != Ordering::Equal { return if *sort == Sort::Ascending { ord } else { ord.reverse() }; } } Ordering::Equal }); if !self.distinct.is_empty() { Ok(self.add_distinct_sorted(results)) } else { Ok(results) } } fn add_distinct_sorted( &self, results: Vec<(i64, IsarObject<'txn>)>, ) -> Vec<(i64, IsarObject<'txn>)> { let properties = self.distinct.clone(); let mut hashes = IntMap::new(); results .into_iter() .filter(|(_, object)| { let hash = Self::hash_properties(*object, &properties); hashes.insert_checked(hash, ()) }) .collect() } fn add_offset_limit_sorted( &self, results: Vec<(i64, IsarObject<'txn>)>, ) -> impl IntoIterator)> { results.into_iter().skip(self.offset).take(self.limit) } pub(crate) fn maybe_matches_wc_filter(&self, id: i64, object: IsarObject) -> bool { let maybe_matches = self .where_clauses .iter() .any(|wc| wc.maybe_matches(id, object)); if !maybe_matches { return false; } if let Some(filter) = &self.filter { filter.evaluate(id, object, None).unwrap_or(true) } else { true } } pub fn find_while(&self, txn: &'txn mut IsarTxn, mut callback: F) -> Result<()> where F: FnMut(i64, IsarObject<'txn>) -> bool, { txn.read(self.instance_id, |cursors| { if self.sort.is_empty() { self.execute_unsorted(cursors, |id, object| { let cont = callback(id, object); Ok(cont) })?; } else { let results = self.execute_sorted(cursors)?; let results_iter = self.add_offset_limit_sorted(results); for (id, object) in results_iter { if !callback(id, object) { break; } } } Ok(()) }) } pub fn find_all_vec(&self, txn: &'txn mut IsarTxn) -> Result)>> { let mut results = vec![]; self.find_while(txn, |id, object| { results.push((id, object)); true })?; Ok(results) } pub fn count(&self, txn: &mut IsarTxn) -> Result { let mut counter = 0; self.find_while(txn, |_, _| { counter += 1; true })?; Ok(counter) } pub fn export_json( &self, txn: &mut IsarTxn, collection: &IsarCollection, id_name: Option<&str>, primitive_null: bool, ) -> Result { let mut items = vec![]; self.find_while(txn, |id, object| { let mut json = JsonEncodeDecode::encode( &collection.properties, &collection.embedded_properties, object, primitive_null, ); if let Some(id_name) = id_name { json.insert(id_name.to_string(), Value::from(id)); } items.push(json); true })?; Ok(json!(items)) } } ================================================ FILE: packages/isar_core/src/query/query_builder.rs ================================================ use super::index_where_clause::IndexWhereClause; use crate::collection::IsarCollection; use crate::error::{illegal_arg, Result}; use crate::index::index_key::IndexKey; use crate::object::property::Property; use crate::query::filter::Filter; use crate::query::id_where_clause::IdWhereClause; use crate::query::link_where_clause::LinkWhereClause; use crate::query::where_clause::WhereClause; use crate::query::{Query, Sort}; pub struct QueryBuilder<'a> { pub collection: &'a IsarCollection, where_clauses: Option>, filter: Option, sort: Vec<(Property, Sort)>, distinct: Vec<(Property, bool)>, offset: usize, limit: usize, } impl<'a> QueryBuilder<'a> { pub(crate) fn new(collection: &'a IsarCollection) -> QueryBuilder { QueryBuilder { collection, where_clauses: None, filter: None, sort: vec![], distinct: vec![], offset: 0, limit: usize::MAX, } } fn init_where_clauses(&mut self) { if self.where_clauses.is_none() { self.where_clauses = Some(vec![]); } } pub fn add_id_where_clause(&mut self, start: i64, end: i64) -> Result<()> { self.init_where_clauses(); let (lower, upper, sort) = if start > end { (end, start, Sort::Descending) } else { (start, end, Sort::Ascending) }; let wc = IdWhereClause::new(self.collection.db, lower, upper, sort); if !wc.is_empty() { self.where_clauses .as_mut() .unwrap() .push(WhereClause::Id(wc)) } Ok(()) } pub fn add_index_where_clause( &mut self, index_id: u64, lower: IndexKey, upper: IndexKey, sort: Sort, skip_duplicates: bool, ) -> Result<()> { self.init_where_clauses(); let index = self.collection.get_index_by_id(index_id)?; let wc = IndexWhereClause::new( self.collection.db, index.clone(), lower, upper, skip_duplicates, sort, )?; self.where_clauses .as_mut() .unwrap() .push(WhereClause::Index(wc)); Ok(()) } pub fn add_link_where_clause( &mut self, collection: &IsarCollection, link_id: u64, id: i64, ) -> Result<()> { let link = collection.get_link_backlink(link_id)?; self.init_where_clauses(); let wc = LinkWhereClause::new(link.clone(), id)?; self.where_clauses .as_mut() .unwrap() .push(WhereClause::Link(wc)); Ok(()) } pub fn set_filter(&mut self, filter: Filter) { self.filter = Some(filter); } pub fn add_sort(&mut self, property: &Property, sort: Sort) -> Result<()> { if property.data_type.is_scalar() { self.sort.push((property.clone(), sort)); Ok(()) } else { illegal_arg("Only scalar types may be used for sorting.") } } pub fn add_distinct(&mut self, property: &Property, case_sensitive: bool) { self.distinct.push((property.clone(), case_sensitive)); } pub fn set_offset(&mut self, offset: usize) { self.offset = offset; } pub fn set_limit(&mut self, limit: usize) { self.limit = limit; } pub fn build(mut self) -> Query { if self.where_clauses.is_none() { self.add_id_where_clause(i64::MIN, i64::MAX).unwrap(); } Query::new( self.collection.instance_id, self.where_clauses.unwrap(), self.filter, self.sort, self.distinct, self.offset, self.limit, ) } } ================================================ FILE: packages/isar_core/src/query/where_clause.rs ================================================ use crate::cursor::IsarCursors; use crate::error::Result; use crate::object::isar_object::IsarObject; use crate::query::id_where_clause::IdWhereClause; use crate::query::index_where_clause::IndexWhereClause; use crate::query::link_where_clause::LinkWhereClause; use intmap::IntMap; #[derive(Clone)] pub(crate) enum WhereClause { Id(IdWhereClause), Index(IndexWhereClause), Link(LinkWhereClause), } impl WhereClause { pub fn maybe_matches(&self, id: i64, object: IsarObject) -> bool { match self { WhereClause::Id(wc) => wc.id_matches(id), WhereClause::Index(wc) => wc.object_matches(object), WhereClause::Link(_) => true, } } pub fn iter<'txn, 'env, 'a, F>( &self, cursors: &IsarCursors<'txn, 'env>, result_ids: Option<&mut IntMap<()>>, callback: F, ) -> Result where F: FnMut(i64, IsarObject<'txn>) -> Result, { match self { WhereClause::Id(wc) => wc.iter(cursors, result_ids, callback), WhereClause::Index(wc) => wc.iter(cursors, result_ids, callback), WhereClause::Link(wc) => wc.iter(cursors, result_ids, callback), } } pub(crate) fn is_overlapping(&self, other: &Self) -> bool { match (self, other) { (WhereClause::Id(wc1), WhereClause::Id(wc2)) => wc1.is_overlapping(wc2), (WhereClause::Index(wc1), WhereClause::Index(wc2)) => wc1.is_overlapping(wc2), _ => true, } } pub(crate) fn has_duplicates(&self) -> bool { match self { WhereClause::Id(_) => false, WhereClause::Index(wc) => wc.has_duplicates(), WhereClause::Link(_) => false, } } } ================================================ FILE: packages/isar_core/src/schema/collection_schema.rs ================================================ use crate::error::{schema_error, IsarError, Result}; use crate::object::data_type::DataType; use crate::object::property::Property; use crate::schema::index_schema::{IndexSchema, IndexType}; use crate::schema::link_schema::LinkSchema; use crate::schema::property_schema::PropertySchema; use itertools::Itertools; use serde::{Deserialize, Serialize}; use super::schema_manager::SchemaManager; #[derive(Serialize, Deserialize, Clone, Eq)] pub struct CollectionSchema { pub(crate) name: String, #[serde(default)] pub(crate) embedded: bool, pub(crate) properties: Vec, #[serde(default)] pub(crate) indexes: Vec, #[serde(default)] pub(crate) links: Vec, #[serde(default)] pub(crate) version: u8, } impl PartialEq for CollectionSchema { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.embedded == other.embedded } } impl CollectionSchema { pub fn new( name: &str, embedded: bool, properties: Vec, indexes: Vec, links: Vec, ) -> CollectionSchema { CollectionSchema { name: name.to_string(), embedded, properties, indexes, links, version: SchemaManager::ISAR_FILE_VERSION, } } fn verify_name(name: &str) -> Result<()> { if name.is_empty() { schema_error("Empty names are not allowed.") } else if name.starts_with('_') { schema_error("Names must not begin with an underscore.") } else { Ok(()) } } pub(crate) fn verify(&self, collections: &[CollectionSchema]) -> Result<()> { Self::verify_name(&self.name)?; if self.embedded && (!self.links.is_empty() || !self.indexes.is_empty()) { schema_error("Embedded objects must not have Links or Indexes.")?; } let verify_target_col_exists = |col: &str, embedded: bool| -> Result<()> { if !collections .iter() .any(|c| c.name == col && c.embedded == embedded) { schema_error("Target collection does not exist.")?; } Ok(()) }; for property in &self.properties { if let Some(name) = &property.name { Self::verify_name(name)?; } if property.data_type == DataType::Object || property.data_type == DataType::ObjectList { if let Some(target_col) = &property.target_col { verify_target_col_exists(target_col, true)?; } else { schema_error("Object property must have a target collection.")?; } } else { if property.target_col.is_some() { schema_error("Target collection can only be set for object properties.")?; } } } for link in &self.links { Self::verify_name(&link.name)?; verify_target_col_exists(&link.target_col, false)?; } let property_names = self .properties .iter() .unique_by(|p| p.name.as_ref().unwrap()); if property_names.count() != self.properties.len() { schema_error("Duplicate property name")?; } let index_names = self.indexes.iter().unique_by(|i| i.name.as_str()); if index_names.count() != self.indexes.len() { schema_error("Duplicate index name")?; } let link_names = self.links.iter().unique_by(|l| l.name.as_str()); if link_names.count() != self.links.len() { schema_error("Duplicate link name")?; } for index in &self.indexes { if index.properties.is_empty() { schema_error("At least one property needs to be added to a valid index")?; } else if index.properties.len() > 3 { schema_error("No more than three properties may be used as a composite index")?; } if !index.unique && index.replace { schema_error("Only unique indexes can replace")?; } for (i, index_property) in index.properties.iter().enumerate() { let property = self .properties .iter() .find(|p| p.name.as_ref() == Some(&index_property.name)); if property.is_none() { schema_error("IsarIndex property does not exist")?; } let property = property.unwrap(); if property.data_type == DataType::Object || property.data_type == DataType::ObjectList { schema_error("Object and ObjectList cannot be indexed.")?; } if property.data_type == DataType::Float || property.data_type == DataType::Double || property.data_type == DataType::FloatList || property.data_type == DataType::DoubleList { if index_property.index_type == IndexType::Hash { schema_error("Float values cannot be hashed.")?; } else if i != index.properties.len() - 1 { schema_error( "Float indexes must only be at the end of a composite index.", )?; } } if property.data_type.get_element_type().is_some() { if index.properties.len() > 1 && index_property.index_type != IndexType::Hash { schema_error("Composite list indexes are not supported.")?; } } else if property.data_type == DataType::String && i != index.properties.len() - 1 && index_property.index_type != IndexType::Hash { schema_error( "Non-hashed string indexes must only be at the end of a composite index.", )?; } if property.data_type != DataType::String && property.data_type.get_element_type().is_none() && index_property.index_type == IndexType::Hash { schema_error("Only string and list indexes may be hashed")?; } if property.data_type != DataType::StringList && index_property.index_type == IndexType::HashElements { schema_error("Only string list indexes may be use hash elements")?; } if property.data_type != DataType::String && property.data_type != DataType::StringList && index_property.case_sensitive { schema_error("Only String and StringList indexes may be case sensitive.")?; } } } Ok(()) } pub(crate) fn merge_properties(&mut self, existing: &Self) -> Result> { let mut properties = existing.properties.clone(); let mut removed_properties = vec![]; for property in &mut properties { if property.name.is_some() && !self.properties.contains(property) { removed_properties.push(property.name.take().unwrap()); } } for property in &self.properties { if !properties.contains(property) { properties.push(property.clone()) } } self.properties = properties; Ok(removed_properties) } pub fn get_properties(&self) -> Vec { let mut properties = vec![]; let mut offset = 2; for property_schema in self.properties.iter() { let property = property_schema.as_property(offset); if let Some(property) = property { properties.push(property); } offset += property_schema.data_type.get_static_size(); } properties.sort_by(|a, b| a.name.cmp(&b.name)); properties } pub fn to_json_bytes(&self) -> Result> { serde_json::to_vec(self).map_err(|_| IsarError::SchemaError { message: "Could not serialize schema.".to_string(), }) } } /*#[cfg(test)] mod tests { use super::*; #[test] fn test_add_property_empty_name() { let mut col = CollectionSchema::new("col"); assert!(col.add_property("", DataType::Int).is_err()) } #[test] fn test_add_property_duplicate_name() { let mut col = CollectionSchema::new("col"); col.add_property("prop", DataType::Int).unwrap(); assert!(col.add_property("prop", DataType::Int).is_err()) } #[test] fn test_add_property_same_type_wrong_order() { let mut col = CollectionSchema::new("col"); col.add_property("b", DataType::Int).unwrap(); assert!(col.add_property("a", DataType::Int).is_err()) } #[test] fn test_add_property_wrong_order() { let mut col = CollectionSchema::new("col"); col.add_property("a", DataType::Long).unwrap(); assert!(col.add_property("b", DataType::Int).is_err()) } #[test] fn test_add_index_without_properties() { let mut col = CollectionSchema::new("col"); assert!(col.add_index(&[], false, false).is_err()) } #[test] fn test_add_index_with_non_existing_property() { let mut col = CollectionSchema::new("col"); col.add_property("prop1", DataType::Int).unwrap(); col.add_index(&["prop1"], false, false).unwrap(); assert!(col.add_index(&["wrongprop"], false, false).is_err()) } #[test] fn test_add_index_with_illegal_data_type() { let mut col = CollectionSchema::new("col"); col.add_property("byte", DataType::Byte).unwrap(); col.add_property("int", DataType::Int).unwrap(); col.add_property("float", DataType::Float).unwrap(); col.add_property("long", DataType::Long).unwrap(); col.add_property("double", DataType::Double).unwrap(); col.add_property("str", DataType::String).unwrap(); col.add_property("byteList", DataType::ByteList).unwrap(); col.add_property("intList", DataType::IntList).unwrap(); col.add_index(&["byte"], false, None, false).unwrap(); col.add_index(&["int"], false, None, false).unwrap(); col.add_index(&["float"], false, None, false).unwrap(); col.add_index(&["long"], false, None, false).unwrap(); col.add_index(&["double"], false, None, false).unwrap(); col.add_index(&["str"], false, Some(StringIndexType::Value), false) .unwrap(); assert!(col.add_index(&["byteList"], false, false).is_err()); assert!(col.add_index(&["intList"], false, false).is_err()); } #[test] fn test_add_index_too_many_properties() { let mut col = CollectionSchema::new("col"); col.add_property("prop1", DataType::Int).unwrap(); col.add_property("prop2", DataType::Int).unwrap(); col.add_property("prop3", DataType::Int).unwrap(); col.add_property("prop4", DataType::Int).unwrap(); assert!(col .add_index(&["prop1", "prop2", "prop3", "prop4"], false, false) .is_err()) } #[test] fn test_add_duplicate_index() { let mut col = CollectionSchema::new("col"); col.add_property("prop1", DataType::Int).unwrap(); col.add_property("prop2", DataType::Int).unwrap(); col.add_index(&["prop2"], false, false).unwrap(); col.add_index(&["prop1", "prop2"], false, false).unwrap(); assert!(col.add_index(&["prop1", "prop2"], false, false).is_err()); assert!(col.add_index(&["prop1"], false, false).is_err()); } #[test] fn test_add_composite_index_with_non_hashed_string_in_the_middle() { let mut col = CollectionSchema::new("col"); col.add_property("int", DataType::Int).unwrap(); col.add_property("str", DataType::String).unwrap(); col.add_index(&["int", "str"], false, false).unwrap(); assert!(col.add_index(&["str", "int"], false, false).is_err()); col.add_index(&["str", "int"], false, true).unwrap(); } #[test] fn test_properties_have_correct_offset() { fn get_offsets(mut schema: CollectionSchema) -> Vec { let mut get_id = || 1; schema.update_with_existing_collections(&[], &mut get_id); let col = schema.get_isar_collection(); let mut offsets = vec![]; for i in 0..schema.properties.len() { let (_, p) = col.get_properties().get(i).unwrap(); offsets.push(p.offset); } offsets } let mut col = CollectionSchema::new("col"); col.add_property("byte", DataType::Byte).unwrap(); col.add_property("int", DataType::Int).unwrap(); col.add_property("double", DataType::Double).unwrap(); assert_eq!(get_offsets(col), vec![0, 2, 10]); let mut col = CollectionSchema::new("col"); col.add_property("byte1", DataType::Byte).unwrap(); col.add_property("byte2", DataType::Byte).unwrap(); col.add_property("byte3", DataType::Byte).unwrap(); col.add_property("str", DataType::String).unwrap(); assert_eq!(get_offsets(col), vec![0, 1, 2, 10]); let mut col = CollectionSchema::new("col"); col.add_property("byteList", DataType::ByteList).unwrap(); col.add_property("intList", DataType::IntList).unwrap(); col.add_property("doubleList", DataType::DoubleList) .unwrap(); assert_eq!(get_offsets(col), vec![2, 10, 18]); } #[test] fn update_with_no_existing_collection() { let mut col = CollectionSchema::new("col"); col.add_property("byte", DataType::Byte).unwrap(); col.add_property("int", DataType::Int).unwrap(); col.add_index(&["byte"], true, false).unwrap(); col.add_index(&["int"], true, false).unwrap(); let mut counter = 0; let mut get_id = || { counter += 1; counter }; col.update_with_existing_collections(&[], &mut get_id); assert_eq!(col.id, Some(1)); assert_eq!(col.indexes[0].id, Some(2)); assert_eq!(col.indexes[1].id, Some(3)); } #[test] fn update_with_existing_collection() { let mut counter = 0; let mut get_id = || { counter += 1; counter }; let mut col1 = CollectionSchema::new("col"); col1.add_property("byte", DataType::Byte).unwrap(); col1.add_property("int", DataType::Int).unwrap(); col1.add_index(&["byte"], true, false).unwrap(); col1.add_index(&["int"], true, false).unwrap(); col1.update_with_existing_collections(&[], &mut get_id); assert_eq!(col1.id, Some(1)); assert_eq!(col1.indexes[0].id, Some(2)); assert_eq!(col1.indexes[1].id, Some(3)); let mut col2 = CollectionSchema::new("col"); col2.add_property("byte", DataType::Byte).unwrap(); col2.add_property("int", DataType::Int).unwrap(); col2.add_index(&["byte"], true, false).unwrap(); col2.add_index(&["int", "byte"], true, false).unwrap(); col2.update_with_existing_collections(&[col1], &mut get_id); assert_eq!(col2.id, Some(1)); assert_eq!(col2.indexes[0].id, Some(2)); assert_eq!(col2.indexes[1].id, Some(4)); let mut col3 = CollectionSchema::new("col3"); col3.update_with_existing_collections(&[col2], &mut get_id); assert_eq!(col3.id, Some(5)); } } */ ================================================ FILE: packages/isar_core/src/schema/index_schema.rs ================================================ use crate::index::{IndexProperty, IsarIndex}; use crate::mdbx::db::Db; use crate::object::property::Property; use itertools::Itertools; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub enum IndexType { Value, Hash, HashElements, } #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct IndexPropertySchema { pub(crate) name: String, #[serde(rename = "type")] pub(crate) index_type: IndexType, #[serde(rename = "caseSensitive")] pub(crate) case_sensitive: bool, } impl IndexPropertySchema { pub fn new(name: &str, index_type: IndexType, case_sensitive: bool) -> IndexPropertySchema { IndexPropertySchema { name: name.to_string(), index_type, case_sensitive, } } } #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct IndexSchema { pub(crate) name: String, pub(crate) properties: Vec, pub(crate) unique: bool, #[serde(default)] pub(crate) replace: bool, } impl IndexSchema { pub fn new( name: &str, properties: Vec, unique: bool, replace: bool, ) -> IndexSchema { IndexSchema { name: name.to_string(), properties, unique, replace, } } pub(crate) fn as_index(&self, db: Db, properties: &[Property]) -> IsarIndex { let index_properties = self .properties .iter() .map(|ip| { let property = properties.iter().find(|p| ip.name == *p.name).unwrap(); IndexProperty::new(property.clone(), ip.index_type, ip.case_sensitive) }) .collect_vec(); IsarIndex::new(&self.name, db, index_properties, self.unique, self.replace) } } ================================================ FILE: packages/isar_core/src/schema/link_schema.rs ================================================ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] pub struct LinkSchema { pub(crate) name: String, #[serde(rename = "target")] pub(crate) target_col: String, } impl LinkSchema { pub fn new(name: &str, target_collection_name: &str) -> Self { LinkSchema { name: name.to_string(), target_col: target_collection_name.to_string(), } } } ================================================ FILE: packages/isar_core/src/schema/migrate_v1.rs ================================================ use itertools::Itertools; use crate::legacy::isar_object_v1::{LegacyIsarObject, LegacyProperty}; use crate::object::data_type::DataType; use crate::object::id::BytesToId; use crate::object::isar_object::IsarObject; use crate::object::object_builder::ObjectBuilder; use crate::schema::schema_manager::SchemaManager; use crate::{cursor::IsarCursors, error::Result, mdbx::txn::Txn}; use super::collection_schema::CollectionSchema; pub fn migrate_v1(txn: &Txn, schema: &mut CollectionSchema) -> Result<()> { let cursors = IsarCursors::new(txn, vec![]); let mut buffer = Some(vec![]); for index in &schema.indexes { let index_db = SchemaManager::open_index_db(txn, schema, index)?; index_db.clear(txn)?; } schema.indexes.clear(); let props = schema.get_properties(); let mut offset = 2; let legacy_props = schema .properties .iter() .map(|p| { let property = LegacyProperty::new(p.data_type, offset); offset += match p.data_type { DataType::Byte => 1, DataType::Int | DataType::Float => 4, _ => 8, }; property }) .collect_vec(); let db = SchemaManager::open_collection_db(txn, schema)?; let mut db_cursor = cursors.get_cursor(db)?; db_cursor.iter_all(false, true, |cursor, id_bytes, obj| { // We need to copy the data here because it will become invalid during the write let id = id_bytes.to_id(); let obj = obj.to_vec(); let legacy_object = LegacyIsarObject::from_bytes(&obj); let mut new_object = ObjectBuilder::new(&props, buffer.take()); for (prop, legacy_prop) in props.iter().zip(&legacy_props) { match prop.data_type { DataType::Bool => { if legacy_object.is_null(*legacy_prop) { new_object.write_bool(prop.offset, None); } else { new_object .write_bool(prop.offset, Some(legacy_object.read_bool(*legacy_prop))) } } DataType::Byte => { new_object.write_byte(prop.offset, legacy_object.read_byte(*legacy_prop)) } DataType::Int => { new_object.write_int(prop.offset, legacy_object.read_int(*legacy_prop)) } DataType::Float => { new_object.write_float(prop.offset, legacy_object.read_float(*legacy_prop)) } DataType::Long => { new_object.write_long(prop.offset, legacy_object.read_long(*legacy_prop)) } DataType::Double => { new_object.write_double(prop.offset, legacy_object.read_double(*legacy_prop)) } DataType::String => { new_object.write_string(prop.offset, legacy_object.read_string(*legacy_prop)) } DataType::BoolList => { let byte_list = legacy_object.read_byte_list(*legacy_prop); let bool_list = byte_list.map(|bytes| { bytes .into_iter() .map(|b| IsarObject::byte_to_bool(*b)) .collect_vec() }); new_object.write_bool_list(prop.offset, bool_list.as_deref()) } DataType::ByteList => new_object .write_byte_list(prop.offset, legacy_object.read_byte_list(*legacy_prop)), DataType::IntList => new_object.write_int_list( prop.offset, legacy_object.read_int_list(*legacy_prop).as_deref(), ), DataType::FloatList => new_object.write_float_list( prop.offset, legacy_object.read_float_list(*legacy_prop).as_deref(), ), DataType::LongList => new_object.write_long_list( prop.offset, legacy_object.read_long_list(*legacy_prop).as_deref(), ), DataType::DoubleList => new_object.write_double_list( prop.offset, legacy_object.read_double_list(*legacy_prop).as_deref(), ), DataType::StringList => new_object.write_string_list( prop.offset, legacy_object.read_string_list(*legacy_prop).as_deref(), ), _ => unreachable!(), } } cursor.put(&id, new_object.finish().as_bytes())?; buffer.replace(new_object.recycle()); Ok(true) })?; Ok(()) } ================================================ FILE: packages/isar_core/src/schema/mod.rs ================================================ pub mod collection_schema; pub mod index_schema; pub mod link_schema; pub(crate) mod migrate_v1; pub mod property_schema; pub(crate) mod schema_manager; use crate::error::{schema_error, Result}; use crate::schema::collection_schema::CollectionSchema; use itertools::Itertools; use serde::{Deserialize, Serialize}; use xxhash_rust::xxh3::xxh3_64_with_seed; #[derive(Serialize, Deserialize, Clone)] pub struct Schema { pub(crate) collections: Vec, } impl Schema { pub fn new(collections: Vec) -> Result { let collection_names = collections.iter().unique_by(|c| &c.name); if collection_names.count() != collections.len() { schema_error("Duplicate collection name")?; } for col in &collections { col.verify(&collections)?; } let schema = Schema { collections }; Ok(schema) } pub fn from_json(json: &[u8]) -> Result { if let Ok(collections) = serde_json::from_slice::>(json) { Schema::new(collections) } else { schema_error("Could not deserialize schema JSON") } } pub(crate) fn get_collection(&self, name: &str, embedded: bool) -> Option<&CollectionSchema> { self.collections .iter() .find(|c| c.name == name && c.embedded == embedded) } pub(crate) fn count_dbs(&self) -> usize { let mut count = 0; for col in &self.collections { count += 1; count += col.indexes.len(); count += col.links.len() * 2; } count } } /*#[cfg(test)] mod tests { use super::*; use crate::object::data_type::DataType; #[test] fn test_add_collection() { let mut schema = Schema::new(); let col1 = CollectionSchema::new("col"); schema.add_collection(col1).unwrap(); let col2 = CollectionSchema::new("other"); schema.add_collection(col2).unwrap(); let duplicate = CollectionSchema::new("col"); assert!(schema.add_collection(duplicate).is_err()); } #[test] fn test_update_with_existing_schema() -> Result<()> { let mut schema1 = Schema::new(); let mut col = CollectionSchema::new("col"); col.add_property("byteProperty", DataType::Byte)?; col.add_property("intProperty", DataType::Int)?; col.add_property("longProperty", DataType::Long)?; col.add_property("stringProperty", DataType::String)?; col.add_index(&["byteProperty"], false, false)?; col.add_index(&["intProperty", "byteProperty"], true, false)?; col.add_index(&["longProperty"], false, false)?; col.add_index(&["intProperty", "longProperty"], false, false)?; col.add_index(&["stringProperty"], false, true)?; schema1.add_collection(col)?; let mut counter = 0; let get_id = || { counter += 1; counter }; schema1.update_with_existing_schema_internal(None, get_id); let col = &schema1.collections[0]; assert_eq!(col.id, Some(1)); assert_eq!(col.indexes[0].id, Some(2)); assert_eq!(col.indexes[1].id, Some(3)); assert_eq!(col.indexes[2].id, Some(4)); assert_eq!(col.indexes[3].id, Some(5)); assert_eq!(col.indexes[4].id, Some(6)); let mut schema2 = Schema::new(); let mut col = CollectionSchema::new("col"); col.add_property("byteProperty", DataType::Byte)?; col.add_property("intProperty", DataType::Int)?; col.add_property("longProperty", DataType::Double)?; // changed type col.add_property("stringProperty", DataType::String)?; col.add_index(&["byteProperty"], false, false)?; col.add_index(&["intProperty", "byteProperty"], false, false)?; // changed unique col.add_index(&["longProperty"], false, false)?; // changed property type col.add_index(&["intProperty", "longProperty"], false, false)?; // changed property type- col.add_index(&["stringProperty"], false, false)?; // changed hash_value schema2.add_collection(col)?; let mut counter = 0; let get_id = || { counter += 1; counter }; schema2.update_with_existing_schema_internal(Some(&schema1), get_id); let col = &schema2.collections[0]; assert_eq!(col.id, Some(1)); assert_eq!(col.indexes[0].id, Some(2)); assert_eq!(col.indexes[1].id, Some(7)); assert_eq!(col.indexes[2].id, Some(8)); assert_eq!(col.indexes[3].id, Some(9)); assert_eq!(col.indexes[4].id, Some(10)); Ok(()) } } */ ================================================ FILE: packages/isar_core/src/schema/property_schema.rs ================================================ use crate::object::data_type::DataType; use crate::object::property::Property; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Eq)] pub struct PropertySchema { pub(crate) name: Option, #[serde(rename = "type")] pub(crate) data_type: DataType, #[serde(default)] #[serde(rename = "target")] pub(crate) target_col: Option, } impl PropertySchema { pub fn new( name: Option, data_type: DataType, target_col: Option, ) -> PropertySchema { PropertySchema { name, data_type, target_col, } } pub(crate) fn as_property(&self, offset: usize) -> Option { if let Some(name) = &self.name { let p = Property::new(name, self.data_type, offset, self.target_col.as_deref()); Some(p) } else { None } } } impl PartialEq for PropertySchema { fn eq(&self, other: &Self) -> bool { let type_bool_byte = (self.data_type == DataType::Bool || self.data_type == DataType::Byte) && (other.data_type == DataType::Bool || other.data_type == DataType::Byte); let type_bool_byte_list = (self.data_type == DataType::BoolList || self.data_type == DataType::ByteList) && (other.data_type == DataType::BoolList || other.data_type == DataType::ByteList); let type_eq = self.data_type == other.data_type || type_bool_byte || type_bool_byte_list; self.name == other.name && type_eq && self.target_col == other.target_col } } ================================================ FILE: packages/isar_core/src/schema/schema_manager.rs ================================================ use super::collection_schema::CollectionSchema; use super::index_schema::IndexSchema; use super::link_schema::LinkSchema; use super::Schema; use crate::collection::IsarCollection; use crate::cursor::IsarCursors; use crate::error::{schema_error, IsarError, Result}; use crate::index::index_key::IndexKey; use crate::index::IsarIndex; use crate::link::IsarLink; use crate::mdbx::cursor::{Cursor, UnboundCursor}; use crate::mdbx::{db::Db, txn::Txn}; use crate::object::property::Property; use crate::schema::migrate_v1::migrate_v1; use intmap::IntMap; use once_cell::sync::Lazy; use std::ops::Deref; use xxhash_rust::xxh3::xxh3_64; static OLD_INFO_VERSION_KEY: Lazy = Lazy::new(|| { let mut key = IndexKey::new(); key.add_string(Some("version"), true); key }); static OLD_INFO_SCHEMA_KEY: Lazy = Lazy::new(|| { let mut key = IndexKey::new(); key.add_string(Some("schema"), true); key }); pub(crate) struct SchemaManager { instance_id: u64, info_db: Db, pub schemas: Vec, } impl SchemaManager { pub const ISAR_FILE_VERSION: u8 = 2; pub fn create(instance_id: u64, txn: &Txn) -> Result { let info_db = Db::open(txn, Some("_info"), false, false, false)?; let mut info_cursor = UnboundCursor::new().bind(txn, info_db)?; Self::migrate_old_info(&mut info_cursor)?; let schemas = Self::get_schemas(&mut info_cursor)?; let manager = SchemaManager { instance_id, info_db, schemas, }; Ok(manager) } fn migrate_old_info(info_cursor: &mut Cursor) -> Result<()> { let version = info_cursor.move_to(OLD_INFO_VERSION_KEY.deref())?; if let Some((_, version)) = version { let version_num = u64::from_le_bytes(version.try_into().unwrap()); info_cursor.delete_current()?; let schema_bytes = info_cursor.move_to(OLD_INFO_SCHEMA_KEY.deref())?; let mut schema = if let Some((_, schema_bytes)) = schema_bytes { if let Ok(schema) = serde_json::from_slice::(schema_bytes) { Ok(schema) } else { schema_error("Could not deserialize schema JSON") } } else { Schema::new(vec![]) }?; info_cursor.delete_current()?; for col in &mut schema.collections { col.version = version_num as u8; Self::save_schema(info_cursor, col)?; } } Ok(()) } fn get_schemas(info_cursor: &mut Cursor) -> Result> { let mut schemas = vec![]; info_cursor.iter_all(false, true, |_, _, bytes| { let col = serde_json::from_slice::(bytes).map_err(|_| { IsarError::DbCorrupted { message: "Could not deserialize existing schema.".to_string(), } })?; schemas.push(col); Ok(true) })?; Ok(schemas) } fn save_schema(info_cursor: &mut Cursor, schema: &CollectionSchema) -> Result<()> { let key = IndexKey::from_bytes(schema.name.as_bytes().to_vec()); let bytes = schema.to_json_bytes()?; info_cursor.put(&key, &bytes)?; Ok(()) } fn delete_schema(info_cursor: &mut Cursor, schema: &CollectionSchema) -> Result<()> { let key = IndexKey::from_bytes(schema.name.as_bytes().to_vec()); if info_cursor.move_to(&key)?.is_some() { info_cursor.delete_current()?; } Ok(()) } pub fn open_collection_db(txn: &Txn, col: &CollectionSchema) -> Result { Db::open(txn, Some(&col.name), true, false, false) } pub fn open_index_db(txn: &Txn, col: &CollectionSchema, index: &IndexSchema) -> Result { let db_name = format!("_i_{}_{}", col.name, index.name); Db::open(txn, Some(&db_name), false, !index.unique, false) } pub fn open_link_dbs(txn: &Txn, col: &CollectionSchema, link: &LinkSchema) -> Result<(Db, Db)> { let link_db_name = format!("_l_{}_{}", col.name, link.name); let db = Db::open(txn, Some(&link_db_name), true, true, true)?; let backlink_db_name = format!("_b_{}_{}", col.name, link.name); let bl_db = Db::open(txn, Some(&backlink_db_name), true, true, true)?; Ok((db, bl_db)) } fn delete_collection(txn: &Txn, col: &CollectionSchema) -> Result<()> { let db = Self::open_collection_db(txn, col)?; db.drop(txn)?; for index in &col.indexes { Self::delete_index(txn, col, index)?; } for link in &col.links { Self::delete_link(txn, col, link)?; } Ok(()) } fn delete_index(txn: &Txn, col: &CollectionSchema, index: &IndexSchema) -> Result<()> { let db = Self::open_index_db(txn, col, index)?; db.drop(txn) } fn delete_link(txn: &Txn, col: &CollectionSchema, link: &LinkSchema) -> Result<()> { let (db, bl_db) = Self::open_link_dbs(txn, col, link)?; db.drop(txn)?; bl_db.drop(txn) } fn perform_migration( txn: &Txn, schema: &mut CollectionSchema, existing_schema: &CollectionSchema, ) -> Result> { let removed_properties = schema.merge_properties(existing_schema)?; let mut added_indexes = IntMap::new(); for index in &schema.indexes { if !existing_schema.indexes.contains(index) { let index_id = xxh3_64(index.name.as_bytes()); added_indexes.insert(index_id, ()); } } for existing_index in &existing_schema.indexes { let removed_index = !schema.indexes.contains(existing_index); let changed_property = existing_index .properties .iter() .any(|p| removed_properties.contains(&p.name)); if removed_index || changed_property { Self::delete_index(txn, existing_schema, existing_index)?; } if !removed_index && changed_property { let index_id = xxh3_64(existing_index.name.as_bytes()); added_indexes.insert(index_id, ()); } } for link in &existing_schema.links { if !schema.links.contains(link) { Self::delete_link(txn, existing_schema, link)?; } } Ok(added_indexes.keys().copied().collect()) } pub fn migrate_schema(&mut self, txn: &Txn, schemas: &mut Schema) -> Result>> { let cursors = IsarCursors::new(txn, vec![]); let mut added_indexes = IntMap::new(); for col_schema in &mut schemas.collections { let mut existing_schema = self .schemas .iter() .position(|s| s.name == col_schema.name) .map(|index| self.schemas.remove(index)); if let Some(existing_schema) = &mut existing_schema { if existing_schema.version == 1 { migrate_v1(txn, existing_schema)?; } else if existing_schema.version != Self::ISAR_FILE_VERSION { return Err(IsarError::VersionError {}); } let ai = Self::perform_migration(txn, col_schema, existing_schema)?; if !ai.is_empty() { added_indexes.insert(xxh3_64(col_schema.name.as_bytes()), ai); } } let mut info_cursor = cursors.get_cursor(self.info_db)?; col_schema.version = Self::ISAR_FILE_VERSION; Self::save_schema(&mut info_cursor, &col_schema)?; } Ok(added_indexes) } pub fn open_collection( &mut self, txn: &Txn, schema: &CollectionSchema, schemas: &Schema, added_indexes: &[u64], ) -> Result { let cursors = IsarCursors::new(txn, vec![]); let db = Self::open_collection_db(txn, &schema)?; let properties = schema.get_properties(); let mut embedded_properties = IntMap::new(); Self::get_embedded_properties(schemas, &properties, &mut embedded_properties); let indexes = Self::open_indexes(txn, &schema, &properties)?; let links = Self::open_links(txn, db, &schema, schemas)?; let backlinks = Self::open_backlinks(txn, db, &schema, schemas)?; let col = IsarCollection::new( db, self.instance_id, &schema.name, properties, embedded_properties, indexes, links, backlinks, ); col.init_auto_increment(&cursors)?; if !added_indexes.is_empty() { col.fill_indexes(&added_indexes, &cursors)?; } Ok(col) } fn get_embedded_properties( schemas: &Schema, properties: &[Property], embedded_properties: &mut IntMap>, ) { for property in properties { if let Some(target_id) = property.target_id { if !embedded_properties.contains_key(target_id) { let embedded_col_schema = schemas .collections .iter() .find(|c| xxh3_64(c.name.as_bytes()) == target_id) .unwrap(); let properties = embedded_col_schema.get_properties(); embedded_properties.insert(target_id, properties.clone()); Self::get_embedded_properties(schemas, &properties, embedded_properties) } } } } fn open_indexes( txn: &Txn, schema: &CollectionSchema, properties: &[Property], ) -> Result> { let mut indexes = vec![]; for index_schema in &schema.indexes { let db = Self::open_index_db(txn, schema, index_schema)?; let index = index_schema.as_index(db, &properties); indexes.push(index); } Ok(indexes) } fn open_links( txn: &Txn, db: Db, schema: &CollectionSchema, schemas: &Schema, ) -> Result> { let mut links = vec![]; for link_schema in &schema.links { let (link_db, backlink_db) = Self::open_link_dbs(txn, schema, link_schema)?; let target_col_schema = schemas .get_collection(&link_schema.target_col, false) .unwrap(); let target_db = Self::open_collection_db(txn, target_col_schema)?; let link = IsarLink::new( &schema.name, &link_schema.name, false, link_db, backlink_db, db, target_db, ); links.push(link); } Ok(links) } fn open_backlinks( txn: &Txn, db: Db, schema: &CollectionSchema, schemas: &Schema, ) -> Result> { let mut backlinks = vec![]; for other_col_schema in &schemas.collections { for link_schema in &other_col_schema.links { if link_schema.target_col == schema.name { let other_col_db = Self::open_collection_db(txn, other_col_schema)?; let (link_db, bl_db) = Self::open_link_dbs(txn, other_col_schema, link_schema)?; let backlink = IsarLink::new( &other_col_schema.name, &link_schema.name, true, bl_db, link_db, db, other_col_db, ); backlinks.push(backlink); } } } Ok(backlinks) } pub fn delete_unopened_collections(&self, txn: &Txn) -> Result<()> { let mut info_cursor = UnboundCursor::new().bind(txn, self.info_db)?; for col in &self.schemas { Self::delete_collection(txn, col)?; Self::delete_schema(&mut info_cursor, col)?; } Ok(()) } } ================================================ FILE: packages/isar_core/src/txn.rs ================================================ use crate::cursor::IsarCursors; use crate::error::{IsarError, Result}; use crate::mdbx::cursor::UnboundCursor; use crate::mdbx::db::Db; use crate::mdbx::txn::Txn; use crate::watch::change_set::ChangeSet; use std::cell::RefCell; pub struct IsarTxn<'env> { instance_id: u64, txn: Txn<'env>, write: bool, change_set: RefCell>>, unbound_cursors: RefCell>>, } impl<'env> IsarTxn<'env> { pub(crate) fn new( instance_id: u64, txn: Txn<'env>, write: bool, change_set: Option>, ) -> Result { Ok(IsarTxn { instance_id, txn, write, change_set: RefCell::new(change_set), unbound_cursors: RefCell::new(Some(vec![])), }) } pub fn is_active(&self) -> bool { self.unbound_cursors.borrow().is_some() } fn verify_instance_id(&self, instance_id: u64) -> Result<()> { if self.instance_id != instance_id { Err(IsarError::InstanceMismatch {}) } else { Ok(()) } } pub(crate) fn read<'txn, T, F>(&'txn mut self, instance_id: u64, job: F) -> Result where F: FnOnce(&IsarCursors<'txn, 'env>) -> Result, { self.verify_instance_id(instance_id)?; if let Some(unbound_cursors) = self.unbound_cursors.take() { let cursors = IsarCursors::new(&self.txn, unbound_cursors); let result = job(&cursors); self.unbound_cursors.borrow_mut().replace(cursors.close()); result } else { Err(IsarError::TransactionClosed {}) } } pub(crate) fn write<'txn, T, F>(&'txn mut self, instance_id: u64, job: F) -> Result where F: FnOnce(&IsarCursors<'txn, 'env>, Option<&mut ChangeSet<'_>>) -> Result, { self.verify_instance_id(instance_id)?; if !self.write { return Err(IsarError::WriteTxnRequired {}); } if let Some(unbound_cursors) = self.unbound_cursors.take() { let mut change_set = self.change_set.take(); let cursors = IsarCursors::new(&self.txn, unbound_cursors); let result = job(&cursors, change_set.as_mut()); let unbounded_cursors = cursors.close(); if result.is_ok() { self.unbound_cursors.borrow_mut().replace(unbounded_cursors); if let Some(change_set) = change_set { self.change_set.borrow_mut().replace(change_set); } } result } else { Err(IsarError::TransactionClosed {}) } } pub fn commit(self) -> Result<()> { if !self.is_active() { return Err(IsarError::TransactionClosed {}); } if self.write { self.txn.commit()?; if let Some(change_set) = self.change_set.take() { change_set.notify_watchers(); } } Ok(()) } pub fn abort(self) { self.txn.abort() } pub(crate) fn db_names(&mut self) -> Result> { let unnamed_db = Db::open(&self.txn, None, false, false, false)?; let cursor = UnboundCursor::new(); let mut cursor = cursor.bind(&self.txn, unnamed_db)?; let mut names = vec![]; cursor.iter_all(false, true, |_, name, _| { names.push(String::from_utf8(name.to_vec()).unwrap()); Ok(true) })?; Ok(names) } } ================================================ FILE: packages/isar_core/src/watch/change_set.rs ================================================ use crate::object::isar_object::IsarObject; use crate::watch::isar_watchers::IsarWatchers; use crate::watch::watcher::Watcher; use intmap::IntMap; use std::sync::{Arc, MutexGuard}; pub(crate) struct ChangeSet<'a> { watchers: MutexGuard<'a, IsarWatchers>, changed_watchers: IntMap>, } impl<'a> ChangeSet<'a> { pub fn new(watchers: MutexGuard<'a, IsarWatchers>) -> Self { ChangeSet { watchers, changed_watchers: IntMap::new(), } } fn register_watchers(changed_watchers: &mut IntMap>, watchers: &[Arc]) { for w in watchers { let registered = changed_watchers.contains_key(w.get_id()); if !registered { changed_watchers.insert(w.get_id(), w.clone()); } else { break; } } } pub fn register_change(&mut self, col_id: u64, id: i64, object: IsarObject) { let cw = self.watchers.get_col_watchers(col_id); Self::register_watchers(&mut self.changed_watchers, &cw.watchers); if let Some(object_watchers) = cw.object_watchers.get(id as u64) { Self::register_watchers(&mut self.changed_watchers, object_watchers); } for (q, w) in &cw.query_watchers { if !self.changed_watchers.contains_key(w.get_id()) && q.maybe_matches_wc_filter(id, object) { self.changed_watchers.insert(w.get_id(), w.clone()); } } } pub fn register_all(&mut self, col_id: u64) { let cw = self.watchers.get_col_watchers(col_id); Self::register_watchers(&mut self.changed_watchers, &cw.watchers); for watchers in cw.object_watchers.values() { Self::register_watchers(&mut self.changed_watchers, watchers) } for (_, w) in &cw.query_watchers { self.changed_watchers.insert(w.get_id(), w.clone()); } } pub fn notify_watchers(self) { for watcher in self.changed_watchers.values() { watcher.notify(); } } } ================================================ FILE: packages/isar_core/src/watch/isar_watchers.rs ================================================ use crate::query::Query; use crate::watch::watcher::{Watcher, WatcherCallback}; use crossbeam_channel::Receiver; use intmap::IntMap; use itertools::Itertools; use std::sync::Arc; pub(crate) type WatcherModifier = Box; pub(crate) struct IsarWatchers { modifiers: Receiver, collection_watchers: IntMap, } impl IsarWatchers { pub fn new(modifiers: Receiver) -> Self { IsarWatchers { modifiers, collection_watchers: IntMap::new(), } } pub(crate) fn get_col_watchers(&mut self, col_id: u64) -> &mut IsarCollectionWatchers { if !self.collection_watchers.contains_key(col_id) { self.collection_watchers .insert(col_id, IsarCollectionWatchers::new()); } self.collection_watchers.get_mut(col_id).unwrap() } pub(crate) fn sync(&mut self) { let modifiers = self.modifiers.try_iter().collect_vec(); for modifier in modifiers { modifier(self) } } } pub struct IsarCollectionWatchers { pub(super) watchers: Vec>, pub(super) object_watchers: IntMap>>, pub(super) query_watchers: Vec<(Query, Arc)>, } impl IsarCollectionWatchers { fn new() -> Self { IsarCollectionWatchers { watchers: Vec::new(), object_watchers: IntMap::new(), query_watchers: Vec::new(), } } pub fn add_watcher(&mut self, watcher_id: u64, callback: WatcherCallback) { let watcher = Arc::new(Watcher::new(watcher_id, callback)); self.watchers.push(watcher); } pub fn remove_watcher(&mut self, watcher_id: u64) { let position = self .watchers .iter() .position(|w| w.get_id() == watcher_id) .unwrap(); self.watchers.remove(position); } pub fn add_object_watcher(&mut self, watcher_id: u64, id: i64, callback: WatcherCallback) { let watcher = Arc::new(Watcher::new(watcher_id, callback)); if let Some(object_watchers) = self.object_watchers.get_mut(id as u64) { object_watchers.push(watcher); } else { self.object_watchers.insert(id as u64, vec![watcher]); } } pub fn remove_object_watcher(&mut self, id: i64, watcher_id: u64) { let watchers = self.object_watchers.get_mut(id as u64).unwrap(); let position = watchers .iter() .position(|w| w.get_id() == watcher_id) .unwrap(); watchers.remove(position); } pub fn add_query_watcher(&mut self, watcher_id: u64, query: Query, callback: WatcherCallback) { let watcher = Arc::new(Watcher::new(watcher_id, callback)); self.query_watchers.push((query, watcher)); } pub fn remove_query_watcher(&mut self, watcher_id: u64) { let position = self .query_watchers .iter() .position(|(_, w)| w.get_id() == watcher_id) .unwrap(); self.query_watchers.remove(position); } } ================================================ FILE: packages/isar_core/src/watch/mod.rs ================================================ pub(crate) mod change_set; pub(crate) mod isar_watchers; pub(crate) mod watcher; pub struct WatchHandle { stop_callback: Option>, } impl WatchHandle { pub(crate) fn new(stop_callback: Box) -> Self { WatchHandle { stop_callback: Some(stop_callback), } } pub fn stop(self) {} } impl Drop for WatchHandle { fn drop(&mut self) { let callback = self.stop_callback.take().unwrap(); callback(); } } ================================================ FILE: packages/isar_core/src/watch/watcher.rs ================================================ pub type WatcherCallback = Box; pub(super) struct Watcher { id: u64, callback: WatcherCallback, } impl Watcher { pub fn new(id: u64, callback: WatcherCallback) -> Self { Watcher { id, callback } } pub fn get_id(&self) -> u64 { self.id } pub fn notify(&self) { (*self.callback)() } } ================================================ FILE: packages/isar_core/tests/binary_golden.json ================================================ [File too large to display: 19.9 MB] ================================================ FILE: packages/isar_core/tests/test_binary.rs ================================================ use std::{collections::HashMap, fs}; use intmap::IntMap; use isar_core::object::isar_object::IsarObject; use isar_core::object::json_encode_decode::JsonEncodeDecode; use isar_core::object::object_builder::ObjectBuilder; use isar_core::object::{data_type::DataType, property::Property}; use isar_core::schema::collection_schema::CollectionSchema; use isar_core::schema::property_schema::PropertySchema; use itertools::Itertools; use serde::{Deserialize, Serialize}; use serde_json::{from_str, json, Value}; #[derive(PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct BinaryTest { pub types: Vec, pub values: Vec, pub bytes: Vec, } impl BinaryTest { fn create(data: &[(DataType, Value)]) -> Self { let (types, values): (Vec<_>, Vec<_>) = data.iter().cloned().unzip(); let properties = Self::create_properties(&types); let embedded_properties = IntMap::new(); let json = Self::create_temp_json(&properties, &values); let mut ob = ObjectBuilder::new(&properties, None); JsonEncodeDecode::decode(&properties, &embedded_properties, &mut ob, &json).unwrap(); BinaryTest { types, values, bytes: ob.finish().as_bytes().to_vec(), } } fn create_properties(types: &[DataType]) -> Vec { let prop_schemas = types .iter() .enumerate() .map(|(i, t)| PropertySchema::new(Some(format!("{}", i)), *t, None)) .collect(); let schema = CollectionSchema::new("col", false, prop_schemas, vec![], vec![]); schema.get_properties() } fn create_temp_json(properties: &[Property], values: &[Value]) -> Value { let map: HashMap = properties .iter() .zip(values.iter()) .map(|(p, v)| (p.name.clone(), v.clone())) .collect(); json!(map) } } fn generate_binary_golden() -> Vec { let bool_blocks = (DataType::Bool, vec![json!(null), json!(true), json!(false)]); let byte_blocks = (DataType::Byte, vec![json!(0), json!(123), json!(255)]); let int_blocks = ( DataType::Int, vec![ json!(null), json!(i32::MIN + 1), json!(0i32), json!(i32::MAX), ], ); let float_blocks = ( DataType::Float, vec![ json!(f32::MIN), json!(-0f32), json!(0f32), json!(core::f32::consts::PI), json!(f32::MAX), ], ); let long_blocks = ( DataType::Long, vec![ json!(null), json!(i64::MIN + 1), json!(0i64), json!(i64::MAX), ], ); let double_blocks = ( DataType::Double, vec![ json!(f64::MIN), json!(-0f64), json!(0f64), json!(core::f64::consts::PI), json!(f64::MAX), ], ); let string_blocks = ( DataType::String, vec![ json!(null), json!(""), json!("a"), json!("רוצח עז קטנה"), json!("👱👱🏻👱🏼👱🏽👱🏾👱🏿👨‍❤️‍💋‍👨👩‍👩‍👧‍👦🏳️‍⚧️🇵🇷"), json!("Z̤͔ͧ̑̓ä͖̭̈̇lͮ̒ͫǧ̗͚̚o̙̔ͮ̇͐̇"), ], ); let bool_list_blocks = ( DataType::BoolList, vec![ json!(null), json!([]), json!([null]), json!([null, null, null]), json!([true]), json!([false]), json!([true, null, false, null]), ], ); let byte_list_blocks = ( DataType::ByteList, vec![json!([]), json!([255]), json!([0]), json!([255, 0, 0, 255])], ); let int_list_blocks = ( DataType::IntList, vec![ json!(null), json!([]), json!([null]), json!([null, null, null]), json!([12345i32]), json!([null, i32::MIN + 1, null, i32::MAX]), ], ); let float_list_blocks = ( DataType::FloatList, vec![ json!(null), json!([]), json!([-0f32, 0f32]), json!([f32::MIN, std::f32::consts::PI, f32::MAX]), ], ); let long_list_blocks = ( DataType::LongList, vec![ json!(null), json!([]), json!([null]), json!([null, null, null]), json!([-324234643i64]), json!([null, i64::MIN + 1, null, null, i64::MAX]), ], ); let double_list_blocks = ( DataType::DoubleList, vec![ json!(null), json!([]), json!([-0f64, 0f64]), json!([f64::MIN, std::f64::consts::PI, f64::MAX]), ], ); let string_list_blocks = ( DataType::StringList, vec![ json!(null), json!([]), json!([null]), json!([null, null]), json!([null, null, null]), json!([""]), json!(["", ""]), json!(["", "", ""]), json!(["", null]), json!([null, ""]), json!(["", null, null]), json!([null, "", null]), json!([null, null, ""]), json!([null, "", ""]), json!(["", null, ""]), json!(["", "", null]), json!(["a"]), json!(["a", "ab"]), json!(["a", "ab", "abc"]), json!([null, "a"]), json!(["a", null]), json!([null, "a"]), json!(["a", null, null]), json!([null, "a", null]), json!([null, null, "a"]), json!([null, "a", "bbb"]), json!(["a", null, "bbb"]), json!(["a", "bbb", null]), ], ); let mut combinations = vec![]; let static_blocks = normalize(&[ bool_blocks, byte_blocks, int_blocks, float_blocks, long_blocks, double_blocks, ]); for case1 in &static_blocks { combinations.push(vec![case1.clone()]); for case2 in &static_blocks { combinations.push(vec![case1.clone(), case2.clone()]); for case3 in &static_blocks { combinations.push(vec![case1.clone(), case2.clone(), case3.clone()]); } } } let dynamic_blocks = normalize(&[ string_blocks, bool_list_blocks, byte_list_blocks, int_list_blocks, float_list_blocks, long_list_blocks, double_list_blocks, ]); for case1 in &dynamic_blocks { combinations.push(vec![case1.clone()]); for case2 in &dynamic_blocks { combinations.push(vec![case1.clone(), case2.clone()]); for case3 in &dynamic_blocks { combinations.push(vec![case1.clone(), case2.clone(), case3.clone()]); } } } let string_list_blocks = normalize(&[string_list_blocks]); for case1 in &string_list_blocks { combinations.push(vec![case1.clone()]); for case2 in &string_list_blocks { combinations.push(vec![case1.clone(), case2.clone()]); for case3 in &string_list_blocks { combinations.push(vec![case1.clone(), case2.clone(), case3.clone()]); } } } for case1 in &static_blocks { for case2 in &dynamic_blocks { combinations.push(vec![case1.clone(), case2.clone()]); combinations.push(vec![case2.clone(), case1.clone()]); } } for case1 in &static_blocks { for case2 in &string_list_blocks { combinations.push(vec![case1.clone(), case2.clone()]); combinations.push(vec![case2.clone(), case1.clone()]); } } for case1 in &dynamic_blocks { for case2 in &string_list_blocks { combinations.push(vec![case1.clone(), case2.clone()]); combinations.push(vec![case2.clone(), case1.clone()]); } } combinations .into_iter() .map(|cases| BinaryTest::create(&cases)) .collect() } fn normalize(blocks: &[(DataType, Vec)]) -> Vec<(DataType, Value)> { blocks .into_iter() .flat_map(|(t, blocks)| blocks.into_iter().map(move |b| (*t, b.clone()))) .collect_vec() } #[allow(dead_code)] fn overwrite_binary_golden() { let tests = generate_binary_golden(); let json = json!(tests); fs::write("tests/binary_golden.json", json.to_string()).unwrap(); } #[test] fn test_binary_serialize() { let golden_str = fs::read_to_string("tests/binary_golden.json").unwrap(); let golden = from_str::>(&golden_str).unwrap(); for test in golden.iter() { let properties = BinaryTest::create_properties(&test.types); let embedded_properties = IntMap::new(); let golden_json = BinaryTest::create_temp_json(&properties, &test.values); let mut ob = ObjectBuilder::new(&properties, None); JsonEncodeDecode::decode(&properties, &embedded_properties, &mut ob, &golden_json).unwrap(); let bytes = ob.finish().as_bytes(); if bytes != test.bytes { assert_eq!(bytes, test.bytes); } } } #[test] fn test_binary_parse() { let golden_str = fs::read_to_string("tests/binary_golden.json").unwrap(); let golden = from_str::>(&golden_str).unwrap(); for test in golden.iter() { let properties = BinaryTest::create_properties(&test.types); let embedded_properties = IntMap::new(); let golden_json = BinaryTest::create_temp_json(&properties, &test.values); let object = IsarObject::from_bytes(&test.bytes); let generated_map = JsonEncodeDecode::encode(&properties, &embedded_properties, object, true); let generated_json = json!(generated_map); if generated_json != golden_json { assert_eq!(generated_json, golden_json); } } } ================================================ FILE: packages/isar_core/tests/test_hash.rs ================================================ use xxhash_rust::xxh3::xxh3_64; const PRIME32: u64 = 2654435761; const PRIME64: u64 = 11400714785074694797; #[test] fn test_xxh3() { fn generate_test_data(len: u64) -> Vec { let mut byte_gen = PRIME32; let mut buffer = Vec::new(); for _ in 0..len { buffer.push((byte_gen >> 56) as u8); let (b, _) = byte_gen.overflowing_mul(PRIME64); byte_gen = b; } buffer } let data = vec![ (0u64, 0x2D06800538D394C2u64), /* empty string */ (1, 0xC44BDFF4074EECDB), /* 1 - 3 */ (6, 0x27B56A84CD2D7325), /* 4 - 8 */ (12, 0xA713DAF0DFBB77E7), /* 9 - 16 */ (24, 0xA3FE70BF9D3510EB), /* 17 - 32 */ (48, 0x397DA259ECBA1F11), /* 33 - 64 */ (80, 0xBCDEFBBB2C47C90A), /* 65 - 96 */ (195, 0xCD94217EE362EC3A), /* 129-240 */ (403, 0xCDEB804D65C6DEA4), /* one block, last stripe is overlapping */ (512, 0x617E49599013CB6B), /* one block, finishing at stripe boundary */ (2048, 0xDD59E2C3A5F038E0), /* 2 blocks, finishing at block boundary */ (2240, 0x6E73A90539CF2948), /* 3 blocks, finishing at stripe boundary */ (2367, 0xCB37AEB9E5D361ED), /* 3 blocks, last stripe is overlapping */ ]; for (len, output) in data { let input = generate_test_data(len); assert_eq!(xxh3_64(&input), output); } } ================================================ FILE: packages/isar_core_ffi/.cargo/config.toml ================================================ [target.aarch64-linux-android] rustflags = ["-C", "link-arg=-Wl,--hash-style=both"] [target.armv7-linux-androideabi] rustflags = ["-C", "link-arg=-Wl,--hash-style=both"] [target.x86_64-linux-android] rustflags = ["-C", "link-arg=-Wl,--hash-style=both"] [target.i686-linux-android] rustflags = ["-C", "link-arg=-Wl,--hash-style=both"] ================================================ FILE: packages/isar_core_ffi/Cargo.toml ================================================ [package] name = "isar" version = "0.0.0" authors = ["Simon Leier "] edition = "2021" [dependencies] isar-core = { path = "../isar_core" } threadpool = "1.8.1" once_cell = "1.10.0" serde_json = "1.0" paste = "1.0" unicode-segmentation = "1.9.0" intmap = "2.0.0" itertools = "0.10.3" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] objc = "0.2.7" objc-foundation = "0.1.1" [target.'cfg(target_os = "android")'.dependencies] jni = "0.20.0" ndk-context = "0.1.1" once_cell = "1.10.0" [target.'cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))'.dependencies] dirs = "4.0.0" [lib] crate-type=["staticlib", "cdylib"] ================================================ FILE: packages/isar_core_ffi/build.rs ================================================ use std::{env, fs::File, io::Write, path::Path}; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("version.rs"); let mut f = File::create(&dest_path).unwrap(); let version = option_env!("ISAR_VERSION"); let version = version.map_or("debug", |version| { let version = if version.starts_with("v") { &version[1..] } else { version }; version }); write!(&mut f, "const ISAR_VERSION: &str = \"{version}\0\";").unwrap(); println!("cargo:rerun-if-env-changed=ISAR_VERSION"); } ================================================ FILE: packages/isar_core_ffi/src/c_object_set.rs ================================================ use isar_core::object::isar_object::IsarObject; use std::{ptr, slice}; #[repr(C)] pub struct CObject { id: i64, buffer: *mut u8, buffer_length: u32, } unsafe impl Send for CObject {} impl CObject { pub fn new() -> Self { CObject { id: i64::MIN, buffer: std::ptr::null_mut(), buffer_length: 0, } } #[allow(clippy::mut_from_ref)] pub fn get_object(&self) -> IsarObject { let bytes = unsafe { slice::from_raw_parts(self.buffer, self.buffer_length as usize) }; IsarObject::from_bytes(bytes) } pub fn get_id(&mut self) -> i64 { self.id } pub fn set_id(&mut self, id: i64) { self.id = id; } pub fn set_object(&mut self, object: Option) { if let Some(object) = object { let bytes = object.as_bytes(); let buffer_length = bytes.len() as u32; let buffer = bytes as *const _ as *mut u8; self.buffer = buffer; self.buffer_length = buffer_length; } else { self.buffer = ptr::null_mut(); self.buffer_length = 0; } } } #[repr(C)] pub struct CObjectSet { objects: *mut CObject, length: u32, } unsafe impl Send for CObjectSet {} impl CObjectSet { pub fn fill_from_vec(&mut self, objects: Vec) { let mut objects = objects.into_boxed_slice(); self.objects = objects.as_mut_ptr(); self.length = objects.len() as u32; std::mem::forget(objects); } #[allow(clippy::mut_from_ref)] pub unsafe fn get_objects(&self) -> &mut [CObject] { std::slice::from_raw_parts_mut(self.objects, self.length as usize) } pub fn get_length(&self) -> usize { self.length as usize } } #[no_mangle] pub unsafe extern "C" fn isar_free_c_object_set(ros: &mut CObjectSet) { Vec::from_raw_parts(ros.objects, ros.length as usize, ros.length as usize); ros.objects = ptr::null_mut(); ros.length = 0; } ================================================ FILE: packages/isar_core_ffi/src/crud.rs ================================================ use crate::c_object_set::{CObject, CObjectSet}; use crate::txn::CIsarTxn; use crate::{from_c_str, BoolSend, UintSend}; use intmap::IntMap; use isar_core::collection::IsarCollection; use isar_core::error::IsarError; use isar_core::index::index_key::IndexKey; use serde_json::Value; use std::os::raw::c_char; #[no_mangle] pub unsafe extern "C" fn isar_get( collection: &'static IsarCollection, txn: &mut CIsarTxn, object: &'static mut CObject, ) -> i64 { isar_try_txn!(txn, move |txn| { let id = object.get_id(); let result = collection.get(txn, id)?; object.set_object(result); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_get_by_index( collection: &'static IsarCollection, txn: &mut CIsarTxn, index_id: u64, key: *mut IndexKey, object: &'static mut CObject, ) -> i64 { let key = *Box::from_raw(key); isar_try_txn!(txn, move |txn| { let result = collection.get_by_index(txn, index_id, &key)?; if let Some((id, obj)) = result { object.set_id(id); object.set_object(Some(obj)); } else { object.set_object(None); } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_get_all( collection: &'static IsarCollection, txn: &mut CIsarTxn, objects: &'static mut CObjectSet, ) -> i64 { isar_try_txn!(txn, move |txn| { for object in objects.get_objects() { let id = object.get_id(); let result = collection.get(txn, id)?; object.set_object(result); } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_get_all_by_index( collection: &'static IsarCollection, txn: &mut CIsarTxn, index_id: u64, keys: *const *mut IndexKey, objects: &'static mut CObjectSet, ) -> i64 { let slice = std::slice::from_raw_parts(keys, objects.get_length()); let keys: Vec = slice.iter().map(|k| *Box::from_raw(*k)).collect(); isar_try_txn!(txn, move |txn| { for (object, key) in objects.get_objects().iter_mut().zip(keys) { let result = collection.get_by_index(txn, index_id, &key)?; if let Some((id, obj)) = result { object.set_id(id); object.set_object(Some(obj)); } else { object.set_object(None); } } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_put( collection: &'static mut IsarCollection, txn: &mut CIsarTxn, object: &'static mut CObject, ) -> i64 { isar_try_txn!(txn, move |txn| { let id = if object.get_id() != i64::MIN { Some(object.get_id()) } else { None }; let id = collection.put(txn, id, object.get_object())?; object.set_id(id); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_put_by_index( collection: &'static mut IsarCollection, txn: &mut CIsarTxn, index_id: u64, object: &'static mut CObject, ) -> i64 { isar_try_txn!(txn, move |txn| { let id = collection.put_by_index(txn, index_id, object.get_object())?; object.set_id(id); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_put_all( collection: &'static IsarCollection, txn: &mut CIsarTxn, objects: &'static mut CObjectSet, ) -> i64 { isar_try_txn!(txn, move |txn| { for object in objects.get_objects() { let id = if object.get_id() != i64::MIN { Some(object.get_id()) } else { None }; let id = collection.put(txn, id, object.get_object())?; object.set_id(id) } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_put_all_by_index( collection: &'static IsarCollection, txn: &mut CIsarTxn, index_id: u64, objects: &'static mut CObjectSet, ) -> i64 { isar_try_txn!(txn, move |txn| { for object in objects.get_objects() { let id = collection.put_by_index(txn, index_id, object.get_object())?; object.set_id(id) } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_delete( collection: &'static IsarCollection, txn: &mut CIsarTxn, id: i64, deleted: &'static mut bool, ) -> i64 { let deleted = BoolSend(deleted); isar_try_txn!(txn, move |txn| { *deleted.0 = collection.delete(txn, id)?; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_delete_by_index( collection: &'static IsarCollection, txn: &mut CIsarTxn, index_id: u64, key: *mut IndexKey, deleted: &'static mut bool, ) -> i64 { let deleted = BoolSend(deleted); let key = *Box::from_raw(key); isar_try_txn!(txn, move |txn| { *deleted.0 = collection.delete_by_index(txn, index_id, &key)?; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_delete_all( collection: &'static IsarCollection, txn: &mut CIsarTxn, ids: *const i64, ids_length: u32, count: &'static mut u32, ) -> i64 { let ids = std::slice::from_raw_parts(ids, ids_length as usize); let count = UintSend(count); isar_try_txn!(txn, move |txn| { let mut n = 0u32; for id in ids { if collection.delete(txn, *id)? { n += 1; } } *count.0 = n; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_delete_all_by_index( collection: &'static IsarCollection, txn: &mut CIsarTxn, index_id: u64, keys: *const *mut IndexKey, keys_length: u32, count: &'static mut u32, ) -> i64 { let slice = std::slice::from_raw_parts(keys, keys_length as usize); let keys: Vec = slice.iter().map(|k| *Box::from_raw(*k)).collect(); let count = UintSend(count); isar_try_txn!(txn, move |txn| { let mut n = 0u32; for key in keys { if collection.delete_by_index(txn, index_id, &key)? { n += 1; } } *count.0 = n; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_clear( collection: &'static IsarCollection, txn: &mut CIsarTxn, ) -> i64 { isar_try_txn!(txn, move |txn| collection.clear(txn)) } #[no_mangle] pub unsafe extern "C" fn isar_json_import( collection: &'static IsarCollection, txn: &mut CIsarTxn, id_name: *const c_char, json_bytes: *const u8, json_length: u32, ) -> i64 { let id_name = from_c_str(id_name).unwrap(); let bytes = std::slice::from_raw_parts(json_bytes, json_length as usize); isar_try_txn!(txn, move |txn| { let json: Value = serde_json::from_slice(bytes).map_err(|_| IsarError::InvalidJson {})?; collection.import_json(txn, id_name, json) }) } #[no_mangle] pub unsafe extern "C" fn isar_count( collection: &'static IsarCollection, txn: &mut CIsarTxn, count: &'static mut i64, ) -> i64 { isar_try_txn!(txn, move |txn| { *count = collection.count(txn)? as i64; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_get_size( collection: &'static IsarCollection, txn: &mut CIsarTxn, include_indexes: bool, include_links: bool, size: &'static mut i64, ) -> i64 { isar_try_txn!(txn, move |txn| { *size = collection.get_size(txn, include_indexes, include_links)? as i64; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_verify( collection: &'static IsarCollection, txn: &mut CIsarTxn, objects: &'static mut CObjectSet, ) -> i64 { let mut objects_map = IntMap::new(); for object in objects.get_objects() { objects_map.insert(object.get_id() as u64, object.get_object()); } isar_try_txn!(txn, move |txn| { collection.verify(txn, &objects_map) }) } ================================================ FILE: packages/isar_core_ffi/src/dart.rs ================================================ use once_cell::sync::OnceCell; static DART_POST_C_OBJECT: OnceCell = OnceCell::new(); pub fn dart_post_int(port: DartPort, value: i64) { let dart_post = DART_POST_C_OBJECT.get().unwrap(); dart_post(port, &mut DartCObject::new(value)); } pub type DartPort = i64; pub type DartPostCObjectFnType = extern "C" fn(port_id: DartPort, message: *mut DartCObject) -> i8; #[repr(C)] pub struct DartCObject { ty: i32, value: DartCObjectValue, } impl DartCObject { fn new(value: i64) -> Self { DartCObject { ty: 3, value: DartCObjectValue { value }, } } } #[repr(C)] union DartCObjectValue { pub value: i64, _union_align: [u64; 5usize], } #[no_mangle] pub unsafe extern "C" fn isar_connect_dart_api(ptr: DartPostCObjectFnType) { let _ = DART_POST_C_OBJECT.set(ptr); } ================================================ FILE: packages/isar_core_ffi/src/error.rs ================================================ use isar_core::error::Result; use once_cell::sync::Lazy; use std::ffi::CString; use std::os::raw::c_char; use std::sync::Mutex; type ErrCounter = (Vec<(i64, String)>, i64); static ERRORS: Lazy> = Lazy::new(|| Mutex::new((vec![], 1))); pub trait DartErrCode { fn into_dart_result_code(self) -> i64; } impl DartErrCode for Result<()> { fn into_dart_result_code(self) -> i64 { if let Err(err) = self { let mut lock = ERRORS.lock().unwrap(); let (errors, counter) = &mut (*lock); if errors.len() > 10 { errors.remove(0); } let err_code = *counter; errors.push((err_code, err.to_string())); *counter = counter.wrapping_add(1); if *counter == 0 { *counter = 1 } err_code } else { 0 } } } #[macro_export] macro_rules! isar_try { { $($token:tt)* } => {{ use crate::error::DartErrCode; #[allow(unused_mut)] { let mut l = || -> isar_core::error::Result<()> { $($token)* Ok(()) }; l().into_dart_result_code() } }} } #[macro_export] macro_rules! isar_try_txn { { $txn:expr, $closure:expr } => { isar_try! { $txn.exec(Box::new($closure))?; } } } #[no_mangle] pub unsafe extern "C" fn isar_get_error(err_code: i64) -> *mut c_char { let lock = ERRORS.lock().unwrap(); let error = lock.0.iter().find(|(code, _)| *code == err_code); if let Some((_, err_msg)) = error { CString::new(err_msg.as_str()).unwrap().into_raw() } else { std::ptr::null_mut() } } ================================================ FILE: packages/isar_core_ffi/src/filter.rs ================================================ use crate::from_c_str; use isar_core::collection::IsarCollection; use isar_core::error::illegal_arg; use isar_core::error::Result; use isar_core::object::data_type::DataType; use isar_core::object::property::Property; use isar_core::query::filter::*; use std::os::raw::c_char; use std::slice; #[no_mangle] pub unsafe extern "C" fn isar_filter_static(filter: *mut *const Filter, value: bool) { let query_filter = Filter::stat(value); let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } #[no_mangle] pub unsafe extern "C" fn isar_filter_and_or_xor( filter: *mut *const Filter, and: bool, exclusive: bool, conditions: *mut *mut Filter, length: u32, ) { let filters = slice::from_raw_parts(conditions, length as usize) .iter() .map(|f| *Box::from_raw(*f)) .collect(); let and_or = if and { Filter::and(filters) } else if exclusive { Filter::xor(filters) } else { Filter::or(filters) }; let ptr = Box::into_raw(Box::new(and_or)); filter.write(ptr); } #[no_mangle] pub unsafe extern "C" fn isar_filter_not(filter: *mut *const Filter, condition: *mut Filter) { let condition = *Box::from_raw(condition); let not = Filter::not(condition); let ptr = Box::into_raw(Box::new(not)); filter.write(ptr); } pub fn get_property( collection: &IsarCollection, embedded_col_id: u64, property_id: u64, ) -> Result<&Property> { let properties = if embedded_col_id != 0 { if let Some(properties) = collection.embedded_properties.get(embedded_col_id) { properties } else { return illegal_arg("Embedded collection does not exist."); } } else { &collection.properties }; if let Some(property) = properties.get(property_id as usize) { Ok(property) } else { illegal_arg("Property does not exist.") } } #[no_mangle] pub unsafe extern "C" fn isar_filter_object( collection: &IsarCollection, filter: *mut *const Filter, condition: *mut Filter, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let condition = if !condition.is_null() { Some(*Box::from_raw(condition)) } else { None }; let query_filter = Filter::object(property, condition)?; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_filter_link( collection: &IsarCollection, filter: *mut *const Filter, condition: *mut Filter, link_id: u64, ) -> i64 { isar_try! { let condition = *Box::from_raw(condition); let query_filter = Filter::link(collection, link_id, condition)?; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_filter_link_length( collection: &IsarCollection, filter: *mut *const Filter, lower: u32, upper: u32, link_id: u64, ) -> i64 { isar_try! { let query_filter = Filter::link_length(collection, link_id, lower as usize,upper as usize)?; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_filter_list_length( collection: &IsarCollection, filter: *mut *const Filter, lower: u32, upper: u32, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let query_filter = Filter::list_length(property, lower as usize,upper as usize)?; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_filter_null( collection: &IsarCollection, filter: *mut *const Filter, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let query_filter = Filter::null(property); let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[macro_export] macro_rules! include_num { ($type:ident, $lower:ident, $include_lower:expr, $upper:ident, $include_upper:expr) => {{ let lower = $lower.clamp($type::MIN as i64, $type::MAX as i64) as $type; let lower = if !$include_lower { lower.checked_add(1) } else { Some(lower) }; let upper = $upper.clamp($type::MIN as i64, $type::MAX as i64) as $type; let upper = if !$include_upper { upper.checked_sub(1) } else { Some(upper) }; (lower, upper) }}; } #[no_mangle] pub unsafe extern "C" fn isar_filter_id( filter: *mut *const Filter, lower: i64, include_lower: bool, upper: i64, include_upper: bool, ) { let query_filter = if let (Some(lower), Some(upper)) = include_num!(i64, lower, include_lower, upper, include_upper) { Filter::id(lower, upper) } else { Filter::stat(false) }; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } #[no_mangle] pub unsafe extern "C" fn isar_filter_long( collection: &IsarCollection, filter: *mut *const Filter, lower: i64, include_lower: bool, upper: i64, include_upper: bool, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let query_filter = if property.data_type == DataType::Byte || property.data_type == DataType::ByteList || property.data_type == DataType::Bool || property.data_type == DataType::BoolList { if let (Some(lower), Some(upper)) = include_num!(u8, lower, include_lower, upper, include_upper) { Filter::byte(property, lower, upper)? } else { Filter::stat(false) } } else if property.data_type == DataType::Int || property.data_type == DataType::IntList { if let (Some(lower), Some(upper)) = include_num!(i32, lower, include_lower, upper, include_upper) { Filter::int(property, lower, upper)? } else { Filter::stat(false) } } else { if let (Some(lower), Some(upper)) = include_num!(i64, lower, include_lower, upper, include_upper) { Filter::long(property, lower, upper)? } else { Filter::stat(false) } }; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_filter_double( collection: &IsarCollection, filter: *mut *const Filter, lower: f64, upper: f64, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let query_filter = if property.data_type == DataType::Float || property.data_type == DataType::FloatList { let lower = if lower.is_finite() { lower.clamp(f32::MIN as f64, f32::MAX as f64) } else { lower }; let upper = if upper.is_finite() { upper.clamp(f32::MIN as f64, f32::MAX as f64) } else { upper }; Filter::float(property, lower as f32, upper as f32)? } else { Filter::double(property, lower, upper)? }; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } unsafe fn get_lower_str(lower: Option>, include_lower: bool) -> Option> { if include_lower { lower } else if let Some(mut lower) = lower { if let Some(last) = lower.pop() { if last < 255 { lower.push(last + 1); } else { lower.push(255); lower.push(0); } } else { lower.push(0); } Some(lower) } else { Some(vec![]) } } unsafe fn get_upper_str(upper: Option>, include_upper: bool) -> Option>> { if include_upper { Some(upper) } else if let Some(mut upper) = upper { if upper.is_empty() { Some(None) } else { for i in (upper.len() - 1)..0 { if upper[i] > 0 { upper[i] -= 1; return Some(Some(upper)); } } Some(Some(vec![])) } } else { // cannot exclude upper limit None } } #[no_mangle] pub unsafe extern "C" fn isar_filter_string( collection: &IsarCollection, filter: *mut *const Filter, lower: *const c_char, include_lower: bool, upper: *const c_char, include_upper: bool, case_sensitive: bool, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let lower_bytes = Filter::string_to_bytes(from_c_str(lower)?, case_sensitive); let lower = get_lower_str(lower_bytes, include_lower); let upper_bytes = Filter::string_to_bytes(from_c_str(upper)?, case_sensitive); let upper = get_upper_str(upper_bytes, include_upper); let query_filter = if let Some(upper) = upper { Filter::byte_string(property, lower, upper, case_sensitive)? } else { Filter::stat(false) }; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } #[macro_export] macro_rules! filter_string_ffi { ($filter_name:ident, $function_name:ident) => { #[no_mangle] pub unsafe extern "C" fn $function_name( collection: &IsarCollection, filter: *mut *const Filter, value: *const c_char, case_sensitive: bool, embedded_col_id: u64, property_id: u64, ) -> i64 { isar_try! { let property = get_property(collection, embedded_col_id, property_id)?; let str = from_c_str(value)?.unwrap(); let query_filter = isar_core::query::filter::Filter::$filter_name(property, str, case_sensitive)?; let ptr = Box::into_raw(Box::new(query_filter)); filter.write(ptr); } } } } filter_string_ffi!(string_starts_with, isar_filter_string_starts_with); filter_string_ffi!(string_ends_with, isar_filter_string_ends_with); filter_string_ffi!(string_contains, isar_filter_string_contains); filter_string_ffi!(string_matches, isar_filter_string_matches); ================================================ FILE: packages/isar_core_ffi/src/index_key.rs ================================================ use crate::from_c_str; use isar_core::index::index_key::IndexKey; use isar_core::object::isar_object::IsarObject; use paste::paste; use std::os::raw::c_char; #[no_mangle] pub unsafe extern "C" fn isar_key_create(key: *mut *const IndexKey) { let index_key = IndexKey::new(); let ptr = Box::into_raw(Box::new(index_key)); key.write(ptr); } #[no_mangle] pub unsafe extern "C" fn isar_key_increase(key: &mut IndexKey) -> bool { key.increase() } #[no_mangle] pub unsafe extern "C" fn isar_key_decrease(key: &mut IndexKey) -> bool { key.decrease() } #[no_mangle] pub extern "C" fn isar_key_add_byte(key: &mut IndexKey, value: u8) { key.add_byte(value); } #[no_mangle] pub extern "C" fn isar_key_add_int(key: &mut IndexKey, value: i32) { key.add_int(value); } #[no_mangle] pub extern "C" fn isar_key_add_long(key: &mut IndexKey, value: i64) { key.add_long(value); } #[no_mangle] pub extern "C" fn isar_key_add_float(key: &mut IndexKey, value: f64) { let value = if value.is_finite() { value.clamp(f32::MIN as f64, f32::MAX as f64) } else { value }; key.add_float(value as f32); } #[no_mangle] pub extern "C" fn isar_key_add_double(key: &mut IndexKey, value: f64) { key.add_double(value); } #[no_mangle] pub unsafe extern "C" fn isar_key_add_string( key: &mut IndexKey, value: *const c_char, case_sensitive: bool, ) { let value = from_c_str(value).unwrap(); key.add_string(value, case_sensitive) } #[no_mangle] pub unsafe extern "C" fn isar_key_add_string_hash( key: &mut IndexKey, value: *const c_char, case_sensitive: bool, ) { let value = from_c_str(value).unwrap(); let hash = IsarObject::hash_string(value, case_sensitive, 0); key.add_hash(hash); } #[no_mangle] pub unsafe extern "C" fn isar_key_add_string_list_hash( key: &mut IndexKey, value: *const *const c_char, length: u32, case_sensitive: bool, ) { let value = if !value.is_null() { let raw_strings = std::slice::from_raw_parts(value, length as usize); let mut strings = vec![]; for raw_str in raw_strings { let str = from_c_str(*raw_str).unwrap(); strings.push(str); } Some(strings) } else { None }; let hash = IsarObject::hash_string_list(value, case_sensitive, 0); key.add_hash(hash); } #[macro_export] macro_rules! hash_list { ($name:ident, $type:ty) => { paste! { #[no_mangle] pub unsafe extern "C" fn []( key: &mut IndexKey, value: *const $type, length: u32, ) { let value = if !value.is_null() { Some(std::slice::from_raw_parts(value, length as usize)) } else { None }; let hash = IsarObject::hash_list(value, 0); key.add_hash(hash); } } }; } hash_list!(byte, u8); hash_list!(int, i32); hash_list!(long, i64); ================================================ FILE: packages/isar_core_ffi/src/instance.rs ================================================ use crate::dart::{dart_post_int, DartPort}; use crate::error::DartErrCode; use crate::from_c_str; use crate::txn::run_async; use crate::txn::CIsarTxn; use crate::CharsSend; use isar_core::collection::IsarCollection; use isar_core::error::{illegal_arg, Result}; use isar_core::instance::{CompactCondition, IsarInstance}; use isar_core::schema::Schema; use std::ffi::CString; use std::os::raw::c_char; use std::sync::Arc; include!(concat!(env!("OUT_DIR"), "/version.rs")); struct IsarInstanceSend(*mut *const IsarInstance); unsafe impl Send for IsarInstanceSend {} #[no_mangle] pub unsafe extern "C" fn isar_version() -> *const c_char { ISAR_VERSION.as_ptr() as *const c_char } #[no_mangle] pub unsafe extern "C" fn isar_instance_create( isar: *mut *const IsarInstance, name: *const c_char, path: *const c_char, schema_json: *const c_char, max_size_mib: i64, relaxed_durability: bool, compact_min_file_size: u32, compact_min_bytes: u32, compact_min_ratio: f64, ) -> i64 { let open = || -> Result<()> { let name = from_c_str(name).unwrap().unwrap(); let path = from_c_str(path).unwrap(); let schema_json = from_c_str(schema_json).unwrap().unwrap(); let schema = Schema::from_json(schema_json.as_bytes())?; let compact_condition = if compact_min_ratio.is_nan() { None } else { Some(CompactCondition { min_file_size: compact_min_file_size as u64, min_bytes: compact_min_bytes as u64, min_ratio: compact_min_ratio, }) }; let instance = IsarInstance::open( name, path, schema, max_size_mib as usize, relaxed_durability, compact_condition, )?; isar.write(Arc::into_raw(instance)); Ok(()) }; open().into_dart_result_code() } #[no_mangle] pub unsafe extern "C" fn isar_instance_create_async( isar: *mut *const IsarInstance, name: *const c_char, path: *const c_char, schema_json: *const c_char, max_size_mib: i64, relaxed_durability: bool, compact_min_file_size: u32, compact_min_bytes: u32, compact_min_ratio: f64, port: DartPort, ) { let isar = IsarInstanceSend(isar); let name = CharsSend(name); let path = CharsSend(path); let schema_json = CharsSend(schema_json); run_async(move || { let isar = isar; let name = name; let path = path; let schema_json = schema_json; let result = isar_instance_create( isar.0, name.0, path.0, schema_json.0, max_size_mib, relaxed_durability, compact_min_file_size, compact_min_bytes, compact_min_ratio, ); dart_post_int(port, result); }); } #[no_mangle] pub unsafe extern "C" fn isar_instance_close(isar: *const IsarInstance) -> bool { let isar = Arc::from_raw(isar); isar.close() } #[no_mangle] pub unsafe extern "C" fn isar_instance_close_and_delete(isar: *const IsarInstance) -> bool { let isar = Arc::from_raw(isar); isar.close_and_delete() } #[no_mangle] pub unsafe extern "C" fn isar_instance_get_path(isar: &'static IsarInstance) -> *mut c_char { CString::new(isar.dir.as_str()).unwrap().into_raw() } #[no_mangle] pub unsafe extern "C" fn isar_instance_get_collection<'a>( isar: &'a IsarInstance, collection: *mut &'a IsarCollection, collection_id: u64, ) -> i64 { isar_try! { let new_collection = isar.collections.iter().find(|c| c.id == collection_id); if let Some(new_collection) = new_collection { collection.write(new_collection); } else { illegal_arg("Collection id is invalid.")?; } } } #[no_mangle] pub unsafe extern "C" fn isar_instance_get_size( instance: &'static IsarInstance, txn: &mut CIsarTxn, include_indexes: bool, include_links: bool, size: &'static mut i64, ) -> i64 { isar_try_txn!(txn, move |txn| { *size = instance.get_size(txn, include_indexes, include_links)? as i64; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_instance_copy_to_file( instance: &'static IsarInstance, path: *const c_char, port: DartPort, ) { let path = CharsSend(path); run_async(move || { let path = path; let path = from_c_str(path.0).unwrap().unwrap(); let result = instance.copy_to_file(path); dart_post_int(port, result.into_dart_result_code()); }); } #[no_mangle] pub unsafe extern "C" fn isar_instance_verify( instance: &'static IsarInstance, txn: &mut CIsarTxn, ) -> i64 { isar_try_txn!(txn, move |txn| { instance.verify(txn) }) } #[no_mangle] pub unsafe extern "C" fn isar_get_offsets( collection: &IsarCollection, embedded_col_id: u64, offsets: *mut u32, ) -> u32 { let properties = if embedded_col_id == 0 { &collection.properties } else { collection.embedded_properties.get(embedded_col_id).unwrap() }; let offsets = std::slice::from_raw_parts_mut(offsets, properties.len()); for (i, p) in properties.iter().enumerate() { offsets[i] = p.offset as u32; } let property = properties.iter().max_by_key(|p| p.offset); property.map_or(2, |p| p.offset + p.data_type.get_static_size()) as u32 } ================================================ FILE: packages/isar_core_ffi/src/lib.rs ================================================ #![allow(clippy::missing_safety_doc)] use isar_core::error::{illegal_arg, Result}; use std::ffi::CStr; use std::ffi::CString; use std::mem; use std::os::raw::c_char; use unicode_segmentation::UnicodeSegmentation; #[macro_use] mod error; pub mod c_object_set; pub mod crud; mod dart; pub mod filter; pub mod index_key; pub mod instance; pub mod link; pub mod query; pub mod query_aggregation; pub mod txn; pub mod watchers; pub unsafe fn from_c_str<'a>(str: *const c_char) -> Result> { if !str.is_null() { match CStr::from_ptr(str).to_str() { Ok(str) => Ok(Some(str)), Err(_) => illegal_arg("The provided String is not valid."), } } else { Ok(None) } } pub struct UintSend(&'static mut u32); unsafe impl Send for UintSend {} pub struct BoolSend(&'static mut bool); unsafe impl Send for BoolSend {} pub struct CharsSend(*const c_char); unsafe impl Send for CharsSend {} #[no_mangle] pub unsafe extern "C" fn isar_find_word_boundaries( input_bytes: *const u8, length: u32, number_words: *mut u32, ) -> *mut u32 { let bytes = std::slice::from_raw_parts(input_bytes, length as usize); let str = std::str::from_utf8_unchecked(bytes); let mut result = vec![]; for (offset, word) in str.unicode_word_indices() { result.push(offset as u32); result.push((offset + word.len()) as u32); } result.shrink_to_fit(); number_words.write((result.len() / 2) as u32); let result_ptr = result.as_mut_ptr(); mem::forget(result); result_ptr } #[no_mangle] pub unsafe extern "C" fn isar_free_word_boundaries(boundaries: *mut u32, word_count: u32) { let len = (word_count * 2) as usize; Vec::from_raw_parts(boundaries, len, len); } #[no_mangle] pub unsafe extern "C" fn isar_free_string(string: *mut c_char) { let _ = CString::from_raw(string); } ================================================ FILE: packages/isar_core_ffi/src/link.rs ================================================ use crate::txn::CIsarTxn; use isar_core::collection::IsarCollection; use isar_core::error::Result; use itertools::Itertools; #[no_mangle] pub unsafe extern "C" fn isar_link( collection: &'static IsarCollection, txn: &mut CIsarTxn, link_id: u64, id: i64, target_id: i64, ) -> i64 { isar_try_txn!(txn, move |txn| -> Result<()> { collection.link(txn, link_id, id, target_id)?; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_link_unlink( collection: &'static IsarCollection, txn: &mut CIsarTxn, link_id: u64, id: i64, target_id: i64, ) -> i64 { isar_try_txn!(txn, move |txn| -> Result<()> { collection.unlink(txn, link_id, id, target_id)?; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_link_unlink_all( collection: &'static IsarCollection, txn: &mut CIsarTxn, link_id: u64, id: i64, ) -> i64 { isar_try_txn!(txn, move |txn| -> Result<()> { collection.unlink_all(txn, link_id, id)?; Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_link_update_all( collection: &'static IsarCollection, txn: &mut CIsarTxn, link_id: u64, id: i64, ids: *const i64, link_count: u32, unlink_count: u32, replace: bool, ) -> i64 { let ids = std::slice::from_raw_parts(ids, (link_count + unlink_count) as usize); isar_try_txn!(txn, move |txn| { if replace { collection.unlink_all(txn, link_id, id)?; } for target_id in ids.iter().take(link_count as usize) { collection.link(txn, link_id, id, *target_id)?; } for target_id in ids .iter() .skip(link_count as usize) .take(unlink_count as usize) { collection.unlink(txn, link_id, id, *target_id)?; } Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_link_verify( collection: &'static IsarCollection, txn: &mut CIsarTxn, link_id: u64, ids: *const i64, ids_count: u32, ) -> i64 { let ids = std::slice::from_raw_parts(ids, ids_count as usize); let links = ids.iter().copied().tuples().collect_vec(); isar_try_txn!(txn, move |txn| -> Result<()> { collection.verify_link(txn, link_id, &links)?; Ok(()) }) } ================================================ FILE: packages/isar_core_ffi/src/query.rs ================================================ use super::c_object_set::{CObject, CObjectSet}; use crate::filter::get_property; use crate::txn::CIsarTxn; use crate::{from_c_str, UintSend}; use isar_core::collection::IsarCollection; use isar_core::index::index_key::IndexKey; use isar_core::query::filter::Filter; use isar_core::query::query_builder::QueryBuilder; use isar_core::query::{Query, Sort}; use std::os::raw::c_char; #[no_mangle] pub extern "C" fn isar_qb_create(collection: &IsarCollection) -> *mut QueryBuilder { let builder = collection.new_query_builder(); Box::into_raw(Box::new(builder)) } #[no_mangle] pub unsafe extern "C" fn isar_qb_add_id_where_clause( builder: &mut QueryBuilder, start_id: i64, end_id: i64, ) -> i64 { isar_try! { builder.add_id_where_clause(start_id, end_id)?; } } #[no_mangle] pub unsafe extern "C" fn isar_qb_add_index_where_clause( builder: &mut QueryBuilder, index_id: u64, lower_key: *mut IndexKey, upper_key: *mut IndexKey, sort_asc: bool, skip_duplicates: bool, ) -> i64 { let lower_key = *Box::from_raw(lower_key); let upper_key = *Box::from_raw(upper_key); let sort = if sort_asc { Sort::Ascending } else { Sort::Descending }; isar_try! { builder.add_index_where_clause( index_id , lower_key, upper_key, sort, skip_duplicates, )?; } } #[no_mangle] pub unsafe extern "C" fn isar_qb_add_link_where_clause( builder: &mut QueryBuilder, source_collection: &IsarCollection, link_id: u64, id: i64, ) -> i64 { isar_try! { builder.add_link_where_clause(source_collection, link_id, id)?; } } #[no_mangle] pub unsafe extern "C" fn isar_qb_set_filter(builder: &mut QueryBuilder, filter: *mut Filter) { let filter = *Box::from_raw(filter); builder.set_filter(filter); } #[no_mangle] pub unsafe extern "C" fn isar_qb_add_sort_by( builder: &mut QueryBuilder, property_id: u64, asc: bool, ) -> i64 { let sort = if asc { Sort::Ascending } else { Sort::Descending }; isar_try! { let property = get_property(builder.collection, 0, property_id)?; builder.add_sort(property, sort)?; } } #[no_mangle] pub unsafe extern "C" fn isar_qb_add_distinct_by( builder: &mut QueryBuilder, property_id: u64, case_sensitive: bool, ) -> i64 { isar_try! { let property = get_property(builder.collection, 0, property_id)?; builder.add_distinct(property, case_sensitive); } } #[no_mangle] pub unsafe extern "C" fn isar_qb_set_offset_limit( builder: &mut QueryBuilder, offset: i64, limit: i64, ) { let offset = if offset < 0 { 0 } else { offset as usize }; let limit = if limit < 0 { usize::MAX } else { limit as usize }; builder.set_offset(offset); builder.set_limit(limit); } #[no_mangle] pub unsafe extern "C" fn isar_qb_build(builder: *mut QueryBuilder) -> *mut Query { let query = Box::from_raw(builder).build(); Box::into_raw(Box::new(query)) } #[no_mangle] pub unsafe extern "C" fn isar_q_free(query: *mut Query) { let _ = Box::from_raw(query); } #[no_mangle] pub unsafe extern "C" fn isar_q_find( query: &'static Query, txn: &mut CIsarTxn, result: &'static mut CObjectSet, limit: u32, ) -> i64 { isar_try_txn!(txn, move |txn| { let mut objects = vec![]; let mut count = 0; query.find_while(txn, |id, object| { let mut raw_obj = CObject::new(); raw_obj.set_id(id); raw_obj.set_object(Some(object)); objects.push(raw_obj); count += 1; count < limit })?; result.fill_from_vec(objects); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_q_delete( query: &'static Query, collection: &'static IsarCollection, txn: &mut CIsarTxn, limit: u32, count: &'static mut u32, ) -> i64 { let limit = limit as usize; let count = UintSend(count); isar_try_txn!(txn, move |txn| { let mut ids_to_delete = vec![]; query.find_while(txn, |id, _| { ids_to_delete.push(id); ids_to_delete.len() <= limit })?; *count.0 = ids_to_delete.len() as u32; for id in ids_to_delete { collection.delete(txn, id)?; } Ok(()) }) } struct JsonBytes(*mut *mut u8); unsafe impl Send for JsonBytes {} struct JsonLen(*mut u32); unsafe impl Send for JsonLen {} #[no_mangle] pub unsafe extern "C" fn isar_q_export_json( query: &'static Query, collection: &'static IsarCollection, txn: &mut CIsarTxn, id_name: *const c_char, json_bytes: *mut *mut u8, json_length: *mut u32, ) -> i64 { let id_name = from_c_str(id_name).unwrap(); let json = JsonBytes(json_bytes); let json_length = JsonLen(json_length); isar_try_txn!(txn, move |txn| { let json = json; let json_length = json_length; let exported_json = query.export_json(txn, collection, id_name, true)?; let bytes = serde_json::to_vec(&exported_json).unwrap(); let mut bytes = bytes.into_boxed_slice(); json_length.0.write(bytes.len() as u32); json.0.write(bytes.as_mut_ptr()); std::mem::forget(bytes); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_free_json(json_bytes: *mut u8, json_length: u32) { Vec::from_raw_parts(json_bytes, json_length as usize, json_length as usize); } ================================================ FILE: packages/isar_core_ffi/src/query_aggregation.rs ================================================ use crate::filter::get_property; use crate::txn::CIsarTxn; use isar_core::collection::IsarCollection; use isar_core::error::Result; use isar_core::object::data_type::DataType; use isar_core::object::isar_object::IsarObject; use isar_core::object::property::Property; use isar_core::query::Query; use isar_core::txn::IsarTxn; use std::cmp::Ordering; pub enum AggregationResult { Long(i64), Double(f64), Null, } #[derive(PartialEq)] #[repr(u8)] pub enum AggregationOp { Min, Max, Sum, Average, Count, IsEmpty, } impl AggregationOp { fn from_u8(index: u8) -> AggregationOp { match index { 0 => AggregationOp::Min, 1 => AggregationOp::Max, 2 => AggregationOp::Sum, 3 => AggregationOp::Average, 4 => AggregationOp::Count, 5 => AggregationOp::IsEmpty, _ => unreachable!(), } } } const EMPTY_PROP: &Property = &Property { name: String::new(), data_type: DataType::Bool, offset: 0, target_id: None, }; fn aggregate( query: &Query, txn: &mut IsarTxn, op: AggregationOp, property: Option<&Property>, ) -> Result { let mut count = 0usize; let (mut long_value, mut double_value) = if op == AggregationOp::Min { (i64::MAX, f64::INFINITY) } else if op == AggregationOp::Max { (i64::MIN, f64::NEG_INFINITY) } else { (0, 0.0) }; let min_max_cmp = if op == AggregationOp::Max { Ordering::Greater } else { Ordering::Less }; let property = property.unwrap_or(EMPTY_PROP); query.find_while(txn, |_, obj| { match op { AggregationOp::Min | AggregationOp::Max => { if obj.is_null(property.offset, property.data_type) { return true; } count += 1; match property.data_type { DataType::Int | DataType::Long => { let value = if property.data_type == DataType::Int { obj.read_int(property.offset) as i64 } else { obj.read_long(property.offset) }; if value.cmp(&long_value) == min_max_cmp { long_value = value; } } DataType::Float | DataType::Double => { let value = if property.data_type == DataType::Float { obj.read_float(property.offset) as f64 } else { obj.read_double(property.offset) }; if value.total_cmp(&double_value) == min_max_cmp { double_value = value; } } _ => {} } } AggregationOp::Sum | AggregationOp::Average => { if obj.is_null(property.offset, property.data_type) { return true; } count += 1; match property.data_type { DataType::Int => { long_value = long_value.saturating_add(obj.read_int(property.offset) as i64) } DataType::Long => { long_value = long_value.saturating_add(obj.read_long(property.offset)) } DataType::Float => double_value += obj.read_float(property.offset) as f64, DataType::Double => double_value += obj.read_double(property.offset), _ => {} } } AggregationOp::Count => { count += 1; } AggregationOp::IsEmpty => { count += 1; return false; } } true })?; match op { AggregationOp::Min | AggregationOp::Max | AggregationOp::Average => { if count == 0 { return Ok(AggregationResult::Null); } } _ => {} } let result = match op { AggregationOp::Average => match property.data_type { DataType::Int | DataType::Long => { AggregationResult::Double((long_value as f64) / (count as f64)) } DataType::Float | DataType::Double => { AggregationResult::Double(double_value / (count as f64)) } _ => AggregationResult::Null, }, AggregationOp::Count => AggregationResult::Long(count as i64), AggregationOp::IsEmpty => AggregationResult::Long(if count > 0 { 0 } else { 1 }), _ => match property.data_type { DataType::Int | DataType::Long => AggregationResult::Long(long_value), DataType::Float | DataType::Double => AggregationResult::Double(double_value), _ => AggregationResult::Null, }, }; Ok(result) } pub struct AggregationResultSend(*mut *const AggregationResult); unsafe impl Send for AggregationResultSend {} #[no_mangle] pub unsafe extern "C" fn isar_q_aggregate( collection: &'static IsarCollection, query: &'static Query, txn: &mut CIsarTxn, operation: u8, property_id: u64, result: *mut *const AggregationResult, ) -> i64 { let op = AggregationOp::from_u8(operation); let result = AggregationResultSend(result); isar_try_txn!(txn, move |txn| { let result = result; let property = if op != AggregationOp::Count { Some(get_property(collection, 0, property_id)?) } else { None }; let aggregate_result = aggregate(query, txn, op, property)?; result.0.write(Box::into_raw(Box::new(aggregate_result))); Ok(()) }) } #[no_mangle] pub unsafe extern "C" fn isar_q_aggregate_long_result(result: &AggregationResult) -> i64 { match result { AggregationResult::Long(long) => *long, AggregationResult::Double(double) => *double as i64, AggregationResult::Null => IsarObject::NULL_LONG, } } #[no_mangle] pub unsafe extern "C" fn isar_q_aggregate_double_result(result: &AggregationResult) -> f64 { match result { AggregationResult::Long(long) => *long as f64, AggregationResult::Double(double) => *double, AggregationResult::Null => IsarObject::NULL_DOUBLE, } } ================================================ FILE: packages/isar_core_ffi/src/txn.rs ================================================ use crate::dart::{dart_post_int, DartPort}; use crate::error::DartErrCode; use isar_core::error::{IsarError, Result}; use isar_core::instance::IsarInstance; use isar_core::txn::IsarTxn; use once_cell::sync::Lazy; use std::borrow::BorrowMut; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::sync::Arc; use std::sync::Mutex; use threadpool::{Builder, ThreadPool}; static THREAD_POOL: Lazy> = Lazy::new(|| Mutex::new(Builder::new().thread_name("isarworker".to_string()).build())); pub fn run_async(job: F) { THREAD_POOL.lock().unwrap().execute(job); } type AsyncJob = (Box, bool); #[no_mangle] pub unsafe extern "C" fn isar_txn_begin( isar: &'static IsarInstance, txn: *mut *const CIsarTxn, sync: bool, write: bool, silent: bool, port: DartPort, ) -> i64 { isar_try! { let new_txn = if sync { CIsarTxn::begin_sync(isar, write, silent)? } else { CIsarTxn::begin_async(isar, write, silent, port) }; let txn_ptr = Box::into_raw(Box::new(new_txn)); txn.write(txn_ptr); } } #[no_mangle] pub unsafe extern "C" fn isar_txn_finish(txn: *mut CIsarTxn, commit: bool) -> i64 { let txn = Box::from_raw(txn); isar_try! { txn.finish(commit)?; } } pub struct IsarTxnSend(IsarTxn<'static>); unsafe impl Send for IsarTxnSend {} pub enum CIsarTxn { Sync { txn: Option>, }, Async { tx: Sender, port: DartPort, txn: Arc>>, }, } impl CIsarTxn { fn begin_sync(isar: &'static IsarInstance, write: bool, silent: bool) -> Result { let sync_txn = CIsarTxn::Sync { txn: Some(isar.begin_txn(write, silent)?), }; Ok(sync_txn) } fn begin_async( isar: &'static IsarInstance, write: bool, silent: bool, port: DartPort, ) -> CIsarTxn { let (tx, rx): (Sender, Receiver) = mpsc::channel(); let txn = Arc::new(Mutex::new(None)); let txn_clone = txn.clone(); run_async(move || { let new_txn = isar.begin_txn(write, silent); match new_txn { Ok(new_txn) => { txn_clone.lock().unwrap().replace(IsarTxnSend(new_txn)); dart_post_int(port, 0); loop { let (job, stop) = rx.recv().unwrap(); job(); if stop { break; } } } Err(e) => { dart_post_int(port, Err(e).into_dart_result_code()); } } }); CIsarTxn::Async { tx, port, txn } } pub fn exec_async_internal Result<()> + Send + 'static>( job: F, port: DartPort, tx: Sender, stop: bool, ) { let handle_response_job = move || { let result = job().into_dart_result_code(); dart_post_int(port, result as i64); }; tx.send((Box::new(handle_response_job), stop)).unwrap(); } pub fn exec( &mut self, job: Box Result<()> + Send + 'static>, ) -> Result<()> { match self.borrow_mut() { CIsarTxn::Sync { ref mut txn } => { if let Some(ref mut txn) = txn { job(txn) } else { Err(IsarError::TransactionClosed {}) } } CIsarTxn::Async { txn, tx, port } => { let txn = txn.clone(); let job = move || -> Result<()> { let mut lock = txn.lock().unwrap(); if let Some(ref mut txn) = *lock { job(&mut txn.0) } else { Err(IsarError::TransactionClosed {}) } }; CIsarTxn::exec_async_internal(job, *port, tx.clone(), false); Ok(()) } } } pub fn finish(self, commit: bool) -> Result<()> { match self { CIsarTxn::Sync { mut txn } => { if let Some(txn) = txn.take() { if commit { txn.commit() } else { txn.abort(); Ok(()) } } else { Err(IsarError::TransactionClosed {}) } } CIsarTxn::Async { txn, tx, port } => { let txn = txn.clone(); let job = move || -> Result<()> { let mut lock = txn.lock().unwrap(); if let Some(txn) = (*lock).take() { if commit { txn.0.commit() } else { txn.0.abort(); Ok(()) } } else { Err(IsarError::TransactionClosed {}) } }; CIsarTxn::exec_async_internal(job, port, tx.clone(), true); Ok(()) } } } } ================================================ FILE: packages/isar_core_ffi/src/watchers.rs ================================================ use isar_core::collection::IsarCollection; use isar_core::instance::IsarInstance; use isar_core::query::Query; use isar_core::watch::WatchHandle; use crate::dart::{dart_post_int, DartPort}; #[no_mangle] pub extern "C" fn isar_watch_collection( isar: &IsarInstance, collection: &IsarCollection, port: DartPort, ) -> *mut WatchHandle { let handle = isar.watch_collection( collection, Box::new(move || { dart_post_int(port, 1); }), ); Box::into_raw(Box::new(handle)) } #[no_mangle] pub unsafe extern "C" fn isar_watch_object( isar: &IsarInstance, collection: &IsarCollection, id: i64, port: DartPort, ) -> *mut WatchHandle { let handle = isar.watch_object( collection, id, Box::new(move || { dart_post_int(port, 1); }), ); Box::into_raw(Box::new(handle)) } #[no_mangle] pub extern "C" fn isar_watch_query( isar: &IsarInstance, collection: &IsarCollection, query: &Query, port: DartPort, ) -> *mut WatchHandle { let handle = isar.watch_query( collection, query.clone(), Box::new(move || { dart_post_int(port, 1); }), ); Box::into_raw(Box::new(handle)) } #[no_mangle] pub unsafe extern "C" fn isar_stop_watching(handle: *mut WatchHandle) { Box::from_raw(handle).stop(); } ================================================ FILE: packages/isar_flutter_libs/.pubignore ================================================ !*.so !*.a !*.dylib !*.dll !*.xcframework/ ================================================ FILE: packages/isar_flutter_libs/CHANGELOG.md ================================================ See [Isar Changelog](https://pub.dev/packages/isar/changelog) ================================================ FILE: packages/isar_flutter_libs/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: packages/isar_flutter_libs/README.md ================================================ ### Flutter binaries for the [Isar Database](https://github.com/isar-community/isar) please go there for documentation. ================================================ FILE: packages/isar_flutter_libs/android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: packages/isar_flutter_libs/android/build.gradle ================================================ group 'dev.isar.isar_flutter_libs' version '1.0' buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' } } rootProject.allprojects { repositories { google() mavenCentral() } } apply plugin: 'com.android.library' android { if (project.android.hasProperty("namespace")) { namespace 'dev.isar.isar_flutter_libs' } compileSdkVersion 34 defaultConfig { minSdkVersion 16 } } dependencies { implementation "androidx.startup:startup-runtime:1.1.1" } ================================================ FILE: packages/isar_flutter_libs/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Sat Nov 12 16:30:49 CET 2022 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: packages/isar_flutter_libs/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: packages/isar_flutter_libs/android/settings.gradle ================================================ rootProject.name = 'isar_flutter_libs' ================================================ FILE: packages/isar_flutter_libs/android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: packages/isar_flutter_libs/android/src/main/java/dev/isar/isar_flutter_libs/IsarFlutterLibsPlugin.java ================================================ package dev.isar.isar_flutter_libs; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; public class IsarFlutterLibsPlugin implements FlutterPlugin { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } } ================================================ FILE: packages/isar_flutter_libs/ios/.gitignore ================================================ .idea/ .vagrant/ .sconsign.dblite .svn/ .DS_Store *.swp profile DerivedData/ build/ GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m .generated/ *.pbxuser *.mode1v3 *.mode2v3 *.perspectivev3 !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 xcuserdata *.moved-aside *.pyc *sync/ Icon? .tags* /Flutter/Generated.xcconfig /Flutter/ephemeral/ /Flutter/flutter_export_environment.sh ================================================ FILE: packages/isar_flutter_libs/ios/Assets/.gitkeep ================================================ ================================================ FILE: packages/isar_flutter_libs/ios/Classes/IsarFlutterLibsPlugin.h ================================================ #import @interface IsarFlutterLibsPlugin : NSObject @end ================================================ FILE: packages/isar_flutter_libs/ios/Classes/IsarFlutterLibsPlugin.m ================================================ #import "IsarFlutterLibsPlugin.h" #if __has_include() #import #else // Support project import fallback if the generated compatibility header // is not copied when this plugin is created as a library. // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 #import "isar_flutter_libs-Swift.h" #endif @implementation IsarFlutterLibsPlugin + (void)registerWithRegistrar:(NSObject*)registrar { [SwiftIsarFlutterLibsPlugin registerWithRegistrar:registrar]; } @end ================================================ FILE: packages/isar_flutter_libs/ios/Classes/SwiftIsarFlutterLibsPlugin.swift ================================================ import Flutter import UIKit public class SwiftIsarFlutterLibsPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { result(nil) } public func dummyMethodToEnforceBundling() { // dummy calls to prevent tree shaking isar_get_error(0) } } ================================================ FILE: packages/isar_flutter_libs/ios/Classes/binding.h ================================================ #include #include #include #include char* isar_get_error(uint32_t err); ================================================ FILE: packages/isar_flutter_libs/ios/Resources/PrivacyInfo.xcprivacy ================================================ NSPrivacyTracking NSPrivacyTrackingDomains NSPrivacyCollectedDataTypes NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryDiskSpace NSPrivacyAccessedAPITypeReasons E174.1 ================================================ FILE: packages/isar_flutter_libs/ios/isar_flutter_libs.podspec ================================================ Pod::Spec.new do |s| s.name = 'isar_flutter_libs' s.version = '1.0.0' s.summary = 'Flutter binaries for the Isar Database. Needs to be included for Flutter apps.' s.homepage = 'https://isar.dev' s.license = { :file => '../LICENSE' } s.author = { 'Isar' => 'hello@isar.dev' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.platform = :ios, '11.0' s.swift_version = '5.3' s.vendored_frameworks = 'isar.xcframework' s.resource_bundles = {'isar_flutter_libs_apple_privacy' => ['Resources/PrivacyInfo.xcprivacy']} end ================================================ FILE: packages/isar_flutter_libs/lib/isar_flutter_libs.dart ================================================ library isar_flutter_libs; ================================================ FILE: packages/isar_flutter_libs/linux/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) set(PROJECT_NAME "isar_flutter_libs") project(${PROJECT_NAME} LANGUAGES CXX) # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "isar_flutter_libs_plugin") add_library(${PLUGIN_NAME} SHARED "isar_flutter_libs_plugin.cc" ) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) # List of absolute paths to libraries that should be bundled with the plugin set(isar_flutter_libs_bundled_libraries "${CMAKE_CURRENT_SOURCE_DIR}/libisar.so" PARENT_SCOPE ) ================================================ FILE: packages/isar_flutter_libs/linux/include/isar_flutter_libs/isar_flutter_libs_plugin.h ================================================ #ifndef FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ #define FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ #include G_BEGIN_DECLS #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) #else #define FLUTTER_PLUGIN_EXPORT #endif typedef struct _IsarFlutterLibsPlugin IsarFlutterLibsPlugin; typedef struct { GObjectClass parent_class; } IsarFlutterLibsPluginClass; FLUTTER_PLUGIN_EXPORT GType isar_flutter_libs_plugin_get_type(); FLUTTER_PLUGIN_EXPORT void isar_flutter_libs_plugin_register_with_registrar( FlPluginRegistrar* registrar); G_END_DECLS #endif // FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ ================================================ FILE: packages/isar_flutter_libs/linux/isar_flutter_libs_plugin.cc ================================================ #include "include/isar_flutter_libs/isar_flutter_libs_plugin.h" #include #include #include #include #define ISAR_FLUTTER_LIBS_PLUGIN(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), isar_flutter_libs_plugin_get_type(), \ IsarFlutterLibsPlugin)) struct _IsarFlutterLibsPlugin { GObject parent_instance; }; G_DEFINE_TYPE(IsarFlutterLibsPlugin, isar_flutter_libs_plugin, g_object_get_type()) // Called when a method call is received from Flutter. static void isar_flutter_libs_plugin_handle_method_call( IsarFlutterLibsPlugin* self, FlMethodCall* method_call) { g_autoptr(FlMethodResponse) response = nullptr; const gchar* method = fl_method_call_get_name(method_call); if (strcmp(method, "getPlatformVersion") == 0) { struct utsname uname_data = {}; uname(&uname_data); g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version); g_autoptr(FlValue) result = fl_value_new_string(version); response = FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } fl_method_call_respond(method_call, response, nullptr); } static void isar_flutter_libs_plugin_dispose(GObject* object) { G_OBJECT_CLASS(isar_flutter_libs_plugin_parent_class)->dispose(object); } static void isar_flutter_libs_plugin_class_init(IsarFlutterLibsPluginClass* klass) { G_OBJECT_CLASS(klass)->dispose = isar_flutter_libs_plugin_dispose; } static void isar_flutter_libs_plugin_init(IsarFlutterLibsPlugin* self) {} static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { IsarFlutterLibsPlugin* plugin = ISAR_FLUTTER_LIBS_PLUGIN(user_data); isar_flutter_libs_plugin_handle_method_call(plugin, method_call); } void isar_flutter_libs_plugin_register_with_registrar(FlPluginRegistrar* registrar) { IsarFlutterLibsPlugin* plugin = ISAR_FLUTTER_LIBS_PLUGIN( g_object_new(isar_flutter_libs_plugin_get_type(), nullptr)); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); g_autoptr(FlMethodChannel) channel = fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), "isar_flutter_libs", FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler(channel, method_call_cb, g_object_ref(plugin), g_object_unref); g_object_unref(plugin); } ================================================ FILE: packages/isar_flutter_libs/macos/Classes/IsarFlutterLibsPlugin.swift ================================================ import Cocoa import FlutterMacOS public class IsarFlutterLibsPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "isar_flutter_libs", binaryMessenger: registrar.messenger) let instance = IsarFlutterLibsPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getPlatformVersion": result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) default: result(FlutterMethodNotImplemented) } } } ================================================ FILE: packages/isar_flutter_libs/macos/isar_flutter_libs.podspec ================================================ Pod::Spec.new do |s| s.name = 'isar_flutter_libs' s.version = '1.0.0' s.summary = 'Flutter binaries for the Isar Database. Needs to be included for Flutter apps.' s.homepage = 'https://isar.dev' s.license = { :file => '../LICENSE' } s.author = { 'Isar' => 'hello@isar.dev' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'FlutterMacOS' s.platform = :osx, '10.11' s.swift_version = '5.3' s.vendored_libraries = 'libisar.dylib' end ================================================ FILE: packages/isar_flutter_libs/pubspec.yaml ================================================ name: isar_flutter_libs description: Isar Core binaries for the Isar Database. Needs to be included for Flutter apps. version: 3.1.8 repository: https://github.com/isar-community/isar homepage: https://isar.dev publish_to: https://pub.isar-community.dev/ environment: sdk: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" dependencies: flutter: sdk: flutter isar: version: 3.1.8 hosted: https://pub.isar-community.dev flutter: plugin: platforms: android: package: dev.isar.isar_flutter_libs pluginClass: IsarFlutterLibsPlugin ios: pluginClass: IsarFlutterLibsPlugin macos: pluginClass: IsarFlutterLibsPlugin linux: pluginClass: IsarFlutterLibsPlugin windows: pluginClass: IsarFlutterLibsPlugin ================================================ FILE: packages/isar_flutter_libs/pubspec_overrides.yaml ================================================ dependency_overrides: isar: path: ../isar ================================================ FILE: packages/isar_flutter_libs/windows/.gitignore ================================================ flutter/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: packages/isar_flutter_libs/windows/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) set(PROJECT_NAME "isar_flutter_libs") project(${PROJECT_NAME} LANGUAGES CXX) # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "isar_flutter_libs_plugin") add_library(${PLUGIN_NAME} SHARED "isar_flutter_libs_plugin.cpp" ) apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) # List of absolute paths to libraries that should be bundled with the plugin set(isar_flutter_libs_bundled_libraries "${CMAKE_CURRENT_SOURCE_DIR}/isar.dll" PARENT_SCOPE ) ================================================ FILE: packages/isar_flutter_libs/windows/include/isar_flutter_libs/isar_flutter_libs_plugin.h ================================================ #ifndef FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ #define FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ #include #ifdef FLUTTER_PLUGIN_IMPL #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) #else #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) #endif #if defined(__cplusplus) extern "C" { #endif FLUTTER_PLUGIN_EXPORT void IsarFlutterLibsPluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) } // extern "C" #endif #endif // FLUTTER_PLUGIN_ISAR_FLUTTER_LIBS_PLUGIN_H_ ================================================ FILE: packages/isar_flutter_libs/windows/isar_flutter_libs_plugin.cpp ================================================ #include "include/isar_flutter_libs/isar_flutter_libs_plugin.h" // This must be included before many other Windows headers. #include // For getPlatformVersion; remove unless needed for your plugin implementation. #include #include #include #include #include #include #include namespace { class IsarFlutterLibsPlugin : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); IsarFlutterLibsPlugin(); virtual ~IsarFlutterLibsPlugin(); private: // Called when a method is called on this plugin's channel from Dart. void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); }; // static void IsarFlutterLibsPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows *registrar) { auto channel = std::make_unique>( registrar->messenger(), "isar_flutter_libs", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); registrar->AddPlugin(std::move(plugin)); } IsarFlutterLibsPlugin::IsarFlutterLibsPlugin() {} IsarFlutterLibsPlugin::~IsarFlutterLibsPlugin() {} void IsarFlutterLibsPlugin::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { if (method_call.method_name().compare("getPlatformVersion") == 0) { std::ostringstream version_stream; version_stream << "Windows "; if (IsWindows10OrGreater()) { version_stream << "10+"; } else if (IsWindows8OrGreater()) { version_stream << "8"; } else if (IsWindows7OrGreater()) { version_stream << "7"; } result->Success(flutter::EncodableValue(version_stream.str())); } else { result->NotImplemented(); } } } // namespace void IsarFlutterLibsPluginRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { IsarFlutterLibsPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } ================================================ FILE: packages/isar_generator/CHANGELOG.md ================================================ See [Isar Changelog](https://pub.dev/packages/isar/changelog) ================================================ FILE: packages/isar_generator/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: packages/isar_generator/README.md ================================================ ### Code generator for the [Isar Database](https://github.com/isar-community/isar) please go there for documentation. ================================================ FILE: packages/isar_generator/analysis_options.yaml ================================================ include: package:very_good_analysis/analysis_options.yaml analyzer: errors: cascade_invocations: ignore avoid_positional_boolean_parameters: ignore parameter_assignments: ignore public_member_api_docs: ignore use_string_buffers: ignore ================================================ FILE: packages/isar_generator/build.yaml ================================================ builders: isar_generator: import: "package:isar_generator/isar_generator.dart" builder_factories: ["getIsarGenerator"] build_extensions: { ".dart": ["isar_generator.g.part"] } auto_apply: dependents build_to: cache applies_builders: ["source_gen|combining_builder"] ================================================ FILE: packages/isar_generator/lib/isar_generator.dart ================================================ import 'package:build/build.dart'; import 'package:isar_generator/src/collection_generator.dart'; import 'package:source_gen/source_gen.dart'; Builder getIsarGenerator(BuilderOptions options) => SharedPartBuilder( [ IsarCollectionGenerator(), IsarEmbeddedGenerator(), ], 'isar_generator', ); ================================================ FILE: packages/isar_generator/lib/src/code_gen/by_index_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar_generator/src/object_info.dart'; String generateByIndexExtension(ObjectInfo oi) { final uniqueIndexes = oi.indexes.where((e) => e.unique).toList(); if (uniqueIndexes.isEmpty) { return ''; } var code = 'extension ${oi.dartName}ByIndex on IsarCollection<${oi.dartName}> {'; for (final index in uniqueIndexes) { code += generateSingleByIndex(oi, index); code += generateAllByIndex(oi, index); if (!index.properties.first.isMultiEntry) { code += generatePutByIndex(oi, index); } } return ''' $code }'''; } extension on ObjectIndex { String get dartName { return properties.map((e) => e.property.dartName.capitalize()).join(); } } String generateSingleByIndex(ObjectInfo oi, ObjectIndex index) { final params = index.properties .map((i) => '${i.property.dartType} ${i.property.dartName}') .join(','); final paramsList = index.properties.map((i) => i.property.dartName).join(','); return ''' Future<${oi.dartName}?> getBy${index.dartName}($params) { return getByIndex(r'${index.name}', [$paramsList]); } ${oi.dartName}? getBy${index.dartName}Sync($params) { return getByIndexSync(r'${index.name}', [$paramsList]); } Future deleteBy${index.dartName}($params) { return deleteByIndex(r'${index.name}', [$paramsList]); } bool deleteBy${index.dartName}Sync($params) { return deleteByIndexSync(r'${index.name}', [$paramsList]); } '''; } String generateAllByIndex(ObjectInfo oi, ObjectIndex index) { String valsName(ObjectProperty p) => '${p.dartName}Values'; final props = index.properties; final params = props .map((ip) => 'List<${ip.property.dartType}> ${valsName(ip.property)}') .join(','); String createValues; if (props.length == 1) { final p = props.first.property; createValues = 'final values = ${valsName(p)}.map((e) => [e]).toList();'; } else { final lenAssert = props .sublist(1) .map((i) => '${valsName(i.property)}.length == len') .join('&&'); createValues = ''' final len = ${valsName(props.first.property)}.length; assert($lenAssert, 'All index values must have the same length'); final values = >[]; for (var i = 0; i < len; i++) { values.add([${props.map((ip) => '${valsName(ip.property)}[i]').join(',')}]); } '''; } return ''' Future> getAllBy${index.dartName}($params) { $createValues return getAllByIndex(r'${index.name}', values); } List<${oi.dartName}?> getAllBy${index.dartName}Sync($params) { $createValues return getAllByIndexSync(r'${index.name}', values); } Future deleteAllBy${index.dartName}($params) { $createValues return deleteAllByIndex(r'${index.name}', values); } int deleteAllBy${index.dartName}Sync($params) { $createValues return deleteAllByIndexSync(r'${index.name}', values); } '''; } String generatePutByIndex(ObjectInfo oi, ObjectIndex index) { return ''' Future putBy${index.dartName}(${oi.dartName} object) { return putByIndex(r'${index.name}', object); } Id putBy${index.dartName}Sync(${oi.dartName} object, {bool saveLinks = true}) { return putByIndexSync(r'${index.name}', object, saveLinks: saveLinks); } Future> putAllBy${index.dartName}(List<${oi.dartName}> objects) { return putAllByIndex(r'${index.name}', objects); } List putAllBy${index.dartName}Sync(List<${oi.dartName}> objects, {bool saveLinks = true}) { return putAllByIndexSync(r'${index.name}', objects, saveLinks: saveLinks); } '''; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/collection_schema_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; String generateSchema(ObjectInfo object) { var code = 'const ${object.dartName.capitalize()}Schema = '; if (!object.isEmbedded) { code += 'CollectionSchema('; } else { code += 'Schema('; } final properties = object.objectProperties .mapIndexed( (i, e) => "r'${e.isarName}': ${_generatePropertySchema(object, i)}", ) .join(','); code += ''' name: r'${object.isarName}', id: ${object.id}, properties: {$properties}, estimateSize: ${object.estimateSizeName}, serialize: ${object.serializeName}, deserialize: ${object.deserializeName}, deserializeProp: ${object.deserializePropName},'''; if (!object.isEmbedded) { final indexes = object.indexes .map((e) => "r'${e.name}': ${_generateIndexSchema(e)}") .join(','); final links = object.links .map((e) => "r'${e.isarName}': ${_generateLinkSchema(object, e)}") .join(','); final embeddedSchemas = object.embeddedDartNames.entries .map((e) => "r'${e.key}': ${e.value.capitalize()}Schema") .join(','); code += ''' idName: r'${object.idProperty.isarName}', indexes: {$indexes}, links: {$links}, embeddedSchemas: {$embeddedSchemas}, getId: ${object.getIdName}, getLinks: ${object.getLinksName}, attach: ${object.attachName}, version: '${Isar.version}', '''; } return '$code);'; } String _generatePropertySchema(ObjectInfo object, int index) { final property = object.objectProperties[index]; var enumMap = ''; if (property.isEnum) { enumMap = 'enumMap: ${property.enumValueMapName(object)},'; } var target = ''; if (property.targetIsarName != null) { target = "target: r'${property.targetIsarName}',"; } return ''' PropertySchema( id: $index, name: r'${property.isarName}', type: IsarType.${property.isarType.name}, $enumMap $target ) '''; } String _generateIndexSchema(ObjectIndex index) { final properties = index.properties.map((e) { return ''' IndexPropertySchema( name: r'${e.property.isarName}', type: IndexType.${e.type.name}, caseSensitive: ${e.caseSensitive}, )'''; }).join(','); return ''' IndexSchema( id: ${index.id}, name: r'${index.name}', unique: ${index.unique}, replace: ${index.replace}, properties: [$properties], )'''; } String _generateLinkSchema(ObjectInfo object, ObjectLink link) { var linkName = ''; if (link.isBacklink) { linkName = "linkName: r'${link.targetLinkIsarName}',"; } return ''' LinkSchema( id: ${link.id(object.isarName)}, name: r'${link.isarName}', target: r'${link.targetCollectionIsarName}', single: ${link.isSingle}, $linkName )'''; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_distinct_by_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; String generateDistinctBy(ObjectInfo oi) { var code = ''' extension ${oi.dartName}QueryWhereDistinct on QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct> {'''; for (final property in oi.objectProperties) { if (property.isarType == IsarType.string) { code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct>distinctBy${property.dartName.capitalize()}({bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'${property.isarName}', caseSensitive: caseSensitive); }); }'''; } else if (!property.isarType.containsObject) { code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QDistinct>distinctBy${property.dartName.capitalize()}() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'${property.isarName}'); }); }'''; } } return '$code}'; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_filter_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/code_gen/query_filter_length.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; class FilterGenerator { FilterGenerator(this.object) : objName = object.dartName; final ObjectInfo object; final String objName; String generate() { var code = 'extension ${objName}QueryFilter on QueryBuilder<$objName, $objName, ' 'QFilterCondition> {'; for (final property in object.properties) { if (property.nullable) { code += generateIsNull(property); code += generateIsNotNull(property); } if (property.elementNullable) { code += generateElementIsNull(property); code += generateElementIsNotNull(property); } if (!property.isarType.containsObject) { code += generateEqualTo(property); if (!property.isarType.containsBool) { code += generateGreaterThan(property); code += generateLessThan(property); code += generateBetween(property); } } if (property.isarType.containsString) { code += generateStringStartsWith(property); code += generateStringEndsWith(property); code += generateStringContains(property); code += generateStringMatches(property); code += generateStringIsEmpty(property); code += generateStringIsNotEmpty(property); } if (property.isarType.isList) { code += generateListLength(property); } } return ''' $code }'''; } String mPrefix(ObjectProperty p, [bool listElement = true]) { final any = listElement && p.isarType.isList ? 'Element' : ''; return 'QueryBuilder<$objName, $objName, QAfterFilterCondition> ' '${p.dartName.decapitalize()}$any'; } String generateEqualTo(ObjectProperty p) { final optional = [ if (p.isarType.containsString) 'bool caseSensitive = true', if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' ${mPrefix(p)}EqualTo(${p.nScalarDartType} value ${optional.isNotBlank ? ', {$optional,}' : ''}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'${p.isarName}', value: value, ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} )); }); }'''; } String generateGreaterThan(ObjectProperty p) { final optional = [ 'bool include = false', if (p.isarType.containsString) 'bool caseSensitive = true', if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' ${mPrefix(p)}GreaterThan(${p.nScalarDartType} value, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, property: r'${p.isarName}', value: value, ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} )); }); }'''; } String generateLessThan(ObjectProperty p) { final optional = [ 'bool include = false', if (p.isarType.containsString) 'bool caseSensitive = true', if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' ${mPrefix(p)}LessThan(${p.nScalarDartType} value, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, property: r'${p.isarName}', value: value, ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} )); }); }'''; } String generateBetween(ObjectProperty p) { final optional = [ 'bool includeLower = true', 'bool includeUpper = true', if (p.isarType.containsString) 'bool caseSensitive = true', if (p.isarType.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' ${mPrefix(p)}Between(${p.nScalarDartType} lower, ${p.nScalarDartType} upper, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( property: r'${p.isarName}', lower: lower, includeLower: includeLower, upper: upper, includeUpper: includeUpper, ${p.isarType.containsString ? 'caseSensitive: caseSensitive,' : ''} ${p.isarType.containsFloat ? 'epsilon: epsilon,' : ''} )); }); }'''; } String generateIsNull(ObjectProperty p) { return ''' ${mPrefix(p, false)}IsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( property: r'${p.isarName}', )); }); }'''; } String generateElementIsNull(ObjectProperty p) { return ''' ${mPrefix(p)}IsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.elementIsNull( property: r'${p.isarName}', )); }); }'''; } String generateIsNotNull(ObjectProperty p) { return ''' ${mPrefix(p, false)}IsNotNull() { return QueryBuilder.apply(this, (query) { return query .addFilterCondition(const FilterCondition.isNotNull( property: r'${p.isarName}', )); }); }'''; } String generateElementIsNotNull(ObjectProperty p) { return ''' ${mPrefix(p)}IsNotNull() { return QueryBuilder.apply(this, (query) { return query .addFilterCondition(const FilterCondition.elementIsNotNull( property: r'${p.isarName}', )); }); }'''; } String generateStringStartsWith(ObjectProperty p) { return ''' ${mPrefix(p)}StartsWith(String value, {bool caseSensitive = true,}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.startsWith( property: r'${p.isarName}', value: value, caseSensitive: caseSensitive, )); }); }'''; } String generateStringEndsWith(ObjectProperty p) { return ''' ${mPrefix(p)}EndsWith(String value, {bool caseSensitive = true,}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.endsWith( property: r'${p.isarName}', value: value, caseSensitive: caseSensitive, )); }); }'''; } String generateStringContains(ObjectProperty p) { return ''' ${mPrefix(p)}Contains(String value, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( property: r'${p.isarName}', value: value, caseSensitive: caseSensitive, )); }); }'''; } String generateStringMatches(ObjectProperty p) { return ''' ${mPrefix(p)}Matches(String pattern, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( property: r'${p.isarName}', wildcard: pattern, caseSensitive: caseSensitive, )); }); }'''; } String generateStringIsEmpty(ObjectProperty p) { return ''' ${mPrefix(p)}IsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'${p.isarName}', value: '', )); }); }'''; } String generateStringIsNotEmpty(ObjectProperty p) { return ''' ${mPrefix(p)}IsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( property: r'${p.isarName}', value: '', )); }); }'''; } String generateListLength(ObjectProperty p) { return generateLength(objName, p.dartName, (lower, includeLower, upper, includeUpper) { return ''' QueryBuilder.apply(this, (query) { return query.listLength( r'${p.isarName}', $lower, $includeLower, $upper, $includeUpper, ); })'''; }); } } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_filter_length.dart ================================================ import 'package:dartx/dartx.dart'; String generateLength( String objectName, String propertyName, String Function( String lower, String includeLower, String upper, String includeUpper, ) codeGen, ) { return ''' QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthEqualTo(int length) { return ${codeGen('length', 'true', 'length', 'true')}; } QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}IsEmpty() { return ${codeGen('0', 'true', '0', 'true')}; } QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}IsNotEmpty() { return ${codeGen('0', 'false', '999999', 'true')}; } QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthLessThan( int length, { bool include = false, }) { return ${codeGen('0', 'true', 'length', 'include')}; } QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthGreaterThan( int length, { bool include = false, }) { return ${codeGen('length', 'include', '999999', 'true')}; } QueryBuilder<$objectName, $objectName, QAfterFilterCondition> ${propertyName.decapitalize()}LengthBetween( int lower, int upper, { bool includeLower = true, bool includeUpper = true, }) { return ${codeGen('lower', 'includeLower', 'upper', 'includeUpper')}; } '''; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_link_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar_generator/src/code_gen/query_filter_length.dart'; import 'package:isar_generator/src/object_info.dart'; String generateQueryLinks(ObjectInfo oi) { var code = 'extension ${oi.dartName}QueryLinks on QueryBuilder<${oi.dartName}, ' '${oi.dartName}, QFilterCondition> {'; for (final link in oi.links) { code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> ${link.dartName.decapitalize()}(FilterQuery<${link.targetCollectionDartName}> q) { return QueryBuilder.apply(this, (query) { return query.link(q, r'${link.isarName}'); }); }'''; if (link.isSingle) { code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> ${link.dartName.decapitalize()}IsNull() { return QueryBuilder.apply(this, (query) { return query.linkLength(r'${link.isarName}', 0, true, 0, true); }); }'''; } else { code += generateLength(oi.dartName, link.dartName, (lower, includeLower, upper, includeUpper) { return ''' QueryBuilder.apply(this, (query) { return query.linkLength(r'${link.isarName}', $lower, $includeLower, $upper, $includeUpper); })'''; }); } } return ''' $code }'''; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_object_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; String generateQueryObjects(ObjectInfo oi) { var code = 'extension ${oi.dartName}QueryObject on QueryBuilder<${oi.dartName}, ' '${oi.dartName}, QFilterCondition> {'; for (final property in oi.objectProperties) { if (!property.isarType.containsObject) { continue; } var name = property.dartName.decapitalize(); if (property.isarType.isList) { name += 'Element'; } code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterFilterCondition> $name(FilterQuery<${property.typeClassName}> q) { return QueryBuilder.apply(this, (query) { return query.object(q, r'${property.isarName}'); }); }'''; } return ''' $code }'''; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_property_generator.dart ================================================ import 'package:isar_generator/src/object_info.dart'; String generatePropertyQuery(ObjectInfo oi) { var code = ''' extension ${oi.dartName}QueryProperty on QueryBuilder<${oi.dartName}, ${oi.dartName}, QQueryProperty> {'''; // Ids are always non-nullable regardless of their specified nullability code += ''' QueryBuilder<${oi.dartName}, int, QQueryOperations>${oi.idProperty.dartName}Property() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'${oi.idProperty.isarName}'); }); }'''; for (final property in oi.objectProperties) { code += ''' QueryBuilder<${oi.dartName}, ${property.dartType}, QQueryOperations>${property.dartName}Property() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'${property.isarName}'); }); }'''; } return '$code}'; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_sort_by_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; String generateSortBy(ObjectInfo oi) { var code = ''' extension ${oi.dartName}QuerySortBy on QueryBuilder<${oi.dartName}, ${oi.dartName}, QSortBy> {'''; for (final property in oi.objectProperties) { if (property.isarType.isList || property.isarType.containsObject) { continue; } code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>sortBy${property.dartName.capitalize()}() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'${property.isarName}', Sort.asc); }); } QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>sortBy${property.dartName.capitalize()}Desc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'${property.isarName}', Sort.desc); }); }'''; } code += ''' } extension ${oi.dartName}QuerySortThenBy on QueryBuilder<${oi.dartName}, ${oi.dartName}, QSortThenBy> {'''; for (final property in oi.properties) { if (property.isarType.isList || property.isarType.containsObject) { continue; } code += ''' QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>thenBy${property.dartName.capitalize()}() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'${property.isarName}', Sort.asc); }); } QueryBuilder<${oi.dartName}, ${oi.dartName}, QAfterSortBy>thenBy${property.dartName.capitalize()}Desc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'${property.isarName}', Sort.desc); }); }'''; } return '$code}'; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/query_where_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/object_info.dart'; class WhereGenerator { WhereGenerator(this.object) : objName = object.dartName, id = object.idProperty; final ObjectInfo object; final String objName; final ObjectProperty id; final existing = {}; String generate() { var code = 'extension ${objName}QueryWhereSort on QueryBuilder<$objName, ' '$objName, QWhere> {'; code += generateAnyId(); for (final index in object.indexes) { if (index.properties.all((element) => element.type == IndexType.value)) { code += generateAny(index); } } code += ''' } extension ${objName}QueryWhere on QueryBuilder<$objName, $objName, QWhereClause> { '''; code += generateWhereIdEqualTo(); code += generateWhereIdNotEqualTo(); code += generateWhereIdGreaterThan(); code += generateWhereIdLessThan(); code += generateWhereIdBetween(); for (final index in object.indexes) { for (var n = 0; n < index.properties.length; n++) { final indexProperty = index.properties[n]; final property = indexProperty.property; if ((property.nullable && !indexProperty.isMultiEntry) || (property.elementNullable && indexProperty.isMultiEntry)) { code += generateWhereIsNull(index, n + 1); code += generateWhereIsNotNull(index, n + 1); } code += generateWhereEqualTo(index, n + 1); code += generateWhereNotEqualTo(index, n + 1); if (indexProperty.type == IndexType.value) { if (property.isarType != IsarType.bool && property.isarType != IsarType.boolList) { code += generateWhereGreaterThan(index, n + 1); code += generateWhereLessThan(index, n + 1); code += generateWhereBetween(index, n + 1); } if (property.isarType == IsarType.string || property.isarType == IsarType.stringList) { code += generateWhereStartsWith(index, n + 1); code += generateStringIsEmpty(index, n + 1); code += generateStringIsNotEmpty(index, n + 1); } } } } return '$code}'; } String getMethodName(ObjectIndex index, int propertyCount, [String? method]) { String propertyName(ObjectIndexProperty p) { var name = p.property.dartName.capitalize(); if (p.isMultiEntry) { name += 'Element'; } return name; } var name = ''; final eqProperties = index.properties.sublist(0, propertyCount - (method != null ? 1 : 0)); if (eqProperties.isNotEmpty) { name += eqProperties.map(propertyName).join(); name += 'EqualTo'; } if (method != null) { name += propertyName(index.properties[propertyCount - 1]); name += method; } final remainingProperties = propertyCount < index.properties.length ? index.properties.sublist(propertyCount) : null; if (remainingProperties != null) { name += 'Any'; name += remainingProperties.map(propertyName).join(); } return name.decapitalize(); } String paramType(ObjectIndexProperty p) { if (p.property.isarType.isList && p.type == IndexType.hash) { return p.property.dartType; } else { return p.property.nScalarDartType; } } String paramName(ObjectIndexProperty p) { if (p.property.isarType.isList && p.type != IndexType.hash) { return '${p.property.dartName}Element'; } else { return p.property.dartName; } } String joinToParams(List properties) { return properties .map((it) => '${paramType(it)} ${paramName(it)}') .join(','); } String joinToValues(List properties) { return properties.map((it) { if (it.property.isarType.isList && it.type != IndexType.hash) { return '${it.property.dartName}Element'; } else { return paramName(it); } }).join(', '); } String generateAnyId() { return ''' QueryBuilder<$objName, $objName, QAfterWhere> any${id.dartName.capitalize()}() { return QueryBuilder.apply(this, (query) { return query.addWhereClause(const IdWhereClause.any()); }); } '''; } String generateAny(ObjectIndex index) { final name = getMethodName(index, 0); if (!existing.add(name)) { return ''; } return ''' QueryBuilder<$objName, $objName, QAfterWhere> $name() { return QueryBuilder.apply(this, (query) { return query.addWhereClause( const IndexWhereClause.any(indexName: r'${index.name}'), ); }); } '''; } String get mPrefix => 'QueryBuilder<$objName, $objName, QAfterWhereClause>'; String generateWhereIdEqualTo() { final idName = id.dartName.decapitalize(); return ''' $mPrefix ${idName}EqualTo(Id $idName) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IdWhereClause.between( lower: $idName, upper: $idName, )); }); } '''; } String generateWhereEqualTo(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount); final values = joinToValues(properties); final params = joinToParams(properties); return ''' $mPrefix $name($params ${properties.containsFloat ? ', {double epsilon = Query.epsilon,}' : ''}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( indexName: r'${index.name}', value: [$values], ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); }); } '''; } String generateWhereIdNotEqualTo() { final idName = id.dartName.decapitalize(); return ''' $mPrefix ${idName}NotEqualTo(Id $idName) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query.addWhereClause( IdWhereClause.lessThan(upper: $idName, includeUpper: false), ).addWhereClause( IdWhereClause.greaterThan(lower: $idName, includeLower: false), ); } else { return query.addWhereClause( IdWhereClause.greaterThan(lower: $idName, includeLower: false), ).addWhereClause( IdWhereClause.lessThan(upper: $idName, includeUpper: false), ); } }); } '''; } String generateWhereNotEqualTo(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'NotEqualTo'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount); final params = joinToParams(properties); final equalProperties = properties.dropLast(1); final notEqualProperty = properties.last; final equalValues = joinToValues(equalProperties); var notEqualValue = joinToValues([notEqualProperty]); if (equalValues.isNotEmpty) { notEqualValue = ',$notEqualValue'; } return ''' $mPrefix $name($params ${properties.containsFloat ? ', {double epsilon = Query.epsilon,}' : ''}) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$equalValues], upper: [$equalValues $notEqualValue], includeUpper: false, ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )).addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$equalValues $notEqualValue], includeLower: false, upper: [$equalValues], ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); } else { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$equalValues $notEqualValue], includeLower: false, upper: [$equalValues], ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )).addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$equalValues], upper: [$equalValues $notEqualValue], includeUpper: false, ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); } }); } '''; } String generateWhereIdGreaterThan() { final idName = id.dartName.decapitalize(); return ''' $mPrefix ${idName}GreaterThan(Id $idName, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.greaterThan(lower: $idName, includeLower: include), ); }); } '''; } String generateWhereGreaterThan(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'GreaterThan'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount); final optional = [ 'bool include = false', if (properties.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' $mPrefix $name(${joinToParams(properties)}, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [${joinToValues(properties)}], includeLower: include, upper: [${joinToValues(properties.dropLast(1))}], ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); }); } '''; } String generateWhereIdLessThan() { final idName = id.dartName.decapitalize(); return ''' $mPrefix ${idName}LessThan(Id $idName, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: $idName, includeUpper: include), ); }); } '''; } String generateWhereLessThan(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'LessThan'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount); final optional = [ 'bool include = false', if (properties.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' $mPrefix $name(${joinToParams(properties)}, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [${joinToValues(properties.dropLast(1))}], upper: [${joinToValues(properties)}], includeUpper: include, ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); }); } '''; } String generateWhereIdBetween() { final idName = id.dartName.decapitalize(); final lowerName = 'lower${id.dartName.capitalize()}'; final upperName = 'upper${id.dartName.capitalize()}'; return ''' $mPrefix ${idName}Between(Id $lowerName, Id $upperName, {bool includeLower = true, bool includeUpper = true,}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IdWhereClause.between( lower: $lowerName, includeLower: includeLower, upper: $upperName, includeUpper: includeUpper, )); }); } '''; } String generateWhereBetween(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'Between'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount); final equalProperties = properties.dropLast(1); final betweenProperty = properties.last; var params = joinToParams(equalProperties); if (params.isNotEmpty) { params += ','; } final betweenType = paramType(betweenProperty); final lowerName = 'lower${paramName(betweenProperty).capitalize()}'; final upperName = 'upper${paramName(betweenProperty).capitalize()}'; params += '$betweenType $lowerName, $betweenType $upperName'; var values = joinToValues(equalProperties); if (values.isNotEmpty) { values += ','; } final optional = [ 'bool includeLower = true', 'bool includeUpper = true', if (properties.containsFloat) 'double epsilon = Query.epsilon', ].join(','); return ''' $mPrefix $name($params, {$optional,}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$values $lowerName], includeLower: includeLower, upper: [$values $upperName], includeUpper: includeUpper, ${properties.containsFloat ? 'epsilon: epsilon,' : ''} )); }); } '''; } String generateWhereIsNull(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'IsNull'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount - 1); var values = joinToValues(properties); if (values.isNotEmpty) { values += ','; } final params = joinToParams(properties); return ''' $mPrefix $name($params) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( indexName: r'${index.name}', value: [$values null], )); }); } '''; } String generateWhereIsNotNull(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'IsNotNull'); if (!existing.add(name)) { return ''; } final properties = index.properties.takeFirst(propertyCount - 1); var values = joinToValues(properties); if (values.isNotEmpty) { values += ','; } final params = joinToParams(properties); return ''' $mPrefix $name($params) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$values null], includeLower: false, upper: [$values], )); }); } '''; } String generateWhereStartsWith(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'StartsWith'); if (!existing.add(name)) { return ''; } final equalProperties = index.properties.dropLast(1); var params = joinToParams(equalProperties); if (params.isNotEmpty) { params += ','; } final prefixProperty = index.properties.last; final prefixName = '${paramName(prefixProperty).capitalize()}Prefix'; params += 'String $prefixName'; var values = joinToValues(equalProperties); if (values.isNotEmpty) { values += ','; } return ''' $mPrefix $name($params) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.between( indexName: r'${index.name}', lower: [$values $prefixName], upper: [$values '\$$prefixName\\u{FFFFF}'], )); }); } '''; } String generateStringIsEmpty(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'IsEmpty'); if (!existing.add(name)) { return ''; } final properties = index.properties.dropLast(1); var values = joinToValues(properties); if (values.isNotEmpty) { values += ','; } final params = joinToParams(properties); return ''' $mPrefix $name($params) { return QueryBuilder.apply(this, (query) { return query.addWhereClause(IndexWhereClause.equalTo( indexName: r'${index.name}', value: [$values ''], )); }); }'''; } String generateStringIsNotEmpty(ObjectIndex index, int propertyCount) { final name = getMethodName(index, propertyCount, 'IsNotEmpty'); if (!existing.add(name)) { return ''; } final properties = index.properties.dropLast(1); var values = joinToValues(properties); if (values.isNotEmpty) { values += ','; } final params = joinToParams(properties); return ''' $mPrefix $name($params) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query.addWhereClause(IndexWhereClause.lessThan( indexName: r'${index.name}', upper: [''], )).addWhereClause(IndexWhereClause.greaterThan( indexName: r'${index.name}', lower: [''], )); } else { return query.addWhereClause(IndexWhereClause.greaterThan( indexName: r'${index.name}', lower: [''], )).addWhereClause(IndexWhereClause.lessThan( indexName: r'${index.name}', upper: [''], )); } }); }'''; } } extension on List { bool get containsFloat => last.isarType == IsarType.float || last.isarType == IsarType.floatList; } ================================================ FILE: packages/isar_generator/lib/src/code_gen/type_adapter_generator.dart ================================================ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/object_info.dart'; String _prepareSerialize( bool nullable, String value, String Function(String) size, ) { var code = ''; if (nullable) { code += ''' { final value = $value; if (value != null) {'''; value = 'value'; } code += 'bytesCount += ${size(value)};'; if (nullable) { code += '}}'; } return code; } String _prepareSerializeList( bool nullable, bool elementNullable, String value, String size, [ String? prepare, ]) { var code = ''; if (nullable) { code += ''' { final list = $value; if (list != null) {'''; value = 'list'; } code += ''' bytesCount += 3 + $value.length * 3; { ${prepare ?? ''} for (var i = 0; i < $value.length; i++) { final value = $value[i];'''; if (elementNullable) { code += 'if (value != null) {'; } code += 'bytesCount += $size;'; if (elementNullable) { code += '}'; } code += '}}'; if (nullable) { code += '}}'; } return code; } String generateEstimateSerialize(ObjectInfo object) { var code = ''' int ${object.estimateSizeName}( ${object.dartName} object, List offsets, Map> allOffsets, ) { var bytesCount = offsets.last;'''; for (final property in object.properties) { final value = 'object.${property.dartName}'; switch (property.isarType) { case IsarType.string: final enumValue = property.isEnum ? '.${property.enumProperty}' : ''; code += _prepareSerialize( property.nullable, value, (value) => '3 + $value$enumValue.length * 3', ); break; case IsarType.stringList: final enumValue = property.isEnum ? '.${property.enumProperty}' : ''; code += _prepareSerializeList( property.nullable, property.elementNullable, value, 'value$enumValue.length * 3', ); break; case IsarType.object: code += _prepareSerialize( property.nullable, value, (value) { return '3 + ${property.targetSchema}.estimateSize($value, ' 'allOffsets[${property.scalarDartType}]!, allOffsets)'; }, ); break; case IsarType.objectList: code += _prepareSerializeList( property.nullable, property.elementNullable, value, '${property.targetSchema}.estimateSize(value, offsets, allOffsets)', 'final offsets = allOffsets[${property.scalarDartType}]!;', ); break; case IsarType.byteList: case IsarType.boolList: code += _prepareSerialize( property.nullable, value, (value) => '3 + $value.length', ); break; case IsarType.intList: case IsarType.floatList: code += _prepareSerialize( property.nullable, value, (value) => '3 + $value.length * 4', ); break; case IsarType.longList: case IsarType.doubleList: case IsarType.dateTimeList: code += _prepareSerialize( property.nullable, value, (value) => '3 + $value.length * 8', ); break; // ignore: no_default_cases default: break; } } return ''' $code return bytesCount; }'''; } String generateSerialize(ObjectInfo object) { var code = ''' void ${object.serializeName}( ${object.dartName} object, IsarWriter writer, List offsets, Map> allOffsets, ) {'''; for (var i = 0; i < object.objectProperties.length; i++) { final property = object.objectProperties[i]; var value = 'object.${property.dartName}'; if (property.isEnum) { final nOp = property.nullable ? '?' : ''; final elNOp = property.elementNullable ? '?' : ''; value = property.isarType.isList ? '$value$nOp.map((e) => e$elNOp.${property.enumProperty}).toList()' : '$value$nOp.${property.enumProperty}'; } switch (property.isarType) { case IsarType.bool: code += 'writer.writeBool(offsets[$i], $value);'; break; case IsarType.byte: code += 'writer.writeByte(offsets[$i], $value);'; break; case IsarType.int: code += 'writer.writeInt(offsets[$i], $value);'; break; case IsarType.float: code += 'writer.writeFloat(offsets[$i], $value);'; break; case IsarType.long: code += 'writer.writeLong(offsets[$i], $value);'; break; case IsarType.double: code += 'writer.writeDouble(offsets[$i], $value);'; break; case IsarType.dateTime: code += 'writer.writeDateTime(offsets[$i], $value);'; break; case IsarType.string: code += 'writer.writeString(offsets[$i], $value);'; break; case IsarType.object: code += ''' writer.writeObject<${property.typeClassName}>( offsets[$i], allOffsets, ${property.targetSchema}.serialize, $value, );'''; break; case IsarType.byteList: code += 'writer.writeByteList(offsets[$i], $value);'; break; case IsarType.boolList: code += 'writer.writeBoolList(offsets[$i], $value);'; break; case IsarType.intList: code += 'writer.writeIntList(offsets[$i], $value);'; break; case IsarType.longList: code += 'writer.writeLongList(offsets[$i], $value);'; break; case IsarType.floatList: code += 'writer.writeFloatList(offsets[$i], $value);'; break; case IsarType.doubleList: code += 'writer.writeDoubleList(offsets[$i], $value);'; break; case IsarType.dateTimeList: code += 'writer.writeDateTimeList(offsets[$i], $value);'; break; case IsarType.stringList: code += 'writer.writeStringList(offsets[$i], $value);'; break; case IsarType.objectList: code += ''' writer.writeObjectList<${property.typeClassName}>( offsets[$i], allOffsets, ${property.targetSchema}.serialize, $value, );'''; break; } } return '$code}'; } String generateDeserialize(ObjectInfo object) { var code = ''' ${object.dartName} ${object.deserializeName}( Id id, IsarReader reader, List offsets, Map> allOffsets, ) { final object = ${object.dartName}('''; final propertiesByMode = object.properties.groupBy((ObjectProperty p) => p.deserialize); final positional = propertiesByMode[PropertyDeser.positionalParam] ?? []; final sortedPositional = positional.sortedBy((ObjectProperty p) => p.constructorPosition!); for (final p in sortedPositional) { final index = object.objectProperties.indexOf(p); final deser = _deserializeProperty(object, p, 'offsets[$index]'); code += '$deser,'; } final named = propertiesByMode[PropertyDeser.namedParam] ?? []; for (final p in named) { final index = object.objectProperties.indexOf(p); final deser = _deserializeProperty(object, p, 'offsets[$index]'); code += '${p.dartName}: $deser,'; } code += ');'; final assign = propertiesByMode[PropertyDeser.assign] ?? []; for (final p in assign) { final index = object.objectProperties.indexOf(p); final deser = _deserializeProperty(object, p, 'offsets[$index]'); code += 'object.${p.dartName} = $deser;'; } return ''' $code return object; }'''; } String generateDeserializeProp(ObjectInfo object) { var code = ''' P ${object.deserializePropName}

( IsarReader reader, int propertyId, int offset, Map> allOffsets, ) { switch (propertyId) {'''; for (var i = 0; i < object.objectProperties.length; i++) { final property = object.objectProperties[i]; final deser = _deserializeProperty(object, property, 'offset'); code += 'case $i: return ($deser) as P;'; } return ''' $code default: throw IsarError('Unknown property with id \$propertyId'); } } '''; } String _deserializeProperty( ObjectInfo object, ObjectProperty property, String propertyOffset, ) { if (property.isId) { return 'id'; } final deser = _deserialize(property, propertyOffset); var defaultValue = ''; if (!property.nullable) { if (property.userDefaultValue != null) { defaultValue = '?? ${property.userDefaultValue}'; } else if (property.isarType == IsarType.object) { defaultValue = '?? ${property.typeClassName}()'; } else if (property.isarType.isList) { defaultValue = '?? []'; } else if (property.isEnum) { defaultValue = '?? ${property.defaultEnumElement}'; } } if (property.isEnum) { if (property.isarType.isList) { final elDefault = !property.elementNullable ? '?? ${property.defaultEnumElement}' : ''; return '$deser?.map((e) => ${property.valueEnumMapName(object)}[e] ' '$elDefault).toList() $defaultValue'; } else { return '${property.valueEnumMapName(object)}[$deser] $defaultValue'; } } else { return '$deser $defaultValue'; } } String _deserialize(ObjectProperty property, String propertyOffset) { final orNull = property.nullable || property.userDefaultValue != null || property.isEnum ? 'OrNull' : ''; final orElNull = property.elementNullable ? 'OrNull' : ''; switch (property.isarType) { case IsarType.bool: return 'reader.readBool$orNull($propertyOffset)'; case IsarType.byte: return 'reader.readByte$orNull($propertyOffset)'; case IsarType.int: return 'reader.readInt$orNull($propertyOffset)'; case IsarType.float: return 'reader.readFloat$orNull($propertyOffset)'; case IsarType.long: return 'reader.readLong$orNull($propertyOffset)'; case IsarType.double: return 'reader.readDouble$orNull($propertyOffset)'; case IsarType.dateTime: return 'reader.readDateTime$orNull($propertyOffset)'; case IsarType.string: return 'reader.readString$orNull($propertyOffset)'; case IsarType.object: return ''' reader.readObjectOrNull<${property.typeClassName}>( $propertyOffset, ${property.targetSchema}.deserialize, allOffsets, )'''; case IsarType.boolList: return 'reader.readBool${orElNull}List($propertyOffset)'; case IsarType.byteList: return 'reader.readByteList($propertyOffset)'; case IsarType.intList: return 'reader.readInt${orElNull}List($propertyOffset)'; case IsarType.floatList: return 'reader.readFloat${orElNull}List($propertyOffset)'; case IsarType.longList: return 'reader.readLong${orElNull}List($propertyOffset)'; case IsarType.doubleList: return 'reader.readDouble${orElNull}List($propertyOffset)'; case IsarType.dateTimeList: return 'reader.readDateTime${orElNull}List($propertyOffset)'; case IsarType.stringList: return 'reader.readString${orElNull}List($propertyOffset)'; case IsarType.objectList: return ''' reader.readObject${orElNull}List<${property.typeClassName}>( $propertyOffset, ${property.targetSchema}.deserialize, allOffsets, ${!property.elementNullable ? '${property.typeClassName}(),' : ''} )'''; } } String generateGetId(ObjectInfo object) { final defaultVal = object.idProperty.nullable ? '?? Isar.autoIncrement' : ''; return ''' Id ${object.getIdName}(${object.dartName} object) { return object.${object.idProperty.dartName} $defaultVal; } '''; } String generateGetLinks(ObjectInfo object) { return ''' List> ${object.getLinksName}(${object.dartName} object) { return [${object.links.map((e) => 'object.${e.dartName}').join(',')}]; } '''; } String generateAttach(ObjectInfo object) { var code = ''' void ${object.attachName}(IsarCollection col, Id id, ${object.dartName} object) {'''; if (object.idProperty.assignable) { code += 'object.${object.idProperty.dartName} = id;'; } for (final link in object.links) { // ignore: leading_newlines_in_multiline_strings code += '''object.${link.dartName}.attach( col, col.isar.collection<${link.targetCollectionDartName}>(), r'${link.isarName}', id );'''; } return '$code}'; } String generateEnumMaps(ObjectInfo object) { var code = ''; for (final property in object.properties) { final enumName = property.typeClassName; if (property.isEnum) { code += 'const ${property.enumValueMapName(object)} = {'; for (final enumElementName in property.enumMap!.keys) { final value = property.enumMap![enumElementName]; if (value is String) { code += "r'$enumElementName': r'$value',"; } else { code += "'$enumElementName': $value,"; } } code += '};'; code += 'const ${property.valueEnumMapName(object)} = {'; for (final enumElementName in property.enumMap!.keys) { final value = property.enumMap![enumElementName]; if (value is String) { code += "r'$value': $enumName.$enumElementName,"; } else { code += '$value: $enumName.$enumElementName,'; } } code += '};'; } } return code; } ================================================ FILE: packages/isar_generator/lib/src/collection_generator.dart ================================================ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/code_gen/by_index_generator.dart'; import 'package:isar_generator/src/code_gen/collection_schema_generator.dart'; import 'package:isar_generator/src/code_gen/query_distinct_by_generator.dart'; import 'package:isar_generator/src/code_gen/query_filter_generator.dart'; import 'package:isar_generator/src/code_gen/query_link_generator.dart'; import 'package:isar_generator/src/code_gen/query_object_generator.dart'; import 'package:isar_generator/src/code_gen/query_property_generator.dart'; import 'package:isar_generator/src/code_gen/query_sort_by_generator.dart'; import 'package:isar_generator/src/code_gen/query_where_generator.dart'; import 'package:isar_generator/src/code_gen/type_adapter_generator.dart'; import 'package:isar_generator/src/isar_analyzer.dart'; import 'package:source_gen/source_gen.dart'; const ignoreLints = [ 'duplicate_ignore', 'non_constant_identifier_names', 'constant_identifier_names', 'invalid_use_of_protected_member', 'unnecessary_cast', 'prefer_const_constructors', 'lines_longer_than_80_chars', 'require_trailing_commas', 'inference_failure_on_function_invocation', 'unnecessary_parenthesis', 'unnecessary_raw_strings', 'unnecessary_null_checks', 'join_return_with_assignment', 'prefer_final_locals', 'avoid_js_rounded_ints', 'avoid_positional_boolean_parameters', 'always_specify_types', ]; class IsarCollectionGenerator extends GeneratorForAnnotation { @override Future generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep, ) async { final object = IsarAnalyzer().analyzeCollection(element); return ''' // coverage:ignore-file // ignore_for_file: ${ignoreLints.join(', ')} extension Get${object.dartName}Collection on Isar { IsarCollection<${object.dartName}> get ${object.accessor} => this.collection(); } ${generateSchema(object)} ${generateEstimateSerialize(object)} ${generateSerialize(object)} ${generateDeserialize(object)} ${generateDeserializeProp(object)} ${generateEnumMaps(object)} ${generateGetId(object)} ${generateGetLinks(object)} ${generateAttach(object)} ${generateByIndexExtension(object)} ${WhereGenerator(object).generate()} ${FilterGenerator(object).generate()} ${generateQueryObjects(object)} ${generateQueryLinks(object)} ${generateSortBy(object)} ${generateDistinctBy(object)} ${generatePropertyQuery(object)} '''; } } class IsarEmbeddedGenerator extends GeneratorForAnnotation { @override Future generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep, ) async { final object = IsarAnalyzer().analyzeEmbedded(element); return ''' // coverage:ignore-file // ignore_for_file: ${ignoreLints.join(', ')} ${generateSchema(object)} ${generateEstimateSerialize(object)} ${generateSerialize(object)} ${generateDeserialize(object)} ${generateDeserializeProp(object)} ${generateEnumMaps(object)} ${FilterGenerator(object).generate()} ${generateQueryObjects(object)} '''; } } ================================================ FILE: packages/isar_generator/lib/src/helper.dart ================================================ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:source_gen/source_gen.dart'; const TypeChecker _collectionChecker = TypeChecker.fromRuntime(Collection); const TypeChecker _enumeratedChecker = TypeChecker.fromRuntime(Enumerated); const TypeChecker _embeddedChecker = TypeChecker.fromRuntime(Embedded); const TypeChecker _ignoreChecker = TypeChecker.fromRuntime(Ignore); const TypeChecker _nameChecker = TypeChecker.fromRuntime(Name); const TypeChecker _indexChecker = TypeChecker.fromRuntime(Index); const TypeChecker _backlinkChecker = TypeChecker.fromRuntime(Backlink); extension ClassElementX on ClassElement { bool get hasZeroArgsConstructor { return constructors.any( (ConstructorElement c) => c.isPublic && !c.parameters.any((ParameterElement p) => !p.isOptional), ); } List get allAccessors { final ignoreFields = collectionAnnotation?.ignore ?? embeddedAnnotation!.ignore; return [ ...accessors.mapNotNull((e) => e.variable), if (collectionAnnotation?.inheritance ?? embeddedAnnotation!.inheritance) for (InterfaceType supertype in allSupertypes) ...[ if (!supertype.isDartCoreObject) ...supertype.accessors.mapNotNull((e) => e.variable) ] ] .where( (PropertyInducingElement e) => e.isPublic && !e.isStatic && !_ignoreChecker.hasAnnotationOf(e.nonSynthetic) && !ignoreFields.contains(e.name), ) .distinctBy((e) => e.name) .toList(); } List get enumConsts { return fields.where((e) => e.isEnumConstant).map((e) => e.name).toList(); } } extension PropertyElementX on PropertyInducingElement { bool get isLink => type.element2!.name == 'IsarLink'; bool get isLinks => type.element2!.name == 'IsarLinks'; Enumerated? get enumeratedAnnotation { final ann = _enumeratedChecker.firstAnnotationOfExact(nonSynthetic); if (ann == null) { return null; } final typeIndex = ann.getField('type')!.getField('index')!.toIntValue()!; return Enumerated( EnumType.values[typeIndex], ann.getField('property')?.toStringValue(), ); } Backlink? get backlinkAnnotation { final ann = _backlinkChecker.firstAnnotationOfExact(nonSynthetic); if (ann == null) { return null; } return Backlink(to: ann.getField('to')!.toStringValue()!); } List get indexAnnotations { return _indexChecker.annotationsOfExact(nonSynthetic).map((DartObject ann) { final rawComposite = ann.getField('composite')!.toListValue(); final composite = []; if (rawComposite != null) { for (final c in rawComposite) { final indexTypeField = c.getField('type')!; IndexType? indexType; if (!indexTypeField.isNull) { final indexTypeIndex = indexTypeField.getField('index')!.toIntValue()!; indexType = IndexType.values[indexTypeIndex]; } composite.add( CompositeIndex( c.getField('property')!.toStringValue()!, type: indexType, caseSensitive: c.getField('caseSensitive')!.toBoolValue(), ), ); } } final indexTypeField = ann.getField('type')!; IndexType? indexType; if (!indexTypeField.isNull) { final indexTypeIndex = indexTypeField.getField('index')!.toIntValue()!; indexType = IndexType.values[indexTypeIndex]; } return Index( name: ann.getField('name')!.toStringValue(), composite: composite, unique: ann.getField('unique')!.toBoolValue()!, replace: ann.getField('replace')!.toBoolValue()!, type: indexType, caseSensitive: ann.getField('caseSensitive')!.toBoolValue(), ); }).toList(); } } extension ElementX on Element { String get isarName { final ann = _nameChecker.firstAnnotationOfExact(nonSynthetic); late String name; if (ann == null) { name = displayName; } else { name = ann.getField('name')!.toStringValue()!; } checkIsarName(name, this); return name; } Collection? get collectionAnnotation { final ann = _collectionChecker.firstAnnotationOfExact(nonSynthetic); if (ann == null) { return null; } return Collection( inheritance: ann.getField('inheritance')!.toBoolValue()!, accessor: ann.getField('accessor')!.toStringValue(), ignore: ann .getField('ignore')! .toSetValue()! .map((e) => e.toStringValue()!) .toSet(), ); } String get collectionAccessor { var accessor = collectionAnnotation?.accessor; if (accessor != null) { return accessor; } accessor = displayName.decapitalize(); if (!accessor.endsWith('s')) { accessor += 's'; } return accessor; } Embedded? get embeddedAnnotation { final ann = _embeddedChecker.firstAnnotationOfExact(nonSynthetic); if (ann == null) { return null; } return Embedded( inheritance: ann.getField('inheritance')!.toBoolValue()!, ignore: ann .getField('ignore')! .toSetValue()! .map((e) => e.toStringValue()!) .toSet(), ); } } void checkIsarName(String name, Element element) { if (name.isBlank || name.startsWith('_')) { err('Names must not be blank or start with "_".', element); } } Never err(String msg, [Element? element]) { throw InvalidGenerationSourceError(msg, element: element); } ================================================ FILE: packages/isar_generator/lib/src/isar_analyzer.dart ================================================ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/helper.dart'; import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; class IsarAnalyzer { ObjectInfo analyzeCollection(Element element) { final constructor = _checkValidClass(element); final modelClass = element as ClassElement; final properties = []; final links = []; for (final propertyElement in modelClass.allAccessors) { if (propertyElement.isLink || propertyElement.isLinks) { final link = analyzeObjectLink(propertyElement); links.add(link); } else { final property = analyzeObjectProperty(propertyElement, constructor); properties.add(property); } } _checkValidPropertiesConstructor(properties, constructor); if (links.map((e) => e.isarName).distinct().length != links.length) { err('Two or more links have the same name.', modelClass); } final indexes = []; for (final propertyElement in modelClass.allAccessors) { indexes.addAll(analyzeObjectIndex(properties, propertyElement)); } if (indexes.map((e) => e.name).distinct().length != indexes.length) { err('Two or more indexes have the same name.', modelClass); } final idProperties = properties.where((it) => it.isId); if (idProperties.isEmpty) { err( 'No id property defined. Use the "Id" type for your id property.', modelClass, ); } else if (idProperties.length > 1) { err('Two or more properties with type "Id" defined.', modelClass); } return ObjectInfo( dartName: modelClass.displayName, isarName: modelClass.isarName, accessor: modelClass.collectionAccessor, properties: properties, embeddedDartNames: _getEmbeddedDartNames(element), indexes: indexes, links: links, ); } ObjectInfo analyzeEmbedded(Element element) { final constructor = _checkValidClass(element); final modelClass = element as ClassElement; if (constructor.parameters.any((e) => e.isRequired)) { err( 'Constructors of embedded objects must not have required parameters.', constructor, ); } final properties = []; for (final propertyElement in modelClass.allAccessors) { if (propertyElement.isLink || propertyElement.isLinks) { err('Embedded objects must not contain links', propertyElement); } else { final property = analyzeObjectProperty(propertyElement, constructor); properties.add(property); } } _checkValidPropertiesConstructor(properties, constructor); final hasIndex = modelClass.allAccessors.any( (it) => it.indexAnnotations.isNotEmpty, ); if (hasIndex) { err('Embedded objects must not have indexes.', modelClass); } final hasIdProperty = properties.any((it) => it.isId); if (hasIdProperty) { err('Embedded objects must not define an id.', modelClass); } return ObjectInfo( dartName: modelClass.displayName, isarName: modelClass.isarName, properties: properties, ); } ConstructorElement _checkValidClass(Element modelClass) { if (modelClass is! ClassElement || modelClass is EnumElement || modelClass is MixinElement) { err( 'Only classes may be annotated with @Collection or @Embedded.', modelClass, ); } if (modelClass.isAbstract) { err('Class must not be abstract.', modelClass); } if (!modelClass.isPublic) { err('Class must be public.', modelClass); } final constructor = modelClass.constructors .firstOrNullWhere((ConstructorElement c) => c.periodOffset == null); if (constructor == null) { err('Class needs an unnamed constructor.', modelClass); } final hasCollectionSupertype = modelClass.allSupertypes.any((type) { return type.element.collectionAnnotation != null || type.element.embeddedAnnotation != null; }); if (hasCollectionSupertype) { err( 'Class must not have a supertype annotated with @Collection or ' '@Embedded.', modelClass, ); } return constructor; } void _checkValidPropertiesConstructor( List properties, ConstructorElement constructor, ) { if (properties.map((e) => e.isarName).distinct().length != properties.length) { err( 'Two or more properties have the same name.', constructor.enclosingElement, ); } final unknownConstructorParameter = constructor.parameters.firstOrNullWhere( (p) => p.isRequired && properties.none((e) => e.dartName == p.name), ); if (unknownConstructorParameter != null) { err( 'Constructor parameter does not match a property.', unknownConstructorParameter, ); } } Map _getEmbeddedDartNames(ClassElement element) { void _fillNames(Map names, ClassElement element) { for (final property in element.allAccessors) { final type = property.type.scalarType.element; if (type is ClassElement && type.embeddedAnnotation != null) { final isarName = type.isarName; if (!names.containsKey(isarName)) { names[type.isarName] = type.displayName; _fillNames(names, type); } } } } final names = {}; _fillNames(names, element); return names; } ObjectProperty analyzeObjectProperty( PropertyInducingElement property, ConstructorElement constructor, ) { final dartType = property.type; final scalarDartType = dartType.scalarType; Map? enumMap; String? enumPropertyName; String? defaultEnumElement; late final IsarType isarType; if (scalarDartType.element is EnumElement) { final enumeratedAnn = property.enumeratedAnnotation; if (enumeratedAnn == null) { err('Enum property must be annotated with @enumerated.', property); } final enumClass = scalarDartType.element! as EnumElement; final enumElements = enumClass.fields.where((f) => f.isEnumConstant).toList(); defaultEnumElement = '${enumClass.name}.${enumElements.first.name}'; if (enumeratedAnn.type == EnumType.ordinal) { isarType = dartType.isDartCoreList ? IsarType.byteList : IsarType.byte; enumMap = { for (var i = 0; i < enumElements.length; i++) enumElements[i].name: i, }; enumPropertyName = 'index'; } else if (enumeratedAnn.type == EnumType.ordinal32) { isarType = dartType.isDartCoreList ? IsarType.intList : IsarType.int; enumMap = { for (var i = 0; i < enumElements.length; i++) enumElements[i].name: i, }; enumPropertyName = 'index'; } else if (enumeratedAnn.type == EnumType.name) { isarType = dartType.isDartCoreList ? IsarType.stringList : IsarType.string; enumMap = { for (final value in enumElements) value.name: value.name, }; enumPropertyName = 'name'; } else { enumPropertyName = enumeratedAnn.property; if (enumPropertyName == null) { err( 'Enums with type EnumType.value must specify which property ' 'should be used.', property, ); } final enumProperty = enumClass.getField(enumPropertyName); if (enumProperty == null || enumProperty.isEnumConstant) { err('Enum property "$enumProperty" does not exist.', property); } else if (enumProperty.nonSynthetic is PropertyAccessorElement) { err('Only fields are supported for enum properties', enumProperty); } final enumIsarType = enumProperty.type.isarType; if (enumIsarType != IsarType.byte && enumIsarType != IsarType.int && enumIsarType != IsarType.long && enumIsarType != IsarType.string) { err('Unsupported enum property type.', enumProperty); } isarType = dartType.isDartCoreList ? enumIsarType!.listType : enumIsarType!; enumMap = {}; for (final element in enumElements) { final property = element.computeConstantValue()!.getField(enumPropertyName)!; final propertyValue = property.toBoolValue() ?? property.toIntValue() ?? property.toDoubleValue() ?? property.toStringValue(); if (propertyValue == null) { err( 'Null values are not supported for enum properties.', enumProperty, ); } if (enumMap.values.contains(propertyValue)) { err( 'Enum property has duplicate values.', enumProperty, ); } enumMap[element.name] = propertyValue; } } } else { if (dartType.isarType != null) { isarType = dartType.isarType!; } else { err( 'Unsupported type. Please annotate the property with @ignore.', property, ); } } final nullable = dartType.nullabilitySuffix != NullabilitySuffix.none; final elementNullable = isarType.isList && dartType.scalarType.nullabilitySuffix != NullabilitySuffix.none; if ((isarType == IsarType.byte && nullable) || (isarType == IsarType.byteList && elementNullable)) { err('Bytes must not be nullable.', property); } final constructorParameter = constructor.parameters.firstOrNullWhere((p) => p.name == property.name); int? constructorPosition; late PropertyDeser deserialize; if (constructorParameter != null) { if (constructorParameter.type != property.type) { err( 'Constructor parameter type does not match property type', constructorParameter, ); } deserialize = constructorParameter.isNamed ? PropertyDeser.namedParam : PropertyDeser.positionalParam; constructorPosition = constructor.parameters.indexOf(constructorParameter); } else { deserialize = property.setter == null ? PropertyDeser.none : PropertyDeser.assign; } return ObjectProperty( dartName: property.displayName, isarName: property.isarName, typeClassName: dartType.scalarType.element!.name!, targetIsarName: isarType.containsObject ? dartType.scalarType.element!.isarName : null, isarType: isarType, isId: dartType.isIsarId, enumMap: enumMap, enumProperty: enumPropertyName, defaultEnumElement: defaultEnumElement, nullable: nullable, elementNullable: elementNullable, userDefaultValue: constructorParameter?.defaultValueCode, deserialize: deserialize, assignable: property.setter != null, constructorPosition: constructorPosition, ); } ObjectLink analyzeObjectLink(PropertyInducingElement property) { if (property.type.nullabilitySuffix != NullabilitySuffix.none) { err('Link properties must not be nullable.', property); } else if (property.isLate) { err('Link properties must not be late.', property); } final type = property.type as ParameterizedType; final linkType = type.typeArguments[0]; if (linkType.nullabilitySuffix != NullabilitySuffix.none) { err('Links type must not be nullable.', property); } final targetCol = linkType.element! as ClassElement; if (targetCol.collectionAnnotation == null) { err('Link target is not annotated with @collection'); } final backlinkAnn = property.backlinkAnnotation; String? targetLinkIsarName; if (backlinkAnn != null) { final targetProperty = targetCol.allAccessors .firstOrNullWhere((e) => e.displayName == backlinkAnn.to); if (targetProperty == null) { err('Target of Backlink does not exist', property); } else if (targetProperty.backlinkAnnotation != null) { err('Target of Backlink is also a backlink', property); } if (!targetProperty.isLink && !targetProperty.isLinks) { err('Target of backlink is not a link', property); } final targetLink = analyzeObjectLink(targetProperty); targetLinkIsarName = targetLink.isarName; } return ObjectLink( dartName: property.displayName, isarName: property.isarName, targetLinkIsarName: targetLinkIsarName, targetCollectionDartName: linkType.element!.name!, targetCollectionIsarName: targetCol.isarName, isSingle: property.isLink, ); } Iterable analyzeObjectIndex( List properties, PropertyInducingElement element, ) sync* { final property = properties.firstOrNullWhere((it) => it.dartName == element.name); if (property == null || property.isId) { return; } for (final index in element.indexAnnotations) { final indexProperties = []; final isString = property.isarType == IsarType.string || property.isarType == IsarType.stringList; final defaultType = property.isarType.isList || isString ? IndexType.hash : IndexType.value; indexProperties.add( ObjectIndexProperty( property: property, type: index.type ?? defaultType, caseSensitive: index.caseSensitive ?? isString, ), ); for (final c in index.composite) { final compositeProperty = properties.firstOrNullWhere((it) => it.dartName == c.property); if (compositeProperty == null) { err('Property does not exist: "${c.property}".', element); } else if (compositeProperty.isId) { err('Ids cannot be indexed', element); } else { final isString = compositeProperty.isarType == IsarType.string || compositeProperty.isarType == IsarType.stringList; final defaultType = compositeProperty.isarType.isList || isString ? IndexType.hash : IndexType.value; indexProperties.add( ObjectIndexProperty( property: compositeProperty, type: c.type ?? defaultType, caseSensitive: c.caseSensitive ?? isString, ), ); } } final name = index.name ?? indexProperties.map((e) => e.property.isarName).join('_'); checkIsarName(name, element); final objectIndex = ObjectIndex( name: name, properties: indexProperties, unique: index.unique, replace: index.replace, ); _verifyObjectIndex(objectIndex, element); yield objectIndex; } } void _verifyObjectIndex(ObjectIndex index, Element element) { final properties = index.properties; if (properties.map((it) => it.property.isarName).distinct().length != properties.length) { err('Composite index contains duplicate properties.', element); } for (var i = 0; i < properties.length; i++) { final property = properties[i]; if (property.isarType.isList && property.type != IndexType.hash && properties.length > 1) { err('Composite indexes do not support non-hashed lists.', element); } if (property.isarType.containsFloat && i != properties.lastIndex) { err( 'Only the last property of a composite index may be a ' 'double value.', element, ); } if (property.isarType == IsarType.string) { if (property.type != IndexType.hash && i != properties.lastIndex) { err( 'Only the last property of a composite index may be a ' 'non-hashed String.', element, ); } } if (property.isarType.containsObject) { err( 'Embedded objects may not be indexed.', element, ); } if (property.type != IndexType.value) { if (!property.isarType.isList && property.isarType != IsarType.string) { err('Only Strings and Lists may be hashed.', element); } else if (property.isarType.containsFloat) { err('List may must not be hashed.', element); } } if (property.isarType != IsarType.stringList && property.type == IndexType.hashElements) { err('Only String lists may have hashed elements.', element); } } if (!index.unique && index.replace) { err('Only unique indexes can replace.', element); } } } ================================================ FILE: packages/isar_generator/lib/src/isar_type.dart ================================================ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:isar/isar.dart'; import 'package:isar_generator/src/helper.dart'; import 'package:source_gen/source_gen.dart'; const TypeChecker _dateTimeChecker = TypeChecker.fromRuntime(DateTime); bool _isDateTime(Element element) => _dateTimeChecker.isExactly(element); extension DartTypeX on DartType { IsarType? get _primitiveIsarType { if (isDartCoreBool) { return IsarType.bool; } else if (isDartCoreInt) { if (alias?.element.name == 'byte') { return IsarType.byte; } else if (alias?.element.name == 'short') { return IsarType.int; } else { return IsarType.long; } } else if (isDartCoreDouble) { if (alias?.element.name == 'float') { return IsarType.float; } else { return IsarType.double; } } else if (isDartCoreString) { return IsarType.string; } else if (_isDateTime(element2!)) { return IsarType.dateTime; } else if (element2!.embeddedAnnotation != null) { return IsarType.object; } return null; } bool get isIsarId { return alias?.element.name == 'Id'; } DartType get scalarType { if (isDartCoreList) { final parameterizedType = this as ParameterizedType; final typeArguments = parameterizedType.typeArguments; if (typeArguments.isNotEmpty) { return typeArguments[0]; } } return this; } IsarType? get isarType { final primitiveType = _primitiveIsarType; if (primitiveType != null) { return primitiveType; } if (isDartCoreList) { switch (scalarType._primitiveIsarType) { case IsarType.bool: return IsarType.boolList; case IsarType.byte: return IsarType.byteList; case IsarType.int: return IsarType.intList; case IsarType.float: return IsarType.floatList; case IsarType.long: return IsarType.longList; case IsarType.double: return IsarType.doubleList; case IsarType.dateTime: return IsarType.dateTimeList; case IsarType.string: return IsarType.stringList; case IsarType.object: return IsarType.objectList; // ignore: no_default_cases default: return null; } } return null; } } extension IsarTypeX on IsarType { bool get containsBool => this == IsarType.bool || this == IsarType.boolList; bool get containsFloat => this == IsarType.float || this == IsarType.floatList || this == IsarType.double || this == IsarType.doubleList; bool get containsDate => this == IsarType.dateTime || this == IsarType.dateTimeList; bool get containsString => this == IsarType.string || this == IsarType.stringList; bool get containsObject => this == IsarType.object || this == IsarType.objectList; } ================================================ FILE: packages/isar_generator/lib/src/object_info.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; import 'package:xxh3/xxh3.dart'; class ObjectInfo { ObjectInfo({ required this.dartName, required this.isarName, this.accessor, required List properties, this.embeddedDartNames = const {}, this.indexes = const [], this.links = const [], }) { this.properties = properties.sortedBy((e) => e.isarName).toList(); } final String dartName; final String isarName; final String? accessor; late final List properties; final Map embeddedDartNames; final List indexes; final List links; int get id => xxh3(utf8.encode(isarName) as Uint8List); bool get isEmbedded => accessor == null; ObjectProperty get idProperty => properties.firstWhere((it) => it.isId); List get objectProperties => properties.where((it) => !it.isId).toList(); String get getIdName => '_${dartName.decapitalize()}GetId'; String get getLinksName => '_${dartName.decapitalize()}GetLinks'; String get attachName => '_${dartName.decapitalize()}Attach'; String get estimateSizeName => '_${dartName.decapitalize()}EstimateSize'; String get serializeName => '_${dartName.decapitalize()}Serialize'; String get deserializeName => '_${dartName.decapitalize()}Deserialize'; String get deserializePropName => '_${dartName.decapitalize()}DeserializeProp'; } enum PropertyDeser { none, assign, positionalParam, namedParam, } class ObjectProperty { ObjectProperty({ required this.dartName, required this.isarName, required this.typeClassName, this.targetIsarName, required this.isarType, required this.isId, required this.enumMap, required this.enumProperty, required this.defaultEnumElement, required this.nullable, required this.elementNullable, this.userDefaultValue, required this.deserialize, required this.assignable, this.constructorPosition, }); final String dartName; final String isarName; final String typeClassName; final String? targetIsarName; final bool isId; final IsarType isarType; final Map? enumMap; final String? enumProperty; final String? defaultEnumElement; final bool nullable; final bool elementNullable; final String? userDefaultValue; final PropertyDeser deserialize; final bool assignable; final int? constructorPosition; bool get isEnum => enumMap != null; String get scalarDartType { if (isId) { return 'Id'; } else if (isEnum) { return typeClassName; } switch (isarType) { case IsarType.bool: case IsarType.boolList: return 'bool'; case IsarType.byte: case IsarType.byteList: case IsarType.int: case IsarType.intList: case IsarType.long: case IsarType.longList: return 'int'; case IsarType.float: case IsarType.floatList: case IsarType.double: case IsarType.doubleList: return 'double'; case IsarType.dateTime: case IsarType.dateTimeList: return 'DateTime'; case IsarType.object: case IsarType.objectList: return typeClassName; case IsarType.string: case IsarType.stringList: return 'String'; } } String get nScalarDartType => isarType.isList ? '$scalarDartType${elementNullable ? '?' : ''}' : '$scalarDartType${nullable ? '?' : ''}'; String get dartType => isarType.isList ? 'List<$nScalarDartType>${nullable ? '?' : ''}' : nScalarDartType; String get targetSchema => '${scalarDartType.capitalize()}Schema'; String enumValueMapName(ObjectInfo object) { return '_${object.dartName}${dartName}EnumValueMap'; } String valueEnumMapName(ObjectInfo object) { return '_${object.dartName}${dartName}ValueEnumMap'; } } class ObjectIndexProperty { const ObjectIndexProperty({ required this.property, required this.type, required this.caseSensitive, }); final ObjectProperty property; final IndexType type; final bool caseSensitive; IsarType get isarType => property.isarType; bool get isMultiEntry => isarType.isList && type != IndexType.hash; } class ObjectIndex { ObjectIndex({ required this.name, required this.properties, required this.unique, required this.replace, }); final String name; final List properties; final bool unique; final bool replace; late final id = xxh3(utf8.encode(name) as Uint8List); } class ObjectLink { const ObjectLink({ required this.dartName, required this.isarName, this.targetLinkIsarName, required this.targetCollectionDartName, required this.targetCollectionIsarName, required this.isSingle, }); final String dartName; final String isarName; // isar name of the original link (only for backlinks) final String? targetLinkIsarName; final String targetCollectionDartName; final String targetCollectionIsarName; final bool isSingle; bool get isBacklink => targetLinkIsarName != null; int id(String objectIsarName) { final col = isBacklink ? targetCollectionIsarName : objectIsarName; final colId = xxh3(utf8.encode(col) as Uint8List, seed: isBacklink ? 1 : 0); final name = targetLinkIsarName ?? isarName; return xxh3(utf8.encode(name) as Uint8List, seed: colId); } } ================================================ FILE: packages/isar_generator/pubspec.yaml ================================================ name: isar_generator description: Code generator for the Isar Database. Finds classes annotated with @Collection. version: 3.1.8 repository: https://github.com/isar-community/isar homepage: https://isar.dev publish_to: https://pub.isar-community.dev/ environment: sdk: ">=2.17.0 <3.0.0" dependencies: analyzer: ">=4.6.0 <7.0.0" build: ^2.3.0 dart_style: ^2.2.3 dartx: ^1.1.0 glob: ^2.0.2 isar: version: 3.1.8 hosted: https://pub.isar-community.dev path: ^1.8.1 source_gen: ^1.2.2 xxh3: ^1.0.1 dev_dependencies: build_test: ^2.1.5 matcher: ^0.12.12 test: ^1.21.0 very_good_analysis: ^3.0.1 ================================================ FILE: packages/isar_generator/pubspec_overrides.yaml ================================================ dependency_overrides: isar: path: ../isar ================================================ FILE: packages/isar_generator/test/error_test.dart ================================================ import 'dart:io'; import 'package:build/build.dart'; import 'package:build_test/build_test.dart'; import 'package:isar_generator/isar_generator.dart'; import 'package:test/test.dart'; void main() { group('Error case', () { for (final file in Directory('test/errors').listSync(recursive: true)) { if (file is! File || !file.path.endsWith('.dart')) continue; test(file.path, () async { final content = await file.readAsLines(); final errorMessage = content.first.split('//').last.trim(); var error = ''; try { await testBuilder( getIsarGenerator(BuilderOptions.empty), {'a|${file.path}': content.join('\n')}, reader: await PackageAssetReader.currentIsolate(), ); } catch (e) { error = e.toString(); } expect(error.toLowerCase(), contains(errorMessage.toLowerCase())); }); } }); } ================================================ FILE: packages/isar_generator/test/errors/class/abstract.dart ================================================ // must not be abstract import 'package:isar/isar.dart'; @collection abstract class Model { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/class/collection_supertype.dart ================================================ // supertype annotated with @collection import 'package:isar/isar.dart'; @collection class Supertype { Id? id; } class Subtype implements Supertype { @override Id? id; } @collection class Model implements Subtype { @override Id? id; } ================================================ FILE: packages/isar_generator/test/errors/class/constructor_named.dart ================================================ // unnamed constructor import 'package:isar/isar.dart'; @collection class Model { Model.create(); Id? id; } ================================================ FILE: packages/isar_generator/test/errors/class/constructor_unknown_parameter.dart ================================================ // constructor parameter does not match a property import 'package:isar/isar.dart'; @collection class Model { // ignore: avoid_unused_constructor_parameters Model(this.prop1, String somethingElse); Id? id; final String prop1; } ================================================ FILE: packages/isar_generator/test/errors/class/constructor_wrong_parameter.dart ================================================ // constructor parameter type does not match property type import 'package:isar/isar.dart'; @collection class Model { // ignore: avoid_unused_constructor_parameters Model(int prop1); Id? id; String prop1 = '5'; } ================================================ FILE: packages/isar_generator/test/errors/class/enum.dart ================================================ // only classes import 'package:isar/isar.dart'; // ignore: invalid_annotation_target @collection enum Test { a, b, c } ================================================ FILE: packages/isar_generator/test/errors/class/invalid_name.dart ================================================ // must not be abstract import 'package:isar/isar.dart'; @collection abstract class Model { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/class/mixin.dart ================================================ // only classes import 'package:isar/isar.dart'; // ignore: invalid_annotation_target @collection mixin Test {} ================================================ FILE: packages/isar_generator/test/errors/class/private.dart ================================================ // must be public import 'package:isar/isar.dart'; @collection // ignore: unused_element class _Model { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/class/variable.dart ================================================ // only classes import 'package:isar/isar.dart'; // ignore: invalid_annotation_target @collection const t = 'hello'; ================================================ FILE: packages/isar_generator/test/errors/id/duplicate.dart ================================================ // two or more properties with type "Id" defined import 'package:isar/isar.dart'; @collection class Test { Id? id1; Id? id2; } ================================================ FILE: packages/isar_generator/test/errors/id/missing.dart ================================================ // no id property defined import 'package:isar/isar.dart'; @collection class Test { late int id; late String name; } ================================================ FILE: packages/isar_generator/test/errors/index/composite_double_not_last.dart ================================================ // only the last property of a composite index may be a double value import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('val2')]) double? val1; String? val2; } ================================================ FILE: packages/isar_generator/test/errors/index/composite_non_hashed_list.dart ================================================ // composite indexes do not support non-hashed lists import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('str')], type: IndexType.value) List? list; String? str; } ================================================ FILE: packages/isar_generator/test/errors/index/composite_string_value_not_last.dart ================================================ // last property of a composite index may be a non-hashed string import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('str2')], type: IndexType.value) String? str1; String? str2; } ================================================ FILE: packages/isar_generator/test/errors/index/contains_id.dart ================================================ // ids cannot be indexed import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('id')]) String? str; } ================================================ FILE: packages/isar_generator/test/errors/index/double_list_hashed.dart ================================================ // list may must not be hashed import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(type: IndexType.hash) List? list; } ================================================ FILE: packages/isar_generator/test/errors/index/duplicate_name.dart ================================================ // same name import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(name: 'myindex') String? prop1; @Index(name: 'myindex') String? prop2; } ================================================ FILE: packages/isar_generator/test/errors/index/duplicate_property.dart ================================================ // composite index contains duplicate properties import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('str1')], type: IndexType.value) String? str1; String? str2; } ================================================ FILE: packages/isar_generator/test/errors/index/invalid_name.dart ================================================ // names must not be blank or start with "_" import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(name: '_index') String? str; } ================================================ FILE: packages/isar_generator/test/errors/index/non_string_hashed.dart ================================================ // only strings and lists may be hashed import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(type: IndexType.hash) int? val; } ================================================ FILE: packages/isar_generator/test/errors/index/non_string_list_hashed_elements.dart ================================================ // only string lists may have hashed elements import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(type: IndexType.hashElements) List? list; } ================================================ FILE: packages/isar_generator/test/errors/index/non_unique_replace.dart ================================================ // only unique indexes can replace import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(replace: true) String? str; } ================================================ FILE: packages/isar_generator/test/errors/index/object_hashed.dart ================================================ // objects may not be indexed import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index() EmbeddedModel? obj; } @embedded class EmbeddedModel {} ================================================ FILE: packages/isar_generator/test/errors/index/object_list_hashed.dart ================================================ // objects may not be indexed import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(type: IndexType.hash) List? list; } @embedded class EmbeddedModel {} ================================================ FILE: packages/isar_generator/test/errors/index/property_does_not_exist.dart ================================================ // property does not exist import 'package:isar/isar.dart'; @collection class Model { Id? id; @Index(composite: [CompositeIndex('myProp')]) String? str; } ================================================ FILE: packages/isar_generator/test/errors/link/backlink_target_does_no_exist.dart ================================================ // target of backlink does not exist import 'package:isar/isar.dart'; @collection class Model1 { Id? id; @Backlink(to: 'abc') final IsarLink link = IsarLink(); } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/link/backlink_target_is_backlink.dart ================================================ // target of backlink is also a backlink import 'package:isar/isar.dart'; @collection class Model1 { Id? id; @Backlink(to: 'link') final IsarLink link = IsarLink(); } @collection class Model2 { Id? id; @Backlink(to: 'link') final IsarLink link = IsarLink(); } ================================================ FILE: packages/isar_generator/test/errors/link/backlink_target_not_a_link.dart ================================================ // target of backlink is not a link import 'package:isar/isar.dart'; @collection class Model1 { Id? id; @Backlink(to: 'str') final IsarLink link = IsarLink(); } @collection class Model2 { Id? id; String? str; } ================================================ FILE: packages/isar_generator/test/errors/link/duplicate_name.dart ================================================ // same name import 'package:isar/isar.dart'; @collection class Model { Id? id; final IsarLink prop1 = IsarLink(); @Name('prop1') final IsarLinks prop2 = IsarLinks(); } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/link/invalid_name.dart ================================================ // names must not be blank or start with import 'package:isar/isar.dart'; @collection class Model { Id? id; @Name('_link') final IsarLink link = IsarLink(); } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/link/late.dart ================================================ // must not be late import 'package:isar/isar.dart'; @collection class Model { Id? id; late IsarLink link; } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/link/nullable.dart ================================================ // must not be nullable import 'package:isar/isar.dart'; @collection class Model { Id? id; IsarLink? link; } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/link/target_not_a_collection.dart ================================================ // link target is not annotated with @collection import 'package:isar/isar.dart'; @collection class Model { Id? id; final IsarLink link = IsarLink(); } ================================================ FILE: packages/isar_generator/test/errors/link/type_nullable.dart ================================================ // links type must not be nullable import 'package:isar/isar.dart'; @collection class Model { Id? id; final IsarLink link = IsarLink(); } @collection class Model2 { Id? id; } ================================================ FILE: packages/isar_generator/test/errors/property/duplicate_name.dart ================================================ // same name import 'package:isar/isar.dart'; @collection class Model { Id? id; String? prop1; @Name('prop1') String? prop2; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_bool_type.dart ================================================ // unsupported enum property type import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum field; } enum MyEnum { optionA; final bool value = true; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_double_type.dart ================================================ // unsupported enum property type import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum field; } enum MyEnum { optionA; final double value = 5.5; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_duplicate.dart ================================================ // has duplicate values import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum field; } enum MyEnum { option1(1), option2(2), option3(1); const MyEnum(this.value); final int value; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_float_type.dart ================================================ // unsupported enum property type import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum field; } enum MyEnum { optionA; final float value = 5.5; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_list_type.dart ================================================ // unsupported enum property type import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum prop; } enum MyEnum { optionA; final List value = []; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_not_annotated.dart ================================================ // enum property must be annotated with @enumerated import 'package:isar/isar.dart'; @collection class Model { Id? id; late MyEnum? prop; } enum MyEnum { a; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_null_value.dart ================================================ // null values are not supported import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum prop; } enum MyEnum { optionA; final String? value = null; } ================================================ FILE: packages/isar_generator/test/errors/property/enum_object_type.dart ================================================ // unsupported enum property type import 'package:isar/isar.dart'; @collection class Model { Id? id; @Enumerated(EnumType.value, 'value') late MyEnum prop; } enum MyEnum { optionA; final value = EmbeddedModel(); } @embedded class EmbeddedModel {} ================================================ FILE: packages/isar_generator/test/errors/property/invalid_name.dart ================================================ // names must not be blank or start with "_" import 'package:isar/isar.dart'; @collection class Model { Id? id; @Name('_prop') String? prop; } ================================================ FILE: packages/isar_generator/test/errors/property/null_byte.dart ================================================ // bytes must not be nullable import 'package:isar/isar.dart'; @collection class Model { Id? id; late byte? prop; } ================================================ FILE: packages/isar_generator/test/errors/property/null_byte_element.dart ================================================ // bytes must not be nullable import 'package:isar/isar.dart'; @collection class Model { Id? id; late List prop; } ================================================ FILE: packages/isar_generator/test/errors/property/unsupported_type.dart ================================================ // unsupported type import 'package:isar/isar.dart'; @collection class Model { Id? id; late Set? prop; } ================================================ FILE: packages/isar_inspector/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c - platform: macos create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: packages/isar_inspector/README.md ================================================ ## Isar Inspector ================================================ FILE: packages/isar_inspector/analysis_options.yaml ================================================ include: package:very_good_analysis/analysis_options.yaml analyzer: exclude: - "**/*.g.dart" errors: cascade_invocations: ignore avoid_positional_boolean_parameters: ignore parameter_assignments: ignore public_member_api_docs: ignore use_string_buffers: ignore ================================================ FILE: packages/isar_inspector/isar_inspector.iml ================================================ ================================================ FILE: packages/isar_inspector/lib/collection/button_prev_next.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar_inspector/collection/collection_area.dart'; class PrevNextButtons extends StatelessWidget { const PrevNextButtons({ super.key, required this.page, required this.count, required this.onChanged, }); final int page; final int count; final void Function(int newPage) onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); var from = 0; var to = 0; if (count > 0) { from = page * objectsPerPage + 1; to = from + objectsPerPage - 1; if (to > count) { to = count; } } return Row( mainAxisSize: MainAxisSize.min, children: [ Tooltip( message: 'Previous page', child: TextButton( onPressed: page > 0 ? () => onChanged(page - 1) : null, child: const Text('Prev'), ), ), const SizedBox(width: 10), Tooltip( message: 'Current page', child: Row( children: [ Text( '$from', style: const TextStyle(fontWeight: FontWeight.bold), ), Text( ' - ', style: TextStyle( color: theme.colorScheme.onBackground.withOpacity(0.7), ), ), Text( '$to', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), Text( ' of ', style: TextStyle( color: theme.colorScheme.onBackground.withOpacity(0.7), ), ), Tooltip( message: 'Total number of objects', child: Text( '$count', style: const TextStyle(fontWeight: FontWeight.bold), ), ), const SizedBox(width: 10), Tooltip( message: 'Next page', child: TextButton( onPressed: to == count ? null : () => onChanged(page + 1), child: const Text('Next'), ), ), ], ); } } ================================================ FILE: packages/isar_inspector/lib/collection/button_sort.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; class SortButtons extends StatelessWidget { const SortButtons({ super.key, required this.properties, required this.property, required this.asc, required this.onChanged, }); final List properties; final String property; final bool asc; final void Function(String property, bool asc) onChanged; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Tooltip( message: 'Sort results by this property', child: DropdownButtonHideUnderline( child: DropdownButton( isDense: true, items: [ for (final property in properties) if (property.type != IsarType.object && !property.type.isList) DropdownMenuItem( value: property.name, child: Text(property.name), ), ], value: property, onChanged: (value) { if (value != null) { onChanged(value, asc); } }, ), ), ), const SizedBox(width: 10), ActionChip( label: Text(asc ? 'Asc' : 'Desc'), onPressed: () { onChanged(property, !asc); }, tooltip: 'Toggle sort order', ), ], ); } } ================================================ FILE: packages/isar_inspector/lib/collection/collection_area.dart ================================================ // ignore_for_file: type_annotate_public_apis, avoid_web_libraries_in_flutter import 'dart:async'; import 'dart:convert'; import 'dart:html'; import 'dart:math'; import 'package:clickup_fading_scroll/clickup_fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/collection/button_prev_next.dart'; import 'package:isar_inspector/collection/button_sort.dart'; import 'package:isar_inspector/collection/objects_list_sliver.dart'; import 'package:isar_inspector/connect_client.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/query_builder/query_group.dart'; import 'package:isar_inspector/util.dart'; const objectsPerPage = 20; class CollectionArea extends StatefulWidget { const CollectionArea({ super.key, required this.instance, required this.collection, required this.schemas, required this.client, }); final String instance; final String collection; final Map> schemas; final ConnectClient client; CollectionSchema get collectionSchema => schemas[collection]! as CollectionSchema; @override State createState() => _CollectionAreaState(); } class _CollectionAreaState extends State { final controller = ScrollController(); late final StreamSubscription querySubscription; var page = 0; var filter = const FilterGroup.and([]); late var sortProperty = widget.collectionSchema.idName; var sortAsc = true; var objects = []; var objectsCount = 0; @override void initState() { querySubscription = widget.client.queryChanged.listen((_) { _runQuery(); }); _runQuery(); super.initState(); } @override void dispose() { querySubscription.cancel(); super.dispose(); } Future _runQuery() async { final query = ConnectQuery( instance: widget.instance, collection: widget.collection, filter: filter, offset: page * objectsPerPage, limit: (page + 1) * objectsPerPage, sortProperty: sortProperty, sortAsc: sortAsc, ); final result = await widget.client.executeQuery(query); final objects = (result['objects']! as List) .map((e) => IsarObject(e as Map)) .toList(); if (mounted) { setState(() { this.objects = objects; objectsCount = result['count']! as int; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: FadingScroll( controller: controller, builder: (context, controller) { return CustomScrollView( controller: controller, slivers: [ SliverToBoxAdapter( child: QueryGroup( collection: widget.collectionSchema, group: filter, level: 0, onChanged: (group) { setState(() { filter = group; }); _runQuery(); }, ), ), ObjectsListSliver( instance: widget.instance, collection: widget.collection, schemas: widget.schemas, objects: objects, onUpdate: _onUpdate, onDelete: _onDelete, ), ], ); }, ), ), const SizedBox(height: 20), Stack( children: [ Positioned.fill( child: Center( child: PrevNextButtons( page: page, count: objectsCount, onChanged: (newPage) { setState(() { page = newPage; }); _runQuery(); }, ), ), ), Row( children: [ SortButtons( properties: widget.collectionSchema.idAndProperties, property: sortProperty, asc: sortAsc, onChanged: (property, asc) { setState(() { sortProperty = property; sortAsc = asc; }); _runQuery(); }, ), const Spacer(), Row( children: [ IconButton( icon: Icon( Icons.add_rounded, color: theme.colorScheme.onBackground, ), iconSize: 26, tooltip: 'Create Object', onPressed: _onCreate, ), const SizedBox(width: 5), IconButton( icon: Icon( Icons.paste_rounded, color: theme.colorScheme.onBackground, ), iconSize: 20, tooltip: 'Import JSON from clipboard', onPressed: _onImport, ), const SizedBox(width: 5), IconButton( icon: Icon( Icons.download_rounded, color: theme.colorScheme.onBackground, ), tooltip: 'Download All', onPressed: _onDownload, ), const SizedBox(width: 5), IconButton( icon: Icon( Icons.delete_forever_rounded, color: theme.colorScheme.onBackground, ), tooltip: 'Delete All', onPressed: _onDeleteAll, ), ], ) ], ), ], ) ], ); } void _onUpdate(String collection, int id, String path, dynamic value) { final edit = ConnectEdit( instance: widget.instance, collection: collection, id: id, path: path, value: value, ); widget.client.editProperty(edit); } void _onDelete(int id) { final query = ConnectQuery( instance: widget.instance, collection: widget.collection, filter: FilterCondition.equalTo( property: widget.collectionSchema.idName, value: id, ), ); widget.client.removeQuery(query); } Future _onCreate() async { final idName = widget.collectionSchema.idName; final randomId = Random().nextInt(100000000); await widget.client.importJson( widget.instance, widget.collection, [ {idName: randomId} ], ); if (!mounted) return; setState(() { filter = FilterGroup.and([ FilterCondition.equalTo( property: idName, value: randomId, ), ]); }); await _runQuery(); } Future _onImport() async { try { final jsonStr = await Clipboard.getData(Clipboard.kTextPlain); var json = jsonDecode(jsonStr!.text!); if (json is! List) { json = [json]; } await widget.client.importJson(widget.instance, widget.collection, json); } on PlatformException { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Could not access clipboard.')), ); } on FormatException { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Invalid JSON in clipboard.')), ); } } void _onDeleteAll() { final query = ConnectQuery( instance: widget.instance, collection: widget.collection, filter: filter, ); widget.client.removeQuery(query); } Future _onDownload() async { final query = ConnectQuery( instance: widget.instance, collection: widget.collection, filter: filter, ); final data = await widget.client.exportJson(query); try { final base64 = base64Encode(utf8.encode(jsonEncode(data))); final anchor = AnchorElement(href: 'data:application/octet-stream;base64,$base64') ..target = 'blank' ..download = '${widget.collection}.json'; document.body!.append(anchor); anchor.click(); anchor.remove(); } catch (_) {} } } ================================================ FILE: packages/isar_inspector/lib/collection/objects_list_sliver.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/object/object_view.dart'; class ObjectsListSliver extends StatelessWidget { const ObjectsListSliver({ super.key, required this.instance, required this.collection, required this.schemas, required this.objects, required this.onUpdate, required this.onDelete, }); final String instance; final String collection; final Map> schemas; final List objects; final void Function( String collection, int id, String path, dynamic value, ) onUpdate; final void Function(int id) onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); final collectionSchema = schemas[collection]! as CollectionSchema; return SliverList( delegate: SliverChildBuilderDelegate( childCount: objects.length, (BuildContext context, int index) { final object = objects[index]; return Card( key: Key('object ${object.getValue(collectionSchema.idName)}'), child: Padding( padding: const EdgeInsets.all(5), child: Stack( children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 15), child: ObjectView( schemaName: collection, schemas: schemas, object: object, root: true, onUpdate: (collection, id, path, value) { onUpdate(collection, id!, path, value); }, ), ), Positioned( top: 0, right: 0, child: Row( children: [ IconButton( icon: Icon( Icons.copy_rounded, color: theme.colorScheme.onPrimaryContainer, ), tooltip: 'Copy as JSON', visualDensity: VisualDensity.standard, onPressed: () => _copyObject(object), ), IconButton( icon: Icon( Icons.delete_rounded, color: theme.colorScheme.onPrimaryContainer, ), tooltip: 'Delete', visualDensity: VisualDensity.standard, onPressed: () { final id = object.getValue(collectionSchema.idName); onDelete(id as int); }, ), ], ), ) ], ), ), ); }, ), ); } void _copyObject(IsarObject object) { final json = Map.of(object.data); final schema = schemas[collection]! as CollectionSchema; for (final linkName in schema.links.keys) { json.remove(linkName); } Clipboard.setData(ClipboardData(text: jsonEncode(json))); } } ================================================ FILE: packages/isar_inspector/lib/collections_list.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/connect_client.dart'; class CollectionsList extends StatelessWidget { const CollectionsList({ super.key, required this.collections, required this.collectionInfo, required this.selectedCollection, required this.onSelected, }); final List> collections; final Map collectionInfo; final String? selectedCollection; final void Function(String collection) onSelected; @override Widget build(BuildContext context) { final theme = Theme.of(context); return ListView.builder( primary: false, itemBuilder: (BuildContext context, int index) { final collection = collections[index]; final info = collectionInfo[collection.name]; return Padding( padding: const EdgeInsets.only(bottom: 10), child: ElevatedButton( style: collection.name == selectedCollection ? ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primaryContainer, foregroundColor: theme.colorScheme.onPrimaryContainer, ) : null, onPressed: () { onSelected(collection.name); }, child: Padding( padding: const EdgeInsets.only( left: 25, right: 10, top: 12, bottom: 12, ), child: Row( children: [ Expanded( child: Text( collection.name, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, overflow: TextOverflow.ellipsis, ), ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( info?.count.toString() ?? 'loading', style: const TextStyle( fontSize: 12, ), ), const SizedBox(height: 2), Text( _formatSize(info?.size ?? 0), style: const TextStyle( fontSize: 12, ), ), ], ), ], ), ), ), ); }, itemCount: collections.length, ); } } String _formatSize(int bytes) { if (bytes <= 0) return '0 B'; const suffixes = ['B', 'KB', 'MB', 'GB']; final n = (log(bytes) / log(1024)).floor(); final index = min(n, suffixes.length - 1); final value = bytes / pow(1024, index); return '${value.toStringAsFixed(index == 0 ? 0 : 2)} ${suffixes[index]}'; } ================================================ FILE: packages/isar_inspector/lib/connect_client.dart ================================================ // ignore_for_file: implementation_imports import 'dart:async'; import 'dart:convert'; import 'package:isar/isar.dart'; import 'package:isar/src/isar_connect_api.dart'; import 'package:vm_service/vm_service.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; export 'package:isar/src/isar_connect_api.dart'; class ConnectClient { ConnectClient(this.vmService, this.isolateId); static const Duration kNormalTimeout = Duration(seconds: 4); static const Duration kLongTimeout = Duration(seconds: 10); final VmService vmService; final String isolateId; final collectionInfo = {}; final _instancesChangedController = StreamController.broadcast(); final _collectionInfoChangedController = StreamController.broadcast(); final _queryChangedController = StreamController.broadcast(); Stream get instancesChanged => _instancesChangedController.stream; Stream get collectionInfoChanged => _collectionInfoChangedController.stream; Stream get queryChanged => _queryChangedController.stream; static Future connect(String port, String secret) async { final wsUrl = Uri.parse('ws://127.0.0.1:$port/$secret=/ws'); final channel = WebSocketChannel.connect(wsUrl); // ignore: avoid_print final stream = channel.stream.handleError(print); final service = VmService( stream, channel.sink.add, disposeHandler: channel.sink.close, ); final vm = await service.getVM(); final isolateId = vm.isolates!.where((e) => e.name == 'main').first.id!; await service.streamListen(EventStreams.kExtension); final client = ConnectClient(service, isolateId); final handlers = { ConnectEvent.instancesChanged.event: (_) { client._instancesChangedController.add(null); }, ConnectEvent.collectionInfoChanged.event: (Map json) { final collectionInfo = ConnectCollectionInfo.fromJson(json); client.collectionInfo[collectionInfo.collection] = collectionInfo; client._collectionInfoChangedController.add(null); }, ConnectEvent.queryChanged.event: (_) { client._queryChangedController.add(null); }, }; service.onExtensionEvent.listen((Event event) { final data = event.extensionData?.data ?? {}; handlers[event.extensionKind]?.call(data); }); return client; } Future _call( ConnectAction action, { Duration? timeout = kNormalTimeout, Map? args, }) async { var responseFuture = vmService.callServiceExtension( action.method, isolateId: isolateId, args: { if (args != null) 'args': jsonEncode(args), }, ); if (timeout != null) { responseFuture = responseFuture.timeout(timeout); } final response = await responseFuture; return response.json?['result'] as T; } Future>> getSchema() async { final schema = await _call>(ConnectAction.getSchema); return schema .map( (e) => CollectionSchema.fromJson(e as Map), ) .toList(); } Future> listInstances() async { final instances = await _call>(ConnectAction.listInstances); return instances.cast(); } Future watchInstance(String instance) async { collectionInfo.clear(); await _call( ConnectAction.watchInstance, args: {'instance': instance}, ); } Future> executeQuery(ConnectQuery query) async { return _call>( ConnectAction.executeQuery, args: query.toJson(), timeout: kLongTimeout, ); } Future removeQuery(ConnectQuery query) async { await _call( ConnectAction.removeQuery, args: query.toJson(), timeout: kLongTimeout, ); } Future importJson( String instance, String collection, List objects, ) async { await _call( ConnectAction.importJson, args: { 'instance': instance, 'collection': collection, 'objects': objects, }, ); } Future> exportJson(ConnectQuery query) async { final data = await _call>( ConnectAction.exportJson, args: query.toJson(), timeout: kLongTimeout, ); return data.cast(); } Future editProperty(ConnectEdit edit) async { await _call( ConnectAction.editProperty, args: edit.toJson(), timeout: kLongTimeout, ); } Future disconnect() async { await vmService.dispose(); } } ================================================ FILE: packages/isar_inspector/lib/connected_layout.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/collection/collection_area.dart'; import 'package:isar_inspector/connect_client.dart'; import 'package:isar_inspector/sidebar.dart'; class ConnectedLayout extends StatefulWidget { const ConnectedLayout({ super.key, required this.client, required this.instances, required this.collections, }); final ConnectClient client; final List instances; final List> collections; @override State createState() => _ConnectedLayoutState(); } class _ConnectedLayoutState extends State { late String selectedInstance; late String selectedCollection = widget.collections.first.name; late StreamSubscription infoSubscription; @override void initState() { _selectInstance(widget.instances.first); infoSubscription = widget.client.collectionInfoChanged.listen((_) { setState(() {}); }); super.initState(); } @override void didUpdateWidget(covariant ConnectedLayout oldWidget) { if (!widget.instances.contains(selectedInstance)) { _selectInstance(widget.instances.first); } super.didUpdateWidget(oldWidget); } @override void dispose() { infoSubscription.cancel(); super.dispose(); } void _selectInstance(String instance) { selectedInstance = instance; widget.client.watchInstance(instance); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(25), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: 320, child: Sidebar( instances: widget.instances, selectedInstance: selectedInstance, onInstanceSelected: (instance) { setState(() { _selectInstance(instance); }); }, collections: widget.collections, collectionInfo: widget.client.collectionInfo, selectedCollection: selectedCollection, onCollectionSelected: (collection) { setState(() { selectedCollection = collection; }); }, ), ), const SizedBox(width: 25), Expanded( child: CollectionArea( key: Key('$selectedInstance.$selectedCollection'), instance: selectedInstance, collection: selectedCollection, client: widget.client, schemas: { for (final schema in widget.collections) ...{ schema.name: schema, for (final embedded in schema.embeddedSchemas.values) ...{ embedded.name: embedded, } } }, ), ) ], ), ); } } ================================================ FILE: packages/isar_inspector/lib/connection_screen.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/connect_client.dart'; import 'package:isar_inspector/connected_layout.dart'; import 'package:isar_inspector/error_screen.dart'; class ConnectionScreen extends StatefulWidget { const ConnectionScreen({ super.key, required this.port, required this.secret, }); final String port; final String secret; @override State createState() => _ConnectionPageState(); } class _ConnectionPageState extends State { late Future clientFuture; @override void initState() { clientFuture = ConnectClient.connect(widget.port, widget.secret); super.initState(); } @override void didUpdateWidget(covariant ConnectionScreen oldWidget) { if (oldWidget.port != widget.port || oldWidget.secret != widget.secret) { clientFuture = ConnectClient.connect(widget.port, widget.secret); } super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return FutureBuilder( future: clientFuture, builder: (context, snapshot) { if (snapshot.hasData) { return _SchemaLoader(client: snapshot.data!); } else if (snapshot.hasError) { return const ErrorScreen(); } else { return const Loading(); } }, ); } } class _SchemaLoader extends StatefulWidget { const _SchemaLoader({required this.client}); final ConnectClient client; @override State<_SchemaLoader> createState() => _SchemaLoaderState(); } class _SchemaLoaderState extends State<_SchemaLoader> { late Future> instancesFuture; late Future>> collectionsFuture; late StreamSubscription _instancesSubscription; @override void initState() { instancesFuture = widget.client.listInstances(); collectionsFuture = widget.client.getSchema(); _instancesSubscription = widget.client.instancesChanged.listen((event) { setState(() { instancesFuture = widget.client.listInstances(); }); }); super.initState(); } @override void didUpdateWidget(covariant _SchemaLoader oldWidget) { instancesFuture = widget.client.listInstances(); collectionsFuture = widget.client.getSchema(); super.didUpdateWidget(oldWidget); } @override void dispose() { _instancesSubscription.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return FutureBuilder>( future: Future.wait([instancesFuture, collectionsFuture]), builder: (context, snapshot) { if (snapshot.hasData) { return ConnectedLayout( client: widget.client, instances: snapshot.data![0] as List, collections: snapshot.data![1] as List>, ); } else if (snapshot.hasError) { return const ErrorScreen(); } else { return const Loading(); } }, ); } } class Loading extends StatelessWidget { const Loading({super.key}); @override Widget build(BuildContext context) { return const Center( child: CircularProgressIndicator(), ); } } ================================================ FILE: packages/isar_inspector/lib/error_screen.dart ================================================ // ignore: avoid_web_libraries_in_flutter import 'dart:html'; import 'package:flutter/material.dart'; class ErrorScreen extends StatelessWidget { const ErrorScreen({super.key}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Disconnected', style: TextStyle(fontSize: 20), ), const SizedBox(height: 10), const Text('Please make sure your Isar instance is running.'), const SizedBox(height: 40), ElevatedButton( onPressed: window.location.reload, child: const Text('Retry Connection'), ), ], ), ); } } ================================================ FILE: packages/isar_inspector/lib/instance_selector.dart ================================================ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class InstanceSelector extends StatefulWidget { const InstanceSelector({ super.key, required this.instances, required this.selectedInstance, required this.onSelected, }); final List instances; final String selectedInstance; final void Function(String instance) onSelected; @override State createState() => _InstanceSelectorState(); } class _InstanceSelectorState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); late final Animation _animation = CurvedAnimation( parent: _controller, curve: Curves.easeOut, ); @override void initState() { _animation.addStatusListener((AnimationStatus status) { setState(() {}); }); super.initState(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Stack( alignment: Alignment.bottomCenter, children: [ Card( margin: const EdgeInsets.all(10), color: theme.colorScheme.secondaryContainer, child: SizeTransition( sizeFactor: _animation, axisAlignment: -1, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 10), for (var instance in widget.instances) if (instance != widget.selectedInstance) InstanceButton( instance: instance, onTap: () { widget.onSelected(instance); _controller.reverse(); }, ), const SizedBox(height: 75), ], ), ), ), SelectedInstanceButton( instance: widget.selectedInstance, hasMultiple: widget.instances.length > 1, color: _animation.status != AnimationStatus.dismissed ? Colors.blue : null, onTap: () { if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); } }, ), ], ), ], ); } } class InstanceButton extends StatelessWidget { const InstanceButton({ super.key, required this.instance, required this.onTap, }); final String instance; final VoidCallback onTap; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Card( margin: EdgeInsets.zero, color: Colors.transparent, clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(20), child: Center( child: Text( instance, textAlign: TextAlign.start, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ), ), ); } } class SelectedInstanceButton extends StatelessWidget { const SelectedInstanceButton({ super.key, required this.instance, required this.onTap, required this.hasMultiple, required this.color, }); final String instance; final VoidCallback onTap; final bool hasMultiple; final Color? color; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.all(10), color: theme.colorScheme.secondaryContainer, clipBehavior: Clip.antiAlias, child: InkWell( onTap: hasMultiple ? onTap : null, child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ Icon( FontAwesomeIcons.database, size: 25, color: theme.colorScheme.onSecondaryContainer, ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( instance, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: theme.colorScheme.onSecondaryContainer, ), ), Text( 'Isar Instance', style: theme.textTheme.bodyMedium!.copyWith( color: theme.colorScheme.onSecondaryContainer, ), ) ], ), const Spacer(), if (hasMultiple) Column( mainAxisSize: MainAxisSize.min, children: const [ Icon( FontAwesomeIcons.chevronUp, size: 12, ), Icon( FontAwesomeIcons.chevronDown, size: 12, ), ], ), ], ), ), ), ), ); } } ================================================ FILE: packages/isar_inspector/lib/main.dart ================================================ // ignore: avoid_web_libraries_in_flutter import 'dart:html'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:isar_inspector/connection_screen.dart'; void main() async { if (window.navigator.userAgent.toLowerCase().contains('chrome')) { runApp( DarkMode( notifier: DarkModeNotifier(), child: const App(), ), ); } else { runApp(const UnsupportedBrowser()); } } class UnsupportedBrowser extends StatelessWidget { const UnsupportedBrowser({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Isar Inspector', theme: ThemeData.from( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF9FC9FF), brightness: Brightness.dark, ), useMaterial3: true, ), home: const Scaffold( body: Center( child: Text( 'This browser is not supported. Please use a Chrome based browser.', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), ), ), ); } } final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { return const Material( child: Center( child: Text( 'Welcome to the Isar Inspector!\nPlease open the link ' 'displayed when running the debug version of an Isar app.', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), ), ); }, ), GoRoute( path: '/:port/:secret', builder: (BuildContext context, GoRouterState state) { return GestureDetector( onTap: () { FocusScope.of(context).requestFocus(FocusNode()); }, child: Scaffold( body: Material( child: ConnectionScreen( port: state.params['port']!, secret: state.params['secret']!, ), ), ), ); }, ), ], ); class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Isar Inspector', routeInformationProvider: _router.routeInformationProvider, routeInformationParser: _router.routeInformationParser, routerDelegate: _router.routerDelegate, theme: ThemeData.from( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF9FC9FF), brightness: DarkMode.of(context).darkMode ? Brightness.dark : Brightness.light, ), useMaterial3: true, ), ); } } class DarkMode extends InheritedNotifier { const DarkMode({ super.key, super.notifier, required super.child, }); static DarkModeNotifier of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType()!.notifier!; } } class DarkModeNotifier extends ChangeNotifier { var _darkMode = true; bool get darkMode => _darkMode; void toggle() { _darkMode = !_darkMode; notifyListeners(); } } ================================================ FILE: packages/isar_inspector/lib/object/isar_object.dart ================================================ class IsarObject { const IsarObject( this.data, ); final Map data; dynamic getValue(String propertyName) => data[propertyName]; IsarObject? getNested(String propertyName, {String? linkCollection}) { final data = this.data[propertyName] as Map?; if (data != null) { return IsarObject(data); } else { return null; } } List? getNestedList( String propertyName, { String? linkCollection, }) { final list = data[propertyName] as List?; if (list == null) { return null; } final objects = []; for (var i = 0; i < list.length; i++) { final data = list[i] as Map; objects.add(IsarObject(data)); } return objects; } } ================================================ FILE: packages/isar_inspector/lib/object/object_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/object/property_embedded_view.dart'; import 'package:isar_inspector/object/property_link_view.dart'; import 'package:isar_inspector/object/property_view.dart'; class ObjectView extends StatelessWidget { const ObjectView({ super.key, this.root = false, required this.schemaName, required this.schemas, required this.object, required this.onUpdate, }); final bool root; final String schemaName; final Map> schemas; final IsarObject object; final void Function( String collection, int? id, String path, dynamic value, ) onUpdate; @override Widget build(BuildContext context) { final schema = schemas[schemaName]!; final id = schema is CollectionSchema ? object.getValue(schema.idName) as int : null; return Column( children: [ if (schema is CollectionSchema) PropertyView( property: PropertySchema( id: -1, name: schema.idName, type: IsarType.long, ), value: id, isId: true, isIndexed: false, onUpdate: (_) => throw UnimplementedError(), ), for (final property in schema.properties.values) if (property.target == null) PropertyView( property: property, value: object.getValue(property.name), isId: false, isIndexed: schema is CollectionSchema && schema.indexes.values.any( (index) => index.properties.any( (p) => p.name == property.name, ), ), onUpdate: (value) { onUpdate(schemaName, id, property.name, value); }, ) else EmbeddedPropertyView( property: property, schemas: schemas, object: object, onUpdate: (_, path, value) { onUpdate(schemaName, id, '${property.name}.$path', value); }, ), if (root && schema is CollectionSchema) for (final link in schema.links.values) LinkPropertyView( link: link, schemas: schemas, object: object, onUpdate: (id, path, value) { onUpdate(link.target, id, path, value); }, ), ], ); } } ================================================ FILE: packages/isar_inspector/lib/object/property_builder.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class PropertyBuilder extends StatefulWidget { const PropertyBuilder({ super.key, required this.property, this.underline = false, this.value, required this.type, this.children = const [], }); final String property; final bool underline; final Widget? value; final String type; final List children; @override State createState() => _PropertyBuilderState(); } class _PropertyBuilderState extends State { var _expanded = false; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ InkWell( onTap: widget.children.isNotEmpty ? () => setState(() => _expanded = !_expanded) : null, customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), child: Row( children: [ if (widget.children.isNotEmpty) ...[ AnimatedRotation( turns: _expanded ? 0.25 : 0, duration: const Duration(milliseconds: 200), child: Icon( Icons.arrow_right, size: 24, color: theme.colorScheme.onPrimaryContainer, ), ), const SizedBox(width: 4), ] else const SizedBox(width: 28), Tooltip( message: widget.type, child: Text( '${widget.property}:', style: GoogleFonts.jetBrainsMono( fontWeight: FontWeight.w500, color: theme.colorScheme.onPrimaryContainer, decoration: widget.underline ? TextDecoration.underline : null, ), ), ), const SizedBox(width: 8), if (widget.value != null) Expanded(child: widget.value!) else Text( widget.type, style: TextStyle( color: theme.colorScheme.onPrimaryContainer.withOpacity(0.5), ), ), ], ), ), ), if (_expanded && widget.children.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 20), child: AnimatedContainer( duration: const Duration(milliseconds: 200), child: Column(children: widget.children), ), ), ], ); } } ================================================ FILE: packages/isar_inspector/lib/object/property_embedded_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/object/object_view.dart'; import 'package:isar_inspector/object/property_builder.dart'; import 'package:isar_inspector/object/property_value.dart'; class EmbeddedPropertyView extends StatelessWidget { const EmbeddedPropertyView({ super.key, required this.property, required this.schemas, required this.object, required this.onUpdate, }); final PropertySchema property; final Map> schemas; final IsarObject object; final void Function(int? id, String path, dynamic value) onUpdate; @override Widget build(BuildContext context) { if (property.type == IsarType.object) { final child = object.getNested(property.name); return PropertyBuilder( property: property.name, type: property.target!, value: child == null ? const NullValue() : null, children: [ if (child != null) ObjectView( schemaName: property.target!, schemas: schemas, object: child, onUpdate: (_, id, path, value) { onUpdate(id, path, value); }, ), ], ); } else { final children = object.getNestedList(property.name); final childrenLength = children != null ? '(${children.length})' : ''; return PropertyBuilder( property: property.name, type: 'List<${property.target}> $childrenLength', value: children == null ? const NullValue() : null, children: [ for (var i = 0; i < (children?.length ?? 0); i++) PropertyBuilder( property: '$i', type: property.target!, value: children![i] == null ? const NullValue() : null, children: [ if (children[i] != null) ObjectView( schemaName: property.target!, schemas: schemas, object: children[i]!, onUpdate: (_, id, path, value) { onUpdate(id, '$i.$path', value); }, ), ], ), ], ); } } } ================================================ FILE: packages/isar_inspector/lib/object/property_link_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/object/object_view.dart'; import 'package:isar_inspector/object/property_builder.dart'; import 'package:isar_inspector/object/property_value.dart'; class LinkPropertyView extends StatelessWidget { const LinkPropertyView({ super.key, required this.link, required this.schemas, required this.object, required this.onUpdate, }); final LinkSchema link; final Map> schemas; final IsarObject object; final void Function(int id, String path, dynamic value) onUpdate; @override Widget build(BuildContext context) { if (link.single) { final child = object.getNested( link.name, linkCollection: link.target, ); return PropertyBuilder( property: link.name, type: 'IsarLink<${link.target}>', value: child == null ? const NullValue() : null, children: [ if (child != null) ObjectView( schemaName: link.target, schemas: schemas, object: child, onUpdate: (_, id, path, value) { onUpdate(id!, path, value); }, ), ], ); } else { final children = object.getNestedList( link.name, linkCollection: link.target, ); final childrenLength = children != null ? '(${children.length})' : ''; return PropertyBuilder( property: link.name, type: 'IsarLinks<${link.target}> $childrenLength', value: children == null ? const NullValue() : null, children: [ for (var i = 0; i < (children?.length ?? 0); i++) PropertyBuilder( property: '$i', type: link.target, value: children![i] == null ? const NullValue() : null, children: [ if (children[i] != null) ObjectView( schemaName: link.target, schemas: schemas, object: children[i]!, onUpdate: (_, id, path, value) { onUpdate(id!, path, value); }, ), ], ), ], ); } } } ================================================ FILE: packages/isar_inspector/lib/object/property_value.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:isar/isar.dart'; class PropertyValue extends StatelessWidget { const PropertyValue( this.value, { super.key, required this.enumMap, required this.type, this.onUpdate, }); final dynamic value; final IsarType type; final Map? enumMap; final void Function(dynamic newValue)? onUpdate; @override Widget build(BuildContext context) { final value = this.value; if (enumMap != null) { final enumName = enumMap!.entries.firstWhere( (e) => e.value == value, orElse: () { if (type == IsarType.byte || type == IsarType.byteList) { return enumMap!.entries.first; } else { return const MapEntry('null', null); } }, ).key; return GestureDetector( onTapDown: onUpdate == null ? null : (TapDownDetails details) async { final newValue = await showMenu( context: context, position: RelativeRect.fromLTRB( details.globalPosition.dx, details.globalPosition.dy, 100000, 0, ), items: [ if (type != IsarType.byte && type != IsarType.byteList) const PopupMenuItem( child: Text('null'), ), for (final enumName in enumMap!.keys) PopupMenuItem( value: enumMap![enumName], child: Text(enumName), ), ], ); onUpdate?.call(newValue); }, child: Text( enumName, style: GoogleFonts.jetBrainsMono( color: enumName != 'null' ? Colors.yellow : Colors.grey, fontWeight: FontWeight.bold, ), ), ); } switch (type) { case IsarType.bool: case IsarType.boolList: return GestureDetector( onTapDown: onUpdate == null ? null : (TapDownDetails details) async { final newValue = await showMenu( context: context, position: RelativeRect.fromLTRB( details.globalPosition.dx, details.globalPosition.dy, 100000, 0, ), items: const [ PopupMenuItem( child: Text('null'), ), PopupMenuItem( value: true, child: Text('true'), ), PopupMenuItem( value: false, child: Text('false'), ), ], ); onUpdate?.call(newValue); }, child: Text( '$value', style: GoogleFonts.jetBrainsMono( color: value != null ? Colors.orange : Colors.grey, fontWeight: FontWeight.bold, ), ), ); case IsarType.byte: case IsarType.byteList: case IsarType.int: case IsarType.intList: case IsarType.float: case IsarType.floatList: case IsarType.long: case IsarType.longList: case IsarType.double: case IsarType.doubleList: final numController = TextEditingController( text: value == null ? null : value == null ? null : '$value', ); final numFocus = FocusNode(); numFocus.addListener(() { if (!numFocus.hasPrimaryFocus) { final value = numController.text; num? numOrNull; if (type == IsarType.float || type == IsarType.floatList || type == IsarType.double || type == IsarType.doubleList) { numOrNull = double.tryParse(value); } else { numOrNull = int.tryParse(value); } onUpdate?.call(numOrNull); } }); return TextField( controller: numController, focusNode: numFocus, enabled: onUpdate != null, decoration: InputDecoration.collapsed( hintText: 'null', hintStyle: GoogleFonts.jetBrainsMono( color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 14, ), ), style: GoogleFonts.jetBrainsMono( color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 14, ), ); case IsarType.dateTime: case IsarType.dateTimeList: final date = value != null ? DateTime.fromMicrosecondsSinceEpoch(value as int) : null; return GestureDetector( onTap: onUpdate == null ? null : () async { final newDate = await showDatePicker( context: context, initialDate: date ?? DateTime.now(), firstDate: DateTime(1970), lastDate: DateTime(2050), ); onUpdate?.call(newDate?.microsecondsSinceEpoch); }, child: Text( date?.toIso8601String() ?? 'null', style: GoogleFonts.jetBrainsMono( color: date != null ? Colors.blue : Colors.grey, fontWeight: FontWeight.bold, ), ), ); case IsarType.string: case IsarType.stringList: final strController = TextEditingController( text: value == null ? null : '"${value.toString().replaceAll('\n', '⤵')}"', ); final strFocus = FocusNode(); strFocus.addListener(() { if (!strFocus.hasPrimaryFocus) { final value = strController.text; String? strOrNull; if (value.startsWith('"') && value.endsWith('"')) { strOrNull = value.substring(1, value.length - 1).replaceAll('⤵', '\n'); } onUpdate?.call(strOrNull); } }); return TextField( controller: strController, focusNode: strFocus, enabled: onUpdate != null, decoration: InputDecoration.collapsed( hintText: 'null', hintStyle: GoogleFonts.jetBrainsMono( color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 14, ), ), style: GoogleFonts.jetBrainsMono( color: Colors.green, fontWeight: FontWeight.bold, fontSize: 14, ), ); case IsarType.object: case IsarType.objectList: throw ArgumentError('Invalid type'); } } } class NullValue extends StatelessWidget { const NullValue({super.key}); @override Widget build(BuildContext context) { return Text( 'null', style: GoogleFonts.jetBrainsMono( color: Colors.grey, fontWeight: FontWeight.bold, ), ); } } ================================================ FILE: packages/isar_inspector/lib/object/property_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/property_builder.dart'; import 'package:isar_inspector/object/property_value.dart'; class PropertyView extends StatelessWidget { const PropertyView({ super.key, required this.property, required this.value, required this.isId, required this.isIndexed, required this.onUpdate, }); final PropertySchema property; final dynamic value; final bool isId; final bool isIndexed; final void Function(dynamic value) onUpdate; @override Widget build(BuildContext context) { final value = this.value; final valueLength = // ignore: avoid_dynamic_calls value is String || value is List ? '(${value.length})' : ''; return PropertyBuilder( property: property.name, underline: isIndexed, type: isId ? 'Id' : '${property.type.typeName} $valueLength', value: value is List ? null : property.type.isList ? const NullValue() : PropertyValue( value, type: property.type, enumMap: property.enumMap, onUpdate: isId ? null : onUpdate, ), children: [ if (value is List) for (var i = 0; i < value.length; i++) PropertyBuilder( property: '$i', type: property.type.typeName, value: PropertyValue( value[i], type: property.type, enumMap: property.enumMap, onUpdate: onUpdate, ), ), ], ); } } extension TypeName on IsarType { String get typeName { switch (this) { case IsarType.bool: return 'bool'; case IsarType.byte: return 'byte'; case IsarType.int: return 'short'; case IsarType.long: return 'int'; case IsarType.float: return 'float'; case IsarType.double: return 'double'; case IsarType.dateTime: return 'DateTime'; case IsarType.string: return 'String'; case IsarType.object: return 'Object'; case IsarType.boolList: return 'List'; case IsarType.byteList: return 'List'; case IsarType.intList: return 'List'; case IsarType.longList: return 'List'; case IsarType.floatList: return 'List'; case IsarType.doubleList: return 'List'; case IsarType.dateTimeList: return 'List'; case IsarType.stringList: return 'List'; case IsarType.objectList: return 'List'; } } } ================================================ FILE: packages/isar_inspector/lib/query_builder/query_filter.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/object/property_value.dart'; import 'package:isar_inspector/util.dart'; class QueryFilter extends StatelessWidget { const QueryFilter({ super.key, required this.collection, required this.condition, required this.onChanged, }); final CollectionSchema collection; final FilterCondition condition; final void Function(FilterCondition filter) onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); final property = collection.propertyOrId(condition.property); return Container( height: 60, decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)), borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(15), child: Row( children: [ DropdownButtonHideUnderline( child: DropdownButton( isDense: true, items: [ for (final property in collection.idAndProperties) if (property.type != IsarType.object && property.type != IsarType.objectList) DropdownMenuItem( value: property.name, child: Text(property.name), ), ], value: condition.property, onChanged: (value) { if (value == null) return; final newProperty = collection.propertyOrId(value); onChanged( FilterCondition( type: FilterConditionType.equalTo, property: value, value1: newProperty.defaultEditingValue, value2: newProperty.defaultEditingValue, include1: false, include2: false, caseSensitive: false, ), ); }, ), ), const SizedBox(width: 20), DropdownButtonHideUnderline( child: DropdownButton( isDense: true, items: [ for (final type in property.supportedFilters) DropdownMenuItem( value: type, child: Text(type.niceName), ), ], value: condition.type, onChanged: (value) { if (value == null) return; onChanged( FilterCondition( type: value, property: condition.property, value1: condition.value1, include1: value == FilterConditionType.between, value2: condition.value2, include2: value == FilterConditionType.between, caseSensitive: false, ), ); }, ), ), if (condition.type.valueCount > 0) ...[ const SizedBox(width: 20), SizedBox( width: 100, child: PropertyValue( condition.value1, type: property.type, enumMap: property.enumMap, onUpdate: (newValue) { onChanged( FilterCondition( type: condition.type, property: condition.property, value1: newValue, include1: condition.include1, value2: condition.value2, include2: condition.include2, caseSensitive: false, ), ); }, ), ), ], if (condition.type.valueCount == 2) ...[ const SizedBox(width: 20), SizedBox( width: 100, child: PropertyValue( condition.value2, type: property.type, enumMap: property.enumMap, onUpdate: (newValue) { onChanged( FilterCondition( type: condition.type, property: condition.property, value1: condition.value1, include1: condition.include1, value2: newValue, include2: condition.include2, caseSensitive: false, ), ); }, ), ), ], ], ), ), ); } dynamic get value1 {} } extension on PropertySchema { List get supportedFilters { switch (type) { case IsarType.bool: case IsarType.boolList: return [ FilterConditionType.equalTo, FilterConditionType.isNull, FilterConditionType.isNotNull, if (type == IsarType.boolList) ...[ FilterConditionType.elementIsNull, FilterConditionType.elementIsNotNull, FilterConditionType.listLength, ], ]; case IsarType.byte: case IsarType.byteList: return [ FilterConditionType.equalTo, FilterConditionType.greaterThan, FilterConditionType.lessThan, FilterConditionType.between, if (type == IsarType.byteList) FilterConditionType.listLength, ]; case IsarType.int: case IsarType.float: case IsarType.long: case IsarType.double: case IsarType.dateTime: case IsarType.intList: case IsarType.floatList: case IsarType.longList: case IsarType.doubleList: case IsarType.dateTimeList: return [ FilterConditionType.equalTo, FilterConditionType.greaterThan, FilterConditionType.lessThan, FilterConditionType.between, FilterConditionType.isNull, FilterConditionType.isNotNull, FilterConditionType.elementIsNull, FilterConditionType.elementIsNotNull, FilterConditionType.listLength, ]; case IsarType.string: case IsarType.stringList: return [ FilterConditionType.equalTo, FilterConditionType.greaterThan, FilterConditionType.lessThan, FilterConditionType.between, FilterConditionType.startsWith, FilterConditionType.endsWith, FilterConditionType.contains, FilterConditionType.matches, FilterConditionType.isNull, FilterConditionType.isNotNull, if (type == IsarType.stringList) ...[ FilterConditionType.elementIsNull, FilterConditionType.elementIsNotNull, FilterConditionType.listLength, ], ]; case IsarType.object: case IsarType.objectList: return []; } } dynamic get defaultEditingValue { if (enumMap != null) { return enumMap!.values.first; } switch (type) { case IsarType.bool: case IsarType.boolList: return false; case IsarType.byte: case IsarType.byteList: case IsarType.int: case IsarType.intList: case IsarType.long: case IsarType.longList: return 0; case IsarType.float: case IsarType.floatList: case IsarType.double: case IsarType.doubleList: return 0.0; case IsarType.dateTime: case IsarType.dateTimeList: return DateTime.now().microsecondsSinceEpoch; case IsarType.string: case IsarType.stringList: return ''; case IsarType.object: case IsarType.objectList: return null; } } } extension on FilterConditionType { String get niceName { switch (this) { case FilterConditionType.equalTo: return 'is equal to'; case FilterConditionType.greaterThan: return 'is greater than'; case FilterConditionType.lessThan: return 'is less than'; case FilterConditionType.between: return 'is between'; case FilterConditionType.startsWith: return 'starts with'; case FilterConditionType.endsWith: return 'ends with'; case FilterConditionType.contains: return 'contains'; case FilterConditionType.matches: return 'matches'; case FilterConditionType.isNull: return 'is null'; case FilterConditionType.isNotNull: return 'is not null'; case FilterConditionType.elementIsNull: return 'element is null'; case FilterConditionType.elementIsNotNull: return 'element is not null'; case FilterConditionType.listLength: return 'list length between'; } } int get valueCount { switch (this) { case FilterConditionType.isNull: case FilterConditionType.isNotNull: case FilterConditionType.elementIsNull: case FilterConditionType.elementIsNotNull: return 0; case FilterConditionType.equalTo: case FilterConditionType.greaterThan: case FilterConditionType.lessThan: case FilterConditionType.startsWith: case FilterConditionType.endsWith: case FilterConditionType.contains: case FilterConditionType.matches: return 1; case FilterConditionType.between: case FilterConditionType.listLength: return 2; } } } ================================================ FILE: packages/isar_inspector/lib/query_builder/query_group.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/query_builder/query_filter.dart'; class QueryGroup extends StatelessWidget { const QueryGroup({ super.key, required this.collection, required this.group, required this.level, required this.onChanged, this.onDelete, }); final CollectionSchema collection; final FilterGroup group; final int level; final void Function(FilterGroup group) onChanged; final VoidCallback? onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const NeverScrollableScrollPhysics(), child: Card( margin: EdgeInsets.zero, elevation: level.toDouble(), child: Padding( padding: const EdgeInsets.all(20), child: IntrinsicHeight( child: Row( mainAxisSize: MainAxisSize.min, children: [ _Guideline( group: group, onChanged: onChanged, onDelete: onDelete, ), const SizedBox(width: 15), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), if (group.filters.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Text( 'Add a filter or nested group to limit the results.\n' 'Click the group type to change it.', style: TextStyle( color: theme.colorScheme.onPrimaryContainer .withOpacity(0.5), ), ), ), for (final filter in group.filters) ...[ if (filter is FilterGroup) QueryGroup( collection: collection, group: filter, level: level + 1, onChanged: (updated) => _performUpdate(add: updated, remove: filter), onDelete: () => _performUpdate(remove: filter), ) else Row( children: [ QueryFilter( collection: collection, condition: filter as FilterCondition, onChanged: (updated) => _performUpdate( add: updated, remove: filter, ), ), const SizedBox(width: 5), IconButton( icon: const Icon(Icons.close_rounded, size: 20), onPressed: () => _performUpdate(remove: filter), ), ], ), const SizedBox(height: 12), ], GroupFilterButton( idName: collection.idName, group: group, level: level, onAdd: (newFilter) => _performUpdate(add: newFilter), ), const SizedBox(height: 10), ], ), ], ), ), ), ), ); } void _performUpdate({FilterOperation? add, FilterOperation? remove}) { final newFilters = group.filters.toList(); if (remove != null) { if (add != null) { newFilters[newFilters.indexOf(remove)] = add; } else { newFilters.remove(remove); } } else if (add != null) { newFilters.add(add); } onChanged( FilterGroup( type: group.type, filters: newFilters, ), ); } } class _Guideline extends StatelessWidget { const _Guideline({ required this.group, required this.onChanged, this.onDelete, }); final FilterGroup group; final void Function(FilterGroup condition) onChanged; final VoidCallback? onDelete; @override Widget build(BuildContext context) { final color = group.type.color; return Column( children: [ Expanded( child: Container( width: 17.5, margin: const EdgeInsets.only(left: 10), decoration: BoxDecoration( border: Border( top: BorderSide(color: color, width: 2.5), left: BorderSide(color: color, width: 2.5), ), ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: InputChip( backgroundColor: color, deleteIconColor: Colors.white, label: SizedBox( width: 30, child: Center( child: Text( group.type.name, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ), tooltip: 'Change group type', onDeleted: onDelete, onPressed: () { final newType = group.type == FilterGroupType.and ? FilterGroupType.or : group.type == FilterGroupType.or ? FilterGroupType.xor : FilterGroupType.and; onChanged( FilterGroup( type: newType, filters: group.filters, ), ); }, side: BorderSide.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), ), Expanded( child: Container( width: 17.5, margin: const EdgeInsets.only(left: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: color, width: 2.5), left: BorderSide(color: color, width: 2.5), ), ), ), ), ], ); } } class GroupFilterButton extends StatelessWidget { const GroupFilterButton({ super.key, required this.idName, required this.group, required this.level, required this.onAdd, }); final String idName; final FilterGroup group; final int level; final void Function(FilterOperation filter) onAdd; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( icon: const Icon(Icons.workspaces_rounded), label: const Text('Add Group'), style: ButtonStyle( elevation: MaterialStateProperty.all(level + 1), ), onPressed: () { onAdd( FilterGroup( type: FilterGroupType.and, filters: [], ), ); }, ), const SizedBox(width: 20), ElevatedButton.icon( icon: const Icon(Icons.filter_alt_rounded), label: const Text('Add Filter'), style: ButtonStyle( elevation: MaterialStateProperty.all(level + 1), ), onPressed: () { onAdd( FilterCondition.greaterThan( property: idName, value: 0, caseSensitive: false, ), ); }, ), ], ); } } extension on FilterGroupType { String get name => this == FilterGroupType.and ? 'AND' : this == FilterGroupType.or ? 'OR' : 'XOR'; Color get color => this == FilterGroupType.and ? Colors.blue : this == FilterGroupType.or ? Colors.orange : Colors.green; } ================================================ FILE: packages/isar_inspector/lib/sidebar.dart ================================================ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:isar_inspector/collections_list.dart'; import 'package:isar_inspector/connect_client.dart'; import 'package:isar_inspector/instance_selector.dart'; import 'package:isar_inspector/main.dart'; class Sidebar extends StatelessWidget { const Sidebar({ super.key, required this.instances, required this.selectedInstance, required this.onInstanceSelected, required this.collections, required this.collectionInfo, required this.selectedCollection, required this.onCollectionSelected, }); final List instances; final String selectedInstance; final void Function(String instance) onInstanceSelected; final List> collections; final Map collectionInfo; final String? selectedCollection; final void Function(String instance) onCollectionSelected; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 80, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Image.asset( 'assets/logo.png', width: 40, ), const SizedBox(width: 15), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Isar', style: theme.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), Text( 'Inspector', style: theme.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const Spacer(), IconButton( padding: const EdgeInsets.all(20), icon: Icon( theme.brightness == Brightness.light ? Icons.dark_mode_rounded : Icons.light_mode_rounded, ), onPressed: DarkMode.of(context).toggle, ) ], ), ), ), const SizedBox(height: 20), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: CollectionsList( collections: collections, collectionInfo: collectionInfo, selectedCollection: selectedCollection, onSelected: onCollectionSelected, ), ), ), const SizedBox(height: 12), InstanceSelector( instances: instances, selectedInstance: selectedInstance, onSelected: onInstanceSelected, ), ], ), ); } } ================================================ FILE: packages/isar_inspector/lib/util.dart ================================================ import 'package:isar/isar.dart'; extension CollectionSchemaX on CollectionSchema { PropertySchema propertyOrId(String name) { if (name == idName) { return PropertySchema(id: 0, name: name, type: IsarType.long); } else { return property(name); } } List get idAndProperties => [ PropertySchema(id: 0, name: idName, type: IsarType.long), ...properties.values, ]; } ================================================ FILE: packages/isar_inspector/pubspec.yaml ================================================ name: isar_inspector publish_to: "none" version: 1.0.0+1 environment: sdk: ">=2.17.0 <3.0.0" dependencies: clickup_fading_scroll: git: url: https://github.com/clickup/clickup_fading_scroll flutter: sdk: flutter flutter_svg: any font_awesome_flutter: any go_router: ^5.0.0 google_fonts: 6.1.0 isar: path: ../isar vm_service: ^11.0.0 web_socket_channel: ^2.2.0 dev_dependencies: build_runner: any flutter_lints: ^2.0.1 very_good_analysis: ^3.0.1 flutter: uses-material-design: true assets: - assets/logo.png ================================================ FILE: packages/isar_inspector/web/index.html ================================================ Isar Inspector
Isar Logo
================================================ FILE: packages/isar_inspector/web/manifest.json ================================================ { "name": "Isar Inspector", "short_name": "Isar Inspector", "start_url": ".", "display": "standalone", "background_color": "#1a1c1e", "theme_color": "#0175C2", "description": "Inspector for the Isar database.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "https://isar.dev/icon-256x256.png", "sizes": "256x256", "type": "image/png" }, { "src": "https://isar.dev/icon-512x512.png", "sizes": "512x512", "type": "image/png" } ] } ================================================ FILE: packages/isar_test/.gitignore ================================================ *.g.dart *.freezed.dart all_tests.dart filter_long_test.dart filter_long_list_test.dart filter_double_test.dart filter_double_list_test.dart where_long_test.dart where_long_list_test.dart where_double_test.dart where_double_list_test.dart ================================================ FILE: packages/isar_test/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b channel: stable project_type: app ================================================ FILE: packages/isar_test/README.md ================================================ ## Isar tests Use the following commands to run the tests on a connected device: ### Unit tests ``` sh tool/prepare_tests.sh sh tool/build.sh dart test ``` ### Integration tests ``` sh tool/prepare_tests.sh sh tool/build.sh flutter test integration_test/integration_test.dart ``` ================================================ FILE: packages/isar_test/analysis_options.yaml ================================================ include: package:very_good_analysis/analysis_options.yaml analyzer: errors: cascade_invocations: ignore avoid_positional_boolean_parameters: ignore parameter_assignments: ignore public_member_api_docs: ignore use_string_buffers: ignore avoid_equals_and_hash_code_on_mutable_classes: ignore ================================================ FILE: packages/isar_test/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: packages/isar_test/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { applicationId "dev.isar.isar_test" minSdkVersion 16 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } ================================================ FILE: packages/isar_test/android/app/src/androidTest/java/dev/isar/isar_test/MainActivityTest.java ================================================ package dev.isar.isar_test; import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import org.junit.Rule; import org.junit.runner.RunWith; @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); } ================================================ FILE: packages/isar_test/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: packages/isar_test/android/app/src/main/kotlin/dev/isar/isar_test/MainActivity.kt ================================================ package dev.isar.isar_test import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: packages/isar_test/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: packages/isar_test/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: packages/isar_test/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.7.21' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: packages/isar_test/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Nov 09 23:26:39 CET 2022 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: packages/isar_test/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: packages/isar_test/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: packages/isar_test/android/settings_aar.gradle ================================================ include ':app' ================================================ FILE: packages/isar_test/integration_test/integration_test.dart ================================================ // ignore_for_file: avoid_print, depend_on_referenced_packages import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:isar_test/isar_test.dart'; import 'package:path_provider/path_provider.dart'; import 'all_tests.dart' as tests; void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final completer = Completer(); group('Integration test', () { setUpAll(() async { if (!kIsWeb) { final dir = await getTemporaryDirectory(); testTempPath = dir.path; } }); tearDownAll(() { print('Isar test done'); completer.complete(); }); tests.main(); }); testWidgets( 'Isar', (t) async { await completer.future; expect(testCount > 0, true); expect(testErrors, isEmpty); }, timeout: Timeout.none, ); } ================================================ FILE: packages/isar_test/ios/.gitignore ================================================ *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: packages/isar_test/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 9.0 ================================================ FILE: packages/isar_test/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: packages/isar_test/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: packages/isar_test/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: packages/isar_test/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: packages/isar_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images": [ { "size": "76x76", "idiom": "universal", "filename": "Icon-App-76x76@1x.png", "scale": "1x" } ], "info": { "version": 1, "author": "xcode" } } ================================================ FILE: packages/isar_test/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images": [ { "idiom": "universal", "filename": "LaunchImage.png", "scale": "1x" } ], "info": { "version": 1, "author": "xcode" } } ================================================ FILE: packages/isar_test/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: packages/isar_test/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: packages/isar_test/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName isar_test CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone ================================================ FILE: packages/isar_test/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: packages/isar_test/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; E699F95AE0837093B173D5D2 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 293477DA8B12E689D9C8A52F /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0E0B516E592199B3B7770EDA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 293477DA8B12E689D9C8A52F /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E43DAE9F3D1A82DFDBB1023C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; E952EC9ED57F026DE326B330 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( E699F95AE0837093B173D5D2 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 3B4FEC22A81DACCF5DBE5D28 /* Pods */ = { isa = PBXGroup; children = ( 0E0B516E592199B3B7770EDA /* Pods-Runner.debug.xcconfig */, E952EC9ED57F026DE326B330 /* Pods-Runner.release.xcconfig */, E43DAE9F3D1A82DFDBB1023C /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 3B4FEC22A81DACCF5DBE5D28 /* Pods */, C4CE71333639FFD1671CC258 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; C4CE71333639FFD1671CC258 /* Frameworks */ = { isa = PBXGroup; children = ( 293477DA8B12E689D9C8A52F /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 2A343220D399D020D21882A3 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 2A343220D399D020D21882A3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3B533462NK; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dev.isar.integrationTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3B533462NK; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dev.isar.integrationTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 3B533462NK; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = dev.isar.integrationTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: packages/isar_test/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: packages/isar_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: packages/isar_test/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: packages/isar_test/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: packages/isar_test/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: packages/isar_test/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: packages/isar_test/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: packages/isar_test/lib/isar_test.dart ================================================ export 'src/common.dart'; export 'src/listener.dart'; export 'src/matchers.dart'; export 'src/sync_async_helper.dart'; export 'src/sync_future.dart'; export 'src/twitter/entities.dart'; export 'src/twitter/geo.dart'; export 'src/twitter/media.dart'; export 'src/twitter/tweet.dart'; export 'src/twitter/user.dart'; export 'src/twitter/util.dart'; ================================================ FILE: packages/isar_test/lib/src/common.dart ================================================ // ignore_for_file: implementation_imports import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:isar/isar.dart'; import 'package:isar_test/src/init_native.dart' if (dart.library.html) 'package:isar_test/src/init_web.dart'; import 'package:isar_test/src/sync_async_helper.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'package:test_api/src/backend/invoker.dart'; const kIsWeb = identical(0, 0.0); final testErrors = []; int testCount = 0; var _setUp = false; Future _prepareTest() async { if (!_setUp) { await init(); _setUp = true; } } @isTest void isarTest( String name, dynamic Function() body, { Timeout? timeout, bool skip = false, }) { isarTestSync(name, body, timeout: timeout, skip: skip); isarTestAsync(name, body, timeout: timeout, skip: skip); } @isTest void isarTestSync( String name, dynamic Function() body, { Timeout? timeout, bool skip = false, }) { if (!kIsWeb) { _isarTest(name, true, body, timeout: timeout, skip: skip); } } @isTest void isarTestAsync( String name, dynamic Function() body, { Timeout? timeout, bool skip = false, }) { _isarTest(name, false, body, timeout: timeout, skip: skip); } void _isarTest( String name, bool syncTest, dynamic Function() body, { Timeout? timeout, bool skip = false, }) { final testName = syncTest ? '$name SYNC' : name; test( testName, () async { await runZoned( () async { try { await _prepareTest(); await body(); testCount++; } catch (e) { testErrors.add('$testName: $e'); rethrow; } }, zoneValues: { #syncTest: syncTest, }, ); }, timeout: timeout ?? const Timeout(Duration(minutes: 10)), skip: skip, ); } @isTest void isarTestVm(String name, dynamic Function() body) { isarTest(name, body, skip: kIsWeb); } @isTest void isarTestWeb(String name, dynamic Function() body) { isarTest(name, body, skip: !kIsWeb); } String getRandomName() { final random = Random().nextInt(pow(2, 32) as int).toString(); return '${random}_tmp'; } String? testTempPath; Future openTempIsar( List> schemas, { String? name, String? directory, int maxSizeMiB = Isar.defaultMaxSizeMiB, CompactCondition? compactOnLaunch, bool closeAutomatically = true, }) async { await _prepareTest(); if (!kIsWeb && directory == null && testTempPath == null) { final dartToolDir = path.join(Directory.current.path, '.dart_tool'); testTempPath = path.join(dartToolDir, 'test', 'tmp'); await Directory(testTempPath!).create(recursive: true); } final isar = await tOpen( schemas: schemas, name: name ?? getRandomName(), maxSizeMiB: maxSizeMiB, directory: testTempPath ?? '', compactOnLaunch: compactOnLaunch, ); if (Invoker.current != null && closeAutomatically) { addTearDown(() async { if (isar.isOpen) { await isar.close(deleteFromDisk: true); } }); } // ignore: invalid_use_of_visible_for_testing_member if (!kIsWeb) await isar.verify(); return isar; } ================================================ FILE: packages/isar_test/lib/src/init_native.dart ================================================ import 'dart:ffi'; import 'dart:io'; import 'package:isar/isar.dart'; import 'package:path/path.dart' as path; Future init() async { if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { final rootDir = path.dirname(path.dirname(Directory.current.path)); final binaryName = Platform.isWindows ? 'isar.dll' : Platform.isMacOS ? 'libisar.dylib' : 'libisar.so'; try { await Isar.initializeIsarCore(); } catch (e) { await Isar.initializeIsarCore( libraries: { Abi.macosArm64: path.join( rootDir, 'target', 'aarch64-apple-darwin', 'release', binaryName, ), Abi.macosX64: path.join( rootDir, 'target', 'x86_64-apple-darwin', 'release', binaryName, ), Abi.linuxArm64: path.join( rootDir, 'target', 'aarch64-unknown-linux-gnu', 'release', binaryName, ), Abi.linuxX64: path.join( rootDir, 'target', 'x86_64-unknown-linux-gnu', 'release', binaryName, ), Abi.windowsX64: path.join( rootDir, 'target', 'x86_64-pc-windows-msvc', 'release', binaryName, ), }, ); } } } ================================================ FILE: packages/isar_test/lib/src/init_web.dart ================================================ // ignore_for_file: avoid_web_libraries_in_flutter, implementation_imports import 'dart:js' as js; import 'package:isar/src/web/open.dart' as isar_web; import 'package:isar_test/src/isar_web_src.dart'; Future init() async { js.context.callMethod('eval', [isarWebSrc]); // ignore: invalid_use_of_visible_for_testing_member isar_web.doNotInitializeIsarWeb(); } ================================================ FILE: packages/isar_test/lib/src/isar_web_src.dart ================================================ const isarWebSrc = ''; ================================================ FILE: packages/isar_test/lib/src/listener.dart ================================================ import 'dart:async'; import 'package:test/test.dart'; class Listener { Listener(Stream stream) { subscription = stream.listen((event) { if (_completer != null) { _completer!.complete(event); _completer = null; } else { _unprocessed.add(event); } }); } late StreamSubscription subscription; final List _unprocessed = []; Completer? _completer; Future get next { if (_unprocessed.isEmpty) { expect(_completer, null); _completer = Completer(); return _completer!.future; } else { return Future.value(_unprocessed.removeAt(0)); } } Future done() async { await Future.delayed(const Duration(milliseconds: 100)); await subscription.cancel(); expect(_completer, null); expect(_unprocessed, []); } } ================================================ FILE: packages/isar_test/lib/src/matchers.dart ================================================ import 'package:isar/isar.dart'; import 'package:isar_test/src/sync_async_helper.dart'; import 'package:test/test.dart'; Future qEqualSet( QueryBuilder query, Iterable target, ) async { final results = (await query.tFindAll()).toList(); expect(results.toSet(), target.toSet()); } Future qEqual( QueryBuilder query, List target, ) async { final results = (await query.tFindAll()).toList(); await qEqualSync(results, target); } Future qEqualSync(List actual, List target) async { if (actual is List) { for (var i = 0; i < actual.length; i++) { expect(doubleListEquals(actual.cast(), target.cast()), true); } } else if (actual is List?>) { for (var i = 0; i < actual.length; i++) { doubleListEquals( actual[i] as List?, target[i] as List?, ); } } else { expect(actual, target); } } bool doubleListEquals(List? l1, List? l2) { if (l1?.length != l2?.length) { return false; } if (l1 != null && l2 != null) { for (var i = 0; i < l1.length; i++) { if (!doubleEquals(l1[i], l2[i])) { return false; } } } return true; } bool doubleEquals(double? d1, double? d2) { return d1 == d2 || (d1 != null && d2 != null && ((d1.isNaN && d2.isNaN) || (d1 - d2).abs() < 0.001)); } Matcher isIsarError([String? contains]) { return allOf( isA(), predicate( (IsarError e) => contains == null || e.toString().toLowerCase().contains(contains.toLowerCase()), ), ); } Matcher throwsIsarError([String? contains]) { return throwsA(isIsarError(contains)); } Matcher get throwsAssertionError { var matcher = anything; assert( () { matcher = throwsA(isA()); return true; }(), 'only in debug mode', ); return matcher; } bool listEquals(List? a, List? b) { if (a == null) { return b == null; } if (b == null || a.length != b.length) { return false; } if (identical(a, b)) { return true; } for (var index = 0; index < a.length; index += 1) { if (a[index] != b[index]) { return false; } } return true; } bool dateTimeListEquals(List? a, List? b) { assert( (a == null || a.every((e) => e == null || e is DateTime)) && (b == null || b.every((e) => e == null || e is DateTime)), 'Parameters must be lists of `DateTime` or `DateTime?`', ); return listEquals( a?.cast().map((e) => e?.toUtc()).toList(), b?.cast().map((e) => e?.toUtc()).toList(), ); } ================================================ FILE: packages/isar_test/lib/src/sync_async_helper.dart ================================================ import 'dart:async'; import 'dart:typed_data'; import 'package:isar/isar.dart'; import 'package:isar_test/src/sync_future.dart'; bool get syncTest => Zone.current[#syncTest] as bool? ?? false; Future tOpen({ required List> schemas, required String directory, String name = Isar.defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, CompactCondition? compactOnLaunch, }) { if (syncTest) { final isar = Isar.openSync( schemas, directory: directory, name: name, maxSizeMiB: maxSizeMiB, relaxedDurability: relaxedDurability, compactOnLaunch: compactOnLaunch, inspector: false, ); return SynchronousFuture(isar); } else { return Isar.open( schemas, directory: directory, name: name, maxSizeMiB: maxSizeMiB, relaxedDurability: relaxedDurability, compactOnLaunch: compactOnLaunch, inspector: false, ); } } extension TIsar on Isar { Future tTxn(Future Function() callback) { if (syncTest) { return Future.value(txnSync(callback)); } else { return txn(callback); } } Future tWriteTxn(Future Function() callback, {bool silent = false}) { if (syncTest) { return writeTxnSync(callback, silent: silent); } else { return writeTxn(callback, silent: silent); } } Future tClear() { if (syncTest) { clearSync(); return SynchronousFuture(null); } else { return clear(); } } } extension TIsarCollection on IsarCollection { Future tGet(Id id) { if (syncTest) { return SynchronousFuture(getSync(id)); } else { return get(id); } } Future> tGetAll(List ids) { if (syncTest) { return SynchronousFuture(getAllSync(ids)); } else { return getAll(ids); } } Future tPut(OBJ object, {bool saveLinks = false}) { if (syncTest) { return SynchronousFuture(putSync(object, saveLinks: saveLinks)); } else { return put(object); } } Future> tPutAll(List objects, {bool saveLinks = false}) { if (syncTest) { return SynchronousFuture(putAllSync(objects, saveLinks: saveLinks)); } else { return putAll(objects); } } Future tDelete(Id id) { if (syncTest) { return SynchronousFuture(deleteSync(id)); } else { return delete(id); } } Future tDeleteAll(List ids) { if (syncTest) { return SynchronousFuture(deleteAllSync(ids)); } else { return deleteAll(ids); } } Future tClear() { if (syncTest) { clearSync(); return SynchronousFuture(null); } else { return clear(); } } Future tImportJsonRaw(Uint8List jsonBytes) { if (syncTest) { importJsonRawSync(jsonBytes); return SynchronousFuture(null); } else { return importJsonRaw(jsonBytes); } } Future tImportJson(List> json) { if (syncTest) { importJsonSync(json); return SynchronousFuture(null); } else { return importJson(json); } } Future tGetSize({ bool includeIndexes = false, bool includeLinks = false, }) { if (syncTest) { return SynchronousFuture( getSizeSync( includeIndexes: includeIndexes, includeLinks: includeLinks, ), ); } else { return getSize( includeIndexes: includeIndexes, includeLinks: includeLinks, ); } } } extension QueryBuilderExecute on QueryBuilder { Future tFindFirst() { if (syncTest) { return SynchronousFuture(findFirstSync()); } else { return findFirst(); } } Future> tFindAll() { if (syncTest) { return SynchronousFuture(findAllSync()); } else { return findAll(); } } Future tCount() { if (syncTest) { return SynchronousFuture(countSync()); } else { return count(); } } Future tIsEmpty() { if (syncTest) { return SynchronousFuture(isEmptySync()); } else { return isEmpty(); } } Future tIsNotEmpty() { if (syncTest) { return SynchronousFuture(isNotEmptySync()); } else { return isNotEmpty(); } } Future tDeleteFirst() { if (syncTest) { return SynchronousFuture(deleteFirstSync()); } else { return deleteFirst(); } } Future tDeleteAll() { if (syncTest) { return SynchronousFuture(deleteAllSync()); } else { return deleteAll(); } } Future tExportJsonRaw(M Function(Uint8List) callback) { if (syncTest) { return SynchronousFuture(exportJsonRawSync(callback)); } else { return exportJsonRaw(callback); } } Future>> tExportJson() { if (syncTest) { return SynchronousFuture(exportJsonSync()); } else { return exportJson(); } } } /// Extension for QueryBuilders extension QueryExecuteAggregation on QueryBuilder { Future tMin() { if (syncTest) { return SynchronousFuture(minSync()); } else { return min(); } } Future tMax() { if (syncTest) { return SynchronousFuture(maxSync()); } else { return max(); } } Future tAverage() { if (syncTest) { return SynchronousFuture(averageSync()); } else { return average(); } } Future tSum() { if (syncTest) { return SynchronousFuture(sumSync()); } else { return sum(); } } } /// Extension for QueryBuilders extension QueryExecuteDateAggregation on QueryBuilder { Future tMin() { if (syncTest) { return SynchronousFuture(minSync()); } else { return min(); } } Future tMax() { if (syncTest) { return SynchronousFuture(maxSync()); } else { return max(); } } } extension TIsarLinkBase on IsarLinkBase { Future tLoad() { if (syncTest) { loadSync(); return SynchronousFuture(null); } else { return load(); } } Future tSave() { if (syncTest) { saveSync(); return SynchronousFuture(null); } else { return save(); } } Future tReset() { if (syncTest) { resetSync(); return SynchronousFuture(null); } else { return reset(); } } } extension TIsarLinks on IsarLinks { Future tLoad({bool overrideChanges = false}) { if (syncTest) { loadSync(overrideChanges: overrideChanges); return SynchronousFuture(null); } else { return load(overrideChanges: overrideChanges); } } Future tUpdate({ List link = const [], List unlink = const [], }) { if (syncTest) { updateSync(link: link, unlink: unlink); return SynchronousFuture(null); } else { return update(link: link, unlink: unlink); } } Future tCount() => filter().tCount(); } ================================================ FILE: packages/isar_test/lib/src/sync_future.dart ================================================ import 'dart:async'; class SynchronousFuture implements Future { SynchronousFuture(this._value); final T _value; @override Stream asStream() { final controller = StreamController(); controller.add(_value); controller.close(); return controller.stream; } @override Future catchError(Function onError, {bool Function(Object error)? test}) => Completer().future; @override Future then( FutureOr Function(T value) onValue, { Function? onError, }) { final dynamic result = onValue(_value); if (result is Future) { return result; } return SynchronousFuture(result as R); } @override Future timeout(Duration timeLimit, {FutureOr Function()? onTimeout}) { return Future.value(_value).timeout(timeLimit, onTimeout: onTimeout); } @override Future whenComplete(FutureOr Function() action) { try { final result = action(); if (result is Future) { return result.then((dynamic value) => _value); } return this; } catch (e, stack) { return Future.error(e, stack); } } } ================================================ FILE: packages/isar_test/lib/src/twitter/entities.dart ================================================ import 'package:isar/isar.dart'; import 'package:json_annotation/json_annotation.dart'; import 'media.dart'; import 'util.dart'; part 'entities.g.dart'; @JsonSerializable(fieldRename: FieldRename.snake, createToJson: false) @embedded class Entities { Entities(); factory Entities.fromJson(Map json) => _$EntitiesFromJson(json); List? hashtags; List? media; List? urls; List? userMentions; List? symbols; List? polls; } @JsonSerializable(fieldRename: FieldRename.snake, createToJson: false) @embedded class Hashtag { Hashtag(); factory Hashtag.fromJson(Map json) => _$HashtagFromJson(json); List? indices; String? text; } @JsonSerializable(fieldRename: FieldRename.snake, createToJson: false) @embedded class Poll { Poll(); factory Poll.fromJson(Map json) => _$PollFromJson(json); List