This commit is contained in:
4831c0 2025-03-19 19:40:26 +01:00
commit 30b2545f4c
65 changed files with 13788 additions and 0 deletions

431
CHANGELOG.md Normal file
View File

@ -0,0 +1,431 @@
## 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<String, String> libraries)` to `initializeLibraries(Map<IsarAbi, String> 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<double>` indexes can no longer be at the beginning of a composite index
- `List<double>` 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

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
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.

267
README.md Normal file
View File

@ -0,0 +1,267 @@
<p align="center">
<a href="https://isar.dev">
<img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/isar.svg?sanitize=true" height="128">
</a>
<h1 align="center">Isar Database</h1>
</p>
<p align="center">
<a href="https://pub.dev/packages/isar">
<img src="https://img.shields.io/pub/v/isar?label=pub.dev&labelColor=333940&logo=dart">
</a>
<a href="https://github.com/isar/isar/actions/workflows/test.yaml">
<img src="https://img.shields.io/github/actions/workflow/status/isar/isar/test.yaml?branch=main&label=tests&labelColor=333940&logo=github">
</a>
<a href="https://app.codecov.io/gh/isar/isar">
<img src="https://img.shields.io/codecov/c/github/isar/isar?logo=codecov&logoColor=fff&labelColor=333940">
</a>
<a href="https://t.me/isardb">
<img src="https://img.shields.io/static/v1?label=join&message=isardb&labelColor=333940&logo=telegram&logoColor=white&color=229ED9">
</a>
<a href="https://twitter.com/simonleier">
<img src="https://img.shields.io/twitter/follow/simcdev?style=social">
</a>
</p>
<p align="center">
<a href="https://isar.dev">Quickstart</a>
<a href="https://isar.dev/schema">Documentation</a>
<a href="https://github.com/isar/isar/tree/main/examples/">Sample Apps</a>
<a href="https://github.com/isar/isar/discussions">Support & Ideas</a>
<a href="https://pub.dev/packages/isar">Pub.dev</a>
</p>
> #### 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.0 # define the version to be used
dependencies:
isar: *isar_version
isar_flutter_libs: *isar_version # contains Isar Core
dev_dependencies:
isar_generator: *isar_version
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<Recipient>? 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.
<img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/inspector.gif">
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<void> collectionStream = isar.emails.watchLazy();
Stream<List<Post>> 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 😇
| <img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/benchmarks/insert.png" width="100%" /> | <img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/benchmarks/query.png" width="100%" /> |
| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| <img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/benchmarks/delete.png" width="100%" /> | <img src="https://raw.githubusercontent.com/isar/isar/main/.github/assets/benchmarks/size.png" width="100%" /> |
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/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:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AlexisL61"><img src="https://avatars.githubusercontent.com/u/30233189?v=4" width="100px;" alt=""/><br /><sub><b>Alexis</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/buraktabn"><img src="https://avatars.githubusercontent.com/u/49204989?v=4" width="100px;" alt=""/><br /><sub><b>Burak</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CarloDotLog"><img src="https://avatars.githubusercontent.com/u/13763473?v=4" width="100px;" alt=""/><br /><sub><b>Carlo Loguercio</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Frostedfox"><img src="https://avatars.githubusercontent.com/u/84601232?v=4" width="100px;" alt=""/><br /><sub><b>Frostedfox</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hafeezrana"><img src="https://avatars.githubusercontent.com/u/87476445?v=4" width="100px;" alt=""/><br /><sub><b>Hafeez Rana</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/h1376h"><img src="https://avatars.githubusercontent.com/u/3498335?v=4" width="100px;" alt=""/><br /><sub><b>Hamed H.</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jtplouffe"><img src="https://avatars.githubusercontent.com/u/32107801?v=4" width="100px;" alt=""/><br /><sub><b>JT</b></sub></a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ritksm"><img src="https://avatars.githubusercontent.com/u/111809?v=4" width="100px;" alt=""/><br /><sub><b>Jack Rivers</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nohli"><img src="https://avatars.githubusercontent.com/u/43643339?v=4" width="100px;" alt=""/><br /><sub><b>Joachim Nohl</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vothvovo"><img src="https://avatars.githubusercontent.com/u/20894472?v=4" width="100px;" alt=""/><br /><sub><b>Johnson</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VoidxHoshi"><img src="https://avatars.githubusercontent.com/u/55886143?v=4" width="100px;" alt=""/><br /><sub><b>LaLucid</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/letyletylety"><img src="https://avatars.githubusercontent.com/u/16468579?v=4" width="100px;" alt=""/><br /><sub><b>Lety</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lodisy"><img src="https://avatars.githubusercontent.com/u/8101584?v=4" width="100px;" alt=""/><br /><sub><b>Michael</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moseco"><img src="https://avatars.githubusercontent.com/u/10720298?v=4" width="100px;" alt=""/><br /><sub><b>Moseco</b></sub></a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inkomomutane"><img src="https://avatars.githubusercontent.com/u/57417802?v=4" width="100px;" alt=""/><br /><sub><b>Nelson Mutane</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Viper-Bit"><img src="https://avatars.githubusercontent.com/u/24822764?v=4" width="100px;" alt=""/><br /><sub><b>Peyman</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leisim"><img src="https://avatars.githubusercontent.com/u/13610195?v=4" width="100px;" alt=""/><br /><sub><b>Simon Leier</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ika020202"><img src="https://avatars.githubusercontent.com/u/42883378?v=4" width="100px;" alt=""/><br /><sub><b>Ura</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/blendthink"><img src="https://avatars.githubusercontent.com/u/32213113?v=4" width="100px;" alt=""/><br /><sub><b>blendthink</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mnkeis"><img src="https://avatars.githubusercontent.com/u/41247357?v=4" width="100px;" alt=""/><br /><sub><b>mnkeis</b></sub></a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nobkd"><img src="https://avatars.githubusercontent.com/u/44443899?v=4" width="100px;" alt=""/><br /><sub><b>nobkd</b></sub></a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
### 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.
```

11
analysis_options.yaml Normal file
View File

@ -0,0 +1,11 @@
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

3
example/README.md Normal file
View File

@ -0,0 +1,3 @@
## The fastest way to get started is by following the [Quickstart Guide](https://isar.dev/tutorials/quickstart.html)!
Have fun using Isar!

49
lib/isar.dart Normal file
View File

@ -0,0 +1,49 @@
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);

View File

@ -0,0 +1,11 @@
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;
}

View File

@ -0,0 +1,34 @@
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<String> ignore;
}

View File

@ -0,0 +1,17 @@
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<String> ignore;
}

View File

@ -0,0 +1,33 @@
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
}

View File

@ -0,0 +1,11 @@
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();
}

View File

@ -0,0 +1,76 @@
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<String>` 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<CompositeIndex> 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<String>` 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;
}

View File

@ -0,0 +1,13 @@
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;
}

View File

@ -0,0 +1,20 @@
// 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;

View File

@ -0,0 +1,222 @@
// 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<Future<void>> _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<Transaction> beginTxn(bool write, bool silent);
Future<T> _beginTxn<T>(
bool write,
bool silent,
Future<T> Function() callback,
) async {
requireOpen();
_requireNotInTxn();
final completer = Completer<void>();
_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<T> txn<T>(Future<T> Function() callback) {
return _beginTxn(false, false, callback);
}
@override
Future<T> writeTxn<T>(Future<T> Function() callback, {bool silent = false}) {
return _beginTxn(true, silent, callback);
}
/// @nodoc
Future<R> getTxn<R, T extends Transaction>(
bool write,
Future<R> 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<T>(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>(T Function() callback) {
return _beginTxnSync(false, false, callback);
}
@override
T writeTxnSync<T>(T Function() callback, {bool silent = false}) {
return _beginTxnSync(true, silent, callback);
}
/// @nodoc
R getTxnSync<R, T extends Transaction>(
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<bool> 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<void> commit();
/// @nodoc
void commitSync();
/// @nodoc
Future<void> abort();
/// @nodoc
void abortSync();
/// @nodoc
void free() {}
}

View File

@ -0,0 +1,108 @@
import 'package:isar/isar.dart';
/// @nodoc
abstract class IsarLinkBaseImpl<OBJ> implements IsarLinkBase<OBJ> {
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<dynamic> sourceCollection;
/// The target collection of the link. For backlinks it is actually the origin
/// collection.
late final IsarCollection<OBJ> targetCollection;
@override
bool get isAttached => _objectId != null;
@override
void attach(
IsarCollection<dynamic> sourceCollection,
IsarCollection<OBJ> 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<OBJ, OBJ, QAfterFilterCondition> 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<void> update({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
});
/// See [IsarLinks.updateSync].
void updateSync({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
});
}

View File

@ -0,0 +1,92 @@
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<OBJ> extends IsarLinkBaseImpl<OBJ>
with IsarLink<OBJ> {
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<void> load() async {
_value = await filter().findFirst();
isChanged = false;
isLoaded = true;
}
@override
void loadSync() {
_value = filter().findFirstSync();
isChanged = false;
isLoaded = true;
}
@override
Future<void> 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<void> 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)';
}
}

View File

@ -0,0 +1,223 @@
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<OBJ> extends IsarLinkBaseImpl<OBJ>
with IsarLinks<OBJ>, SetMixin<OBJ> {
final _objects = <Id, OBJ>{};
/// @nodoc
final addedObjects = HashSet<OBJ>.identity();
/// @nodoc
final removedObjects = HashSet<OBJ>.identity();
@override
bool isLoaded = false;
@override
bool get isChanged => addedObjects.isNotEmpty || removedObjects.isNotEmpty;
Map<Id, OBJ> get _loadedObjects {
if (isAttached && !isLoaded && !_kIsWeb) {
loadSync();
}
return _objects;
}
@override
void attach(
IsarCollection<dynamic> sourceCollection,
IsarCollection<OBJ> targetCollection,
String linkName,
Id? objectId,
) {
super.attach(sourceCollection, targetCollection, linkName, objectId);
_applyAddedRemoved();
}
@override
Future<void> 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<OBJ> 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<void> 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<void> 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<OBJ> 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<OBJ> 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)';
}
}

View File

@ -0,0 +1,13 @@
import 'package:isar/isar.dart';
/// @nodoc
List<Schema<dynamic>> getSchemas(
List<CollectionSchema<dynamic>> collectionSchemas,
) {
final schemas = <Schema<dynamic>>{};
for (final collectionSchema in collectionSchemas) {
schemas.add(collectionSchema);
schemas.addAll(collectionSchema.embeddedSchemas.values);
}
return schemas.toList();
}

347
lib/src/isar.dart Normal file
View File

@ -0,0 +1,347 @@
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.0+1';
/// 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<String, Isar> _instances = <String, Isar>{};
static final Set<IsarOpenCallback> _openCallbacks = <IsarOpenCallback>{};
static final Set<IsarCloseCallback> _closeCallbacks = <IsarCloseCallback>{};
/// 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<Type, IsarCollection<dynamic>> _collections;
late final Map<String, IsarCollection<dynamic>> _collectionsByName;
bool _isOpen = true;
static void _checkOpen(String name, List<CollectionSchema<dynamic>> 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 = <String>{};
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<Isar> open(
List<CollectionSchema<dynamic>> 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<CollectionSchema<dynamic>> 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<T> txn<T>(Future<T> Function() callback);
/// Executes an asynchronous read-write transaction.
///
/// If [silent] is `true`, watchers are not notified about changes in this
/// transaction.
Future<T> writeTxn<T>(Future<T> Function() callback, {bool silent = false});
/// Executes a synchronous read-only transaction.
T txnSync<T>(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>(T Function() callback, {bool silent = false});
/// @nodoc
@protected
void attachCollections(Map<Type, IsarCollection<dynamic>> collections) {
_collections = collections;
_collectionsByName = {
for (IsarCollection<dynamic> col in collections.values) col.name: col,
};
}
/// Get a collection by its type.
///
/// You should use the generated extension methods instead.
IsarCollection<T> collection<T>() {
requireOpen();
final collection = _collections[T];
if (collection == null) {
throw IsarError('Missing ${T.runtimeType}Schema in Isar.open');
}
return collection as IsarCollection<T>;
}
/// @nodoc
@protected
IsarCollection<dynamic>? getCollectionByNameInternal(String name) {
return _collectionsByName[name];
}
/// Remove all data in this instance and reset the auto increment values.
Future<void> 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<int> 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<void> 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<bool> 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<void> verify();
/// A list of all Isar instances opened in the current isolate.
static Set<String> 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<void> initializeIsarCore({
Map<IsarAbi, String> 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<String> 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;
}

View File

@ -0,0 +1,342 @@
part of isar;
/// Normal keys consist of a single object, composite keys multiple.
typedef IndexKey = List<Object?>;
/// 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<OBJ>()` or
/// by using the generated `isar.yourCollections` getter.
abstract class IsarCollection<OBJ> {
/// The corresponding Isar instance.
Isar get isar;
/// Get the schema of the collection.
CollectionSchema<OBJ> 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<OBJ?> get(Id id) {
return getAll([id]).then((List<OBJ?> 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<List<OBJ?>> getAll(List<Id> ids);
/// {@macro col_get_all}
List<OBJ?> getAllSync(List<Id> 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<OBJ?> getByIndex(String indexName, IndexKey key) {
return getAllByIndex(indexName, [key])
.then((List<OBJ?> 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<List<OBJ?>> getAllByIndex(String indexName, List<IndexKey> keys);
/// {@macro col_get_all_by_index}'
@experimental
List<OBJ?> getAllByIndexSync(String indexName, List<IndexKey> 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<Id> put(OBJ object) {
return putAll([object]).then((List<Id> 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<List<Id>> putAll(List<OBJ> objects);
/// {@macro col_put_all}
List<Id> putAllSync(List<OBJ> 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<Id> putByIndex(String indexName, OBJ object) {
return putAllByIndex(indexName, [object]).then((List<Id> 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<List<Id>> putAllByIndex(String indexName, List<OBJ> objects);
/// {@macro col_put_all_by_index}
@experimental
List<Id> putAllByIndexSync(
String indexName,
List<OBJ> 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<bool> 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<int> deleteAll(List<Id> ids);
/// {@macro col_delete_all}
int deleteAllSync(List<Id> 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<bool> 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<int> deleteAllByIndex(String indexName, List<IndexKey> keys);
/// {@macro col_delete_all_by_index}
@experimental
int deleteAllByIndexSync(String indexName, List<IndexKey> keys);
/// {@template col_clear}
/// Remove all data in this collection and reset the auto increment value.
/// {@endtemplate}
Future<void> 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<void> 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<void> importJson(List<Map<String, dynamic>> json);
/// {@macro col_import_json}
void importJsonSync(List<Map<String, dynamic>> 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<OBJ, OBJ, QWhere> 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<OBJ, OBJ, QFilterCondition> 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<R> buildQuery<R>({
List<WhereClause> whereClauses = const [],
bool whereDistinct = false,
Sort whereSort = Sort.asc,
FilterOperation? filter,
List<SortProperty> sortBy = const [],
List<DistinctProperty> 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<int> 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<int> 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<void> 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<OBJ?> watchObject(Id id, {bool fireImmediately = false});
/// Watch the object with [id] for changes.
///
/// If [fireImmediately] is `true`, an event will be fired immediately.
Stream<void> 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<void> verify(List<OBJ> 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<void> verifyLink(
String linkName,
List<int> sourceIds,
List<int> targetIds,
);
}

263
lib/src/isar_connect.dart Normal file
View File

@ -0,0 +1,263 @@
// coverage:ignore-file
// ignore_for_file: avoid_print
part of isar;
abstract class _IsarConnect {
static const Map<ConnectAction,
Future<dynamic> Function(Map<String, dynamic> _)> _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<CollectionSchema<dynamic>>? _schemas;
// ignore: cancel_subscriptions
static final _querySubscription = <StreamSubscription<void>>[];
static final List<StreamSubscription<void>> _collectionSubscriptions =
<StreamSubscription<void>>[];
static void initialize(List<CollectionSchema<dynamic>> 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<String, String> parameters) async {
try {
final args = parameters.containsKey('args')
? jsonDecode(parameters['args']!) as Map<String, dynamic>
: <String, dynamic>{};
final result = <String, dynamic>{'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.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<dynamic> _getSchema(Map<String, dynamic> _) async {
return _schemas!.map((e) => e.toJson()).toList();
}
static Future<dynamic> _listInstances(Map<String, dynamic> _) async {
return Isar.instanceNames.toList();
}
static Future<bool> _watchInstance(Map<String, dynamic> 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<dynamic> 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<Map<String, dynamic>> _executeQuery(
Map<String, dynamic> 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<dynamic>(
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<bool> _removeQuery(Map<String, dynamic> params) async {
final query = ConnectQuery.fromJson(params).toQuery();
await query.isar.writeTxn(query.deleteAll);
return true;
}
static Future<void> _importJson(Map<String, dynamic> 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<Map<String, dynamic>>();
await instance.writeTxn(() async {
await collection.importJson(objects);
});
}
static Future<List<dynamic>> _exportJson(Map<String, dynamic> params) async {
final query = ConnectQuery.fromJson(params).toQuery();
return query.exportJson();
}
static Future<void> _editProperty(Map<String, dynamic> 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<dynamic>(
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);
}
}
}
}

View File

@ -0,0 +1,215 @@
// 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<String, dynamic> json) {
return ConnectQuery(
instance: json['instance'] as String,
collection: json['collection'] as String,
filter: _filterFromJson(json['filter'] as Map<String, dynamic>?),
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<String, dynamic> 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<String, dynamic>? json) {
if (json == null) {
return null;
}
if (json.containsKey('filters')) {
final filters = (json['filters'] as List)
.map((e) => _filterFromJson(e as Map<String, dynamic>?)!)
.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<String, dynamic> _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<dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'instance': instance,
'collection': collection,
'size': size,
'count': count,
};
}
}

23
lib/src/isar_error.dart Normal file
View File

@ -0,0 +1,23 @@
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');
}

113
lib/src/isar_link.dart Normal file
View File

@ -0,0 +1,113 @@
part of isar;
/// @nodoc
@sealed
abstract class IsarLinkBase<OBJ> {
/// 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<void> 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<void> 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<void> reset();
/// {@macro link_reset}
void resetSync();
/// @nodoc
@protected
void attach(
IsarCollection<dynamic> sourceCollection,
IsarCollection<OBJ> 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<OBJ> implements IsarLinkBase<OBJ> {
/// 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<OBJ> implements IsarLinkBase<OBJ>, Set<OBJ> {
/// Create an empty, unattached link. Make sure to provide the correct
/// generic argument.
factory IsarLinks() => IsarLinksImpl();
@override
Future<void> 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<void> update({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
});
/// {@macro links_update}
void updateSync({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
});
/// Starts a query for linked objects.
QueryBuilder<OBJ, OBJ, QAfterFilterCondition> 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<int> count() => filter().count();
/// {@macro links_count}
int countSync() => filter().countSync();
}

88
lib/src/isar_reader.dart Normal file
View File

@ -0,0 +1,88 @@
// 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<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
);
List<bool>? readBoolList(int offset);
List<bool?>? readBoolOrNullList(int offset);
List<int>? readByteList(int offset);
List<int>? readIntList(int offset);
List<int?>? readIntOrNullList(int offset);
List<double>? readFloatList(int offset);
List<double?>? readFloatOrNullList(int offset);
List<int>? readLongList(int offset);
List<int?>? readLongOrNullList(int offset);
List<double>? readDoubleList(int offset);
List<double?>? readDoubleOrNullList(int offset);
List<DateTime>? readDateTimeList(int offset);
List<DateTime?>? readDateTimeOrNullList(int offset);
List<String>? readStringList(int offset);
List<String?>? readStringOrNullList(int offset);
List<T>? readObjectList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
T defaultValue,
);
List<T?>? readObjectOrNullList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
);
}

53
lib/src/isar_writer.dart Normal file
View File

@ -0,0 +1,53 @@
// 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<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> serialize,
T? value,
);
void writeByteList(int offset, List<int>? values);
void writeBoolList(int offset, List<bool?>? values);
void writeIntList(int offset, List<int?>? values);
void writeFloatList(int offset, List<double?>? values);
void writeLongList(int offset, List<int?>? values);
void writeDoubleList(int offset, List<double?>? values);
void writeDateTimeList(int offset, List<DateTime?>? values);
void writeStringList(int offset, List<String?>? values);
void writeObjectList<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> serialize,
List<T?>? values,
);
}

2241
lib/src/native/bindings.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
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<Char> toCString(Allocator alloc) {
final bufferPtr = alloc<Uint8>(length * 3 + 1);
final buffer = bufferPtr.asTypedList(length * 3 + 1);
final size = encodeString(this, buffer, 0);
buffer[size] = 0;
return bufferPtr.cast();
}
}

View File

@ -0,0 +1,257 @@
// 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<CIndexKey>>();
Pointer<CIndexKey> buildIndexKey(
CollectionSchema<dynamic> 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<CIndexKey> buildLowerUnboundedIndexKey() {
IC.isar_key_create(_keyPtrPtr);
return _keyPtrPtr.value;
}
Pointer<CIndexKey> buildUpperUnboundedIndexKey() {
IC.isar_key_create(_keyPtrPtr);
final keyPtr = _keyPtrPtr.value;
IC.isar_key_add_long(keyPtr, maxLong);
return keyPtr;
}
void _addKeyValue(
Pointer<CIndexKey> 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<bool?>;
final boolListPtr = malloc<Uint8>(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<int>;
final bytesPtr = malloc<Uint8>(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<int?>;
final intListPtr = malloc<Int32>(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<int?>;
final longListPtr = malloc<Int64>(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<DateTime?>;
final longListPtr = malloc<Int64>(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<String?>;
final stringListPtr = malloc<Pointer<Char>>(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<Char> _strToNative(String? str) {
if (str == null) {
return Pointer.fromAddress(0);
} else {
return str.toCString(malloc);
}
}
void _freeStr(Pointer<Char> 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;
}

View File

@ -0,0 +1,649 @@
// 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<OBJ> extends IsarCollection<OBJ> {
IsarCollectionImpl({
required this.isar,
required this.ptr,
required this.schema,
});
@override
final IsarImpl isar;
final Pointer<CIsarCollection> ptr;
@override
final CollectionSchema<OBJ> 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<OBJ> deserializeObjects(CObjectSet objectSet) {
final objects = <OBJ>[];
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<OBJ?> deserializeObjectsOrNull(CObjectSet objectSet) {
final objects = List<OBJ?>.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<Pointer<CIndexKey>> _getKeysPtr(
String indexName,
List<IndexKey> keys,
Allocator alloc,
) {
final keysPtrPtr = alloc<Pointer<CIndexKey>>(keys.length);
for (var i = 0; i < keys.length; i++) {
keysPtrPtr[i] = buildIndexKey(schema, schema.index(indexName), keys[i]);
}
return keysPtrPtr;
}
List<T> deserializeProperty<T>(CObjectSet objectSet, int? propertyId) {
final values = <T>[];
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<CObject> objectsPtr,
List<OBJ> 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<Uint8>(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<List<OBJ?>> getAll(List<int> 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<OBJ?> getAllSync(List<int> ids) {
return isar.getTxnSync(false, (Txn txn) {
final cObjPtr = txn.getCObject();
final cObj = cObjPtr.ref;
final objects = List<OBJ?>.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<List<OBJ?>> getAllByIndex(String indexName, List<IndexKey> 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<OBJ?> getAllByIndexSync(String indexName, List<IndexKey> keys) {
final index = schema.index(indexName);
return isar.getTxnSync(false, (Txn txn) {
final cObjPtr = txn.getCObject();
final cObj = cObjPtr.ref;
final objects = List<OBJ?>.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<List<int>> putAll(List<OBJ> objects) {
return putAllByIndex(null, objects);
}
@override
List<int> putAllSync(List<OBJ> objects, {bool saveLinks = true}) {
return putAllByIndexSync(null, objects, saveLinks: saveLinks);
}
@override
Future<List<int>> putAllByIndex(String? indexName, List<OBJ> 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<int>.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<int> putAllByIndexSync(
String? indexName,
List<OBJ> 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<int> deleteAll(List<int> ids) {
return isar.getTxn(true, (Txn txn) async {
final countPtr = txn.alloc<Uint32>();
final idsPtr = txn.alloc<Int64>(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<int> ids) {
return isar.getTxnSync(true, (Txn txn) {
final deletedPtr = txn.alloc<Bool>();
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<int> deleteAllByIndex(String indexName, List<IndexKey> keys) {
return isar.getTxn(true, (Txn txn) async {
final countPtr = txn.alloc<Uint32>();
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<IndexKey> keys) {
return isar.getTxnSync(true, (Txn txn) {
final countPtr = txn.alloc<Uint32>();
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<void> 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<void> importJson(List<Map<String, dynamic>> json) {
final bytes = const Utf8Encoder().convert(jsonEncode(json));
return importJsonRaw(bytes);
}
@override
Future<void> importJsonRaw(Uint8List jsonBytes) {
return isar.getTxn(true, (Txn txn) async {
final bytesPtr = txn.alloc<Uint8>(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<Map<String, dynamic>> 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<int> count() {
return isar.getTxn(false, (Txn txn) async {
final countPtr = txn.alloc<Int64>();
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<Int64>();
nCall(IC.isar_count(ptr, txn.ptr, countPtr));
return countPtr.value;
});
}
@override
Future<int> getSize({
bool includeIndexes = false,
bool includeLinks = false,
}) {
return isar.getTxn(false, (Txn txn) async {
final sizePtr = txn.alloc<Int64>();
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<Int64>();
nCall(
IC.isar_get_size(
ptr,
txn.ptr,
includeIndexes,
includeLinks,
sizePtr,
),
);
return sizePtr.value;
});
}
@override
Stream<void> watchLazy({bool fireImmediately = false}) {
isar.requireOpen();
final port = ReceivePort();
final handle =
IC.isar_watch_collection(isar.ptr, ptr, port.sendPort.nativePort);
final controller = StreamController<void>(
onCancel: () {
IC.isar_stop_watching(handle);
port.close();
},
);
if (fireImmediately) {
controller.add(null);
}
controller.addStream(port);
return controller.stream;
}
@override
Stream<OBJ?> watchObject(Id id, {bool fireImmediately = false}) {
return watchObjectLazy(id, fireImmediately: fireImmediately)
.asyncMap((event) => get(id));
}
@override
Stream<void> watchObjectLazy(Id id, {bool fireImmediately = false}) {
isar.requireOpen();
final cObjPtr = malloc<CObject>();
final port = ReceivePort();
final handle =
IC.isar_watch_object(isar.ptr, ptr, id, port.sendPort.nativePort);
malloc.free(cObjPtr);
final controller = StreamController<void>(
onCancel: () {
IC.isar_stop_watching(handle);
port.close();
},
);
if (fireImmediately) {
controller.add(null);
}
controller.addStream(port);
return controller.stream;
}
@override
Query<T> buildQuery<T>({
List<WhereClause> whereClauses = const [],
bool whereDistinct = false,
Sort whereSort = Sort.asc,
FilterOperation? filter,
List<SortProperty> sortBy = const [],
List<DistinctProperty> distinctBy = const [],
int? offset,
int? limit,
String? property,
}) {
isar.requireOpen();
return buildNativeQuery(
this,
whereClauses,
whereDistinct,
whereSort,
filter,
sortBy,
distinctBy,
offset,
limit,
property,
);
}
@override
Future<void> verify(List<OBJ> 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<void> verifyLink(
String linkName,
List<int> sourceIds,
List<int> targetIds,
) async {
final link = schema.link(linkName);
return isar.getTxn(false, (Txn txn) async {
final idsPtr = txn.alloc<Int64>(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();
});
}
}

View File

@ -0,0 +1,234 @@
// 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 _githubUrl = 'https://github.com/isar/isar/releases/download';
bool _isarInitialized = false;
// ignore: non_constant_identifier_names
late final IsarCoreBindings IC;
typedef FinalizerFunction = void Function(Pointer<Void> token);
late final Pointer<NativeFinalizerFunction> isarClose;
late final Pointer<NativeFinalizerFunction> isarQueryFree;
FutureOr<void> initializeCoreBinary({
Map<Abi, String> 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<Utf8>().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<Abi, String> 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<void> _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('$_githubUrl/${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<Utf8>().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<void> wrapIsarPort(ReceivePort port) {
final portStreamController = StreamController<void>(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();
}
}

View File

@ -0,0 +1,139 @@
// 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<CIsarInstance> ptr;
late final NativeFinalizer _finalizer;
final offsets = <Type, List<int>>{};
final Pointer<Pointer<CIsarTxn>> _syncTxnPtrPtr = malloc<Pointer<CIsarTxn>>();
String? _directory;
@override
String get directory {
requireOpen();
if (_directory == null) {
final dirPtr = IC.isar_instance_get_path(ptr);
try {
_directory = dirPtr.cast<Utf8>().toDartString();
} finally {
IC.isar_free_string(dirPtr);
}
}
return _directory!;
}
@override
Future<Transaction> beginTxn(bool write, bool silent) async {
final port = ReceivePort();
final portStream = wrapIsarPort(port);
final txnPtrPtr = malloc<Pointer<CIsarTxn>>();
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<int> getSize({
bool includeIndexes = false,
bool includeLinks = false,
}) {
return getTxn(false, (Txn txn) async {
final sizePtr = txn.alloc<Int64>();
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<Int64>();
nCall(
IC.isar_instance_get_size(
ptr,
txn.ptr,
includeIndexes,
includeLinks,
sizePtr,
),
);
return sizePtr.value;
});
}
@override
Future<void> 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<void> verify() async {
return getTxn(false, (Txn txn) async {
IC.isar_instance_verify(ptr, txn.ptr);
await txn.wait();
});
}
}

View File

@ -0,0 +1,121 @@
// 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<OBJ> on IsarLinkBaseImpl<OBJ> {
@override
IsarCollectionImpl<dynamic> get sourceCollection =>
super.sourceCollection as IsarCollectionImpl;
@override
IsarCollectionImpl<OBJ> get targetCollection =>
super.targetCollection as IsarCollectionImpl<OBJ>;
late final int linkId = sourceCollection.schema.link(linkName).id;
@override
late final getId = targetCollection.schema.getId;
@override
Future<void> update({
Iterable<OBJ> link = const [],
Iterable<OBJ> 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<Int64>(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<OBJ> link = const [],
Iterable<OBJ> 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<OBJ> extends IsarLinkCommon<OBJ>
with IsarLinkBaseMixin<OBJ> {}
class IsarLinksImpl<OBJ> extends IsarLinksCommon<OBJ>
with IsarLinkBaseMixin<OBJ> {}

View File

@ -0,0 +1,591 @@
// 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<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> 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<bool>? 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<bool>.filled(length, false);
for (var i = 0; i < length; i++) {
list[i] = _readBool(listOffset + i);
}
return list;
}
@override
List<bool?>? 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<bool?>.filled(length, null);
for (var i = 0; i < length; i++) {
list[i] = _readBoolOrNull(listOffset + i);
}
return list;
}
@override
List<int>? 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<int>? 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<int?>? 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<int?>.filled(length, null);
for (var i = 0; i < length; i++) {
list[i] = _readIntOrNull(listOffset + i * 4);
}
return list;
}
@override
List<double>? 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<double?>? 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<double?>.filled(length, null);
for (var i = 0; i < length; i++) {
list[i] = _readFloatOrNull(listOffset + i * 4);
}
return list;
}
@override
List<int>? 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<int?>? 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<int?>.filled(length, null);
for (var i = 0; i < length; i++) {
list[i] = _readLongOrNull(listOffset + i * 8);
}
return list;
}
@override
List<double>? 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<double?>? 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<double?>.filled(length, null);
for (var i = 0; i < length; i++) {
list[i] = _readDoubleOrNull(listOffset + i * 8);
}
return list;
}
@override
List<DateTime>? readDateTimeList(int offset) {
return readLongOrNullList(offset)?.map((e) {
if (e != null) {
return DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true).toLocal();
} else {
return nullDate;
}
}).toList();
}
@override
List<DateTime?>? readDateTimeOrNullList(int offset) {
return readLongOrNullList(offset)?.map((e) {
if (e != null) {
return DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true).toLocal();
}
}).toList();
}
List<T>? readDynamicList<T>(
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<String>? readStringList(int offset) {
return readDynamicList(offset, '', (startOffset, endOffset) {
return utf8Decoder.convert(_buffer, startOffset, endOffset);
});
}
@override
List<String?>? readStringOrNullList(int offset) {
return readDynamicList(offset, null, (startOffset, endOffset) {
return utf8Decoder.convert(_buffer, startOffset, endOffset);
});
}
@override
List<T>? readObjectList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> 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<T?>? readObjectOrNullList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> 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);
});
}
}

View File

@ -0,0 +1,284 @@
// 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<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> 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<int>? 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<bool?>? 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<int?>? 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<double?>? 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<int?>? 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<double?>? 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<DateTime?>? values) {
final longList = values?.map((e) => e.longValue).toList();
writeLongList(offset, longList);
}
@override
void writeStringList(int offset, List<String?>? 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<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> serialize,
List<T?>? 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;
}

159
lib/src/native/open.dart Normal file
View File

@ -0,0 +1,159 @@
// 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<Pointer<CIsarInstance>> _isarPtrPtr =
malloc<Pointer<CIsarInstance>>();
List<int> _getOffsets(
Pointer<CIsarCollection> colPtr,
int propertiesCount,
int embeddedColId,
) {
final offsetsPtr = malloc<Uint32>(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<CollectionSchema<dynamic>> schemas,
) {
final colPtrPtr = malloc<Pointer<CIsarCollection>>();
final cols = <Type, IsarCollection<dynamic>>{};
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(<OBJ>() {
isar.offsets[OBJ] = offsets;
schema as CollectionSchema<OBJ>;
cols[OBJ] = IsarCollectionImpl<OBJ>(
isar: isar,
ptr: colPtrPtr.value,
schema: schema,
);
});
}
malloc.free(colPtrPtr);
isar.attachCollections(cols);
}
Future<Isar> openIsar({
required List<CollectionSchema<dynamic>> 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<CollectionSchema<dynamic>> 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 diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,261 @@
// 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<T> = List<T> Function(CObjectSet);
class QueryImpl<T> extends Query<T> implements Finalizable {
QueryImpl(this.col, this.queryPtr, this.deserialize, this.propertyId) {
NativeFinalizer(isarQueryFree).attach(this, queryPtr.cast());
}
static const int maxLimit = 4294967295;
final IsarCollectionImpl<dynamic> col;
final Pointer<CQuery> queryPtr;
final QueryDeserialize<T> deserialize;
final int? propertyId;
@override
Isar get isar => col.isar;
@override
Future<T?> findFirst() {
return findInternal(maxLimit).then((List<T> result) {
if (result.isNotEmpty) {
return result[0];
} else {
return null;
}
});
}
@override
Future<List<T>> findAll() => findInternal(maxLimit);
Future<List<T>> findInternal(int limit) {
return col.isar.getTxn(false, (Txn txn) async {
final resultsPtr = txn.alloc<CObjectSet>();
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<T> findAllSync() => findSyncInternal(maxLimit);
List<T> 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<bool> deleteFirst() =>
deleteInternal(1).then((int count) => count == 1);
@override
Future<int> deleteAll() => deleteInternal(maxLimit);
Future<int> deleteInternal(int limit) {
return col.isar.getTxn(false, (Txn txn) async {
final countPtr = txn.alloc<Uint32>();
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<Uint32>();
nCall(IC.isar_q_delete(queryPtr, col.ptr, txn.ptr, limit, countPtr));
return countPtr.value;
});
}
@override
Stream<List<T>> watch({bool fireImmediately = false}) {
return watchLazy(fireImmediately: fireImmediately)
.asyncMap((event) => findAll());
}
@override
Stream<void> 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<void>(
onCancel: () {
IC.isar_stop_watching(handle);
port.close();
},
);
if (fireImmediately) {
controller.add(null);
}
controller.addStream(port);
return controller.stream;
}
@override
Future<R> exportJsonRaw<R>(R Function(Uint8List) callback) {
return col.isar.getTxn(false, (Txn txn) async {
final bytesPtrPtr = txn.alloc<Pointer<Uint8>>();
final lengthPtr = txn.alloc<Uint32>();
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>(R Function(Uint8List) callback) {
return col.isar.getTxnSync(false, (Txn txn) {
final bytesPtrPtr = txn.alloc<Pointer<Uint8>>();
final lengthPtr = txn.alloc<Uint32>();
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<R?> aggregate<R>(AggregationOp op) async {
return col.isar.getTxn(false, (Txn txn) async {
final resultPtrPtr = txn.alloc<Pointer<CAggregationResult>>();
IC.isar_q_aggregate(
col.ptr,
queryPtr,
txn.ptr,
op.index,
propertyId ?? 0,
resultPtrPtr,
);
await txn.wait();
return _convertAggregatedResult<R>(resultPtrPtr.value, op);
});
}
@override
R? aggregateSync<R>(AggregationOp op) {
return col.isar.getTxnSync(false, (Txn txn) {
final resultPtrPtr = txn.alloc<Pointer<CAggregationResult>>();
nCall(
IC.isar_q_aggregate(
col.ptr,
queryPtr,
txn.ptr,
op.index,
propertyId ?? 0,
resultPtrPtr,
),
);
return _convertAggregatedResult(resultPtrPtr.value, op);
});
}
R? _convertAggregatedResult<R>(
Pointer<CAggregationResult> 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;
}
}
}
}

View File

@ -0,0 +1,33 @@
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<String> isarSplitWords(String input) {
initializeCoreBinary();
final bytesPtr = malloc<Uint8>(input.length * 3);
final bytes = bytesPtr.asTypedList(input.length * 3);
final byteCount = encodeString(input, bytes, 0);
final wordCountPtr = malloc<Uint32>();
final boundariesPtr =
IC.isar_find_word_boundaries(bytesPtr.cast(), byteCount, wordCountPtr);
final wordCount = wordCountPtr.value;
final boundaries = boundariesPtr.asTypedList(wordCount * 2);
final words = <String>[];
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;
}

113
lib/src/native/txn.dart Normal file
View File

@ -0,0 +1,113 @@
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<void> 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<CIsarTxn> ptr;
Pointer<CObject>? _cObjPtr;
Pointer<CObjectSet>? _cObjSetPtr;
late Pointer<Uint8> _buffer;
int _bufferLen = -1;
late final Queue<Completer<void>> _completers;
late final StreamSubscription<void>? _portSubscription;
/// Get a shared CObject pointer
Pointer<CObject> getCObject() {
_cObjPtr ??= alloc<CObject>();
return _cObjPtr!;
}
/// Get a shared CObjectSet pointer
Pointer<CObjectSet> getCObjectsSet() {
_cObjSetPtr ??= alloc();
return _cObjSetPtr!;
}
/// Allocate a new CObjectSet with the given capacity.
Pointer<CObjectSet> newCObjectSet(int length) {
final cObjSetPtr = alloc<CObjectSet>();
cObjSetPtr.ref
..objects = alloc<CObject>(length)
..length = length;
return cObjSetPtr;
}
/// Get a shared buffer with at least the specified size.
Pointer<Uint8> 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<void> wait() {
final completer = Completer<void>();
_completers.add(completer);
return completer.future;
}
@override
Future<void> 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<void> 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();
}
}

204
lib/src/query.dart Normal file
View File

@ -0,0 +1,204 @@
part of isar;
/// Querying is how you find records that match certain conditions.
abstract class Query<T> {
/// 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<T?> findFirst();
/// {@macro query_find_first}
T? findFirstSync();
/// {@template query_find_all}
/// Find all objects that match this query.
/// {@endtemplate}
Future<List<T>> findAll();
/// {@macro query_find_all}
List<T> findAllSync();
/// @nodoc
@protected
Future<R?> aggregate<R>(AggregationOp op);
/// @nodoc
@protected
R? aggregateSync<R>(AggregationOp op);
/// {@template query_count}
/// Count how many objects match this query.
///
/// This operation is much faster than using `findAll().length`.
/// {@endtemplate}
Future<int> count() =>
aggregate<int>(AggregationOp.count).then((int? value) => value!);
/// {@macro query_count}
int countSync() => aggregateSync<int>(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<bool> isEmpty() =>
aggregate<int>(AggregationOp.isEmpty).then((value) => value == 1);
/// {@macro query_is_empty}
bool isEmptySync() => aggregateSync<int>(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<bool> isNotEmpty() =>
aggregate<int>(AggregationOp.isEmpty).then((value) => value == 0);
/// {@macro query_is_not_empty}
bool isNotEmptySync() => aggregateSync<int>(AggregationOp.isEmpty) == 0;
/// {@template query_delete_first}
/// Delete the first object that matches this query. Returns whether a object
/// has been deleted.
/// {@endtemplate}
Future<bool> 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<int> 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<List<T>> 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<void> 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<R> exportJsonRaw<R>(R Function(Uint8List) callback);
/// {@macro query_export_json_raw}
R exportJsonRawSync<R>(R Function(Uint8List) callback);
/// {@template query_export_json}
/// Export the results of this query as json.
/// {@endtemplate}
Future<List<Map<String, dynamic>>> exportJson() {
return exportJsonRaw((Uint8List bytes) {
final json = jsonDecode(const Utf8Decoder().convert(bytes)) as List;
return json.cast<Map<String, dynamic>>();
});
}
/// {@macro query_export_json}
List<Map<String, dynamic>> exportJsonSync() {
return exportJsonRawSync((Uint8List bytes) {
final json = jsonDecode(const Utf8Decoder().convert(bytes)) as List;
return json.cast<Map<String, dynamic>>();
});
}
}
/// @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<T extends num> on Query<T?> {
/// {@template aggregation_min}
/// Returns the minimum value of this query.
/// {@endtemplate}
Future<T?> min() => aggregate<T>(AggregationOp.min);
/// {@macro aggregation_min}
T? minSync() => aggregateSync<T>(AggregationOp.min);
/// {@template aggregation_max}
/// Returns the maximum value of this query.
/// {@endtemplate}
Future<T?> max() => aggregate<T>(AggregationOp.max);
/// {@macro aggregation_max}
T? maxSync() => aggregateSync<T>(AggregationOp.max);
/// {@template aggregation_average}
/// Returns the average value of this query.
/// {@endtemplate}
Future<double> average() =>
aggregate<double>(AggregationOp.average).then((double? value) => value!);
/// {@macro aggregation_average}
double averageSync() => aggregateSync<double>(AggregationOp.average)!;
/// {@template aggregation_sum}
/// Returns the sum of all values of this query.
/// {@endtemplate}
Future<T> sum() => aggregate<T>(AggregationOp.sum).then((value) => value!);
/// {@macro aggregation_sum}
T sumSync() => aggregateSync<T>(AggregationOp.sum)!;
}
/// Extension for Queries
extension QueryDateAggregation<T extends DateTime?> on Query<T> {
/// {@macro aggregation_min}
Future<DateTime?> min() => aggregate<DateTime>(AggregationOp.min);
/// {@macro aggregation_min}
DateTime? minSync() => aggregateSync<DateTime>(AggregationOp.min);
/// {@macro aggregation_max}
Future<DateTime?> max() => aggregate<DateTime>(AggregationOp.max);
/// {@macro aggregation_max}
DateTime? maxSync() => aggregateSync<DateTime>(AggregationOp.max);
}

403
lib/src/query_builder.dart Normal file
View File

@ -0,0 +1,403 @@
part of isar;
/// @nodoc
@protected
typedef FilterQuery<OBJ> = QueryBuilder<OBJ, OBJ, QAfterFilterCondition>
Function(QueryBuilder<OBJ, OBJ, QFilterCondition> q);
/// Query builders are used to create queries in a safe way.
///
/// Acquire a `QueryBuilder` instance using `collection.where()` or
/// `collection.filter()`.
class QueryBuilder<OBJ, R, S> {
/// @nodoc
@protected
const QueryBuilder(this._query);
final QueryBuilderInternal<OBJ> _query;
/// @nodoc
@protected
static QueryBuilder<OBJ, R, S> apply<OBJ, R, S>(
QueryBuilder<OBJ, dynamic, dynamic> qb,
QueryBuilderInternal<OBJ> Function(QueryBuilderInternal<OBJ> query)
transform,
) {
return QueryBuilder(transform(qb._query));
}
}
/// @nodoc
@protected
class QueryBuilderInternal<OBJ> {
/// @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<OBJ>? collection;
/// @nodoc
final List<WhereClause> whereClauses;
/// @nodoc
final bool whereDistinct;
/// @nodoc
final Sort whereSort;
/// @nodoc
final FilterGroup filter;
/// @nodoc
final FilterGroupType filterGroupType;
/// @nodoc
final bool filterNot;
/// @nodoc
final List<DistinctProperty> distinctByProperties;
/// @nodoc
final List<SortProperty> sortByProperties;
/// @nodoc
final int? offset;
/// @nodoc
final int? limit;
/// @nodoc
final String? propertyName;
/// @nodoc
QueryBuilderInternal<OBJ> 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<OBJ> addWhereClause(WhereClause where) {
return copyWith(whereClauses: [...whereClauses, where]);
}
/// @nodoc
QueryBuilderInternal<OBJ> group(FilterQuery<OBJ> q) {
// ignore: prefer_const_constructors
final qb = q(QueryBuilder(QueryBuilderInternal()));
return addFilterCondition(qb._query.filter);
}
/// @nodoc
QueryBuilderInternal<OBJ> listLength<E>(
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<OBJ> object<E>(
FilterQuery<E> q,
String property,
) {
// ignore: prefer_const_constructors
final qb = q(QueryBuilder(QueryBuilderInternal()));
return addFilterCondition(
ObjectFilter(filter: qb._query.filter, property: property),
);
}
/// @nodoc
QueryBuilderInternal<OBJ> link<E>(
FilterQuery<E> q,
String linkName,
) {
// ignore: prefer_const_constructors
final qb = q(QueryBuilder(QueryBuilderInternal()));
return addFilterCondition(
LinkFilter(filter: qb._query.filter, linkName: linkName),
);
}
/// @nodoc
QueryBuilderInternal<OBJ> linkLength<E>(
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<OBJ> addSortBy(String propertyName, Sort sort) {
return copyWith(
sortByProperties: [
...sortByProperties,
SortProperty(property: propertyName, sort: sort),
],
);
}
/// @nodoc
QueryBuilderInternal<OBJ> addDistinctBy(
String propertyName, {
bool? caseSensitive,
}) {
return copyWith(
distinctByProperties: [
...distinctByProperties,
DistinctProperty(
property: propertyName,
caseSensitive: caseSensitive,
),
],
);
}
/// @nodoc
QueryBuilderInternal<OBJ> addPropertyName<E>(String propertyName) {
return copyWith(propertyName: propertyName);
}
/// @nodoc
QueryBuilderInternal<OBJ> copyWith({
List<WhereClause>? whereClauses,
FilterGroup? filter,
bool? filterIsGrouped,
FilterGroupType? filterGroupType,
bool? filterNot,
List<FilterGroup>? parentFilters,
List<DistinctProperty>? distinctByProperties,
List<SortProperty>? 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<R> build<R>() {
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 {}

View File

@ -0,0 +1,303 @@
part of isar;
/// Extension for QueryBuilders.
extension QueryWhereOr<OBJ, R> on QueryBuilder<OBJ, R, QWhereOr> {
/// Union of two where clauses.
QueryBuilder<OBJ, R, QWhereClause> or() {
return QueryBuilder(_query);
}
}
/// @nodoc
@protected
typedef WhereRepeatModifier<OBJ, R, E> = QueryBuilder<OBJ, R, QAfterWhereClause>
Function(QueryBuilder<OBJ, R, QWhereClause> q, E element);
/// Extension for QueryBuilders.
extension QueryWhere<OBJ, R> on QueryBuilder<OBJ, R, QWhereClause> {
/// 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<OBJ, R, QAfterWhereClause> anyOf<E, RS>(
Iterable<E> items,
WhereRepeatModifier<OBJ, R, E> modifier,
) {
QueryBuilder<OBJ, R, QAfterWhereClause>? q;
for (final e in items) {
q = modifier(q?.or() ?? QueryBuilder(_query), e);
}
return q ?? QueryBuilder(_query);
}
}
/// Extension for QueryBuilders.
extension QueryFilters<OBJ, R> on QueryBuilder<OBJ, R, QFilter> {
/// Start using filter conditions.
QueryBuilder<OBJ, R, QFilterCondition> filter() {
return QueryBuilder(_query);
}
}
/// @nodoc
@protected
typedef FilterRepeatModifier<OBJ, R, E>
= QueryBuilder<OBJ, R, QAfterFilterCondition> Function(
QueryBuilder<OBJ, R, QFilterCondition> q,
E element,
);
/// Extension for QueryBuilders.
extension QueryFilterAndOr<OBJ, R> on QueryBuilder<OBJ, R, QFilterOperator> {
/// Intersection of two filter conditions.
QueryBuilder<OBJ, R, QFilterCondition> and() {
return QueryBuilder.apply(
this,
(q) => q.copyWith(filterGroupType: FilterGroupType.and),
);
}
/// Union of two filter conditions.
QueryBuilder<OBJ, R, QFilterCondition> or() {
return QueryBuilder.apply(
this,
(q) => q.copyWith(filterGroupType: FilterGroupType.or),
);
}
/// Logical XOR of two filter conditions.
QueryBuilder<OBJ, R, QFilterCondition> xor() {
return QueryBuilder.apply(
this,
(q) => q.copyWith(filterGroupType: FilterGroupType.xor),
);
}
}
/// Extension for QueryBuilders.
extension QueryFilterNot<OBJ, R> on QueryBuilder<OBJ, R, QFilterCondition> {
/// Complement the next filter condition or group.
QueryBuilder<OBJ, R, QFilterCondition> 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<OBJ, R, QAfterFilterCondition> anyOf<E, RS>(
Iterable<E> items,
FilterRepeatModifier<OBJ, OBJ, E> modifier,
) {
return QueryBuilder.apply(this, (query) {
return query.group((q) {
var q2 = QueryBuilder<OBJ, OBJ, QAfterFilterCondition>(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<OBJ, R, QAfterFilterCondition> allOf<E, RS>(
Iterable<E> items,
FilterRepeatModifier<OBJ, OBJ, E> modifier,
) {
return QueryBuilder.apply(this, (query) {
return query.group((q) {
var q2 = QueryBuilder<OBJ, OBJ, QAfterFilterCondition>(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<OBJ, R, QAfterFilterCondition> oneOf<E, RS>(
Iterable<E> items,
FilterRepeatModifier<OBJ, R, E> modifier,
) {
QueryBuilder<OBJ, R, QAfterFilterCondition>? q;
for (final e in items) {
q = modifier(q?.xor() ?? QueryBuilder(_query), e);
}
return q ?? QueryBuilder(_query);
}
}
/// Extension for QueryBuilders.
extension QueryFilterNoGroups<OBJ, R>
on QueryBuilder<OBJ, R, QFilterCondition> {
/// Group filter conditions.
QueryBuilder<OBJ, R, QAfterFilterCondition> group(FilterQuery<OBJ> q) {
return QueryBuilder.apply(this, (query) => query.group(q));
}
}
/// Extension for QueryBuilders.
extension QueryOffset<OBJ, R> on QueryBuilder<OBJ, R, QOffset> {
/// Offset the query results by a static number.
QueryBuilder<OBJ, R, QAfterOffset> offset(int offset) {
return QueryBuilder.apply(this, (q) => q.copyWith(offset: offset));
}
}
/// Extension for QueryBuilders.
extension QueryLimit<OBJ, R> on QueryBuilder<OBJ, R, QLimit> {
/// Limit the maximum number of query results.
QueryBuilder<OBJ, R, QAfterLimit> limit(int limit) {
return QueryBuilder.apply(this, (q) => q.copyWith(limit: limit));
}
}
/// @nodoc
@protected
typedef QueryOption<OBJ, S, RS> = QueryBuilder<OBJ, OBJ, RS> Function(
QueryBuilder<OBJ, OBJ, S> q,
);
/// Extension for QueryBuilders.
extension QueryModifier<OBJ, S> on QueryBuilder<OBJ, OBJ, S> {
/// Only apply a part of the query if `enabled` is true.
QueryBuilder<OBJ, OBJ, RS> optional<RS>(
bool enabled,
QueryOption<OBJ, S, RS> option,
) {
if (enabled) {
return option(this);
} else {
return QueryBuilder(_query);
}
}
}
/// Extension for QueryBuilders
extension QueryExecute<OBJ, R> on QueryBuilder<OBJ, R, QQueryOperations> {
/// Create a query from this query builder.
Query<R> build() => _query.build();
/// {@macro query_find_first}
Future<R?> findFirst() => build().findFirst();
/// {@macro query_find_first}
R? findFirstSync() => build().findFirstSync();
/// {@macro query_find_all}
Future<List<R>> findAll() => build().findAll();
/// {@macro query_find_all}
List<R> findAllSync() => build().findAllSync();
/// {@macro query_count}
Future<int> count() => build().count();
/// {@macro query_count}
int countSync() => build().countSync();
/// {@macro query_is_empty}
Future<bool> isEmpty() => build().isEmpty();
/// {@macro query_is_empty}
bool isEmptySync() => build().isEmptySync();
/// {@macro query_is_not_empty}
Future<bool> isNotEmpty() => build().isNotEmpty();
/// {@macro query_is_not_empty}
bool isNotEmptySync() => build().isNotEmptySync();
/// {@macro query_delete_first}
Future<bool> deleteFirst() => build().deleteFirst();
/// {@macro query_delete_first}
bool deleteFirstSync() => build().deleteFirstSync();
/// {@macro query_delete_all}
Future<int> deleteAll() => build().deleteAll();
/// {@macro query_delete_all}
int deleteAllSync() => build().deleteAllSync();
/// {@macro query_watch}
Stream<List<R>> watch({bool fireImmediately = false}) =>
build().watch(fireImmediately: fireImmediately);
/// {@macro query_watch_lazy}
Stream<void> watchLazy({bool fireImmediately = false}) =>
build().watchLazy(fireImmediately: fireImmediately);
/// {@macro query_export_json_raw}
Future<T> exportJsonRaw<T>(T Function(Uint8List) callback) =>
build().exportJsonRaw(callback);
/// {@macro query_export_json_raw}
T exportJsonRawSync<T>(T Function(Uint8List) callback) =>
build().exportJsonRawSync(callback);
/// {@macro query_export_json}
Future<List<Map<String, dynamic>>> exportJson() => build().exportJson();
/// {@macro query_export_json}
List<Map<String, dynamic>> exportJsonSync() => build().exportJsonSync();
}
/// Extension for QueryBuilders
extension QueryExecuteAggregation<OBJ, T extends num>
on QueryBuilder<OBJ, T?, QQueryOperations> {
/// {@macro aggregation_min}
Future<T?> min() => build().min();
/// {@macro aggregation_min}
T? minSync() => build().minSync();
/// {@macro aggregation_max}
Future<T?> max() => build().max();
/// {@macro aggregation_max}
T? maxSync() => build().maxSync();
/// {@macro aggregation_average}
Future<double> average() => build().average();
/// {@macro aggregation_average}
double averageSync() => build().averageSync();
/// {@macro aggregation_sum}
Future<T> sum() => build().sum();
/// {@macro aggregation_sum}
T sumSync() => build().sumSync();
}
/// Extension for QueryBuilders
extension QueryExecuteDateAggregation<OBJ>
on QueryBuilder<OBJ, DateTime?, QQueryOperations> {
/// {@macro aggregation_min}
Future<DateTime?> min() => build().min();
/// {@macro aggregation_min}
DateTime? minSync() => build().minSync();
/// {@macro aggregation_max}
Future<DateTime?> max() => build().max();
/// {@macro aggregation_max}
DateTime? maxSync() => build().maxSync();
}

View File

@ -0,0 +1,597 @@
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<FilterOperation> 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;
}

View File

@ -0,0 +1,152 @@
part of isar;
/// This schema represents a collection.
class CollectionSchema<OBJ> extends Schema<OBJ> {
/// @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<String, dynamic> json) {
final collection = Schema<dynamic>.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<dynamic>)
(index as Map<String, dynamic>)['name'] as String:
IndexSchema.fromJson(index),
},
links: {
for (final link in json['links'] as List<dynamic>)
(link as Map<String, dynamic>)['name'] as String:
LinkSchema.fromJson(link),
},
embeddedSchemas: {
for (final schema in json['embeddedSchemas'] as List<dynamic>)
(schema as Map<String, dynamic>)['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<String, IndexSchema> indexes;
/// A map of name -> link pairs
final Map<String, LinkSchema> links;
/// A map of name -> embedded schema pairs
final Map<String, Schema<dynamic>> embeddedSchemas;
/// @nodoc
final GetId<OBJ> getId;
/// @nodoc
final GetLinks<OBJ> getLinks;
/// @nodoc
final Attach<OBJ> attach;
/// @nodoc
final String version;
/// @nodoc
void toCollection(void Function<OBJ>() callback) => callback<OBJ>();
/// @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<String, dynamic> 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<T> = Id Function(T object);
/// @nodoc
@protected
typedef GetLinks<T> = List<IsarLinkBase<dynamic>> Function(T object);
/// @nodoc
@protected
typedef Attach<T> = void Function(IsarCollection<T> col, Id id, T object);

View File

@ -0,0 +1,104 @@
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<String, dynamic> 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<dynamic>)
.map((e) => IndexPropertySchema.fromJson(e as Map<String, dynamic>))
.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<IndexPropertySchema> properties;
/// @nodoc
@protected
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'name': name,
'type': _typeName[type],
'caseSensitive': caseSensitive,
};
}
static const _typeName = {
IndexType.value: 'Value',
IndexType.hash: 'Hash',
IndexType.hashElements: 'HashElements',
};
}

View File

@ -0,0 +1,64 @@
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<String, dynamic> 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<String, dynamic> toJson() {
final json = <String, dynamic>{
'name': name,
'target': target,
'single': single,
};
assert(() {
if (linkName != null) {
json['linkName'] = linkName;
}
return true;
}());
return json;
}
}

View File

@ -0,0 +1,183 @@
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<String, dynamic> json) {
return PropertySchema(
id: -1,
name: json['name'] as String,
type: IsarType.values.firstWhere((e) => e.schemaName == json['type']),
enumMap: json['enumMap'] as Map<String, dynamic>?,
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<String, dynamic>? enumMap;
/// For embedded objects: Name of the target schema
final String? target;
/// @nodoc
@protected
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'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;
}
}
}

126
lib/src/schema/schema.dart Normal file
View File

@ -0,0 +1,126 @@
part of isar;
/// This schema either represents a collection or embedded object.
class Schema<OBJ> {
/// @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<String, dynamic> json) {
return Schema(
id: -1,
name: json['name'] as String,
properties: {
for (final property in json['properties'] as List<dynamic>)
(property as Map<String, dynamic>)['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<String, PropertySchema> properties;
/// @nodoc
@protected
final EstimateSize<OBJ> estimateSize;
/// @nodoc
@protected
final Serialize<OBJ> serialize;
/// @nodoc
@protected
final Deserialize<OBJ> 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<String, dynamic> 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<T> = int Function(
T object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
);
/// @nodoc
@protected
typedef Serialize<T> = void Function(
T object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
);
/// @nodoc
@protected
typedef Deserialize<T> = T Function(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
);
/// @nodoc
@protected
typedef DeserializeProp = dynamic Function(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
);

188
lib/src/web/bindings.dart Normal file
View File

@ -0,0 +1,188 @@
// 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<String> objectKeys(dynamic obj);
Map<String, dynamic> jsMapToDart(Object obj) {
final keys = objectKeys(obj);
final map = <String, dynamic>{};
for (final key in keys) {
map[key] = getProperty<dynamic>(obj, key);
}
return map;
}
@JS('Promise')
class Promise {}
extension PromiseX on Promise {
Future<T> wait<T>() => promiseToFuture(this);
}
@JS('openIsar')
external Promise openIsarJs(
String name,
List<dynamic> 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<dynamic> results);
typedef StopWatchingJs = JsFunction;
@JS('IsarCollection')
class IsarCollectionJs {
external IsarLinkJs getLink(String name);
external Promise getAll(IsarTxnJs txn, List<Id> ids);
external Promise getAllByIndex(
IsarTxnJs txn,
String indexName,
List<List<dynamic>> values,
);
external Promise putAll(IsarTxnJs txn, List<dynamic> objects);
external Promise deleteAll(IsarTxnJs txn, List<Id> ids);
external Promise deleteAllByIndex(
IsarTxnJs txn,
String indexName,
List<dynamic> 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<Id> addedTargets,
List<Id> 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<dynamic> 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);
}

View File

@ -0,0 +1,266 @@
// 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<OBJ> extends IsarCollection<OBJ> {
IsarCollectionImpl({
required this.isar,
required this.native,
required this.schema,
});
@override
final IsarImpl isar;
final IsarCollectionJs native;
@override
final CollectionSchema<OBJ> schema;
@override
String get name => schema.name;
late final _offsets = isar.offsets[OBJ]!;
@tryInline
OBJ deserializeObject(Object object) {
final id = getProperty<int>(object, idName);
final reader = IsarReaderImpl(object);
return schema.deserialize(id, reader, _offsets, isar.offsets);
}
@tryInline
List<OBJ?> deserializeObjects(dynamic objects) {
final list = objects as List;
final results = <OBJ?>[];
for (final object in list) {
results.add(object is Object ? deserializeObject(object) : null);
}
return results;
}
@override
Future<List<OBJ?>> getAll(List<Id> ids) {
return isar.getTxn(false, (IsarTxnJs txn) async {
final objects = await native.getAll(txn, ids).wait<List<Object?>>();
return deserializeObjects(objects);
});
}
@override
Future<List<OBJ?>> getAllByIndex(String indexName, List<IndexKey> keys) {
return isar.getTxn(false, (IsarTxnJs txn) async {
final objects = await native
.getAllByIndex(txn, indexName, keys)
.wait<List<Object?>>();
return deserializeObjects(objects);
});
}
@override
List<OBJ?> getAllSync(List<Id> ids) => unsupportedOnWeb();
@override
List<OBJ?> getAllByIndexSync(String indexName, List<IndexKey> keys) =>
unsupportedOnWeb();
@override
Future<List<Id>> putAll(List<OBJ> objects) {
return putAllByIndex(null, objects);
}
@override
List<int> putAllSync(List<OBJ> objects, {bool saveLinks = true}) =>
unsupportedOnWeb();
@override
Future<List<Id>> putAllByIndex(String? indexName, List<OBJ> objects) {
return isar.getTxn(true, (IsarTxnJs txn) async {
final serialized = <Object>[];
for (final object in objects) {
final jsObj = newObject<Object>();
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<List<dynamic>>();
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<Id>().toList();
});
}
@override
List<Id> putAllByIndexSync(
String indexName,
List<OBJ> objects, {
bool saveLinks = true,
}) =>
unsupportedOnWeb();
@override
Future<int> deleteAll(List<Id> ids) async {
await isar.getTxn(true, (IsarTxnJs txn) {
return native.deleteAll(txn, ids).wait<void>();
});
return ids.length;
}
@override
Future<int> deleteAllByIndex(String indexName, List<IndexKey> keys) {
return isar.getTxn(true, (IsarTxnJs txn) {
return native.deleteAllByIndex(txn, indexName, keys).wait();
});
}
@override
int deleteAllSync(List<Id> ids) => unsupportedOnWeb();
@override
int deleteAllByIndexSync(String indexName, List<IndexKey> keys) =>
unsupportedOnWeb();
@override
Future<void> clear() {
return isar.getTxn(true, (IsarTxnJs txn) {
return native.clear(txn).wait();
});
}
@override
void clearSync() => unsupportedOnWeb();
@override
Future<void> importJson(List<Map<String, dynamic>> json) {
return isar.getTxn(true, (IsarTxnJs txn) async {
await native.putAll(txn, json.map(jsify).toList()).wait<dynamic>();
});
}
@override
Future<void> importJsonRaw(Uint8List jsonBytes) {
final json = jsonDecode(const Utf8Decoder().convert(jsonBytes)) as List;
return importJson(json.cast());
}
@override
void importJsonSync(List<Map<String, dynamic>> json) => unsupportedOnWeb();
@override
void importJsonRawSync(Uint8List jsonBytes) => unsupportedOnWeb();
@override
Future<int> count() => where().count();
@override
int countSync() => unsupportedOnWeb();
@override
Future<int> getSize({
bool includeIndexes = false,
bool includeLinks = false,
}) =>
unsupportedOnWeb();
@override
int getSizeSync({
bool includeIndexes = false,
bool includeLinks = false,
}) =>
unsupportedOnWeb();
@override
Stream<void> watchLazy({bool fireImmediately = false}) {
JsFunction? stop;
final controller = StreamController<void>(
onCancel: () {
stop?.apply([]);
},
);
final void Function() callback = allowInterop(() => controller.add(null));
stop = native.watchLazy(callback);
return controller.stream;
}
@override
Stream<OBJ?> watchObject(
Id id, {
bool fireImmediately = false,
bool deserialize = true,
}) {
JsFunction? stop;
final controller = StreamController<OBJ?>(
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<void> watchObjectLazy(Id id, {bool fireImmediately = false}) =>
watchObject(id, deserialize: false);
@override
Query<T> buildQuery<T>({
List<WhereClause> whereClauses = const [],
bool whereDistinct = false,
Sort whereSort = Sort.asc,
FilterOperation? filter,
List<SortProperty> sortBy = const [],
List<DistinctProperty> distinctBy = const [],
int? offset,
int? limit,
String? property,
}) {
return buildWebQuery(
this,
whereClauses,
whereDistinct,
whereSort,
filter,
sortBy,
distinctBy,
offset,
limit,
property,
);
}
@override
Future<void> verify(List<OBJ> objects) => unsupportedOnWeb();
@override
Future<void> verifyLink(
String linkName,
List<int> sourceIds,
List<int> targetIds,
) =>
unsupportedOnWeb();
}

135
lib/src/web/isar_impl.dart Normal file
View File

@ -0,0 +1,135 @@
// 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 = <Type, List<int>>{};
final List<Future<void>> _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<T> _txn<T>(
bool write,
bool silent,
Future<T> Function() callback,
) async {
requireOpen();
requireNotInTxn();
final completer = Completer<void>();
_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<dynamic>();
} 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<T> txn<T>(Future<T> Function() callback) {
return _txn(false, false, callback);
}
@override
Future<T> writeTxn<T>(Future<T> Function() callback, {bool silent = false}) {
return _txn(true, silent, callback);
}
@override
T txnSync<T>(T Function() callback) => unsupportedOnWeb();
@override
T writeTxnSync<T>(T Function() callback, {bool silent = false}) =>
unsupportedOnWeb();
Future<T> getTxn<T>(bool write, Future<T> 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<int> getSize({
bool includeIndexes = false,
bool includeLinks = false,
}) =>
unsupportedOnWeb();
@override
int getSizeSync({
bool includeIndexes = false,
bool includeLinks = false,
}) =>
unsupportedOnWeb();
@override
Future<void> copyToFile(String targetPath) => unsupportedOnWeb();
@override
Future<bool> close({bool deleteFromDisk = false}) async {
requireOpen();
requireNotInTxn();
await Future.wait(_activeAsyncTxns);
await super.close();
await instance.close(deleteFromDisk).wait<dynamic>();
return true;
}
@override
Future<void> verify() => unsupportedOnWeb();
}

View File

@ -0,0 +1,75 @@
// 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<OBJ> on IsarLinkBaseImpl<OBJ> {
@override
IsarCollectionImpl<dynamic> get sourceCollection =>
super.sourceCollection as IsarCollectionImpl;
@override
IsarCollectionImpl<OBJ> get targetCollection =>
super.targetCollection as IsarCollectionImpl<OBJ>;
@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<void> update({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
}) {
final linkList = link.toList();
final unlinkList = unlink.toList();
final containingId = requireAttached();
final backlink = backlinkLinkName != null;
final linkIds = List<Id>.filled(linkList.length, 0);
for (var i = 0; i < linkList.length; i++) {
linkIds[i] = requireGetId(linkList[i]);
}
final unlinkIds = List<Id>.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<dynamic>();
}
return jsLink
.update(txn, backlink, containingId, linkIds, unlinkIds)
.wait();
});
}
@override
void updateSync({
Iterable<OBJ> link = const [],
Iterable<OBJ> unlink = const [],
bool reset = false,
}) =>
unsupportedOnWeb();
}
class IsarLinkImpl<OBJ> extends IsarLinkCommon<OBJ>
with IsarLinkBaseMixin<OBJ> {}
class IsarLinksImpl<OBJ> extends IsarLinksCommon<OBJ>
with IsarLinkBaseMixin<OBJ> {}

View File

@ -0,0 +1,347 @@
// 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<dynamic>(object, offset);
return value == 1;
}
@tryInline
@override
bool? readBoolOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value == 0
? false
: value == 1
? true
: null;
}
@tryInline
@override
int readByte(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int ? value : nullNumber as int;
}
@tryInline
@override
int? readByteOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int && value != nullNumber ? value : null;
}
@tryInline
@override
int readInt(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int ? value : nullNumber as int;
}
@tryInline
@override
int? readIntOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int && value != nullNumber ? value : null;
}
@tryInline
@override
double readFloat(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is double ? value : nullNumber;
}
@tryInline
@override
double? readFloatOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is double && value != nullNumber ? value : null;
}
@tryInline
@override
int readLong(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int ? value : nullNumber as int;
}
@tryInline
@override
int? readLongOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int && value != nullNumber ? value : null;
}
@tryInline
@override
double readDouble(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is double && value != nullNumber ? value : nullNumber;
}
@tryInline
@override
double? readDoubleOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is double && value != nullNumber ? value : null;
}
@tryInline
@override
DateTime readDateTime(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int && value != nullNumber
? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal()
: nullDate;
}
@tryInline
@override
DateTime? readDateTimeOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is int && value != nullNumber
? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal()
: null;
}
@tryInline
@override
String readString(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is String ? value : '';
}
@tryInline
@override
String? readStringOrNull(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is String ? value : null;
}
@tryInline
@override
T? readObjectOrNull<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
) {
final value = getProperty<dynamic>(object, offset);
if (value is Object) {
final reader = IsarReaderImpl(value);
return deserialize(0, reader, allOffsets[T]!, allOffsets);
} else {
return null;
}
}
@tryInline
@override
List<bool>? readBoolList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List ? value.map((e) => e == 1).toList() : null;
}
@tryInline
@override
List<bool?>? readBoolOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value
.map(
(e) => e == 0
? false
: e == 1
? true
: null,
)
.toList()
: null;
}
@tryInline
@override
List<int>? readByteList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is int ? e : nullNumber as int).toList()
: null;
}
@tryInline
@override
List<int>? readIntList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is int ? e : nullNumber as int).toList()
: null;
}
@tryInline
@override
List<int?>? readIntOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is int && e != nullNumber ? e : null).toList()
: null;
}
@tryInline
@override
List<double>? readFloatList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is double ? e : nullNumber).toList()
: null;
}
@tryInline
@override
List<double?>? readFloatOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is double && e != nullNumber ? e : null).toList()
: null;
}
@tryInline
@override
List<int>? readLongList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is int ? e : nullNumber as int).toList()
: null;
}
@tryInline
@override
List<int?>? readLongOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is int && e != nullNumber ? e : null).toList()
: null;
}
@tryInline
@override
List<double>? readDoubleList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is double ? e : nullNumber).toList()
: null;
}
@tryInline
@override
List<double?>? readDoubleOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is double && e != nullNumber ? e : null).toList()
: null;
}
@tryInline
@override
List<DateTime>? readDateTimeList(int offset) {
final value = getProperty<dynamic>(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<DateTime?>? readDateTimeOrNullList(int offset) {
final value = getProperty<dynamic>(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<String>? readStringList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is String ? e : '').toList()
: null;
}
@tryInline
@override
List<String?>? readStringOrNullList(int offset) {
final value = getProperty<dynamic>(object, offset);
return value is List
? value.map((e) => e is String ? e : null).toList()
: null;
}
@tryInline
@override
List<T>? readObjectList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
T defaultValue,
) {
final value = getProperty<dynamic>(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<T?>? readObjectOrNullList<T>(
int offset,
Deserialize<T> deserialize,
Map<Type, List<int>> allOffsets,
) {
final value = getProperty<dynamic>(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;
}
}

48
lib/src/web/isar_web.dart Normal file
View File

@ -0,0 +1,48 @@
// 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<void> initializeCoreBinary({
Map<IsarAbi, String> libraries = const {},
bool download = false,
}) =>
unsupportedOnWeb();

View File

@ -0,0 +1,171 @@
// 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<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> serialize,
T? value,
) {
if (value != null) {
final object = newObject<Object>();
final writer = IsarWriterImpl(object);
serialize(value, writer, allOffsets[T]!, allOffsets);
setProperty(this.object, offset, object);
}
}
@tryInline
@override
void writeByteList(int offset, List<int>? values) {
setProperty(object, offset, values ?? nullNumber);
}
@tryInline
@override
void writeBoolList(int offset, List<bool?>? 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<int?>? values) {
final list = values?.map((e) => e ?? nullNumber).toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeFloatList(int offset, List<double?>? values) {
final list = values?.map((e) => e ?? nullNumber).toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeLongList(int offset, List<int?>? values) {
final list = values?.map((e) => e ?? nullNumber).toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeDoubleList(int offset, List<double?>? values) {
final list = values?.map((e) => e ?? nullNumber).toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeDateTimeList(int offset, List<DateTime?>? values) {
final list = values
?.map((e) => e?.toUtc().millisecondsSinceEpoch ?? nullNumber)
.toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeStringList(int offset, List<String?>? values) {
final list = values?.map((e) => e ?? nullNumber).toList();
setProperty(object, offset, list ?? nullNumber);
}
@tryInline
@override
void writeObjectList<T>(
int offset,
Map<Type, List<int>> allOffsets,
Serialize<T> serialize,
List<T?>? values,
) {
if (values != null) {
final list = values.map((e) {
if (e != null) {
final object = newObject<Object>();
final writer = IsarWriterImpl(object);
serialize(e, writer, allOffsets[T]!, allOffsets);
return object;
}
}).toList();
setProperty(object, offset, list);
}
}
}

82
lib/src/web/open.dart Normal file
View File

@ -0,0 +1,82 @@
// 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<void> 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<Isar> openIsar({
required List<CollectionSchema<dynamic>> 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<dynamic>;
final instance = await openIsarJs(name, schemasJs, relaxedDurability)
.wait<IsarInstanceJs>();
final isar = IsarImpl(name, instance);
final cols = <Type, IsarCollection<dynamic>>{};
for (final schema in schemas) {
final col = instance.getCollection(schema.name);
schema.toCollection(<OBJ>() {
schema as CollectionSchema<OBJ>;
cols[OBJ] = IsarCollectionImpl<OBJ>(
isar: isar,
native: col,
schema: schema,
);
});
}
isar.attachCollections(cols);
return isar;*/
}
Isar openIsarSync({
required List<CollectionSchema<dynamic>> schemas,
String? directory,
required String name,
required int maxSizeMiB,
required bool relaxedDurability,
CompactCondition? compactOnLaunch,
}) =>
unsupportedOnWeb();

View File

@ -0,0 +1,375 @@
// 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<T> buildWebQuery<T, OBJ>(
IsarCollectionImpl<OBJ> col,
List<WhereClause> whereClauses,
bool whereDistinct,
Sort whereSort,
FilterOperation? filter,
List<SortProperty> sortBy,
List<DistinctProperty> 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<T> deserialize;
//if (property == null) {
deserialize = col.deserializeObject as T Function(Object);
/*} else {
deserialize = (jsObj) => col.schema.deserializeProp(jsObj, property) as T;
}*/
return QueryImpl<T>(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<dynamic> 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<dynamic> 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<dynamic> schema,
FilterOperation filter,
) {
final filterStr = _buildFilterOperation(schema, filter);
if (filterStr != null) {
return FilterJs('id', 'obj', 'return $filterStr');
} else {
return null;
}
}
String? _buildFilterOperation(
CollectionSchema<dynamic> 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<dynamic> 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<dynamic> 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<SortProperty> 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<DistinctProperty> 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');
}

180
lib/src/web/query_impl.dart Normal file
View File

@ -0,0 +1,180 @@
// 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> = T Function(Object);
class QueryImpl<T> extends Query<T> {
QueryImpl(this.col, this.queryJs, this.deserialize, this.propertyName);
final IsarCollectionImpl<dynamic> col;
final QueryJs queryJs;
final QueryDeserialize<T> deserialize;
final String? propertyName;
@override
Isar get isar => col.isar;
@override
Future<T?> findFirst() {
return col.isar.getTxn(false, (IsarTxnJs txn) async {
final result = await queryJs.findFirst(txn).wait<Object?>();
if (result == null) {
return null;
}
return deserialize(result);
});
}
@override
T? findFirstSync() => unsupportedOnWeb();
@override
Future<List<T>> findAll() {
return col.isar.getTxn(false, (IsarTxnJs txn) async {
final result = await queryJs.findAll(txn).wait<List<dynamic>>();
return result.map((e) => deserialize(e as Object)).toList();
});
}
@override
List<T> findAllSync() => unsupportedOnWeb();
@override
Future<R?> aggregate<R>(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<R>(AggregationOp op) => unsupportedOnWeb();
@override
Future<bool> deleteFirst() {
return col.isar.getTxn(true, (IsarTxnJs txn) {
return queryJs.deleteFirst(txn).wait();
});
}
@override
bool deleteFirstSync() => unsupportedOnWeb();
@override
Future<int> deleteAll() {
return col.isar.getTxn(true, (IsarTxnJs txn) {
return queryJs.deleteAll(txn).wait();
});
}
@override
int deleteAllSync() => unsupportedOnWeb();
@override
Stream<List<T>> watch({bool fireImmediately = false}) {
JsFunction? stop;
final controller = StreamController<List<T>>(
onCancel: () {
stop?.apply([]);
},
);
if (fireImmediately) {
findAll().then(controller.add);
}
final Null Function(List<dynamic> results) callback =
allowInterop((List<dynamic> results) {
controller.add(results.map((e) => deserialize(e as Object)).toList());
});
stop = col.native.watchQuery(queryJs, callback);
return controller.stream;
}
@override
Stream<void> watchLazy({bool fireImmediately = false}) {
JsFunction? stop;
final controller = StreamController<void>(
onCancel: () {
stop?.apply([]);
},
);
final Null Function() callback = allowInterop(() {
controller.add(null);
});
stop = col.native.watchQueryLazy(queryJs, callback);
return controller.stream;
}
@override
Future<R> exportJsonRaw<R>(R Function(Uint8List) callback) async {
return col.isar.getTxn(false, (IsarTxnJs txn) async {
final result = await queryJs.findAll(txn).wait<dynamic>();
final jsonStr = stringify(result);
return callback(const Utf8Encoder().convert(jsonStr));
});
}
@override
Future<List<Map<String, dynamic>>> exportJson() {
return col.isar.getTxn(false, (IsarTxnJs txn) async {
final result = await queryJs.findAll(txn).wait<List<dynamic>>();
return result.map((e) => jsMapToDart(e as Object)).toList();
});
}
@override
R exportJsonRawSync<R>(R Function(Uint8List) callback) => unsupportedOnWeb();
@override
List<Map<String, dynamic>> exportJsonSync({bool primitiveNull = true}) =>
unsupportedOnWeb();
}

View File

@ -0,0 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'package:isar/src/web/isar_web.dart';
List<String> isarSplitWords(String input) => unsupportedOnWeb();

22
pubspec.yaml Normal file
View File

@ -0,0 +1,22 @@
name: isar
description: Extremely fast, easy to use, and fully async NoSQL database for Flutter.
version: 3.1.0+1
repository: https://github.com/isar/isar/tree/main/packages/isar
homepage: https://github.com/isar/isar
issue_tracker: https://github.com/isar/isar/issues
documentation: https://isar.dev
funding:
- https://github.com/sponsors/leisim/
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
ffi: ">=2.0.0 <3.0.0"
js: ^0.6.4
meta: ^1.7.0
dev_dependencies:
ffigen: ">=6.1.2 <8.0.0"
test: ^1.21.1
very_good_analysis: ^3.0.1

View File

@ -0,0 +1,287 @@
@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<dynamic>)
.map((e) => BinaryTest.fromJson(e as Map<String, dynamic>))
.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<int>(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<String, dynamic> 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<Type> types;
final List<dynamic> values;
final List<int> 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<bool?>? _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<int>? _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<int?>? _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<double?>? _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<int?>? _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<double?>? _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<String?>? _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());
}

6
tool/get_version.dart Normal file
View File

@ -0,0 +1,6 @@
import 'package:isar/isar.dart';
void main() {
// ignore: avoid_print
print(Isar.version);
}

View File

@ -0,0 +1,9 @@
import 'package:isar/isar.dart';
void main(List<String> args) {
if (Isar.version != args[0]) {
throw StateError(
'Invalid Isar version for release: ${Isar.version} != ${args[0]}',
);
}
}